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; char v5; unsigned int i; char v7[40];
__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); 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重定义一下


只能硬找莫得办法,重定义后Tab
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void *sub_401500() { void *result;
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
| void *__cdecl sub_4015C0(int a1, char a2) { void *result;
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; char v4; char v5[264]; int v6; int i;
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; int v4; char v5; char v6; unsigned int i; int v8; int v9;
__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
| 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 = [i ^ 0xCF for i in range(256)]
v5 = [key[i % key_len] for i in range(256)]
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
v9 = 0 v8 = 0 plaintext_bytes = []
for i in range(len(ciphertext)): v9 = (v9 + 1) % 256 v8 = (v8 + v9 * S[v9]) % 256
temp = S[v9] S[v9] = S[v8] S[v8] = temp
v4 = (S[v8] + S[v9]) % 256 k = S[v4]
if i % 2 == 1: plain_byte = (ciphertext[i] - k) % 256 else: 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; int v4; _DWORD *v5; unsigned int v6; _DWORD *v7; unsigned int v8; char *v9; unsigned int v10; unsigned int v11; bool v12; unsigned __int8 v13; unsigned __int8 v14; unsigned __int8 v15; int v16; void *v17; void *v18; const char *v19; int v20; int v22; void *Block[5]; unsigned int v24; void *v25[5]; unsigned int v26; char Src[48]; int v28;
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); 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; __int16 v2; const char *v3; int i; int v5;
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;
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; int v7; char v8;
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) 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; unsigned int v1; unsigned int result; unsigned int v3; char v4;
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。