欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

PE LOADER,可运行MS自带的程序

PE LOADER,可运行MS自带的程序

之前放着的代码,没啥用,干脆把以前的文档整理下发出来。
下面就是在实现一个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

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途。
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功