在开始分析程序之前,我们第一个要解决的问题,就是如何定位到main函数,想要从二进制逆向的角度分析出main函数,就必须要了解正向的代码下main函数的所有的细节和特
征。毕竟逆向的本质就是正向。
程序真正的入口
VS C++开发的程序在调试时总是从main或WinMain函数开始,这就让开发者误认为它们是程序的第一条指令执行处,这个认知其实是错误的。
main或WinMain函数需要有一个调用者,在它们被调用前,编译器其实已经做了很多事情,所以main或WinMain是“语法规定的用户入口”,而不是“应用程序入口”。
应用程序被操作系统加载时,操作系统会分析执行文件内的数据,并分配相关资源,读取执行文件中的代码和数据到合适的内存单元,然后才是执行入口代码,入 口 代 码 其 实 并 不 是 main 或 WinMain 函 数 , 通 常 是mainCRTStartup 、 wmainCRTStartup 、 WinMainCRTStartup 或wWinMainCRTStartup , 具体视编译选项而定 。
- mainCRTStartup和wmainCRTStartup是控制台环境下多字节编码和 Unicode 编 码 的 启 动 函 数
- WinMainCRTStartup 和wWinMainCRTStartup则是Windows环境下多字节编码和Unicode编码的启动函数。
VS2019的启动函数
VS C++ 在 控 制 台 和 多 字 节 编 码 环 境 下 的 启 动 函 数 为 mainCRTStartup , 由 系 统 库 KERNEL32.dll 负 责 调 用 , 在 mainCRTStartup中再调用main函数。使用VS2019进行调试时,入 口断点总是停留在main函数的首地址处。可以利用VS2019的栈回溯功能分析调用main函数的栈信息。
mainCRTSartup函数之前的高级源码无法查看
mainCRTSartup -- _scrt_common_main() -- _scrt_common_main_seh() -- invoke_main() -- main()
main函数的识别
特征如下:
- 有3个参数,分别是命令行参数个数、命令行参数信息和环境变量信息,而且main函数是启动函数中唯一具有3个参数的函数。同理,WinMain也是启动函数中唯一具有4个参数的函数。
- main函数返回后需要调用exit函数,结束程序根据main函数调用的特征,找到入口代码第一次调用exit函数处,离exit最近的且有3个参数的函数通常就是main函数。
调用main()堆栈
样例代码
#include <stdio.h>
int main()
{
printf("Hello World!");
return 0;
}
通过VS2019查看main()函数调用的堆栈空间
转到mainCRTStartup()的反汇编,这是C++程序执行的第一个函数
mainCRTStartup先调用__scrt_common_main函数
转到__scrt_common_main()的反汇编
在反汇编代码中,__scrt_common_main第二个调用函数是__scrt_common_main_seh
之后__scrt_common_main_seh调用invoke_main()
转到invoke_main()的反汇编,发现调用 _main()函数
回到源代码
这就到main函数了,这里就到了main函数了,这里main函数有三个参数,分别是
- __argc是参数个数
- __argv是参数列表
- 最后一个是环境指针
可以得出规律
- mainCRTStartup调用__scrt_common_main
- __scrt_common_main调用__scrt_common_main_seh,对应汇编是在第二个call上
- __scrt_common_main_seh调用invoke_main,对应在汇编的特征如下:
- int const main_result = invoke_main();源码是赋值给一个变量
- 这个函数参数是0个,后面会跟exit函数,而exit函数的参数 是 invoke_main 的返回值
- call指令后面必有一个mov [ebp-0xXXX],eax
- invoke_main调用main
x64dbg定位main函数
进入程序入口,调用mainCRTStartup函数
调用__scrt_common_main函数
调用_scrt_common_main_seh
调用invoke_main比较复杂,先分析一下它的特征,在scrt_common_main_seh
会把invoke_main的返回值给main_result
int const main_result = invoke_main();
返回值会伴随一个exit 和 一个main_result,必然有一个ret
if (!__scrt_is_managed_app())
exit(main_result);
return main_result;
定位invoke_main方法:
- 找到第一个ret
- 定位到call XXXX
- call指令后面必有一个mov [ebp-0xXXX],eax
定位ret
定位invoke_main()
定位到main()函数