查看原文
其他

代码混淆之我见(四)

三十二变 看雪学院 2019-09-17

本文为看雪论坛优秀文章

看雪论坛作者ID:三十二变



传送门:代码混淆之我见代码混淆之我见(三)
我们以 @Wszzy 在《自写保护壳交流——指令的膨胀和花指令的插入》一文中留下的CrackMe为例,简单介绍一下静态分析在解混淆中的应用。



初探


经不完全统计,该程序供使用了如下四种混淆:
1. 花指令
2. 常量展开
3. 模式替换
4. 打乱代码顺序局部性

花指令可以被使用递归下降反汇编算法的IDA识别。如果要去除,则只需搜索E9 01 00 00 00 ??并替换为90 90 90 90 90 90即可。

打乱代码顺序局部性对分析的影响不大,解混淆时就不单独处理了。其实有许多解混淆插件对乱序的处理效果非常好。

模式替换和常量展开在该程序中被结合使用,以我随便选的一段代码为例。

.ZP1:00423023 sub esp, 4
.ZP1:00423026 mov [esp+114h+var_114], esi
.ZP1:00423035 mov esi, 403539h
.ZP1:0042303A sub esi, offset loc_40363D
.ZP1:00423046 not esi
.ZP1:0042304E xchg esi, [esp+114h+var_114]

= >

push 0x103

//这可以看成是模式替换与常量展开的嵌套使用

push imm
=> //模式匹配
sub esp,4
mov [esp],imm
=> //常量展开
sub esp,4
mov [esp],reg
mov reg,xxx
..... //对reg的解密
xchg [esp],reg

解除模式替换时,需要搜集程序使用的模式进行逆变换,对付常量展开则进行常量折叠优化。

对代码进行混淆时,一般都会涉及到一个不精确语义问题。

push imm32为例,Wszzy的混淆器对它混淆时,引入了sub指令,也就是说混淆前后代码的语义并不是完全等价的,因为混淆后的代码会影响标志位。

我在(一)中介绍常量展开时,为了偷懒一笔带过了这个问题(现在我仍然打算一笔带过)……

该问题的解决办法是使用活跃分析,分析出每条指令上寄存器和标志位的活跃状态,死状态的寄存器和标志位可以随意使用。

当然,混淆前后语义不完全相等的混淆过程也可以是合法的,依据Collberg对混淆转换的定义,只要确保混淆后程序的可被用户观测到的行为相同即可。

解混淆


>>>>

1)花指令


void CodeObfs::CleanOPCode(void) {
PBYTE pBuf = (PBYTE)malloc(0x10000);
PBYTE p = pBuf;
BYTE b1[] = {0xE9, 0x01, 0, 0, 0};
BYTE b2[] = {0x90, 0x90, 0x90, 0x90, 0x90, 0x90};
GetData(0x423000, pBuf, 0xF00);
for (int i = 0; i < 0xF00 - 5; i++) {
if (!memcmp(pBuf, b1, sizeof(b1))) {
memcpy(pBuf, b2, sizeof(b2));
}
pBuf++;
}
SetData(0x423000, p, 0xF00);
free(p);
return;
}



>>>>

2)模式替换


1)CALL IMM32

call func
label:

push label
add [esp],0 //重定位
jmp func

call func
jmp label



2)PUSH IMM32

//使用该模式时,需先解常量展开

push imm32

lea esp,[esp-4] | sub esp,4
mov [esp],reg32
mov reg32,imm32
xchg [esp],reg32

push imm32



3)PUSH REG32

push reg32

xchg reg32,Xreg32
sub esp,4 | lea esp,[esp-4]
mov [esp],Xreg32
mov Xreg32,reg32
mov reg32,[esp]

push reg32



4)MOV REG32,IMM32

mov reg32,imm32

push imm32
pop reg32

mov reg32,imm32



5)SUB REG32,IMM32

//需先解除MOV REG32,IMM32的混淆及常量折叠

sub reg32,imm32

push Xreg32
mov Xreg32,imm32
push Xreg32
cmp reg32,[esp]
pushf //保存sub运算结果标志位
not [esp+4]
inc [esp+4] //neg [esp+4]
add reg32,[esp+4] //将减法转为除法
popf //取出运算标志位
lea esp,[esp+4]
xchg Xreg32,[esp]
add esp,4


sub reg32,imm32


void CodeObfs::CleanPattern(std::vector<cs_insn*> &vIns) {
char szAsm[64];
BYTE bCode[15];
cs_insn* pIns;
for (unsigned int i = 0; i < vIns.size(); i++) {
if (vIns[i] == NULL)
continue;
//CALL IMM32
if (!strcmp(vIns[i]->mnemonic, "push") && vIns[i]->detail->x86.operands[0].type == X86_OP_IMM && i - vIns.size() >= 3) {
if (vIns[i + 1] != NULL && vIns[i + 1] != NULL && !strcmp(vIns[i + 1]->mnemonic, "add") && !strcmp(vIns[i + 2]->mnemonic, "jmp")) {
cs_insn* pI1;
/*
push retaddr
add [esp],0
jmp calladdr
=>
call calladdr
jmp retaddr
*/

wsprintfA(szAsm, "call 0x%X", vIns[i + 2]->detail->x86.operands[0].imm);
Asm(DWORD(vIns[i]->address), szAsm, bCode);
cs_disasm(_handle, bCode, 15, vIns[i]->address, 1, &pIns);
pI1 = vIns[i];
vIns[i] = pIns;
wsprintfA(szAsm, "jmp 0x%X", pI1->detail->x86.operands[0].imm);
Asm(DWORD(vIns[i + 1]->address), szAsm, bCode);
cs_disasm(_handle, bCode, 15, vIns[i + 1]->address, 1, &pIns);
cs_free(vIns[i + 1], 1);
cs_free(vIns[i + 2], 1);
vIns[i + 2] = NULL;
vIns[i + 1] = pIns;
cs_free(pI1, 1);
}
}
...................... //省略
}


>>>>

3)常量展开


要进行常量折叠优化,我们首先要分析出各指令上下文中各寄存器的来源,该过程称为定值分析。

定值分析过程如下:
1. 基本块首指令所有寄存器的来源均初始化为-1
2. 每条指令的来源向下传递
3. 当某一指令写入某寄存器时,修改来源中某寄存器为该指令的行数 
为了方便表达,我们约定一种描述指令上下文来源的表示方法。
{寄存器名1:来源指令行数1, ... , 寄存器名n:来源指令行数n}
以一段具体代码为例:
mov esi,2018 {esi:-1}
sub esi,50 {esi:0}
inc esi {esi:1}

使用反汇编引擎分析帝1行指令时,我们注意到它对寄存器esi进行了读写操作,且esi的值来源于第0行指令。

进而分析第0行指令,可知esi是使用一个常数赋值的,于是我们可以断言在第1行指令上,esi的值是该常数。

从而将代码化简为mov esi,1968/inc esi,这个化简过程又可以重复进行,直到不能再化简为止。

定值分析还可以用于基本块内的指令交换,类似于遗传代码变形中的易位突变。

void CodeObfs::CleanImm(std::vector<cs_insn*> &vIns) {
if (vIns.empty())
return;
std::vector<ContextInfo> v;
ContextInfo ct;
for (int i = X86_REG_INVALID; i <= X86_REG_ENDING; i++)
ct[i] = -1;
v.push_back(ct);
for (unsigned int i = 0; i < vIns.size() - 1; i++) {
for (int x = 0; x < vIns[i]->detail->regs_write_count; x++)
ct[vIns[i]->detail->regs_write[x]] = i;
for (int x = 0; x < vIns[i]->detail->x86.op_count; x++) {
if (vIns[i]->detail->x86.operands[x].type == X86_OP_REG && vIns[i]->detail->x86.operands[x].access & CS_AC_WRITE)
ct[vIns[i]->detail->x86.operands[x].reg] = i;
}
v.push_back(ct);
}
//我偷懒了,忽略了一些问题
/*
这是我偷懒忽略掉的情况
mov ax,2010
mov dl,al
*/

std::vector<int> vReg;
for (unsigned int i = 0; i < v.size(); i++) {
if (!(vIns[i]->detail->x86.op_count != 0 && vIns[i]->detail->x86.operands[0].type == X86_OP_REG && vIns[i]->detail->x86.operands[0].access & CS_AC_WRITE))

......

更多更详细的代码请点击阅读原文查看


>>>>

4)整合结果


CleanOPCode(); //去除花指令

std::vector<DWORD> vAccess; //未访问的代码块的起始地址
std::vector<cs_insn*> vIns; //块代码
DWORD pIns = 0x423002;
BYTE bCode[15] = {0};
cs_insn* pDasm;
while (true) {
GetData(pIns, bCode, 15);
cs_disasm(_handle, bCode, 15, pIns, 1, &pDasm);
pIns += pDasm->size;
vIns.push_back(pDasm);
if (IsJxInsn(pDasm)) {
unsigned int k;
do {
k = vIns.size();
CleanNop(vIns);
CleanImm(vIns);
CleanNop(vIns);
CleanPattern(vIns);
} while (k != vIns.size());
break;
}
}
for (unsigned int i = 0; i < vIns.size(); i++)
printf("%s %s \r\n", vIns[i]->mnemonic, vIns[i]->op_str);
system("pause");
return;

...
完整代码请点击阅读原文查看



对比给出的原版CrackMe,去混淆结果较为可观。

附件链接:
https://pan.baidu.com/s/1PHoMlbP47mg6DjY9T6rzhA 
提取码:hhqu 




- End -





看雪ID:三十二变 

https://bbs.pediy.com/user-783210.htm  


*本文由看雪论坛 三十二变 原创,转载请注明来自看雪社区




进阶安全圈,不得不读的一本书





推荐文章++++

Talosec硬件钱包的侧信道攻击测试分析    Part1

Linux Kernel Exploit 内核漏洞学习(3)-Bypass-Smep

HW行动 rdpscan后门简单分析

打造自己的PE解释器







官方微博:看雪安全

商务合作请发邮件至:wsc@kanxue.com





“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存