C++逆向:对main函数的分析和识别
编辑在我们学习用C/C++
写程序的时候,书上说main
函数是程序执行的起点。那么实际上是这样的吗?让我们解开main
函数的神秘的面纱吧。本文介绍的是VC6
中的main
函数在反汇编中的识别和定位的方法,对于高版本的,如:VS2019
的main
函数在反汇编的识别和定位可以作为作业自己完成,具体的方法与本文所示的方法是一致的。
...
分析VC6
中的代码特征特征
找到main
函数的调用方
main
函数是用户入口第一个执行的代码,而不是程序执行的第一行代码。为什么呢?原因很简单,因为在用户程序真正要执行之前,需要准备好一系列的环境,如:全局数据的初始化、浮点协处理器的初始化等等。只有准备好用户程序执行所需要的环境之后,才能将控制权交给用户,否则程序执行可能会出现致命的问题。
那么如何找到程序执行的第一行代码呢?首先使用VC6
编译一个debug
版本的程序,因为这样它的符号都在,可以使用栈回溯来找到调用main
函数的调用,然后顺顺藤摸瓜,找到真正的程序入口。
代码示例:
int main(int argc, char** argv)
{
return 0;
}
在main
函数的入口地方,下一个断点,F5
运行之后,观察堆栈窗口:
可以观察到,main
函数是由一个叫mainCRTStartup
的函数调用的。在堆栈窗口双击这个函数,打开对应的源文件,发现其是一个名叫crt0
的.c
文件,位置位于VC98\sdk_2k3\src\crt
中。该文件源代码很长,源代码如下:
int _tmainCRTStartup(void)
{
int initret;
int mainret;
OSVERSIONINFOA *posvi;
int managedapp;
#ifdef _WINMAIN_
_TUCHAR *lpszCommandLine;
STARTUPINFO StartupInfo;
#endif /* _WINMAIN_ */
/*
* Dynamically allocate the OSVERSIONINFOA buffer, so we avoid
* triggering the /GS buffer overrun detection. That can't be
* used here, since the guard cookie isn't available until we
* initialize it from here!
*/
posvi = (OSVERSIONINFOA *)_alloca(sizeof(OSVERSIONINFOA));
/*
* Get the full Win32 version
*/
posvi->dwOSVersionInfoSize = sizeof(OSVERSIONINFOA);
(void)GetVersionExA(posvi);
_osplatform = posvi->dwPlatformId;
_winmajor = posvi->dwMajorVersion;
_winminor = posvi->dwMinorVersion;
/*
* The somewhat bizarre calculations of _osver and _winver are
* required for backward compatibility (used to use GetVersion)
*/
_osver = (posvi->dwBuildNumber) & 0x07fff;
if ( _osplatform != VER_PLATFORM_WIN32_NT )
_osver |= 0x08000;
_winver = (_winmajor << 8) + _winminor;
/*
* Determine if this is a managed application
*/
managedapp = check_managed_app();
#ifdef _MT
if ( !_heap_init(1) ) /* initialize heap */
#else /* _MT */
if ( !_heap_init(0) ) /* initialize heap */
#endif /* _MT */
fast_error_exit(_RT_HEAPINIT); /* write message and die */
#ifdef _MT
if( !_mtinit() ) /* initialize multi-thread */
fast_error_exit(_RT_THREAD); /* write message and die */
#endif /* _MT */
/*
* Initialize the Runtime Checks stuff
*/
#ifdef _RTC
_RTC_Initialize();
#endif /* _RTC */
/*
* Guard the remainder of the initialization code and the call
* to user's main, or WinMain, function in a __try/__except
* statement.
*/
__try {
if ( _ioinit() < 0 ) /* initialize lowio */
_amsg_exit(_RT_LOWIOINIT);
/* get wide cmd line info */
_tcmdln = (_TSCHAR *)GetCommandLineT();
/* get wide environ info */
_tenvptr = (_TSCHAR *)GetEnvironmentStringsT();
if ( _tsetargv() < 0 )
_amsg_exit(_RT_SPACEARG);
if ( _tsetenvp() < 0 )
_amsg_exit(_RT_SPACEENV);
initret = _cinit(); /* do C data initialize */
if (initret != 0)
_amsg_exit(initret);
#ifdef _WINMAIN_
StartupInfo.dwFlags = 0;
GetStartupInfo( &StartupInfo );
lpszCommandLine = _twincmdln();
mainret = _tWinMain( GetModuleHandleA(NULL),
NULL,
lpszCommandLine,
StartupInfo.dwFlags & STARTF_USESHOWWINDOW
? StartupInfo.wShowWindow
: SW_SHOWDEFAULT
);
#else /* _WINMAIN_ */
_tinitenv = _tenviron;
mainret = _tmain(__argc, _targv, _tenviron);
#endif /* _WINMAIN_ */
if ( !managedapp )
exit(mainret);
_cexit();
}
__except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) )
{
/*
* Should never reach here
*/
mainret = GetExceptionCode();
if ( !managedapp )
_exit(mainret);
_c_exit();
} /* end of try - except */
return mainret;
}
它里面有多个条件宏来构成的,对于不同的条件宏,调用main
函数的位置也不同。由于我测试用例创建的是控制台程序,故其走的是:
#else /* _WINMAIN_ */
_tinitenv = _tenviron;
mainret = _tmain(__argc, _targv, _tenviron);
#endif /* _WINMAIN_ */
这一条件宏的分支,这样就找到了调用main
函数的地方。
提取调用方特征
可以看到main
函数其实是3个参数,而不是两个参数,第三个参数是环境变量。main
函数的完整定义应该是:
int main(int argc, char* argv[], char *env[])
{
return 0;
}
经过调试,可以观察到env
变量所在的内存空间是一块堆区域,从内存窗口可以看到该指针数组是以0x0000,0000
作为数组的结尾标志,所以遍历环境变量的字符串,可以采用如下的形式进行遍历:
int main(int argc, char* argv[], char *env[])
{
for (int i = 0; env[i] != nullptr; ++i) {
printf("%s\n", env[i]);
}
return 0;
}
故,main
函数有三个参数,可以作为调用main
函数的调用方的代码特征。但是这个特征可能并不能立马就能找到,所以还需要其他特征:
- 在调用
main
函数之前,需要先调用GetCommandLineT
函数获取命令含参数以及调用GetEnvironmentStringsT
获取环变量字符串数组 - 然后传递三个参数作为
main
函数的参数,并调用main
函数 - 在
main
函数执行完毕后,调用exit
函数,并最终调用doexit
来退出进程
通过上述分析后,就可以定位main
函数了:
- 首先,
GetCommandLine
函数或者GetEnvrionmentStrings
函数定位到调用main
函数的调用方代码所在位置 - 然后,根据
main
函数具有三个参数,且靠近exit
最近的那个函数,即为main
函数。
反汇编中找到main
函数
使用动态分析工具OllyDbg
或者IDA
均可,不过对于IDA
来说,对于一些编译器的main
函数的特征,其已经做了签名了,故可以自动的定位到main
函数,而对于某些版本的编译器,其因为没有签名,故还是需要手动分析的,因此这一节模拟的是IDA
未能正确识别main
函数该怎么查找main
函数。
根据上面的分析,找到导入表的窗口,找到GetCommandLine
函数:
然后可以找到它的函数所在地址,然后通过交叉参考,双击后可以找到调用GetCommandLine
的地址:
就可以定位到mainCRTStartup
函数中调用GetCommandLineA
的地方了,反汇编代码为:
.text:004010BC loc_4010BC: ; CODE XREF: start+61j
.text:004010BC and [ebp+var_4], 0
.text:004010C0 call __ioinit
.text:004010C0
.text:004010C5 call ds:GetCommandLineA
然后向下找cexit
函数的位置:
.text:00401108 mov [ebp+var_1C], eax
.text:0040110B push eax ; Code
.text:0040110C call _exit
最后往上面找,最接近_exit
函数的带有三个参数的那个函数,即为main
函数:
.text:004010E9 mov eax, envp
.text:004010EE mov dword_40991C, eax
.text:004010F3 push eax ; envp
.text:004010F4 push argv ; argv
.text:004010FA push argc ; argc
.text:00401100 call _main
- 0
-
分享