如何快速解决反汇编问题
对于 DCU 安全编程这门课的反汇编,由于题目有一定套路,使用固定的解题思路,可以快速解题
前置技能
- 熟悉各种汇编命令 汇编指令
- 熟悉 % 和 $ 表示寄存器和立即数 $ 与 % 寄存器与立即数
- 熟悉直接寻址和间接寻址 直接寻址与间接寻址
- 了解一个case C语言代码到汇编的例子
解题思路
找到入参个数
ebp的位置是saved frame ptr,ebp+4的位置是return address。由于题目往往是约定入参全部为int 或者 int * 类型,因此 ebp+8,ebp+c,ebp+10的位置,分别是入参的第一个参数,第二个参数,第三个参数。
因此,在代码中快速浏览,寻找 0x__(%ebp) 的字样,找到最大的偏移量。(偏移量-4)//4
的值就是入参的个数。
例如,下面这段代码
1 | push %ebp <foo+0> |
我们发现存在 0x10(%ebp)
因此,参数个数为(16-4)/4
,也就是是3个。
因此我们可以写出代码的框架,如下
1 | int foo(int a, int b, int c) { |
其中 a, b, c 存在的栈的位置分别是 ebp+8,ebp+c,ebp+10
注意,参数从后向前入栈,因此越接近ebp,参数在参数列表中越靠前。
注意,我们这里先假设全部都是int类型,如果后续有遇到不一致的情况,我们再来修改
找到局部变量个数
局部变量的个数在代码第三行,sub $0x4, %esp
这里减去的数量,就是分配的局部变量的长度。
在上面的代码中,显然这里分配了4字节,也就是只有一个局部变量。我们假设这个局部变量就是int类型,起名为i。
我们继续扩展我们的代码
1 | int foo(int a, int b, int c) { |
识别循环体
循环体通常是while循环或者for循环。
对于常见的while
命令,我们寻找下面相关的指令:
- 判断入口:
判断入口是一个连续的比较命令(如cmp)和一个跳转命令(如jge或jle)组成。找到判断入口有助于我们识别代码中是否有判断或者循环。
- 循环:
假如没有循环标志,那么就不是循环体,而是简单的if判断。循环是一个无条件的jmp命令。循环所跳转的位置就是判断条件的开始。
- 判断条件:
判断条件是判断入口以及判断入口和前面几条命令组合成的完整判断条件。判断条件就是while括号里的内容。
现在我们查看下面的例子:
1 | push %ebp <foo+0> |
判断入口:
我们寻找一个连续的判断指令和一个跳转指令,我们可以找到 cmp 和 jge 这两个连续指令,就说明这里是判断入口。
循环:
我们看到,最后存在一个jmp命令,jmp命令指向了第foo+12的位置,因此foo+12就是循环的判断条件。
判断条件:
判断条件的第一句是 mov -0x4(%ebp), %eax
,其中 -0x4(%ebp)
是最后一个局部变量,由于我们只有一个局部变量 i ,因此这句话的意思是,将本地变量i的值赋值给 eax。
我们看到cmp的命令是:cmp 0x10(%ebp), %eax
,其中cmp 0x10(%ebp)
指第三个入参的值。这句话的意思是计算 eax 寄存器中减去第一个入参的数值,也就是c。
联合起来,我们就知道,这里是计算 i - c的值,并交给 jge 来做跳转。
由于汇编的条件判断和C语言条件判断是反过来的,汇编是满足判断则跳过循环体,而C语言是满足条件则执行循环体,因此我们的判断条件也是反过来的。
jge表示大于等于0则跳过循环体,因此C语言的循环条件是小于0则执行循环体,也就是判断 i - c 是否小于 0
因此我们可以继续完善我们的代码:
1 | int foo(int a, int b, int c) { |
解析剩余代码片段
首先标记下已经处理过的代码
1 | push %ebp <foo+0> 无用 |
- 循环体之前的代码
我们看一下循环开始之前的部分:
1 | mov 0x8(%ebp), %eax <foo+6> |
我们发现这两句都涉及到了 eax 寄存器
对于多条涉及eax 寄存器的操作,我们尽量当作一个整体来看
我们发现,其实这里就是将 0x8(%ebp)
的数值 赋值给了 -0x4(%ebp)
,这两个值分别是 a 和 i
因此这个部分的代码是
1 | i = a |
- 循环体中间的代码
1 | mov 0xc(%ebp), %eax <foo+20> |
对于涉及eax的多条指令,我们不要一条一条看,而是看作一个整体。
其中,mov 0xc(%ebp), %eax
和 incl (%eax)
可以看作 incl (%b)
注意,由于这里是间接寻址,因此,这意味着,b中存的是地址。因此我们需要修改我们的入参,b这里不是数值,而是地址。这句代码意味着
1 | (*b)++ |
注意
*b++
会先对*b
解引用,再对b
自增
因此这里括号不能省略,否则会有问题
而对于 lea -0x4(%ebp), %eax
和 incl (%eax)
,可以看作 incl (&i)
,这里继续是间接寻址,也就是对后者地址上的数值自增。这句代码意味着 *(&i)++
,可以简化为
1 | i++ |
之所以这么复杂,是因为汇编指令不能直接对地址进行自增操作,但是可以对一个包含地址的寄存器做自增操作。
因此,补充了剩余的代码片段,代码应该为
1 | int foo(int a, int *b, int c) { |
识别返回数据
按照C语言的x86的调用约定,返回数据放在eax中。
1 | mov $0x0, %eax |
很显然,这里直接返回了0
最终代码
1 | int foo(int a, int *b, int c) { |