非保姆级二进制炸弹破解分析

本文采用静态分析的方法,即通过阅读反汇编代码理清程序运行逻辑,通过写脚本得到密码。与之相对地也可以采用动态调试,即在 ida 里面设断点,通过修改寄存器信息等绕过一些爆炸点,类似直接在内存里看答案。我不会把整个破解过程分析一遍,只重点分析一些宏观的思路以及容易卡住的点,最后分析了下彩蛋的不同解法。保姆级教程见 Blu3m00n’s Blog

如果没有接触过 IDA PRO,建议逆二进制炸弹之前先写个 a+b 和 hellow world 拉进去看一下,了解一下高级语言反汇编出来是什么样、转成的伪代码又是什么样。

从本人为数不多的逆向和 pwn 的经历中总结的入门经验,仅供参考:

  • 读伪代码先快速过一遍,连蒙带猜先猜一些关键变量的作用,同时判断哪些变量是没有用的,做好标记。
  • 细读代码的时候也得猜,猜主要逻辑,能加快速度。很多复杂代码的逻辑很清楚,干的事很简单。
  • F5 出来的伪代码不是百分百可靠的,尤其是很多跳转会出问题。遇到疑问一定要回去看汇编!
  • 生成的伪代码很喜欢用指针,刚开始各种转换反应不过来也问题不大,分辨好数据类型领会精神即可。

重点分析

一个细节,关键函数 GenerateNumber 里面的 HIDWORD 应该是 ida 自己定义的宏,通过百度之类的手段不难猜出其含义。

phase_1 不说了,phase_2 里面看伪代码发现只有当 rand_div == 0 的时候才不会炸,会进入 phase_2_0,里面没有再调用别的函数了:
在这里插入图片描述
但观察左栏函数名发现还有一系列的 phase_2_n。于是回去看汇编:
在这里插入图片描述
发现前面把 rand_div 放到 eax 中,然后后面实际上是 jmp [eax*4 + 0x40D470],也就是根据 rand_div 的不同取值进入不同函数。发现这一点之后就根据你的 rand_div 值破解对应函数即可。

phase_3 里面,以我的 rand_div=12 为例,进入 phase_3_12,得到正确的 v4(第一个参数),之后我进入 case 36,这里面只对 v3(第三个参数) 的值进行了检查,得到了 v1 但似乎没有检查就直接返回了,也就是我们得不到确定的第二个参数。发现如果第二个参数随便设置并不能够通关,于是尝试将其设置为 v1,竟然过了。

究其原因,再回去看汇编:
在这里插入图片描述
这两行对应检查 v3 是否等于 rand_div,如果等于会跳转到 0x403002:
在这里插入图片描述
原来还藏着一个可能引爆炸弹的判断。不过这里的 var_1 让人摸不着头脑,但猜想应该是 phase_3_12 里面的 v5(第二个参数)。查看栈空间,或者右键重命名以后这俩名字就一样了。

phase_4 也没什么好说的。

第五关难度突然变得不是一个水平的了。进入 phase_impossible,首先把 tohex 读完,发现是把合法的字符串(仅由 “0123456789abcdefABCDEF” 组成)转化为十六进制存在 v2 中。之后经由 check_buf_valid 检查是否合法,再往后面…咋没有出口?感觉最后肯定会进入某个 goto_buf_n,而进入后发现传的参数非常难以理解。遇到困难回去看汇编:
在这里插入图片描述
发现这三个 goto_buf_n 都是把 eip 设置到 eax 处往后执行:
在这里插入图片描述
往前看一点,这时候的 eax 里都是 v2 的地址,也就是说程序会跳到我们写入栈中的一段机器码的首地址开始执行,因此我们要像怎么在这段代码上做手脚。

通过 Shift+F12 查找字符串找到第五关通关的位置:
在这里插入图片描述
那么过关思路就清晰了:我们要向栈中写入一段机器码,作用是这段代码执行完后让 eip 跳到这个位置,输出过关信息。

可能会想到直接用 jmp 指令,因为代码段的加载不是随机的。但这样会带来一系列问题,因为这段代码运行完以后可能会导致栈不平衡,进而出现程序异常退出等情况。

扫描二维码关注公众号,回复: 17284573 查看本文章

注意到我们想要调到的位置恰好是调用 phase_impossible 后面,也就是我们写入的代码只要能够实现退出 phase_impossible 即可。所以我们要写入的代码长这样:

mov esp, ebp
pop ebp		; 先把栈恢复到之前的状态
ret

然后绕过 check_buf_valid 就可以得到密码了。

此外,发现函数栏还有一个叫做 phase_secret 的函数,这是一个独立的只有输出的函数,可见我们的目标就是修改第五关写入的信息,使得这个函数被调用:
在这里插入图片描述

这题解法不止一种。总体思路是我们要把栈打造成我们想要的样子,即在合适的位置放上合适的数据(返回地址等),使得通过程序中自带的 ret 或我们写入的 jmp, ret 等指令的执行来实现跳转。

具体来说,我们想先跳到输出第五关通关信息的那段代码的地址(0x401240),执行到 0x40125e 的时候栈顶应该存放 phase_secret 的地址(0x4014f0),执行到 0x4014fb 的时候栈顶应该存放 0x40125b。所以要写入的代码长这样:

mov esp, ebp
pop ebp
push 0x40125B
push 0x4014F0
push ebp
mov ebp, esp
push 1
push 2
push 3
push 0x401240
ret

不明白的话照着这部分汇编手玩一下就很清楚了,注意观察栈中数据以及 esp、ebp 的变化。蓝月亮在前面多 pop 了两个元素为了让 ebp、esp 位置和原来一样,实际上在这道题里面我认为是没有必要的。

在上面这种方法中,eip 从栈里跳出去之后就再也回不来了,因为我们并不知道栈地址是什么,即控制流程是:stack -> code1 -> code2 -> code3 -> …。如果我们得到了栈地址,就可以在栈里 push 相应的地址使得 ret 的时候多次返回栈里。经测试,本程序没开启栈地址随机,可以通过动态调试直接读出 v2 的地址。

当然也可以利用 call 指令直接把 eip 暂存,同样达到返回栈里执行的效果。而 call 后面接的是偏移地址(我没找到接绝对地址的指令),同样需要动态调试得到 v2 的地址,进而计算其与目标代码段的偏移量。

如果开启了栈地址随机,下面两种方法都是不可取的,第一种方法仍然可取。但如果可以 call 绝对地址的话那么自然也可以用第三种方法。

实际上开启了栈地址随机也是可以泄露栈地址的。这个程序没开 NX 保护,而且直接允许写入长达 256 字节的代码,我们的自主性其实是很大的。完全可以通过写入 call; pop 得到栈地址,然后 push 进栈里供后面 ret,或者直接调用 printf 函数把栈地址输出出来,之后再跳到第五关读密码那里再执行一遍,这次输入就可以利用刚才泄露出来的栈地址了。

最后泄露栈地址这段知识理论上的分析,没有经过尝试,等忙完这一阵再来补吧。

猜你喜欢

转载自blog.csdn.net/DT_Kang/article/details/123325720