我的知识记录

digSelf

C++逆向:对main函数的分析和识别

2021-09-02
C++逆向:对main函数的分析和识别

在我们学习用C/C++写程序的时候,书上说main函数是程序执行的起点。那么实际上是这样的吗?让我们解开main函数的神秘的面纱吧。本文介绍的是VC6中的main函数在反汇编中的识别和定位的方法,对于高版本的,如:VS2019main函数在反汇编的识别和定位可以作为作业自己完成,具体的方法与本文所示的方法是一致的。
...

分析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