-
-
[原创]去除反混淆后程序中的冗余汇编代码
-
发表于: 2024-7-7 15:02 3045
-
一、前言
在之前去混淆的过程中,发现去混淆的程序中,伪代码与原程序伪代码相差无几,然而如果仔细观察汇编代码的话,可以发现存在较多的冗余代码,即混淆过程中产生的垃圾代码。那么如何去除这些冗余汇编指令呢?
二、基于Liveness Analysis的想法
先给出我们需要解决一个例子(这是一个虚假控制流混淆中的部分代码,其中标*
的为冗余汇编指令):
mov rax, [rbp+var_10] movsxd rcx, [rbp+var_14] movsx eax, byte ptr [rax+rcx] add eax, [rbp+var_18] mov [rbp+var_18], eax * mov rax, offset x * mov eax, [rax] * mov rcx, offset y * mov ecx, [rcx] * mov edx, eax * sub edx, 1 * imul eax, edx * and eax, 1 * cmp eax, 0 * setz al * cmp ecx, 0Ah * setl cl * or al, cl * test al, 1 * jnz loc_401235
最开始的想法是从第20行的汇编指令开始,跟踪与该指令以及后续指令相关的指令。如何跟踪呢?简单想就是跟踪这些指令使用到的寄存器。
2.1 Liveness Analysis
Liveness Analysis
是一种数据流分析技术,用于确定程序中的每个变量在程序的不同点上是否“活跃”。“活跃”的意思是变量在某个程序点后将被使用,且在这之前没有被重新定义。这项技术在编译器优化、寄存器分配和死代码消除中起着关键作用。
本文章中主要用到了里面的def
和use
集合的想法。
-
def
集合:当前汇编指令显式和隐式改变的寄存器、内存的集合。 -
use
集合:当前汇编指令显式和隐式使用的寄存器、内存的集合。
2.2 改进Liveness Analysis以适配
然而从图中看,似乎Liveness Analysis
技术没有考虑到标志寄存器的改变和使用。对于Liveness Analysis
技术能否解决我所预设的问题,我也不太清楚。因此在此基础上,进行如下改进:
- 引入标志寄存器来跟踪相关指令,这对于一些条件指令的跟踪是有帮助的。
- 对于内存操作数(例如
[rbp+var_10]
),还应提取其表达式中的寄存器,存入use
集合中。因为我认为既要跟踪内存地址是怎么来的,也要跟踪内存地址对应的值是怎么来的。 - 对于指令中操作数既不是寄存器,也不是内存的(例如
var_4
、全局变量x
),应认为它们是常量,即不加入use
集合中。 - 由于指令中出现的寄存器名可能是同一寄存器的不同位数区分(例如
rax, eax, ax, ah, al
),统一将它们转换成最大位数的寄存器名。
对刚才那个例子进行def
和use
集合的分析,结果如下:
mov rax, [rbp+var_10] def={rax} use={[rbp+var_10], rbp} movsxd rcx, [rbp+var_14] def={rcx} use={[rbp+var_14], rbp} movsx eax, byte ptr [rax+rcx] def={rax} use={[rax+rcx], rax, rcx} add eax, [rbp+var_18] def={rax} use={rax, [rbp+var_18], rbp} mov [rbp+var_18], eax def={[rbp+var_18]} use={rax, rbp}//理应认为[rbp+var_18]的寻址用到了rbp * mov rax, offset x def={rax} use={}//认为x是具体值,不加入到use集合中 * mov eax, [rax] def={rax} use={rax, [rax]}//既要知道内存地址对应的值从哪来的,也要知道内存地址从哪来的 * mov rcx, offset y def={rcx} use={}//认为y是具体值,不加入到use集合中 * mov ecx, [rcx] def={rcx} use={rcx, [rcx]} * mov edx, eax def={rdx} use={rax} * sub edx, 1 def={rdx} use={rdx} * imul eax, edx def={rax} use={rax, rdx} * and eax, 1 def={rax} use={rax} * cmp eax, 0 def={rflags} use={rax} * setz al def={rax} use={rflags} * cmp ecx, 0Ah def={rflags} use={rcx} * setl cl def={rcx} use={rflags} * or al, cl def={rax} use={rax, rcx} * test al, 1 def={rflags} use={rax} * jnz loc_401235 def={} use={rflags}
具体跟踪的初步想法是:
- 给定某一指令 I ,初始化跟踪集合 trace=useI (指令 I 的 use 集合),初始化相关指令列表 relevantInsns=[I]。
- 往上追溯每条指令,如果 trace∩defIi=∅ ,那么认为指令 Ii 是相关指令,将指令 Ii 加入到 relevantInsns中, 同时在 trace 集合删除交集中的元素,并将指令 Ii 的 use 集合中的元素加入进来,即进行如下运算:trace=(trace−(trace∩defIi))∪useIi 。如果 trace∩defIi=∅ ,那么认为指令 Ii 是不相关指令,不进行任何操作。
- 在追溯的过程中,如果 trace=∅ 或者待跟踪指令集为空,则认为跟踪结束。
感觉说的有些混乱,就那上面这个例子解释一下吧。首先我们假设跟踪的指令为jnz loc_401235
,后续步骤如下:
初始化: trace = {rflags} relevantInsns = [20] (为了简洁,用行号代替指令) 往上分析: 19: trace ∩ use = {rflags}, 是相关指令。 更新 trace = {rax}, relevantInsns = [19, 20] 18: trace ∩ use = {rax}, 是相关指令。 更新 trace = {rax, rcx}, relevantInsns = [18, 19, 20] 17: trace ∩ use = {rcx}, 是相关指令。 更新 trace = {rax, rflags}, relevantInsns = [17, 18, 19, 20] 16: trace ∩ use = {rflags}, 是相关指令。 更新 trace = {rax, rcx}, relevantInsns = [16, 18, 19, 20] 15: trace ∩ use = {rax}, 是相关指令。 更新 trace = {rcx, rflags}, relevantInsns = [15, 16, 18, 19, 20] 14: trace ∩ use = {rflags}, 是相关指令。 更新 trace = {rax, rcx}, relevantInsns = [14, 15, 16, 18, 19, 20] 13: trace ∩ use = {rax}, 是相关指令。 更新 trace = {rax, rcx}, relevantInsns = [13, 14, 15, 16, 18, 19, 20] 12: trace ∩ use = {rax}, 是相关指令。 更新 trace = {rax, rcx, rdx}, relevantInsns = [12, 13, 14, 15, 16, 18, 19, 20] 11: trace ∩ use = {rdx}, 是相关指令。 更新 trace = {rax, rcx, rdx}, relevantInsns = [11, 12, 13, 14, 15, 16, 18, 19, 20] 10: trace ∩ use = {rdx}, 是相关指令。 更新 trace = {rax, rcx}, relevantInsns = [10, 11, 12, 13, 14, 15, 16, 18, 19, 20] 9: trace ∩ use = {rcx}, 是相关指令。 更新 trace = {rax, rcx, [rcx]}, relevantInsns = [9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20] 8: trace ∩ use = {rcx}, 是相关指令。 更新 trace = {rax, [rcx]}, relevantInsns = [8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20] 7: trace ∩ use = {rax}, 是相关指令。 更新 trace = {rax, [rax], [rcx]}, relevantInsns = [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20] 6: trace ∩ use = {rax}, 是相关指令。 更新 trace = {[rax], [rcx]}, relevantInsns = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20] 5: trace ∩ use = {}, 不是相关指令。 trace = {[rax], [rcx]}, relevantInsns = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20] 4: trace ∩ use = {}, 不是相关指令。 trace = {[rax], [rcx]}, relevantInsns = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20] 3: trace ∩ use = {}, 不是相关指令。 trace = {[rax], [rcx]}, relevantInsns = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20] 2: trace ∩ use = {}, 不是相关指令。 trace = {[rax], [rcx]}, relevantInsns = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20] 1: trace ∩ use = {}, 不是相关指令。 trace = {[rax], [rcx]}, relevantInsns = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20] 待分析指令集为空,结束跟踪
对于上述例子,我们的想法成功的追踪到了所有冗余指令。然而当存在cmovcc
指令时,我们的想法并不是很理想。
2.3 cmovcc指令所带来的问题
以下是控制流平坦化混淆的垃圾汇编指令(其中标*
的为冗余汇编指令)。
* mov edx, [rbp+var_4] def={rdx} use={[rbp+var_4], rbp} * mov eax, 4A...h def={rdx} use={} * mov ecx, 32..h def={rcx} use={} * cmp edx, 2 def={rflags} use={rdx} * cmovnz eax, ecx def={rax} use={rcx,rflags} 或者 def={} use={rflags} * mov [rbp+var_1c], eax def={[rbp+var_1c]} use={rax, rbp} * jmp loc_401418 def={} use={}
由于cmovnz
存在执行和不执行的情况,因此需要分两种情况讨论:
- 当标志寄存器满足
cmovnz
的条件时,执行cmovnz
指令,此时def={rax}
,use={rcx,rflags}
。 - 当标志寄存器不满足
cmovnz
的条件时,不执行cmovnz
指令,此时def={}
,use={rflags}
。
赞赏记录
参与人
雪币
留言
时间
4Chan
为你点赞~
2024-7-12 16:43
逆天而行
感谢你的积极参与,期待更多精彩内容!
2024-7-8 17:24
Lnju
+1
谢谢你的细致分析,受益匪浅!
2024-7-8 11:39
你瞒我瞒
感谢你的积极参与,期待更多精彩内容!
2024-7-8 10:15
R0g
+1
这个讨论对我很有帮助,谢谢!
2024-7-8 00:48
fallw1nd
你的分享对大家帮助很大,非常感谢!
2024-7-7 18:37
赞赏
他的文章
- [原创]去除反混淆后程序中的冗余汇编代码 3046
- [原创]从Clang到Pass加载与执行的流程 24541
- OLLVM混淆源码解读 24806
赞赏
雪币:
留言: