注入代碼的方式比較
注入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, 0, remoteStartup, remoteCtx, 0, NULL);
if (remoteThread == NULL)
throw runtime_error("Failed to create remote thread");
// 等待注入結束並取RemoteStartup返回值
WaitForSingleObject(remoteThread, INFINITE);
DWORD exitCode;
GetExitCodeThread(remoteThread, &exitCode);
if (exitCode != 0)
{
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 1;
if (!ResolveImportTable(ctx))
return 2;
return ctx->callback();
}
bool RelocateModuleBase(InjectionContext* ctx)
{
// offset = 0,不需要重定位
if (ctx->offset == 0)
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 == 0
|| ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size == 0)
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 != 0)
{
auto relocationData = PWORD((uintptr_t)relocation + sizeof(IMAGE_BASE_RELOCATION));
int nRelocationData = (relocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
for (int i = 0; i < nRelocationData; i++)
{
if (relocationData[i] >> 12 == IMAGE_REL_BASED_HIGHLOW)
{
auto address = (uint32_t*)((uintptr_t)ctx->imageBase + relocation->VirtualAddress + (relocationData[i] & 0x0FFF));
*address += (uint32_t)ctx->offset;
}
#ifdef _WIN64
if (relocationData[i] >> 12 == IMAGE_REL_BASED_DIR64)
{
auto address = (uint64_t*)((uintptr_t)ctx->imageBase + relocation->VirtualAddress + (relocationData[i] & 0x0FFF));
*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 == 0
|| ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size == 0)
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 1;
}
LPVOID remoteImageBase = InjectExe(process, RemoteMain);
if (remoteImageBase == NULL)
return 1;
cout << "remoteImageBase = 0x" << remoteImageBase << endl;
return 0;
}
// 這個函數在目標進程調用
int RemoteMain()
{
// 測試使用API
MessageBox(NULL, _T("RemoteMain()"), _T("InjectExe"), MB_OK);
// 測試malloc(),注意不能使用靜態鏈接的CRT
free(malloc(1));
// 獲取當前進程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 0;
}
效果
拿記事本測試的效果,我這裏記事本是64位程序,所以編譯時也要用64位配置: