代码混淆技术系列
常见简单指令混淆
00 分钟
2022-9-18
2024-11-11
type
status
date
slug
summary
tags
category
icon
password

为什么需要指令混淆

软件的安全性严重依赖于代码复杂化后被分析者理解的难度,通过指令混淆,可以将原始的代码指令转换为等价但极其复杂的指令,从而尽可能地提高分析和破解的成本。

常见的混淆方法

代码变形

代码变形是指将单条或多条指令转变为等价的单条或多条其他指令。其中对单条指令的变形叫做局部变形,对多条指令结合起来考虑的变成叫做全局变形。
例如下面这样的一条赋值指令:
可以使用下面的组合指令来替代:
更进一步:
pushfdpopfd 是为了保护 EFLAGS 寄存器不受变形后指令的影响。
继续替换:
这样的结果就是简单的指令也可能会变成上百上千条指令,大大提高了理解的难度。
再看下面的例子:
可以变成:
而且 IDA 不能识别出这种 label 标签的调用结构。
指令:
可以替换成:
指令:
可以替换成:
下面我们来看看全局变形。对于下面的代码:
因为两条代码具有关联性,在变形时需要综合考虑,例如下面这样:
这种具有关联性的特定使得通过变形后的代码推导变形前的代码更加困难。

花指令

花指令就是在原始指令中插入一些虽然可以被执行但是没有任何作用的指令,它的出现只是为了扰乱分析,不仅是对分析者来说,还是对反汇编器、调试器来说。
来看个例子,原始指令如下:
加入花指令之后:
其中使用了源程序不会使用到的 esi 和 edx 寄存器。这就是一种纯粹的垃圾指令。
有的花指令用于干扰反汇编器,例如下面这样:
加入花指令后:
乍一看似乎很奇怪,其实是加入因为加入了机器码 EB 01 FF,使得线性分析的反汇编器产生了误判。而在执行时,第二条指令会跳转到正确的位置,流程如下:

扰乱指令序列

指令一般都是按照一定序列执行的,例如下面这样:
指令序列看起来很清晰,所以扰乱指令序列就是要打乱这种指令的排列方式,以干扰分析者:
虽然看起来很乱,但真实的执行顺序没有改变。

多分支

多分支是指利用不同的条件跳转指令将程序的执行流程复杂化。与扰乱指令序列不同的时,多分支改变了程序的执行流。举个例子:
变形如下:
代码里加入了一个条件分支,但它究竟会不会触发我们并不关心。于是程序具有了不确定性,需要在执行时才能确定。但可以肯定的时,这段代码的执行结果和原代码相同。
再改进一下,用不同的代码替换分支处的代码:

不透明谓词

不透明谓词是指一个表达式的值在执行到某处时,对程序员而言是已知的,但编译器或静态分析器无法推断出这个值,只能在运行时确定。上面的多分支其实也是利用了不透明谓词。
下面的代码中:
假设我们知道这里 esi 的值肯定是 0,那么就可以在 fake luggage 处插入任意长度和复杂度的指令,以达到混淆的目的。
其它的例子还有(同样假设 esi 为 0):

间接指针

这里通过 dummy_data 来间接地引用 message,但 IDA 就不能正确地分析到对 message 的引用。

代码虚拟化

基于虚拟机的代码保护也可以算是代码混淆技术的一种,是目前各种混淆中保护效果最好的。简单地说,该技术就是通过许多模拟代码来模拟被保护的代码的执行,然后计算出与被保护代码执行时相同的结果。
当原始指令执行到指令序列的开始处,就转入代码虚拟机的入口。此时需要保存当前线程的上下文信息,然后进入模拟执行阶段,该阶段是代码虚拟机的核心。有两种方案来保证虚拟机代码与原始代码的栈空间使用互不冲突,一种是在堆上开辟开辟新的空间,另一种是继续使用原始代码所使用的栈空间,这两种方案互有优劣,在实际中第二种使用较多。
对于怎样模拟原始代码,同样有两种方案。一种是将原本的指令序列转变为一种具有直接或者间接对应关系的,只有虚拟机才能理解的代码数据。例如用 0 来表示 push, 1 表示 mov 等。这种直接或间接等价的数据称为 opcode。另一种方案是将原始代码的意义直接转换成新的代码,类似于代码变形,这种方案基于指令语义,所以设计难度非常大。

参考

上一篇
搭建DVWA靶场
下一篇
2022网鼎杯(青龙组)WriteUp