这篇文章开始进入DLL注入和API钩取的部分,首先来看一下什么叫做DLL注入:
顾名思义,DLL注入是将某些DLL(动态链接库)强行加载到某个程序的进程中,从而完成某些特定的功能,。DLL注入与普通的DLL 加载的区别在于,加载的目标是其自身或者其他特定程序,而注入的目标是强制在某个进程中插入自定义的DLL。
在DLL注入的过程中,会频繁地接触到钩子这个概念,也就是Hook这个操作。
钩子的主要作用就是将消息和进程钩取过来,对于被钩取到的消息和进程,我们可以自己写程序对其进行一些修改或者查看,这样就完成了对于程序原本功能的修改。
本文要实现的功能主要依托于Windows中的消息钩子。Windows为用于提供了相当完备的GUI(图形化操作界面),而用户通过鼠标,键盘,光驱等外设与系统进行交互。在Windows中,每一次鼠标点击和键盘输入都可以被叫做是一个事件,Windows就是基于这些事件驱动的系统。
当按下键盘上的某个键时,这个键被按下的消息传递到Windows的事件队列中等待处理,这时的键盘事件还没有进入到应用程序加以处理,而在系统消息队列和应用之间就是架设消息钩子的空间,在这里可以通过钩子钩取即将被传入应用的事件并加以处理,大概流程如下图:
下面就对钩取键盘消息进行实际操作,开始之前先要准备一个进程查看器:Process Explorer,这个进程查看器功能相当强大,可以看到进程都加载了哪些DLL,以及某个DLL被哪些进程加载过。本次操作是基于notepad.exe(也就是记事本软件)的DLL注入
(下载链接放在文末)
首先来看一下完成主要功能的动态链接库,也就是后面将注入notepad.exe的DLL文件。下面先给出源码,然后在分析源码中用到的API:
#include "stdio.h" #include "windows.h" #define PROCESS_NAME "notepad.exe" HINSTANCE g_hInstance = NULL; HHOOK g_Hook = NULL; HWND g_hWnd = NULL; BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: g_hInstance = hinstDLL; break; case DLL_PROCESS_DETACH: break; default: break; } return TRUE; } LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) { char szPath[MAX_PATH] = { 0, }; char* p = NULL; if (nCode >= 0) { if (!(lParam & 0x80000000)) { GetModuleFileNameA(NULL, szPath, MAX_PATH); p = strrchr(szPath, '\\'); if (!_stricmp(p + 1, PROCESS_NAME)) { return 1; } } } return CallNextHookEx(g_Hook, nCode, wParam, lParam); } #ifdef __cplusplus extern "C" { #endif // __cplusplus __declspec(dllexport) void HookStart() { g_Hook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0); } __declspec(dllexport) void HookStop() { if (g_Hook) { UnhookWindowsHookEx(g_Hook); g_Hook = NULL; } } #ifdef __cplusplus } #endif // __cplusplus
其实程序的逻辑非常简单,大概如下:
下面来具体分析每个部分
在自己编写DLL的过程中,要注意程序的入口点函数是一个固定的模式,这个模式可以在MSDN(Windows的官方帮助文档)上查到,如下:
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpvReserved ) // reserved
{
// Perform actions based on the reason for calling.
switch( fdwReason )
{
case DLL_PROCESS_ATTACH:
// Initialize once for each new process.
// Return FALSE to fail DLL load.
break;
case DLL_THREAD_ATTACH:
// Do thread-specific initialization.
break;
case DLL_THREAD_DETACH:
// Do thread-specific cleanup.
break;
case DLL_PROCESS_DETACH:
if (lpvReserved != nullptr)
{
break; // do not do cleanup if process termination scenario
}
// Perform any necessary cleanup.
break;
}
return TRUE; // Successful DLL_PROCESS_ATTACH.
}
当主程序在调用LoadLibrary和FreeLibrary时,BOOL WINAPI DllMain就是DLL程序开始的地方。
在DllMain函数中有三个参数,分别是:
在函数内部是一个Switch选择结构,根据fdwReason(也就是DLL的加载情况)执行相应操作,本次的复现中采用的是最简单的一种:也就是当DLL模块加载成功时将DLL的实例化句柄赋值给全局变量hInstance。这部分的代码及注释如下:
HINSTANCE g_hInstance = NULL; //实例化句柄类型,简单理解为内存分配了的资源ID HHOOK g_Hook = NULL; //钩子的句柄 HWND g_hWnd = NULL; //窗口句柄 BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) //DLL入口点函数 { switch (fdwReason) { case DLL_PROCESS_ATTACH: //当系统事件为DLL被初次映射到内存中时 g_hInstance = hinstDLL; //前面的实例化句柄被赋值为DLLMain的模块句柄 break; case DLL_PROCESS_DETACH: break; default: break; } return TRUE; }
KeyboardProc,也就是键盘消息进程的函数,它是一个回调函数,它作为参数在SetWindowsHookEx这个API中使用,这个回调函数也有一个比较固定的模板,在MSDN上可以查到:
LRESULT CALLBACK KeyboardProc(
_In_ int code,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
有三个参数:
code:这个参数的值决定如何处理消息,分为0、3和小于0两种情况
wParam:按下键盘的按键后生成的虚拟键值,用于判断按下了哪个键
31 | 转换状态。 如果按下键,则值为 0;如果正在释放键,则值为 1。 | ||||
---|---|---|---|---|---|
这一部分的代码和注释如下:
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) //nCode:确定如何处理消息的代码 wParam:获取键盘输入的虚拟键值,lParam:扩展键值,比较复杂,这里不多解释 { char szPath[MAX_PATH] = { 0, }; //TCHAR szPath[MAX_PATH] = { 0, }; char* p = NULL; if (nCode >= 0) //nCode大于0时表明接收到键盘消息是正常的 { if (!(lParam & 0x80000000)) //lParam的第31位bit位的值代表按键是按下还是释放,0->press 1->release { GetModuleFileNameA(NULL, szPath, MAX_PATH); p = strrchr(szPath, '\\'); //如果使用TCHAR的字符数组要把项目使用的字符集改为多字节字符集 //strrchr函数:在一个字符串中查找目标字符串末次出现的位置 if (!_stricmp(p + 1, PROCESS_NAME)) //判断当前进程是否为notepad //stricmp函数:比较两个字符串,比较过程不区分大小写 { return 1; } } } return CallNextHookEx(g_Hook, nCode, wParam, lParam); //如果当前进程是notepad就将消息传递给下一个程序 }
这两个函数就是后面将被导出到主程序中使用的开启Hook和卸载Hook的函数,本次的复现中写的很简单,就是调用了一个建立钩子进程的API,但是还有些地方需要注意
在我们使用VS编写DLL时,生成的源文件后缀是.cpp,也就是C++文件,但是有些函数是只能在C语言下解析,所以我们使用C++中解析C语言的一个模式:
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
.
.
.
#ifdef __cplusplus
}
#endif // __cplusplus
当我们需要在DLL中导出函数时,要用一个前缀标识这个函数为导出函数,如下:
__declspec(dllexport)
这个前缀标识后面的函数为DLL的导出函数,默认的调用约定是_srdcall
在HookStart创建钩子进程时会调用一个API:SetWindowsHookEx,它在MSDN中可以查询到:
HHOOK SetWindowsHookExA(
[in] int idHook,
[in] HOOKPROC lpfn,
[in] HINSTANCE hmod,
[in] DWORD dwThreadId
);
拥有四个参数:
这一部分的代码及注释如下:
#ifdef __cplusplus extern "C" { //后面的导出函数将使用C语言进行解析 #endif // __cplusplus __declspec(dllexport) void HookStart() //创建钩子进程 { g_Hook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0); //创建钩子进程 } __declspec(dllexport) void HookStop() //卸载钩子进程 { if (g_Hook) { UnhookWindowsHookEx(g_Hook); //卸载钩子进程 g_Hook = NULL; } } #ifdef __cplusplus } #endif // __cplusplus
还是先看总的源码:
#include"stdio.h" #include"Windows.h" #include"conio.h" #define DLL_NAME "KeyHook.dll" #define HOOKSTART "HookStart" #define HOOKSTOP "HookStop" typedef void(*FN_HOOKSTART)(); typedef void(*FN_HOOKSTOP)(); void main() { HMODULE hDll = NULL; FN_HOOKSTART HookStart = NULL; FN_HOOKSTOP HookStop = NULL; hDll = LoadLibraryA(DLL_NAME); HookStart = (FN_HOOKSTART)GetProcAddress(hDll, HOOKSTART); HookStop = (FN_HOOKSTOP)GetProcAddress(hDll, HOOKSTOP); HookStart(); printf("press 'q' to quit this hook procdure"); while (_getch() != 'q'); HookStop(); FreeLibrary(hDll); }
程序流程也比较简单:
这个操作很简单,就是调用LoadLibraryA这个API加载DLL,它在MSDN中可以查到为:
HMODULE LoadLibraryA(
[in] LPCSTR lpLibFileName
);
只有一个参数,就是需要载入的模块的名称,这里还要着重讲一下前面的一些操作:
typedef void(*FN_HOOKSTART)();
typedef void(*FN_HOOKSTOP)();
这个typedef看起来跟平时用到的typedef有点不一样,按照正常的理解,typedef应该是给一个什么东西“取别名”,那么这里就应该是给void取别名为*FN_HOOKSTART,但这样用起来就很奇怪。
其实正确的理解与上面说到的相差不是很大。由于后面会使用GetProcAddress来获取DLL中导出函数的地址,我们要调用就需要一个指向这个的指针。而要导出的两个函数都是参数为void,返回值也是void的函数,所以这里typedef的其实就是一个返回值为void参数也是void的函数指针
这一部分的代码和注释如下:
#include"stdio.h" #include"Windows.h" #include"conio.h" #define DLL_NAME "KeyHook.dll" //定义需要加载的动态库名称 #define HOOKSTART "HookStart" //定义HookStart的全局名称 #define HOOKSTOP "HookStop" //定义HookStop的全局名称 typedef void(*FN_HOOKSTART)(); //定义一个返回值为void参数也是void的函数指针 typedef void(*FN_HOOKSTOP)(); //原理同上 void main() { HMODULE hDll = NULL; //模块载入句柄,用来加载DLL hDll = LoadLibraryA(DLL_NAME); //加载DLL }
在前面的文章中调试程序时经常都会看到LoadLibrary和GetProcAddress这两个函数的联合使用,它们的功能就是在程序中导入外部DLL得函数,这GetProcAddress在MSDN上查到为:
FARPROC GetProcAddress(
[in] HMODULE hModule,
[in] LPCSTR lpProcName
);
这个API有两个参数:
通过这个API获取到的函数需要使用前面定义的函数指针强转一下类型才能正常的赋值给指针使用。
这一部分的代码与注释如下:
HookStart = (FN_HOOKSTART)GetProcAddress(hDll, HOOKSTART); //获取DLL中HookStart的地址,并赋给前面定义好的函数指针 HookStop = (FN_HOOKSTOP)GetProcAddress(hDll, HOOKSTOP); //与上面同理
这一部分所使用的函数和流程都比较简单,不在过多赘述,直接看代码和注释:
HookStart(); //启动钩子进程 printf("press 'q' to quit this hook procdure"); while (_getch() != 'q'); //_getch为包含在conin.h库中的一个函数,功能与getchar差不多,但是没有回显 HookStop(); //卸载钩子进程 FreeLibrary(hDll); //卸载DLL模块
唯一需要注意的就是在结束钩子进程后要将DLL从进程中卸载,也就是要使用FreeLibrary。
由于这个钩子程序在win10和win7运行会有一点小bug(有时候系统会卡住),所以我们在XP下运行和调试这个程序
首先打开Hook程序:
然后打开notepad:
此时在记事本中是无法输入任何内容的,打开ProcessExplorer看一下DLL的加载情况:
可以看见KeyHook.dll已经被强行注入到notepad中了
下面是使用OD调试一下这个键盘钩子程序
使用OD打开程序:
很经典的VC++启动流程,一个call和一个向上的jmp跳转。我们事先知道这个程序是有一个按键提示的,所以我们直接搜索这个字符串:
找到了函数的主要流程,其中有使用到的两次GetProcAddress,后面有卸载DLL时的FreeLibrary,跟随这个CALL进入调用的HookStart函数函数:
可以找到在DLL中设置和写在键盘钩子的函数SetWindowsHookEx,地址在0x10000000开始后的位置上,因为DLL的默认加载位置为0x10000000.
先在OD中打开nootepad,更改OD的调试设置为(快捷键按alt+O):
设置中断于新模块,也就是当DLL加载入内存时断下程序,然后打开Hook程序运行,由于系统不同可能不会第一次就加载KeyHook,需要要在notepad中进行键盘输入,直到模块窗口出现KeyHook:
双击进入这个模块查看:
在本次加载中的加载地址为0x1281000,在这个位置下一个断点,然后我们每次调试运行notepad时,发生键盘输入后程序就会停在这里。
参考资料:《逆向工程核心原理》 [韩] 李承远
工具:ProcessExplorer:https://technet.microsoft.com/en-us/sysinternals/processexplorer.aspx