C/C++逆向:寻找main函数(其他)

admin 2024年9月2日08:16:38评论22 views字数 9506阅读31分41秒阅读模式

在上篇文章中写了在逆向中定位main函数几种方法,其中有一种方法是通过编译器特征定位 main 函数(使用IDA分析简单demo程序获取特征,根据得到的特征可以定位相同编译器编译程序的main函数)。在上一篇文章中我们提取了VS环境(VS2017)中MSVC编译器编译配置与目标平台架构分别为Debugx86生成的程序特征,从而正确定位到了main函数所在位置。本篇文章主要写一下根据编译器特征定位main函数的方法,提取VS环境(VS2017)生成的编译配置与目标平台架构分别为Debug+x64Release+x86Release+x64以及GCC编译器编译生成的程序特征,从而定位到main函数。

1.Debug-x64程序特征提取

先准备一个Debug-x64的Hello World程序(以下称Demo程序);且准备程序的步骤都一样,后续就不再赘述:

C/C++逆向:寻找main函数(其他)

接着右击项目生成程序即可;

C/C++逆向:寻找main函数(其他)

打开程序所在文件夹,找到程序即可:

C/C++逆向:寻找main函数(其他)

exe程序放入IDA(x64-bit)中进行加载解析;pdb文件也最好一并载入,即弹出以下窗口时选择yes:

C/C++逆向:寻找main函数(其他)

.pdb文件包含了与可执行文件关联的调试符号和源代码信息。这些信息包括函数名、变量名、源代码行号等。在调试时,.pdb文件使得调试器能够将二进制代码映射回源代码,从而允许开发者查看变量的名称和类型、设置断点、查看调用堆栈以及源代码的确切行号等。没有.pdb文件,调试器只能展示汇编代码和原始的内存地址,极大地增加了调试的难度。

接着在函数窗口定位Demo程序main函数所在位置:

C/C++逆向:寻找main函数(其他)

定位到main函数:

C/C++逆向:寻找main函数(其他)

接着进行交叉引用(ctrl + x),提取特征(过程在上一篇文章中已经有详细说明,这篇文章就不再做过多赘述),这边补充一下小细节;若是在交叉引用的过程中出现了两个目标,且目标中包含Runtime_Function;此时一律不选Runtime_Function:

C/C++逆向:寻找main函数(其他)

以下是笔者获取的程序(Debug-x64)特征:

第一个jmp

第一个call

第二个call

第一个jnz
sub     rsp, 68h
mov     ecx, 1
call    j___scrt_initialize_crt
movzx eax, al
test eax, eax
jnz short loc_140011FEF


第一个jmp
mov     [rsp+68h+var_48], 0
call    j___scrt_acquire_startup_lock
mov     [rsp+68h+var_47], al
cmp     cs:__scrt_current_native_startup_state, 1
jnz     short loc_140012012
mov     ecx, 7
call    j___scrt_fastfail
jmp     short loc_14001206A


第一个jz
movzx   ecx, [rsp+68h+var_47]
call    j___scrt_release_startup_lock
call    j___scrt_get_dyn_tls_init_callback
mov     [rsp+68h+var_38], rax
mov     rax, [rsp+68h+var_38]
cmp     qword ptr [rax], 0
jz      short loc_1400120C6

第一个jz
call    j___scrt_get_dyn_tls_dtor_callback
mov     [rsp+68h+var_30], rax
mov     rax, [rsp+68h+var_30]
cmp     qword ptr [rax], 0
jz      short loc_1400120F9

第一个call

第4个call

jmp main(跳转表)

接着根据特征进行动态调试定位main函数:

C/C++逆向:寻找main函数(其他)

将程序拖如x64dbg中进行调试;按下F9来到自身程序模块中:

C/C++逆向:寻找main函数(其他)

接着根据特征成功定位到main函数:

C/C++逆向:寻找main函数(其他)

因为具体方法在上一篇文章中已经介绍过了,所以这篇文章主要的侧重点还是来比较一下生成的Debug-x86/x64程序的特征:

①Debug-x64特征
第一个jmp

第一个call

第二个call

第一个jnz
sub     rsp, 68h
mov     ecx, 1
call    j___scrt_initialize_crt
movzx eax, al
test eax, eax
jnz short loc_140011FEF


第一个jmp
mov     [rsp+68h+var_48], 0
call    j___scrt_acquire_startup_lock
mov     [rsp+68h+var_47], al
cmp     cs:__scrt_current_native_startup_state, 1
jnz     short loc_140012012
mov     ecx, 7
call    j___scrt_fastfail
jmp     short loc_14001206A


第一个jz
movzx   ecx, [rsp+68h+var_47]
call    j___scrt_release_startup_lock
call    j___scrt_get_dyn_tls_init_callback
mov     [rsp+68h+var_38], rax
mov     rax, [rsp+68h+var_38]
cmp     qword ptr [rax], 0
jz      short loc_1400120C6

第一个jz
call    j___scrt_get_dyn_tls_dtor_callback
mov     [rsp+68h+var_30], rax
mov     rax, [rsp+68h+var_30]
cmp     qword ptr [rax], 0
jz      short loc_1400120F9

第一个call

第4个call

jmp main(跳转表)
②Debug-x86特征:
第一个jmp

第一个call
push
mov
call

第二个call

第一个jnz
mov     large fs:0, eax
mov     [ebp+ms_exc.old_esp], esp
push    1
call    j____scrt_initialize_crt
add     esp, 4
movzx   eax, al
test    eax, eax
jnz     short loc_411D7B

第一个jmp
mov     [ebp+var_1A], al
cmp     dword_41A158, 1
jnz     short loc_411DA0
push    7
call    j____scrt_fastfail
jmp     short loc_411E01

第一个jz
movzx   ecx, [ebp+var_1A]
push    ecx
call    j____scrt_release_startup_lock
add     esp, 4
call    sub_411195
mov     [ebp+var_20], eax
mov     edx, [ebp+var_20]
cmp     dword ptr [edx], 0
jz      short loc_411E51

第一个jz
call    sub_41123A
mov     [ebp+var_24], eax
mov     edx, [ebp+var_24]
cmp     dword ptr [edx], 0
jz      short loc_411E82

第一个call
call   ?invoke_main@@YAHXZ ; invoke_main(void)  
mov     [ebp+Code], eax
call    j____scrt_is_managed_app
movzx   ecx, al
test    ecx, ecx
jnz     short loc_411E9F
mov     edx, [ebp+Code]

第4个call
第一个jmp

前三个特征:Debug-x64与Debug-x86特征一致;

①Debug-x64特征
第一个jmp

第一个call

第二个call
②Debug-x86特征:
第一个jmp

第一个call
push
mov
call

第二个call

第四个特征:主要的区别在于MSVC运行时初始化函数(j___scrt_initialize_crt)调用时所作的处理方式不同,以下是两者的具体区别分析:

①Debug-x64特征
第一个jnz
sub     rsp, 68h
mov     ecx, 1
call    j___scrt_initialize_crt
---------------------------------------
movzx eax, al
test eax, eax
jnz short loc_140011FEF
  1. 针对 x64架构(64位),使用了 rsp 这个x64 特有的寄存器。

  2. 使用寄存器传递函数参数(mov ecx, 1),不涉及栈操作来传递参数。该片段在调用函数前并没有通过 push 将参数压入栈,而是直接将参数放入寄存器 ecx,这是典型的 x64使用的fastcall调用约定。

在 x64 架构(64位)中,Windows 平台的调用约定通常使用 RCX, RDX, R8, R9 寄存器来传递前四个整数或指针类型的参数,返回值使用 RAX 寄存器。
②Debug-x86特征:
第一个jnz
mov     large fs:0, eax
mov     [ebp+ms_exc.old_esp], esp
push    1
call    j____scrt_initialize_crt
add     esp, 4
---------------------------------------
movzx   eax, al
test    eax, eax
jnz     short loc_411D7B

1.针对 x86架构(32位),使用了 espebpfs 等 x86 特有的寄存器和调用约定。

2.通过 push 指令将参数压入栈(push 1);参数通过 push 指令压栈,这是 x86 调用约定(如 stdcall、cdecl 等)的常见特征。

3.该片段代码 使用了add esp, 4代码进行栈清理操作,这说明该x86程序使用的是 cdecl 调用约定。

除了以上区别,其余的特征一致。接着来对比一下第五个特征:

①Debug-x64特征
mov     [rsp+68h+var_48], 0
call    j___scrt_acquire_startup_lock
-------------对比下面特征--------------------------------------
mov     [rsp+68h+var_47], al
cmp     cs:__scrt_current_native_startup_state, 1
jnz     short loc_140012012
-----------------------------------------
mov     ecx, 7
call    j___scrt_fastfail
jmp     short loc_14001206A
②Debug-x86特征:
mov     [ebp+var_1A], al
cmp     dword_41A158, 1
jnz     short loc_411DA0
-----------------------------------------
push    7
call    j____scrt_fastfail
jmp     short loc_411E01

通关观察可以看出,第五个特征中也是由于x86与x64程序所用的调用约定不同从而使得两个特征又些许差异,除了在使用call调用函数部分会存在区别以外,其他的特征基本是一致的。其余特征的区别也是一洋,这边就不做过多赘述了。

2.Release-x86程序特征提取

准备一个Release-x86程序:

C/C++逆向:寻找main函数(其他)

将程序拖入IDA(32bit)进行解析;

C/C++逆向:寻找main函数(其他)

再解析完成后,一样在函数窗口中搜索main函数:

C/C++逆向:寻找main函数(其他)

在这里我们可以发现 IDA函数窗口中Release 模式下的程序显示的函数数量明显少于 Debug 模式下的程序;这种现象的主要原因在于编译器在 Release模式下会进行各种优化,导致一些函数被内联、删除或合并。以下是部分导致这种现象的具体原因:

①编译器优化
1>函数内联
编译器会将一些小的、频繁调用的函数直接替换为它们的代码实现,避免了函数调用的开销。这种优化会导致这些小函数在最终的可执行文件中不再作为单独的函数存在,而是被“内联”到调用它们的地方,结果是 IDA 在分析 Release 版本时,无法识别出这些函数,导致函数窗口中显示的函数数量减少。
2>死代码消除
编译器会删除在程序执行过程中永远不会被调用的代码和函数,减少可执行文件的大小,在 Debug 模式下,编译器不会执行这种优化,因此即使是未使用的函数也会出现在最终的可执行文件中。
3>代码折叠和合并:
对功能相似或相同的代码段进行折叠或合并,以减少代码的冗余度。这样会导致 IDA 将多个函数识别为一个函数或者直接将多个函数的代码合并成一个函数。
②去除调试信息
Release 模式下通常会去除调试信息,如符号信息(函数名、变量名等)和调试符号(如 PDB 文件),以保护程序的源代码结构和内部逻辑,并且减少可执行文件的大小。

接着回到正题,在定位到main函数后,接着就可以对Release-x86程序进行特征提取,这边步骤就省略了,最后得到的特征为:

第一个call

第二个call

第一个jmp
mov     [ebp+ms_exc.registration.TryLevel], 0
call    ___scrt_acquire_startup_lock
mov     [ebp+is_nested], al
cmp     ___scrt_current_native_startup_state, 1
jnz     short loc_4011D0
push    7               ; code
call    ___scrt_fastfail
jmp     short loc_401231

第一个jz
movzx   ecx, [ebp+is_nested]
push    ecx             ; is_nested
call    ___scrt_release_startup_lock
add     esp, 4
call    ___scrt_get_dyn_tls_init_callback
mov     [ebp+tls_init_callback], eax
mov     edx, [ebp+tls_init_callback]
cmp     dword ptr [edx], 0
jz      short loc_401281

第一个jz
call    ___scrt_get_dyn_tls_dtor_callback
mov     [ebp+tls_dtor_callback], eax
mov     edx, [ebp+tls_dtor_callback]
cmp     dword ptr [edx], 0
jz      short loc_4012B2

第一个call

第4个call

在这边可以看到Release程序的特征层数比较Debug程序的特征层数要来的少;最后根据特征定位到main函数:

C/C++逆向:寻找main函数(其他)

3.Release-x64程序特征提取

生成Release-x64程序;

C/C++逆向:寻找main函数(其他)

打开所在文件夹,并将其载入IDA(64-bit)中进行解析:

C/C++逆向:寻找main函数(其他)

在IDA中一样先在函数窗口中定位main函数:

C/C++逆向:寻找main函数(其他)

接着进行交叉引用,提取特征,步骤省略,最后得到的特征为:

第一个call

第二个call

第一个jmp
call    __scrt_acquire_startup_lock
mov     [rsp+68h+var_47], al
cmp     cs:__scrt_current_native_startup_state, 1
jnz     short loc_140001222
mov     ecx, 7          ; code
call    __scrt_fastfail
jmp     short loc_14000127A

第一个jz
movzx   ecx, [rsp+68h+var_47]
call    __scrt_release_startup_lock
call    __scrt_get_dyn_tls_init_callback
mov     [rsp+68h+target], rax
mov     rax, [rsp+68h+target]
cmp     qword ptr [rax], 0
jz      short loc_1400012D6

第一个jz
call    __scrt_get_dyn_tls_dtor_callback
mov     [rsp+68h+var_30], rax
mov     rax, [rsp+68h+var_30]
cmp     qword ptr [rax], 0
jz      short loc_140001309

第一个call

第四个call

接着根据特征成功定位main函数:

C/C++逆向:寻找main函数(其他)

4.gcc程序特征定位main函数

gcc编译的Demo程序准备,在此之前系统中需要存在gcc环境;若没有的gcc环境的兄弟文章末尾领取。我这边也只记录64位的gcc特征定位main函数的过程。

1.准备一个C文件

C/C++逆向:寻找main函数(其他)

接着在命令行中运行以下命令,生成exe程序:

gcc Hello.c -o Hello.exe

此时就可以开始进行特征提取了,将程序拖入IDA(64-bit)进行加载解析;

C/C++逆向:寻找main函数(其他)

特征如下:

第二个call

第一个jnz
push    r13
push    r12
push    rbp
push    rdi
push    rsi
push    rbx
sub     rsp, 98h
xor     eax, eax
mov     ecx, 0Dh
lea     rdx, [rsp+0C8h+StartupInfo]
mov     rdi, rdx
rep stosq
mov     rdi, cs:_refptr_mingw_app_type
mov     r9d, [rdi]
test    r9d, r9d
jnz     loc_401470


第二个jmp
xor     eax, eax
xchg    rax, [rbx]
jmp     loc_40122C
mov     rcx, rdx        ; lpStartupInfo
call    cs:__imp_GetStartupInfoA
jmp     loc_4011B4

第一个jmp
mov     rax, gs:30h
mov     rbx, cs:_refptr___native_startup_lock
xor     ebp, ebp
mov     rsi, [rax+8]
mov     r12, cs:__imp_Sleep
jmp     short loc_4011E4

第一个jnz
mov     rax, rbp
lock cmpxchg [rbx], rsi
test    rax, rax
jnz     short loc_4011D3

第一个jz
xor     ebp, ebp
mov     rsi, [rax+8]
mov     r12, cs:__imp_Sleep
jmp     short loc_4011E4
cmp     rsi, rax
jz      loc_401410

第一个jnz
mov     rsi, cs:_refptr___native_startup_state
mov     ebp, 1
mov     eax, [rsi]
cmp     eax, 1
jnz     loc_401205

第2个jz
xor     ebp, ebp
mov     eax, [rsi]
cmp     eax, 1
jz      loc_401427
mov     eax, [rsi]
test    eax, eax
jz      loc_401480

第一个jmp
mov     rdx, cs:_refptr___xi_z
mov     dword ptr [rsi], 1
mov     rcx, cs:_refptr___xi_a
call    _initterm
jmp     loc_401219

第一个jz
mov     eax, [rsi]
test    eax, eax
jz      loc_401480
mov     cs:has_cctor,
mov     eax, [rsi]
cmp     eax, 1
jz      loc_40143C

第一个jnz
mov     rdx, cs:_refptr___xc_z
mov     rcx, cs:_refptr___xc_a
call    _initterm
test    ebp, ebp
mov     dword ptr [rsi], 2
jnz     loc_40122C

第一个jz
mov     rax, cs:_refptr___dyn_tls_init_callback
mov     rax, [rax]
test    rax, rax
jz      short loc_401247
xor     r8d, r8d
mov     edx, 2

第一个jmp
call    __p__acmdln
xor     ecx, ecx
mov     rax, [rax]
test    rax, rax
jnz     short loc_4012B2
jmp     short loc_4012F7

第一个jz
mov     r8d, [rdi]
test    r8d, r8d
jz      short loc_401315
test    byte ptr [rsp+0C8h+StartupInfo.dwFlags], 1
mov     eax, 0Ah
jnz     loc_401400


第一个jle
mov     rcx, r12        ; Size
call    malloc
test    ebx, ebx
mov     rdi, cs:argv
mov     rbp, rax
jle     short loc_401387

第二个call:
mov     qword ptr [rax], 0
mov     cs:argv, rbp
call    __main
mov     rax, cs:_refptr___imp___initenv
mov     rdx, cs:envp
mov     ecx, cs:argc    ; argc
mov     rax, [rax]
mov     [rax], rdx
mov     r8, cs:envp     ; envp
mov     rdx, cs:argv    ; argv
call    main
mov     ecx, cs:managedapp
mov     cs:mainret, eax
test    ecx, ecx
jz      loc_40149E

gcc程序的特征定位比较诡异,所以这边简单做一个记录:

①首先按F9进入自身程序模块中

C/C++逆向:寻找main函数(其他)

进入以后查看反汇编代码后发现并未存在特征:

C/C++逆向:寻找main函数(其他)

此时先按F8进行程序运行,但是F8按着按着发现又回到了ntdll.dll模块当中,所以还需要按下F9;

C/C++逆向:寻找main函数(其他)

回到自己领空后接着按F8运行代码,结果运行没一会儿又回到了ntdll.dll模块,继续按F9。

C/C++逆向:寻找main函数(其他)

回到自己程序模块中后接着F8运行,若是又到ntdll.dll模块就再按F9以此类推,直到出现以下特征:

C/C++逆向:寻找main函数(其他)

两个call,依照上述IDA中提取的特征,我们需要进入第二个call;接着就正常根据特征进行main函数的定位即可。

C/C++逆向:寻找main函数(其他)

注:由于gcc编译程序最后一个特征比较明显,所以可以选择一直点击F8,直到最后一个特征出现后,即可根据最后一个特征成功定位到main函数。

第二个call:
mov     qword ptr [rax], 0
mov     cs:argv, rbp
call    __main
mov     rax, cs:_refptr___imp___initenv
mov     rdx, cs:envp
mov     ecx, cs:argc    ; argc
mov     rax, [rax]
mov     [rax], rdx
mov     r8, cs:envp     ; envp
mov     rdx, cs:argv    ; argv
call    main
mov     ecx, cs:managedapp
mov     cs:mainret, eax
test    ecx, ecx
jz      loc_40149E

GCC环境:VX_GZ号,后台回复[mingw64]后添加环境变量即可使用。如若不会,GZH后台回复。

原文始发于微信公众号(风铃Sec):C/C++逆向:寻找main函数(其他)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年9月2日08:16:38
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   C/C++逆向:寻找main函数(其他)http://cn-sec.com/archives/3119459.html

发表评论

匿名网友 填写信息