题目地址:ctf.show

题目概览

题目文件 value.exe,Yoda 壳 + 双重反调试 + 完整性校验 + 加密数据,四层防护的 32 位控制台逆向题。从脱壳一路挖到 TLS 回调里的解密密钥,最终解出 flag flag{9969e2ddd64088f71af0caa891b4dc63}

侦察与脱壳

拖入 exeinfope:UPX 壳,32 位控制台程序。一键脱壳后拖入 IDA,字符串定位到主函数 main_0(text:00415C10)。

主逻辑:main_0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
  char v4; // [esp+0h] [ebp-ECh]
  char Str1[24]; // [esp+D0h] [ebp-1Ch] BYREF

  __CheckForDebuggerJustMyCode(&unk_41C032);
  sub_411406();
  while ( 1 )
  {
    sub_411046((char *)&byte_417B4C, v4);
    sub_4113AC("%s", (char)Str1);
    sub_4113B1(Str1);
    if ( dword_41A4E0 )
      break;
    puts(&Buffer);
  }
  sub_41141A();
  MessageBoxA(0, &Text, &Caption, 0);
  sub_4113CF(Str1);
  system("pause");
  return 0;
}

简单分析逻辑后可得:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
  char v4; // [esp+0h] [ebp-ECh]
  char Str1[24]; // [esp+D0h] [ebp-1Ch] BYREF

  __CheckForDebuggerJustMyCode(byte_41C032);
  sub_411406();
  while ( 1 )
  {
    printf((char *)&input_information, v4);
    scanf("%s", (char)Str1);
    sub_4113B1(Str1);
    if ( key_message )
      break;
    puts(&error_message);
  }
  sub_41141A();
  MessageBoxA(0, &Text, &Caption, 0);
  sub_4113CF(Str1);
  system("pause");
  return 0;
}

观察到 __CheckForDebuggerJustMyCode 函数(text:00415C3D),意识到可能有反调试。

完整性校验

定位 thunk 函数 sub_411406()(text:00415C42),根据其中逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  int i; // [esp+D0h] [ebp-20h]
  _BYTE Buffer[9]; // [esp+DFh] [ebp-11h] BYREF
  LPCVOID lpBaseAddress; // [esp+E8h] [ebp-8h]

  __CheckForDebuggerJustMyCode(&byte_41C032);
  lpBaseAddress = byte_411000;
  Buffer[0] = 0;
  for ( i = 0; i < 24576; ++i )
  {
    ReadProcessMemory((HANDLE)0xFFFFFFFF, lpBaseAddress, Buffer, 1u, 0);
    dword_41A4E4 += Buffer[0];
    lpBaseAddress = (char *)lpBaseAddress + 1;
  }
  return CreateThread(0, 0, StartAddress, 0, 0, 0);
}

判断其为累加自基址以来 0x6000 字节的代码值用以校验——校验完整性函数,若与原值不符则退出进程。

key 检查与 rand 门

继续分析 main_0 的 while 循环:读入 str,与 wangzherongyao 比较,否则退出;一致则 90% 概率进入下一步(rand 控制概率)。

反调试线程与 ScyllaHide

下一步进入 sub_41141A() 函数(text:00415CA0),创建两个后台反调试线程:BeingDebuggedNtGlobalFlag,检测到调试即退出线程。结合前文校验完整性函数,此处不易用 nop,可采用 x32gdb 中的 ScyllaHide 插件,然此处有坑!!!详见下文。

MessageBoxA() 函数(text:00415CB5)输出提示弹窗。

flag 解密(关键)

sub_4113CF(text:00415CC6)关键函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
int __cdecl sub_415770(char *Str)
{
  int j; // [esp+61Ch] [ebp-FC8h]
  size_t i; // [esp+628h] [ebp-FBCh]
  int v4; // [esp+15DCh] [ebp-8h]

  __CheckForDebuggerJustMyCode(byte_41C032);
  v4 = 0;
  for ( i = 0; i < j_strlen(Str); ++i )
    v4 += Str[i];
  for ( j = 0; j < 38; ++j )
  {
    dword_41A038[j] += v4;
    dword_41A038[j] ^= dword_41A4DC;
  }
  dword_41A038[j + 1] = 0;
  return sub_4113E3(dword_41A038);
}
 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
int __cdecl sub_415970(int a1)
{
  int i; // [esp+DCh] [ebp-84h]
  int v3; // [esp+E8h] [ebp-78h]
  int v4; // [esp+E8h] [ebp-78h]
  int v5; // [esp+E8h] [ebp-78h]
  char v6[104]; // [esp+F4h] [ebp-6Ch] BYREF

  __CheckForDebuggerJustMyCode(byte_41C032);
  v3 = 0;
  for ( i = 0; i < 9; ++i )
  {
    v6[v3] = *(_BYTE *)(a1 + 4 * i);
    v4 = v3 + 1;
    v6[v4++] = *(_BYTE *)(a1 + 4 * i + 36);
    v6[v4++] = *(_BYTE *)(a1 + 4 * i + 72);
    v6[v4] = *(_BYTE *)(a1 + 4 * i + 108);
    v3 = v4 + 1;
  }
  v6[v3] = *(_BYTE *)(a1 + 144);
  v5 = v3 + 1;
  v6[v5] = *(_BYTE *)(a1 + 148);
  if ( (unsigned int)(v5 + 1) >= 0x64 )
    j____report_rangecheckfailure();
  v6[v5 + 1] = 0;
  return printf("\n%s\n", (char)v6);
}

两段主题逻辑如上。当下需要知道关键数据 dword_41A038dword_41A4DC,其中 dword_41A038 在 data 节中。只需 ScyllaHide 插件处理反调试后动态运行至此处,发现 dword_41A4DC00 00 00 00

脚本验证:乱码暴露遗漏

根据上文两函数信息,得脚本:

 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
tbl = [
    0xfffff9a6, 0xfffff991, 0xfffff9d3, 0xfffff9a8,
    0xfffff9dc, 0xfffff9d5, 0xfffff9dc, 0xfffff9d4,
    0xfffff9d8, 0xfffff9a0, 0xfffff9d3, 0xfffff9a7,
    0xfffff9a8, 0xfffff9d4, 0xfffff9db, 0xfffff9a9,
    0xfffff9d3, 0xfffff9a8, 0xfffff9ab, 0xfffff9d3,
    0xfffff9da, 0xfffff9d6, 0xfffff9d4, 0xfffff9ab,
    0xfffff9ab, 0xfffff9db, 0xfffff9a9, 0xfffff9a5,
    0xfffff9d6, 0xfffff9a8, 0xfffff9d8, 0xfffff9a6,
    0xfffff9a6, 0xfffff9ab, 0xfffff9aa, 0xfffff9d6,
    0xfffff9d9, 0xfffff98f,
]

order = [
    0, 9, 18, 27, 1, 10, 19, 28, 2, 11, 20, 29,
    3, 12, 21, 30, 4, 13, 22, 31, 5, 14, 23, 32,
    6, 15, 24, 33, 7, 16, 25, 34, 8, 17, 26, 35,
    36, 37,
]

key = "wangzherongyao"
s = 0
for c in key:
    s = s + ord(c)

flag = ""
for i in order:
    v = tbl[i] + s
    v = v & 0xFF
    v = v ^ 0x00          # 先按 key=0 验证,打出乱码
    flag = flag + chr(v)

print(flag)

打印出乱码,发现问题——程序主逻辑已经分析完毕,猜测有遗漏点。

突破口:TLS 回调

将程序拖入CFF Explorer查看TLS Directory,发现AddressOfCallBacks字段值00417720,即回调函数。

也可在x32dbg 中设置用户 TLS 回调断点,发现 TLS 函数 TlsCallback_0_0(text:00411740)。

其主要逻辑 NtQueryInformationProcess,用于检测反调试,若有则修改 dword_41A4DC 的值——而 dword_41A4DC 又是解密的关键数据(XOR 密钥)。

解法

故解法有二:

解法 1:枚举密钥 猜测 dword_41A4DC 为一字节,故枚举 256 种情况;又答案首几个字母必有规律(如 flag{...}),反向推导得 dword_41A4DC 低字节必为 0xFF

解法 2:动态观察 修改 ScyllaHide 设置,放行 NtQueryInformationProcess 函数,并在 text:00415AB7(即 printf 函数处)下断点,即可观察到 flag。

flag: flag{9969e2ddd64088f71af0caa891b4dc63}

总结:四层防护

1
2
3
4
5
① 完整性校验(.text 校验和 + 看门线程)→ 防 patch
② 反调试线程(BeingDebugged / NtGlobalFlag)→ 防调试
③ TLS 回调里 NtQueryInformationProcess → 反调试 + 喂解密密钥
④ 加密 flag(表 + 字节和 + 调试端口作密钥)→ 数据层
破解 = 静态还原算法 + 动态改密钥/枚举密钥