阅读笔记 —— 《Impeding Malware Analysis Using Conditional Code Obfuscation》
条件代码的哈希混淆
使用哈希函数等价替换原有分支条件,隐藏原有信息,示例如下:
图中左侧是原代码,右侧是混淆后的代码。混淆的操作主要是将strcmp(cmd, "startkeylogger")
等价替换为hash(cmd) == H
,其中H = hash("startkeylogger")
,然后增加了一个decrypt_function
解密函数,参数encr_log_keys
是对应函数的地址,作为待解密地址,cmd
为密钥。这样一来,一是隐藏了原本条件中的信息明文,二是加密代码以隐藏代码意图防止被检测。
然而对于其他的一些条件,例如n > 10 && n < 20
,存在多个满足条件的值,就不能用hash混淆了。因此,能够实现条件代码混淆的条件需要满足如下要求:
- 首先,即使是哈希之后的条件代码中的条件顺序也不能被打乱。
- 第二,当条件被满足时,应该从条件中得到一个唯一的值。
- 第三,该条件必须包含一个具有静态可确定的常量值的操作数。
鉴于上述这些要求,检查两个数据值相等的操作符(例如’==’, strcmp, strncmp, memcmp等)是合适的候选者。
对于满足上述条件的条件代码,一般情况下,混淆前后的情况如下图所示。
其中$B_E$代码块是被密钥c
加密了。只有当X
是正确的值,即c
,才能触发$Decr(B_E,X)$函数,从而得到原始代码并执行。恶意软件分析器只能通过监视攻击者触发这种行为、猜测正确的输入或破解密码操作来恢复条件密码B。
寻找条件代码的算法
目标: 识别和提取程序中依赖于特定输入条件的代码块,以便进行条件代码混淆。
步骤:
- 识别候选条件:
- 对每个函数 $F_i$ 构建控制流图(CFG) $G_i = (V_i, E_i)$,其中 $V_i$ 是基本块集合, $E_i$ 是控制流边集合。
- 识别包含条件分支的基本块,这些块有两个输出边。
- 使用循环分析排除循环中的条件分支。
- 选择包含等式运算符的条件分支作为候选条件,形成候选条件块集合 $C_i$。
- 确定条件代码块:
- 使用控制依赖分析识别对候选条件 $C$ 的结果有控制依赖的基本块。构建控制依赖图(CDG),其中每个条件块及其结果都有到对其有控制依赖性的块的边。
- 找到对候选条件 $C$ 的真结果有控制依赖的基本块集合 $CCode(C)$。
- 处理过程间依赖:
- 在过程间控制流图中确定可达性。如果候选条件的条件代码块中存在对某个函数$F$ 的调用,将$F$ 及其所有代码块包含在条件代码集合 $CCode(C)$ 中。
- 重复这一过程,直到所有可达的函数都被包含在内。
- 消除嵌套条件:
- 对于任何块 $B \in \text{CCode}(C)$,如果 $B$ 也在其他候选条件 $C’$ 的条件代码集合 $\text{CCode}(C’)$中,且 $C \in \text{CCode}(C’)$,将 $B$ 从 $\text{CCode}(C’)$ 中删除。
- 对于函数,执行相同的操作。
- 应用混淆:
- 先从没有其他候选条件依赖的候选条件开始混淆。
- 在每次迭代中,混淆没有未混淆候选条件依赖的候选条件。
- 重复这一过程,直到所有候选条件都被混淆。
- 处理不完整的CFG:
- 如果CFG不完整且代码指针无法静态解析目标,则保守地识别条件代码,不加密潜在目标及其可达的代码块。
对于多次出现在不同候选条件代码集合中的代码块,则通过复制代码并为每个候选条件分别加密,具体如下图所示。
对于条件代码是由多个逻辑运算符(如 &&、 ||)组合而成的,可以进行简化,例子如下图所示。
工具实现
主要操作是在Analysis/Transformation Phase
阶段和Encryption Phase
阶段。
Analysis/Transformation Phase
阶段以下操作均在IR中进行:
使用hash加密替换条件代码。
在被加密的目标代码块$B_M$前插入解密函数
Decipher
,也是标记函数。同时其参数size
用占位符表示。在被加密的目标代码块的后面放置加密密钥$K_c$。
在加密密钥$K_c$后面添加
End_marker()
标记函数。
在IR分析期间,无法预见相应代码在二进制文件中的确切位置以及二进制代码的大小,因此在代码中放置占位符来替换
size
,以便在Encryption Phase
阶段中放置实际值。以及在代码中放置标记函数,用于在Encryption Phase
阶段中识别要加密的代码块地址和大小,之后标记函数End_marker()
会被NOP
掉。加密和解密所使用的密钥为
Key(X)=hash256(X|N)
(或Key(C)=hash256(C|N)
),其中N
一个随机数,这么做的主要原因如下:- 如果使用
hash(c)
作为密钥,那么存储在该条件代码中的哈希值就可以用于解密。 - 如果使用
c
作为密钥,则存在密钥长度不为256bit的情况,然而使用hash256
可以达到这一要求。
Encryption Phase
阶段搜索标记函数
Decipher()
和End marker()
。找到后,根据标记函数提取密钥$K_c$,并使用NOP
指令替换$K_c$和End marker()
标记。然后,计算加密块的大小,需是32bytes的倍数,可用刚才的NOP
指令空间填充。对于嵌套的条件代码块,递归地搜索要加密的最内嵌套块,并从最内嵌套块到最外层执行加密。
由于混淆后的程序需要运行时解密,因此需要关闭代码页的写保护。