注入代碼的方式比較
注入shellcode
優點:
1. 簡單,隻需要EXE的一部分。代碼可以用C\C++或彙編寫
缺點:
1. 要寫位置無關代碼,這意味着不能直接使用全局變量、其他編譯單元的函數(包括CRT的
memcpy
)、API等。如果要使用則要由源程序配置設定空間、計算API在目标程序的位址,并傳到目标程序的shellcode。或者shellcode自己計算
LoadLibrary
和
GetProcAddress
的位址也行
2. 沒有符号檔案,難以調試
注入DLL
建議沒什麼特殊需要都優先選擇注入DLL
優點:
1. 代碼沒什麼限制
2. 調試友善,VS可以直接在源代碼下斷點,附加到目标程序,注入DLL後可以正常調試
缺點:
1. 程式要帶上一個DLL。其實我覺得不算缺點,大不了把DLL放到EXE資源裡,要注入時釋放到一個臨時目錄
2. 容易被檢測到注入,不是幹壞事的話也可以忽略這點
注入EXE
優點:
1. 隻需要EXE,少一個DLL檔案。有些遊戲更新檔和修改器是這麼幹的
2. 跟shellcode比起來,因為自帶重定位表、IAT等東西,可以不寫位置無關代碼,隻要修複了重定位和IAT就可以正常運作大部分代碼。而且作業系統在載入源程序時已經做了IAT修複,假設
kernel32.dll
子產品在每個程序加載位址一樣,就可以在目标程序修複IAT之前直接調用部分API
缺點:
1. 沒有符号檔案,難以調試
2. 不能依賴于全局變量的初始狀态,這意味着不能使用靜态連結的CRT,因為CRT的很多函數依賴于全局變量的初始值(比如
malloc
)
注入EXE的方法
前置知識,這裡面提到的不再細講:
1. 注入DLL,這裡用到遠線程注入
2. 加載PE檔案,用到PE檔案的結構和修複重定位、IAT
完整源碼:InjectExe
1. 把整個EXE和需要的變量寫入目标程序
typedef int(* RemoteCallbackType)();
// 需要傳到目标程序的變量
struct InjectionContext
{
LPVOID imageBase; // 目标程序中EXE的位址
uintptr_t offset; // 目标程序中EXE的位址 - 源程序中EXE的位址,用來做重定位
RemoteCallbackType callback; // 注入完畢後在目标程序調用的回調
};
// 注入EXE到process,然後在目标程序調用callback
// callback必須傳回0,否則視為注入失敗
// 如果注入成功則傳回EXE在目标程序的位址,否則傳回NULL
LPVOID InjectExe(HANDLE process, RemoteCallbackType callback)
{
auto dosHeader = (PIMAGE_DOS_HEADER)GetModuleHandle(NULL);
auto ntHeader = PIMAGE_NT_HEADERS((uintptr_t)dosHeader + dosHeader->e_lfanew);
auto imageBase = (LPVOID)dosHeader;
SIZE_T imageSize = ntHeader->OptionalHeader.SizeOfImage;
LPVOID remoteImageBase = NULL;
LPVOID remoteCtx = NULL;
HANDLE remoteThread = NULL;
try
{
// 優先在目前的imageBase配置設定位址,這樣就不用重定位,配置設定失敗則選擇其他位址
remoteImageBase = VirtualAllocEx(process, imageBase, imageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (remoteImageBase == NULL)
{
remoteImageBase = VirtualAllocEx(process, NULL, imageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (remoteImageBase == NULL)
throw runtime_error("Failed to allocate remoteImageBase");
}
// 把源程序EXE寫到目标程序
if (!WriteProcessMemory(process, remoteImageBase, imageBase, imageSize, NULL))
throw runtime_error("Failed to write remoteImageBase");
// 準備要傳到目标程序的變量
uintptr_t offset = (uintptr_t)remoteImageBase - (uintptr_t)imageBase;
InjectionContext ctx;
ctx.imageBase = remoteImageBase;
ctx.offset = offset;
// 注意目标程序中的callback位址 = 源程序中位址 + offset
ctx.callback = RemoteCallbackType((uintptr_t)callback + offset);
// 把ctx寫到目标程序
remoteCtx = VirtualAllocEx(process, NULL, sizeof(ctx), MEM_COMMIT, PAGE_READWRITE);
if (remoteCtx == NULL)
throw runtime_error("Failed to allocate remoteCtx");
if (!WriteProcessMemory(process, remoteCtx, &ctx, sizeof(ctx), NULL))
throw runtime_error("Failed to write remoteCtx");
// 在目标程序調用RemoteStartup函數,參數為ctx,這個函數後面介紹
// 注意目标程序中的RemoteStartup位址 = 源程序中位址 + offset
LPTHREAD_START_ROUTINE remoteStartup = LPTHREAD_START_ROUTINE((uintptr_t)RemoteStartup + offset);
remoteThread = CreateRemoteThread(process, NULL, , remoteStartup, remoteCtx, , NULL);
if (remoteThread == NULL)
throw runtime_error("Failed to create remote thread");
// 等待注入結束并取RemoteStartup傳回值
WaitForSingleObject(remoteThread, INFINITE);
DWORD exitCode;
GetExitCodeThread(remoteThread, &exitCode);
if (exitCode != )
{
SetLastError(exitCode);
throw runtime_error("RemoteStartup failed");
}
}
catch (runtime_error& e)
{
cerr << e.what() << ": 0x" << hex << GetLastError() << oct << endl;
CloseHandle(remoteThread);
VirtualFreeEx(process, remoteCtx, sizeof(InjectionContext), MEM_DECOMMIT);
VirtualFreeEx(process, remoteImageBase, imageSize, MEM_DECOMMIT);
return NULL;
}
CloseHandle(remoteThread);
VirtualFreeEx(process, remoteCtx, sizeof(InjectionContext), MEM_DECOMMIT);
return remoteImageBase;
}
2. 修複重定位和IAT
RemoteStartup
函數很簡單,就是修複重定位和IAT,然後調用callback。修複重定位和IAT的代碼我是從mmLoader修改的
DWORD WINAPI RemoteStartup(InjectionContext* ctx)
{
if (!RelocateModuleBase(ctx))
return ;
if (!ResolveImportTable(ctx))
return ;
return ctx->callback();
}
bool RelocateModuleBase(InjectionContext* ctx)
{
// offset = 0,不需要重定位
if (ctx->offset == )
return true;
auto dosHeader = (PIMAGE_DOS_HEADER)ctx->imageBase;
auto ntHeader = PIMAGE_NT_HEADERS((uintptr_t)dosHeader + dosHeader->e_lfanew);
if (ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress ==
|| ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size == )
return true;
auto relocation = PIMAGE_BASE_RELOCATION((uintptr_t)ctx->imageBase +
ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
if (relocation == NULL) // 無效的重定位表
return false;
while (relocation->VirtualAddress + relocation->SizeOfBlock != )
{
auto relocationData = PWORD((uintptr_t)relocation + sizeof(IMAGE_BASE_RELOCATION));
int nRelocationData = (relocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
for (int i = ; i < nRelocationData; i++)
{
if (relocationData[i] >> == IMAGE_REL_BASED_HIGHLOW)
{
auto address = (uint32_t*)((uintptr_t)ctx->imageBase + relocation->VirtualAddress + (relocationData[i] & x0FFF));
*address += (uint32_t)ctx->offset;
}
#ifdef _WIN64
if (relocationData[i] >> == IMAGE_REL_BASED_DIR64)
{
auto address = (uint64_t*)((uintptr_t)ctx->imageBase + relocation->VirtualAddress + (relocationData[i] & x0FFF));
*address += ctx->offset;
}
#endif
}
relocation = PIMAGE_BASE_RELOCATION((uintptr_t)relocation + relocation->SizeOfBlock);
}
return true;
}
bool ResolveImportTable(InjectionContext* ctx)
{
auto dosHeader = (PIMAGE_DOS_HEADER)ctx->imageBase;
auto ntHeader = PIMAGE_NT_HEADERS((uintptr_t)dosHeader + dosHeader->e_lfanew);
if (ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress ==
|| ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size == )
return true;
auto importTable = PIMAGE_IMPORT_DESCRIPTOR((uintptr_t)ctx->imageBase +
ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
for (; importTable->Name != NULL; importTable++)
{
// 加載DLL
auto dllName = PCHAR((uintptr_t)ctx->imageBase + importTable->Name);
// 假設kernel32.dll在源程序和目标程序被加載到同一個位址,修複IAT之前就可以直接使用LoadLibrary等API了
HMODULE module = LoadLibraryA(dllName);
if (module == NULL)
return false;
PIMAGE_THUNK_DATA originalThunk;
if (importTable->OriginalFirstThunk)
originalThunk = PIMAGE_THUNK_DATA((uintptr_t)ctx->imageBase + importTable->OriginalFirstThunk);
else
originalThunk = PIMAGE_THUNK_DATA((uintptr_t)ctx->imageBase + importTable->FirstThunk);
auto iatThunk = PIMAGE_THUNK_DATA((uintptr_t)ctx->imageBase + importTable->FirstThunk);
for (; originalThunk->u1.AddressOfData != NULL; originalThunk++, iatThunk++)
{
FARPROC function;
if (IMAGE_SNAP_BY_ORDINAL(originalThunk->u1.Ordinal))
function = GetProcAddress(module, (LPCSTR)IMAGE_ORDINAL(originalThunk->u1.Ordinal));
else
{
auto nameInfo = PIMAGE_IMPORT_BY_NAME((uintptr_t)ctx->imageBase + originalThunk->u1.AddressOfData);
function = GetProcAddress(module, nameInfo->Name);
}
iatThunk->u1.Function = (uintptr_t)function;
}
}
return true;
}
3. 使用方法
int main()
{
// 注入到記事本
HWND hwnd = FindWindow(_T("Notepad"), NULL);
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (process == NULL)
{
cerr << "Failed to open process: 0x" << hex << GetLastError() << oct << endl;
return ;
}
LPVOID remoteImageBase = InjectExe(process, RemoteMain);
if (remoteImageBase == NULL)
return ;
cout << "remoteImageBase = 0x" << remoteImageBase << endl;
return ;
}
// 這個函數在目标程序調用
int RemoteMain()
{
// 測試使用API
MessageBox(NULL, _T("RemoteMain()"), _T("InjectExe"), MB_OK);
// 測試malloc(),注意不能使用靜态連結的CRT
free(malloc());
// 擷取目前程序EXE路徑
array<WCHAR, MAX_PATH> processPath;
GetModuleFileNameW(GetModuleHandle(NULL), &processPath.front(), MAX_PATH);
wstringstream stream;
stream << L"Hello world!\nI'm called from " << &processPath.front();
MessageBoxW(NULL, stream.str().c_str(), L"InjectExe", MB_OK);
// 也可以在這裡添加hook,就不示範了
return ;
}
效果
拿記事本測試的效果,我這裡記事本是64位程式,是以編譯時也要用64位配置:
