在上篇文章中写了在逆向中定位main
函数几种方法,其中有一种方法是通过编译器特征定位 main
函数(使用IDA
分析简单demo
程序获取特征,根据得到的特征可以定位相同编译器编译程序的main
函数)。在上一篇文章中我们提取了VS
环境(VS2017
)中MSVC
编译器编译配置与目标平台架构分别为Debug
和x86
生成的程序特征,从而正确定位到了main
函数所在位置。本篇文章主要写一下根据编译器特征定位main
函数的方法,提取VS
环境(VS2017
)生成的编译配置与目标平台架构分别为Debug
+x64
、Release
+x86
和Release
+x64
以及GCC
编译器编译生成的程序特征,从而定位到main函数。
1.Debug-x64程序特征提取
先准备一个Debug-x64的Hello World
程序(以下称Demo
程序);且准备程序的步骤都一样,后续就不再赘述:
接着右击项目生成程序即可;
打开程序所在文件夹,找到程序即可:
将exe
程序放入IDA(x64-bit)
中进行加载解析;pdb
文件也最好一并载入,即弹出以下窗口时选择yes
:
.pdb文件包含了与可执行文件关联的调试符号和源代码信息。这些信息包括函数名、变量名、源代码行号等。在调试时,.pdb文件使得调试器能够将二进制代码映射回源代码,从而允许开发者查看变量的名称和类型、设置断点、查看调用堆栈以及源代码的确切行号等。没有.pdb文件,调试器只能展示汇编代码和原始的内存地址,极大地增加了调试的难度。
接着在函数窗口定位Demo
程序main
函数所在位置:
定位到main
函数:
接着进行交叉引用(ctrl + x),提取特征(过程在上一篇文章中已经有详细说明,这篇文章就不再做过多赘述),这边补充一下小细节;若是在交叉引用的过程中出现了两个目标,且目标中包含Runtime_Function
;此时一律不选Runtime_Function
:
以下是笔者获取的程序(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
函数:
将程序拖如x64dbg
中进行调试;按下F9
来到自身程序模块中:
接着根据特征成功定位到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
-
针对
x64
架构(64位),使用了rsp
这个x64
特有的寄存器。 -
使用寄存器传递函数参数(
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位),使用了 esp
、ebp
和 fs
等 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程序:
将程序拖入IDA(32bit)
进行解析;
再解析完成后,一样在函数窗口中搜索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
函数:
3.Release-x64程序特征提取
生成Release-x64程序;
打开所在文件夹,并将其载入IDA(64-bit)
中进行解析:
在IDA中一样先在函数窗口中定位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
函数:
4.gcc程序特征定位main函数
gcc编译的Demo
程序准备,在此之前系统中需要存在gcc环境;若没有的gcc环境的兄弟文章末尾领取。我这边也只记录64位的gcc特征定位main函数的过程。
1.准备一个C文件
接着在命令行中运行以下命令,生成exe程序:
gcc Hello.c -o Hello.exe
此时就可以开始进行特征提取了,将程序拖入IDA(64-bit)
进行加载解析;
特征如下:
第二个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进入自身程序模块中
进入以后查看反汇编代码后发现并未存在特征:
此时先按F8进行程序运行,但是F8按着按着发现又回到了ntdll.dll模块当中,所以还需要按下F9;
回到自己领空后接着按F8运行代码,结果运行没一会儿又回到了ntdll.dll模块,继续按F9。
回到自己程序模块中后接着F8运行,若是又到ntdll.dll模块就再按F9以此类推,直到出现以下特征:
两个call,依照上述IDA中提取的特征,我们需要进入第二个call;接着就正常根据特征进行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号[风铃Sec]后台回复[mingw64]获取MinGW,后添加环境变量即可使用。如若不会,GZH后台回复。