smc

wuuu,上次烽火杯的smc没写出来,这次的nepctf的smc也没写出了,所以打算总结一下smc,并且以后碰到smc都往这个blog里面放了。

简述一下原理:

在CTF逆向工程中,SMC(Self-Modifying Code,自修改代码) 是一种常见的反分析技术。其核心原理是程序在运行时动态修改自身的指令代码,使得静态分析工具(如IDA Pro)无法直接获取完整的可执行逻辑,从而增加逆向难度。

感觉smc根本就不难,动调一下基本都可以解决,但是难在有些反调试等方面感觉只单出smc的没见过。

话不多说直接上例题,我目前写过的smc有:NepCTF的realme,[网鼎杯 2020 青龙组]jocker,[SCTF2019]creakme,和烽火杯的smc

今天一天可能写不完四个,慢慢写吧,就按这个顺序来写。

NepCTF realme

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
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
char v4; // [esp+0h] [ebp-104h]
char v5; // [esp+0h] [ebp-104h]
unsigned int i; // [esp+D0h] [ebp-34h]
char v7[40]; // [esp+DCh] [ebp-28h] BYREF

__CheckForDebuggerJustMyCode(&unk_41200F);
qmemcpy(v7, "PY", 2);
v7[2] = -94;
v7[3] = -108;
v7[4] = 46;
v7[5] = -114;
v7[6] = 92;
v7[7] = -107;
v7[8] = 121;
v7[9] = 22;
v7[10] = -27;
v7[11] = 54;
v7[12] = 96;
v7[13] = -57;
v7[14] = -24;
v7[15] = 6;
v7[16] = 51;
v7[17] = 120;
v7[18] = -16;
v7[19] = -48;
v7[20] = 54;
v7[21] = -56;
v7[22] = 115;
v7[23] = 27;
v7[24] = 101;
v7[25] = 64;
v7[26] = -75;
v7[27] = -44;
v7[28] = -24;
v7[29] = -100;
v7[30] = 101;
v7[31] = -12;
v7[32] = -70;
v7[33] = 98;
v7[34] = -48;
sub_40108C("Please input the flag:\n", v4);
sub_4011C2("%s", byte_410158);
sub_401050(byte_410158, Str); //魔改RC4 KSA多了一个^0x66,最后的和秘钥流的异或变成了模取
for ( i = 0; i < 0x23; ++i )
{
if ( byte_410158[i] != v7[i] )
{
sub_40108C("Wrong flag!\n", v5);
return 0;
}
}
sub_40108C("Correct flag!\n", v5);
return 0;
}

这个rc4的魔改解出来是个错的,就不详细解释了。

进入正文,反思一下我这里没解出来主要是没找到反调试的地方,因为IDA没把他识别成函数,藏在汇编里面。

要自己去翻汇编代码,给他P重定义一下

1

2

只能硬找莫得办法,重定义后Tab

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// write access to const memory has been detected, the output may be wrong!
void *sub_401500()
{
void *result; // eax

result = (void *)(NtCurrentPeb()->NtGlobalFlag & 0x70);
if ( !result )
{
*(&loc_40902A + 1) ^= 0x65u;
*((_BYTE *)&loc_40902A + 2) ^= 0xFAu;
*(&loc_407080 + 2) ^= 5u;
result = &loc_40B077;
loc_40B077 ^= 0x10u;
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// write access to const memory has been detected, the output may be wrong!
void *__cdecl sub_4015C0(int a1, char a2)
{
void *result; // eax

sub_401023("%s", a2);
if ( CloseHandle((HANDLE)0x1234) || GetLastError() != 6 )
return 0;
*(&loc_407080 + 1) ^= 0xD0u;
*(&loc_40B104 + 2) ^= 0xCBu;
loc_40B078 ^= 0x61u;
result = &loc_40B079;
loc_40B079 ^= 0x5Fu;
return result;
}

很明显的反调试和smc了

smc没什么好讲的得会动调就全部解决了,这里要解决反调试

第一个反调试我们要把(!result)改成(result)。

第二个反调试我要把CloseHandle((HANDLE)0x1234) || GetLastError() != 6的CloseHandle((HANDLE)0x1234) nop掉,不然动调会报错。

还要把if的条件反过来。

1
.text:0040153F                 jz      short loc_401543  --> jnz      short loc_401543
1
2
.text:004015EE                 push    1234h           ; hObject        -->nop
.text:004015F3 call ds:__imp_CloseHandle -->nop
1
2
3
4
5
6
7
.text:00401607                 jnz     short loc_40161D                jnz-->jz
.text:00401609 mov esi, esp
.text:0040160B call ds:__imp_GetLastError
.text:00401611 cmp esi, esp
.text:00401613 call j___RTC_CheckEsp
.text:00401618 cmp eax, 6
.text:0040161B jz short loc_401635 jz--->jnz

如果不会改可以上网查一下IDA怎么patch。

改完之后我们就可以看到真实的加密逻辑了

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
void *__cdecl sub_40B000(int a1, int a2, unsigned int a3)
{
void *result; // eax
char v4; // [esp+D3h] [ebp-129h]
char v5[264]; // [esp+DCh] [ebp-120h] BYREF
int v6; // [esp+1E4h] [ebp-18h]
int i; // [esp+1F0h] [ebp-Ch]

v6 = 0;
result = memset(v5, 0, 0x100u);
for ( i = 0; i < 256; ++i )
{
*(_BYTE *)(i + a1) = i ^ 0xCF;
v5[i] = *(_BYTE *)(a2 + i % a3);
result = (void *)(i + 1);
}
for ( i = 0; i < 256; ++i )
{
v6 = ((unsigned __int8)v5[i] + v6 + *(unsigned __int8 *)(i + a1)) % 256;
v4 = *(_BYTE *)(i + a1);
*(_BYTE *)(i + a1) = *(_BYTE *)(v6 + a1);
*(_BYTE *)(v6 + a1) = v4 ^ 0xAD;
result = (void *)(i + 1);
}
return result;
}
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
unsigned int __cdecl sub_401A60(int a1, int a2, unsigned int a3)
{
unsigned int result; // eax
int v4; // ecx
char v5; // al
char v6; // [esp+D3h] [ebp-35h]
unsigned int i; // [esp+DCh] [ebp-2Ch]
int v8; // [esp+F4h] [ebp-14h]
int v9; // [esp+100h] [ebp-8h]

__CheckForDebuggerJustMyCode(&unk_41200F);
v9 = 0;
v8 = 0;
for ( i = 0; ; ++i )
{
result = i;
if ( i >= a3 )
break;
v9 = (v9 + 1) % 256;
v8 = (v8 + v9 * *(unsigned __int8 *)(v9 + a1)) % 256;
v6 = *(_BYTE *)(v9 + a1);
*(_BYTE *)(v9 + a1) = *(_BYTE *)(v8 + a1);
*(_BYTE *)(v8 + a1) = v6;
v4 = (*(unsigned __int8 *)(v8 + a1) + *(unsigned __int8 *)(v9 + a1)) % 256;
if ( i % 2 )
v5 = *(_BYTE *)(v4 + a1) + *(_BYTE *)(i + a2);
else
v5 = *(_BYTE *)(i + a2) - *(_BYTE *)(v4 + a1);
*(_BYTE *)(i + a2) = v5;
}
return result;
}

解密脚本

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
# 密文(从main函数的v7数组转换为无符号字节)
ciphertext = [
0x50, 0x59, 0xA2, 0x94, 0x2E, 0x8E, 0x5C, 0x95, 0x79, 0x16,
0xE5, 0x36, 0x60, 0xC7, 0xE8, 0x06, 0x33, 0x78, 0xF0, 0xD0,
0x36, 0xC8, 0x73, 0x1B, 0x65, 0x40, 0xB5, 0xD4, 0xE8, 0x9C,
0x65, 0xF4, 0xBA, 0x62, 0xD0
]

# 密钥
key = b"Y0u_Can't_F1nd_Me!"
key_len = len(key)

# 初始化S盒(sub_40B000的第一个循环)
S = [i ^ 0xCF for i in range(256)]

# 准备v5数组(密钥扩展)
v5 = [key[i % key_len] for i in range(256)]

# sub_40B000的第二个循环:置换S盒
v6 = 0
for i in range(256):
v6 = (v5[i] + v6 + S[i]) % 256
v4 = S[i]
S[i] = S[v6]
S[v6] = v4 ^ 0xAD # 注意这里的异或操作

# 执行PRGA并解密(模拟sub_401A60的逆向过程)
v9 = 0
v8 = 0
plaintext_bytes = []

for i in range(len(ciphertext)):
# 更新v9和v8
v9 = (v9 + 1) % 256
v8 = (v8 + v9 * S[v9]) % 256 # 关键的v8计算

# 交换S[v9]和S[v8]
temp = S[v9]
S[v9] = S[v8]
S[v8] = temp

# 计算密钥流字节k
v4 = (S[v8] + S[v9]) % 256
k = S[v4]

# 根据索引奇偶性解密
if i % 2 == 1: # 奇数索引:密文 = 明文 + k → 明文 = 密文 - k
plain_byte = (ciphertext[i] - k) % 256
else: # 偶数索引:密文 = 明文 - k → 明文 = 密文 + k
plain_byte = (ciphertext[i] + k) % 256

plaintext_bytes.append(plain_byte)

# 转换为字符串并输出
plaintext = bytes(plaintext_bytes).decode('ascii')
print("解密得到的flag:", plaintext)
1
NepCTF{Y0u_FiN1sH_Th1s_E3sy_Smc!!!}

[网鼎杯 2020 青龙组]jocker

链接

[SCTF2019]creakme

简述一下这里主要是反调试+smc+AES的CBC

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
117
118
119
120
121
122
123
124
125
int __cdecl main(int argc, const char **argv, const char **envp)
{
HMODULE ModuleHandleW; // eax
int v4; // eax
_DWORD *v5; // eax
unsigned int v6; // edx
_DWORD *v7; // ecx
unsigned int v8; // ebx
char *v9; // edi
unsigned int v10; // esi
unsigned int v11; // esi
bool v12; // cf
unsigned __int8 v13; // al
unsigned __int8 v14; // al
unsigned __int8 v15; // al
int v16; // esi
void *v17; // ecx
void *v18; // ecx
const char *v19; // edx
int v20; // eax
int v22; // [esp+0h] [ebp-80h]
void *Block[5]; // [esp+10h] [ebp-70h] BYREF
unsigned int v24; // [esp+24h] [ebp-5Ch]
void *v25[5]; // [esp+28h] [ebp-58h] BYREF
unsigned int v26; // [esp+3Ch] [ebp-44h]
char Src[48]; // [esp+40h] [ebp-40h] BYREF
int v28; // [esp+7Ch] [ebp-4h]

ModuleHandleW = GetModuleHandleW(0);
sub_402320(ModuleHandleW);
sub_4024A0();
v4 = sub_402870(std::cout, "welcome to 2019 sctf");
std::ostream::operator<<(v4, sub_402AC0);
sub_402870(std::cout, "please input your ticket:");
sub_402AF0(v22);
v25[4] = 0;
v26 = 15;
LOBYTE(v25[0]) = 0;
sub_401D30(v25, Src, strlen(Src));
v28 = 0;
v5 = sub_4020D0(Block, (int)v25); // AES的CBC模式
v6 = strlen(aPvfqyc4ttc2uxr);
v7 = v5;
if ( v5[5] >= 0x10u )
v7 = (_DWORD *)*v5;
v8 = v5[4];
v9 = aPvfqyc4ttc2uxr;
v10 = v8;
if ( v6 < v8 )
v10 = v6;
v12 = v10 < 4;
v11 = v10 - 4;
if ( v12 )
{
LABEL_8:
if ( v11 == -4 )
goto LABEL_17;
}
else
{
while ( *v7 == *(_DWORD *)v9 )
{
++v7;
v9 += 4;
v12 = v11 < 4;
v11 -= 4;
if ( v12 )
goto LABEL_8;
}
}
v12 = *(_BYTE *)v7 < (unsigned __int8)*v9;
if ( *(_BYTE *)v7 != *v9
|| v11 != -3
&& ((v13 = *((_BYTE *)v7 + 1), v12 = v13 < (unsigned __int8)v9[1], v13 != v9[1])
|| v11 != -2
&& ((v14 = *((_BYTE *)v7 + 2), v12 = v14 < (unsigned __int8)v9[2], v14 != v9[2])
|| v11 != -1 && (v15 = *((_BYTE *)v7 + 3), v12 = v15 < (unsigned __int8)v9[3], v15 != v9[3]))) )
{
v16 = v12 ? -1 : 1;
goto LABEL_18;
}
LABEL_17:
v16 = 0;
LABEL_18:
if ( !v16 )
{
if ( v6 <= v8 )
v16 = v6 < v8;
else
v16 = -1;
}
if ( v24 >= 0x10 )
{
v17 = Block[0];
if ( v24 + 1 >= 0x1000 )
{
v17 = (void *)*((_DWORD *)Block[0] - 1);
if ( (unsigned int)(Block[0] - v17 - 4) > 0x1F )
invalid_parameter_noinfo_noreturn();
}
sub_402F05(v17);
}
v28 = -1;
Block[4] = 0;
v24 = 15;
LOBYTE(Block[0]) = 0;
if ( v26 >= 0x10 )
{
v18 = v25[0];
if ( v26 + 1 >= 0x1000 )
{
v18 = (void *)*((_DWORD *)v25[0] - 1);
if ( (unsigned int)(v25[0] - v18 - 4) > 0x1F )
invalid_parameter_noinfo_noreturn();
}
sub_402F05(v18);
}
v19 = "Have fun!";
if ( v16 )
v19 = "A forged ticket!!";
v20 = sub_402870(std::cout, v19);
std::ostream::operator<<(v20, sub_402AC0);
system("pause");
return 0;
}

AES的CBC怎么看出来的,emmm,我暂时没深入了解这个加密,所以问ai的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void __thiscall sub_402320(_DWORD *this)
{
int v1; // eax
__int16 v2; // bx
const char *v3; // esi
int i; // edi
int v5; // eax

v1 = this[15];
v2 = *(_WORD *)((char *)this + v1 + 6);
v3 = (char *)this + v1 + 248;
for ( i = 0; i < v2; ++i )
{
v5 = strcmp(v3, ".SCTF");
if ( v5 )
v5 = v5 < 0 ? -1 : 1;
if ( !v5 )
{
DebugBreak();
return;
}
v3 += 40;
}
}
1
2
3
4
5
6
7
8
9
int sub_4024A0()
{
unsigned int NtGlobalFlag; // [esp+10h] [ebp-20h]

NtGlobalFlag = NtCurrentPeb()->NtGlobalFlag;
if ( NtCurrentPeb()->BeingDebugged || NtGlobalFlag == 112 )
exit(-5);
return ((int (*)(void))dword_404000[0])();
}

这两个是反调试,这个比NepCTF好多了。至少反调试容易找到wuuuuu~~~~

和nep一样直接patch掉反调试,当然你也可以动调的时候改变ZF来改变线程的走向。

绕过反调试,但是到了call就会报错404000。

这里参考大佬的用IDA的python脚本来手动还原

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
int __fastcall sub_402450(int a1, int a2, int a3, int a4)
{
int result; // eax
int v7; // edx
char v8; // cl

result = 0;
if ( a2 > 0 )
{
while ( 1 )
{
v7 = 0;
if ( a4 > 0 )
break;
LABEL_5:
if ( result >= a2 )
return result;
}
while ( result < a2 )
{
v8 = aSycloversyclov[v7++];
*(_BYTE *)(result + a1) = ~(*(_BYTE *)(result + a1) ^ v8);
++result;
if ( v7 >= a4 )
goto LABEL_5;
}
}
return result;
}

找到smc的地方,用脚本来还原

1
2
3
4
5
6
7
8
add1=0x404000
add2=0x405000
key="sycloversyclover"
bb=0
for i in range(add1,add2,1):
wr=(~(idc.get_wide_byte(i) ^ ord(key[bb%len(key)]))&0xff) #这里&0xff是避免取反时高位补一
ida_bytes.patch_byte(i,wr)
bb+=1
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
unsigned int sub_404000()
{
unsigned int i; // edx
unsigned int v1; // esi
unsigned int result; // eax
unsigned int v3; // eax
char v4; // dl

for ( i = 0; i < strlen(aPvfqyc4ttc2uxr); ++i )
--aPvfqyc4ttc2uxr[i];
v1 = 0;
result = strlen(aPvfqyc4ttc2uxr);
if ( (result & 0xFFFFFFFE) != 0 )
{
do
{
v3 = result - v1;
v4 = *(_BYTE *)(v3 + 4231191);
*(_BYTE *)(v3 + 4231191) = aPvfqyc4ttc2uxr[v1];
aPvfqyc4ttc2uxr[v1++] = v4;
result = strlen(aPvfqyc4ttc2uxr);
}
while ( v1 < result >> 1 );
}
return result;
}

解密得到nKnbHsgqD3aNEB91jB3gEzAr+IklQwT1bSs3+bXpeuo=

最后秘钥为sycloversyclover,偏移量为sctfsctfsctfsctf

1
sctf{Ae3_C8c_I28_pKcs79ad4}

烽火杯的smc

这个算是最简单的反调试改jz就可以,直接linux动调就可以还原了,就不都说了xixi。