DLL注入浅析(上)
DLL注入浅析(上):我会讲到DLL的基本原理,挂钩函数的安装,最后写一个键盘钩子。
DLL注入浅析(下):介绍广为流传的CreateRemoteThread方法,达到注入DLL的目的。
1. 什么是DLL
2. DLL的入口函数
3. 显示载入和卸载DLL
4. 使用Windows挂钩
1.什么是DLL
DLL通俗来讲只是一组源代码模块,每个模块包含一些可供应用程序或其他DLL调用的函数,因此在DLL中,通常是没有用来处理消息循环或穿件窗口的代码。在所有源文件编译完成后,链接器会像链接应用程序的可执行文件那样,对它们进行链接。
在应用程序(或其他DLL)调用一个DLL中的函数之前,必须将该DLL的文件映像映射到调用进程的地址空间中,我们可以使用两种方法来达到这种目地:
隐式载入和显式载入。我这里仅介绍显式载入,这在DLL注入中应用最为广泛。
使用过windows的朋友一定都见过Kernel32.dll,这是windows的内核dll,包含的函数用来管理内存,进程以及线程。但是为什么微软要把这些函数放在kernel32.dll这个DLL中呢?因为DLL的优势非常明显。
1.DLL可以自由的扩展功能,而无需对应用程序进行操作。
2.DLL使项目更加清晰便于管理。
3.DLL只需要被载入内存一次就可以供其他应用程序使用,节省了内存。
4.DLL促进了应用程序本地化
5.DLL有助于解决平台差异
6.DLL可用于特殊目地,比如我们将要使用的SetWindowsHookEx这类挂钩函数。
2.DLL的入口函数DLL有一个入口函数:
(注意这个入口函数的拼写,很容易错写为DLLMain)
参数hinstDLL是这个DLL的句柄。这个值表示一个虚拟内存地址,DLL的文件映射就被映射到进程地址空间的这个位置。通常我们会把它保存在一个全局变量中。
参数fdwReason表示系统调用入口函数的原因,有四个值:
DLL_PROCESS_ATTACH
当系统第一次将一个DLL映射到进程的地址空间中时,会调用DllMain函数 ,并在fdwReason参数中传入DLL_PROCESS_ATTACH。
DLL_PROCESS_DETACH
当系统将一个DLL从进程的地址空间中撤销映射时,会调用DLL的DllMain函数,并在fdwReason参数中传入DLL_PROCESS_DETACH
DLL_THREAD_ATTACH
当进程创建一个线程的时候,系统会检查当前映射到该进程的地址空间中的所有DLL文件映射,并用DLL_PROCESS_DETACH来调用每个DLL的DllMain函数。
DLL_THREAD_DETACH
如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。
为了节省篇幅,这四个值的更为详细的内容这里不累赘,附上MSDN地址,各位自己研究
(https://msdn.microsoft.com/zh-cn/windows/ms682583%28v=vs.100%29)
参数lpvReserved保留,没有太多用得上的地方,详情仍见MSDN。
任何时候,进程中的一个线程可以调用下面这两个函数来将一个DLL映射到进程的地址空间:
LoadLibrary ()
参数lpFileName是要载入的DLL名。LoadLibraryEx ()
参数hFile保留,填为NULL
参数 dwFlags指定了载入DLL的动作,如果为NULL,将与LoadLibrary没有区别,具体参数请查阅MSDN
(https://msdn.microsoft.com/en-us/library/windows/desktop/ms684179%28v=vs.85%29.aspx)
卸载DLL使用FreeLibrary(Ex)函数
一旦显示的载入了一个DLL模版,线程必须使用下面函数老得到它想要引用的函数:
参数hModule指定了DLL的句柄,它是之前调用LoadLibrary(Ex),LoadPackagedLibrary或者GetModuleHandle的返回值。
参数lpProcname是你要引用的函数或者变量的名字。
4.使用Windows挂钩
首先介绍SetWindowsHookEx()函数:
参数idHook是钩子类型
参数lpfn是钩取过程
参数hMod是钩取过程所属的DLL句柄,一般来说就是该DLL的句柄
参数dwThreadId是要挂钩的线程ID,如果是0,就将安装一个全局钩子
有了SetWindowsHookEx()函数,我们就可以把KeyboardProc()添加到键盘钩链
例如:
SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, threadId);
我们再来看一下KeyboardProc()函数:
如果code小于0,则必须让KeyboardProc返回CallNextHookEx()
如果code等于0,则wParam和lParam包含按键消息
如果code等于3,wParam和lParam包含按键消息,并且按键消息不能从消息队列中移除
参数wParam代表一个虚拟值,对应每一个键盘按键
参数lParam 是一个长为32位的值:
其0 – 15位 代表按下键盘次数
其16 – 23位 代表扫描码(详细内容自行查阅资料)
其 24 位 如果是1,代表按键是扩展按键。如果是0,代表是小键盘数字按键。
其 25 – 28位 保留
其 29 位 如果是1,代表ALT键被按下
其 30 位 在消息发送前键被按下则为1,键松开则为0
其 31 位 有键被按下则为1,否则为0
安装好键盘钩子后,只要指定进程发生键盘输入事件,OS就会强制将我们的这个DLL注入到相应进程,键盘事件将由KeyboardProc接管。
我们将这个dll命名为KeyHook.dll
这个DLL代码如下:
// KeyHook.cpp : 定义 DLL 应用程序的导出函数。 // #include "stdafx.h" #include <stdio.h> #include <Windows.h> #include <iostream> #include <string> using namespace std; #define DEF_PROCESS_NAME "notepad.exe" HINSTANCEg_hInstance = NULL; HHOOKg_hHook = NULL; HWNDg_hWnd = NULL; BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved) { switch (dwReason) //通常使用switch分别处理每种情况 { case DLL_PROCESS_ATTACH: //将DLL句柄传递给g_hInstance g_hInstance =hinstDLL; break; case DLL_PROCESS_DETACH: break; } return TRUE; } LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) { if (!(lParam & 0x80000000))/与16进制80000000做与运算是验证最高位是否为1,最高位为1代表键被按下 { MessageBoxA(NULL, "I captured your input", "Hook", MB_OK); //如果按下了键,则return 1,终止KeyboardProc函数,这意味着截获且删除信息。 return 1; } return CallNextHookEx(g_hHook, nCode, wParam, lParam); } #ifdef __cplusplus extern "C" { #endif // __cplusplus __declspec(dllexport) voidHookStart() { HWND hDll = NULL; DWORD threadId = 0; hDll = FindWindow(NULL,TEXT("无标题 - 记事本")); //使用FindWindow函数找到记事本的句柄 threadId =GetWindowThreadProcessId(hDll, NULL); //找到记事本的线程ID if (threadId ==0) printf("SetHookfailed!!!\n"); else { printf("SetHook succeed!!!\n"); g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, threadId); //为记事本安装钩子 } } __declspec(dllexport) voidHookStop() { if (g_hHook) { UnhookWindowsHookEx(g_hHook); //卸载钩子 g_hHook =NULL; } } #ifdef __cplusplus } #endif
几点补充:
__declspec(dllexport)修饰符说明了接下来的函数或者变量是要导出DLL的函数或者变量,也就是我们在应用程序中载入DLL后,可以使用这些函数和变量,因为它们被导出了。
extern “C” C++编译器通常会对函数名和变量名进行改编,当我们打算在可执行文件里面调用这个函数时,链接器会发现可执行文件引用了一个不存在的函数并报错。用extern “C” 告诉编译器不要对函数名或者变量名进行改编。
至于#ifdef #endif的用法,各位自行查找资料。
下面简述下这段代码的流程:
当DLL被载入时,首先执行DllMain,将DLL句柄传给了g_hInstance。
当 应用程序调用HookStart时,执行DLL中HookStart里面的代码,首先获取空白记事本的句柄,通过句柄再得到线程ID,再使用 SetwindowsHookEx为该线程安装钩子,调用KeyboradProc函数,KeyboardProc函数检测到记事本发生键盘输入事件后将 拦截下键盘消息,并弹出对话框(注意,当在记事本内双击鼠标时,也会使lParam的最高位被设置为1)。当应用程序调用HookStop时,卸载钩子, 结束。
我们已经写好了DLL,接下来就要写调用该DLL的程序
这个程序很好实现:
1.载入DLL
2.调用HookStart
3.调用HookStop
4.结束
// HookMain.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <stdio.h> #include <Windows.h> #define DEF_DLL_NAME "KeyHook.dll" #define DEF_HOOKSTART "HookStart" #define DEF_HOOKSTOP "HookStop" //定义两个空的函数指针数据类型,具体原因下面会解释,不懂typedef语法的请自行查阅相关资料 typedef void(*PFN_HOOKSTART)(); typedef void(*PFN_HOOKSTOP)(); int main() { HMODULE hDll= NULL; //DLL的地址 PFN_HOOKSTART LetStart = NULL; //申明两个函数指针 PFN_HOOKSTOP LetStop = NULL; char ch = 0; hDll =LoadLibraryA(DEF_DLL_NAME); //使用LodLibrary载入DLL,取得句柄 //使用GetProcAddress得到DLL导出的函数HookStart和HookStop LetStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART); LetStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP); LetStart(); //执行DLL中的HookStart printf("press 'q' to quit!\n"); while (getchar()!= 'q'); LetStop(); //执行DLL中的HookStop FreeLibrary(hDll); //卸载DLL }
这段代码很容易理解,唯一需要注意的是,在能够使用GetProcAddress返回的函数指针来调用函数之前,我们需要将它转型为与函数的签名(结构)相匹配的正确类型。
例如若想调用kernel32.dll中的GetNativeSystemInfo函数。我们查看下这个函数结构
那我们就该定义一个这样的类型,与函数签名(结构)相符合,仔细观察这个定义
typedef void (WINAPI *PGNSI)(LPSYSTEM_INFO);
然后申明一个函数指针
PGNSI pGNSI;
再使用GetProcAddress得到函数地址,记住要用(PGNSI)转换类型
pGNSI = (PGNSI) GetProcAddress( GetModuleHandle(TEXT("kernel32.dll")), "GetNativeSystemInfo");
在这里我们的HookStart和HookStop同理,读者可以自行比较下KeyHook中函数签名和HookMain中定义的类型是否相符。
最后提醒一点:32位DLL和程序只能注入同样32位程序,同理,64位DLL和程序也只能注入64位程序,否则会发生注入失败或者其他意想不到的结果。
我们运行HookMain.exe,结果十分满意