终于开始缓慢复现强pwn杯了
easyre
这题我真的是气死了啊啊啊啊啊啊浪费我一天时间!!!!!!!
初步分析
先finger一把梭了恢复符号表。
主函数:
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
sub_402150写了一个叫re3的文件,然后fork一个线程。父进程进入sub_401F2F运行re3,结束后remove掉re3,子进程接受父进程的ptrace
sub_401F2F:
1 | __int64 __fastcall sub_401F2F(unsigned int a1) |
sub_4017E5取了一堆抽象的数据。起子进程后等待子进程运行,wait子进程发送信号,为0就退出for循环。不为0的话就保存子进程的寄存器,然后去判断if-else(就是一个子进程命中异常后把控制权交给父进程,父进程保存子进程上下文,处理完异常后恢复子进程上下文,此后继续运行子进程的过程)。
依据从RIP-1处取到的数据来判断进入if还是else,if会对子进程进行一个SMC的操作,else则会将子进程SMC回去。
其实findcrypto可以找到MD5常数,然后推测SMC使用了对前面取的数据的MD5值来异或。但是这样毕竟太玄学了,想要恢复re3还是应该采取一些正常人能想到的方法。
re3分析
获取re3很简单,手动dumpunk_4BC380
处的数据或者把remove扬了然后跑一遍程序就能获取到re3。
对re3分析主函数(分析不出来的函数一路C然后P就彳亍):
1 | __int64 __fastcall main(int a1, char **a2, char **a3) |
sub_2490接受输入,控制输入只能为0或1,21F9进行了一些处理,但由于此函数被SMC了所以我们不知道它干了什么,然后把savedregs(做了一些混淆)和byte_50A0处的数据对比,能看出来数据是两组25*25的表。想要看到验证逻辑还是要恢复21F9处的逻辑。
恢复re3
对于SMC,要么是静态分析自己写脚本恢复,要么是动态调试去解密程序。但是问题在于,子进程本身就处于一个被父进程调试的过程,所以我们没法再向子进程附加调试器。而当父进程结束时,子进程也会结束。不过正是因为父子进程的通信过程,我们又会多一种思路。对于本题而言,恢复re3有以下4种思路。
通过patch程序让子进程成为僵尸进程
思路来自FallW1nd师傅,直接看原文8 我就不抄一遍了
整体的patch思路:
- 让父进程不必等待子进程结束就可以自行结束
- 扬了父进程中对子进程SMC回去的操作
- 让子进程陷入死循环无法结束
但是我WSL调试根本起不来子进程,怎么会是捏?
准备之后配个ubuntu虚拟机了🤗fork调的血压高
硬刚加密函数
findcrypto能看到MD5的常数,可以猜测是用了MD5。
可能调试看的更快吧,但是我起不来子进程,看看PZ师傅的WP
通过trace程序获取真实数据
思路来自track神(track神TTTTQQQQLLLL!!!
本题父进程对子进程的操作都需要从子进程读数据、在父进程中修改、写回子进程这3个过程。其中第1、3步会用到ptrace这个系统调用。
而对于系统调用,可以使用trace来leak出程序运行过程中的各种参数。
使用命令strace -e trace=ptrace -o ./easyre.log ./easyre flag{hhhhhhhhhh}
,然后去看ptrace的log,只要把这些参数写回就好了。
1 | ptrace(PTRACE_POKETEXT, 149, 0x561dac238213, 0xe900000000fc45c7) = 0 |
PTRACE_POKETEXT
是向子进程写回数据,取这个数据就好。
IDAPython脚本:
1 | key = [0xe900000000fc45c7,0xf445c7000000,0x1ec45c700,0x8dd00102e0c1d089,0xd06348d001f8458b,0x45830d74c08400b6,0x7400f07d8347eb00,0x14802e0c148d089,0xc0458b48c2014800,0x20c889848ec458b,0xf045c701ec45,0xf07d83407519f8,0x4802e0c148d08948,0x458b48c201480000,0xc889848ec458bc1,0x18f87d8301ec4583,0x6348fc458bc189ec,0x85148d48d00148,0xff518dd00148c045,0xfffffed48e0f18fc,0xe445c700000122,0xdc45c70000,0xe4558b000000c7e9,0xc201000000008514,0xfd00148c8458b48,0x1dc45c701e0,0x48d06348e8458b3a,0x85148d48d0,0xc189e0458bc20148,0x8300000000e045c7,0x7d8301e445830000,0xd06348e8458b3a74,0x85148d48d001,0x89e0458bc20148b8,0xe045c702,0x458bffffff2f8e0f,0x2e0c148d08948d0,0x8b48c20148000000,0x7d8301e845831088] |
patch完慢慢分析伪代码就彳亍。
然后就是注意一下,日常翻翻init段 = =
里面调用了一个函数sub_257D,里面对验证数据做了一些修改:
1 | __int64 sub_257D() |
在unk_3020附近有unk_32A0,交叉引用可以看到用它对0x50A0做了修改:
1 | __int64 __fastcall sigabbrev_np(__int64 a1, __int64 a2, __int64 a3, __int64 a4) |
正好和主函数里进行验证的数据对上了,所以这两组数据才是真实数据。分析出来是数织游戏,去在线网站解一下就好。
GameMaster
双击游戏,拿20块赢到3700w
对gamemessage的两处解密:
1 | //1 |
python解密得到二进制文件,找PE头,前面的全部删掉。
得到解密后的文件拖进dnspy读代码,z3解方程照抄逻辑即可。
1 | from z3 import * |
1 | xor_key = [60,100,36,86,51,251,167,108,116,245,207,223,40,103,34,62,22,251,227] |
deeprev
题目思路跟googleCTF的eldar一样的,链接是我写的翻译,但是建议读原文。
运行一下,然后去so库里面搜flag,可以搜到一个出题人写好的flag,提示我们应该patch这里。当我们把flag patch成正确的flag,这个程序就会提示win。
用IDA打开deeprev可以看到巨大无比的重定位表,并且这个表是可读可写可执行的。
注意一堆0xc3,这是ret的机器码,而0xc3类型都是REL,这就是r.r_offset = r.r_addend
(见eldar那篇wp)。
找一个连着REL类型的0xc3的机器码反编译一下
直接双击stru_8040C4
进去按U看看内部结构
注意IDA已经注释出来,0x8040cc地址处的数据是Copy of shared data
,也就是从so文件里copy过来的flag值,所以这里肯定跟flag关系密切。
如果不嫌麻烦愿意直接调试硬啃的话就可以直接在这里下内存断点然后跑程序了,调试就可以发现它不断地copy so库里的flag到这个地址,然后经过一个异或和加的运算,再把密文存到另一处(0x8042BA处),然后再不断copy此处存的密文到0x8040FC处,最后再对这个地址异或一个值。
我们当然可以慢慢调试去一个一个记录这些值,不过这样还是 太印度人了 太麻烦了,所以可以思考选择一些工具。
首先经过调试可以发现,关键代码都在ret前面(也就是0xc3),那么就可以以这一条为依据来筛选我们需要的r.r_addend
值,然后输出这些值的反汇编代码。
可以先readelf -r deeprev > rel.txt
大概看一下重定位表,只有1.2k+行是有意义的,后面都是0,所以写代码的时候也不用遍历整个重定位表,这样会很慢,遍历到1300的时候就可以了。
1 | import lief |
打印出来的值可以直接反汇编,我这里手动去掉了一些0。
1 | from capstone import * |
输出:
1 | xor byte ptr [0x8040cc], 0x16 |
提取值手搓解密即可。
1 | xor1 = [0x16, 0x17, 0x10, 0x12, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x24, 0x2c, 0x26, 0x1e, 0x1f, 0x20, 0x20, 0x21, 0x23, 0x27, 0x24, 0x25, 0x26, 0x27] |
注意上面输出的最后4句汇编不再是对0x8040fc处的异或,而是在0x80415c处进行异或,所以后4位flag显然是换了验证方式。
对0x80415c下硬件断点可以发现,其逻辑就是两个一组的二元方程,自己解或者z3解均可……
不过比赛的时候给了flag的SHA256值,所以也可以直接爆破。
当然了,毕竟flag只剩最后4位(实际上是3位,因为最后一位一定是}
),再去看这个逻辑属实有点麻烦。所以对于我这么一个爆破走天下的人来说当然还是直接爆破方便一些。
1 | import subprocess |
flag{366c950370fec47e34581a0574}
下面是逆向分享会的文档
前置知识
ELF动态重定位条目的结构:
1 | typedef struct { |
info包括一个type和symble(可选)索引:
1 |
动态符号表条目的结构:
1 | typedef struct { |
本题使用了以下重定位信息:
REL(type 8)
将重定位信息设置为一个相对基址的值(但是基址始终为0)
1 | *(uint64_t *)r.r_offset = r.r_addend; |
立即数寻址:mov *0xbeef0000, $0x04
写成重定位条目就是{type=RELATIVE, offset=0xbeef0000,symbol=0,addend=0x04}
在IDA中显示为Elf64_Rela <beef0000h, 8, 4h>
CPY(type 5)
从符号地址复制n字节到重定位地址
1 | Elf64_Sym sym = symbols[ELF64_R_SYM(r.r_info)]; |
立即数寻址:mov *0xbeef0000, [%foo]
{type=COPY, offset=0xbeef0000,symbol=foo,addend=0}
Elf64_Rela <beef0000h, ?00000005, 0>
复制foo指向的n个字节到0xbeef0000,n是foo的st_size。
R64(type 1)
将重定位信息设置为 符号值+常数
1 | *(uint64_t *)r.r_offset = symbols[ELF64_R_SYM(r.r_info)] + r.r_addend; |
怎么做?
调试
当我们对这些数据具体如何填充并不清楚的时候,最好的办法当然是让链接器来帮我们做这些事情。我们只需要下断点调试,去找数据的加密逻辑就可以了。
对于这个题而言,调试就可以发现它不断地copy so库里的flag到这个地址,然后经过一个异或和加的运算,再把密文存到另一处(0x8042BA处),然后再不断copy此处存的密文到0x8040FC处,最后再对这个地址异或一个值。
不过其实可以发现,这个题虽然对单个字符串的加密数据不同,但大部分代码是复用的。一个典型的表现就是每次复制数据、异或、加之后,都会有一个ret。所以我们可以认为ret前就是用于加密的关键代码,其他地方是一些对逆向价值不高的代码。
那么就可以写一个脚本来解析这些数据:
1 | import lief |
这个脚本输出了所有REL类型的重定位信息的addend值,其中可能会有一些值不是我们需要的,但是不用管。
后续反编译的时候,capstone遇到无法反编译的数据就会直接跳过。
然后写一个脚本,筛选ret之前的值反汇编:
1 | from capstone import * |
自己写一个解析脚本
顺便一提:eldar这个题是真正实现了使用元数据来隐藏代码逻辑的。
deeprev这个题更像是直接把机器码放在了重定位表里,所以上面那种偷懒的解析方法(直接dump重定位表的数据来反编译)才能奏效。
eldar里的重定位表是没有depprev那样的长数据机器码的,所以基本只能硬调,或者像上面这个大佬一样自己写脚本解析。
关于本文
本文作者 云之君, 许可由 CC BY-NC 4.0.