首页
社区
课程
招聘
[原创]代码混淆之我见(四)
发表于: 2019-8-8 22:27 13128

[原创]代码混淆之我见(四)

2019-8-8 22:27
13128
传送门:

我们以 @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)常量展开

  • 初探
.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期)

收藏
免费 4
支持
分享
打赏 + 5.00雪花
打赏次数 1 雪花 + 5.00
 
赞赏  上海刘一刀   +5.00 2019/08/17 优秀
最新回复 (5)
雪    币: 2348
活跃值: (127)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
2
顶, 分析的非常透彻了
2019-8-8 22:32
0
雪    币: 2630
活跃值: (5078)
能力值: ( LV9,RANK:225 )
在线值:
发帖
回帖
粉丝
3
补附件……
上传的附件:
2019-8-8 22:32
0
雪    币: 1431
活跃值: (4413)
能力值: ( LV9,RANK:220 )
在线值:
发帖
回帖
粉丝
4
学习了,我最近也在看这部分的知识。
2019-8-10 14:22
0
雪    币: 2630
活跃值: (5078)
能力值: ( LV9,RANK:225 )
在线值:
发帖
回帖
粉丝
5
黑手鱼 学习了,我最近也在看这部分的知识。
加入wszzy的群一起交流。
2019-8-10 16:02
0
雪    币: 782
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
厉害了,大佬
2019-11-2 08:50
0
游客
登录 | 注册 方可回帖
返回
//