用 SEH 技术实现 API Hook

下载本节例子程序和源代码 (5.21 KB)

阅读本文之前,我先假设读者已经知道了 SEH 和 API Hook 的基本概念,因为我不打算在此进行扫盲工作。什么?你不懂什么叫 SEH 和 API Hook ?那……先去找点资料看看吧,到处都有哦,推荐读物:Jeffrey Richter 大牛的《Windows核心编程》。(没话可说,研究系统底层编程的葵花宝典,必备!)

另外值得补充的是,API Hook 跟一般的 Hook 是一点关系都没有的,虽然它们都是“Hook”,但是在技术上却有着天壤之别。啊……不明白?先去看看葵花宝典吧……

呵呵,废话不多说了,让我们开始吧。

经常研究 Crack 的朋友一定会知道 INT 3 这个指令。(你不知道?我倒……) 这个指令在软件调试中非常有用,因为我们可以利用它来设置特定的断点(BreakPoint),当程序遇到 INT 3 指令的时候,将会产生一个断点异常,这个异常在 Windows.inc 里面定义为 EXCEPTION_BREAKPOINT ,对应值是 080000003h 。Hoho,说了那么多,你想到什么了吗?

是的,聪明的你应该已经想到了!既然是异常,就肯定可以通过 SEH 来进行处理。于是我们可以这样做:在调用 API 之前,先设置一个断点,然后当 API 正式运行的时候,就会因为碰到 INT 3 指令而进入我们的异常处理模块,接着我们就可以在处理模块里面为所欲为了——是改变什么东西还是让它顺利通过,我没话说,看你喜欢吧……

简单地说,过程就是类似这样的:

程序遇到 INT 3 指令后,产生一个中断异常,这时 Windows 就拿着一份处理异常的活挨个问 SEH 链表上的回调函数:“你干不干?”,“不干”,“你呢?”,“我也不干”……当 Windows 终于问到我们定义好的断点异常处理函数后,他说:“让我来干好了!”,于是 Windows 就不会再问余下的人了,他把全权托给了我们的处理函数,至于我们的函数在之后做了什么手脚……呵呵,只有天知道!

明白了吗?其实在这里我们是利用了软件调试上的一个小技巧,实现了“伪 API Hook”。严格来说,这种方法不能算是真正的 API Hook ,但是由于我们可以在 SEH 回调函数中为所欲为,而系统不会发觉,所以也可以勉强算个数吧。

弄清楚原理后,剩下的就不难了。我们首先要保存目标 API 的入口地址,接着要设置一个 INT 3 指令,然后就在 SEH 的回调函数中进行地址修正等工作,最后万事倶备,只欠东风了。程序一运行,就进入了我们的 SEH 回调函数,呵呵,你爱怎么样就怎么样吧……

怎么样?一点都不难吧。罗里罗嗦地说了一大堆,可能有人会开始不耐烦了……呵,别着急,下面我就给出源代码。补充一句:本方法只是提供了一种新的思路,如果你在深入研究中发现了我的错误,或者有更好的解决方法,请给我来信啊,我的邮箱:lcother@163.net

(注意,本技术只能在 NT/2000/XP 平台下使用)

;*********************************************************
;程序名称:用 SEH 技术实现 API Hook
;适用系统:Win NT/2000/XP
;作者:罗聪
;日期:2002-11-22
;出处:http://www.LuoCong.com(老罗的缤纷天地)
;注意事项:如欲转载,请保持本程序的完整,并注明:
;转载自“老罗的缤纷天地”(http://www.LuoCong.com)
;*********************************************************

.386
.model flat, stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib

WndProc         proto :DWORD, :DWORD, :DWORD, :DWORD
Error_Handler   proto :DWORD, :DWORD, :DWORD, :DWORD
SetHook         proto

.const
IDI_LC                  equ 1
IDC_CHECKBUTTON_HOOK    equ 3000
IDC_BUTTON_ABOUT        equ 3001
IDC_BUTTON_EXIT         equ 3002

.data
szDlgName               db  "lc_dialog", 0
szMsgAbout              db  "-= SEH for API Hook =-", 13, 10, 13, 10,\
                            "作者:罗聪(lcother@163.net)", 13, 10, 13, 10,\
                            "老罗的缤纷天地", 13, 10,\
                            "http://www.LuoCong.com", 13, 10, 0
szMyText                db  13, 10, 13, 10, "(哈哈,看到有什么不同了吗?)", 0
szMsgHooked             db  "MessageBoxIndirectA() has been hooked!",\
                            13, 10, 13, 10,\
                            "即将改变原来的 MessageBoxIndirectA() 的参数,", 13, 10,\
                            "请注意后面的对话框跟没有 Hook 之前有什么不同……", 0
szCaption               db  "SEH for API Hook by LC", 0
szLibUser               db  "user32", 0
szProcMsgBoxInd         db  "MessageBoxIndirectA", 0
dwAddress               dd  0
dwOldProtect            dd  0
bOldByte                db  0
dwRetAddr               dd  0

.data?
hInstance               HINSTANCE       ?
mbp                     MSGBOXPARAMS    <>
szText                  db  1024 dup(?)

.code
main:
    ; 设置 SEH 链:
    assume  fs:nothing
    push    offset Error_Handler
    push    fs:[0]
    mov     fs:[0], esp

    invoke  GetModuleHandle, NULL
    mov     hInstance, eax
    invoke  DialogBoxParam, hInstance, offset szDlgName, 0, WndProc, 0

    ; 恢复原来的 SEH 链:
    pop     fs:[0]
    pop     eax
    invoke  ExitProcess, 0

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    .if uMsg == WM_CLOSE
        invoke EndDialog, hWnd, 0

    .elseif uMsg == WM_INITDIALOG
        mov eax, hWnd
        mov [mbp.hwndOwner], eax
        invoke LoadIcon, hInstance, IDI_LC
        invoke SendMessage, hWnd, WM_SETICON, ICON_SMALL, eax
        ; 储存 API 的原入口地址:
        invoke GetModuleHandle, addr szLibUser
        invoke GetProcAddress, eax, addr szProcMsgBoxInd
        mov [dwAddress], eax
        ; 保存原对话框的输出文字:
        invoke lstrcpy, addr szText, addr szMsgAbout

    .elseif uMsg == WM_COMMAND
        mov eax, wParam
        mov edx, eax
        shr edx, 16
        movzx eax, ax
        .if edx == BN_CLICKED
            .if eax == IDC_BUTTON_EXIT || eax == IDCANCEL
                invoke EndDialog, hWnd, NULL

            .elseif eax == IDC_BUTTON_ABOUT || eax == IDOK
                mov [mbp.cbSize], sizeof mbp
                mov eax, hInstance
                mov [mbp.hInstance], eax
                mov [mbp.lpszText], offset szMsgAbout
                mov [mbp.lpszCaption], offset szCaption
                mov [mbp.dwStyle], MB_OK or MB_APPLMODAL or MB_USERICON
                mov [mbp.lpszIcon], IDI_LC
                invoke MessageBoxIndirect, addr mbp

            .elseif eax == IDC_CHECKBUTTON_HOOK
                ; 把内存保护设置成 可读/可写/可执行:
                invoke VirtualProtect, [dwAddress], 1, PAGE_EXECUTE_READWRITE, addr dwOldProtect
                invoke IsDlgButtonChecked, hWnd, IDC_CHECKBUTTON_HOOK
                mov edx, [dwAddress]
                test eax, eax
                .if zero?                                           ; uninstall hook
                    mov cl, [bOldByte]                              ; bOldByte = API 原入口地址
                    mov byte ptr [edx], cl                          ; 恢复 API 的原入口地址
                    invoke lstrcpy, addr szMsgAbout, addr szText    ; 恢复原对话框的输出文字:
                .else                                               ; re-install hook
                    mov cl, byte ptr [edx]                          ; byte ptr [edx] = API 原入口地址
                    mov byte ptr [edx], 0CCh                        ; 断点异常(INT 3 指令)
                    mov [bOldByte], cl                              ; 储存 API 的原入口地址
                    invoke lstrcat, addr szMsgAbout, addr szMyText  ; 改变原对话框的输出文字:
                .endif

            .endif
        .endif
    .else
        mov eax, FALSE
        ret
    .endif
    mov eax, TRUE
    ret
WndProc endp

;****************************************
; 函数功能:处理异常错误
;****************************************
Error_Handler proc uses ecx lpExceptRecord:DWORD, lpFrame:DWORD, lpContext:DWORD, lpDispatch:DWORD
    ; 输出 "API hooked":
    invoke  MessageBox, [mbp.hwndOwner], addr szMsgHooked, addr szCaption,\
            MB_OK or MB_ICONINFORMATION

    ; 储存并改变 SetHook 函数的返回值:(经过修正)
    ; (想不明白?呵呵,用调试器跟踪一下吧,我也说不清楚,只能意会不能言传……)
    mov eax, [lpContext]
    mov eax, [eax][CONTEXT.regEsp]
    mov ecx, [eax]
    mov [eax], offset SetHook
    mov [dwRetAddr], ecx

    ; 把 API 原入口地址写回去,以便继续运行原 API:
    ; (跟踪一下吧,我实在是不知道怎么才能说得清楚……)
    mov eax, [dwAddress]
    mov cl, [bOldByte]
    mov byte ptr [eax], cl

    ; 继续下一个 Execution:
    mov eax, ExceptionContinueExecution
    ret
Error_Handler endp

;****************************************
; 函数功能:设置 API Hook
;****************************************
SetHook proc uses ecx
    mov eax, [dwAddress]
    mov cl, [eax]
    mov byte ptr [eax], 0CCh    ; 断点异常(INT 3 指令)
    mov [bOldByte], cl
    jmp [dwRetAddr]             ; 跳回经过 Hook 之后的 API 的返回地址(很重要!)
SetHook endp

end main
;********************   over    ********************
;by LC

它的资源文件:

#include "resource.h"

#define IDI_LC              1
#define IDC_CHECKBOX_HOOK   3000
#define IDC_BUTTON_ABOUT    3001
#define IDC_BUTTON_EXIT     3002
#define IDC_STATIC          -1

IDI_LC  ICON    "lc.ico"

LC_DIALOG DIALOGEX 10, 10, 200, 50
STYLE DS_SETFONT | DS_CENTER | WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "SEH for API Hook by LC, 2002-11-22"
FONT 8, "MS Sans Serif"
BEGIN
    AUTOCHECKBOX    "&Hook MessageBoxIndirectA", IDC_CHECKBOX_HOOK, 5, 5, 190, 12
    PUSHBUTTON      "关于(&A)", IDC_BUTTON_ABOUT, 5, 30, 90, 14, BS_FLAT | BS_CENTER
    PUSHBUTTON      "退出(&X)", IDC_BUTTON_EXIT, 105, 30, 90, 14, BS_FLAT | BS_CENTER
END

没啥特别的,仔细一想就明白了。

老罗
2002-11-22