首页 > 编程语言 > 汇编 > 汇编 Ring3 下实现 HOOK API
2012
06-12

汇编 Ring3 下实现 HOOK API

1. 介绍
1.1 什么叫Hook API?
1.2 API Hook的应用介绍
1.3 API Hook的原则
2. 挂钩方法
2.1 改写IAT导入表法
2.2 改写内存地址JMP法
3. 汇编实现
3.1 代码
3.2 分析

[ 1. 介绍 ]
  这篇文章是有关 OS Windows 下挂钩 API 函数的方法。所有例子都在基于NT技术的 Windows NT4.0 及以上有效(Windows NT 4.0, Windows 2000, Windows XP)。可能在其它 Windows 系统也会有效。

[ 1.1 什么叫 Hook API? ]
  所谓 Hook 就是钩子的意思,而 API 是指 Windows 开放给程序员的编程接口,使得在用户级别下可以对操作系统进行控制,也就是一般的应用程序都需要调用 API 来完成某些功能,Hook API 的意思就是在这些应用程序调用真正的系统 API 前可以先被截获,从而进行一些处理再调用真正的 API 来完成功能。

[ 1.2 API Hook 的应用介绍 ]
  API Hook技术应用广泛,常用于屏幕取词,网络防火墙,病毒木马,加壳软件,串口红外通讯,游戏外挂,internet通信等领域 API HOOK 的中文意思就是钩住 API ,对 API 进行预处理,先执行我们的函数,例如我们用 API Hook 技术挂接 ExitWindowsEx API 函数,使关机失效,挂接 ZwOpenProcess 函数(如:老王的 EncryptPE),隐藏进程等等……

[ 1.3 API Hook的原则 ]
  HOOK API有一个原则,这个原则就是:被 HOOK 的 API 的原有功能不能受到任何影响。如果你 HOOK API 之后,你的目的达到了,但 API 的原有功能失效了,这样不是 HOOK,而是 REPLACE ,操作系统的正常功能就会受到影响,甚至会崩溃。

[ 2. 挂钩方法 ]

[ 2.1 改写IAT导入表法 ]
  修改可执行文件的 IAT 表(即输入表)因为在该表中记录了所有调用 API 的函数地址,则只需将这些地址改为自己函数的地址即可,但是这样有一个局限,因为有的程序会加壳,这样会隐藏真实的 IAT 表,从而使该方法失效。

[ 2.2 改写内存地址JMP法 ]
  直接跳转,改变API函数的入口或出口的几个字节,使程序跳转到自己的函数,该方法不受程序加壳的限制。这种技术,说起来也不复杂,就是改变程序流程的技术。在CPU的指令里,有几条指令可以改变程序的流程:JMP,CALL,INT,RET,RETF,IRET等指令。理论上只要改变API入口和出口的任何机器码,都可以 HOOK,下面我就说说常用的改写 API 入口点的方法:
  因为工作在 Ring3 模式下,我们不能直接修改物理内存,只能一个一个打开修改,但具体的方法又分成好几种,我给大家介绍几种操作思路:
  <1>首先改写 API 首字节,要实现原 API 的功能需要调用 API 时先还原被修改的字节,然后再调用原 API,调用完后再改回来,这样实现有点麻烦,但最简单,从理论上说有漏 HOOK 的可能,因为我们先还原了 API,如果在这之前程序调用了 API,就有可能逃过 HOOK 的可能!
  <2>把被覆盖的汇编代码保存起来,在替代函数里模拟被被覆盖的功能,然后调用原函数(原地址+被覆盖长度).但这样会产生一个问题,不同的汇编指令长度是不一样的(比如说我们写入的JMP指令占用5个字节,而我们写入的这5个字节占用的位置不一定正好是一个或多个完整的指令,有可能需要保存7个字节,才不能打乱程序原有的功能,需要编写一个庞大的判断体系来判断指令长度,网上已经有这样的汇编程序(Z0MBiE 写的 LDE32),非常的复杂!
  <3>把被 HOOK 的函数备份一下,调用时在替代函数里调用备份函数.为了避免麻烦,可以直接备份整个 DLL,缺点就是太牺牲内存,一般不推荐使用这种方法!

[ 3. 汇编实现 ]
  本文就是建立在第2种方法之上的!本着先易后难的原则,今天我们先来说说它的第1种操作思路.
  我们拿 API 函数 ExitWindowsEx 来说明,下面是我在OD里拦下的 ExitWindowsEx 原入口部分
   77D59E2D $ 8BFF mov edi,edi
   77D59E2F . 55 push ebp
   77D59E30 . 8BEC mov ebp,esp
   77D59E32 . 83EC 18 sub esp,18
   ……
  如果我们把 ExitWindowsEx 的入口点改为下面的,会出现什么情况?
   77D59E2D B8 00400000 mov eax,4000
   77D59E32 FFE0 jmp eax
   ……
  我们可想而知,程序执行到 77D59E32 处就会改变流程跳到 00400000 的地方

  如果我们的00400000处是这样的子程:

  MyAPI proc bs:DWORD ,dwReserved:DWORD ;和ExitWindowsEx一样带2个参数
   做你想做的事
   ……
   ;这里放 API 入口点改回原机器码的代码
   ;如果你是备份的整个DLL,就直接调用备份API,不用改来改去了,不会有漏勾API的可能!
   invoke ExitWindowsEx,bs,dwReserved
   ;这里放HOOK API的代码
   .endif
   mov eax,TRUE
  ret

  这里的 MyAPI 是和 ExitWindowsEx 参数一样的的子程,因为程序是在 API 的入口部分跳转的,根据 stdcall 约定(参数数据从右向左依次压栈,恢复堆栈的工作交由被调用者),此时堆栈还没有恢复,我们在子程里取出的参数数据依然有效,我们可以在这里执行自己的代码,你可以决定是否继续按原参数或改变参数后再调用原 API,也可以什么都不做,当然在调用之前,我们要先还原我们修改过的 API(可以事先用 API 函数 ReadProcessMemory 读出原 API 的前几个字节备份之),调用完后再改回来继续 HOOK API,不过这种方法有漏 API 的可能(原因前面已经说了),你如果觉得这个方法不妥,因为一般系统 DLL 都不大,你可以备份整个 DLL。

  下面我就列出 Ring3 下 HOOK API 的几个步骤:
  1.得到要挂勾 API 的入口点
  2.修改 API 的入口点所在页的页面保护为可读写模式
  3.用 ReadProcessMemory 读出 API 的入口点开始的几字节备份
  4.用 WriteProcessMemory 修改 API 的入口点象这样的形式:
   mov eax,4000
   jmp eax
  其中的 4000 要用和原 API 参数一样的子程序地址代替

  在这个子程序里我们决定用什么参数再调用原 API,不过调用之前要用备份的前8字节改回来调用之后在挂勾,如此反复.

[ 3.1. 代码 ]
  前面所讲的是本进程挂勾,我们要挂勾所有进程,可以用全局勾子,需要单独的一个 DLL,我们可以在 DLL 的 DLL_PROCESS_ATTACH 事件里来 HOOK API

=================================hookdll.dll==========================
.486
.model flat,stdcall ;参数的传递约定是stdcall(从右到左,恢复堆栈的工作交由被调用者)
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

HOOKAPI struct
  a byte ?
  PMyapi DWORD ?
  d BYTE ?
  e BYTE ?
HOOKAPI ends

;子程序声明
WriteApi proto :DWORD ,:DWORD,:DWORD,:DWORD
MyAPI proto :DWORD ,:DWORD
GetApi proto :DWORD,:DWORD

;已初始化数据
.data
hInstance dd 0
WProcess dd 0
hacker HOOKAPI <>
CommandLine LPSTR ?

Papi1 DWORD ?
Myapi1 DWORD ?
ApiBak1 db 10 dup(?)
DllName1 db “user32.dll”,0
ApiName1 db “ExitWindowsEx”,0
mdb db “下面的程序想关闭计算机,要保持阻止吗?”,0

;未初始化数据
.data?
hHook dd ?
hWnd dd ?

;程序代码段
.code
DllEntry proc hInst:HINSTANCE, reason:DWORD, reserved1:DWORD
  .if reason==DLL_PROCESS_ATTACH ;当DLL加载时产生此事件
    push hInst
    pop hInstance

    invoke GetCommandLine
    mov CommandLine,eax ;取程序命令行

    ;初始化
    mov hacker.a,0B8h ;mov eax,
    mov hacker.PMyapi ;0x000000
    mov hacker.d,0FFh ;jmp
    mov hacker.e, 0E0h ;eax

    invoke GetCurrentProcess ;取进程伪句柄
    mov WProcess ,eax
    invoke GetApi,addr DllName1,addr ApiName1 ;取API地址
    mov Papi1,eax ;保存API地址
    invoke ReadProcessMemory,WProcess,Papi1,addr ApiBak1,8,NULL ;备份原API的前8字节
    mov hacker.PMyapi,offset MyAPI ;0x0000,这里设置替代API的函数地址
    invoke WriteApi,WProcess,Papi1, addr hacker ,size HOOKAPI ;HOOK API
  .endif

  .if reason==DLL_PROCESS_DETACH
    invoke WriteApi,WProcess,Papi1, addr ApiBak1 ,8 ;还原API
  .endif

  mov eax,TRUE
  ret
DllEntry Endp

GetMsgProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
  invoke CallNextHookEx,hHook,nCode,wParam,lParam
  mov eax,TRUE
  ret
GetMsgProc endp

InstallHook proc
  invoke SetWindowsHookEx,WH_GETMESSAGE,addr GetMsgProc,hInstance,NULL
  mov hHook,eax
  ret
InstallHook endp

UninstallHook proc
  invoke UnhookWindowsHookEx,hHook
  invoke WriteApi,WProcess,Papi1, addr ApiBak1 ,8
  ret
UninstallHook endp

GetApi proc DllNameAddress:DWORD,ApiNameAddress:DWORD
  invoke GetModuleHandle,DllNameAddress ;取DLL模块句柄
  .if eax==NULL
    invoke LoadLibrary ,DllNameAddress ;加载DLL
  .endif
  invoke GetProcAddress,eax,ApiNameAddress ;取API地址
  mov eax,eax
  ret
GetApi endp

;============================下面是核心部分=========================
WriteApi proc Process:DWORD ,Papi:DWORD,Ptype:DWORD,Psize:DWORD
  LOCAL mbi:MEMORY_BASIC_INFORMATION
  LOCAL msize:DWORD

  ;返回页面虚拟信息
  invoke VirtualQueryEx,Process, Papi,addr mbi,SIZEOF MEMORY_BASIC_INFORMATION

  ;修改为可读写模式
  invoke VirtualProtectEx,Process, mbi.BaseAddress,8h,PAGE_EXECUTE_READWRITE,addr
  mbi.Protect

  ;开始写内存
  invoke WriteProcessMemory,Process, Papi, Ptype,Psize ,NULL
  PUSH eax

  ;改回只读模式
  invoke VirtualProtectEx,Process,mbi.BaseAddress,8h,PAGE_EXECUTE_READ,addr mbi.Protect
  pop eax

  ret
WriteApi endp

;替代的API,参数要和原来一样
MyAPI proc bs:DWORD ,dwReserved:DWORD
  invoke MessageBox, NULL, CommandLine, addr mdb, MB_YESNO ;弹出信息框选择是否阻止
  .if eax==7 ;如果选择否
    invoke WriteApi,WProcess,Papi1, addr ApiBak1 ,8 ;先还原API
    invoke ExitWindowsEx,bs,dwReserved ;再调用API
    invoke WriteApi,WProcess,Papi1, addr hacker ,sizeof HOOKAPI ;调用完后再改回来
  .endif
  mov eax,TRUE
  ret
MyAPI endp

End DllEntry

===============================hookdll.def=============================
LIBRARY hookdll
EXPORTS InstallHook
EXPORTS UninstallHook

[ 3.2. 分析 ]
HOOKAPI struct
  a byte ?
  PMyapi DWORD ?
  d BYTE ?
  e BYTE ?
HOOKAPI ends

  为了便于理解和使用,我定义了一个结构:这个结构有4个成员,第一个成员a,是个字节型,我用来放 0B8h(mov eax), PMyapi一个整数型,用来放我们的替代 API 函数的地址(0X000),第3个和第4个成员我分别用来放 JMP 和 EAX(jmp eax) 那么连起来就是 mov,0X0000 ; jmp eax

.if reason==DLL_PROCESS_ATTACH
  push hInst
  pop hInstance
  invoke GetCommandLine
  mov CommandLine,eax

  ;初始化
  mov hacker.a,0B8h ;mov eax,
  ;mov hacker.d PMyapi ;0x0000
  mov hacker.d,0FFh ;jmp
  mov hacker.e, 0E0h ;eax
  invoke GetCurrentProcess
  mov WProcess ,eax

  当 DLL 加载时,我们先保存模块句柄,读取程序命令行,然后初始化 HOOKAPI 结构,写入我们要写到内存的指令( PMyapi 以后写入)并调用 GetCurrentProcess 取出进程伪句柄方便以后写内存。
  invoke GetApi,addr DllName1,addr ApiName1
  mov Papi1,eax
  invoke ReadProcessMemory,WProcess,Papi1,addr ApiBak1,8,NULL
  mov hacker.PMyapi,offset MyAPI ;0x0000
  invoke WriteApi,WProcess,Papi1, addr hacker ,size HOOKAPI ;HOOK API

  接下来用子程 GetApi 取出要挂勾 API 的入口点,并用 ReadProcessMemory 读出入口点8字节备份之,写入 PMyapi 调用子程 WriteApi 改写 API 的入口点,这个子程我不准备详细说了,它非常的简单,无非就是几个 API 的调用.它的核心就是通过 WriteProcessMemory 改写内存。
  .if reason==DLL_PROCESS_DETACH
    invoke WriteApi,WProcess,Papi1, addr ApiBak1 ,8
  .endif
  mov eax,TRUE
  ret

  如果这个 DLL 被卸载了,那么那个在 DLL 里的替代函数( MyAPI )将是无效的,如果这个时候程序再调用这个 API ,将出现非法操作,因此在 DLL 卸载前,我们必须还原 API。
  总结一下,现在只要程序加载这个 DLL,这个程序的 ExitWindowsEx 就会被我们勾住,接下来要怎样才能让所有的程序都加载这个 DLL呢?这就需要安装全局勾子:
InstallHook proc
  invoke SetWindowsHookEx,WH_GETMESSAGE,addr GetMsgProc,hInstance,NULL
  invoke WriteApi,WProcess,Papi1, addr hacker ,sizeof HOOKAPI
  mov hHook,eax
  ret
InstallHook endp

  通过 SetWindowsHookEx 安装勾子,最后一个参数可以决定该钩子是局部的还是系统范围的。如果该值为 NULL,那么该钩子将被解释成系统范围内的,那它就可以监控所有的进程及它们的线程。如果该函数调用成功的话,将在 eax 中返回钩子的句柄,否则返回 NULL。我们必须保存该句柄,因为后面我们还要它来卸载钩子,可以看出,我们创建的 Hook 类型是 WH_CALLWNDPROC 类型,该类型的 Hook 在进程与系统一通信时就会被加载到进程空间,从而调用 dll 的初始化函数完成真正的 Hook,值得一提的是:因为要调用 SetWindowsHookEx 来安装钩子,我们 GUI 程序的这个 DLL 不会被 UnhookWidowHookEx 卸载,也就只有一次 DLL_PROCESS_ATTACH 事件,因此这里再要 HOOK API 一次!

  我们回头来看看钩子回调函数:
GetMsgProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
  invoke CallNextHookEx,hHook,nCode,wParam,lParam
  mov eax,TRUE
  ret
GetMsgProc endp

  可以看到这里只是调用 CallNextHookEx 将消息交给 Hook 链中下一个环节处理,因为这里 API 函数 SetWindowsHookEx 的唯一作用就是让进程加载我们的 dll。
UninstallHook proc
  invoke UnhookWindowsHookEx,hHook
  invoke WriteApi,WProcess,Papi1, addr ApiBak1 ,8
  ret
UninstallHook endp

  要卸载一个钩子时调用 UnhookWidowHookEx 函数,该函数仅有一个参数,就是欲卸载的钩子的句柄。钩子卸载后我们也要还原我们 GUI 程序的 API。

LIBRARY hookdll
EXPORTS InstallHook
EXPORTS UninstallHook

  我们公开 DLL 里的 InstallHook 和 UninstallHook 函数,方便程序调用,这样我们只要在另外的程序中调用 InstallHook 便可安装全局勾子,勾住所有程序中的 API:ExitWindowsEx,执行我们自定的子程!如果不需要了,可以调用 UninstallHook 卸载全局勾子。
  请注意:对于远程钩子,钩子函数必须放到 DLL 中,它们将从 DLL 中映射到其它的进程空间中去。当 WINDOWS 映射 DLL 到其它的进程空间中去时,不会把数据段也进行映射。简言之,所有的进程仅共享 DLL 的代码,至于数据段,每一个进程都将有其单独的拷贝。这是一个很容易被忽视的问题。您可能想当然的以为,在 DLL 中保存的值可以在所有映射该 DLL 的进程之间共享。在通常情况下,由于每一个映射该 DLL 的进程都有自己的数据段,所以在大多数的情况下您的程序运行得都不错。但是钩子函数却不是如此。对于钩子函数来说,要求 DLL 的数据段对所有的进程也必须相同。这样您就必须把数据段设成共享的:

  一般来说, 目标文件有三个段, 分别是 text/data/bss 段.
   .text 段放置代码, 是只读且可运行段
   .data 段放置静态数据, 这些数据会被放置入 exe 文件. 这个段是可读写, 但是不能运行的.
   .bss 段放置动态数据, 这些数据不被放入 exe 文件, 在exe文件被加载入内存后才分配的空间.
  你可以通过在链接开关中指定段的属性来实现:
   /SECTION:name,[E][R][W][S][D][K][L][P][X]
  其中 S 表示共享,已初期化的段名是.data,未初始化的段名是.bss。假如您想要写一个包含钩子函数的 DLL,而且想使它的未初始化的数据段在所有进程间共享,您必须这么做:
   link /section:.bss[S] /DLL /SUBSYSTEM:WINDOWS ……….
  否则,您的全局勾子将不能正常工作!

最后编辑:
作者:NINE
这个作者貌似有点懒,什么都没有留下。

留下一个回复

你的email不会被公开。