首页
论坛
课程
招聘
[调试逆向] [原创]Windows消息钩取(简单DLL注入)
2021-4-6 00:13 1817

[调试逆向] [原创]Windows消息钩取(简单DLL注入)

2021-4-6 00:13
1817

Windows消息钩取

简单的说,消息钩取就是半路截取信息。

一:Windows消息流

1、发生键盘输入事件时,WM_KEYDOWN消息被添加到[OS message queue];


2、OS判断哪个应用程序中发生了事件,然后从[OS message queue]中取出消息,添加到相应应用程序的[application message queue]中;


3、应用程序监视自身的[application message queue],发现新添加的WM_KEYDOWN消息后,调用相应的事件处理程序处理。

二:消息钩子

OS消息队列和应用程序消息队列之间存在一条钩链(Hook Chain),设置好键盘消息钩子后,处于钩链中的键盘消息钩子会比应用程序先一步看到相应信息。在键盘消息钩子函数的内部,除了可以查看消息之外,还可以修改消息本身,而且还能对消息实施拦截,阻止消息传递。可以同时设置多个相同的键盘消息钩子,按照设置的顺序依次调用,从而组成的链条称为钩链。

image

三:SetWindowsHookEx()

在Windows编程中,使用SetWindowsHookEx() API可以简便地实现消息钩子,其用于将指定的钩子注册到钩链中,无论在DLL内部或外部都可调用。


函数原型:

HHOOK SetWindowsHookEx(

   int idHook,   //hook type

   HOOKPROC lpfn,  //hook procedure

   HINSTANCE hMod,  //hook procedure所属的DLL句柄

   DWORD dwThreadId //将要挂钩的目标线程ID

);

HHOOK:返回值,钩子句柄,需要保留,等不使用钩子时通过UnhookWindowsHookEx函数卸载钩子。

idHook:钩子的拦截消息类型,选择钩子程序的拦截范围,具体值参考文章结尾的消息类型。

Lpfn:消息的回调函数地址,一般是填函数名。

hMod:钩子函数所在的实例的句柄。对于线程钩子,该参数为NULL;对于系统钩子,该参数为钩子函数所在的DLL句柄。在dll中可通过AfxInitExtensionModule(MousehookDLL, hInstance)获得DLL句柄。

dwThreadId:钩子所监视的线程的线程号,可通过GetCurrentThreadId()获得线程号。对于全局钩子,该参数为NULL(或0)


钩子过程(hook procedure)是由OS调用的回调函数。安装消息钩子时,钩子过程需要存在于某个DLL内部,并且该DLL的实例句柄即是hMod。

使用SetWindowsHookEx()设置好钩子后,在某个进程中生成指定消息时,OS会将相关的DLL文件强制注入相应的进程,然后调用注册的钩子过程。

回调函数:简言之,就是某个特定的事件发生时被指定调用的函数。窗口Windows过程(WndProc)就是一个典型的回调函数(键盘,鼠标等事件发生时OS会调用注册的窗口过程)

四:键盘消息钩取

KeyHook.dll文件是含有钩子过程的DLL文件,HookMain.exe是最先加载KeyHook.dll并安装键盘钩子的程序。HookMain.exe加载KeyHook.dll文件后SetWindowsHookEx()安装键盘钩子。若其他进程中发生键盘输入事件,则OS会强制将KeyHook.dll加载到相应进程的内存,然后调用KeyboardProc()函数。注意的是,OS会将KeyHook.dll加载到发生键盘输入事件的所有进程,即消息钩取技术是一种DLL注入技术。

五:源码分析

HookMain.exe的源码

//HookMain.cpp

#include "stdio.h"

#include "conio.h"  //控制台输入输出头文件

#include "windows.h"


#define DEF_DLL_NAME "KeyHook.dll"    //define一个dll的名称

#define DEF_HOOKSTART "HookStart"

#define DEF_HOOKSTOP "HookStop"      //define两个GetProcAdress要查找的函数的名称


typedef void(*PFN_HOOKSTART)();      //typedef一个名叫PFN_HOOKSTART的函数指针类型

typedef void(*PFN_HOOKSTOP)();       //同上


void main()

{

    HMODULE hDll = NULL;      //声明并初始化HMODULE类型的变量hDll

    PFN_HOOKSTART HookStart = NULL;  //使用PFN_HOOKSTART定义一个名叫HookStart的函数指针

    PFN_HOOKSTOP HookStop = NULL;   //同上

    char ch = 0;


    hDll = LoadLibraryA(DEF_DLL_NAME);     //调用LoadLibraryA函数加载DLL并获取DLL的句柄,此时dll中的DLLMain就会自动被执行


    HookStart = (PFN_HOOKSTART)GetProcAddress(hDllDEF_HOOKSTART); //调用GetProcAddress函数获取Dll中HookStart函数的地址,并将其赋给函数指针HookStart

    HookStop = (PFN_HOOKSTOP)GetProcAddress(hDllDEF_HOOKSTOP);    //调用GetProcAddress函数获取Dll中HookStop函数的地址,并将其赋给函数指针HookStop


    HookStart();    //调用DLL中的HookStart函数


    printf("press 'q' to quit!\n");

    while(_getch() != 'q');   //带下划线_的函数一般是函数库内部的函数,而不带下划线的一般是提供给用户使用的函数。带下划线的目的是为了防止用户定义的函数和函数库的函数重名冲突,所以直接使用也是可以的。


    HookStop();    //调用DLL中的HookStop函数


    FreeLibrary(hDll);     //调用FreeLibrary来释放指定的动态链接库

}


KeyHook.dll的源码

#include "stdio.h"

#include "windows.h"


#define DEF_PROCESS_NAME "notepad.exe"     //定义目标的进程名为notepad.exe


HINSTANCE g_hInstance = NULL;    

HHOOK g_hHook = NULL;

HWND g_hWnd = NULL;



//DllMain()函数在DLL被加载到进程后会自动执行

/*

hinstDLL:指向自身的句柄

dwReason: 调用原因

lpvReserved:隐式加载和显示加载

*/

BOOL WINAPI DllMain(HINSTANCE hinstDLLDWORD dwReasonLPVOID lpvReserved)

{

    switchdwReason )

    {

        case DLL_PROCESS_ATTACH:

            g_hInstance = hinstDLL;

            break;

 

        case DLL_PROCESS_DETACH:

            break;

    }

    return TRUE;

}


/*

nCode: 根据这个数值决定怎样处理消息

wParam: 按键的虚拟键值消息,例如:VK_F1

lParam: 根据不同的位数具有多种不同的含义

LRESULT是一个数据类型,指的是从窗口程序或者回调函数返回的32位值。

CALLBACK是由用户设计却由windows系统呼叫的函数,统称为callback函数。某些API函数要求以callback作为你参数之一。

*/

LRESULT CALLBACK KeyboardProc(int nCodeWPARAM wParamLPARAM lParam)

{

    char szPath[MAX_PATH= {0,};        //MAX_PATH定义了编译器所支持的最长全路径名的长度

    char *p = NULL;


    ifnCode >= 0 )

    {

 

        //释放键盘按键时,bit 31 : 0 => press, 1 => release

        if!(lParam & 0x80000000) )   //10000000 00000000 00000000 00000000,即当第31位是0的时候表示press,第31位是1的时候表示release

        {

            GetModuleFileNameA(NULLszPathMAX_PATH);   //当前进程的全路径储存到szPath中

            p = strrchr(szPath'\\');     //p为字符'\\'之后到szPath字符串的结尾,说明取的是当前程序的进程名

 

            //比较当前进程名称,若为notepad.exe,则消息不会传递给应用程序或下一个钩子函数

            //_stricmp()函数用于比较字符串,i表示不区分大小写,若两个值相等则返回0

            if!_stricmp(p + 1DEF_PROCESS_NAME) )

            {

                return 1;        //如果比较来两个值相等,则return 1

            }

        }

    }

 

    //比较当前进程名称,若非notepad.exe,则消息传递给应用程序或下一个钩子函数

    return CallNextHookEx(g_hHooknCodewParamlParam);

}


#ifdef __cplusplus    //如果这是一段cpp代码,那么就加入extern "C"{

extern "C"{

#endif


__declspec(dllexport) void HookStart() //__declspec(dllexport),显式的声明这是一个导出函数

    {

        g_hHook = SetWindowsHookEx(WH_KEYBOARDKeyboardProcg_hInstance0);

    } //当调用HookStart函数的时候,SetWindowsHookEx函数就将KeyboardProc添加到钩链


__declspec(dllexport) void HookStop()

{

    if(g_hHook)

    {

        UnhookWindowsHookEx(g_hHook);

        g_hHook = NULL;

    }

}

#ifdef __cplusplus  //如果这是一段cpp代码,那么就加入}

}

#endif

其中使用到的API函数的解析

DLLMain

函数原型:

BOOL WINAPI DllMain(

HINSTANCE hinstDLL, // 指向自身的句柄

DWORD fdwReason, // 调用原因

LPVOID lpvReserved // 隐式加载和显式加载

);

静态链接时,或动态链接时调用LoadLibraryFreeLibrary都会调用DllMain函数。DllMain的第二个参数fdwReason指明了系统调用Dll的原因,它可能是:

DLL_PROCESS_ATTACH、

DLL_PROCESS_DETACH、

DLL_THREAD_ATTACH、

DLL_THREAD_DETACH。

进程映射

DLL_PROCESS_ATTACH

大家都知道,一个程序要调用Dll里的函数,首先要先把DLL文件映射到进程的地址空间。要把一个DLL文件映射到进程的地址空间,有两种方法:静态链接动态链接的LoadLibrary或者LoadLibraryEx。

当一个DLL文件被映射到进程的地址空间时,系统调用该DLL的DllMain函数,传递的fdwReason参数为DLL_PROCESS_ATTACH,这种调用只会发生在第一次映射时。如果同一个进程后来为已经映射进来的DLL再次调用LoadLibrary或者LoadLibraryEx,操作系统只会增加DLL的使用次数,它不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。不同进程用LoadLibrary同一个DLL时,每个进程的第一次映射都会用DLL_PROCESS_ATTACH调用DLL的DllMain函数。

可参考DllMainTest的DLL_PROCESS_ATTACH_Test函数。

进程卸载

DLL_PROCESS_DETACH

当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的fdwReason值是DLL_PROCESS_DETACH。当DLL处理该值时,它应该执行进程相关的清理工作。

那么什么时候DLL被从进程的地址空间解除映射呢?两种情况:

◆FreeLibrary解除DLL映射(有几个LoadLibrary,就要有几个FreeLibrary)

◆进程结束而解除DLL映射,在进程结束前还没有解除DLL的映射,进程结束后会解除DLL映射。(如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。)

注意:当用DLL_PROCESS_ATTACH调用DLL的DllMain函数时,如果返回FALSE,说明没有初始化成功,系统仍会用DLL_PROCESS_DETACH调用DLL的DllMain函数。因此,必须确保清理那些没有成功初始化的东西。

可参考DllMainTest的DLL_PROCESS_DETACH_Test函数。

线程映射

DLL_THREAD_ATTACH

当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,并用值DLL_THREAD_ATTACH调用DLL的DllMain函数。

新创建的线程负责执行这次的DLL的DllMain函数,只有当所有的DLL都处理完这一通知后,系统才允许进程开始执行它的线程函数。

注意跟DLL_PROCESS_ATTACH的区别,我们在前面说过,第n(n>=2)次以后地把DLL映像文件映射到进程的地址空间时,是不再用DLL_PROCESS_ATTACH调用DllMain的。而DLL_THREAD_ATTACH不同,进程中的每次建立线程,都会用值DLL_THREAD_ATTACH调用DllMain函数,哪怕是线程中建立线程也一样。

线程卸载

DLL_THREAD_DETACH

如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。

注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。

DLL加载和应用程序退出的使用都会调用该函数(DllMain)的哦,是应用程序一上来就调用的,不是用到该函数时才调用的


KeyboardProc

KeyboardProc是键盘钩子处理函数。

函数原型:

LRESULT CALLBACK KeyboardProc

{

int code,

WPARAM wParam,

LPARAM lParam

};

参数:

code:

根据这个数值决定怎样处理消息

如果 code 小于0,则 必须让KeyboardProc()函数返回CallNextHookEx()

code可以是下列值:

HC_ACTION:wParam和lParam包含按键消息

HC_NOREMOVE:wParam和lParam包含按键消息,并且按键消息不能从消息队列中移除(一个被PeekMessage函数调用的请求,指定PM_NOREMOVE标志)

wParam:

按键的虚拟键值消息,例如:VK_F1

lParam:

32位内存,内容描述包括:指定扩展键值,扫描码,上下文,重复次数。(具体的细节直接百度即可)

返回值:

如果参数1:code小于0,则必须 返回CallNextHookEx(),也就是返回CallNextHookEx()的返回值

如果参数1:code大于等于0,并且钩子处理函数没有处理消息,强烈建议您 返回CallNextHookEx()的返回值,否则当您安装WH_KEYBOARD钩子时,钩子将不会得到通知,并返回错误结果。

如果钩子处理的消息,您可以返回一个非0值,防止系统把消息传递给钩子链中的下一个钩子,或者把消息发送到目标窗口。

GetModuleFileName

获取当前进程已加载模块的文件的完整路径,该模块必须由当前进程加载。

如果想要获取另一个已加载模块的文件路径,可以使用GetModuleFileNameEx函数。

 GetModuleFileName{

HMODULE hModule,

LPTSTR lpFilename,

DWORD nSize

};

hModule Long

一个模块的句柄。可以是一个DLL模块,或者是一个应用程序的实例句柄。如果该参数为NULL,

该函数返回该应用程序全路径。

lpFileName String

指定一个字串缓冲区,要在其中容纳文件的用NULL字符中止的路径名,hModule模块就是从这个文件装载进来的

nSize Long

装载到缓冲区lpFileName的最大字符数

CallNextHookEx

将钩子信息传递到当前钩子链中的下一个子程,一个钩子程序可以调用这个函数之前或之后处理钩子信息

函数原型

LRESULT WINAPI CallNextHookEx{

HHOOK hhk,

int nCode,

WPARAM wParam,

LPARAM lParam

};

参数:

1. hhk[可选]

说明:当前钩子的句柄

类型:HHOOK

此参数将被忽略。

2. nCode [in]

说明:钩子代码; 就是给下一个钩子要交待的

类型:INT

钩传递给当前Hook过程的代码。下一个钩子程序使用此代码,以确定如何处理钩的信息。

3. wParam[in]

说明:要传递的参数; 由钩子类型决定是什么参数

类型:WPARAM

wParam参数值传递给当前Hook过程。此参数的含义取决于当前的钩链与钩的类型。

4. lParam[in]

说明:要传递的参数; 由钩子类型决定是什么参数

类型:LPARAM

lParam的值传递给当前Hook过程。此参数的含义取决于当前的钩链与钩的类型。

返回值:

1. 类型:LRESULT

2. 返回这个值链中的下一个钩子程序。当前Hook过程也必须返回该值。返回值的含义取决于钩型


对句柄类型的分析

hInstance是操作系统分配给实例的指针. 程序根据hInstance访问其相应的内存空间

hInstance是操作系统分配给程序自身实例的句柄.句柄是用来标识实例的,句柄是实例指针的索引. 通过句柄能找到实例的地址.

HINSTANCE hInstance;是应用程序的实例句柄


HMODULE 是代表应用程序载入的模块,win32系统下通常是被载入模块的线性地址

HINSTANCE 在win32下与HMODULE是相同的东西,在Win32下还存在主要是因为win16

HWND 是窗口句柄


六:运行并检测是否hook成功

先运行HookMain.exe,

image.png

然后打开notepad.exe,尝试输入,发现输入不了,然后

使用Process Explorer检查notepad.exe导入的dll

image.png

如下发现这个dll被注入到了该进程中

我们的消息钩子就安装成功

七:调试Windows消息钩取

1.调试HookMain.exe

使用OD打开程序,发现就是典型的VC++启动函数

image.png

image.png


我们已经运行过了HookMain.exe程序,知道了那个字符串,我们下面直接搜索字符串找到主要的代码

image.png

然后单步调试下去,先是调用了LoadLibrary,然后在call ebx的地方调用了KeyHook.HookStart函数

image.png

F7单步步入

image.png

这样就看见了Dll中的HookStart()函数

在地址0x100010EF处可以看到,调用了SetWindowsHookExW()函数,其上方两条PUSH指令将该函数的第一、第二两个参数压入栈。其中第一个参数idHook值为2,即WH_KEYBOARD,第二个参数lpfn值为0x10001020,该值即为钩子过程的地址。

返回main()函数之后的代码即为接收用户输入的q后即终止钩取。

2.调试exe内的dll

先使用od打开notepad.exe运行起来

然后在od中设置

image.png

中断于新模块(DLL)

然后运行HookMain.exe安装钩子

当我们在notepad中输入,也就是发生键盘事件的时候

此时Ollydbg暂停调试并弹出Executable modules窗口

image.png

但是并没有KeyHook.dll

根据系统环境不同,有时不会先显示KeyHook.dll,而是先加载其他DLL库。此时可以F9直至KeyHook.dll加载完成。

F9再次运行,发现了KeyHook.dll

image.png

然后发现了入口为0x10001587

image.png

我们在入口处直接下个断点

然后取消之前的Break on new module(DLL)设置


之后我们就可以发现,当每次notepad.exe程序中发生键盘输入事件时,调试都会停止在该断点处。



[看雪官方培训] Unicorn Trace还原Ollvm算法!《安卓高级研修班》2021年6月班火热招生!!

收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回