dreamingctf 发表于 2021-12-6 00:22

NCTF2021 - re - 狗狗的秘密

这个题的考点主要是两个:
1)patch 掉 SMC 和 反调试
2)算法爆破姿势

把感谢写在前面
1)感谢出题人非常耐心的解答。一开始是在OD里一直下断停不下来,修改的字节有些许问题,导致在关键处停不下来。然后又是OD停下来了之后,IDA动态停不下来,各种细节问题都是出题人帮忙解决的。实名感谢NCTF出题人。
2)感谢分享,算法爆破写得非常清楚,在此再做一些补充。其链接如下:
https://www.52pojie.cn/thread-1553124-1-1.html

1)部分分析:
1.1 如何 patch

拿到题 main 是这样的
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; //
int v5; //
HANDLE hHandle; //
char v7; // BYREF

memset(v7, 0, sizeof(v7));
sub_401510("Input:\n", v4);
sub_401550("%50s", (char)v7);
v5 = &v7 - &v7;
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,一定有解密的部分
void __stdcall TlsCallback_0(int a1, int a2, int a3)
{
BOOL (__stdcall *CheckRemoteDebuggerPresent)(HANDLE, PBOOL); //
HANDLE v4; //
HMODULE hModule; //
int j; //
int i; //
char *v8; //
int v9; // 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,这样绕过反调试,可以继续到解密部分

这样处理还不够,因为还有这样一个函数
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 功能,将上方函数结尾修改到正确的结尾。将鼠标拖放到函数最后
.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)算法部分
void __cdecl sub_2C3000(const char *flag)
{
signed int v1; //
unsigned int v2; //
signed int v3; //
int v4; //
int v5; //
char v6; //
signed int Size; //
unsigned int v8; //
int k; //
unsigned __int8 *v10; //
int i; //
signed int j; //
signed int l; //
signed int m; //
signed int n; //
char v16; //
int v17; //
int v18; //
int v19; //
int v20; //
__int16 v21; //

v2 = strlen(flag);
Size = 0x92 * v2 / 0x64 + 1;
v3 = 0;
v10 = (unsigned __int8 *)malloc(Size);
v16 = 82;
v16 = -61;
v16 = 26;
v16 = -32;
v16 = 22;
v16 = 93;
v16 = 94;
v16 = -30;
v16 = 103;
v16 = 31;
v16 = 31;
v16 = 6;
v16 = 6;
v16 = 31;
v16 = 23;
v16 = 6;
v16 = 15;
v16 = -7;
v16 = 6;
v16 = 103;
v16 = 88;
v16 = -78;
v16 = -30;
v16 = -116;
v16 = 15;
v16 = 42;
v16 = 6;
v16 = -119;
v16 = -49;
v16 = 42;
v16 = 6;
v16 = 31;
v16 = -104;
v16 = 26;
v16 = 62;
v16 = 23;
v16 = 103;
v16 = 31;
v16 = -9;
v16 = 58;
v16 = 68;
v16 = -61;
v16 = 22;
v16 = 51;
v16 = 105;
v16 = 26;
v16 = 117;
v16 = 22;
v16 = 62;
v16 = 23;
v16 = -43;
v16 = 105;
v16 = 122;
v16 = 27;
v16 = 68;
v16 = 68;
v16 = 62;
v16 = 103;
v16 = -9;
v16 = -119;
v16 = 103;
v16 = -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;
    byte_2C5018 = 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;
    for ( j = 0x92 * v2 / 0x64; ; --j )
    {
      v5 = v4 + (v10 << 8);
      v10 = v5 % 47;
      v4 = v5 / 47;
      if ( j < v3 )
      v3 = j;
      if ( !v4 && j <= v3 )
      break;
    }
    ++v8;
}
for ( k = 0; !v10; ++k )
    ;
for ( l = 0; l < Size; ++l )
    v10 = byte_2C5118];
while ( l < Size )
    v10 = 0;
v1 = strlen((const char *)v10);
for ( m = 0; m < v1; ++m )
    v10 ^= byte_2C5018];
for ( n = 0; n < v1; ++n )
{
    if ( v10 != (unsigned __int8)v16 )
    {
      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 行的代码逆向起来很容易!我们正向没法算,因为数据太大。但是我们可以逐个位置爆破。
拿数据说话(具体数据要么自己逆向得,要么看感谢链接)
for v17 in byte_v17:
    for i in range(256):
      c = byte_405018 ^ i
      if c == v17:
            # print(hex(i))
            if i in byte_405118:
                print(byte_405118.index(i), end=" ")
    print("")
这个代码的意思是,我们枚举一个字节的所有值(00 - FF),每个字节可以正向算出来最终的需要的 v17 的结果,那么就是 47 进制中的可以用的字符
爆破完了之后,得到这样一个表格
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

跑出来的对比结果是这样的
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 是原题

alongzhenggang 发表于 2021-12-6 23:51

我是狗子,祝你幸福(^o^)o

。。。。。。。。某大佬

lhl0235 发表于 2021-12-7 09:25

研究研究

ctf101 发表于 2021-12-8 10:03

正在学习CTF逆向,学习一下思路{:301_998:}

Hmily 发表于 2021-12-9 15:27

@dreamingctf 凤毛麟角是用户组别,不是账号名称,他的账号名称在左侧:zsky。

dreamingctf 发表于 2021-12-10 19:30

Hmily 发表于 2021-12-9 15:27
@dreamingctf 凤毛麟角是用户组别,不是账号名称,他的账号名称在左侧:zsky。

知道了,感谢提醒,我找找编辑在哪,改一下

hua111 发表于 2021-12-28 11:58

页: [1]
查看完整版本: NCTF2021 - re - 狗狗的秘密