2020 “第五空间”智能安全大赛初赛两道 RE 解题报告(writeup)

Posted by HX on 2020-06-25 | 👓

0x0. nop-127pt

这题主要考察的是 Linux 下的反调试技巧。

先运行观察下行为,提示输入 flag,随便输入后程序退出。

拖进 IDA 看代码,main 函数一上来进入 sub_804865B,这是第一处反调试。

Linux 下,程序无调试器运行时,环境变量_会是程序自身路径,有调试器时,这个环境变量会被调试器改变,可以据此区分程序是否被调试。

回到 main,把下图两条指令 nop 掉,去掉这个反调试。

接着,程序提示输入 flag,将其作为整数存入全局变量 dword_804A038。在块 loc_80486E7loc_80486F5 中,将其值加 1 并存回。

loc_80486F5 中调用函数 sub_804857B,这是第二处反调试。函数中,首先布置好系统调用所需的参数和调用号。

然后跳到 loc_80485A0 进行系统调用。

查表知 Linux x86 的系统调用号 0x20 是 ptrace,这个系统调用以 C 语言写出相当于 ptrace(0,0,1,0); 这是个典型的反调试。在上图中,根据 ptrace 的返回值决定是否进入错误的分支。将 jl short loc_80485AE 这条指令 nop 掉,去掉这个反调试。

回到 main,在块 loc_804870Dloc_8048701 中,再次将全局变量 dword_804A038 加 1 并存回。接着调用 sub_80485C4,在此函数中布置系统调用号 0x14,然后跳到 loc_80485F8 进行系统调用,相当于 C 语言 getpid();,作用是获取当前进程 PID。

同时 loc_80485F8 还布置了下一个系统调用号 0x40,接着,跳到 loc_804860B 进行系统调用,相当于 C 语言 getppid();,作用是获取父进程 PID。

同时 loc_804860B 还布置了下一个系统调用号 0x93,接着,跳到 loc_804861E 进行系统调用,参数是刚才获取的当前进程 PID,相当于 C 语言 getsid(getpid());,作用是获取当前进程 SID。

上图将 getsid(getpid())getppid() 比较,不相等则进入错误分支。将 jnz short loc_8048634 这条指令 nop 掉,去掉这个反调试。

回到 main,在 loc_8048727loc_804871B 将全局变量 dword_804A038 加上 0xCCCCCCCC 并存回。接着调用 sub_80485C4,即刚才的第二处反调试,因为已经处理,所以不管它,接着往下看。

loc_804873B 中将全局变量 dword_804A038 加 1 并存回,调用 sub_8048753。此函数中又调用 sub_8048691,进入查看。

sub_8048691 的功能是把 eax 所指向的内存地址存储的字节替换为 0x90(即 nop 指令)。前面操作全局变量 dword_804A038 时以 eax 为中介,所以 eax 里现在存的就是 dword_804A038 的值。但是这个函数有什么意义呢?现在还不清楚,先接着往后看。

回到 sub_8048753,将 eax 加 1 后再次调用 sub_8048691。所以,这一块代码应该是把内存某个位置连续的两个字节 nop 掉了。

接着,程序直接跳入了错误分支,与此同时 IDA 中可以看到正确分支就在附近,且如果不经过 0x8048765 处的 jmp,就可以进入正确分支。

这时就明白了上面 sub_8048691 的功能,它给了我们 nop 掉 0x8048765 的 jmp 的能力,这个 jmp 正好占两字节。

到此,程序就分析完了。流程整理如下(不考虑反调试):

  1. 输入 flag,为一个整数

  2. 将 flag+1

  3. 将 flag+1

  4. 将 flag+0xCCCCCCCC

  5. 将 flag+1

  6. 将 flag 所指向的内存地址连续的两字节 nop

我们希望把 0x8048765 的 jmp 替换为 nop,所以可以列出方程:

1
flag+0xCCCCCCCF==0x8048765

注意是对 32 位整数运算,所以结果要对 0x1 0000 0000 取模。解得 flag 为 0x3B37BA96 ,即十进制 993507990。

最后按 flag 格式,提交 \[ flag\{993507990\} \]

0x1. 逆向-rev-232pt

涉及指令 gadget 的题,本来这种技术在 pwn 里很常见,现在搬到了 re 里…

先看 main,很简单,要求两个参数,然后 sub_400D19 验证第二个参数,返回值为 1 则正确。

重点看 sub_400D19,只有三条指令,将变量 data 的地址传送给 rsp,然后返回到这个地址。

显然 data 是代码了,进去看。

这里有许多指针,每个都指向一个 gadget,依次执行这些 gadget 进行运算,最后产生结果。

因为指令被打散成 gadget,分析起来很麻烦,再加上这种进行一堆运算,最后根据返回值进入正确或错误分支的题一般可以尝试下走捷径,所以先偷懒试下符号执行。

IDA 里看到正确分支在 0x400481,编写 angr 脚本如下。之所以知道 flag 是 21 位,是因为换题目之前的旧题里透露了 flag 位数,定长一点也行,angr 一样能解出来。

运行,等几秒就得到 flag 了。

0x2. 总结

太菜了,twice 那题栈溢出都没做出来… 另外等一个 rev 正经解法的 writeup。