这个题的考点主要是两个:
1)patch 掉 SMC 和 反调试
2)算法爆破姿势
把感谢写在前面
1)感谢出题人非常耐心的解答。一开始是在OD里一直下断停不下来,修改的字节有些许问题,导致在关键处停不下来。然后又是OD停下来了之后,IDA动态停不下来,各种细节问题都是出题人帮忙解决的。实名感谢NCTF出题人。
2)感谢分享,算法爆破写得非常清楚,在此再做一些补充。其链接如下:
https://www.52pojie.cn/thread-1553124-1-1.html
1)部分分析:
1.1 如何 patch
拿到题 main 是这样的
[Asm] 纯文本查看 复制代码 int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [esp+0h] [ebp-68h]
int v5; // [esp+0h] [ebp-68h]
HANDLE hHandle; // [esp+8h] [ebp-60h]
char v7[80]; // [esp+14h] [ebp-54h] BYREF
memset(v7, 0, sizeof(v7));
sub_401510("Input:\n", v4);
sub_401550("%50s", (char)v7);
v5 = &v7[strlen(v7) + 1] - &v7[1];
if ( v5 != 42 )
{
sub_401510("Wrong!\n", v5);
exit(0);
}
hHandle = CreateThread(0, 0, StartAddress, 0, 0, 0);
WaitForSingleObject(hHandle, 0xFFFFFFFF);
CloseHandle(hHandle);
off_405014(v7);
return 0;
}
长度为 42 个字节,然后就进入了一个 off_的流程,off_ 好像是个地址
两个都分析一下,发现一个是403000里面是一些奇奇怪怪没有写完的代码,一个是假的flag
那我们当然要走到403000,奇奇怪怪一定是作者搞的,结合SMC,一定有解密的部分
[Asm] 纯文本查看 复制代码 void __stdcall TlsCallback_0(int a1, int a2, int a3)
{
BOOL (__stdcall *CheckRemoteDebuggerPresent)(HANDLE, PBOOL); // [esp+4h] [ebp-20h]
HANDLE v4; // [esp+8h] [ebp-1Ch]
HMODULE hModule; // [esp+Ch] [ebp-18h]
int j; // [esp+10h] [ebp-14h]
int i; // [esp+14h] [ebp-10h]
char *v8; // [esp+18h] [ebp-Ch]
int v9; // [esp+1Ch] [ebp-8h] BYREF
if ( a2 == 3 )
{
v9 = 0;
hModule = GetModuleHandleA("Kernel32");
CheckRemoteDebuggerPresent = (BOOL (__stdcall *)(HANDLE, PBOOL))GetProcAddress(
hModule,
"CheckRemoteDebuggerPresent");
v4 = GetCurrentProcess();
CheckRemoteDebuggerPresent(v4, &v9);
if ( !v9 && !IsDebuggerPresent() )
{
off_405014 = sub_403000;
v8 = (char *)sub_403000 + 256;
for ( i = 0; i < 24; ++i )
v8 += 8;
for ( j = 0; j < 24; ++j )
{
v8 -= 8;
sub_4011F0(v8);
}
}
}
}
可以看到,观察v8,和403000有关,而且是连续的,403000的数据被计算了(一定要想清楚,我们不需要搞懂是怎么加密怎么解密的,不重要,因为我们让程序自己解密完了之后,跳到已经解密之后的403000之后继续处理即可)
可以判断:403000是关键函数 且 回调函数中的反调试需要 patch 掉
这里选择把标红的6个字节修改成 nop,这样绕过反调试,可以继续到解密部分
这样处理还不够,因为还有这样一个函数
[Asm] 纯文本查看 复制代码 int sub_4011C0()
{
if ( NtCurrentPeb()->BeingDebugged )
exit(0);
return 0;
}
这个是线程的 PEB 的被调试指针,也要 patch 掉
这个作为对比参考。至此,patch 问题解决
1.2 如何得到正确的 403000 函数
patch成功后,可保存一份,在main起始处+403000处下断,F2运行起来即可
但会存在这样一个问题:函数识别不全
使用 u 将对应坐标处数据转换为IDA里的未知类型数据,再按快捷键C转换为代码,IDA就可以将他转换为汇编代码
这个时候,F5 是没有效果的,因为 403000 函数不完整,堆栈不平衡,但是可以看到正确完整的反汇编代码了。通过IDA 菜单栏上Edit - Function - Edit function 功能,将上方函数结尾修改到正确的结尾。将鼠标拖放到函数最后
[Asm] 纯文本查看 复制代码 .SMC:002C344B mov esp, ebp
.SMC:002C344D pop ebp
.SMC:002C344E retn
.SMC:002C344E sub_2C3000 endp
.SMC:002C344E
.SMC:002C344E ; ---------------------------------------------------------------------------
.SMC:002C344F align 200h
.SMC:002C3600 dd 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
这时候可以使用 F5 大法了
2)算法部分
[Asm] 纯文本查看 复制代码 void __cdecl sub_2C3000(const char *flag)
{
signed int v1; // [esp+0h] [ebp-98h]
unsigned int v2; // [esp+10h] [ebp-88h]
signed int v3; // [esp+1Ch] [ebp-7Ch]
int v4; // [esp+2Ch] [ebp-6Ch]
int v5; // [esp+2Ch] [ebp-6Ch]
char v6; // [esp+32h] [ebp-66h]
signed int Size; // [esp+34h] [ebp-64h]
unsigned int v8; // [esp+38h] [ebp-60h]
int k; // [esp+38h] [ebp-60h]
unsigned __int8 *v10; // [esp+3Ch] [ebp-5Ch]
int i; // [esp+40h] [ebp-58h]
signed int j; // [esp+40h] [ebp-58h]
signed int l; // [esp+40h] [ebp-58h]
signed int m; // [esp+40h] [ebp-58h]
signed int n; // [esp+40h] [ebp-58h]
char v16[62]; // [esp+44h] [ebp-54h]
int v17; // [esp+82h] [ebp-16h]
int v18; // [esp+86h] [ebp-12h]
int v19; // [esp+8Ah] [ebp-Eh]
int v20; // [esp+8Eh] [ebp-Ah]
__int16 v21; // [esp+92h] [ebp-6h]
v2 = strlen(flag);
Size = 0x92 * v2 / 0x64 + 1;
v3 = 0;
v10 = (unsigned __int8 *)malloc(Size);
v16[0] = 82;
v16[1] = -61;
v16[2] = 26;
v16[3] = -32;
v16[4] = 22;
v16[5] = 93;
v16[6] = 94;
v16[7] = -30;
v16[8] = 103;
v16[9] = 31;
v16[10] = 31;
v16[11] = 6;
v16[12] = 6;
v16[13] = 31;
v16[14] = 23;
v16[15] = 6;
v16[16] = 15;
v16[17] = -7;
v16[18] = 6;
v16[19] = 103;
v16[20] = 88;
v16[21] = -78;
v16[22] = -30;
v16[23] = -116;
v16[24] = 15;
v16[25] = 42;
v16[26] = 6;
v16[27] = -119;
v16[28] = -49;
v16[29] = 42;
v16[30] = 6;
v16[31] = 31;
v16[32] = -104;
v16[33] = 26;
v16[34] = 62;
v16[35] = 23;
v16[36] = 103;
v16[37] = 31;
v16[38] = -9;
v16[39] = 58;
v16[40] = 68;
v16[41] = -61;
v16[42] = 22;
v16[43] = 51;
v16[44] = 105;
v16[45] = 26;
v16[46] = 117;
v16[47] = 22;
v16[48] = 62;
v16[49] = 23;
v16[50] = -43;
v16[51] = 105;
v16[52] = 122;
v16[53] = 27;
v16[54] = 68;
v16[55] = 68;
v16[56] = 62;
v16[57] = 103;
v16[58] = -9;
v16[59] = -119;
v16[60] = 103;
v16[61] = -61;
v17 = 0;
v18 = 0;
v19 = 0;
v20 = 0;
v21 = 0;
memset(v10, 0, Size);
v8 = 0;
for ( i = 0; i < 256; ++i )
{
v6 = byte_2C5018[i];
byte_2C5018[i] = byte_2C5018[(i + *((unsigned __int8 *)&dword_2C5168 + i % 4)) % 256];
byte_2C5018[(i + *((unsigned __int8 *)&dword_2C5168 + i % 4)) % 256] = v6;
}
while ( v8 < strlen(flag) )
{
v4 = flag[v8];
for ( j = 0x92 * v2 / 0x64; ; --j )
{
v5 = v4 + (v10[j] << 8);
v10[j] = v5 % 47;
v4 = v5 / 47;
if ( j < v3 )
v3 = j;
if ( !v4 && j <= v3 )
break;
}
++v8;
}
for ( k = 0; !v10[k]; ++k )
;
for ( l = 0; l < Size; ++l )
v10[l] = byte_2C5118[v10[k++]];
while ( l < Size )
v10[l++] = 0;
v1 = strlen((const char *)v10);
for ( m = 0; m < v1; ++m )
v10[m] ^= byte_2C5018[v10[m]];
for ( n = 0; n < v1; ++n )
{
if ( v10[n] != (unsigned __int8)v16[n] )
{
sub_2C1510("Wrong!\n", v1);
exit(0);
}
}
sub_2C1510("Right!\n", v1);
}
一定要看清楚,我们的输入是什么时候参与运算的,在之前的数据,都可以下断,当成常数值
byte_2C5018 可以在 104 行 while 之前下断得到具体数据
dword_2C5168 和 v16 都未经过修改,都是常数数据
line 104 - line 118 是把输入从字符串转为 47 进制的过程
所以,line121 - 127 行的代码逆向起来很容易!我们正向没法算,因为数据太大。但是我们可以逐个位置爆破。
拿数据说话(具体数据要么自己逆向得,要么看感谢链接)
[Python] 纯文本查看 复制代码 for v17 in byte_v17:
for i in range(256):
c = byte_405018[i] ^ i
if c == v17:
# print(hex(i))
if i in byte_405118:
print(byte_405118.index(i), end=" ")
print("")
这个代码的意思是,我们枚举一个字节的所有值(00 - FF),每个字节可以正向算出来最终的需要的 v17 的结果,那么就是 47 进制中的可以用的字符
爆破完了之后,得到这样一个表格
[Asm] 纯文本查看 复制代码 2
0
33 45
44
30
40
8
23
22 11 7
37 34
37 34
19 20 43
19 20 43
37 34
24
19 20 43
31 4
29
19 20 43
22 11 7
13
5
23
41
31 4
35
19 20 43
9
14
35
19 20 43
37 34
3
33 45
10
24
22 11 7
37 34
38
1
25
0
30
6
42
33 45
36
30
10
24
21
42
26
28
25
25
10
22 11 7
38
9
22 11 7
这个表格也在 感谢2 的链接里有
那是怎么转化为最终的 flag 的呢?有的行有多个数,有的行只有一个数,为什么呢?
答案是:继续爆破 + 合理性
因为输入需要转为 47 进制,有的是不可输入字符,有的会让结果非常非常奇怪。而且!
一个数是有高低位的,当顺着顺序爆破的时候,如果对了,那字符串的前面会是非常漂亮的输出格式,如果错了,那输出的东西会非常不舒服。拿别人的代码举个例子:
第一个多选项是 33、45
跑出来的对比结果是这样的
[Asm] 纯文本查看 复制代码 b'NCTF{ADF0E239-D911-3781-7E40-A575A19E5835}'
b"N\rx^-\x80\xf7\xbf\xd1\xd3\xddu\x8dU\x06\xdcK\xd7\xcc\xe8\xfd\xec\x9aE\x83C\x05\xc0\x89'\xea\x05gxF\xcbW^\xd3\x14[\xf1"
这就说明,高位更重要,高位对了,就“好看”,高位错了,就“不好看”,所以就可以顺着一个个试出来。
有选项的(2个的、3个的)并不多,一会儿就整出来了。
完结撒花。
把附件上传一下:
attachment_1.exe 是 patch 后的
attachment_1.idb 是 patch 后的
attachment_1_original_oo.exe 是原题
gougou.zip
(95.9 KB, 下载次数: 10)
|