最近大三学长告诉我研究生学长那边有个嵌入式恶意代码检测的项目在招人,可能也问到我们本科部这边有没有人了,然后我就说有兴趣去了,虽然不知道是不是当牛马干苦力,但是没有过类似的经历,所以还是想硬着头皮去体验一下。当然,我对逆向的各个方向都抱有兴趣。然后没想到学长还要腾讯会议面试一下,有点没想到,毕竟我们学院大二这边不知道学逆向工程的是不是只有我一个,如果只招大二的话好像也没有其他人了。
有点尴尬,我的逆向经历?我好像啥逆向经历也没有吧,打过比赛,但也没拿什么有含金量的奖项
第一个问题谈谈学习过比较擅长的技术点,这个我的初步想法是谈谈 Python 逆向。
第二个问题是输出自己对于栈的理解。这个其实我很早之前就想再看一遍《逆向工程核心原理》中对栈的介绍了。记得学汇编指令的时候,对栈也只是 Push,Pop 和栈的特征的理解。感觉大二学数据结构也只是仅限于这些理解。但是读了《逆向工程核心原理》之后,才知道栈如何在程序中发挥用处,书里面还结合了程序在 OD 中调试。只看了一次读书的内容感觉自己记得还是不牢,今天就来温习一下栈。栈感觉在二进制方向当中存在感还是很高的,记得之前还遇到基于栈的虚拟机保护,难度挺高。
第三个问题我倒是不知道我应该要了解啥,我只是个牛马,学长老师说啥我都可以干。
本篇文章完全参考《逆向工程核心原理》。
栈的特征
栈是一种数据结构,它按照 FILO(First In Last Out,即后进先出)的原则存储数据。
栈有一个栈顶指针 ESP(Extended Stack Pointer),初始状态指向栈的底部。
执行 Push 命令可以将数据压入栈中,栈顶指针会移动到栈的顶部。
执行 Pop 命令可以将栈中数据弹出,如果栈为空,则栈顶指针重新移动到栈底部。
如图是一个示例程序
右上记录了初始情况下栈顶指针 ESP 的值,为 0019FF78,右下图则是栈中的情况。
当我们调试程序,按 F8 执行第一条汇编指令,即 push 0x100 时,数据 0x100 便被压入栈中,而 ESP 的值则减少了四个字节,向上移动。
这里有个细节需要提及,我们发现压入栈时,ESP 的值减小,即栈顶部在低地址处,而栈底部在高地址处,也就是说,栈是一种由高地址向低地址扩展的数据结构。这也是栈的一大特征。
当我们再执行下一条汇编指令 pop eax,就会恢复初始的栈的状态,数据 0x100 被弹出栈,并且 ESP 的值加上四个字节。
总结一下,向栈压入数据时,栈顶指针减小,向低地址移动,而从栈中弹出数据时,栈顶指针增加,向高地址移动。
栈帧
栈帧还是很重要的,因为在 IDA 看汇编指令的时候,会经常出现 EBP(栈帧指针,即 Extended Base Pointer)
栈帧在函数中用于声明局部变量、调用函数。栈帧就是利用 EBP 寄存器访问栈内的局部变量、参数、函数返回地址等的手段。ESP 寄存器承担着栈顶指针的作业,而 EBP 寄存器则负责行使栈帧指针的功能。因为在运行程序时,我们的 ESP 寄存器的值是随时变化的,如果通过 ESP 访问栈中函数的局部变量、参数时,这个基准就一直在变,不太方便进行访问。
所以我们在调用某个函数时,会先把用作基准点(函数起始地址)的 ESP 值保存到 EBP,并维持在函数内部,这样,我们可以不管 ESP,就以 EBP 的值为基准 (base) 能够安全访问到相关函数的局部变量、参数、返回地址。
栈帧对应得汇编代码通常如下
在分析 32 位程序的时候,我们会经常看到这样的汇编代码,他们象征一个函数的入口。
64 位程序倒是稍有不同
接下来调试一个程序来了解栈帧,程序源代码如下
main()
用 OD 打开示例程序,代码的 main 函数是程序入口,我们首先分析,如图中地址 401020 处即为 main 函数的汇编指令,图为程序运行时的初始状态。
我们可以看到 ESP 的值为 19FF30,EBP 的值为 19FF74,地址 401250 现在在栈顶的位置,即保存在 ESP (19FF30) 当中,这个地址是 main 函数执行完毕之后需要返回的地址。
main () 函数首先把 ebp 的值压入到栈中,这是一次备份,当 main () 函数执行完毕之后,会进行 Pop,恢复 ebp 的原始值,然后 retn
接着用 ebp 寄存器存储 esp 的值,此时的 ebp 就指向了栈顶的位置,ebp 就持有与当前 esp 相同的值,并且直到 main () 函数执行完毕,它都不会被修改,我们就可以通过 ebp 安全访问到存储在栈中的函数参数与局部变量。执行好这两条指令之后,main () 函数的栈帧就生成了,也就是设置好 ebp 了。所以设置 ebp 的过程就是栈帧生成的过程?
根据书中指引,在栈窗口右键,然后选择地址 -> 相对于 ebp,方便我们后面观察,我用的吾爱破解的中文版。
栈窗口就变得很直观了,再看主函数代码。
上述代码用于在栈中为局部变量 (a,b) 分配空间,并赋初始值,下一条汇编指令如下
sub 在汇编指令当中是一条减法指令,这条指令将 esp 的值减去 8 字节,而减去的 8 字节,实质是为函数的局部变量 a 与 b 开辟空间,以便让它们保存在栈中,long 类型的数据在 32 位的操作系统当中占据 4 个字节,所以两个 long,也就刚好是 8 个字节。
开辟好栈空间后,在 main () 函数的内部,无论 esp 的值如何变化,变量 a 和 b 的栈空间都不会受到破坏,ebp 是固定不变的,我们就可以以它为基准来访问函数的局部变量。看下两条指令
我们重点分析一下这两句汇编指令,ss 是 stack segment 的缩写,也就是栈段,ss:[ebp-0x4] 这种就是表示栈段中 ebp-0x4 地址处的内存空间,很前面的 dword,则是说明这段内存空间是 4 个字节,这里汇编指令的作用很明显就是对这两段内存空间赋值,把局部变量 1 放到 ebp-0x4,2 放到 ebp-0x8。
执行完两条汇编指令之后就如上图所示,1 和 2 都被压入栈内。
接下来的五条汇编指令
这五条指令描述了调用 add () 函数的整个过程,call 指令的作用就是调用函数,401000 处的函数就是我们的 add () 函数,而在调用函数前,我们需要将函数的参数 push 入栈,可以看到这里用了两条 push 指令将 ecx 寄存器中的参数 push 入栈。
这里有一个点需要注意的就是参数入栈的顺序与 C 语言源码当中的参数顺序恰好相反(函数参数的逆行存储),源码是 add (a,b),这里的 push 是先将参数 2 入栈,执行汇编指令后栈内情况如图。
接下来我们进入 add () 函数内部,来分析 401000 处的汇编指令.
这里还有一个重点,执行 call 命令进入被调用的函数之前,CPU 会先吧函数的返回地址压入栈,用作函数执行完毕后的返回地址,比如我们是在地址 40103C 处调用的函数,下一条汇编指令的地址为 401041,我们在执行完 add () 函数之后,程序执行流就该返回到 401041 地址处,然后执行 main () 函数的剩下代码,该地址也被称为 add () 函数的返回地址,如图,很直观,这里已经进入了 add () 函数了
add()
函数开始执行,和 main () 函数一样生成栈帧
这里我们可以看到先把 main () 函数的基址指针 (ebp) 保存一份在栈中,即 19FF2C,然后呢再将 esp 的值赋值给 ebp,这样就能保持 add () 函数的 ebp 值不变了。这时候 ebp 的新值就是 19FF14。
然后是声明变量的代码
对应汇编指令
这里还是很清晰的,这里是找 ebp+0x8 和 ebp+0xC 的值,也就是我们 push 进去的函数参数,先把 1 放入地址 ebp-0x8 处,再把 2 放入地址 ebp-0x4 处。也就是说低地址先放进去,高地址再放进去。
接着 add () 函数 return 两者之和,汇编指令如下
这里的指令也很好理解,需要注意的是,eax 寄存器在算术运算中存储输入输出数据,函数的返回值通常也都存储再 eax 寄存器。执行这两句汇编指令的过程中,栈内的情况并没有变化。
栈帧删除
至此,我们的 add () 函数就执行完毕了,在 retn 前我们需要删除栈帧,汇编代码如下
这里删除的过程和前面生成栈帧的过程其实是相对应的,把 ebp 的值赋值给 esp
执行完 mov 命令之后,sub esp,8 的命令就会失效,函数 add () 中的两个局部变量 x,y 不再有效。
接着将 add () 函数开始执行时备份到栈中的 ebp 的值恢复为原来的 19FF2C,此时的栈和寄存器的情况如下
其中 esp 的值指向的地址存储了另外一个地址,就是 call 命令执行时 CPU 存储到栈中的返回地址。接着 retn 就回到 main () 函数的指令处。此时调用栈就返回到调用 add () 函数时的状态了。
应用程序以这样的方式管理栈,可以使得不管有多少函数嵌套调用,栈都能得到较好的维护,不会崩溃,书中特别说
但是由于函数的局部变量、参数、返回地址等是一次性保存到栈中的,利用字符串函数的漏洞等很容易引起栈缓冲区溢出,最终导致程序或者系统崩溃。
这一句话我倒是不是很理解。
回到 main 函数当中
在执行完 add () 函数之后,就不再需要函数 a 和 b 了,所以把 esp 的值加上 8,将两个参数从从栈当中清理掉。
接着调用 printf () 函数,汇编指令如下
eax 寄存器当中存储 add () 函数的返回值,即结果 3,指令将其推入栈中,再将参数”% d\n” 也推入栈中。符合函数参数的逆行存储,这两个参数总共 8 字节,推入栈中后调用 401067 地址处的函数,很明显是 printf () 函数,然后再用 add 的汇编指令,将两个函数参数从栈中清理掉。
然后用 xor 指令将 eax 寄存器的值置为 0,用这个指令而不用 mov 指令是因为,xor 指令比 mov eax,0 的指令执行速度快,所以我们常用这个指令来初始化寄存器。
然后再和 add () 函数中删除栈帧一样的操作,我们执行完 retn 之后可以看到也返回到地址 401250 处
还有一点需要注意,执行 pop 指令,还有当我们用 add 指令清理栈时,其中的数据并没有删除,可能只是等待下一次的被覆盖,这些指令只是让 esp 指针往栈底移动。
完结撒花