究竟是什么是混淆?如果仔细思考一下这个问题,就会发现混淆其实是打破思维惯性的一种方式。不可否认惯性思维带来的方便,但是同样有其局限性。我们一般对反汇编代码进行还原时,默认CALL就是对一个函数的调用,碰到RET就是函数返回,条件分支两侧的代码都有可能被执行。而代码混淆就是打破了这种思维惯性,让逆向工程变得更加复杂。
说到混淆,就不得不提到编译原理。编译器在把中间代码翻译为目标程序时,会先经过一个代码优化器来处理。
其过程可以表示为:
源程序 - > 前端 - > 中间代码 - > 代码优化器 - > 中间代码 - > 代码生成器 - > 目标程序
而混淆,就是代码优化器的逆过程。
相应的,混淆技术也可以分为两类,基于控制流的混淆,基于数据流的混淆。而实际应用中,这两类技术都会被紧密结合在一起。
所以如果一个人要进行解混淆操作,而这个人恰好对编译优化技术一窍不通
( 比如我 :) ),可以说,他能做到的最好的解混淆也就是搜一搜特征码,做模式替换。
-
基于数据流的混淆技术
-
(1)常量展开(常量合并的逆操作)
编译器在编译时,会把那些在每次运行时总是得到相同常量值的表达式替换为该常量值。
比如上面这段代码,在现代编译器中是不可能把"5 * 7 + 10"这个计算过程编译进目标程序的,因为这个值在编译时就可以推算出来。
对代码进行混淆时,我们可以提取出一些指令的立即数,对其进行展开。
比如上面这段代码,在现代编译器中是不可能把"5 * 7 + 10"这个计算过程编译进目标程序的,因为这个值在编译时就可以推算出来。
对代码进行混淆时,我们可以提取出一些指令的立即数,对其进行展开。
push 1
inc dword ptr[esp],1
push 1
inc dword ptr[esp],1
这两段代码是完全等价的,如果忽略标志位的变化的话。在VMProtect 1.X(早期版本)中,该混淆技术被大量运用。
x - 1 == ~-x
x + 1 == -x
ror x,y == x >> y | (x << (lenbite(x) - y))
rol x,y ==x << y | (x >> (lenbite(x) - y))
lenbite指取位数,比如DWORD取位数是32 。
上面两条指令是完全等价的,但将not reg32替换为xor reg32,-1,足以使一些人摸不着头脑。
lea esp,[esp - 4]
mov [esp],x
push reg32
mov reg32,esp
xchg [reg32],esp
pop esp
上述第1段代码与第2段代码是完全等价的,而第3段代码与第4段代码又是完全等价的。
lea esp,[esp - 4]
mov [esp],x
push reg32
mov reg32,esp
xchg [reg32],esp
pop esp
上述第1段代码与第2段代码是完全等价的,而第3段代码与第4段代码又是完全等价的。
lea esp,[esp - 4]
mov [esp],x
push reg32
mov reg32,esp
xchg [reg32],esp
pop esp
上述第1段代码与第2段代码是完全等价的,而第3段代码与第4段代码又是完全等价的。
push reg32
mov reg32,esp
xchg [reg32],esp
pop esp
上述第1段代码与第2段代码是完全等价的,而第3段代码与第4段代码又是完全等价的。
push reg32
mov reg32,esp
xchg [reg32],esp
pop esp
上述第1段代码与第2段代码是完全等价的,而第3段代码与第4段代码又是完全等价的。
这项技术的可怕在于,这个过程是可以不断重复的。混淆了一次代码,可能又会有新的可以混淆的代码出现。
这项技术在Themida的虚拟机里运用地非常成熟。
当然,尽管其变形后的代码往往让人读了之后有想吐的感觉,但其实去混淆非常简单。你放心,我绝不会编这么一个无聊的小孩子谎话来骗你,只要搜集到足够多的模板,对其替换回来就好了。唯一要考虑的难题就是,你的编码能力能不能支撑你写完一个高阶的模式匹配器。
-
基于控制流的混淆技术
-
(1)插入死代码(消除死代码的逆操作)
如果一个变量在程序的某一点上的值以后可能会被用到,则该变量在改点上是活的,否则,它在该点上就是死的。与此相关的一个概念就是死代码,即永远不会被使用的语句。
基于该项技术,还可以延伸出伪造基本块等混淆技术。
其思路就是在两个基本块之间,或者我们可以在一个基本块里强行再随机选择首指令划出两个基本块,这两个新基本块本应该有一条无条件转移指令连接,我们可以伪造一条条件跳转指令,而有效的,永远是两侧分支中的一支。如果更进一步,对于那些永远不会执行的代码,我们可以进行巧妙构造来对抗静态反汇编工具。
如何不让破解者轻易的发现这是伪造的基本块呢?
我们可以引入一些必然成立的数学公式,或者在某个范围上必然成立的数学公式。
比如,贯穿整个高中函数题的基本不等式。
a^2 + b^2 >= 2(ab)^(1/2)
或者柯西不等式。
(a^2+b^2)(c^2 + d^2)≥(ac+bd)^2
再阴险一点的话,可以考虑去数论里面找一些灵感,比如费马小定理。
流图是用来表示中间代码的一种方式。流图就是一个有向图,基本块构成图的节点,基本块之间以转移指令构造出来的联系就是图的边。
借助IDA等工具,我们可以清楚地看到一段代码的流图,而这对我们逆向工程的进行是很有益处的。有了控制流,我们就知道往哪里入手,如果走控制流行不通,我们就只能走数据流,比如常见的我们对内存中的关键信息下内存断点。如果数据流和控制流都走不通,那么唯有设计一个解混淆工具,这是背水一战。
所谓流图展平,其实和虚拟机是差不多的一项技术。
如下图所示,就是一个非常典型的虚拟机。
而流图展平,和虚拟机一样,都是对程序的控制流进行了处理。唯一不同的就是,虚拟机里的Handle,变成了基本块。
没什么多说的,大名鼎鼎的一项技术。
简单来说就是将部分代码重新编译一次,并打包解释器进程序,运行的时候动态解释执行。
但这种技术会带来很多额外的CPU开销,不能用来,或者说不会有人用来保护算法性质的代码。
同一基本块中的指令基本都是按顺序排列的,而不同基本块之间也基本上是按照一定顺序排列的,因为这可以减少跳转指令的数量。
在十年前,或者说更早的时候,用JMP IMM32来打乱代码顺序这种技术的运用可以说是数不胜数。
但以现在的眼光来看,这种技术完全是幼儿园五岁小孩的把戏。有太多办法可以把它KILL掉,这里不多赘述。
我们可以采取异常处理机制,设置程序的下一步状态。但这也是基于系统的,你不能指望在Linux上面还能使用VEH。
实现思路就很简单了,现在也还有一些壳会用到这种技术。
比较常见的应用就是,安装一个SEH,然后触发一个异常,在异常处理Handle里面将程序设置回正常的状态。
如果说阴险的话,我首推PESpin壳,该壳的v1.33版本会使用Debug机制接管程序,并转移一部分指令进虚拟机,异常触发时由调试程序来执行。
总结:
如果把上述的混淆技术,单独提出一两种来运用,则其效果是非常差的。一个好的混淆引擎,应该设计成可重用的,支持多次轮环加密的。
源程序里面用到的PE操作库,反汇编引擎,汇编引擎,我都是直接在Github上面找的,它们分别是,PEFile、BeaEngine、XDEParser。
如果要实现流图展平的话,首先我们得理出程序的基本块。
基本块的定义如下。
(1)控制流只能从基本块的第一条指令进入该块。也就是说,没有跳转到该基本块中间的指令。
(2)除了基本块尾指令,否则不会有机会离开基本块。
如果要划分基本块的话,首先需要确定首指令。
首指令的选择规则如下。
(1)待混淆的所有代码的第一条指令是首指令。
(2)任意一条转移指令的目标指令是一个首指令。
(3)紧跟在一条转移指令后的指令是一个首指令。
每个首指令对应的基本块包括了从它开始,直到下一个首指令(不含)或者结尾指令的所有指令。
以一段汇编代码为例。
如下图所示。
首先,我们需要选择首指令,根据首指令的选择规则,标出首指令。
接着选择基本块。
按照基本块划分规则,我们可以划分出。
1>
mov eax,2018h
cmp eax,7DFh
jbe Label1
2>
mov ecx,1
jmp Label2
3>
mov ecx,0
4>
ret
1>
mov eax,2018h
cmp eax,7DFh
jbe Label1
2>
mov ecx,1
jmp Label2
3>
mov ecx,0
4>
ret
实际处理的时候,如果尾指令是流程转移指令,我们应该把这条指令抹掉,因为每个代码块之间的联系不再是由流程转移指令维护了,而是隐藏在对Dispatch的调用中,而对于尾指令不是流程转移指令的,我们可以向其尾部附加一条无条件转移指令,当做尾指令是流程转移指令的情况来处理。
Dispatch的设计如下。
pushad
pushfd
mov edx,dword ptr[esp] ;edx - > eflags
mov ecx,dword ptr[esp + 02Ch] ;ecx - > BranchType
cmp ecx,0 ;JO
jnz @F
push edx
popfd
mov edx,0
mov eax,1
cmovo edx,eax
jmp end5
@@:
cmp ecx,1 ;JC | JB
jnz @F
push edx
popfd
mov edx,0
mov eax,1
cmovc edx,eax
jmp end5
@@:
cmp ecx,2 ;JE
jnz @F
push edx
popfd
mov edx,0
mov eax,1
cmove edx,eax
jmp end5
@@:
cmp ecx,3 ;JS
jnz @F
push edx
popfd
mov edx,0
mov eax,1
cmovs eax,edx
jmp end5
@@:
cmp ecx,4 ;JP
jnz @F
push edx
popfd
mov edx,0
mov eax,1
cmovp eax,edx
jmp end5
@@:
cmp ecx,5 ;JL
jnz @F
mov eax,1
push edx
popfd
mov edx,0
cmovl edx,eax
jmp end5
@@:
cmp ecx,7 ;JG
jnz @F
mov eax,1
push edx
popfd
mov edx,0
cmovg edx,eax
jmp end5
@@:
cmp ecx,6 ;JA
jnz @F
mov eax,1
push edx
popfd
mov edx,0
cmova edx,eax
@@:
mov eax,dword ptr[esp + 01Ch] ;JECXZ
cmp eax,0
mov ebx,1
cmovz edx,ebx
jmp end5
end5:
push dword ptr[esp + edx * 4 + 024h]
add esp,4
popfd
popad
sub esp,28h
ret 30h
pushad
pushfd
mov edx,dword ptr[esp] ;edx - > eflags
mov ecx,dword ptr[esp + 02Ch] ;ecx - > BranchType
cmp ecx,0 ;JO
jnz @F
push edx
popfd
mov edx,0
mov eax,1
cmovo edx,eax
jmp end5
@@:
cmp ecx,1 ;JC | JB
jnz @F
push edx
popfd
mov edx,0
mov eax,1
cmovc edx,eax
jmp end5
@@:
cmp ecx,2 ;JE
jnz @F
push edx
popfd
mov edx,0
mov eax,1
cmove edx,eax
jmp end5
@@:
cmp ecx,3 ;JS
jnz @F
push edx
popfd
mov edx,0
mov eax,1
cmovs eax,edx
jmp end5
@@:
cmp ecx,4 ;JP
jnz @F
push edx
popfd
mov edx,0
mov eax,1
cmovp eax,edx
jmp end5
@@:
cmp ecx,5 ;JL
jnz @F
mov eax,1
push edx
popfd
mov edx,0
cmovl edx,eax
jmp end5
@@:
cmp ecx,7 ;JG
jnz @F
mov eax,1
push edx
popfd
mov edx,0
cmovg edx,eax
jmp end5
@@:
cmp ecx,6 ;JA
jnz @F
mov eax,1
push edx
popfd
mov edx,0
cmova edx,eax
@@:
mov eax,dword ptr[esp + 01Ch] ;JECXZ
cmp eax,0
mov ebx,1
cmovz edx,ebx
jmp end5
end5:
push dword ptr[esp + edx * 4 + 024h]
add esp,4
popfd
popad
sub esp,28h
ret 30h
调用方式很简单。其实是我为我自己的懒惰找了个借口 :)
push BranchType
push Branch2 //跳转成立的目标代码块
push Branch1 //跳转不成立的目标代码块
push BranchType
push Branch2 //跳转成立的目标代码块
push Branch1 //跳转不成立的目标代码块
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-6-10 23:23
被天水姜伯约编辑
,原因: