之前放着的代码,没啥用,干脆把以前的文档整理下发出来。
下面就是在实现一个PE LOADER过程中碰到的问题以及一些解决办法,更详细的请自己阅读代码,这个PE LOADER可以跑WINDOWS的大部分程序,在XP 下几乎MS的程序都可以跑,包括NOTEPAD, REGEDIT, 计算器等。
     主要的问题如下
     1)FormatMessage
     2)GetModuleFileName
     3)msvcrt与参数
     4)TLS
     5)捕获输出
     6)地址被占用的问题
     7)进程退出
     8)命令行
     9)重定向
 
Loader 要做的事情
1) 读取磁盘文件到内存
2) 处理导入表,重定位表,资源表
3) CALL入口函数

因为是加载EXE模块,XP的话默认加载基地址就是PE头种的ImageBase,这个地址空间一般状态是MEM_FREE,
XP
0:000> !address 0x01000000
    003a3000 : 003a3000 - 5fc4d000
                    Type     00000000 
                    Protect  00000001 PAGE_NOACCESS
                    State    00010000 MEM_FREE
                    Usage    RegionUsageFree
所以申请这地址开始的内存成功概率很大,这种情况下Loader会变得比较简单,只需要处理导入表就可以,而WIN 7因为采用了 地址随机分布 机制,EXE加载的地址不一定是ImageBase,ImageBase开始的内存已经被占用了
WIN 7
Usage:                  MemoryMappedFile
Allocation Base:        00870000
Base Address:           0096d000
End Address:            01470000 ;@@@@@@@@@@
Region Size:            00b03000
Type:                   00040000 MEM_MAPPED
State:                  00002000 MEM_RESERVE
Protect:                00000000 
Mapped file name:       PageFile
强制使用的后果是造成莫名的崩溃。
在XP为了让程序尽可能的在其默认加载的基地址运行,
需要减低ImageBase跟我们的EXE地址冲,解决办法就是我们编译的模块选一个加载的基地址。
这里我选的是 0x5FFF0000,这个地址就可以避免跟要RUN的EXE模块的基地址冲突了,另外为了能在WIN 7跑,还需要Disable Image Randomization (/DYNAMICBASE:NO)。为了避免DEP而导致出错,所以分配的内存的属性一律改为PAGE_EXECUTE_READWRITE
 
1)FormatMessage
XP很多程序的字符串都放在资源中,这样只要访问资源就可以了。但WIN 7中,很多字符串都是存在.MUI文件中而不是资源中。
因为微软的控制台程序几乎都会调用FormatMessage,所以这个函数如果失败,后面的功能就得不到执行。在WIN 7中需要对这个API进行处理,因为它没有去访问资源而是.MUI文件。MUI文件一般放在SYSTEM32\ZH-CN\目录下
,文件对应的.MUI文件可以通过GetFileMUIPath来获取
注:如果操作系统是 英文的或者其他语言的,那就不是ZH-CN了,具体可以通过API 
BOOL SetThreadPreferredUILanguages(
  DWORD  dwFlags,
  PCWSTR  pwszLanguagesBuffer,
  PULONG  pulNumLanguages
);来获取。
.MUI加载的时机
.MUI加载的时机是比较早的,也就是在进入MAIN之前被LDR给加载了。arp为例子,当运行arp.exe时,LdrpRunInitializeRoutines就会加载arp.exe.mui这个文件,然后才会进入到MAIN函数里面。
问题来了,因为我们自己加载要运行的模块,并没有走LdrpRunInitializeRoutines,自然我们进程空间就没有相应的.mui文件。解决办法就是需要自己LoadLibraryEx(RUN_IMAGE_FILE_PATH, NULL, NULL);一下。。最主要就是获取 .MUI文件的全路径。
DWORD WINAPI FormatMessage(
  __in          DWORD dwFlags,
  __in          LPCVOID lpSource,
  __in          DWORD dwMessageId,
  __in          DWORD dwLanguageId,
  __out         LPTSTR lpBuffer,
  __in          DWORD nSize,
  __in          va_list* Arguments
);
对访问.MUI获取字符串来说,dwFlags必须是要有FORMAT_MESSAGE_FROM_HMODULE
,所以这个标志是我们判断的重要依据。MS自己的lpSource传入为NULL,这样它会自动访问.MUI文件。而如果.MUI是我们自己加载的,那传入NULL无疑会导致失败,没能找到资源。所以需要HOOK这个API把lpSource改成我们LOAD .MUI文件的内存地址。
 
 
2)GetModuleFileName
崩溃
7282f0ef ff766c          push    dword ptr [esi+6Ch]
7282f0f2 ff1568838372    call    dword ptr [MFC42u!_imp__GetModuleFileNameW (72838368)]
7282f0f8 8d8584010000    lea     eax,[ebp+184h]
7282f0fe 6a2e            push    2Eh
7282f100 50              push    eax
7282f101 ff1558878372    call    dword ptr [MFC42u!_imp__wcsrchr (72838758)]
7282f107 66832000        and     word ptr [eax],0         ds:0023:00000000=????
eax = 0,可见GetModuleFileNameW返回的是NULL,而微软又没有对这个函数的返回值进行判断。所以导致崩溃。
为什么GetModuleFileNameW会崩溃呢?
GetModuleFileName的定义
DWORD WINAPI GetModuleFileName(
  __in          HMODULE hModule,
  __out         LPTSTR lpFilename,
  __in          DWORD nSize
);

跟进去看堆栈
0:000> kvnf
 #   Memory  ChildEBP RetAddr  Args to Child              
00           0012f650 7282f0f8 01000000 0012f874 00000104 kernel32!GetModuleFileNameW (FPO: [Non-Fpo])
传入的   hModule 的值是 01000000,注意这个值不是我们主程序的基地址,而是被RUN的EXE得基地址。
接着看
GetModuleFileName内部实现:
.text:7C80B458 loc_7C80B458:                           ; CODE XREF: GetModuleFileNameW(x,x,x)+85j
.text:7C80B458                 mov     [ebp+var_38], esi
.text:7C80B45B                 mov     ecx, [ebp+var_24]
.text:7C80B45E                 cmp     ecx, [esi+18h]
.text:7C80B461                 jz      short loc_7C80B481
.text:7C80B463                 mov     esi, [esi]
.text:7C80B465
.text:7C80B465 loc_7C80B465:                           ; CODE XREF: GetModuleFileNameW(x,x,x)+54j
.text:7C80B465                 mov     [ebp+var_2C], esi
.text:7C80B468                 cmp     esi, eax
.text:7C80B46A                 jnz     short loc_7C80B458
..........
.text:7C80B413 loc_7C80B413:                           ; CODE XREF: GetModuleFileNameW(x,x,x)+1C9j
.text:7C80B413                                         ; GetModuleFileNameW(x,x,x)+35979j
.text:7C80B413                 lea     eax, [ebp+var_1C]
.text:7C80B416                 push    eax
.text:7C80B417                 push    edi
.text:7C80B418                 push    1
.text:7C80B41A                 call    _LdrLockLoaderLock@12 ; LdrLockLoaderLock(x,x,x)
.text:7C80B41F                 mov     [ebp+ms_exc.disabled], edi
.text:7C80B422                 mov     eax, large fs:18h ;@@@@@@@@@@ TEB
.text:7C80B428                 mov     [ebp+var_30], eax
.text:7C80B42B                 mov     eax, [eax+30h] ;@@@@@@@@PEB
.text:7C80B42E                 mov     eax, [eax+0Ch] ; @@@@@@@@ _PEB_LDR_DATA
.text:7C80B431                 add     eax, 0Ch ;@@@@@@@@ nLoadOrderModuleList : _LIST_ENTRY
.text:7C80B434                 mov     [ebp+var_34], eax
.text:7C80B437                 mov     esi, [eax]
.text:7C80B439                 jmp     short loc_7C80B465
nLoadOrderModuleList 链表中成员的结构是:
typedef struct _LDR_MODULE
{
    LIST_ENTRY          InLoadOrderModuleList;   +0x00
    LIST_ENTRY          InMemoryOrderModuleList; +0x08  
    LIST_ENTRY          InInitializationOrderModuleList; +0x10
    void*               BaseAddress;  +0x18
    void*               EntryPoint;   +0x1c
    ULONG               SizeOfImage;
    UNICODE_STRING      FullDllName;
    UNICODE_STRING      BaseDllName;
    ULONG               Flags;
    SHORT               LoadCount;
    SHORT               TlsIndex;
    HANDLE              SectionHandle;
    ULONG               CheckSum;
    ULONG               TimeDateStamp;
} LDR_MODULE, *PLDR_MODULE;
可见 GetModuleFileName是通过遍历LoadOrderModuleList,然后看hModule 跟LDR_MODULE::BaseAddress 相等,相等的话就取出里面的FullDllName。但由于我们传入的hModule是被处理过的,所以自然在链表里找不到导致返回NULL,而返回之后MS又不做判断就直接引用返回值,导致崩溃。
 
 
3)msvcrt与参数
WIN 7 ARP.EXE
.text:0100239E                 push    dword_1005968
.text:010023A4                 push    dword_1005960
.text:010023AA                 call    _main
msvcrt.dll 第一次被加载的时候,会调用GetCommandLine来获取当前参数,然后进行处理填充msvcrt的内部变量 
msvcrt!__argc 和 msvcrt!__argv。之后通过__getmainargs, __wgetmainargs就可以获取到相应参数,这时候并不会再走GetCommandLine。WIN 7的ARP.EXE获取命令行参数就是通过__getmainargs。因为msvcrt.dll在进程运行的时候已经被加载,所以无法拦截到GetCommandLine. 俩个参数在LOAD msvcrt.dll时被初始化。。WIN 7每个进程一运行就会加载这个DLL.所以需要自己HOOK __getmainargs, __wgetmainargs
 
 
4)TLS
0100887a 648b0d2c000000  mov     ecx,dword ptr fs:[2Ch]
01008881 56              push    esi
01008882 8d3481          lea     esi,[ecx+eax*4]
01008885 57              push    edi
01008886 8b3e            mov     edi,dword ptr [esi]  ds:0023:00000000=????????
01008888 83bf0400000000  cmp     dword ptr [edi+4],0

因为是  mov     ecx,dword ptr fs:[2Ch] 访问TEB 的 ThreadLocalStoragePointer, 如果我们没模拟LOADER初始话得话就会失败。。。所以需要自己分析TLS目录获取数据,填到内存,然后修改 ThreadLocalStoragePointer指针。其实只需要分配合适空间大小就可以。。里面的内容程序自己会添加,如果你得程序没使用TLS,那你得线程的ThreadLocalStoragePointer就为NULL,显然如果要跑的程序使用TLS,那访问该地方就蹦而了
void LdrInitThreadTls(PIMAGE_TLS_DIRECTORY pTlsDir)
{
 PVOID *ThreadLocalStoragePointer = NULL;
 UCHAR *pData = NULL;
 ULONG TlsSize = 0;
 ULONG TlsInitDataSize = 0;
 TlsInitDataSize = pTlsDir->EndAddressOfRawData - pTlsDir->StartAddressOfRawData; 
 TlsSize = (pTlsDir->EndAddressOfRawData - pTlsDir->StartAddressOfRawData) + pTlsDir->SizeOfZeroFill;
 ThreadLocalStoragePointer = (PVOID*)malloc(TlsSize+ sizeof(PVOID));
 pData = (UCHAR*)ThreadLocalStoragePointer + sizeof(PVOID);
 pData = (UCHAR*)ThreadLocalStoragePointer ;
 memcpy( pData, (void *)pTlsDir->StartAddressOfRawData, TlsInitDataSize );
 memset( pData + TlsInitDataSize, 0, pTlsDir->SizeOfZeroFill );
 NtCurrentTeb()->ThreadLocalStoragePointer = ThreadLocalStoragePointer;
 *(PVOID*)ThreadLocalStoragePointer = ThreadLocalStoragePointer;
}
 
5)捕获输出
MS控制台的输出不是用printf,而是fprintf
.text:0100215B loc_100215B:                            ; CODE XREF: _PutMsg+5Ej
.text:0100215B                 mov     dl, [eax]
.text:0100215D                 inc     eax
.text:0100215E                 test    dl, dl
.text:01002160                 jnz     short loc_100215B
.text:01002162                 sub     eax, ecx
.text:01002164                 push    eax             ; cchDstLength
.text:01002165                 push    dword ptr [ebp+lpszSrc] ; lpszDst
.text:01002168                 push    dword ptr [ebp+lpszSrc] ; lpszSrc
.text:0100216B                 call    ds:__imp__CharToOemBuffA@12 ; CharToOemBuffA(x,x,x)
.text:01002171                 push    dword ptr [ebp+lpszSrc]
.text:01002174                 push    offset aS       ; "%s"
.text:01002179                 push    esi             ; File
.text:0100217A                 call    ds:__imp__fprintf
.text:01002180                 add     esp, 0Ch
.text:01002183                 push    dword ptr [ebp+lpszSrc] ; hMem
.text:01002186                 call    ds:__imp__LocalFree@4 ; LocalFree(x)
.text:0100218C                 mov     eax, edi
.text:0100218E                 pop     esi
所以只要 IAT HOOK fprintf就可以。
 
6)地址被占用的问题
PELOADER地址被占用的问题
问题引人
LOADER要加载一个EXE文件,这个EXE文件加载的地址是在0x400000。在我们LOADER的MAIN函数里面,这个地址已经被占用,而你是不能去Free这个地址重新分布的,这样可能会导致程序崩溃。
问题产生
LOADER引用了一些DLL里面的API,这样导入表里会出现这些DLL,当你LOADER运行的时候,系统会帮你加载这些DLL,这些DLL会进行初始话,创建线程等操作,那地址被占用的概率是非常大的。这个也是在TLS执行之前的。
问题解决
1)延迟加载DLL,保证导入表中只有kernel32.dll
2)不要使用MFC,因为这个时候很难有机会占用要加载的地址
3)用驱动HOOK 分配内存的函数,禁止其分配某个地址开始的一片地址。
如果要使用MFC写LOADER,那唯一的办法就是
延迟加载DLL,保证导入表中只有kernel32.dll
接着挂起创建自己,分配保留内存,退出。
 
 
7)进程退出
问题:
当在内存中运行的程序,比如arp.EXE执行完之后就会退出,那结果是ExitProcess被调用,那将是我们主进程也结束,显然我们不希望这样。
处理办法:HOOK ExitProcess。问题来了
对MS的许多控制台程序,它们退出都是调用exit
 #   Memory  ChildEBP RetAddr  Args to Child              
00           003ef554 7c92df5a 7c939b23 000007f4 00000000 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
01         4 003ef558 7c939b23 000007f4 00000000 00000000 ntdll!NtWaitForSingleObject+0xc (FPO: [3,0,0])
02        88 003ef5e0 7c921046 00c31ae8 77c0a5eb 77c31ae8 ntdll!RtlpWaitForCriticalSection+0x132 (FPO: [Non-Fpo])
03         8 003ef5e8 77c0a5eb 77c31ae8 7c8099cf 003ef608 ntdll!RtlEnterCriticalSection+0x46 (FPO: [1,0,0])
04        10 003ef5f8 77c09de8 00000008 7c8099cf 003ef61c msvcrt!_lock+0x30 (FPO: [Non-Fpo])
05        10 003ef608 77c09e90 00000000 00000000 00000000 msvcrt!_cinit+0x5e (FPO: [Non-Fpo])
06        14 003ef61c 01002735 00000000 00000000 003efbc4 msvcrt!exit+0x12 (FPO: [Non-Fpo])

里面持有了一个锁,所以如果HOOK ExitProcess, 那我们俩次在内存中运行arp.EXE之后就会死锁。所以对这类程序而言,不能HOOK ExitProcess,只能HOOK msvcrt!exit

另外还需要一个技巧,在内存运行的程序由我们EXE的一个线程来完成,否则HOOK msvcrt!exit也没办法处理好逻辑
void Fakeexit( 
    int status 
    )
{
 if( status == 0xbeebee )
  Oldexit(status);
 ExitThread(0xbeebee);
}
 
因为是创建的线程,所以只需要替换成ExitThread(0xbeebee);之后,进程退出这动作就被捕获了,然后我们替exit代为ExitThread(0xbeebee)后线程就退出返回控制到我们主EXE模块而不会出错。另外创建线程时要自己注意堆栈问题,否则堆栈可能会占用我们要放EXE的地址空间,导致失败,所以必须先分配内存空间后才能创建线程。
 
 
8)命令行
MIAN的参数怎样压的?

int __cdecl _tmainCRTStartup()
.text:60022B50 ; int __cdecl _tmainCRTStartup()
//
//调用GetCommandLineA之后调用setargv设置命令行,其实就是设置内部变量___argv和___argc,之后PUSH这俩个变量,在CALL MAIN
//
.text:60022BE8 loc_60022BE8:                           ; CODE XREF: __tmainCRTStartup+8Cj
.text:60022BE8                 call    ds:__imp__GetCommandLineA@0 ; GetCommandLineA()
.text:60022BEE                 mov     __acmdln, eax
.text:60022BF3                 call    j____crtGetEnvironmentStringsA
.text:60022BF8                 mov     __aenvptr, eax
.text:60022BFD                 call    j___setargv

....
.text:60022C42 loc_60022C42:                           ; CODE XREF: __tmainCRTStartup+E4j
.text:60022C42                 mov     ecx, __environ
.text:60022C48                 mov     ___initenv, ecx
.text:60022C4E                 mov     edx, __environ
.text:60022C54                 push    edx
.text:60022C55                 mov     eax, ___argv ;@@@@@@@@@内部变量
.text:60022C5A                 push    eax
.text:60022C5B                 mov     ecx, ___argc  ;@@@@@@@@@内部变量
.text:60022C61                 push    ecx
.text:60022C62                 call    j__main
 
int __cdecl _setargv()
.text:60031F40 ; int __cdecl _setargv()
.text:60031F40 __setargv       proc near               ; CODE XREF: j___setargvj
.text:60031F6E                 push    0               ; hModule
.text:60031F70                 call    ds:__imp__GetModuleFileNameA@12 ; GetModuleFileNameA(x,x,x)
.text:60031F76                 push    offset _pgmname ; _Value
.text:6003201B loc_6003201B:                           ; CODE XREF: __setargv+D4j
.text:6003201B                 lea     ecx, [ebp+numchars]
.text:6003201E                 push    ecx             ; numchars
.text:6003201F                 lea     edx, [ebp+numargs]
.text:60032022                 push    edx             ; numargs
.text:60032023                 mov     eax, [ebp+numargs]
.text:60032026                 mov     ecx, [ebp+p]
.text:60032029                 lea     edx, [ecx+eax*4]
.text:6003202C                 push    edx             ; args
.text:6003202D                 mov     eax, [ebp+p]
.text:60032030                 push    eax             ; argv
.text:60032031                 mov     ecx, [ebp+cmdstart]
.text:60032034                 push    ecx             ; cmdstart
.text:60032035                 call    parse_cmdline
.text:6003203A                 add     esp, 14h
.text:6003203D                 mov     edx, [ebp+numargs]
.text:60032040                 sub     edx, 1
.text:60032043                 mov     ___argc, edx  ;@@@@@@@@@内部变量
.text:60032049                 mov     eax, [ebp+p]
.text:6003204C                 mov     ___argv, eax ;@@@@@@@@@内部变量
.text:60032051                 xor     eax, eax

kernel32!GetCommandLineA:
7c812fbd a1f455887c      mov     eax,dword ptr [kernel32!BaseAnsiCommandLine+0x4 (7c8855f4)] ds:0023:7c8855f4=00151ee0
kernel32!GetCommandLineW:
7c817023 a10450887c      mov     eax,dword ptr [kernel32!BaseUnicodeCommandLine+0x4 (7c885004)]
7c817028 c3              ret
可见他们没走PEB。HOOK  GetCommandLine或者修改BaseAnsiCommandLine,BaseUnicodeCommandLine就可以捕获或者修改命令行传入的命令行
 
 
9)重定向
因为是加载EXE模块,XP的话默认加载基地址就是PE头种的ImageBase,而WIN 7因为采用了 地址随机分布 机制,EXE加载的地址不一定是
ImageBase。
所以需要重定向,但这里有个问题,就是加了/GS编译选项后编译出来的EXE会有问题。
在进入MAIN函数之前会执行这么一段代码
60022e60 8bff            mov     edi,edi
60022e62 55              push    ebp
60022e63 8bec            mov     ebp,esp
60022e65 e84ba6ffff      call    _________!ILT+1200(___security_init_cookie) (6001d4b5)
60022e6a e811000000      call    _________!__tmainCRTStartup (60022e80)
_________!__security_init_cookie:
600312f0 8bff            mov     edi,edi
600312f2 55              push    ebp
600312f3 8bec            mov     ebp,esp
600312f5 83ec18          sub     esp,18h
600312f8 c745f800000000  mov     dword ptr [ebp-8],0
600312ff c745fc00000000  mov     dword ptr [ebp-4],0
60031306 813d38d108604ee640bb cmp dword ptr [_________!__security_cookie (6008d138)],0BB40E64Eh
...........

里面会引用许多security cookie的全局变量,而这些变量没有在重定向表里,所以如果加载的 地址不是PE头种的ImageBase,就会出现访问错误,导致崩溃。
解决办法:

不要加载到 PE头的ImageBase 以外的地址。那如果跟我们的EXE地址冲突怎么办? 解决办法就是我们编译的模块选一个加载的基地址。
这里我选的是 0x5FFF0000,这个地址就可以避免跟要RUN的EXE模块的基地址冲突了,另外为了能在WIN 7跑,还需要Disable Image Randomization (/DYNAMICBASE:NO)。 为了避免DEP而导致出错,所以分配的内存的属性一律改为
PAGE_EXECUTE_READWRITE

上传的附件 RunPE.rar