3. 递归下降反汇编-代码与数据混合场景的处理
Last updated
Last updated
这一章将介绍如何将机器码进行”反汇编“,虽然常用语句上的反汇编指的是将机器码转义为汇编;不过在反编译的语境中,“反汇编”往往是指将机器码转换为中间语言。
由于冯·诺依曼体系下的计算机结构在主存中是不区分指令和数据的(对应,哈弗结构将指令和数据存储分离)。反汇编的第一个难题就是从存储中区分数据与指令,精确的反汇编不能无条件的相信.text段里都全是指令。尤其实在CSIC复杂指令集的场景中,不定长的指令使用“线性扫描”功能很容易出现问题。
控制流依赖的中间语言翻译,或者控制流依赖的反汇编技术目的是为了处理可能存在的数据与代码的混合情况。
为了避免将数据翻译成代码,现在的反编译工具使用基于控制流的递归下降算法进行反编译,来看下面这个示例。
在较为复杂的switch代码中,往往使用跳转表甚至多级跳转表进行判断,而有的编译器会将跳转表记录在函数体内部。例如glibc的pathconf函数,它本体是个巨大的switch语句
https://codebrowser.dev/glibc/glibc/sysdeps/posix/pathconf.c.html
在IDA中反编译这个函数,可以看到0x00B9D11这部分是数据,但是被保存在text段中。
跳转表在.text段并且IDA正确的识别出来。
但是如果我们使用objdump这一类的软件,它就啥都不管了”把text全反编译了“,就会变成这样,错误的将数据识别成代码。
当前主流的反汇编算法分为两大类:线性扫描和递归下降;
何为递归下降反汇编,简单说就是程序运行他得有个入口点是吧,经过这个入口点往下一直去找他调用的函数地址,把那个地址当作函数反汇编,层层递归去反汇编。
而线性扫描就是反汇编完这个函数,然后就把后面的地址直接当函数或者汇编直接给反汇编了,这样线性的去反汇编。
ref: https://bbs.kanxue.com/thread-269617.htm
递归下降的实现基于控制流的反汇编能力。(注,现在的反编译器往往直接将机器码翻译成中间语言,汇编本身和中间语言在一个层次上)
并且在翻译中间语言过程中会注意到跳转与函数调用语义,根据内部的规则进一步判断如何更具控制流进行翻译。换句话说,在反汇编的时候,翻译器知道程序会如何运行。如下图
但是还有两个问题不好解决:
我该从哪里开始翻译机器码,换句话说,需要找到所有函数的入口
间接跳转,间接调用,比如 call eax、 jmp eax; 以及no-return-function 和 尾递归、尾调用应该怎么处理,反汇编的时候我怎么知道它会跳转到哪里去?函数会不会一去就不回来了。
那么剩下的第一个问题,找到函数的入口,确认反汇编起始点。
当前逆向工具的识别技术基本上有这么几类:
通过符号识别函数起始位置,比如main等等
基于规则匹配函数起始位置
基于Xref寻找函数起始位置
对于main函数识别通用方法:
基于_libc_start_main 符号寻找
去_start/_scrt_common_main_seh里找
对于常用函数识别的通用方法:
去二进制符号里找
在异常处理结构里找
识别并标记直接的函数调用,并尝试处理间接跳转
基于特殊的决策树/规则直接匹配二进制寻找(较为少见)
基于通用的二进制入口结构,比如_init_array和_fini_array
当然,如果上面的方法,都用不了,还可以在我们逆向的时候对函数人工进行标记。
对于控制流无法正常获取,或者奇奇怪怪的回调函数经常能用到这种方法。
在IDA中就能使用create-function功能强制标记,甚至可以指定方法的起始与结束位置
ref:https://hex-rays.com/blog/igors-tip-of-the-week-87-function-chunks-and-the-decompiler/
看看下面这个代码示例:
十分有趣的花指令小玩意,其本身的防护强度不高。处理办法有很多,现场写pattern+patch都可以。大家在一些简单的ctf题目中一定经常遇到
--—————————————————————-
为什么花指令能够对抗基于控制流的反编译?
因为,若不添加额外的转换规则,反编译器并不知道 jz+jnz ==> jmp;本质上就是一个跳转。
基于控制流的反编译处理就会把跳转的目标加入处理队列,使用默认方法继续顺着控制流往下翻译(将0xe8当成call进行翻译),最后产生反编译的冲突。
普通指令 | 不改变流程 |
---|---|
条件跳转
将两个分支都加入处理队列
无条件跳转
改变流程,进行跳转
函数调用
目标加入子函数流程
返回
结束流程