DLL注入(英语:DLL injection)是一种计算机编程技术,它可以强行使另一个进程加载一个动态链接库以在其地址空间内运行指定代码[1]。在Windows操作系统上,每个进程都有独立的进程空间,即一个进程是无法直接操作另一个进程的数据的(事实上,不仅Windows,许多操作系统也是如此)。但是DLL注入是用一种不直接的方式,来实现操作其他进程的数据。假设我们有一个DLL文件,里面有操作目标进程数据的程序代码逻辑,DLL注入就是使目标进程加载这个DLL,加载后,这个DLL就成为目标进程的一部分,目标进程的数据也就可以直接操作了。
本文编写的代码所做的工作相当于上图中注入器所做的工作,所提及的DLL均为C/C++语言生成的DLL。
(1) 打开目标进程
(2) 在目标进程开辟一段内存空间
(3) 往开辟的内存空间中写入要注入的DLL的路径
(4) 给目标创建一个线程, 加载DLL
Windows下有个名为OpenProcess的函数可以打开一个进程,它的原型如下:
HANDLE OpenProcess(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwProcessId
);
返回值:如果打开成功,返回一个进程句柄,否则返回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
VirtualAllocEx函数可以在目标进程申请一块内存空间,函数原型如下:
LPVOID VirtualAllocEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
返回值: 如果内存申请成功,返回内存空间的首地址,否则返回NULL
代码实现如下:
LPVOID lpAddr = VirtualAllocEx(hProcess, NULL, strlen(DllPath), MEM_COMMIT, PAGE_READWRITE);
if (NULL == lpAddr) {
OutputDebugString("Cannot alloc memory.\n");
return -1;
}
这段代码是在目标进程申请开辟一块内存空间,申请开辟的内存空间大小是DLL完整路径所占用的字节数,申请成功将会返回内存空间的起始地址
WriteProcessMemory可以向指定的内存地址中写入数据,函数原型如下:
BOOL WriteProcessMemory(
HANDLE hProcess,
LPVOID lpBaseAddress,
LPCVOID lpBuffer,
SIZE_T nSize,
SIZE_T *lpNumberOfBytesWritten
);
返回值: 返回一个非零值代表写入成功,返回零则写入失败
代码实现如下:
BOOL isOk = WriteProcessMemory(hProcess , lpAddr, DllPath, strlen(DllPath), NULL);
if (!isOk) {
OutputDebugString("Cannot write memory.\n");
return -1;
}
这段代码是将DLL的完整路径写入到上一步开辟的内存空间中
(1)GetModuleHandle函数根据模块名称得到模块的句柄,原型如下:
HMODULE GetModuleHandleA(
LPCSTR lpModuleName
);
返回值:指定模块的句柄
(2)GetProcAddress函数可以根据函数名来得到模块中的一个导出函数的地址,原型如下:
FARPROC GetProcAddress(
HMODULE hModule,
LPCSTR lpProcName
);
返回值:相应的导出函数的地址
(3)CreateRemoteThread用于在指定进程的虚拟空间中开启一个线程,原型如下:
HANDLE CreateRemoteThread(
HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
返回值:创建成功返回一个线程句柄,否则返回一个NULL
(4)LoadLibraryA是LoadLibrary函数的ASCII码版本,它的函数原型如下:
HMODULE LoadLibraryA(
LPCSTR lpLibFileName
);
返回值:加载成功会返回模块的句柄,加载失败返回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注入了目标进程后,如果想要把它从目标进程卸载,需要进行以下步骤:
(1) 打开目标进程
(2) 给目标创建一个线程, 卸载DLL
打开目标进程的操作和注入一样,不再详细展开,代码如下:
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid);
if (NULL == hProcess) {
OutputDebugString("Cannot open this process.\n");
return -1;
}
这一步代码实现和注入的最后一步大体一样,同样是开启线程调用一个函数,但这个在线程里执行的函数是FreeLibrary。
FreeLibrary函数原型如下:
BOOL FreeLibrary(
HMODULE 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;
}