自修改代码Self-Modified Code(SMC)

摘要

自修改代码Self-Modified Code(SMC)

是一类特殊的代码技术,即在运行时修改自身代码,从而使得程序实际行为与反汇编结果不符,同时修改前的代码段数据也可能非合法指令,从而无法被反汇编器识

解法:

根据静态分析结果直接修改程序二进制文件

在动态调试时将解密后的程序从内存中 dump 下来。

SMC出题&题解

明明还不懂就要出题了(

出题

这次出题有一个重大发现….原来smc是先生成未加密的二进制,然后另外用脚本去加密的。思路打开了

challenge.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>

/* * 禁用优化并阻止内联,确保函数机器码原汁原味并且在 ELF 中顺序固定。
*/
void stage2_end(void) __attribute__((optimize("O0")));
int encrypt_stage2(char *half_flag) __attribute__((noinline, optimize("O0")));

void stage1_end(void) __attribute__((optimize("O0")));
int encrypt_stage1(char *a1) __attribute__((noinline, optimize("O0")));

int encrypt_stage2(char *half_flag)
{
// 目标后半段明文: "v3ry_c00l}"
// 运算规则: ~(half_flag[i] ^ 0x42)
char target[10] = {
(char)(~('v' ^ 0x42)), (char)(~('3' ^ 0x42)),
(char)(~('r' ^ 0x42)), (char)(~('y' ^ 0x42)),
(char)(~('_' ^ 0x42)), (char)(~('c' ^ 0x42)),
(char)(~('0' ^ 0x42)), (char)(~('0' ^ 0x42)),
(char)(~('l' ^ 0x42)), (char)(~('}' ^ 0x42))};
int i;

for (i = 0; i < 10; ++i)
{
if ((char)(~half_flag[i] ^ 0x42) != target[i])
return 0;
}
return 1;
}
void stage2_end(void) { asm("nop"); } // 填充汇编指令防止被编译器优化掉

int encrypt_stage1(char *a1)
{
// 前 16 字节验证: "flag{SMC_n3st3d_"
char expected[17] = "flag{SMC_n3st3d_";
int i, j;
int pagesize;
void *aligned_addr;
unsigned int stage2_size;

for (i = 0; i < 16; ++i)
{
if (a1[i] != expected[i])
return 0;
}

// 第二层 SMC 解密准备
pagesize = sysconf(_SC_PAGESIZE);
aligned_addr = (void *)((unsigned int)encrypt_stage2 & ~(pagesize - 1));

// 赋予 RWX 权限
mprotect(aligned_addr, pagesize * 2, PROT_READ | PROT_WRITE | PROT_EXEC);

// 动态计算 stage2 函数体的大小
stage2_size = (unsigned int)stage2_end - (unsigned int)encrypt_stage2;

for (j = 0; j < stage2_size; ++j)
{
// 用输入的前 4 个字节 ("flag") 作为 rolling key 动态解密
*((unsigned char *)encrypt_stage2 + j) ^= a1[j % 4];
}

return encrypt_stage2(a1 + 16); // 将剩余的 10 字节传给第二阶段
}
void stage1_end(void) { asm("nop"); }

// ================= MAIN =================
int main(int argc, char **argv)
{
char Str[60];
size_t v4;
int i;
int pagesize;
void *aligned_addr;
unsigned int stage1_size;

puts("Please input your flag:");
if (scanf("%50s", Str) != 1)
exit(1);

v4 = strlen(Str);
if (v4 != 26)
{
puts("Wrong length!");
exit(0);
}

// 第一层 SMC 解密准备
pagesize = sysconf(_SC_PAGESIZE);
aligned_addr = (void *)((unsigned int)encrypt_stage1 & ~(pagesize - 1));

if (mprotect(aligned_addr, pagesize * 2, PROT_READ | PROT_WRITE | PROT_EXEC) != 0)
{
perror("mprotect failed");
exit(1);
}

stage1_size = (unsigned int)stage1_end - (unsigned int)encrypt_stage1;

for (i = 0; i < stage1_size; ++i)
{
// 用硬编码的 0x33 解密第一层
*((unsigned char *)encrypt_stage1 + i) ^= 0x33;
}

if (encrypt_stage1(Str))
puts("Congratulations! You got the real flag!");
else
puts("Wrong! Try again.");

return 0;
}

build.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from pwn import *

# 1. 加载未加密的原始 ELF
elf = ELF('./challenge_raw')

# 2. 获取函数在文件中的地址和大小
stage1_addr = elf.symbols['encrypt_stage1']
stage1_end = elf.symbols['stage1_end']
stage1_size = stage1_end - stage1_addr

stage2_addr = elf.symbols['encrypt_stage2']
stage2_end = elf.symbols['stage2_end']
stage2_size = stage2_end - stage2_addr

print(f"[*] Stage 1 Size: {stage1_size} bytes")
print(f"[*] Stage 2 Size: {stage2_size} bytes")

# 3. 读取明文机器码
stage1_code = bytearray(elf.read(stage1_addr, stage1_size))
stage2_code = bytearray(elf.read(stage2_addr, stage2_size))

# 4. 加密 Stage 2 (Key 是 "flag")
key = b"flag"
for i in range(stage2_size):
stage2_code[i] ^= key[i % 4]

# 将加密后的 Stage 2 覆盖回 ELF
elf.write(stage2_addr, bytes(stage2_code))

# 5. 加密 Stage 1 (Key 是 0x33)
for i in range(stage1_size):
stage1_code[i] ^= 0x33

# 将加密后的 Stage 1 覆盖回 ELF
elf.write(stage1_addr, bytes(stage1_code))

# 6. 保存最终的 Challenge ELF 并在文件系统赋权
elf.save('./smc_revenge')
os.chmod('./smc_revenge', 0o755)
print("[+] SMC Challenge successfully built: ./smc_revenge")

编译

1
2
3
4
5
6
//32位的,可能需要
sudo apt install gcc-multilib
//编译和加密
gcc -m32 -O0 challenge.c -o challenge_raw
python3 build.py
strip ./smc_revenge

题解

SMC解题的两种方法。如果加密比较复杂,就不如直接使用gdb查看解密后的代码了。不过这种可能就会加上点反调试手段

这就是解题的博弈吗

IDA脚本解密

main里能看到stage1的加密逻辑。异或0x33

1
2
for ( i = 0; (char *)stage1_end - (char *)encrypt_stage1 > i; ++i )
*((_BYTE *)encrypt_stage1 + i) ^= 0x33u;

解密stage1的ida python脚本:

1
2
3
4
5
6
7
start = 0x000012A4  # encrypt_stage1 的起始地址
end = 0x000013DD # stage1_end 的地址

for i in range(start, end):
patch_byte(i, get_wide_byte(i) ^ 0x33)

print("Decryption done!")

得到stage1,得到半段flag:flag{SMC_n3st3d_,以及stage2的加密逻辑,以flag为key进行异或

解密stage2

1
2
3
4
5
6
7
8
9
10
11
start = 0x0000120D  # encrypt_stage2 的起始地址
end = 0x00001293 # 下一个函数的起始地址

key = "flag"
for i in range(end - start):
addr = start + i
byte = get_wide_byte(addr)
# 异或 flag 的对应字符
patch_byte(addr, byte ^ ord(key[i % 4]))

print("Stage 2 Decrypted!")

gdb

在call stage1那一行下断点。因为开了PIE所以要加偏移

1
2
pwndbg> breakrva 0x152C
Breakpoint 2 at 0x5655652c

因为有判断长度的逻辑,运行后输入一个长度正确的字符串,不然不会跑到解密去。到断点之后查看指令是:

1
0x5655652c    call   0x565562a4                  <0x565562a4>

0x565562a4就是stage1的地址了。用x/i指令查看stage1的汇编,可以直接分析也可以patch到IDA里反编译。得到半段flag:flag{SMC_n3st3d_,并且发现需要以输入的前四位为key解密stage2,所以要求输入一个flag开头的长度正确的值,再次运行。

后面就不说了,就是停在stage2再查看汇编

[网鼎杯 2020 青龙组]jocker

题目链接:https://buuoj.cn/challenges#[%E7%BD%91%E9%BC%8E%E6%9D%AF%202020%20%E9%9D%92%E9%BE%99%E7%BB%84]jocker