钩子的编写与使用

Posted 小哈龙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了钩子的编写与使用相关的知识,希望对你有一定的参考价值。

说到钩子也许我们都很陌生(我以前从来没有接触过这个东西。。。),但是说到木马程序记录你的键盘信息,盗取账号密码等信息这种事情,我们应该很清楚, 其实钩子实现的功能就是这样,windows操作系统是以消息机制为基础的,钩子:WIN32API手册中这样描述--钩子是windows的消息处理机制中的一个监视点,应用程序可以在这里安装一个监视子程序,这样就可以在系统中的消息流到达目的窗口过程前监控它们。(其实钩子就是监视某种消息,当收到这种消息的时候,钩子会将这种消息拦截并根据自己的回调函数将不同的消息进行不同的处理,然后将处理结果直接以自定义的消息发送到监视进程的窗口过程中进行消息处理)

这样的解释也许会和DOS中拦截中断信息的操作相混淆(对于这种情况我只想说:不要想太多,在如今windows的保护模式下不可能再做那种为所欲为的事了),钩子可以分为局部和远程的,按照字面上的意思我们也应该可以猜到,局部钩子只限于自身进程内(也就是说局部钩子只能挂钩自身的进程,需要指定自身进程的中某个线程的ID号,也许只有一个线程)。远程钩子就显而易见了,它监视的是自身和其它进程(也就是说它可以包含局部钩子),钩子也必须依赖一个主程序(这个程序的进程也可以是后台进程,远程钩子的回调函数必须放在DLL文件中,当监视其他程序的的进程的时候,钩子监视的是消息,当某个程序收到这个消息的时候,就会调用钩子,就会在本程序的进程中安装钩子,此时必须调用钩子函数,因为进程间的地址空间是隔离的,所以钩子必须位于DLL文件中才可以被其他程序调用)本次例子中就是建立的一个全局钩子来监视键盘消息(WH_KEYBOARD类型),来记录键盘输入的信息,然后在建立的一个对话框的窗口过程中处理自定义消息WM_HOOK,将记录的信息显示在编辑文本框中。这就是本次程序的功能。

下面就来看一下钩子的实现过程。

首先是主程序资源文件:

主程序资源文件
图标 IDI_ICON2
对话框 IDD_DIALOG1
编辑文本框 IDC_TEXT


dll文件
DLL入口函数 DllEntry
钩子回调函数 HookProc
安装钩子 InstallHook
卸载钩子 UninstallHook
Dll文件结尾 End DllEntry

主程序
窗口过程 _ProcDlgMain
程序入口 创建模态对话框


首先看一下主程序的资源文件:

依然使用的是ResEdit工具,资源文件包含3部分 一个对话框,一个29字样的图标,一个可以输入文字的文本编辑框,这部分没有什么大问题,注意在使用此工具时在对话框的风格中一般会被加上WS_SHELLFONT这个风格,由于不支持,在使用RadASM工具编译的时候会出现无法识别这个风格的问题,以前也遇见过,对此我采取的措施是直接将这个没有什么用途的风格直去掉,然后一切OK。下面来看一下资源文件代码:

// Generated by ResEdit 1.6.6
// Copyright (C) 2006-2015
// http://www.resedit.net

#include <windows.h>
#include <commctrl.h>
#include <richedit.h>
#include "resource.h"

//
// Dialog resources
//
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
IDD_DIALOG1 DIALOG 0, 0, 279, 159
STYLE DS_3DLOOK | DS_CENTER | DS_MODALFRAME | WS_CAPTION | WS_VISIBLE | WS_POPUP | WS_SYSMENU
CAPTION "键盘钩子"
FONT 8, "Ms Shell Dlg"
{
    EDITTEXT        IDC_TEXT, 11, 10, 244, 128, WS_VSCROLL | WS_SIZEBOX | ES_AUTOHSCROLL | ES_MULTILINE | ES_READONLY, WS_EX_LEFT
}

//
// Icon resources
//
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
IDI_ICON2          ICON           "icon2.ico"


下面来看一下DLL文件的编写,DLL文件的格式注意,一定要有入口函数以及结尾标记。编写DLL文件遇见了一个问题那就是 在编写完代码29.asm和定义文件29.def之后,编译连接构建生成DLL文件(29.dll)以及29.inc

29.lib  (后面这两个文件需要在主程序代码中用到,也就是主程序中的头文件include  29.inc     includelib 29.lib,用来包含DLL文件中的功能函数的,这样就可以在主程序中直接使用DLL文件中的相关函数了),但是构建生成的文件29.inc其实是个空的文本,里面什么内容都没有,是需要自己写入的,不写入的话,在主程序编译的时候会出现未定义的函数的错误,在本次的程序中29.inc文件的内容如下:

HookProc          proto      _dwCode:dword,_wParam:dword,_lParam:dword
InstallHook        proto       _hWnd:dword,_dwMessage:dword
UninstallHook   proto

这个也是我根据上一次编写DLL文件时在书上看到的书写格式,如上使用的是一个 proto    后面的跟的是函数的各个参数名称(这个跟DLL文件中的函数的参数的名称一样)还要指定参数的类型,各个参数之间用冒号隔开。这其实就是个函数声明(我在网上查的是这样说的,我感觉也就是那个意思,就是声明一下这个函数)。

还有一个问题就是全局钩子的DLL文件要求共享数据段,在本程序中共享的就是.data?段  这个段在可执行文件中的节区名称是.bss,所以在用LINK.EXE进行连接的时候需要更改一下连接选项:

/subsystem:windows /Dll /section:.bss,S      其实就是在后面加上/section:.bss,S  选项就是共享.data?段的意思,详细的可以查阅LINK.EXE的参数。在编写上面的DLL文件的时候,不是使用RadASM工具编写的,只是资源文件用RadASM编译,为啥呢? 因为我在配置RadASM的   masm.ini中的makefile文件或者是RadASM中的工程-->工程选项中的“连接”那一栏的,加入上面的连接选项,但是改过之后使用RadASM连接的时候会出现错误,找不到一些文件,搞了很久也没有解决这个问题,最后我还是果断使用黑框了(就是NMAKE 工具  这个还需要自己编写MAKEFILE文件,而且还得编写批处理文件设置路径,很麻烦),使用这个方式可以编译好,但是RadASM的我实在搞不出来,求破。

下面看一下DLL代码:

                .386
                .model flat,stdcall
                option casemap:none
        
include         windows.inc
include         user32.inc
includelib      user32.lib
include         kernel32.inc
includelib      kernel32.lib


                .data
hInstance       dd         ?
                
                .data?
hWnd            dd         ?
hHook           dd         ?
dwMessage       dd         ?
szAscii         dd         4  dup(?)


                .code
DllEntry        proc       _hInstance,_dwReason,_dwReserved
       
                push       _hInstance
                pop        hInstance
                mov        eax,TRUE
       ret


DllEntry endp


HookProc        proc       _dwCode,_wParam,_lParam


                LOCAL      @szKeyState[256]:byte
                invoke     CallNextHookEx,hHook,_dwCode,_wParam,_lParam
                invoke     GetKeyboardState,addr @szKeyState
                invoke     GetKeyState,VK_SHIFT
                mov        @szKeyState + VK_SHIFT,al
                mov        ecx,_lParam
                shr        ecx,16
                invoke     ToAscii,_wParam,ecx,addr @szKeyState,addr szAscii,0
                mov        byte ptr szAscii [eax],0
                invoke     SendMessage,hWnd,dwMessage,dword ptr szAscii,NULL
                xor        eax,eax
       ret


HookProc endp

InstallHook     proc       _hWnd,_dwMessage

       push       _hWnd
       pop        hWnd
       push       _dwMessage
       pop        dwMessage
       invoke     SetWindowsHookEx,WH_KEYBOARD,addr HookProc,hInstance,NULL
       mov        hHook,eax
       
       ret
InstallHook endp


UninstallHook    proc

       invoke     UnhookWindowsHookEx,hHook
       ret
UninstallHook    endp

                end        DllEntry


29.def文件如下:

EXPORTS       HookProc
                        InstallHook
                        UninstallHook

下面来看一下主程序:

主程序中只包含了一个处理消息的窗口过程,需要注意的就是WM_HOOK这个自定义的消息,windows的ID值在WM_USER以后的值都可以由用户使用,所以说,在此后的ID之中都可以定义自定义的消息。代码如下:


                .386
                .model flat,stdcall
                option casemap :none
                
include         windows.inc
include         user32.inc
includelib      user32.lib
include         kernel32.inc
includelib      kernel32.lib
include         29.inc
includelib      29.lib


IDI_ICON2       equ      101
IDD_DIALOG1     equ      102
IDC_TEXT        equ      40000
WM_HOOK         equ      WM_USER + 100h

                .code
_ProcDlgMain    proc     uses ebx edi esi hWnd,wMsg,wParam,lParam
       
                LOCAL    @dwTemp
                mov      eax,wMsg
                .if      eax  ==  WM_CLOSE
                invoke   UninstallHook
                invoke   EndDialog,hWnd,NULL
                .elseif  eax  ==  WM_INITDIALOG
                invoke   InstallHook,hWnd,WM_HOOK
                .if      ! eax
                 invoke  EndDialog,hWnd,NULL
                .endif
                .elseif  eax  ==  WM_HOOK                           ;注意此处的自定义的消息的处理
                mov      eax,wParam
                .if      al  ==  0dh
                 mov      eax,0a0dh
                .endif
                         mov      @dwTemp,eax
                         invoke   SendDlgItemMessage,hWnd,IDC_TEXT,EM_REPLACESEL,0,addr @dwTemp
                .else    
                mov      eax,FALSE
                ret
                .endif
                mov      eax,TRUE
       ret
_ProcDlgMain endp

start:
                 invoke  GetModuleHandle,NULL
                 invoke  DialogBoxParam,eax,IDD_DIALOG1,NULL,offset _ProcDlgMain,NULL
                 invoke  ExitProcess,NULL
                 end     start


下面来看一下API函数:

CallNextHookEx()

功能:

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


 原型:

LRESULT WINAPI CallNextHookEx( _In_opt_ HHOOK hhk, _In_ int nCode, _In_ WPARAM wParam, _In_ 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过程也必须返回该值。返回值的含义取决于钩型。有关详细信息,请参阅个人钩子程序的描述



GetKeyboardState()

功能:

该函数将256个虚拟键的状态拷贝到指定的缓冲区中。

原型:
BOOL GetKeyboardState(PBYTE IpKeyState);

参数:

IpKeyState:指向一个256字节的数组,数组用于接收每个虚拟键的状态。

返回值:
函数调用成功,则返回非0值。若函数调用不成功,则返回值为0。若要获得更多的错误信息,可以调用GetLastError函数。


GetKeyState(

功能:

该函数检取指定虚拟键的状态。该状态指定此键是UP状态,DOWN状态,还是被触发的(开关每次按下此键时进行切换)。

原型:

SHORT GetKeyState(int nVirtKey);

参数:

nVrtKey:定义一虚拟键。若要求的虚拟键是字母或数字(A~Z,a~z或0~9),nVirtKey必须被置为相应字符的ASCII码值,对于其他的键,nVirtKey必须是一虚拟键码
返回值:

例子:

::GetKeyState(VK_SHIFT) > 0 没按下

::GetKeyState(VK_SHIFT) < 0被按下

返回值给出了给定虚拟键的状态,状态如下:

若高序位为1,则键处于DOWN状态,否则为UP状态。

若低序位为1,则键被触发。例如CAPS LOCK键,被找开时将被触发。若低序位置为0,则键被关闭,且不被触发。触发键在键盘上的指示灯,当键被触发时即亮,键不被触发时即灭


ToAscii(

功能:

该函数将指定的虚拟键码和键盘状态翻译为相应的字符或字符串。该函数使用由给定的键盘布局句柄标识的物理键盘布局和输入语言来翻译代码。

原型:

int ToAscii(UINT uVirtKey,UINT uScanCode,PBYTE lpKeyState,LPWORD lpChar,UINT uFlags);
参数:

nVirtkey:指定要翻译的虚拟键码。

uScanCode:定义被翻译键的硬件扫描码。若该键处于up状态,则该值的最高位被设置。

LpKeyState:指向包含当前键盘状态的一个256字节数组数组的每个成员包含一个键的状态。若某字节的最高位被设置,则该键处于down状态。若最低位被设置,则表明该键被触发。在此函数中,仅有capslock键的触发位是相关的。NumloCk和scroll loCk键的触发状态将被忽略。

LpChar:指向接受翻译所得字符或字符串的缓冲区。

UFlags:定义一个菜单是否处于激活状态。若一菜单是活动的,则该参数为1,否则为0

返回值:

若定义的键为死键,则返回值为负值。否则,返回值应为如下的值:

O:对于当前键盘状态,所定义的虚拟键没有翻译。

1:一个字符被拷贝到缓冲区。

2:两个字符被拷贝到缓冲区。当一个存储在键盘布局中的死键(重音或双音字符)无法与所定义的虚拟键形成一个单字符时,通常会返回该值。


SetWindowsHookEx()

功能:

把一个应用程序定义的钩子安装到钩子链表中

参数原型:

HHOOK SetWindowsHookEx(

int idHook, // 钩子的类型,即它处理的消息类型

HOOKPROC lpfn, // 钩子子程的地址指针。如果dwThreadId参数为0

// 或是一个由别的进程创建的线程的标识,

// lpfn必须指向DLL中的钩子。

// 除此以外,lpfn可以指向当前进程的一段钩子子程代码。

// 钩子函数的入口地址,当钩子钩到任何消息后便调用这个函数。

HINSTANCE hMod, // 应用程序实例的句柄。标识包含lpfn所指的子程的DLL。

// 如果dwThreadId 标识当前进程创建的一个线程,

// 而且子程代码位于当前进程,hMod必须为NULL。

// 可以很简单的设定其为本应用程序的实例句柄。

DWORD dwThreadId // 与安装的钩子相关联的线程的标识符。

// 如果为0,钩子与所有的线程关联,即为全局钩子。);

返回值:

函数成功则返回钩子的句柄,失败返回NULL。


UnhookWindowsHookEx(

功能:

卸载安装的钩子

原型:

BOOL WINAPI UnhookWindowsHookEx( __in HHOOK hhk);

参数:

类型: HHOOK

要删除的钩子的句柄。这个参数是上一个函数SetWindowsHookEx的返回值.

返回值:

类型: BOOL

如果函数成功,返回值为非零值。

如果函数失败,返回值为零。 要获得更多的错误信息,调用GetLastError函数.
















以上是关于钩子的编写与使用的主要内容,如果未能解决你的问题,请参考以下文章

如何使用反冲为自定义钩子编写测试代码

如何在 React 中编写 useComponent 自定义钩子?

编写一个 git post-receive 钩子来处理特定的分支

如何在pytorch中为nn.Transformer编写一个前向钩子函数?

Vue生命周期及钩子函数

Vue3 Composition API——生命周期钩子Provide函数 和 Inject函数封装Hook案例setup顶层编写方式