概念
DLL注入(英語:DLL injection)是一種計算機編程技術,它可以強行使另一個進程加載一個動態鏈接庫以在其地址空間內運行指定代碼[1]。在Windows操作系統上,每個進程都有獨立的進程空間,即一個進程是無法直接操作另一個進程的數據的(事實上,不僅Windows,許多操作系統也是如此)。但是DLL注入是用一種不直接的方式,來實現操作其他進程的數據。假設我們有一個DLL文件,裏面有操作目標進程數據的程序代碼邏輯,DLL注入就是使目標進程加載這個DLL,加載後,這個DLL就成爲目標進程的一部分,目標進程的數據也就可以直接操作了。
本文編寫的代碼所做的工作相當於上圖中注入器所做的工作,所提及的DLL均爲C/C++語言生成的DLL。
DLL注入基本流程
(1) 打開目標進程
(2) 在目標進程開闢一段內存空間
(3) 往開闢的內存空間中寫入要注入的DLL的路徑
(4) 給目標創建一個線程, 加載DLL
1.打開目標進程
Windows下有個名爲OpenProcess的函數可以打開一個進程,它的原型如下:
HANDLE OpenProcess(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwProcessId
);
- dwDesiredAccess 訪問權限
- bInheritHandle 是否繼承句柄
- dwProcessId 要打開的進程pid
返回值:如果打開成功,返回一個進程句柄,否則返回NULL
代碼實現如下:
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid);
if (NULL == hProcess) {
OutputDebugString("Cannot open this process.\n");
return -1;
}
這段代碼根據進程的pid以PROCESS_ALL_ACCESS權限來打開一個進程,並返回進程句柄,進程pid可以通過以下方式獲得:
int GetPidByProcessName(const char* ProcessName) {
HANDLE Processes = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,NULL);
PROCESSENTRY32 ProcessInfo = { 0 };
ProcessInfo.dwSize = sizeof(PROCESSENTRY32);
while (Process32Next(Processes, &ProcessInfo)) {
if (strcmp(ProcessInfo.szExeFile, ProcessName) == 0) {
return ProcessInfo.th32ProcessID;
}
}
return -1;
}
該函數通過進程名返回它對應的進程pid
2. 在目標進程開闢一段內存空間
VirtualAllocEx函數可以在目標進程申請一塊內存空間,函數原型如下:
LPVOID VirtualAllocEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
- hProcess 要向其申請內存空間的進程
- lpAddress 申請的內存所在的地址,傳入NULL函數幫我們決定地址
- dwSize 申請的內存空間的大小,單位爲字節
- flAllocationType 要申請的內存空間類型
- flProtect 內存保護常量
返回值: 如果內存申請成功,返回內存空間的首地址,否則返回NULL
代碼實現如下:
LPVOID lpAddr = VirtualAllocEx(hProcess, NULL, strlen(DllPath), MEM_COMMIT, PAGE_READWRITE);
if (NULL == lpAddr) {
OutputDebugString("Cannot alloc memory.\n");
return -1;
}
這段代碼是在目標進程申請開闢一塊內存空間,申請開闢的內存空間大小是DLL完整路徑所佔用的字節數,申請成功將會返回內存空間的起始地址
3. 往開闢的內存空間中寫入要注入的DLL的路徑
WriteProcessMemory可以向指定的內存地址中寫入數據,函數原型如下:
BOOL WriteProcessMemory(
HANDLE hProcess,
LPVOID lpBaseAddress,
LPCVOID lpBuffer,
SIZE_T nSize,
SIZE_T *lpNumberOfBytesWritten
);
- hProcess 要寫入的進程的句柄
- lpBaseAddress 要寫入的目標地址
- lpBuffer 要寫入的數據
- nSize 要寫入的數據的大小
- lpNumberOfBytesWritten 一個用於接收傳入目標進程的字節數的指針變量
返回值: 返回一個非零值代表寫入成功,返回零則寫入失敗
代碼實現如下:
BOOL isOk = WriteProcessMemory(hProcess , lpAddr, DllPath, strlen(DllPath), NULL);
if (!isOk) {
OutputDebugString("Cannot write memory.\n");
return -1;
}
這段代碼是將DLL的完整路徑寫入到上一步開闢的內存空間中
4.給目標創建一個線程, 加載DLL
(1)GetModuleHandle函數根據模塊名稱得到模塊的句柄,原型如下:
HMODULE GetModuleHandleA(
LPCSTR lpModuleName
);
- lpModuleName 模塊名
返回值:指定模塊的句柄
(2)GetProcAddress函數可以根據函數名來得到模塊中的一個導出函數的地址,原型如下:
FARPROC GetProcAddress(
HMODULE hModule,
LPCSTR lpProcName
);
- hModule 模塊句柄
- lpProcName 導出函數名
返回值:相應的導出函數的地址
(3)CreateRemoteThread用於在指定進程的虛擬空間中開啓一個線程,原型如下:
HANDLE CreateRemoteThread(
HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
- hProcess 目標進程句柄
- lpThreadAttributes 線程安全屬性,傳入NULL時使用默認屬性
- dwStackSize 線程初始棧大小,傳入0使用默認棧大小
- lpStartAddress 線程中要執行的函數的地址
- lpParameter 傳入線程函數的參數
- dwCreationFlags 創建線程的參數,傳入0時線程立即執行
- lpThreadId 接收線程標識的指針,傳入NULL時線程不返回標識
返回值:創建成功返回一個線程句柄,否則返回一個NULL
(4)LoadLibraryA是LoadLibrary函數的ASCII碼版本,它的函數原型如下:
HMODULE LoadLibraryA(
LPCSTR lpLibFileName
);
- lpLibFileName 要加載的DLL的完整路徑名
返回值:加載成功會返回模塊的句柄,加載失敗返回NULL
代碼實現如下:
HMODULE hKernel32Module = GetModuleHandle("kernel32.dll");
if (NULL == hKernel32Module) {
OutputDebugString("Cannot find kernel32.dll.\n");
return -1;
}
FARPROC hFarProc = GetProcAddress(hKernel32Module, "LoadLibraryA");
if (NULL == hFarProc) {
OutputDebugString("Cannot get function address.\n");
return -1;
}
HANDLE hThread = CreateRemoteThread(hProcess
, NULL
, 0
, (LPTHREAD_START_ROUTINE)hFarProc
, lpAddr
, 0
, NULL
);
上面的代碼使用GetProcAddress函數獲得kernel32.dll模塊中LoadLibraryA函數的地址,然後在目標進程開啓一個線程調用LoadLibraryA函數。lpAddr被寫入DLL的完整路徑,把它傳入CreateRemoteThread函數,相當於就是把DLL的完整路徑傳給LoadLibraryA函數。
卸載DLL注入流程
DLL注入了目標進程後,如果想要把它從目標進程卸載,需要進行以下步驟:
(1) 打開目標進程
(2) 給目標創建一個線程, 卸載DLL
1. 打開目標進程
打開目標進程的操作和注入一樣,不再詳細展開,代碼如下:
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid);
if (NULL == hProcess) {
OutputDebugString("Cannot open this process.\n");
return -1;
}
2. 給目標創建一個線程, 卸載DLL
這一步代碼實現和注入的最後一步大體一樣,同樣是開啓線程調用一個函數,但這個在線程裏執行的函數是FreeLibrary。
FreeLibrary函數原型如下:
BOOL FreeLibrary(
HMODULE hLibModule
);
- hLibModule 要釋放的模塊的句柄
返回值:成功會返回一個非0值
代碼實現如下:
HMODULE hKernel32Module = GetModuleHandle("kernel32.dll");
if (NULL == hKernel32Module) {
OutputDebugString("Cannot find kernel32.dll.\n");
return -1;
}
FARPROC hFarProc = GetProcAddress(hKernel32Module, "FreeLibrary");
if (NULL == hFarProc) {
OutputDebugString("Cannot get function address.\n");
return -1;
}
HMODULE hModule = GetModuleHandleByName(ModuleName,pid);
if (NULL == hModule) {
OutputDebugString("Cannot find this module.\n");
return -1;
}
HANDLE hThread = CreateRemoteThread(hProcess
,NULL
, 0
, (LPTHREAD_START_ROUTINE)hFarProc
, hModule
, 0
, NULL
);
由於FreeLibrary函數需要傳入一個模塊的句柄,那麼我們需要從目標進程中掃描並找到我們想要卸載的模塊,然後返回它的句柄:
HMODULE GetModuleHandleByName(const char* ModuleName,DWORD pid) {
HANDLE Processes = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid);
MODULEENTRY32 ModuleInfo = { 0 };
ModuleInfo.dwSize = sizeof(MODULEENTRY32);
char buf[0x100];
while (Module32Next(Processes, &ModuleInfo)) {
if (strcmp(ModuleInfo.szModule, ModuleName) == 0) {
return ModuleInfo.hModule;
}
}
return NULL;
}