7.变量还原:变量的机器级表示

什么是变量?

看到这个有人会问,我这每天写代码,这变量是啥我还不知道吗?

那么对于计算机,或者对于汇编,对于机器指令,有没有变量这个概念?

甚至说,对于计算机来说,有没有"编程语言"这个概念?

有人说:计算机系统的本质就是一组离散状态空间及其变化规则,程序不过是符合规则的一组变化动作的组合。

有人说:电子计算机,是人利用电子电路和电子运动的物理规律,精心构建输入与输出效果,表达自己的设定。

有人说。。。。。。

------------------------------

好吧,这里也不是讨论哲学问题的地方,让我们回到现实里来。

什么是变量?:对于计算机来说没有变量,只有电路状态间的转换和输入输出的交互。编程语言和其中的变量都是给人类看的高层次抽象,它们是一种更符合程序员思考并操作的临时状态。

什么是反编译中的变量还原?:通过研究编译器如何将上层抽象转换到计算机汇编底层实现,依照一系列的约定与特征,从汇编中还原出上层抽象。

因此反编译的前提是:明确编译器的行为。

------------------------------

这还有一个绕不开的问题:在反编译器的眼中,变量是什么?

对于现在流行的编译器而言,变量是什么?变量就是SSA。就是有且在一个地方定义,在其它地方使用的东西。

对于反编译而言,最开始变量是寄存器和内存(栈+堆)的使用,然后是转换为SSA形态的中间语言,这就和编译的过程差不多了,最后通过染色算法将同一个SSA个还原掉。

但是反编译的事情总是没有那么简单。。。

变量在汇编中的表示(x86_64_ELF示例)

拆分常见的各种变量,展示他们在二进制汇编中的样子。知道了编译器对变量的表示才知道如何进行还原。

全局变量

全局变量在汇编中是比较好识别的,通常是对BSS、data、rdata段上数据的引用;

他们的地址固定(静态分析场景,不考虑动态的地址随机化保护),在IDA中会以unk_<address>标签展示,本质上是第一个数据地址的立即数:

函数局部变量(函数参数)

函数参数的局部变量,取决于函数的调用约定。通常是在栈与寄存器上;具体的判断逻辑,可以回头看一下之前调用约定与函数推导的那一节。

在IDA中,将鼠标放到F5后的函数参数上,会提示这个参数是如何传入的。比如下面这个this指正就是rdi传入的。

函数局部变量(寄存器)

编译器的寄存器着色分配算法;会尽可能的将所有的变量分配到寄存器中。如果变量很多,还会尽可能的复用寄存器。

在IDA的F5代码中,会在局部变量后的注释显示使用的寄存器:

多个变量经常使用相同的寄存器,这很正常,因为寄存器复用的机制。

函数局部变量(栈)

局部变量分配到栈上,有这么几个场景:

  • 数组形态的局部变量,如果数组很大会在栈上开辟固定长度的空间

  • 无法分配到寄存器的局部变量:寄存器不够用了,或者强制要求在栈上的变量(比如canary)

  • 动态开辟的栈空间(这种情况比较罕见)

在IDA中栈上变量后的注释会以相对于栈顶和栈底的偏移,注意,这里面的rbp/rsp只是栈顶和栈底的抽象,并不一定是真正存在的。

堆变量

现在的反编译的维度通常是函数级的,没有对于动态分配的“堆抽象”因此,堆变量就会被翻译成指针操作。

临时变量

除此之外,还有一些临时变量,他们是数据操作过程的中间产物。比如,代码中有一个很复杂的计算语句:

a = b + c + d + e .......

在编译器转换为ssa的过程中,每个加法操作符,都会添加一个ssa的临时变量。转换到汇编中去,如果寄存器着色算法能够分配足够的寄存器存放临时变量,那么寄存器会被多次重用,否则就需要栈变量作为临时变量的存放地点了

反编译为会出什么问题?

那么变量恢复后,会出现哪些“错误”,为什么会产生这个问题。

实际上,很多“错误”并非反编译变量还原的问题,而是其它场景导致的,以及反编译器无法真正理解“语意”导致的。

控制流的还原/函数调用约定不完整

第一个问题,也是最常见的问题,控制流还原错误和函数调用约定分析从而导致的变量还原错误。

比如,函数A的最后一个参数是一个指针。但是IDA没有正确的分析出函数的参数,漏掉了最后一个,那么在函数A之后的代码,一旦使用这个指针,就会出现“Value may be undefine”的红色变量。

再比如,有的特殊函数,比如__chkstk_darwin 这种不会对程序状态造成特殊影响,但是IDA不知道,他就会认为传入的变量已经被污染了,后面再使用就会出现各种问题。

还有各种控制流混淆导致的不完整,比如OLLVM这种,就更加平常了。

变量复用

变量复用是编译器中寄存器分配着色算法的核心指标,将在下一章更为详细的描述。

简单的来说:把程序里用的变量尽量塞到CPU的寄存器里,不够用时再把多出来的变量临时存到内存(栈变量)里,从而让程序跑得更快

核心逻辑是,如果一个变量用完了就不需要了,它的“位置”可以立刻腾出来给别的变量用,避免浪费。对于反编译来说,一个寄存器在函数的不同地方可能会存在不同的含义。那么问题就来了,发编译器没有高级变量的概念,他不知道不同位置的变量是否应该合并。

fixme - 这里需要一个案例

结构体

所有反编译器都不会主动恢复结构体变量,当前,所有的自动结构体恢复都是基于系统函数或者类似ABI出参入参,使用各种预置的规则实现的。

为了弥补这个缺陷,以及定义结构体的繁琐工作,现在的反编译工具提供对于内存偏移读写的自动化的结构体分析功能。

高级语言的特性

很多的高级语言,为了支持各种高层次的抽象特性,例如面向对象,自动化管理内存等等;会添加很多语言框架层级的代码。

甚至可以说,一个语言的特性越多,支持的框架约丰富,他在编译成为binary之后的反编译就越困难。比如说java2c和python2c这种加固的方式,处理起来的工作量就会大得多。

同时,在变量还原的时候,就会多出大量的混乱的的逻辑和变量,他们一般来说是高级语言为了管理内存以及面向对象所附加的各种特性实现的逻辑。

Last updated