win32程序分为两种:
1.控制台(/SUBSYSTEM:CONSOLE )
2.GUI(/SUBSYSTEM:WINDOWS)
首先看控制台版本的:
写一段最简单的,或者就直接使用编译器参数的默认main函数,如下:
- // EntryFunction.cpp : 定义控制台应用程序的入口点。
- //
- #include "stdafx.h"
- int _tmain(int argc, _TCHAR* argv[])
- {
- return 0;
- }
1.使用/MD选项
F9下断到main函数的标签处,F11后,在Call Stack中回溯到_tmainCRTStartup(),当前文件为crtexe.c。如下:
2.使用/MT选项
F9下断到main函数的标签处,F11后,在Call Stack中回溯到_tmainCRTStartup(),当前文件为crt0.c。如下:
也就是说,不同的运行时链接方式,其实现代码也是不同的。但是其入口点函数的名称是相同的,都是_tmainCRTStartup。这便是Windows下的启动函数(真正的入口函数)。
下面我们分别讨论,这两者在入口函数中都干了些什么。
首先看_security_init_cookie,这个函数在两个实现文件中是一样的,代码如下:
- /***
- *__security_init_cookie(cookie) - init buffer overrun security cookie.
- *
- *Purpose:
- * Initialize the global buffer overrun security cookie which is used by
- * the /GS compile switch to detect overwrites to local array variables
- * the potentially corrupt the return address. This routine is called
- * at EXE/DLL startup.
- *
- *Entry:
- *
- *Exit:
- *
- *Exceptions:
- *
- *******************************************************************************/
- void __cdecl __security_init_cookie(void)
- {
- UINT_PTR cookie;
- FT systime={0};
- LARGE_INTEGER perfctr;
- /*
- * Do nothing if the global cookie has already been initialized. On x86,
- * reinitialize the cookie if it has been previously initialized to a
- * value with the high word 0x0000. Some versions of Windows will init
- * the cookie in the loader, but using an older mechanism which forced the
- * high word to zero.
- */
- if (__security_cookie != DEFAULT_SECURITY_COOKIE
- #if defined (_M_IX86)
- && (__security_cookie & 0xFFFF0000) != 0
- #endif /* defined (_M_IX86) */
- )
- {
- __security_cookie_complement = ~__security_cookie;
- return;
- }
- /*
- * Initialize the global cookie with an unpredictable value which is
- * different for each module in a process. Combine a number of sources
- * of randomness.
- */
- GetSystemTimeAsFileTime(&systime.ft_struct);
- #if defined (_WIN64)
- cookie = systime.ft_scalar;
- #else /* defined (_WIN64) */
- cookie = systime.ft_struct.dwLowDateTime;
- cookie ^= systime.ft_struct.dwHighDateTime;
- #endif /* defined (_WIN64) */
- cookie ^= GetCurrentThreadId();
- cookie ^= GetCurrentProcessId();
- #if _CRT_NTDDI_MIN >= NTDDI_VISTA
- #if defined (_WIN64)
- cookie ^= (((UINT_PTR)GetTickCount64()) << 56);
- #endif /* defined (_WIN64) */
- cookie ^= (UINT_PTR)GetTickCount64();
- #endif /* _CRT_NTDDI_MIN >= NTDDI_VISTA */
- QueryPerformanceCounter(&perfctr);
- #if defined (_WIN64)
- cookie ^= (((UINT_PTR)perfctr.LowPart << 32) ^ perfctr.QuadPart);
- #else /* defined (_WIN64) */
- cookie ^= perfctr.LowPart;
- cookie ^= perfctr.HighPart;
- #endif /* defined (_WIN64) */
- /*
- * Increase entropy using ASLR relocation
- */
- cookie ^= (UINT_PTR)&cookie;
- #if defined (_WIN64)
- /*
- * On Win64, generate a cookie with the most significant word set to zero,
- * as a defense against buffer overruns involving null-terminated strings.
- * Don't do so on Win32, as it's more important to keep 32 bits of cookie.
- */
- cookie &= 0x0000FFFFffffFFFFi64;
- #endif /* defined (_WIN64) */
- /*
- * Make sure the cookie is initialized to a value that will prevent us from
- * reinitializing it if this routine is ever called twice.
- */
- if (cookie == DEFAULT_SECURITY_COOKIE)
- {
- cookie = DEFAULT_SECURITY_COOKIE + 1;
- }
- #if defined (_M_IX86)
- else if ((cookie & 0xFFFF0000) == 0)
- {
- cookie |= ( (cookie|0x4711) << 16);
- }
- #endif /* defined (_M_IX86) */
- __security_cookie = cookie;
- __security_cookie_complement = ~cookie;
- }
_security_init_cookie(使用/GS选项后提供)的作用是通过初始化一个全部变量用来检测局部缓冲区溢出和对函数返回地址潜在的修改(这可能导致程序执行流程改变,以至于执行不安全的shellcode)。
下图是使用了/GS选项后的函数栈帧:
L | ||
ARG2 | 参数2 | |
ARG1 | 参数1 | |
COOKIE VAR | cookie变量 | |
EBP | EBP栈指针 | |
H | RET | 函数返回地址 |
看到这个布局,就可以猜到,如果cookie被改变了(因为人为的缓冲区溢出导致),那么很有可能函数的返回地址被改变了,也可能ebp改变了。这样便能检测危险所在。
现在继续分析_security_init_cookie的代码,看看其到底做了些什么:
这个security cookie是一个已经预先定义好的全局变量(定义与gs_cookie.c中):
- * The global security cookie. This name is known to the compiler.
- * Initialize to a garbage non-zero value just in case we have a buffer overrun
- * in any code that gets run before __security_init_cookie() has a chance to
- * initialize the cookie to the final value.
- */
- DECLSPEC_SELECTANY UINT_PTR __security_cookie = DEFAULT_SECURITY_COOKIE;
- DECLSPEC_SELECTANY UINT_PTR __security_cookie_complement = ~(DEFAULT_SECURITY_COOKIE);
虽然这个值已经预先被定义好,但是在_security_init_cookie函数中会有一次机会继续对其重新赋值。
如果cookie已经初始化过,就不继续处理,但是如果在32位下,其高字节为0x0000,则需要对其进行重新赋值(获得一个高度随机的值),因为这个值在进程初始化后不会再发生变化。
现在看看security cookie是怎么被编译器安排并使用的。编译器会在可能发生栈缓冲区溢出的函数时,定义一个全局cookie,它位于局部变量和返回地址之间,测试代码如下:
- // EntryFunction.cpp : Defines the entry point for the console application.
- //
- #include "stdafx.h"
- #include <string.h>
- int _tmain(int argc, _TCHAR* argv[])
- {
- char szBuffer[5];
- strcpy( szBuffer, "hello world" );
- return 0;
- }
其中,szBuffer会在strcpy后发生溢出,反汇编代码如下:
- int _tmain(int argc, _TCHAR* argv[])
- {
- 00EE1990 push ebp
- 00EE1991 mov ebp,esp
- 00EE1993 sub esp,0D4h
- 00EE1999 push ebx
- 00EE199A push esi
- 00EE199B push edi
- 00EE199C lea edi,[ebp-0D4h]
- 00EE19A2 mov ecx,35h
- 00EE19A7 mov eax,0CCCCCCCCh
- 00EE19AC rep stos dword ptr es:[edi]
- 00EE19AE mov eax,dword ptr ds:[00F53084h] ;将全局变量cookie保存到eax
- 00EE19B3 xor eax,ebp ;将eax与ebp异或后保存到eax
- 00EE19B5 mov dword ptr [ebp-4],eax ;将异或的结果保存到ebp-4
- char szBuffer[5];
- strcpy( szBuffer, "hello world" );
- 00EE19B8 push 0F3FD34h
- 00EE19BD lea eax,[szBuffer]
- 00EE19C0 push eax
- 00EE19C1 call _strcpy (0EDF758h)
- 00EE19C6 add esp,8
- return 0;
- 00EE19C9 xor eax,eax
- }
- 00EE19CB push edx
- 00EE19CC mov ecx,ebp
- 00EE19CE push eax
- 00EE19CF lea edx,ds:[0EE19FCh]
- 00EE19D5 call @_RTC_CheckStackVars@8 (0EDF5E6h)
- 00EE19DA pop eax
- 00EE19DB pop edx
- 00EE19DC pop edi
- 00EE19DD pop esi
- 00EE19DE pop ebx
- 00EE19DF mov ecx,dword ptr [ebp-4] ;将ebp-4中的值(之前cookie与ebp异或后的值)保存到ecx
- 00EE19E2 xor ecx,ebp ;将ecx与ebp异或并保存到ecx中(如果ebp被破坏了,那么ecx中存放的应该与之前的cookie不同,否则相同)
- 00EE19E4 call @__security_check_cookie@4 (0EDF1DBh) ;fastcall 使用ecx传递参数(ecx存放了计算得出的cookie值)
- 00EE19E9 add esp,0D4h
- 00EE19EF cmp ebp,esp
- 00EE19F1 call __RTC_CheckEsp (0EDFEA1h)
- 00EE19F6 mov esp,ebp
- 00EE19F8 pop ebp
- 00EE19F9 ret
- }
在退出函数时在ecx中保存[ebp-4] xor ebp
调用_security_check_cookie函数,代码如下:
- /* x86 version written in asm to preserve all regs */
- __asm {
- cmp ecx, __security_cookie
- 00B4AFD0 cmp ecx,dword ptr ds:[0BB3084h] ;比较ecx中的值是否与全局cookie相同
- jne failure
- 00B4AFD6 jne failure (0B4AFDAh);不同则跳转到failure标签(_report_gsfailure函数)中
- rep ret /* REP to avoid AMD branch prediction penalty */
- 00B4AFD8 rep ret ;相同则直接返回,说明栈帧是没有被破坏的
- failure:
- jmp __report_gsfailure
- 00B4AFDA jmp ___report_gsfailure (0B3F645h)
通过_security_check_cookie函数便能知道ebp是否发生变化了。这是栈帧被破坏的一个标志。如果ebp发生变化了,则会调用__report_gsfailure显示错误。
现在分别看看在/MD和/MT下CRT入口函数的区别:
1./MT
__tmainCRTStartup代码如下:(crt0.c中)
- __declspec(noinline)
- int
- __tmainCRTStartup(
- void
- )
- {
- int initret;
- int mainret=0;
- int managedapp;
- #ifdef _WINMAIN_
- _TUCHAR *lpszCommandLine = NULL;
- WORD showWindowMode = 0;
- #ifndef _KERNELX
- showWindowMode = __crtGetShowWindowMode();
- __set_app_type(_GUI_APP);
- #endif /* _KERNELX */
- #else /* _WINMAIN_ */
- #ifndef _KERNELX
- __set_app_type(_CONSOLE_APP); ;设置当前程序类型:1.console 2.gui
- #endif /* _CRT_APP */
- #endif /* _WINMAIN_ */
- /*
- * Determine if this is a managed application
- */
- managedapp = check_managed_app(); ;检查是否为托管程序
- if ( !_heap_init() ) /* initialize heap */ ;初始化堆(这个在vs2012之前和之后版本不一样)
- fast_error_exit(_RT_HEAPINIT); /* write message and die */
- if( !_mtinit() ) /* initialize multi-thread */ ;初始化多线程环境
- fast_error_exit(_RT_THREAD); /* write message and die */
- /* Enable buffer count checking if linking against static lib */
- _CrtSetCheckCount(TRUE);
- /*
- * Initialize the Runtime Checks stuff
- */
- #if defined (_RTC)
- _RTC_Initialize();
- #endif /* defined (_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)
- fast_error_exit(_RT_LOWIOINIT); /* write message and die */
- #if !defined (_KERNELX)
- /* 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);
- #endif /* !defined (_CRT_APP) */
- initret = _cinit(TRUE); /* do C data initialize */ ;执行全局数据和浮点寄存器的初始化
- if (initret != 0)
- _amsg_exit(initret);
- #ifdef _WINMAIN_
- #if !defined (_KERNELX)
- lpszCommandLine = _twincmdln();
- #endif /* _KERNELX */
- mainret = _tWinMain( (HINSTANCE)&__ImageBase, ;调用gui的WinMain函数
- NULL,
- lpszCommandLine,
- showWindowMode
- );
- #else /* _WINMAIN_ */
- #if !defined (_KERNELX)
- _tinitenv = _tenviron;
- mainret = _tmain(__argc, _targv, _tenviron); ;调用console的main函数
- #else /* !defined (_KERNELX) */
- mainret = _tmain(0, NULL, NULL);
- #endif /* !defined (_KERNELX) */
- #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;
- }
大致的流程步骤如下:
1.设置应用程序类型,判断是否为托管程序
2.初始化堆
3.初始化多线程环境
4.初始化全局数据和浮点寄存器
5.获取命令行参数和环境变量
6.调用main/WinMain函数
分别深入源码;
1.设置应用程序类型,判断是否为托管程序
主要看判断是否为托管程序,跟进check_managed_app():
- /***
- *check_managed_app() - Check for a managed executable
- *
- *Purpose:
- * Determine if the EXE the startup code is linked into is a managed app
- * by looking for the COM Runtime Descriptor in the Image Data Directory
- * of the PE or PE+ header.
- *
- *Entry:
- * None
- *
- *Exit:
- * 1 if managed app, 0 if not.
- *
- *Exceptions:
- *
- *******************************************************************************/
- static int __cdecl check_managed_app (
- void
- )
- {
- PIMAGE_DOS_HEADER pDOSHeader;
- PIMAGE_NT_HEADERS pPEHeader;
- pDOSHeader = &__ImageBase;
- if (pDOSHeader->e_magic != IMAGE_DOS_SIGNATURE)
- {
- return 0;
- }
- pPEHeader = (PIMAGE_NT_HEADERS) ((BYTE *) pDOSHeader + pDOSHeader->e_lfanew);
- if (pPEHeader->Signature != IMAGE_NT_SIGNATURE)
- {
- return 0;
- }
- if (pPEHeader->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC)
- {
- return 0;
- }
- /* prefast assumes we are overflowing __ImageBase */
- #pragma warning(push)
- #pragma warning(disable:26000)
- if (pPEHeader->OptionalHeader.NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR)
- {
- return 0;
- }
- #pragma warning(pop)
- return pPEHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].VirtualAddress != 0;
- }
原理很简单,通过判断PE节表中的最后一项IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR,它保存了.net的一些高级结构IMAGE_COR20_HEADER.
2.初始化堆
_heap_init这个函数在vs2012之前的版本和之后的版本中实现是不一样的,在讨论这个前可以先推荐一个关于堆的文章(http://msdn.microsoft.com/en-us/library/ms810466.aspx).
先看vs2008版本的代码:
- /***
- *_heap_init() - Initialize the heap
- *
- *Purpose:
- * Setup the initial C library heap.
- *
- * NOTES:
- * (1) This routine should only be called once!
- * (2) This routine must be called before any other heap requests.
- *
- *Entry:
- * <void>
- *Exit:
- * Returns 1 if successful, 0 otherwise.
- *
- *Exceptions:
- * If heap cannot be initialized, the program will be terminated
- * with a fatal runtime error.
- *
- *******************************************************************************/
- int __cdecl _heap_init (
- int mtflag
- )
- {
- #if defined _M_AMD64 || defined _M_IA64
- // HEAP_NO_SERIALIZE is incompatible with the LFH heap
- mtflag = 1;
- #endif /* defined _M_AMD64 || defined _M_IA64 */
- // Initialize the "big-block" heap first.
- if ( (_crtheap = HeapCreate( mtflag ? 0 : HEAP_NO_SERIALIZE,
- BYTES_PER_PAGE, 0 )) == NULL )
- return 0;
- #ifndef _WIN64
- // Pick a heap, any heap
- __active_heap = __heap_select();
- if ( __active_heap == __V6_HEAP )
- {
- // Initialize the small-block heap
- if (__sbh_heap_init(MAX_ALLOC_DATA_SIZE) == 0)
- {
- HeapDestroy(_crtheap);
- _crtheap=NULL;
- return 0;
- }
- }
- #ifdef CRTDLL
- else if ( __active_heap == __V5_HEAP )
- {
- if ( __old_sbh_new_region() == NULL )
- {
- HeapDestroy( _crtheap );
- _crtheap=NULL;
- return 0;
- }
- }
- #endif /* CRTDLL */
- #elif defined _M_AMD64 || defined _M_IA64
- {
- // Enable the Low Fragmentation Heap for AMD64 and IA64 by default
- // It's the 8 byte overhead heap, and has generally better
- // performance charateristics than the 16 byte overhead heap,
- // particularly for apps that perform lots of small allocations
- ULONG HeapType = 2;
- HeapSetInformation(_crtheap, HeapCompatibilityInformation,
- &HeapType, sizeof(HeapType));
- }
- #endif /* defined _M_AMD64 || defined _M_IA64 */
- return 1;
- }