原文: d2 {' l7 d0 E8 F( e
http://www.cnblogs.com/rogeryu/archive/2009/06/04/1496538.html4 B1 _- j" F" |' b. T
, [; X' }: g9 V& v
所谓的API Hook,就是利用某种技术将API的调用转为我们自己定义的函数的调用。这种技术在实际项目里面的应用也是很广泛的。最近,我在做关于我们项目的自动化测试的时候,就遇到了这种情况。在写测试代码之前,我们对测试代码有一些要求。1. 不能因为测试代码而修改原代码。2. 原有的模块是以dll格式输出的,在做测试的时候,要测的类和函数也只能使用dll的导出类或者函数,而不能将源文件重新编译。由于这些限制,导致测试用例往往不能在普通的机器上运行。比如这样一个函数:
. ^" l: M* }8 g% b. J0 i, i
( W4 J; B+ }2 d5 L: Hint func()
/ ^2 N2 ^, t/ V0 p/ R$ J* z{
5 x8 o# i# }7 T5 G //Some initializing codes
3 j3 Y2 J* `) @ int hardware_code = get_hardware_code();
( V d8 m2 z4 F, ^( h* ^# P if (is_valid_code(hardware_code))
, I) S: t) W+ L& W. @* }& c7 \ {/ I/ f, d6 t4 P* l7 M% g" R
//% z5 w. l J7 X/ W" Q# G- V6 z
}$ q0 w. X+ s4 q# _
//# y$ S) k' {7 |( P$ m/ a
return ret;/ T' g$ G6 c5 r4 w3 N
}/ k" ~' O1 }$ G) ]- ?
此处,函数get_hardware_code()是与特定平台相关的,在普通PC上运行肯定无法获得正确的结果。如果拿不到正确的结果,也就不能对函数func()进行测试了。于是,我们就可以利用API Hook技术,在测试代码里面,把所有对get_hardware_code()的调用换成我们自定义的函数mock_get_hardware_code()的调用,这样,在我们自己定义的函数里面,可以返回一个有效的代码以保证原代码能够正确的往下执行。
$ k8 M0 s, f3 M- @+ d9 z; R; S
( o9 Q. N; D( D# P7 H6 X. c经过研究,API Hook有这么几种方法。
; y6 ~1 h+ x4 b4 Z+ t
- Z5 [+ h7 P1 k! s ^1. 改写函数的首地址。
, E5 b$ J) g. u1 K5 L( h
+ F" C# `+ d+ U/ i" [0 Y' }这个是在《Windows核心编程》里面大师提到的API Hook的方法之一。原理就是,首先获得要被Hook的函数的地址(比如get_hardware_code()),然后将其首地址之后的若干字节(通常是5个字节)改成一条jmp指令,而jmp的目标地址就是自定义函数的地址(此处为mock_get_hardware_code())。这样,当函数每次执行目标函数的时候,就会跳转到我们自定义的函数里面去。这种方法很简洁,据说在Win16的年代经常被使用。但是大师并不推荐,好像是因为这种方法在多线程的环境下会有什么问题(具体的我忘记了,大家可以翻书看看)。( J! D5 w; v1 s- Y( C, I, n
$ V! n3 d9 ^3 v, F0 M: d这样的话,只要我们能得到被调函数的地址,我们就可以随心所欲的修改。当然,由于大师的不推荐,我这种方法只是实现了一下,并未真正应用。$ _' T9 S0 x. P5 k3 _ n
7 z! y3 ]/ c) W8 @% b
2. 改写导入表
( R4 n2 q* i7 N" s2 N! u- f1 j; @" _7 l9 p- Y# [, b5 U6 r$ S
这个也是《Windows核心编程》里面提到的,也是大师所推荐的。具体来说,就是遍历当前进程里面的所有模块,对其中每一个模块查找它的导入表。如果找到被测函数所在的dll,并且发现这个函数,那么就把这个地址修改成自定义函数的地址。关于如何从导入表中发现被测函数,我也总结了两种方式。1)对于一般的C导出函数,可以直接通过比较地址的方式去找,这个也在核心编程上面有一个小例子。2)对于C++中的导出的类成员函数而言,由于C++的指向成员函数的指针和普通的指针有所差别(我没仔细研究过,从网上查的),在将一个成员函数指针转化成普通的函数指针的时候编译通不过,因此我采取了第二种查找方式,也就是查找函数名。这还有一个问题,由于C++的导出成员函数名都进行了修饰,类似于?MethodName@ClassName@...@Z这种怪异的名字,不过,只要知道类名和方法名,然后查找MethodName@ClassName字符串就行。如果找到了这样的函数,然后在修改它的地址就行了。" t: s/ ~8 E) O t8 M# H
8 M; l, E! s3 D/ { X; e. ^另,关于导入表,大家可以去看雪论坛上有关Windows PE文件格式的介绍。此处就不多说了。0 z, ~1 {$ x$ [ W0 i
% k# n" t5 d% i7 y4 m# G' F7 r6 C3. 改写虚函数表。4 a6 [6 K8 n) j$ o! {) S
) ^' [ @, h3 d本来以为通过方法2就能hook住所有的导出类成员函数和普通函数,但还是出现了一个问题,因为我在尝试hook一个成员函数的时候,发现这个函数根本没有在导入表里面。后通过反汇编发现,由于那个导出类成员函数是一个虚函数,因为在通过指针调用的时候,它实际上是从虚函数表里面获得的函数地址进行调用的。因此对于hook这类函数,就需要改写它的虚函数表了。关于这个需要对C++的内存布局有所了解。我在这里就说一种比较简单的方式吧。' u+ x: s# ?3 F7 d
1 n6 u0 y x& r( v
一般来说,对于某个含有虚函数表的C++类,this指针指向的地址,取值就是虚函数表指针。虚函数表指针指向了虚函数表,里面的每一个元素都指向了实际要调用的函数的地址。因此,可以按照这样的方式访问虚函数表指针:) g' m3 q1 ?0 E* k
& [- ?, u2 o6 \& s2 P1 _) aint** pVTable = (int**)this;
+ e$ d% g/ [' a# E9 ^) n6 e7 p7 R Z3 }0 D! e* o
也就是将指向对象的指针强制转化成指针的指针,这样就可以通过取值就可以访问虚函数表:
1 F8 L: t* C% }5 H7 B: R! T& c' q3 E3 A% N( B
(*pVTable)[0] = address of virtual function 1;
8 n; }/ ? f! r4 n6 w( e2 v6 i: j( G3 q' T( ~/ U! J/ X
(*pVTable)[1] = address of virtual function 2;! j1 c3 n* f6 r' Z) v
! \" B; }: w: e! \...
6 i0 ~! o$ }+ |% b% e# X) O
& n, @' S4 P; g( V5 b V因此,我们就可以改写虚函数的地址了,从而达到hook的目的。这种技术来源于网上。当然,我对C++的内存布局也不是十分的清楚,如果一个类进行了多重继承,它的虚函数表是什么样子我也不太明白,这里只是说明了这样一种技术。3 K0 c* p( H" D5 ^
4 T2 f7 C3 c; l& ?$ z9 d- {以上就是目前为止我应用到的三种API Hook的技术,其实实际应用到的也就是后两种,这两种技术能够满足我目前项目的需要了。如果还有其他关于API Hook技术的话大家也可以交流。$ O; T( U2 l/ _% P; j4 i8 h
4 [. G' R; h) x, v" G
另外需要说明的一点是,上述三种方法中不管哪一种在改写地址的时候,由于Windows一般将那个地址所在的页面设置了保护属性,因为你需要用VirtualProtect函数将页面改为可读可写的属性才能改写,否则会有异常的。 |