Hook Windows NT

本文深入探讨了在C++中如何利用HOOKAPI技术来Hook Windows NT系统调用,揭示程序开发中的钩子机制及其在系统层面的应用。
摘要由CSDN通过智能技术生成



To be continue...

Hook API

2013年11月16日
功能追溯

Windows编程的最简单的程序结构,只需要一个消息环。以下展示一个基本的Win32程序,它在开发执行时,会播放Windows 7启动时的使用的音响。#pragma 是VC平台的专用指令,使用它来替代手动设置工程属性,免去手动添加链接所需的LIB库。和普通控制台程序

    #pragma comment(lib,"winmm.lib")
    #include 
    #include 
     
    using namespace std;
     
    static TCHAR szTitle[] = TEXT("GUI App");
    static TCHAR szSound[] = TEXT( "c:\\windows\\media\\Windows Logon Sound.wav" );
     
    HWND hWin;
    HINSTANCE hAPP;
     
    LRESULT CALLBACK circle( HWND, UINT, WPARAM, LPARAM );
     
    int WINAPI WinMain(HINSTANCE hApp, HINSTANCE hPre, LPSTR lpLine, int iCmd )
    {
        MSG msg;
        WNDCLASS winClass;
     
        hAPP = hApp;
     
        winClass.style                  = CS_HREDRAW | CS_VREDRAW;
        winClass.lpfnWndProc  = circle;
        winClass.hInstance        = hApp;
        winClass.hIcon                = LoadIcon( NULL, IDI_APPLICATION );
        winClass.hCursor            = LoadCursor( NULL,IDC_CROSS );
        winClass.hbrBackground = (HBRUSH) GetStockObject( WHITE_BRUSH );
        winClass.lpszClassName = szTitle;
        winClass.lpszMenuName = NULL;
        winClass.cbClsExtra          = 0;
        winClass.cbWndExtra       = 0;
     
        if( !RegisterClass( &winClass ) ){
            MessageBox( NULL, TEXT("I need a Window!"), szTitle, MB_ICONERROR );
            return 0;
        }
     
        hWin = CreateWindow(
                        szTitle, szTitle, //window class and caption
                       WS_OVERLAPPEDWINDOW,
                       CW_USEDEFAULT,  CW_USEDEFAULT, // position x, y
                       CW_USEDEFAULT,  CW_USEDEFAULT, // size width, height
                       NULL, // parent windows handle
                       NULL, // menu handle
                       hApp,
                       NULL); //creation parameters
     
        ShowWindow( hWin, iCmd );
        UpdateWindow( hWin );
     
        while(GetMessage( &msg, NULL, 0, 0 ) ){
            TranslateMessage( &msg );
            DispatchMessage( &msg );
        }
     
        return msg.wParam;
     
    }
     
    LRESULT CALLBACK circle( HWND hWin, UINT iMsg, WPARAM wp, LPARAM lp ){
        HDC         hdc ;
        PAINTSTRUCT ps ;
        RECT        rect ;
     
        switch( iMsg ){
        case WM_CREATE:
            PlaySound( szSound, NULL, SND_FILENAME | SND_ASYNC );
            return 0;
        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;
        case WM_PAINT:
            hdc = BeginPaint( hWin, &ps);
            GetClientRect( hWin, &rect );
            string text =  "Appication API HOOK";
            DrawText( hdc, text.c_str(), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER );
            EndPaint( hWin, &ps);
            return 0;
        }
     
        return DefWindowProc( hWin, iMsg, wp, lp);
    }

上面的程序结构和普通的DOS程序大体相同,只是加入了Windows平台的各种对象。为了新建一个GUI界面,首先设置了一个窗口类winClass,然后通过RegisterClass来注册到系统内,最后通过CreateWindow完成窗口的创建并用ShowWindow将其显示出来程序的结束同样是以WinMain的结束而完成的。不同的地方就在while循环,这个就是Windows系统特有的消息环。Windows通过消息机制来管理着系统设备的各种事件,如鼠标移动了,键盘被按下了,网络连接上了,或者是停电了等等。每一个事件有伴随着消息的传递,每条消息按先后缓急顺序被存储到一个称为消息队列Queue的地方。用户程序在消息环调用GetMessage方法时,就会进入一个不消耗CUP的等待状态,Windows在消息队列中发现一个属于当前用户程序的消息时,就会发送给用户程序,这样GetMessage就取得消息并返回到用户程序的消息环。这时最重要的事情就是对消息进行向应,这就是Windows编程要做的事。在前面,创建窗口时,给GreateWindow传入了一个窗口类,这个类的lpfnWndProc成员设置了一个引用circle的函数指针,这就是程序用来响应消息的方法,称为窗口过程。当消息环中调用DispatchMessage方法时,就会将消息传递给窗口过程进行处理。而在窗口过程中,将对不同的消息进行选择性处理,如程序完成初始化时的WM_CREATE消息,程序将播放一段乐音来响应。当用户通过鼠标点击窗口右上角的关闭按钮时,将产生一个WM_DESTORY消息,这就是一个关闭程序的意图。窗口过程在接收到这个关闭程序的消息时,就以调用PostQuitMesage来响应,它就是用来告知系统,程序需要关闭,不需要再做处理其它消息了。然后系统给消息环发送一个空消息,以使用while终止循环,最后WinMain即,程序结束。

注意,和PostQuitMessage相似的PostMessage可以用来向程序发送任意消息,它只负责将消息放到消息队列中,然后直接返回,相应的SendMessage要等到受到消息处理的返回码后才继续。

WM_PAINT是Windows窗口系统中一条重要的消息,应用程序通过处理该消息实现在窗口上的绘制工作。系统会在多个不同的时机发送WM_PAINT消息:当第一次创建一个窗口时,当改变窗口的大小时,当把窗口从另一个窗口背后移出时,当最大化或最小化窗口时,等等,这些动作都是由系统管理的,应用只是被动地接收该消息,在消息处理函数中进行绘制操作。大多数的时候应用也需要能够主动引发窗口中的绘制操作,比如当窗口显示的数据改变的时候,这一般是通过InvalidateRect和InvalidateRgn函数来完成的,前者把指定的区域加到窗口的待更新区域Update Region中,当应用的消息队列没有其他消息且待更新区域不为空时,系统就会自动产生WM_PAINT消息。待更新区域是用一个RECT结构表示的,如下定义:

    typedef struct _RECT {
      LONG left; // 窗口左边开始计算的像素位置
      LONG top; // 窗口的顶部计算的像素位置
      LONG right;
      LONG bottom;
    } RECT, *PRECT;

系统为什么不在调用Invalidate时发送WM_PAINT消息呢?又为什么非要等应用消息队列为空时才发送WM_PAINT消息呢?这是因为系统把在窗口中的绘制操作当作一种低优先级的操作,于是尽可能地推后做。待更新区域区域会被累加起来,然后在一个WM_PAINT消息中一次得到更新,不仅能避免多次重复地更新同一区域,也优化了应用的更新操作。这种通过InvalidateRect和InvalidateRgn来使窗口区域无效,依赖于系统在合适的时机发送WM_PAINT消息的机制实际上是一种异步工作方式,也就是说,在无效化窗口区域和发送WM_PAINT消息之间是有延迟的;有时候这种延迟并不是我们希望的,这时我们当然可以在无效化窗口区域后利用SendMessage发送一条WM_PAINT消息来强制立即重画,但不如使用Windows GDI API:UpdateWindow和RedrawWindow或者使用WM_PRINT 、WM_PRINTCLIENT消息。

BeginPaint和WM_PAINT消息紧密相关。试一试在WM_PAINT处理函数中不写BeginPaint会怎样?程序会像进入了一个死循环一样达到惊人的CPU占用率,因为程序总有处理不完的WM_PAINT消息。其实BeginPaint的一个作用就是把待更新区域清空。BeginPaint和WM_ERASEBKGND消息也有关系。当窗口的待更新区域被标志为需要擦除背景时,BeginPaint会发送WM_ERASEBKGND消息来重画背景,同时在其返回信息里有一个标志表明窗口背景是否被重画过。当我们用InvalidateRect和InvalidateRgn来把指定区域加到待更新区域中时,可以设置该区域是否需要被擦除背景,这样下一个BeginPaint就知道是否需要发送WM_ERASEBKGND消息了。

要注意的是,BeginPaint只能在WM_PAINT处理函数中使用,在其它消息下无法实现窗口重绘功能。例如可以使用以下方法来产生一个待更新区,以强制刷新窗口。

    RECT rect;
    GetClientRect( hWin, &rect );
    InvalidateRect( hWin, &rect, TRUE);
    UpdateWindow( hWin );

Windows程序就是这样一个基本的工作过程,然而,从DOS开始,程序开发就有个传统:程序需求对系统功能的监视和响应以实现程序的功能。这也是程序开发的基本需求,Windows 3.x的时代HOOK已经普遍应用。HOOK根本上来讲就是一处提供给开发者嵌入自定义例程以实现程序功能的场所。目前普遍将HOOK翻译为钩子的做法其实不太恰当,如果按照我在做Wordpress二次开发的经验,我更愿意将HOOK称作过滤器 Filter,从本质上讲Windows的HOOK和Wordpress的Filter是一致的。它们所起的作用就像是水管中间接上的一个过滤器,把某些东西过滤出来。事实上,1993年Kyle Marsh在MSDN上发表过一篇文章Win32 Hooks,里就是将钩子回调过程为过滤器函数,这篇文章可以在MSDN的技术文章栏目中找到。

实现程序功能代码嵌入的方法有各种形式,如下:

    使用注册表注入,将程序注册到:HKLM/Software/Microsoft/Windows NT/CurrentVersion/Windows/AppInit_DLLs。这AppInit_DLLs这个键记录了一个或一组逗号分隔的DLL文件,当一个使用USER32.DLL的程序载入时,就会透过LoadLibrary()API依次加载AppInit_DLLs指定的链接库。这种方法只在NT架构系统且2K+版本上才有效。
    使用API注入,通过SetWindowsHookEx()来注册HOOK处理程序,使用CallNextHookEx() 来保持钩子链正常工作,退出时,使用UnhookWindowsHookEx()卸载钩子。
    通过修改PE文件注入,PE程序文件中有一个导入地址表IAT Import Address Table,它记录了程序要调用的外部函数的地址,改PE文件的IAT,使之指向自己的代码,这样EXE/DLL在调用系统API的时候便会调用你自己的函数。要注意,Windows API均有两个版本:Ansi和Unicode。例如获取程序标题的GetWindowTextAPI实际上只是一个宏,根据编译条件UNICODE来决定是调用GetWindowTextA还是GetWindowTextW。在NT系统下所有ANSI版本API会转换成UNICODE版本。
    通过遥距线程注入,使用CreateRemoteThread()方法可以创建一个线程,将要注入的程序透过LPTHREAD_START_ROUTINE参数传递给创建的线程,但是程序要先使用ThreadProc()API包装。这种方法是Jeffrey Ritcher提出来的,他写的文档也很完善,不过也只在NT架构系统且2K+版本上才有效。
    透过BHO插件注入,BHO是Browser Helper Objects,只在IE浏览器中使用。IE运行时会加载所有实现IObjectWithSite接口的COM组件。
    通过Office插件注入,和BHO方式相似,使用范围限制在Office内。

本文主要涉及API注入、PE文件注入及遥距线程注入。
API钩子程序结构

钩子,按习惯,大伙都叫它为钩了。在伴随Windows系统的发展中,钩子也发展出好多的分类,有系统层次的,有应用程序层次的,有核心层次的,有处理键盘消息的,有处理系统日志的,有监视API调用的,各式各样。对于一个钩子程序,按上面水管过滤的理解,首先就需要安装一个钩子,主钩子在系统中起作用;然后程序按功能逻辑进行处理,这需要一个钩子回调函数hook procedure;完了,程序要退出,就要清场,把钩子回收。

而每条水管可以安装多个不同的过滤器,同理钩子不也可以有多个,因此组成了一条链,系统将按注册顺序来调用。先调用线程钩子,然后调用系统钩子,后注册的先调用。

对于用户层次的钩子,只需要一个可以安装和回收钩子的程序就可以了,连带程序功能都在一个程序内实现。当钩子需要处理Windows内核消息时,就需要钩子运行于内核模式,这时就需要可以开发内核程序的DDK,它才是用来开发内核应用的,像驱动程序这类一样,而且必需将钩子程序编译到DLL程序中。只Win16程序才允许在程序内容注册一个系统钩子。先来看看SetWindowsHookEx原型:

    HHOOK SetWindowsHookEx(
      int idHook,        // hook type
      HOOKPROC lpfn,     // hook procedure
      HINSTANCE hMod,    // handle to application instance
      DWORD dwThreadId   // thread identifier
    );

hMod 指定钩子回调函数所在DLL的实例句柄。如果安装的是局部钩子的话,由于局部钩子的回调函数并不需要放在动态链接库中,这时这个参数就使用NULL。

dwThreadID是安装钩子后想监控的线程的ID号。该参数可以决定钩子是线程钩子局部范围的还是系统钩子全局范围。如果参数指定的是自己进程中的某个线程ID号,那么该钩子是一个局部钩子;如果指定的线程ID是另一个进程中某个线程的ID,那么安装的钩子是一个局部的远程钩子;如果想要安装系统范围的全局钩子的话,可以将这个参数指定为NULL,这样钩子就会被解释成系统范围的,可以用来监控所有的进程及它们的线程。

由于32-bit钩子不能注入到64-b
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值