rewolf-wow64ext的目的就是讓運行在Wow64環境中的x86應用程序可以直接調用x64下ntdll.dll中的Native API。
學習中可以得到幾個結論
- 在X64環境下的進程,32位程序,映射了兩個地址空間,一個32位,一個64位。而且這兩種工作模式是可以切換的的。
- WOW64進程中的R12寄存器指向其64位的TEB結構(線程環境塊)。
- 我們可以將進程的32位模式改為64位模式,然后來調用64位的函數。
進程的32位模式改為64位模式的具體代碼
主要方法是 借助retf將CS寄存器的值設置為0x33。
在CPU中,CS的全拼為"Code Segment",翻譯為"代碼段寄存器",對應於內存中的存放代碼的內存區域,用來存放內存代碼段區域的入口地址(段基址)。
在CPU執行指令時,通過代碼段寄存器(CS,Code Segment)和指令指針寄存器(IP,Instruction Pointer)來確定要執行的下一條指令的內存地址。
至於為什么要將CS寄存器的值設置為0x33。因為我們所需CPU的解碼模式需要是64位模式,而當前模式是32位。64位CPU是通過GDT表中CS段所對應的表項中L標志位來確定當前解碼模式的。
看雪論壇上這篇關於Wow64的原理的文章https://bbs.pediy.com/thread-221236.htm 介紹了這一機制
#pragma once #define EMIT(a) __asm __emit (a) #define X64_Start_with_CS(_cs) \ { \ EMIT(0x6A) EMIT(_cs) /*1 push _cs 壓入我們傳入的cs */ \ EMIT(0xE8) EMIT(0) EMIT(0) EMIT(0) EMIT(0) /*2 call $+5 壓入eip,jmp到3 */ \ EMIT(0x83) EMIT(4) EMIT(0x24) EMIT(5) /*3 add dword [esp], 5 跳過接下來五個字節 得到下下條指令的地址 */ \ EMIT(0xCB) /*4 retf 先彈出堆棧中的IP/EIP,然后彈出CS */ \ } /* 00EE169E 6A 33 push 33h 00EE16A0 E8 00 00 00 00 call main+25h (0EE16A5h) 00EE16A5 83 04 24 05 add dword ptr [esp],5 00EE16A9 CB retf //當代碼執行完這一句以后 eip的值為00EE16AA cs=33h 00EE16AA E8 00 00 00 00 call main+2Fh (0EE16AFh) 00EE16AF C7 44 24 04 23 00 00 00 mov dword ptr [esp+4],23h 00EE16B7 83 04 24 0D add dword ptr [esp],0Dh 00EE16BB CB retf */ #define X64_End_with_CS(_cs) \ { EMIT(0xE8) EMIT(0) EMIT(0) EMIT(0) EMIT(0) /* call $+5 */ \ EMIT(0xC7) EMIT(0x44) EMIT(0x24) EMIT(4) EMIT(_cs) EMIT(0) EMIT(0) EMIT(0) /* mov dword [rsp + 4], _cs */ \ EMIT(0x83) EMIT(4) EMIT(0x24) EMIT(0xD) /* add dword [rsp], 0xD */ \ EMIT(0xCB) /* retf */ \ } #define X64_Start() X64_Start_with_CS(0x33) #define X64_End() X64_End_with_CS(0x23)
ret,iret和retf的區別 ,call $+5的意思
ret:也可以叫做近返回,即段內返回。處理器從堆棧中彈出IP或者EIP,然后根據當前的CS:IP跳轉到新的執行地址。如果之前壓棧的還有其余的參數,則這些參數也會被彈出。 retf:也叫遠返回,從一個段返回到另一個段。先彈出堆棧中的IP/EIP,然后彈出CS,有之前壓棧的參數也會彈出。(近跳轉與遠跳轉的區別就在於CS是否壓棧。) iret:用於從中斷返回,會彈出IP/EIP,然后CS,以及一些標志。然后從CS:IP執行。 $表示當前地址,call $+5表示,調用用 本地址+5字節后的 子程序
我們首先要自己定義一個拷貝內存的函數。
void getMem64(void* dstMem, DWORD64 srcMem, size_t sz) { if ((nullptr == dstMem) || (0 == srcMem) || (0 == sz)) return; reg64 _src = { srcMem }; __asm { X64_Start(); ;// below code is compiled as x86 inline asm, but it is executed as x64 code ;// that's why it need sometimes REX_W() macro, right column contains detailed ;// transcription how it will be interpreted by CPU push edi ;// push rdi push esi ;// push rsi ;// mov edi, dstMem ;// mov edi, dword ptr [dstMem] ; high part of RDI is zeroed REX_W mov esi, _src.dw[0] ;// mov rsi, qword ptr [_src] mov ecx, sz ;// mov ecx, dword ptr [sz] ; high part of RCX is zeroed ;// mov eax, ecx ;// mov eax, ecx and eax, 3 ;// and eax, 3 最后2位 shr ecx, 2 ;// shr ecx, 2 除4 因為接下來 每次MOVSD 傳送四個字節. ;// rep movsd ;// rep movs dword ptr [rdi], dword ptr [rsi] ;// test eax, eax ;// test eax, eax 比較是否為空 je _move_0 ;// je _move_0 cmp eax, 1 ;// cmp eax, 1 是否為一 je _move_1 ;// je _move_1 ;// movsw ;// movs word ptr [rdi], word ptr [rsi] cmp eax, 2 ;// cmp eax, 2 je _move_0 ;// je _move_0 ;// _move_1: ;// movsb ;// movs byte ptr [rdi], byte ptr [rsi] ;// _move_0: ;// 還原后返回 pop esi ;// pop rsi pop edi ;// pop rdi X64_End(); } }
現在我們可以去查找目標函數地址
思想是 目標函數從動態庫(ntdll)中獲得,我們需要從LDR中匹配動態庫,LDR可以在PEB中找到,PEB可以在TEB中找到,TEB可以通過R12寄存器獲得。
故 R12-TEB-PEB-LDR-NTDLL。
獲得R12 #define _R12 12 #define X64_Push(r) EMIT(0x48 | ((r) >> 3)) EMIT(0x50 | ((r) & 7)) #define X64_Pop(r) EMIT(0x48 | ((r) >> 3)) EMIT(0x58 | ((r) & 7)) 獲得TEB DWORD64 getTEB64() { reg64 reg; reg.v = 0; X64_Start(); // R12 register should always contain pointer to TEB64 in WoW64 processes X64_Push(_R12); // below pop will pop QWORD from stack, as we're in x64 mode now __asm pop reg.dw[0] X64_End(); return reg.v; } 獲得PEB PEB64 peb64; getMem64(&peb64, teb64.ProcessEnvironmentBlock, sizeof(PEB64)); 獲得LDR PEB_LDR_DATA64 ldr; getMem64(&ldr, peb64.Ldr, sizeof(PEB_LDR_DATA64)); 獲得ntdll基地址 wchar_t lpModuleName=L"ntdll.dll" DWORD64 LastEntry = peb64.Ldr + offsetof(PEB_LDR_DATA64, InLoadOrderModuleList); LDR_DATA_TABLE_ENTRY64 head; head.InLoadOrderLinks.Flink = ldr.InLoadOrderModuleList.Flink; do { getMem64(&head, head.InLoadOrderLinks.Flink, sizeof(LDR_DATA_TABLE_ENTRY64)); wchar_t* tempBuf = (wchar_t*)malloc(head.BaseDllName.MaximumLength); if (nullptr == tempBuf) return 0; WATCH(tempBuf); getMem64(tempBuf, head.BaseDllName.Buffer, head.BaseDllName.MaximumLength); if (0 == _wcsicmp(lpModuleName, tempBuf)) return head.DllBase; } while (head.InLoadOrderLinks.Flink != LastEntry);
找到模塊基地址以后,我們就可以通過PE文件結構去獲得我們需要的函數了
思想是 得到目標函數地址,需要利用函數名找到對應函數的相對偏移RVA,那就需要找到IMAGE_EXPORT_DIRECTORY結構體下的 AddressOfNames,AddressOfNameOrdinals,AddressOfFunctions 三個成員。
故 IMAGE_DOS_HEADER---IMAGE_NT_HEADERS64---IMAGE_DATA_DIRECTORY---IMAGE_EXPORT_DIRECTORY-AddressOfNames,AddressOfNameOrdinals,AddressOfFunctions---目標函數RVA
DWORD64 getLdrGetProcedureAddress() { DWORD64 modBase = getNTDLL64(); if (0 == modBase) return 0; IMAGE_DOS_HEADER idh; getMem64(&idh, modBase, sizeof(idh)); IMAGE_NT_HEADERS64 inh; getMem64(&inh, modBase + idh.e_lfanew, sizeof(IMAGE_NT_HEADERS64)); IMAGE_DATA_DIRECTORY & idd = inh.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; if (0 == idd.VirtualAddress) return 0; IMAGE_EXPORT_DIRECTORY ied; getMem64(&ied, modBase + idd.VirtualAddress, sizeof(ied)); DWORD* rvaTable = (DWORD*)malloc(sizeof(DWORD)*ied.NumberOfFunctions); if (nullptr == rvaTable) return 0; WATCH(rvaTable); getMem64(rvaTable, modBase + ied.AddressOfFunctions, sizeof(DWORD)*ied.NumberOfFunctions); WORD* ordTable = (WORD*)malloc(sizeof(WORD)*ied.NumberOfFunctions); if (nullptr == ordTable) return 0; WATCH(ordTable); getMem64(ordTable, modBase + ied.AddressOfNameOrdinals, sizeof(WORD)*ied.NumberOfFunctions); DWORD* nameTable = (DWORD*)malloc(sizeof(DWORD)*ied.NumberOfNames); if (nullptr == nameTable) return 0; WATCH(nameTable); getMem64(nameTable, modBase + ied.AddressOfNames, sizeof(DWORD)*ied.NumberOfNames); // lazy search, there is no need to use binsearch for just one function for (DWORD i = 0; i < ied.NumberOfFunctions; i++) { if (!cmpMem64("LdrGetProcedureAddress", modBase + nameTable[i], sizeof("LdrGetProcedureAddress"))) continue; else return modBase + rvaTable[ordTable[i]]; } return 0; }
注意!我們得到函數地址以后,不能直接調用
需要通過X64Call()來執行。 X64調用約定前4個參數通過rcx,rdx,r8,r9來傳遞,之后通過堆棧傳遞。
#pragma warning(push) #pragma warning(disable : 4409) DWORD64 __cdecl X64Call(DWORD64 func, int argC, ...) { if (!g_isWow64) return 0; va_list args;//VA_LIST 是在C語言中解決變參問題的一組宏,所在頭文件:#include <stdarg.h>,用於獲取不確定個數的參數。 va_start(args, argC);//VA_START宏,獲取可變參數列表的第一個參數的地址(ap是類型為va_list的指針,v是可變參數最左邊的參數): reg64 _rcx = { (argC > 0) ? argC--, va_arg(args, DWORD64) : 0 }; reg64 _rdx = { (argC > 0) ? argC--, va_arg(args, DWORD64) : 0 };//VA_ARG宏,獲取可變參數的當前參數,返回指定類型並將指針指向下一參數(t參數描述了當前參數的類型): reg64 _r8 = { (argC > 0) ? argC--, va_arg(args, DWORD64) : 0 }; reg64 _r9 = { (argC > 0) ? argC--, va_arg(args, DWORD64) : 0 }; reg64 _rax = { 0 }; reg64 restArgs = { (DWORD64)&va_arg(args, DWORD64) };//剩余成員用堆棧傳遞 // conversion to QWORD for easier use in inline assembly reg64 _argC = { (DWORD64)argC }; DWORD back_esp = 0; WORD back_fs = 0; __asm { ;// reset FS segment, to properly handle RFG mov back_fs, fs mov eax, 0x2B mov fs, ax ;// keep original esp in back_esp variable mov back_esp, esp ;// align esp to 0x10, without aligned stack some syscalls may return errors ! ;// (actually, for syscalls it is sufficient to align to 8, but SSE opcodes ;// requires 0x10 alignment), it will be further adjusted according to the ;// number of arguments above 4 ;//對齊 and esp, 0xFFFFFFF0 X64_Start(); ;// below code is compiled as x86 inline asm, but it is executed as x64 code ;// that's why it need sometimes REX_W() macro, right column contains detailed ;// transcription how it will be interpreted by CPU ;// fill first four arguments REX_W mov ecx, _rcx.dw[0] ;// mov rcx, qword ptr [_rcx] REX_W mov edx, _rdx.dw[0] ;// mov rdx, qword ptr [_rdx] push _r8.v ;// push qword ptr [_r8] X64_Pop(_R8); ;// pop r8 push _r9.v ;// push qword ptr [_r9] X64_Pop(_R9); ;// pop r9 ;// REX_W mov eax, _argC.dw[0] ;// mov rax, qword ptr [_argC] ;// ;// final stack adjustment, according to the ;// ;// number of arguments above 4 ;// test al, 1 ;// test al, 1 jnz _no_adjust ;// jnz _no_adjust sub esp, 8 ;// sub rsp, 8 對齊對齊對齊!假如有單個參數 需要對齊!!! _no_adjust: ;// ;// push edi ;// push rdi REX_W mov edi, restArgs.dw[0] ;// mov rdi, qword ptr [restArgs] ;// ;// put rest of arguments on the stack ;// REX_W test eax, eax ;// test rax, rax jz _ls_e ;// je _ls_e REX_W ea edi, dword ptr [edi + 8*eax - 8] ;// lea rdi, [rdi + rax*8 - 8]取最后一個的地址 ;// _ls: ;// REX_W test eax, eax ;// test rax, rax 判斷是否為0 jz _ls_e ;// je _ls_e push dword ptr [edi] ;// push qword ptr [rdi] REX_W sub edi, 8 ;// sub rdi, 8 REX_W sub eax, 1 ;// sub rax, 1 jmp _ls ;// jmp _ls _ls_e: ;// ;// ;// create stack space for spilling registers ;// REX_W sub esp, 0x20 ;// sub rsp, 20h ;// call func ;// call qword ptr [func] ;// ;// cleanup stack ;// REX_W mov ecx, _argC.dw[0] ;// mov rcx, qword ptr [_argC] REX_W lea esp, dword ptr [esp + 8*ecx + 0x20] ;// lea rsp, [rsp + rcx*8 + 20h] ;// pop edi ;// pop rdi ;// // set return value ;// REX_W mov _rax.dw[0], eax ;// mov qword ptr [_rax], rax X64_End(); mov ax, ds mov ss, ax mov esp, back_esp ;// restore FS segment mov ax, back_fs mov fs, ax } return _rax.v; }
一個例子
DWORD64 __cdecl GetProcAddress64(DWORD64 hModule, char* funcName) { static DWORD64 _LdrGetProcedureAddress = 0; if (0 == _LdrGetProcedureAddress) { _LdrGetProcedureAddress = getLdrGetProcedureAddress();//從ntdll中獲得的目標函數地址 if (0 == _LdrGetProcedureAddress) return 0; } _UNICODE_STRING_T<DWORD64> fName = { 0 }; fName.Buffer = (DWORD64)funcName; fName.Length = (WORD)strlen(funcName); fName.MaximumLength = fName.Length + 1; DWORD64 funcRet = 0; X64Call(_LdrGetProcedureAddress, 4, (DWORD64)hModule, (DWORD64)&fName, (DWORD64)0, (DWORD64)&funcRet); return funcRet; }
最近在學反調試,其中就用到了這個方法,等學習完以后會將例子放上來