传送门:
经不完全统计,该程序供使用了如下四种混淆:
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对混淆转换的定义,只要确保混淆后程序的可被用户观测到的行为相同即可。
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;
}
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);
}
}
...................... //省略
}
.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对混淆转换的定义,只要确保混淆后程序的可被用户观测到的行为相同即可。
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;
}
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;
}
1)CALL IMM32
call func
label:
push label
add [esp],0 //重定位
jmp func
call func
jmp label
call func
label:
push label
add [esp],0 //重定位
jmp func
call func
jmp label
//使用该模式时,需先解常量展开
push imm32
lea esp,[esp-4] | sub esp,4
mov [esp],reg32
mov reg32,imm32
xchg [esp],reg32
push imm32
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
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
//需先解除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);
}
}
...................... //省略
}
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);
}
}
...................... //省略
}
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))
continue;
if (vIns[i]->detail->x86.op_count != 1 && vIns[i]->detail->x86.operands[1].type != X86_OP_IMM)
continue;
int k = v[i][vIns[i]->detail->x86.operands[0].reg];
if (k == -1 || vIns[k] == NULL)
continue;
if (strcmp(vIns[k]->mnemonic, "mov") || vIns[k]->detail->x86.operands[1].type != CS_OP_IMM)
continue;
DWORD dwImm1 = DWORD(vIns[k]->detail->x86.operands[1].imm);
DWORD dwImm2 = DWORD(vIns[i]->detail->x86.operands[1].imm);
if (!strcmp(vIns[i]->mnemonic, "ror")) {
__asm {
push ecx
mov ecx,dwImm2
ror dwImm1,cl
pop ecx
}
}
else if (!strcmp(vIns[i]->mnemonic, "rol")) {
__asm {
push ecx
mov ecx,dwImm2
rol dwImm1,cl
pop ecx
}
}
else if (!strcmp(vIns[i]->mnemonic, "add")) {
__asm {
push edx
mov edx,dwImm2
add dwImm1,edx
pop edx
}
}
else if (!strcmp(vIns[i]->mnemonic, "sub")) {
__asm {
push edx
mov edx,dwImm2
sub dwImm1,edx
pop edx
}
}
else if (!strcmp(vIns[i]->mnemonic, "xor")) {
__asm {
push edx
mov edx,dwImm2
xor dwImm1,edx
pop edx
}
}
else if (!strcmp(vIns[i]->mnemonic, "not")) {
__asm {
push edx
mov edx,dwImm2
not dwImm1
pop edx
}
}
else if (!strcmp(vIns[i]->mnemonic, "inc")) {
__asm {
push edx
mov edx,dwImm2
inc dwImm1
pop edx
}
}
else if (!strcmp(vIns[i]->mnemonic, "dec")) {
__asm {
push edx
mov edx,dwImm2
dec dwImm1
pop edx
}
}
else if (!strcmp(vIns[i]->mnemonic, "and")) {
__asm {
push edx
mov edx,dwImm2
and dwImm1,edx
pop edx
}
}
else if (!strcmp(vIns[i]->mnemonic, "or")) {
__asm {
push edx
mov edx,dwImm2
or dwImm1,edx
pop edx
}
}
else
continue;
char szIns[255];
char szFmt[255] = {0};
strcat_s(szFmt, sizeof(szFmt), "mov ");
strcat_s(szFmt, sizeof(szFmt), cs_reg_name(_handle, vIns[k]->detail->x86.operands[0].reg));
strcat_s(szFmt, sizeof(szFmt), ",0x%X");
wsprintfA(szIns, szFmt, dwImm1);
BYTE bCode[15];
Asm(DWORD(vIns[k]->address), szIns, bCode);
vIns[i] = NULL;
cs_insn* insn;
cs_disasm(_handle, bCode, 15, vIns[k]->address, 1, &insn);
cs_free(vIns[k], 1);
vIns[k] = insn;
}
return;
};
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
.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
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;
}
call func
label:
push label
add [esp],0 //重定位
jmp func
call func
jmp label
//使用该模式时,需先解常量展开
push imm32
lea esp,[esp-4] | sub esp,4
mov [esp],reg32
mov reg32,imm32
xchg [esp],reg32
push imm32
push reg32
xchg reg32,Xreg32
sub esp,4 | lea esp,[esp-4]
mov [esp],Xreg32
mov Xreg32,reg32
mov reg32,[esp]
push reg32
mov reg32,imm32
push imm32
pop reg32
mov 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
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)