在使用IDA Pro的过程中,栈帧的恢复以及栈变量识别错误的修正,这类问题通常会在程序开启编译优化、函数边界识别出错,或者程序使用了动态栈空间的情况下出现。IDA会依据函数入口处的建栈指令、栈指针的变化、调用约定以及内存访问方式,对局部变量进行推断,但这种推断并不一定每次都准确。一旦栈帧计算出现错误,反编译结果中就可能会发生变量重叠、参数位置异常、数组被拆成多个变量,甚至直接提示栈指针分析失败等情况。
一、IDA Pro栈帧怎么恢复
在着手恢复栈帧之前,不宜急于更改变量的名称。栈变量仅仅属于分析的结果,真正需要确认的部分,是函数的起始与结束位置,以及每一条指令执行完毕后栈指针所发生的偏移量。上层的变量名称即便整理得十分整齐,假若底层的栈偏移错误未被修正,重新反编译之后,结果也依然会是混乱的。
1、先检查函数边界
应当确认函数的入口是否含有完整的序言指令,以及函数的结尾是否停在了正确的返回指令或者尾调用位置。如果IDA把前一个函数的尾部、跳转表,或者后一个函数的开头,错误地归并到了当前函数里面,栈空间的大小就很有可能被错误地放大。
当函数边界不准确的时候,应当先把错误的函数删除,再按照真实的入口位置重新创建;如果仅仅是在尾部范围存在一些偏差,也可以直接去调整函数的结束地址。等到函数的边界处理完毕之后,再回过头来查看反编译的结果,许多看上去相当复杂的栈变量问题,通常也会跟着一同消失。
2、显示并检查栈指针变化
在【Options】→【General】中启用栈指针显示,观察函数内各位置的SP变化是否连续。
在正常的情况下,函数申请完局部空间之后,栈指针应当会遵循调用、压栈和释放等操作,进行有规律的变动。假若在某一条指令之后,SP的值突然出现了很明显的异常,那么原因可能就是IDA没能正确地识别出自定义的调用、动态的栈分配,或者是内联汇编。到了这个时候,可以在对应的那个地址上,使用【Change stack pointer】来手动修正SP的变化量。Hex-Rays同样会把函数的边界和SP的值,列为反编译失败时需要优先检查的内容。
3、重新确认栈帧大小
进入【Edit】→【Functions】→【Stack variables】查看当前函数的局部变量区、保存寄存器区和参数区。
如果局部变量区设置得过小,那么后续的栈访问就可能会被误判成参数;反过来,如果这个区域设置得过大,又有可能会把参数区也给吞并到局部变量的范围当中。在动手修改之前,比较妥当的做法是结合函数序言里面申请栈空间的指令来加以判断,例如去观察sub esp或者sub rsp指令后面跟着的那个数值,而不是仅仅根据伪代码里显示出来的变量个数去推测。栈帧视图本身,也同样支持对当前函数的栈变量进行查看、创建和调整。
二、IDA Pro栈变量识别错误怎么修正
在栈帧的大小恢复之后,变量仍然有可能被错误地拆开。编译器常常会让多个临时变量去共同使用同一段栈空间,也有可能把一个结构体或者数组,拆成若干次不同偏移地址的访问。IDA所能看到的,仅仅是汇编层面留下来的访问痕迹,它并不清楚源代码当初是如何声明的,因此还是需要结合上下文的逻辑,依靠人工去进行判断。
1、检查变量偏移和使用范围
可以先去看一看同一个栈偏移量,是在哪些指令里面被读取和写入的,以及它每次访问的宽度,究竟是一个字节、四个字节,还是八个字节。假如同一个位置,在不同的代码分支里面承担着完全不同的用途,那么它很有可能只是被编译器拿来复用的临时空间,并不一定要强行合并成一个单独的变量。
反过来说,如果一连串连续的偏移量,总是被放在一起初始化、一起传给同一个函数,或者是按照某个固定的步长在循环中进行访问,那么它就更像是数组或者结构体。这个时候,就可以在栈帧当中,把那些零碎分散的变量删除掉,再重新去创建一个尺寸更大一些的数组或者结构体成员,这样能够让伪代码更加贴近真实的数据布局。
2、修正变量类型和函数原型
把光标放在变量或者函数的上面,通过【Set type】这个操作,把正确的数据类型和函数的声明补充进去。
调用约定、参数个数或者返回类型一旦出错,就有可能会导致IDA把传入的参数错认成局部变量,或者是引起栈清理量计算方面的偏差。特别是像__stdcall、__fastcall、可变参数的函数,以及自行定义寄存器传参的这些情况,更是需要结合调用的位置一同去确认。等到类型都被修正之后,再重新运行一次反编译,变量的数量和相关的表达式,往往就能够得到很明显的简化。
3、处理变量重叠和红色变量
在Hex-Rays生成的伪代码当中,如果出现了红色的局部变量,这通常就是在表明,局部变量的分配或者重叠部分的处理,并没有能成功完成。遇到这种情况,应当先去检查SP的数值和栈帧的边界,然后再来判断这些变量是不是应该被合并成结构体、数组,或者是联合体,而不要一碰到重叠的情况,就立刻动手去删除变量。
有一部分被优化过的代码,会在不同的生存周期里,重复地去使用同一个栈槽,面对这种情形,并不需要勉强去还原出源代码里那个唯一的变量名称。只要偏移量、数据类型还有使用的范围都能够解释得通,就可以按照分析的实际需要,保留多个逻辑上的名称。
三、IDA Pro栈分析怎么减少反复返工
栈帧的修正工作,并不仅仅是单独去改动一个数值就能全部完成的。函数的原型、被它调用的那个函数的类型、异常退出的路径,以及非返回函数的标记,这些因素都会对分析的结果产生影响。假如每一次都只是去修改伪代码的表层,那么一旦数据库被重新分析,结果是极其容易再次发生变动的。
1、先修底层再整理伪代码
处理这类问题的先后顺序,比较建议安排为:函数边界、SP变化、栈帧大小、函数类型、变量类型,最末了再去做变量的命名与注释。排在前面的这几项信息,属于分析的根基,后面那些名字则更多是出于阅读方便的辅助,如果把这个顺序颠倒过来,往往就会造成许多重复的劳动。
2、留意无栈帧指针函数
被优化过的程序,有可能会把EBP或者RBP这种固定的帧指针省去,转而改用SP来进行相对的寻址。动态数组、alloca以及栈的对齐操作,还会让偏移量继续产生变化。对于这一类的函数,就不能只是去寻找那种传统的push ebp和mov ebp,esp指令了,而是应当沿着控制流,去把每一个涉及到栈调整的地方都仔细检查一遍。
3、用调试结果验证静态判断
在条件允许的情况下,可以在函数的入口位置,以及一些关键的调用指令之前设置断点,去观察实际运行当中的SP、BP寄存器值,以及局部内存里面所存放的内容。那些仅仅依靠静态分析很难判断清楚的结构体大小、参数的具体位置,以及栈是否平衡等问题,通过一次实际的运行调试,往往就能够梳理明白。不过,调试所得的结果,也需要对应到正确的模块版本上面,不能把另一份二进制文件里的地址,直接就拿过来硬套。
总结
有关IDA Pro的栈帧应当怎样恢复,以及栈变量识别出错之后应当怎样去修正,其关键之处在于,先要恢复函数的边界和栈指针的变化情况,然后再去对局部变量区、保存寄存器区,以及参数区进行调整。在遇到变量识别错误的时候,则需要综合栈偏移、访问宽度、生存周期以及函数原型这些因素,来判断它到底是一个普通的变量、数组、结构体,还是编译器为了复用而开辟的临时空间。等到把底层的栈分析处理正确之后,再着手去修改类型、变量名称和伪代码,这样子得到的结果,才会变得稳定许多。
