Zj_W1nd's BLOG

Windows函数隐藏——逆向工程实验2022WP

2024/09/19

前情提要

这道题目是按照课堂上演示的逻辑只改了加密用的数组(换成了学号),逻辑是一样的。上课的演示是啥也看不懂然后ida搜索异或指令进去一个个看,诶🤓,我们就找到了一个输入异或的简单加密函数,这题就解了。事实上之前没有逆向经验的同学也是按照这个步骤很简单就做出了这个题。

但是实际在实验课上我被这个题卡了很久,按照正常执行流程,程序的入口点后面没有任何关于flag的“判断”内容,将我们的输入异或一次后程序就退出了。怪异。

题目分析

这个题目的结构非常简单,入口点main0进去后就是一个输入,检查长度,cpy到另一块内存之后检查第八个字符是不是’A’,然后就是一个简单的异或:

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

__CheckForDebuggerJustMyCode(&unk_1166007);
sub_FFCD46(&unk_11631E0, "Please Input:");
input("%s", Source);
len = j__strlen(Source);
if ( len > 30 || len < 10 )
{
sub_FFA6DB((int)&defined_str, v4);
j___exit_0(0);
}
j__strcpy_s(Destination, 0x1Eu, Source);
if ( Destination[7] != 'A' )
{
sub_FFA6DB((int)&defined_str, v4);
j___exit_0(0);
}
j_xor(Destination);
return 0;
}

异或函数是这样:

1
2
3
4
5
6
7
8
9
10
char *__cdecl xor1(char *Str)
{
size_t i; // [esp+D0h] [ebp-8h]

__CheckForDebuggerJustMyCode(&unk_1166007);
Str[7] = '#';
for ( i = 0; i < j__strlen(Str); ++i )
Str[i] ^= 0x1Fu;
return Str;
}

非常的简单啊,但是问题出现了,这个程序要我们输入什么?我们没有看到任何相关的判断和比较,就这么结束了。很吸引人。

猜测与实践

IDA坏了?

很容易想到的是,程序通过一些花指令或者混淆的方式干扰了IDA的反汇编伪代码,但是直接看汇编发现没有任何花指令和混淆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
.text:01011A6B 0E0 call j___exit_0
.text:01011A70 ; ---------------------------------------------------------------------------
.text:01011A70
.text:01011A70 loc_1011A70: ; CODE XREF: _main_0+AA↑j
.text:01011A70 0DC mov eax, Destination
.text:01011A75 0DC push eax
.text:01011A76 0E0 call j_xor
.text:01011A7B 0E0 add esp, 4
.text:01011A7E
.text:01011A7E loc_1011A7E: ; CODE XREF: _main_0+7E↑j
.text:01011A7E 0DC xor eax, eax
.text:01011A80 0DC pop edi
.text:01011A81 0D8 pop esi
.text:01011A82 0D4 pop ebx
.text:01011A83 0D0 add esp, 0CCh
.text:01011A89 004 cmp ebp, esp
.text:01011A8B 004 call j___RTC_CheckEsp
.text:01011A90 004 mov esp, ebp
.text:01011A92 004 pop ebp
.text:01011A93 000 retn
.text:01011A93 _main_0 endp ; sp-analysis failed // 不知道为什么,栈是平衡的
...

难道IDA出问题了??(当时我真的是这么想的),IDA内动态调试按汇编一步步走也没有任何的报错,可以确定反汇编没有问题了,那程序真正的逻辑在哪?

hint

程序在平台给的时候是一个大压缩包,里面放了所有选这门课的人的学号.exe文件,并且强调flag没法共享每个人都不一样。那我们直接字符串搜索学号试试,果然有,交叉引用找到了另外一个加密函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __stdcall sub_100DC80(char *Str1)
{
size_t i; // [esp+E8h] [ebp-14h]

__CheckForDebuggerJustMyCode(&unk_1166007);
if ( Str1 )
{
for ( i = 0; i < j__strlen(Str1); ++i )
Str1[i] ^= 0x1Cu;
if ( !j__strcmp(Str1, Destination) )
{
sub_FFB4AA((int)&unk_11631E0, 'o');
sub_FFB4AA((int)&unk_11631E0, 'k');
}
}
return 0;
}

可以看到这里应该是会输出OK,也就代表这里非常可能,或者说就是我们真正要去的地方。但是这个函数在哪里被调用了?为什么main_0里面没有?

发生了什么?

动调在这里下断点然后看trace,一看吓一跳:

后续:这些都是IDA识别错了,函数名都是假的,都是找已经有的库函数加的偏移backtrace.png
这里我们附带主函数执行到retn的时候的trace:backtrace2.png
感觉这里是类似于linux下_exit会触发的fini_array,但是windows这个debug模式编译的程序除了用户代码以外塞了太多构式东西了,很多机制都很复杂,我们从主函数返回看一眼,首先退出到invoke_main:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:0037A9D0 ?invoke_main@@YAHXZ proc near           ; CODE XREF: __scrt_common_main_seh(void):loc_37A882↑p
.text:0037A9D0 push ebp
.text:0037A9D1 mov ebp, esp
.text:0037A9D3 call j_unknown_libname_935
.text:0037A9D8 push eax ; envp
.text:0037A9D9 call sub_31E6DC
.text:0037A9DE mov eax, [eax]
.text:0037A9E0 push eax ; argv
.text:0037A9E1 call sub_31C2DD
.text:0037A9E6 mov ecx, [eax]
.text:0037A9E8 push ecx ; argc
.text:0037A9E9 call _main
.text:0037A9EE add esp, 0Ch // <----here
.text:0037A9F1 pop ebp
.text:0037A9F2 retn
.text:0037A9F2 ?invoke_main@@YAHXZ endp

然后退出到__scrt_common_main_seh,在16A的地方调用一个loaddll,然后内部call这么一个函数:?common_exit@@YAXHW4_crt_exit_cleanup_mode@@W4_crt_exit_return_mode@@@Z,在common_exit里,调用了一个函数(没有符号不知道是什么)

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
.text:003D9F90 sub_3D9F90      proc near ; CODE XREF: common_exit(int,_crt_exit_cleanup_mode,_crt_exit_return_mode)+45↓p
.text:003D9F90
.text:003D9F90 var_C = byte ptr -0Ch
.text:003D9F90 var_8 = byte ptr -8
.text:003D9F90 var_1 = byte ptr -1
.text:003D9F90 arg_0 = byte ptr 8
.text:003D9F90 arg_4 = dword ptr 0Ch
.text:003D9F90
.text:003D9F90 mov edi, edi
.text:003D9F92 push ebp
.text:003D9F93 mov ebp, esp
.text:003D9F95 sub esp, 0Ch
.text:003D9F98 lea eax, [ebp+arg_0]
.text:003D9F9B push eax
.text:003D9F9C lea ecx, [ebp+var_8]
.text:003D9F9F call unknown_libname_845 ; Microsoft VisualC universal runtime
.text:003D9FA4 push eax
.text:003D9FA5 mov ecx, [ebp+arg_4]
.text:003D9FA8 push ecx
.text:003D9FA9 lea edx, [ebp+arg_0]
.text:003D9FAC push edx
.text:003D9FAD lea ecx, [ebp+var_C]
.text:003D9FB0 call unknown_libname_844 ; Microsoft VisualC universal runtime
.text:003D9FB5 push eax
.text:003D9FB6 lea ecx, [ebp+var_1]
.text:003D9FB9 call sub_3D9EF0 // <--here
.text:003D9FBE mov esp, ebp
.text:003D9FC0 pop ebp
.text:003D9FC1 retn
.text:003D9FC1 sub_3D9F90 endp

这个函数又call了3d9ef0,然后莫名其妙跳两次就到了我们的加密函数了。从IDAtrace中压入的内容来看(可能是自动识别),猜测程序是把加密函数注册在了visualc++编译的一个退出处理程序中。

最后的分析

不知道从哪里开始看,我们先看这个加密函数头IDA为我们自动标注的内容:SEH

SEH

copilot:

SEH(Structured Exception Handling)是Windows操作系统提供的一种异常处理机制。它允许程序在运行时捕获和处理异常,如访问冲突、除零错误等。SEH的主要特点包括:

  1. 异常处理框架:SEH使用__try和__except块来定义异常处理代码。
  2. 异常过滤器:__except块可以包含一个过滤器表达式,用于确定是否处理异常。
  3. 异常处理程序:如果过滤器表达式返回EXCEPTION_EXECUTE_HANDLER,则执行异常处理代码。

但是用x64dbg查seh链没查到,不是这个(也可能不直接是)。

aexit和回调函数

这个应该是这个程序实现隐藏的方式。跟踪到执行加密2函数的时候查看调用堆栈,存在一个ida识别为__execute_onexit_table的函数,这个函数用于在程序退出的时候执行回调函数,而这些函数在编写程序的时候通过atexit注册并形成指针数组(类似finiarray)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.text:00173D20 ; int __cdecl _execute_onexit_table(_onexit_table_t *Table)
.text:00173D20 __execute_onexit_table proc near ; CODE XREF: j___execute_onexit_table↑j
.text:00173D20
.text:00173D20 var_4 = byte ptr -4
.text:00173D20 Table = dword ptr 8
.text:00173D20
.text:00173D20 mov edi, edi
.text:00173D22 push ebp
.text:00173D23 mov ebp, esp
.text:00173D25 push ecx
.text:00173D26 lea eax, [ebp+Table]
.text:00173D29 push eax
.text:00173D2A lea ecx, [ebp+var_4]
.text:00173D2D call unknown_libname_942 ; Microsoft VisualC universal runtime
.text:00173D32 push eax
.text:00173D33 call j_unknown_libname_848
.text:00173D38 push eax
.text:00173D39 call sub_173500 // <--here
.text:00173D3E add esp, 8
.text:00173D41 mov esp, ebp
.text:00173D43 pop ebp
.text:00173D44 retn
.text:00173D44 __execute_onexit_table endp

跟进table(居然是栈):

1
2
3
4
5
6
7
Stack[000070E0]:00BCF958                 dd 0A08BC5FCh
Stack[000070E0]:00BCF95C dd offset sub_1BFCB0
Stack[000070E0]:00BCF960 dd 0CCCCCCCCh
Stack[000070E0]:00BCF964 dd 0CCCCCCCCh
Stack[000070E0]:00BCF968 dd offset sub_1BFCB0
Stack[000070E0]:00BCF96C dd 0A0373C60h
Stack[000070E0]:00BCF970 dd 0CCCCCCCCh

可以看到有个函数在里面,而这个函数内部调用了真正的加密函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
text:001BFCB0 sub_1BFCB0      proc near               ; DATA XREF: sub_A57B0+28↑o
.text:001BFCB0 ; Stack[000070E0]:00BCF95C↓o ...
.text:001BFCB0
.text:001BFCB0 var_C0 = byte ptr -0C0h
.text:001BFCB0
.text:001BFCB0 push ebp
.text:001BFCB1 mov ebp, esp
.text:001BFCB3 sub esp, 0C0h
.text:001BFCB9 push ebx
.text:001BFCBA push esi
.text:001BFCBB push edi
.text:001BFCBC lea edi, [ebp+var_C0]
.text:001BFCC2 mov ecx, 30h
.text:001BFCC7 mov eax, 0CCCCCCCCh
.text:001BFCCC rep stosd
.text:001BFCCE mov ecx, offset unk_203086
.text:001BFCD3 call sub_9C28D
...

.text:0009C28D sub_9C28D proc near ; CODE XREF: sub_1BFCB0+23↓p
.text:0009C28D jmp sub_ACEB0 // <--加密函数
.text:0009C28D sub_9C28D endp

而中间的那些lambda调用估计是源码风格的原因,可能是套了几个调用搞出来的东西。

总结

神秘

CATALOG
  1. 1. 前情提要
  2. 2. 题目分析
  3. 3. 猜测与实践
    1. 3.1. IDA坏了?
    2. 3.2. hint
    3. 3.3. 发生了什么?
  4. 4. 最后的分析
    1. 4.1. SEH
    2. 4.2. aexit和回调函数
  5. 5. 总结