PE文件结构//

自修改代码

Self-Modifying Code

自修改代码(Self-Modifying Code),指在一段代码执行前对它进行修改。把代码以加密的形式保存在可执行文件中(或静态资源中),然后在程序执行的时候进行动态解密。这样我们在采用静态分析时,看到的都是加密的内容,从而减缓甚至阻止静态分析。

原理与示例

SMC思路:

1
2
3
4
5
6
7
if (运行条件满足) {
DecryptProc(Address of Check) // 对 Check 代码解密
// ........
Check(); // 调用 Check
// ........
EncryptProc(Address of Check) // 再对代码进行加密,防止程序被 dump
}

SMC静态分析对抗示例:

正常程序:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int check(int in) {
return in == 12345;
}
int main() {
int input;
scanf("%d", &input);
if (check(input))
printf("good!");
}

在 VS 中 Release 32 位下编译(关闭随机基址),放入 IDA 中查看:

image-20240122225317272

1
2
3
4
5
6
7
unsigned char _check__YAHH_Z[] =
{
0x55, 0x8B, 0xEC, 0x51, 0x81, 0x7D, 0x08, 0x39, 0x30, 0x00,
0x00, 0x75, 0x09, 0xC7, 0x45, 0xFC, 0x01, 0x00, 0x00, 0x00,
0xEB, 0x07, 0xC7, 0x45, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x8B,
0x45, 0xFC, 0x8B, 0xE5, 0x5D, 0xC3
};

现在我们想把 check 函数保护起来,先把其机器码摘出来进行加密:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
unsigned char check[] =
{
0x55, 0x8B, 0xEC, 0x33, 0xC0, 0x81, 0x7D, 0x08, 0x39, 0x30,
0x00, 0x00, 0x0F, 0x94, 0xC0, 0x5D, 0xC3
};
int main() {
for (int i = 0; i < sizeof(check); i++)
{
check[i] ^= 0x90;
printf("%02x ", check[i]);
}
}

加密后:

1
c5 1b 7c c1 11 ed 98 a9 a0 90 90 e5 99 57 d5 6c 91 90 90 90 7b 97 57 d5 6c 90 90 90 90 1b d5 6c 1b 75 cd 53

修改初始代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <windows.h>
int check(int in) {
return in == 12345;
}
// 运行时解密 check 函数
void decrypt() {
DWORD old;
VirtualProtect(check, 4096, PAGE_EXECUTE_READWRITE, &old); // 修改内存页权限
for (int i = 0; i < 17; i++)
{
*((char*)check + i) ^= 0x90;
}
VirtualProtect(check, 4096, old, NULL);
}
int main() {
int input;
scanf("%d", &input);
decrypt(); // 新增
if (check(input))
printf("good!");
}

重新编译后,在 16 进制编辑器中找到并修改 check 函数的机器码为加密后的数据

一个具备 SMC 属性的可执行文件已经构造完成了,现在再用 IDA 进行分析:

发现原始逻辑已经被很好地隐藏了。

另一种常见的实现方法是通过新增一个具备 RWX 属性的程序段,将需要保护的代码书写在其中,这样就可以避免调用 VirtualAlloc / VirtualProtect / mprotect 这类 API 来暴露 SMC 的意图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

#include <stdio.h>
#pragma code_seg(".qaq")
int check(int in) {
return in == 12345;
}
#pragma code_seg()
#pragma comment(linker, "/SECTION:.qaq,ERW")
// 运行时解密 check 函数
void decrypt() {
for (int i = 0; i < 17; i++)
*((char*)check + i) ^= 0x90;
}
int main() {
int input;
scanf("%d", &input);
decrypt();
if (check(input))
printf("good!");
}

可以在 IDA 中看到新增了一个段:

image-20240122235013392

对抗思路

能动态调试最好直接动态调试,因为在程序运行的某一时刻,它一定是解密完成的,这时也就暴露了,使用动态分析运行到这一时刻即可过掉保护

其次是根据静态分析获得解密算法,写出解密脚本提前解密这段代码。

解密得到的机器码可以通过 IDAPython 的 patch_byte 接口很方便地写回。