本文是我的本科学位论文, 今发表在此, 以示原创之据
第1章 绪论
研究背景
研究意义
相关技术简介
COM概述
COM内存模型描述及C语言和C++语言实现
调用约定
Hook API原理
Windows钩子原理及进程注入
开发及调试环境
第2章 问题抽象及关键技术研究
实验01:通过调试器查看C++类的虚函数表
实验02:通过函数指针调用C++虚函数
实验03:交换两个相同C++类的虚函数表
实验04-1:替换C++虚函数表中的虚函数(__thiscall)地址
实验04-2:替换C++虚函数表中的虚函数(__stdcall) 地址
实验05:进程内Hook API(MessageBoxW)
本章小结
第3章 研究方案总体设计
实验06:准备COM组件样本,并实现一个IMath接口(程序名称:Sample.dll)
实验07-1:测试Sample.dll组件(程序名称:SampleTest.exe)
实验07-2:测试Sample.dll组件(程序名称:SampleTest.html)
实验08:使用对象代理法来Hook IMath的接口函数(程序名称:SampleProxy.dll)
实验09:使用虚表补丁法来Hook IMath的接口函数(程序名称:SampleVtable.dll)
实验10:对实验程序SampleProxy.dll,SampleVtable.dll的综合测试(程序名称:SampleHook.exe)
本章小结
第4章 实例:面向IE的网页弹窗拦截器
实现背景
网页弹窗原理1:IHTMLWindow2Vtable::open
网页弹窗例程1:例程 Open_IHTMLWindow2.html
网页弹窗原理2:IWMPCore::launchURL
网页弹窗例程2:例程 Open_IWMPCore.html
使用虚表补丁法Hook IHTMLWindow2Vtable::open,IWMPCore::launchURL
Dll入口函数(DllMain)需要注意的问题
本章小结
第5章 结论与展望
参考文献
致谢
附录(术语对照表)
原创声明
第1章 绪论
1.1 研究背景
两年前,我由于工作需要,需要在开发的软件中加入截取其他播放器中正在播放的电影画面功能,起初也没觉得困难,也就是利用Windows的GDI函数而已。在程序测试的过程中发现,偶尔截取出来的画面是黑的。经探究得知,是由于开启了硬件加速造成了截取的图像黑屏。
在Windows环境下开发视频播放器,多数使用微软的DirectXSDK。通常屏幕上的数据都是通过PrimarySurface(中文称为主表面)送至显示器。除了PrimarySurface还有OffScreenSuface(中文称之为离屏表面),故名思义,这种表表处理的图像数据是无法直接用于显示的,必须传输至PrimarySurface才能够显示在显示器上。对于开启了硬件加速的机器,还可以使用OverlaySurface(中文称之为覆盖表面)。对OverlaySurface的支持完全由硬件提供,使用OverlaySurface能够充份利用硬件优势。通常使用的截屏函数都是针对PrimarySurface,而无法获取OverlaySurface中的数据。所以,当播放器使用OverlaySurface显示图像时,截出来的画面黑屏就不足为奇了。而且微软也没有提供相应的API来截取OverlaySurface中的数据。并且,DirectX开发包是以COM接口形式提供给用户使用的,就这意味着,不能使用HookAPI方法来获取COM接口中的数据。
任何程序的核心功能,其本质都是处理数理。如果一个程序在它的生命周期内,没有处理并输出任何数据,那么这个程序肯定是毫无意义的。程序处理的数据通常来源于以下几个渠道:来自用户输入的数据,来自数库中的数据,来自存储设备的数据,以及来自网络的数据。这些数据的源都是可以“正常”获取的。换言之,在各个编语言中都提供了公开的API来读写这些数据。但是这些数据的获取方式,并不能满足所有的应用。例如前文中所描述的情况:我们要获取的是第三方播放器的屏幕图像数据。既然没有公开的API来获取,就只能另辟蹊径了。
1.2 研究意义
当前的应用要求越来越复杂,从第三方应用的进程中获取数据的场合也会越来越多。例如:针对进程级的文件透明加密,可以使用HookAPI技术来实现。内核级的文件透明加密,可以Hook内核相关函数,或通过文件过滤技术来实现。还有一些系统还原软件(例如,影子系统)可以使用卷过滤技术来实现。这些常见的应用,其本质都是先获取“别人”的数据再进行处理。可见,处理第三方应用程序中的数据这种要求在未来还会有很多。
随着个人计算机的普及,Windows用户也越来越多。在Windows系统中,COM技术渗透到了各个领域内。几经发展,现在COM技术已经非常成熟悉。在未来会有越来越多的程序使用COM技术来构建。这就是说,我们还会有很多的机会遇到从COM接口中获取数据的需求。有鉴于此,研究从如何HookCOM接口函数,是非常有必要的。
1.3 相关技术简介
1.3.1 COM概述
COM(Component Object Mode),中文称为组件对象模型。COM是微软公司制定的面向对象的二进制可重用组件标准。COM技术的前身是OLE技术,起源于上世纪90年代。在Windows平台环境中,COM技术随处可见。最被人们所熟知的就是基于COM技术的ActiveX技术了(ActiveX是COM技术的一个分支)。COM的主要特点体现在:内存模型确定性,组件面向对象,编写组件语言无关性,调用组件语言无关性,组件位置透明性。
内存模型确定性:在COM标准中,明确规范了COM接口及的二进制内存模型,无论使用什么语言来编写COM组件,只要能够按照COM标准,编译出它规范的内存模型即可。
组件面向对象:COM组件是面向对象的,即COM组件中的接口都是由各个COM对象来实现。
编写组件语言无关性:COM组件可以使用任何语言进行编写,只要能够实际COM标准中的内存模型即可。
调用组件语言无关性:COM组件是二进制一级的重用标准,理论上可以使用任何语言调用。但实际上有一些编程语言调用起来还是比较复杂的,故此基于COM自动化技术的ActiveX技术得以发展,其原因是客户程序使用起来非常方便。
组件位置透明性:调用一个COM组件前,不需要知道它在什么位置,只需要知道要使用地接口GUID和对象的GUID即可进行调用。
1.3.2 COM内存模型描述及C语言和C++语言实现
COM接口通常是一个指针(有的编程语言以已去掉了指针的概念,例如C#),接口指针指向的另一块内存,被称为接口
函数表,接口函数表里包含了一组属于该接口的函数的实际地址。COM组件的结构如图1.1所示:
图1.1 COM组件结构
COM接口的内存结构如图1.2所示:
图1.2 COM接口的内存结构
COM接口的内存模型可用如下C语言来描述:
struct *ISampleVtable;
struct ISample
{
ISampleVtable* VtablePtr;
};
RETVAL (*Function1)(ISample* ThisPtr); // 声明Function1类型的指针
RETVAL (*Function2)(ISample* ThisPtr); // 声明Function2类型的指针
RETVAL (*Function3)(ISample* ThisPtr); // 声明Function3类型的指针
struct ISampleVtable
{
Function1 pFxn1;
Function1 pFxn2;
Function1 pFxn3;
};
使用C语言来描述COM接口的内存模型,需要用到两个结构,上面代码段中的第二个结构保存了所有的函数指针。而第一个结构保存了第二个结构的指针。调用过程如下:
ISampleVtable->VtablePtr->pFxn1();
ISampleVtable->VtablePtr->pFxn2();
ISampleVtable->VtablePtr->pFxn3();
COM接口的内存模型有点类似于C++接口,当一个C++类中的成员函数全部是纯虚函数时,那么这样的C++类被称为“接口”。也可以理解为C++接口就是一个只有声明,但没有任何实现的C++类。要使用C++接口,必须创建一个继承于C++接口类的子类,并实现其全部纯虚函数。C++编译器在进行编译时,会为C++接口类生成一个“虚函数表”,在运行时程序初始这个表,并将真实的函数地址添加到虚函数表中。事实上,只要C++类中有虚函数的存在,编译器总会为这样的C++类生成一个虚函数表(后续实验中有所体现)。使用Viaual C++编译器进行编译时,这个虚函数表的位置就是C++对象的this指针所指向的地址。故此上面的COM接口模型,可以用下面的C++代码来描述:
class ISample
{
public:
virtual RETVAL Function1() = 0;
virtual RETVAL Function2() = 0;
virtual RETVAL Function3() = 0;
};
注:在C++语言标准中,并没有明确各个编译器处理虚函数表的机制,签于此,本文所有的实验代码,若无特殊说明,均是基于Visual C++来开发并编译的。
1.3.3 调用约定
任何程序在函数调用的过程中,都会涉及到函数的返回值放在哪里,函数的参数如何传递,函数的Stack(栈帧)由谁来Cleanup(清除)等问题。所以需要在函数的Caller(调用者)及Callee(被调用者)之间制定一些公约,即调用约定。下文主要说明三种在本文中涉及的调用约定。
__cdecl:C语言及Visual C++编译器默认的调用约定,该调用约定的特点是支持可变参数的传递,如典型的printf函数。函数参数从右至右依次入栈,函数调用完毕后,由调用者清除栈帧。
__stdcall: 函数参数从右至右依次入栈,函数调用完毕后,由被调者自已来清除栈帧。
__thiscall: 该调用约定是C++成员函数的默认调用约定。this指针通过ECX寄存器传递,其他参数从右到右依次入栈。函数调用完毕后,由被调者自已来清除栈帧。如果在C++成员函数前,显示的指定其他调用约定,则this是第一个参数(即最左边的)。MSDN中明确指出,在Visual C++ 2005以前__thiscall不能被显示的指定,因为它不是调用约定的关键字。
返回值: 小于等于4字节的值通过EAX返回,如果返回值小于4字节,将被扩展成4字节。8字节的值通过EDX:EAX返回,高4字节位于EDX,低4字节位于EAX。
大于8字节的结构,将一个指向结构的隐含的指针通过EAX返回。
1.3.4 Hook API原理
在Windows环境中,Hook这个单词极为常见,在一些中文的文献中Hook被翻译成“钩子”,“挂钩”,“挂接”,从名称就可看出,Hook就是要“钩拄什么”,“挂住什么”,或者“要与什么对接”。也可以理解为Hook就是要将函数的调用进行拦截。
Windows中较为常见的是鼠标钩子,键盘钩子,和消息钩子。尤其是键盘钩子,早就因各种盗号木马而名扬天下。这就不难看出,安装钩子的目的就是要先于目标程序得到一些信息,比如,要先于目标程序得到用户的按键信息。Windows系统内部维护了一个“钩子链”的数据结构,最后安装钩子的程序,可以最先得到将要“钩住”的信息。既然是一个“链”,那么当程序在钩住信息后,可以放行这个信息,由Windows将信息继续传递到这条链的下一个节点去。当然,也可以告诉Windows:“这条信息我扣留了,你直接返回吧”,这样的话,先安装钩子的程序就什么也得不到了。有一些安全性要求较高的应用(如银行的密码输入框,支付宝的密码输入框)也正是利用了Windows钩子链原理来达到不让其他程序截获键盘按钮消息的目的。当然,安全控件要做一些特殊的处理,以防止木马安装的钩子是有效的。因为键盘钩子常被用于非法目的,所以键盘钩子也是杀毒软件监示的最重要的目标行为。单就从技术上讲,没有正与反,技术就是技术,正如一把枪,它最终的作用,要取决于是在警察手里,还是在恶魔手里。
Windows提供了安装钩子的API,函数为SetWindowsHookEx,其原型如下:
HHOOK SetWindowsHookEx(
int idHook,
HOOKPROC lpfn,
HINSTANCE hMod,
DWORD dwThreadId
);
idHook:要安装的钩子类型
lpfn::钩子过程回调函数,原型如下:
LRESULT CALLBACK HookProc(
int nCode,
WPARAM wParam,
LPARAM lParam
);
需要注意的是,钩子可以是针对进程本身的,也可以是针对整个系统的。如果安装的钩子工作在系统范围内,那么这个回调函数必须实现在一个DLL中,并传递DLL的句柄给hMod参数,dwThreadId也必须置为零。
hMod::如果安装的是系统范围的钩子,则需要传递一个DLL的实例句柄。如果 dwThreadId与创建钩子的线程ID相同,则此值必须置为空。
dwThreadId:钩子工作在哪一个线程中,对于工作在系统范围的钩子,此值需置为零。
Hook API(Application Programming Interface),API即应用程序编程接口,在Windows环境下,程序员主要依赖于Windows API进行编程。挂钩API与挂钩键盘有所异同,相同之处是,它们的目的都是要先于目标程序获得信息。Windows的普通钩子是系统内置支持的,而API钩子需要开发者自己实现。
Windows API一般都是实现在系统的DLL中的。开发者可以使用LoadLibrary和GetProcAddress来确定一个函数的地址。当确定了函数的地址,我们可以将函数的前几个字节修改为一个跳转指令,并令其指向我们准备好的函数。这样一来,当目标进程调用函数时,就会跳转到我们准备好的函数中,待我们从函数的参数中拿到数据后,还原函数的前几个字节的数据,将控制权返回给原函数。这一复杂的过程我们可以使用一些开源的较稳定的库来操作。以避免自己计算原函数到我们的“替换函数”偏移操作的麻烦,以及修改内存的麻烦。比较好的开源库有,微软研究院开发的Detours库,该库的免费版仅能工作在x86平台下。还有另外一个较好的开源库mhook,不仅可以工作在x86平台上,而且也可以工作在x64平台上。并且它是免费的。后续实验代码中有详细使用示程。
1.3.5 Windows钩子原理及进程注入
Windows支持多务任并行,Windows系统为了运行稳健安全,Windows使用了进程隔离技术,各个进程独立拥有4GB地址地址空间。本文所讨论的Hook COM接口技术,其目的,也是要先于目标程序获得信息。而正是由于Windows进程隔离的特性,要想修改目标程序的内存地址,首要我们自己写的代码就得工作在目标程序的进程空间内。即我们的写好的代码要注入到目标进程空间中。
另外,针对本文所讨论的题目,还有一个注入时机的问题。下文的实验中,我们将要在目标进程中挂钩CoCreateInstance函数,以便获得目标进程创建的接口指。这就要求,在目标进程创建接口指针前,我们的代码就已经工作在目标进程空间,并完成了CoCreateInstance函数挂钩的工作。所以,最佳的注入时机就是目标进程启动的过程中。基于这一点,我们需要利用Windows和WH_GETMESSAGE(消息钩子),这样,当有消息到达目标进程时,Windows会自动替我们完成注入。
1.3.6 开发及调试环境
操作系统:
Windows XP SP3 (32-bit) 中文版
开发工具:
Visual Studio 6.0 Enterprise Edition
Visual Studio 6.0 Service Pack 5
Visual Studio 6.0 Service Pack 6
Platform SDK February 2003
Visual Studio 2008
MSDN Library for Visual Studio 2008
浏览器:
Internet Explorer (32-bit)
第2章 问题抽象及关键技术研究
实验01:通过调试器查看C++类的虚函数表
0001 class A
0002 {
0003 public:
0004 virtual void test1() = 0;
0005 virtual void test2() = 0;
0006 };
0007
0008 class B : public A
0009 {
0010 public:
0011 virtual void test1() { printf("test1\n"); }
0012 virtual void test2() { printf("test2\n"); }
0013 };
0014
0015 int main(int argc, char* argv[])
0016 {
0017 A* p = new B();
0018
0019 __asm int 3; // break point
0020 delete p;
0021 p = NULL;
0022
0023 return 0;
0024 }
图2-1 通过调试查看C++虚函数表
若C++类中包含有虚函数,C++编译器在进行编译时,会通过动态联编机制,为这个类生成一个“虚函数表”。由实验观察到,对象的this指针值为0x003f2da8,来到0x003f2da8内存处,并查看它的内存数据,前四个字节正是虚函数表的指针0x0042501c。由此可见,虚函数表的指针值,就是对象this指针所指向的首地址。再进一步查看0x0042501c处的内存数前,前两个4字节,正是第一个和第二个虚函数的地址0x0040100a,0x00401005。通过调试看到,这个C++接口的内存模型与COM接口的内存模型是一致的。如图
注1:Intel X86体系结构的CPU使用Little Endian(小字节序)来存储数字。例如有一个数字为0x11223344,那么这个数据在内存中序列的顺序是44 33 22 11,即内存的低地址存放这个数字的低位。与此相对应的,还有Big Endia(大字节序),如PowerPC,所以数字0x11223344,在内存中的存储的序列为 11 22 33 44。
注2:C++语言规范没有强制要求编译器如何处理虚函数。因为本文所讨论的是COM接口挂钩,而COM是标准的。故此,在实验接段,暂且认为这些实验的片断代码就是COM对象,并以此来推导出挂钩真正的COM接口的原理。
注3:实际上,只要C++类中存在虚函数,都会产生这个虚函数表,而不仅仅是C++接口类。
实验02:通过函数指针调用C++虚函数
0001 class A
0002 {
0003 public:
0004 A() { n = 45; }
0005 virtual void __stdcall test1() { printf("test1, n = %d\n", n++); }
0006 virtual void __stdcall test2() { printf("test2, n = %d\n", n++); }
0007
0008 int n;
0009 };
0010
0011 int main(int argc, char* argv[])
0012 {
0013 A* p = new A();
0014
0015 p->test1();
0016 p->test2();
0017
0018 LPDWORD VtablePtr = (LPDWORD)(*((LPDWORD)p)); // 取虚函数表指针
0019
0020 // 取test1,test2两函数地址
0021 DWORD Fn_0 = *(VtablePtr + 0);
0022 DWORD Fn_1 = *(VtablePtr + 1);
0023
0024 // 声明函数指针
0025 typedef void (__stdcall *PFN_Member)(void* pThis);
0026
0027 PFN_Member fn0 = (PFN_Member)(Fn_0);
0028 PFN_Member fn1 = (PFN_Member)(Fn_1);
0029
0030 fn0(p);
0031 fn1(p);
0032
0033 delete p;
0034 p = NULL;
0035
0036 return 0;
0037 }
test1, n = 45
test2, n = 46
test1, n = 47
test2, n = 48
该例程很简单,首先使用对象p来调用test1,test2。然后手动取得虚函数表的指针值,进而取得test1,test2这两个虚函数地址,并通过函指指针来调用。通过实验输出的结果观察到,使用函数指针也能正确调用test1,test2这两个虚函数。
该例程中,C++成员函数被显示的声明为__stdcall调用约定,这是为什么呢?回想一下“调用约定”那一章节。C++成员函数默认使用__thiscall调用约定,而在类外部声明函数指针时,不能指为__thiscall调用约定。在Visual C++ 2005以前,__thiscall并不是调用约定关键字。虽然从Visual C++ 2005开始可以显示的指定调用约定为__thiscall,但这只适用于C++类成员。所以,本例程中,将C++成员函数显示的指定为__stdcall调用约定,以确保程序正确执行。
注1: COM标准中规定,COM接口函数统一使用__stdcall调用约定。
实验03:交换两个相同C++类的虚函数表
0001 class A
0002 {
0003 public:
0004 A() { n = 90; }
0005 virtual void __stdcall test1(int a) { printf("A::test1, a = %d, n = %d\n", a, n++); }
0006 virtual void __stdcall test2(int a) { printf("A::test2, a = %d, n = %d\n", a, n++); }
0007
0008 int n;
0009 };
0010
0011 class B
0012 {
0013 public:
0014 B() { n = 50; }
0015 virtual void __stdcall test1(int a) { printf("B::test1, a = %d, n = %d\n", a, n++); }
0016 virtual void __stdcall test2(int a) { printf("B::test2, a = %d, n = %d\n", a, n++); }
0017
0018 int n;
0019 };
0020
0021 void exchange(void** p1, void** p2)
0022 {
0023 void* temp = *p1;
0024 *p1 = *p2;
0025 *p2 = temp;
0026 }
0027
0028 int main(int argc, char* argv[])
0029 {
0030 A* p1 = new A();
0031 B* p2 = new B();
0032
0033 p1->test1(1);
0034 p1->test2(2);
0035
0036 p2->test1(3);
0037 p2->test2(4);
0038
0039 exchange((void**)&p1, (void**)&p2);
0040
0041 p1->test1(1);
0042 p1->test2(2);
0043
0044 p2->test1(3);
0045 p2->test2(4);
0046
0047 delete p1;
0048 delete p2;
0049 p1 = NULL;
0050 p2 = NULL;
0051
0052 return 0;
0053 }
A::test1, a = 1, n = 90
A::test2, a = 2, n = 91
B::test1, a = 3, n = 50
B::test2, a = 4, n = 51
B::test1, a = 1, n = 52
B::test2, a = 2, n = 53
A::test1, a = 3, n = 92
A::test2, a = 4, n = 93
该例程中将p1,p2的指针值进行了交换。试想一下,他们到底交换了什么?首先p1,p2两对象的this指针被交换,this指针即是对象的首地址,所以交换了p1,p2的指针值,也就是交换了两个对象的this指针。其次,两个对象的this指针被交换又意味着什么?通过“实验01”,和“实验02”,已经清楚一个事实,即含有虚函数的C++类,它的对象的this指针所指向的地址即是虚函数表指针的所在。就是说,随着p1,p2的交换,他们各自的虚函数表也跟着进行了交换。从实验结果可以看出,p1,p2交换后,class B替代了class
A,也可以说是class A替代了class B,即它们成了各自对方的“替身”。从输出的A::n,B::n的值来看,两个对象替换后,其内部运行也是正确的。
下文件称这种方法为Object Proxy(对象代理法)。
这个实验要注意的是,两个C++类必须是一样的(虚函数的个数及原型,数据成员的内存分布),即两个C++类的内存模型要一致,否则程序会产生异常。另外,我们“偷梁换柱”的目的在于控制目标。通常我们可能只是想控制目标类里的一个或少有的几个函数,而不是控制整个C++类,但该例程要求两个C++类必须要一样。就是说,如果class A包含了几十个甚至上百个虚函数,那我们在编写class B时也要写好和class A相同数量的虚函数并且函数原型也要相同。所以,该例程中使用对象替换对象的方法,显得过于愚笨。
实验04-1:替换C++虚函数表中的虚函数(__thiscall)地址
0001 class A
0002 {
0003 public:
0004 A() {n = 99; m = 25;}
0005 virtual void test1(int a){printf("A::test1, a = %d, n = %d, m = %d\n", a, n++, m++);}
0006 virtual void test2(int a){printf("A::test2, a = %d, n = %d, m = %d\n", a, n++, m++);}
0007
0008 int n;
0009 int m;
0010 };
0011
0012 class B
0013 {
0014 public:
0015
0016 // the first byte is virtual table pointer of ‘class A‘
0017 void* VtableAddr;
0018 int n2; // A::n
0019 int m2; // A::m
0020 A* p1;
0021
0022 B()
0023 {
0024 n2 = 20; /* used for reference */
0025 m2 = 60; /* used for reference */
0026 p1 = new A();
0027 }
0028 ~B()
0029 {
0030 if (p1)
0031 {
0032 delete p1;
0033 p1 = NULL;
0034 }
0035 }
0036
0037 void A_test2(int a) // 替换A::test2
0038 {
0039 printf("A::test2, hook successed, a = %d, n2 = %d, m2 = %d\n",
0040 a, B::n2++/* output A::n */, B::m2++ /* output A::m */);
0041 }
0042
0043 template <typename T>
0044 static DWORD GetMemberFxnAddr(T MemberFxnName)
0045 {
0046 union
0047 {
0048 T From;
0049 DWORD To;
0050 } union_cast;
0051 union_cast.From = MemberFxnName;
0052 return union_cast.To;
0053 }
0054
0055 void test()
0056 {
0057 p1->test1(1);
0058 p1->test2(2);
0059
0060 LPDWORD VtablePtr = (LPDWORD)(*((LPDWORD)(LPVOID)p1)); // 取虚表指针
0061
0062 DWORD dwOld = 0;
0063 if(!::VirtualProtect(VtablePtr, sizeof(LONG) * 2, PAGE_READWRITE, &dwOld))
0064 return;
0065
0066 // 修改class A的第二个虚函数,即A::test2, 下标为[1]
0067 VtablePtr[1] = GetMemberFxnAddr(&B::A_test2);
0068
0069 if(!::VirtualProtect(VtablePtr, sizeof(LONG) * 2, dwOld, &dwOld))
0070 return;
0071
0072 p1->test1(3);
0073 p1->test2(4);
0074 }
0075 };
0076
0077 int main(int argc, char* argv[])
0078 {
0079 B obj;
0080 obj.test();
0081
0082 return 0;
0083 }
A::test1, a = 1, n = 99, m = 25
A::test2, a = 2, n = 100, m = 26
A::test1, a = 3, n = 101, m = 27
A::test2, hook successed, a = 4, n2 = 102, m2 = 28
该例程中,class A有两个虚函数,test1,test2,以及两个数据成员n,m。这个类是做为我们将要控制的目标类。class B中的A_test2用于替换A::test2函数。就是说,本例程仅仅是要控制A::test2的行为,而不是控制整个class A。
实际上只是为了替换一个A::test2函数,其实也没必要大动干戈的再写一个class B。既然是替换,这就要求,目标函数和我们准备的替换函数要相同,如返回值,调用约定,参数个数,参数类型,参数顺序要完全相同。所以,还是调用约定的问题。A::test2的调用约定是__thiscall,所以为了与目标函数调用约定相同,我们准备的替换函数只能写在一个C++类中。
该例程是通过修改虚函数表中的某个表项来达到替换函数的目的。虚函数表里的表项,就是一个一个虚函数的地址。在x86平台上,内存地址长度为4字节,所以本例程中直接使用了DWORD数据类型来表示函数地址。在x86-64(简称为x64)平台上,内存地址长度为8字节,所以,如果考虑到程序要编译为x64平台上运行的代码,地址就要使用8字节的数据类型,例如DWORD64。可以通过一个宏来切换地址这个数据类型,如下:
#if defined WIN64
typedef DWORD64* MEMADDR_PTR;
typedef DWORD64 MEMADDR_VAL;
#else
typedef DWORD* MEMADDR_PTR;
typedef DWORD MEMADDR_VAL;
#endif
虚函数表实际上是一个线性数组,在修改其中的值时,我们要事先清楚,要修改的虚函数地址在虚函数表里是第几项。通过查看class A的声明可知,A::test2是class A的第二个虚函数,虚函数表下标从0开始,所以我们要修改的虚函数表项表示为[1]。在挂钩实际的COM接口函数时,也是通过查看接口声明来确定接口函数是第几个。还要注意的是,接口是可以继承的,数接口函数是第几个的时候,一定要从最顶层的接口开始,即IUnknown接口。
该实验中还涉及到了内存属性的修改。Windows使用 Page(内存页面)来管理内存,在32 位Windows环境下,每个内存页面是4k。每个页面都有自己的保护属性。虚函数表的内存区则是受写保护的,即我们不能修改或在这个区域写入内容,如果直接修改,程序是会发生错误的。Windows提供了若干内存管理函数,其中VirtualProtect函数就是用来修改内存属性的。对于虚函数表的修改,我们可以使用该函数,先修改内存的属性,使之可读写,然后再修改虚函数表中的表项数据。
VirtualProtect,该函数原型如下:
BOOL WINAPI VirtualProtect(
__in LPVOID lpAddress, // 内存起始地址
__in SIZE_T dwSize, // 要修改的内存大小
__in DWORD flNewProtect, // 新的保护属性
__out PDWORD lpflOldProtect // 旧的保护属性
);
由于Windows是使用页面文件的方式来管理虚似内存的,即便是修改2字节大小的内存,若修改的内存恰巧在页面边距上,也会造成相邻间的两个页面的属性被修改。这可能会引起安全问题,所以,我们在修改内存属性后,修改完内存,一定要修改回原来的内存属性。
该实验中,需要将一个成员函数的地址赋给一个DWORD变量,成员函数地址是无法强制转换成DWORD类型的。代码中使用了一个小技巧,即利用union成员共用同一内存地址的特性,通过union将一个成员函数地址转换为一个DWORD类型的值,并且写成了一个模板函数,如下:
0043 template <typename T>
0044 static DWORD GetMemberFxnAddr(T MemberFxnName)
0045 {
0046 union
0047 {
0048 T From;
0049 DWORD To;
0050 } union_cast;
0051 union_cast.From = MemberFxnName;
0052 return union_cast.To;
0053 }
通过打印输出观察到,class A虚函数表的表项修改后,原来的A::test2指向了事先准备好的B::A_test2。但是B::A_test2函数打印输出时,指定了要输出的是B::n2,和B::m2,但是最终却输出了A::n,和A::m的值。这是因为,C++对象在访问数据成员时,其通过this指针加偏移方式来访问的。简短代码如下:
class A
{
public:
A(){n = 80; c = ‘E‘; m = 90;}
virtual void test1(){printf("A %d, %d\r\n", n++, m++);}
virtual void test2(){printf("B %d, %d\r\n", n++, m++);}
private:
int n;
char c;
int m;
};
int main(int argc, char* argv[])
{
A* p1 = new A();
LPDWORD ThisPtr = (LPDWORD)(p1); // this
printf("%d\n", *(ThisPtr + 1)); // A::n
printf("%c\n", *(ThisPtr + 2)); // A::c, 内存自对齐, 占字节
printf("%d\n", *(ThisPtr + 3)); // A::m
delete p1;
p1 = NULL;
return 0;
}
class A对象偏移为0的位置是它的虚函表指针,this+1后偏移4字节是A::n的值,this+2后偏移8字节的位置是A::m的值。在class B中为了正确打印出A::n,A::m,所以在class B类第一个数据成员是为了“占位”用的。这也就是B::A_test2能够正确打印出A::n,A::m的关键。
下文件称这种方法为VTable Patching(虚表补丁法)。
实验04-2:替换C++虚函数表中的虚函数(__stdcall) 地址
0001 class A
0002 {
0003 public:
0004 A() {n = 99; m = 25;}
0005 virtual void __stdcall test1(int a){printf("A::test1, a = %d, n = %d, m = %d\n", a, n++, m++);}
0006 virtual void __stdcall test2(int a){printf("A::test2, a = %d, n = %d, m = %d\n", a, n++, m++);}
0007
0008 int n;
0009 int m;
0010 };
0011
0012 typedef void (__stdcall* PFN_MEMBER)(void* ThisPtr, int a);
0013
0014 void __stdcall A_test2(void* ThisPtr, int a) // 替换A::test2
0015 {
0016 printf("A::test2, hook successed, a = %d, n = %d, m = %d\n",
0017 a, ((A*)ThisPtr)->n++, ((A*)ThisPtr)->m++);
0018 }
0019
0020 int main(int argc, char* argv[])
0021 {
0022 A* p1 = new A();
0023
0024 p1->test1(1);
0025 p1->test2(2);
0026
0027 LPDWORD VtablePtr = (LPDWORD)(*((LPDWORD)(LPVOID)p1)); // 取虚表指针
0028
0029 DWORD dwOld = 0;
0030 if(!::VirtualProtect(VtablePtr, sizeof(LONG) * 2, PAGE_READWRITE, &dwOld))
0031 goto _FREE;
0032
0033 // 修改class A的第二个虚函数,即A::test2
0034 VtablePtr[1] = (DWORD)(LPVOID)A_test2;
0035
0036 if(!::VirtualProtect(VtablePtr, sizeof(LONG) * 2, dwOld, &dwOld))
0037 goto _FREE;
0038
0039 p1->test1(3);
0040 p1->test2(4);
0041
0042 _FREE:
0043 if (p1)
0044 {
0045 delete p1;
0046 p1 = NULL;
0047 }
0048
0049 return 0;
0050 }
A::test1, a = 1, n = 99, m = 25
A::test2, a = 2, n = 100, m = 26
A::test1, a = 3, n = 101, m = 27
A::test2, hook successed, a = 4, n = 102, m = 28
本例程是实验04-1的改版,A::test1,A::test2的两个虚函数在类成员中被指定为__stdcall调用约定。这就是说,我们可以实现一个全局函数而不是一个C++成员函数,对class A中的某个虚函数进行替换。COM标准中规定,所有的接口函数的调用约定必须是__stdcall,本例程更接近于真实的COM接口。虚函数的替换过程,及实验输出结果与实验04-1相同。
实验05:进程内Hook API(MessageBoxW)
0001 #include "mhook/mhook-lib/mhook.h"
0002 #if defined _DEBUG || defined DEBUG
0003 #pragma comment(lib, "Debug/mhook.lib")
0004 #else
0005 #pragma comment(lib, "Release/mhook.lib")
0006 #endif
0007
0008 // 声明MessageBoxW的函数指针
0009 typedef int (WINAPI* PFN_MessageBoxW)(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);
0010
0011 // 保存MessageBoxW原来的地址
0012 PFN_MessageBoxW MessageBoxW_OLD = (PFN_MessageBoxW)GetProcAddress(GetModuleHandle(_T("user32.dll")), "MessageBoxW");
0013
0014 // 用于替换原始的MessageBoxW函数
0015 int __stdcall MessageBoxW_NEW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType)
0016 {
0017 // 简单的修改一下标题栏文字,将控制权返回给原始的MessageBoxW
0018 return MessageBoxW_OLD(hWnd, lpText, L"Hook - We are modified the caption", uType);
0019 }
0020
0021 // 开始拦截MessageBoxW的调用
0022 VOID MessageBoxW_Hook()
0023 {
0024 if (Mhook_SetHook((PVOID*)&MessageBoxW_OLD, MessageBoxW_NEW))
0025 {
0026 // successfully
0027 }
0028 }
0029
0030 // 关闭拦截MessageBoxW的调用
0031 VOID MessageBoxW_UnHook()
0032 {
0033 Mhook_Unhook((PVOID*)&MessageBoxW_OLD);
0034 }
0035
0036 // 主函数
0037 int APIENTRY _tWinMain(HINSTANCE hInstance,
0038 HINSTANCE hPrevInstance,
0039 LPTSTR lpCmdLine,
0040 int nCmdShow)
0041 {
0042 MessageBoxW(NULL, L"Hello World", L"Title", MB_OK);
0043
0044 MessageBoxW_Hook();
0045 MessageBoxW(NULL, L"Hello World", L"Title", MB_OK);
0046 MessageBoxW_UnHook();
0047
0048 MessageBoxW(NULL, L"Hello World", L"Title", MB_OK);
0049
0050 return 0;
0051 }
什么是Hook API? 这里单指Win32 API,由于在Windows环境下,应用程序必须要使用Windows 提供的API才能编写一个合格的Windows程序。在程序中更是频频调用系统API来完成各种功能,如窗口管理,进程线程管理,文件读写,内存管理等等。一部分应用,由于需求较为特殊,例如文件透明加密,需要在其他进程在文件读写时,为用户将文件解密和加密,必须先于目标进程获取文件读写的内容。也就是说,在目标进程调用文件读写相关API时,先要调用文件透明加密系统提供的“替代”函数。自己进行挂钩系统API,需要开发者了解DLL基址重定位,远线程注入,跳转指令机器码,跳转指令的偏移计算等相关知识。虽说Hook
API并不是什么新兴技术,但是要自己实现起来,还是要花一定的功夫。
该例程中使用了一个开原并免费的Hook API库-mhook来实现这一过程。该例程仅仅是为了演示如何使用mhook,所以直接挂钩的是进程内的MessageBoxW函数。而真实的挂钩Win32 API通常要实现在一个DLL中,并且在适时,将DLL注入到目标进程中。对于mhook库的使用,可以说是傻瓜化的。一般的步骤如下:
1 声明一个与将要挂钩函数相同的函数指针。本例程中要挂钩的函数是MessageBox,实际上系统并没有提供MessageBox函数,MessageBox实际是一个宏,跟据用户所创建的工程使用的字符集,如果是UNICODE,则MessageBox自动切换为MessageBoxW函数,如果工程使用的字符集是ANSI,则MessageBox,自动切换为MessageBoxA函数。因为本例程使用的是UNICODE字符集,所以所以声明函数指针时,参照了MessageBoxW。基本上,只要涉及到字符串的函数,Windows都提供了两个版本。
2 使用声明好的函数指针保存原始的API函数地址。通常我们挂钩的API都是在DLL中实现的,DLL在被进程加载后,有一个基址重定位的过程。使用mhook库时,不用考虑这个。只需要使用GetModuleHandle函数和GetProcAddress获取地址即可。这两个函数的原型如下:
HMODULE GetModuleHandle(
LPCTSTR lpModuleName // 模块名称
);
FARPROC WINAPI GetProcAddress(
__in HMODULE hModule, // 模块句柄
__in LPCSTR lpProcName // 函数名称(只能是ANSI字符串)
);
要挂钩的API所在的模块名称,可以通过查阅MSDN来获得。而函数的真实名称,有一个简单有效的方法得到。即在Visual C++编辑器中输入函数名称,如MessageBox,然后在此函数名上点击鼠标右键,在弹出的菜单是,点选Go To Definition,即可跳转到真实的函数声明处,如图2-2所示
图2-2 查看函数声明
3 按照将要挂钩的函数原型,准备一个“替代”函数。
4 使用Mhook_SetHook函数开始挂钩,或使用Mhook_Unhook函数停止挂钩。
5 将写好的挂钩模块,注入到目标进程。本文后续实验中仍然使用WH_GETMESSAGE钩子来完成注入过程。
程序运行结果如图2-3,图示2-4,图2-5所示:
图2-3 未挂钩前输出
图2-4 挂钩后输出
图2-5 取消挂钩后输出
本章小结
通过前几个实验,我们已经撑握了如何交换两个C++对象的的虚函数表;我们也撑握了通过虚函数表指针查找表中的虚函数,及进行虚函数地址改写,
这些知识,为我们挂钩真正的COM接口奠定了基础。但是在Hook实际的COM接口时,还有一些问题有待解决。
第3章 研究方案总体设计
实验06:准备COM组件样本,并实现一个IMath接口(程序名称:Sample.dll)
// Sample.dll
0001 STDMETHODIMP CMath::Add(int a, int b, int *c)
0002 {
0003 *c = a + b;
0004 return S_OK;
0005 }
0006
0007 STDMETHODIMP CMath::Sub(int a, int b, int *c)
0008 {
0009 *c = a - b;
0010 return S_OK;
0011 }
0012
0013 MIDL_DEFINE_GUID(IID, IID_IMath,0x09783CD5,0xCCAF,0x4D1D,0xA4,0x53,0x01,0x6E,0x68,0xE8,0x52,0x09);
0014 MIDL_DEFINE_GUID(CLSID, CLSID_Math,0x6BD26856,0x7090,0x4F8F,0xA8,0xF8,0xE6,0x05,0x9C,0xEB,0x73,0xAC);
在此例程中,我们创建了一个真正的COM组件,即Sample组件。在此组件中添加了一个接口IMath,并由CMath对象实现它。然后在IMath接口中添加了两个接口函数,一个是Add用于计算两数之和,一个是Sub用于计算两数之差。IMath接口除了继承了必须的接口(如IUnknow),还继承自IDispatchImpl,这是一个自动化支持接口,在后续的实验是,我们将还可以使用网页来调用Sample组件。
实验07-1:测试Sample.dll组件(程序名称:SampleTest.exe)
// SampleTest.exe
0001 class CSampleTestDlg : public CDialog
0002 {
0003 ...
0004 private:
0005
0006 IMath* m_lpIMath;
0007 };
0008
0009 BOOL CSampleTestDlg::OnInitDialog()
0010 {
0011 ...
0012
0013 AfxMessageBox("Start ?");
0014
0015 ::CoInitialize(NULL);
0016
0017 HRESULT hr;
0018
0019 // 方法一
0020
0021 /*
0022 CLSID clsid;
0023 CLSIDFromProgID(OLESTR("Sample.Math"),&clsid);
0024 hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, __uuidof(IMath), (LPVOID*)&m_lpIMath);
0025 */
0026
0027 // 方法二
0028 hr = CoCreateInstance(CLSID_Math, NULL, CLSCTX_INPROC_SERVER, IID_IMath, (LPVOID*)&m_lpIMath);
0029
0030 if (FAILED(hr))
0031 {
0032 CString str;
0033 str.Format("Error: 0x%08X", hr);
0034 AfxMessageBox(str);
0035 }
0036
0037 return TRUE; // return TRUE unless you set the focus to a control
0038 }
0039
0040 void CSampleTestDlg::OnButton1()
0041 {
0042 if (NULL == m_lpIMath)
0043 {
0044 AfxMessageBox("NULL");
0045 return;
0046 }
0047
0048 int a = GetDlgItemInt(IDC_EDIT1);
0049 int b = GetDlgItemInt(IDC_EDIT2);
0050
0051 int c;
0052 m_lpIMath->Add(a, b, &c);
0053
0054 CString str;
0055 str.Format("%d", c);
0056 SetDlgItemInt(IDC_EDIT3, c);
0057 }
0058
0059 void CSampleTestDlg::OnButton2()
0060 {
0061 IMath* lpIMath = NULL;
0062 HRESULT hr = CoCreateInstance(CLSID_Math, NULL, CLSCTX_INPROC_SERVER, IID_IMath, (LPVOID*)&lpIMath);
0063
0064 if (FAILED(hr))
0065 {
0066 CString str;
0067 str.Format("Error: 0x%08X", hr);
0068 AfxMessageBox(str);
0069 return;
0070 }
0071
0072 int a = GetDlgItemInt(IDC_EDIT4);
0073 int b = GetDlgItemInt(IDC_EDIT5);
0074
0075 int c;
0076 lpIMath->Sub(a, b, &c);
0077
0078 CString str;
0079 str.Format("%d", c);
0080 SetDlgItemInt(IDC_EDIT6, c);
0081
0082 lpIMath->Release();
0083 }
在调用实验06中的组件前,首先要将编译好的Sample.dll注册到系统当中。注册COM组件可以使用系统提供的实用工具Regsvr32。具体的注册方法是,在命令行中输入:regsvr32 -i "组件的完整路径及名称",反注册一个COM组件只需将-i改为-u即可。在客户程序(调用组件的程序)调用一个COM组件前,最重要的是知道将要使用的接口的GUID,以及实现接口函数的COM对象的GUID。这两个信息,在实验06中已经给出。
组件注册好以后,启动本例程SampleTest.exe,上面一行文本框,分别是,加数1,加数2,和数。下面一行文本框分别是减数1,减数2,差值。通过运行观察,Sample组个提供的加减法功能正常,测试通过。
后续实验中,我们将通过挂钩IMath接口中的函数,进而从SampleTest.exe中获取用户输入的原始值,并将其“偷偷修改”。
实验07-2:测试Sample.dll组件(程序名称:SampleTest.html)
// 文件名:SampleTest.html
0001 <HTML>
0002 <HEAD>
0003 <TITLE>test for hook COM interface and method</TITLE>
0004 </HEAD>
0005 <BODY>
0006 <script type="text/javascript">
0007 function TestAdd()
0008 {
0009 var c = math.Add(document.getElementById("AddNum_1").value, document.getElementById("AddNum_2").value);
0010 document.getElementById("Result1").value = c;
0011 }
0012 function TestSub()
0013 {
0014 var c = math.Sub(document.getElementById("SubNum_1").value, document.getElementById("SubNum_2").value);
0015 document.getElementById("Result2").value = c;
0016 }
0017 </script>
0018 <object id="math" width="0" height="0" classid="CLSID:6BD26856-7090-4F8F-A8F8-E6059CEB73AC"></object>
0019 <input type="text" id="AddNum_1"> + <input type="text" id="AddNum_2"> =
0020 <input type="text" id="Result1">
0021 <input type="button" value="button1" onClick = "javascript: TestAdd()">
0022 <br>
0023 <input type="text" id="SubNum_1"> - <input type="text" id="SubNum_2"> =
0024 <input type="text" id="Result2">
0025 <input type="button" value="button2" onClick = "javascript: TestSub()">
0026 </BODY>
0027 </HTML>
该例程,也是针对Sample组件的测试。前文中提及,Sample组件实现了IDispatchImpl接口,这就意味着,该组件支持自动化调用。在html网页中,通过object标签来嵌入该组件。例如:<object id="math" width="0" height="0" classid="CLSID:6BD26856-7090-4F8F-A8F8-E6059CEB73AC"></object>然后就可以通过javascript脚本来调用IMath接口提供的加减法功能了。
后续实验中,我们将通过挂钩IMath接口中的函数,进而从SampleTest.html中(IE浏览器)获取用户输入的原始值,并将其“偷偷修改”。
实验08:使用对象代理法来Hook IMath的接口函数(程序名称:SampleProxy.dll)
// SampleProxy.dll
0001 class IMathProxy : public IMath
0002 {
0003 public:
0004 IMathProxy(IMath* lpIMath, HANDLE EventRelease);
0005
0006 /*** IUnknown methods ***/
0007 ...
0008
0009 /*** IDispatch methods ***/
0010 ...
0011
0012 /*** IMath methods ***/
0013 virtual /* [helpstring][id] */ HRESULT STDMETHODCALLTYPE Add(
0014 /* [in] */ int a,
0015 /* [in] */ int b,
0016 /* [out] */ int __RPC_FAR *c);
0017
0018 virtual /* [helpstring][id] */ HRESULT STDMETHODCALLTYPE Sub(
0019 /* [in] */ int a,
0020 /* [in] */ int b,
0021 /* [out] */ int __RPC_FAR *c);
0022
0023 //private:
0024
0025 HANDLE m_hValid; // 用于标识对象是否有效
0026 IMath* m_lpIMath; // 原始接口指针, 用于最终调调原始接口
0027
0028 };
0029
0030 /*** IMath methods ***/
0031 HRESULT STDMETHODCALLTYPE IMathProxy::Add(
0032 /* [in] */ int a,
0033 /* [in] */ int b,
0034 /* [out] */ int __RPC_FAR *c)
0035 {
0036 char buffer[200] = { 0 };
0037 sprintf(buffer, "Original value = %d, %d\r\n", a, b);
0038
0039 a += 10;
0040 b += 10;
0041
0042 char buffer2[200] = { 0 };
0043 sprintf(buffer2, "New value(+=10) = %d, %d", a, b);
0044
0045 char buffer3[400] = { 0 };
0046 strcpy(buffer3, buffer);
0047 strcat(buffer3, buffer2);
0048 MessageBox(NULL, buffer3, "IMathProxy::Add", MB_OK);
0049
0050 return m_lpIMath->Add( a, b, c);
0051 }
0052
0053 HRESULT STDMETHODCALLTYPE IMathProxy::Sub(
0054 /* [in] */ int a,
0055 /* [in] */ int b,
0056 /* [out] */ int __RPC_FAR *c)
0057 {
0058 char buffer[200] = { 0 };
0059 sprintf(buffer, "Original value = %d, %d\r\n", a, b);
0060
0061 a += 20;
0062 b += 10;
0063
0064 char buffer2[200] = { 0 };
0065 sprintf(buffer2, "New value(+=20, 10) = %d, %d", a, b);
0066
0067 char buffer3[400] = { 0 };
0068 strcpy(buffer3, buffer);
0069 strcat(buffer3, buffer2);
0070 MessageBox(NULL, buffer3, "IMathProxy::Sub", MB_OK);
0071
0072 return m_lpIMath->Sub( a, b, c);
0073 }
0074
0075 typedef int (WINAPI* PFN_CoCreateInstance)
0076 (REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID * ppv);
0077
0078 PFN_CoCreateInstance CoCreateInstance_OLD = (PFN_CoCreateInstance)
0079 GetProcAddress(GetModuleHandle(_T("ole32.dll")), "CoCreateInstance");
0080
0081 HRESULT __stdcall CoCreateInstance_NEW(REFCLSID rclsid,
0082 LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID * ppv)
0083 {
0084 HRESULT hr = CoCreateInstance_OLD(rclsid, pUnkOuter, dwClsContext, riid, ppv);
0085
0086 if (FAILED(hr))
0087 return hr;
0088
0089 if (CLSID_Math == rclsid && IID_IMath == riid)
0090 {
0091 HANDLE event = CreateEvent(NULL, TRUE, FALSE, NULL);
0092
0093 // Create proxy object
0094 IMathProxy* NewObj = new IMathProxy((IMath*)(*ppv), event);
0095
0096 // save
0097 ...
0098
0099 // replace object
0100 *ppv = NewObj;
0101 }
0102
0103 return hr;
0104 }
0105
0106 VOID CoCreateInstance_Hook()
0107 {
0108 if (Mhook_SetHook((PVOID*)&CoCreateInstance_OLD, CoCreateInstance_NEW))
0109 {
0110 // successfully
0111 }
0112 }
0113
0114 VOID CoCreateInstance_UnHook()
0115 {
0116 Mhook_Unhook((PVOID*)&CoCreateInstance_OLD);
0117 }
0118
0119 BOOL APIENTRY DllMain(HANDLE hModule,
0120 DWORD ul_reason_for_call,
0121 LPVOID lpReserved)
0122 {
0123 g_hModule = hModule;
0124
0125 switch (ul_reason_for_call)
0126 {
0127 case DLL_PROCESS_ATTACH:
0128 if (!CheckModuleName()) return FALSE;
0129 CoCreateInstance_Hook();
0130 break;
0131 case DLL_PROCESS_DETACH:
0132 if (!CheckModuleName()) return FALSE;
0133 CoCreateInstance_UnHook();
0134 IMathProxy_UnHook();
0135 break;
0136 }
0137
0138 return TRUE;
0139 }
该例程挂钩了一个真正的COM接口:IMath,使用的方法就是前文所提及的对象代理法。所以我们自己重定定义了一个C++类,名称为class IMathProxy,既然class IMathProxy是原始接口IMath的代理,那自然class IMathProxy就需要实现IMath中所有的函数。IMath是我们自己实现的,其内部也就是两个函数而已。但需要注意的是,IMath所有父类中的函数,都要在class IMathProxy中实现,这才算是一完整的IMath的代理对象。本文仅给出了最关键的代码部分,完整的代码请参见本文的附档文件中的源码。
IMathProxy实现好了,剩下的问题就是,如何将IMathProxy对象的指针与真正的IMath的指针进行交换。在关键技术研究那些小实验中,对像的指针都是我们手动new出来的。IMathProxy是我们自己实现的,当然可以使用new操作符进行创建,但是IMath呢?IMath接口指针应该是客户程序创建出来的啊。一般情况下,COM接口指针都是由CoCreateInstance这个API来创建的,该函数原型如下:
STDAPI CoCreateInstance(
REFCLSID rclsid,
LPUNKNOWN pUnkOuter,
DWORD dwClsContext,
REFIID riid,
LPVOID * ppv
);
其中,最后一个参数即是返回给客户程序使用的接口指针。既然这个函数是由客户程序进行调用的,要想得到从这个函数中输出的返回值,所以我们必须还要通过Hook API的办法,将这个函数进行挂钩。并且,我们不知道客户程序在什么时候调用这个函数创建接口指针,所以我们挂钩CoCreateInstance的程序必须先于客户程序启动。这样客户程序在任何时候调用CoCreateInstance,我们都可以挂钩到它。如果客户先启动,并在启动后的第一时间就调用了CoCreateInstance来创建接口指针,而我们的挂钩操作在后,这就没有任何意义可言了。本例程中挂钩的操作还是使用的mhook库,前文中已经讲过如何使用该库,请参见本例程给出的关键代码。
在已经挂钩的IMath::Add函数中,我们仅仅是简单的将两个加数的值加上了10,然后将函数控制权就返回给了原始函数。如果未挂钩前,例如客户输入的两个加数是1,2,程序输出3,挂钩后1变成了11,2变成了12,所以客户得到的结果就会是23。
在已经挂钩的IMath::Sub函数中,我们仅仅是简单的将两个减数的值分别加上20,10,然后将函数控制权就返回给了原始函数。如果未挂钩前,例如客户输入的两个减数是1,2,程序输出-1,挂钩后1变成了21,2变成了12,所以客户得到的结果就会是9。
客户程序就是实验07-1写的程序SampleTest.exe,以及实验07-2写的网页SampleTest.html,要注意请使用IE浏览器打开这个网页。其实只要是调用了IMath接口的任何客户程序都可以用该例程挂钩,但是在该例程通过宿主的进程名称判断,是不是要挂钩的目标程序。所以只能用前面两个实验中的代码,且SampleTest.html必须使用IE浏览打开。
实验09:使用虚表补丁法来Hook IMath的接口函数(程序名称:SampleVtable.dll)
// SampleVtable.dll
0001 typedef HRESULT (__stdcall* PFN_ADD)(IUnknown* This, int a, int b, int* c);
0002 typedef HRESULT (__stdcall* PFN_SUB)(IUnknown* This, int a, int b, int* c);
0003
0004 #if defined WIN64
0005 typedef DWORD64* MEMADDR_PTR;
0006 typedef DWORD64 MEMADDR_VAL;
0007 #else
0008 typedef LPDWORD MEMADDR_PTR;
0009 typedef DWORD MEMADDR_VAL;
0010 #endif
0011
0012 class IMathVtable
0013 {
0014 public:
0015
0016 static HRESULT HookMethod();
0017 static HRESULT UnHookMethod();
0018
0019 static HRESULT __stdcall Add(IUnknown* This, int a, int b, int* c);
0020 static HRESULT __stdcall Sub(IUnknown* This, int a, int b, int* c);
0021
0022 static MEMADDR_PTR m_Add_OLD;
0023 static MEMADDR_PTR m_Sub_OLD;
0024
0025 static MEMADDR_PTR m_lpVtable;
0026 static HMODULE m_hModule;
0027 };
0028
0029 MEMADDR_PTR IMathVtable::m_Add_OLD = NULL;
0030 MEMADDR_PTR IMathVtable::m_Sub_OLD = NULL;
0031 MEMADDR_PTR IMathVtable::m_lpVtable = NULL;
0032 HMODULE IMathVtable::m_hModule = NULL;
0033
0034 /*
0035 ------------------------------------
0036 // IUnknown
0037 QueryInterface // 0
0038 AddRef // 1
0039 Release // 2
0040 // IDispatch
0041 GetTypeInfoCount // 3
0042 GetTypeInfo // 4
0043 GetIDsOfNames // 5
0044 Invoke // 6
0045 // IMath
0046 Add // 7
0047 Sub // 8
0048 ------------------------------------
0049 */
0050
0051 HRESULT IMathVtable::HookMethod()
0052 {
0053 m_hModule = ::LoadLibrary("Sample.dll");
0054
0055 // 改写虚表前九项(DWORD_PTR)内存的属性
0056 DWORD dwVtableMaxLen = 9;
0057
0058 // 确保COM环境补初始化
0059 ::CoInitialize(NULL);
0060
0061 // 自己创建一个接口
0062
0063 IMath* lpIMath = NULL;
0064 HRESULT hr = CoCreateInstance(CLSID_Math, NULL,
0065 CLSCTX_INPROC_SERVER, IID_IMath, (LPVOID*)&lpIMath);
0066
0067 if (FAILED(hr))
0068 return hr;
0069
0070 // 有了接口指针就可以确定[虚函数表(Virtual table)]的地址了
0071 // 这个虚表是for class的, 而不是for object的,
0072 // 也就是说所有的IMath实例接口的虚表地址都是一样的
0073
0074 MEMADDR_VAL VtableAddr = *((MEMADDR_PTR)lpIMath);
0075 m_lpVtable = (MEMADDR_PTR)VtableAddr;
0076
0077 // 将内存属性改为可读写
0078
0079 DWORD dwOld = 0;
0080 if(!::VirtualProtect(m_lpVtable, sizeof(MEMADDR_VAL) * dwVtableMaxLen,
0081 PAGE_EXECUTE_READWRITE, &dwOld))
0082 return E_FAIL;
0083
0084 // Hook Add method, Add函数位于虚表的第七项
0085
0086 m_Add_OLD = (MEMADDR_PTR)m_lpVtable[7];
0087 m_lpVtable[7] = (MEMADDR_VAL)(MEMADDR_PTR)IMathVtable::Add;
0088
0089 // Hook Sub method, Sub函数位于虚表的第八项
0090
0091 m_Sub_OLD = (MEMADDR_PTR)m_lpVtable[8];
0092 m_lpVtable[8] = (MEMADDR_VAL)(MEMADDR_PTR)IMathVtable::Sub;
0093
0094 // 内存属性再改回来
0095
0096 if(!::VirtualProtect(m_lpVtable, sizeof(MEMADDR_VAL) * dwVtableMaxLen,
0097 dwOld, &dwOld))
0098 return E_FAIL;
0099
0100 lpIMath->Release();
0101 return S_OK;
0102 }
0103
0104 HRESULT IMathVtable::UnHookMethod()
0105 {
0106 if (NULL == m_lpVtable)
0107 return E_FAIL;
0108
0109 DWORD dwVtableMaxLen = 9;
0110
0111 DWORD dwOld = 0;
0112 if(!::VirtualProtect(m_lpVtable, sizeof(LONG_PTR) * dwVtableMaxLen,
0113 PAGE_EXECUTE_READWRITE, &dwOld))
0114 return E_FAIL;
0115
0116 m_lpVtable[7] = (MEMADDR_VAL)(MEMADDR_PTR)m_Add_OLD;
0117 m_lpVtable[8] = (MEMADDR_VAL)(MEMADDR_PTR)m_Sub_OLD;
0118
0119 if(!::VirtualProtect(m_lpVtable, sizeof(MEMADDR_VAL) * dwVtableMaxLen,
0120 dwOld, &dwOld))
0121 return E_FAIL;
0122
0123 if (m_hModule)
0124 ::FreeLibrary(m_hModule);
0125
0126 return S_OK;
0127 }
0128
0129 /////////////////////////////////////////////////////////////////////////////////
0130 // New functions
0131
0132 HRESULT __stdcall IMathVtable::Add(IUnknown* This, int a, int b, int* c)
0133 {
0134 char buffer[200] = { 0 };
0135 sprintf(buffer, "Original value = %d, %d\r\n", a, b);
0136
0137 a += 10;
0138 b += 10;
0139
0140 char buffer2[200] = { 0 };
0141 sprintf(buffer2, "New value(+=10, +=10) = %d, %d", a, b);
0142
0143 char buffer3[400] = { 0 };
0144 strcpy(buffer3, buffer);
0145 strcat(buffer3, buffer2);
0146 MessageBox(NULL, buffer3, "Add", MB_OK);
0147
0148 // 调用原始的函数
0149 PFN_ADD pFN_Add = (PFN_ADD)m_Add_OLD;
0150 return pFN_Add(This, a, b, c);
0151 }
0152
0153 HRESULT __stdcall IMathVtable::Sub(IUnknown* This, int a, int b, int* c)
0154 {
0155 char buffer[200] = { 0 };
0156 sprintf(buffer, "Original value = %d, %d\r\n", a, b);
0157
0158 a += 20;
0159 b += 10;
0160
0161 char buffer2[200] = { 0 };
0162 sprintf(buffer2, "New value(+=20, +=10) = %d, %d", a, b);
0163
0164 char buffer3[400] = { 0 };
0165 strcpy(buffer3, buffer);
0166 strcat(buffer3, buffer2);
0167 MessageBox(NULL, buffer3, "Sub", MB_OK);
0168
0169 // 调用原始的函数
0170 PFN_SUB pFN_Sub = (PFN_SUB)m_Sub_OLD;
0171 return pFN_Sub(This, a, b, c);
0172 }
0173
0174 BOOL APIENTRY DllMain(HANDLE hModule,
0175 DWORD ul_reason_for_call,
0176 LPVOID lpReserved)
0177 {
0178 g_hModule = hModule;
0179
0180 switch (ul_reason_for_call)
0181 {
0182 case DLL_PROCESS_ATTACH:
0183 if (!CheckModuleName()) return FALSE;
0184 IMathVtable::HookMethod();
0185 break;
0186 case DLL_PROCESS_DETACH:
0187 if (!CheckModuleName()) return FALSE;
0188 IMathVtable::UnHookMethod();
0189 break;
0190 }
0191
0192 return TRUE;
0193 }
该例程和前面的实验都是挂钩IMath接口,前面的实验使用的是对象代理法,而本例程使用的是虚表补丁法。在前面的实验中讲到,要得到客户创建的接口指针,必须挂钩CoCreateInstance函数,而且挂钩程序必须要先于客户程序启动。在实际的应用中,这是很难做到的,我们很难保证我们的程序一定会先于客户程序启动。而对于实验08中的例程来说,从客户程序那里获取的按口指针是要和我们自己创建的对象指针进行交换。而本例程仅仅是想获得客户接口指针所指向的接口函数地址表(即虚函数表)。而接口指针,对本例程来说只是一个寻找接口函数表的挢梁。
在C++中,同一个C++类的所有对象,分别拥有各自的对象指针,即this指针。但是对于同一个C++类,所有对象的this指针却指向了相同的虚函数表指针,都指向了相同的内存区。也就是说,每个对象各有各的对像指针,但是虚函数表,以及虚函数表中的函数地址却是同一个C++类所有对象所共有的。事实上也没有必要有多个复本。即使是普通的C++类,无论有多少个对象,其成员函数也是这些对象所共有的,仅是因为每个对象的this指针不同,所以数据成员会有多个复本,为每个对象所独有。
所以,我们可以在要挂钩的目标进程空间内,自己创建一个要挂钩的接口IMath,并通过这个指针来寻找接口函数表。这就是说,只要我们自己能创建出要挂钩的接口指针,就不用从客户程序那里得到这个指针了。所以,我们的挂钩程序和客户程序所就无所谓运行顺序了。这也符合用户使用程序的随意性。参见实验代码中的IMathVtable::HookMethod函数。
在此例程中,我们也是简单的将IMath::Add,IMath::Sub中的数据进行修改,然后就将函数的控制权返回给原始接口函数。客户程序依然用SampleTest.exe,IE浏览器(SampleTest.html),程序的运行结果同上一实验一样。
实验10:对实验程序SampleProxy.dll,SampleVtable.dll的综合测试(程序名称:SampleHook.exe)
// SampleTest.exe
0001 class CSampleTestDlg : public CDialog
0002 {
0003 ...
0004 private:
0005
0006 IMath* m_lpIMath;
0007 };
0008
0009 BOOL CSampleTestDlg::OnInitDialog()
0010 {
0011 ...
0012
0013 AfxMessageBox("Start ?");
0014
0015 ::CoInitialize(NULL);
0016
0017 HRESULT hr;
0018
0019 // 方法一
0020
0021 /*
0022 CLSID clsid;
0023 CLSIDFromProgID(OLESTR("Sample.Math"),&clsid);
0024 hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, __uuidof(IMath), (LPVOID*)&m_lpIMath);
0025 */
0026
0027 // 方法二
0028 hr = CoCreateInstance(CLSID_Math, NULL, CLSCTX_INPROC_SERVER, IID_IMath, (LPVOID*)&m_lpIMath);
0029
0030 if (FAILED(hr))
0031 {
0032 CString str;
0033 str.Format("Error: 0x%08X", hr);
0034 AfxMessageBox(str);
0035 }
0036
0037 return TRUE; // return TRUE unless you set the focus to a control
0038 }
0039
0040 void CSampleTestDlg::OnButton1()
0041 {
0042 if (NULL == m_lpIMath)
0043 {
0044 AfxMessageBox("NULL");
0045 return;
0046 }
0047
0048 int a = GetDlgItemInt(IDC_EDIT1);
0049 int b = GetDlgItemInt(IDC_EDIT2);
0050
0051 int c;
0052 m_lpIMath->Add(a, b, &c);
0053
0054 CString str;
0055 str.Format("%d", c);
0056 SetDlgItemInt(IDC_EDIT3, c);
0057 }
0058
0059 void CSampleTestDlg::OnButton2()
0060 {
0061 IMath* lpIMath = NULL;
0062 HRESULT hr = CoCreateInstance(CLSID_Math, NULL, CLSCTX_INPROC_SERVER, IID_IMath, (LPVOID*)&lpIMath);
0063
0064 if (FAILED(hr))
0065 {
0066 CString str;
0067 str.Format("Error: 0x%08X", hr);
0068 AfxMessageBox(str);
0069 return;
0070 }
0071
0072 int a = GetDlgItemInt(IDC_EDIT4);
0073 int b = GetDlgItemInt(IDC_EDIT5);
0074
0075 int c;
0076 lpIMath->Sub(a, b, &c);
0077
0078 CString str;
0079 str.Format("%d", c);
0080 SetDlgItemInt(IDC_EDIT6, c);
0081
0082 lpIMath->Release();
0083 }
测试一:
涉骤1:先启动SampleHook.exe,然后点击Button1。此时,程序内部通过对象代理法对IMath::Add,IMath::Sub进行挂钩。
步骤2: 启动SampleTest.exe,输入数字进行加减法。这时会弹出一个输入值被修改的提示,点击确定后查看计算结果。即是被修改过的值的计算结果。
步骤3:关闭SampleHook.exe程序,再次使用SampleTest.exe和SampleTest.html进和数值计算,此时计算结果正是用户输入的原始值的计算结果。
测试结果,如图3-1所示:
图3-1 综合测试一
测试二:
步骤1: 启动SampleTest.exe,或者启动IE浏览器,并打开SampleTest.html,输入数字进行加减法,计算结果正确。
步骤2:启协SampleHook.exe,点击Button3,此时,程序内部通过虚表补丁法对IMath::Add,IMath::Sub进行挂钩。
步骤3:再次使用SampleTest.exe和SampleTest.html进和数值计算,这时会弹出一个输入值被修改的提示,点击确定后查看计算结果。即是被修改过的值的计算结果。
步骤4:关闭SampleHook.exe程序,再次使用SampleTest.exe和SampleTest.html进和数值计算,此时计算结果正是用户输入的原始值的计算结果。
测试结果,如图3-2所示:
图3-2 综合测试二
该实验,分别对SampleProxy.dll程序中的对象代理法及SampleVtable.dll程序中的虚表补丁法进行了测试。测试结果表明,当SampleTest.exe和SampleTest.html(IE)调用IMath::Add,IMath::Sub这两个函数时,均已被挂钩,因为挂钩程序修改了参数中的值,使得计算结果也被改变了。这也订明了,在前面所得出的实验结论都是正确的。
本章小结
本章中所做的实验,经过编译,生成如下程序。
Sample.dll - IMath接口
SampleTest.exe -- IMath测试程序。
SampleTest.html -- IMath测试程序,必须使用IE浏览器打开。
SampleProxy.dll -- 运用对象代理法挂接IMath接口中的Add,Sub函数
SampleVtable.dll -- 运用虚表补丁法挂接IMath接口中的Add,Sub函数
SampleHook.exe -- 用于调用SampleProxy.dll,SampleVtable.dll中提供的函数完成进程注入及挂钩。
通过挂钩一个真正的COM组件(Sample.dll),充份印证了前一章理论的正确性。并在实际的实践过程中,对这些理论有了更清昕的进一步认识和理解。但是,这仅仅是初始阶段,要想真正将挂钩COM接口的技术应用在各种场合,还会存在各种各样的困难。同时,出于对系统以及目标进程和稳定性,和效率的考虑,编程过程中,还有储多细节问题还需仔细处理。特别是目标COM组件的线程模程及同步问题。这些问题,将在实际的应用中遇到,并且需要开发者依具自身的技术功底,以及对众多问题的综合认识,才能够得以完成一个稳定且安全的挂钩程序。这些问题,已然超出了本文范围,还需开发者有的放矢针对具体问题做出最合适的处理。
第4章 实例:面向IE的网页弹窗拦截器
实现背景
毫无疑问,网络在今天已经渗透到每个人的工作,生活的方方面面。在使用网络的过程中,最重要的就是通过浏览器浏览互联网上万千变化的我们感兴趣的信息。在浏览网页的过程中,比较让人头疼的第一号问题就是病毒,木马。但随着网民的安全意识逐步提高,加之国内安全厂商已免费开放杀毒软件,所以,对于当前的环境,较前年要好得很多。所以在生活中,其次让人头疼的问题,就是许许多多不道德的网站站长,为了追求利益,在网站上加了许许多多的弹窗广告,甚至是循环弹出,直到耗尽系统资源为止。有的甚至是色性的页面。签于此,有必要开发一款针对网页的弹窗拦截器。这个实例仅是针对IE浏览器。
网页弹窗原理1:IHTMLWindow2Vtable::open
知其然,才知其所以然。要想开发一个网页拦截器,必须得先知道,网页是如何弹出一个窗口的。对于IE浏览器来说,最为官方的弹出方法是调用IE浏览器内置的window对象的open方法。典型的调用方法如下:
<input type = "button" value = "button" onClick = "window.open(‘http://www.zhangluduo.com/‘)">
众所周知,IE浏览器是基于COM技术构建的。在MSDN中,可以找到window对象的原型。window对象实际对应的就是IHTMLWindow接口。而window.open实际上就是IHTMLWindow2::open,该接口函数原型如下:
HRESULT open(
BSTR url,
BSTR name,
BSTR features,
VARIANT_BOOL replace,
IHTMLWindow2 **pomWindowResult
);
在该接口函数中,最重要的参数就是url,也就是IE弹出窗口要加载的网页的网址。所以,只要我们将IHTMLWindow2::open进行挂钩即可防止window.open这类的弹窗。
网页弹窗例程1:例程 Open_IHTMLWindow2.html
0001 <HTML>
0002 <HEAD>
0003 <TITLE>test for hook COM interface and method</TITLE>
0004 </HEAD>
0005 <BODY>
0006 <input type = "button" value = "button" onClick = "window.open(‘http://www.zhangluduo.com/‘)">
0007 </BODY>
0008 </HTML>
网页弹窗原理2:IWMPCore::launchURL
还有一类弹出窗口使用的技术,也是微软官方的,但是却不是IE的正规弹窗方法。该方法是利用Windows系统在本地安装的Windows Media Player播放器的功能来实现的。Windows Media Player随着Windows操作系统一起发行,几乎是每一台安装了Windows机器都会安装Windows Media Player,并且Windows Media Player也是基于COM技术构建的,而且它支持自动化。我们常常看的在线电影,有一部分就是在网页是嵌了一个Windows Media Player来播放的。调用Windows
Media Player提供的弹窗功能来进行页面弹窗,该方法典型的调用如下:
<object id="wmp" width="0" height="0" classid="CLSID:6BF52A52-394A-11D3-B153-00C04F79FAA6"></object>
<input type = "button" value = "button" onClick = "wmp.launchURL(‘http://www.zhangluduo.com‘)">
通过注册表可以查询到这个CLSID对应的组件,在注册表里也记录着该组件的特理路径,即C:\WINDOWS\system32\wmp.dll。Visual Studio 6.0企业版有一个实用工具OLE View,它可以用来查看一个COM组件的所有接口,及接口函数,但前题是,COM组件必须支持自动化。我们就可以通过OLE View显示wmp.dll 类型库。如图4-1所示:
图4-1 在OLE View中查看COM接口函数
图4-2 在OLE View中查看COM接口函数
在OLE View右侧的窗口(如图4-2所示)中是全部的接口声明,及对象对应的GUID。通过搜索6BF52A52-394A-11D3-B153-00C04F79FAA6以及launchURL函数,我们很快就找到了我们在挂钩时将要用到的重要信息,如下:
[
uuid(6BF52A52-394A-11D3-B153-00C04F79FAA6),
helpstring("Windows Media Player ActiveX Control")
]
coclass WindowsMediaPlayer
[
odl,
uuid(D84CCA99-CCE2-11D2-9ECC-0000F8085981),
helpstring("IWMPCore: Public interface."),
dual,
oleautomation
]
interface IWMPCore : IDispatch
这样我们就确定了,在网页中调用的launchURL,其实就是IWMPCore::launchURL。我们在挂钩时,需要创建真正的IWMPCore接口,需要用到IWMPCore接口所在头文件的一些声明,所以编程前需要先安装Windows Media Player SDK。
网页弹窗例程2:例程 Open_IWMPCore.html
0001 <HTML>
0002 <HEAD>
0003 <TITLE>test for hook COM interface and method</TITLE>
0004 </HEAD>
0005 <BODY>
0006 <script type="text/javascript">
0007 function OpenUrl()
0008 {
0009 wmp.launchURL(‘http://www.zhangluduo.com‘);
0010 }
0011 function OpenInit()
0012 {
0013 document.body.innerHTML += ‘<object id="wmp" width="0" height="0" classid="CLSID:6BF52A52-394A-11D3-B153-00C04F79FAA6"></object>‘;
0014 }
0015 eval("window.attachEvent(‘onload‘,OpenInit);");
0016 </script>
0017 <input type = "button" value = "button" onClick = "OpenUrl()">
0018 </BODY>
0019 </HTML>
使用虚表补丁法Hook IHTMLWindow2Vtable::open,IWMPCore::launchURL
0001 //IHTMLWindow2Vtable.h
0002
0003 #include <atlbase.h>
0004 #include <Mshtml.h>
0005 #include <Exdisp.h>
0006
0007 typedef HRESULT (STDMETHODCALLTYPE* PFN_CONFIRM)
0008 (IHTMLWindow2* This, BSTR message, VARIANT_BOOL __RPC_FAR *confirmed);
0009
0010 typedef HRESULT (STDMETHODCALLTYPE* PFN_OPEN)
0011 (IHTMLWindow2* This,
0012 /* [in][defaultvalue] */ BSTR url,
0013 /* [in][defaultvalue] */ BSTR name,
0014 /* [in][defaultvalue] */ BSTR features,
0015 /* [in][defaultvalue] */ VARIANT_BOOL replace,
0016 /* [out][retval] */ IHTMLWindow2 __RPC_FAR *__RPC_FAR *pomWindowResult);
0017
0018 #if defined WIN64
0019 typedef DWORD64* MEMADDR_PTR;
0020 typedef DWORD64 MEMADDR_VAL;
0021 #else
0022 typedef DWORD* MEMADDR_PTR;
0023 typedef DWORD MEMADDR_VAL;
0024 #endif
0025
0026 class IHTMLWindow2Vtable
0027 {
0028 public:
0029
0030 static HRESULT HookMethod(MEMADDR_PTR lpVtable);
0031 static HRESULT UnHookMethod();
0032
0033 static /* [id] */ HRESULT STDMETHODCALLTYPE open(IHTMLWindow2* This,
0034 /* [in][defaultvalue] */ BSTR url,
0035 /* [in][defaultvalue] */ BSTR name,
0036 /* [in][defaultvalue] */ BSTR features,
0037 /* [in][defaultvalue] */ VARIANT_BOOL replace,
0038 /* [out][retval] */ IHTMLWindow2 __RPC_FAR *__RPC_FAR *pomWindowResult);
0039
0040 static MEMADDR_PTR m_open_OLD;
0041
0042 static MEMADDR_PTR m_lpVtable;
0043 static HMODULE m_hModule;
0044 };
0045
0046 //IHTMLWindow2Vtable.cpp
0047
0048 MEMADDR_PTR IHTMLWindow2Vtable::m_open_OLD = NULL;
0049 MEMADDR_PTR IHTMLWindow2Vtable::m_lpVtable = NULL;
0050 HMODULE IHTMLWindow2Vtable::m_hModule = NULL;
0051
0052 HRESULT IHTMLWindow2Vtable::HookMethod(MEMADDR_PTR lpVtable)
0053 {
0054 m_hModule = ::LoadLibrary("mshtml.dll");
0055
0056 m_lpVtable = lpVtable;
0057 MEMADDR_VAL dwVtableMaxLen = 30;
0058
0059 DWORD dwOld = 0;
0060 if(!::VirtualProtect(m_lpVtable, sizeof(MEMADDR_VAL) * dwVtableMaxLen,
0061 PAGE_READWRITE, &dwOld))
0062 return E_FAIL;
0063
0064 m_open_OLD = (MEMADDR_PTR)m_lpVtable[29];
0065
0066 m_lpVtable[29] = (MEMADDR_VAL)(MEMADDR_PTR)IHTMLWindow2Vtable::open;
0067
0068 if(!::VirtualProtect(m_lpVtable, sizeof(MEMADDR_VAL) * dwVtableMaxLen,
0069 dwOld, &dwOld))
0070 return E_FAIL;
0071
0072 return S_OK;
0073 }
0074
0075 HRESULT IHTMLWindow2Vtable::UnHookMethod()
0076 {
0077 MEMADDR_VAL dwVtableMaxLen = 30;
0078
0079 if (0 != IsBadReadPtr(m_lpVtable, sizeof(MEMADDR_VAL) * dwVtableMaxLen))
0080 return E_FAIL;
0081
0082 DWORD dwOld = 0;
0083 if(!::VirtualProtect(m_lpVtable, sizeof(MEMADDR_VAL) * dwVtableMaxLen,
0084 PAGE_READWRITE, &dwOld))
0085 return E_FAIL;
0086
0087 m_lpVtable[29] = (MEMADDR_VAL)(MEMADDR_PTR)m_open_OLD;
0088
0089 if(!::VirtualProtect(m_lpVtable, sizeof(MEMADDR_VAL) * dwVtableMaxLen,
0090 dwOld, &dwOld))
0091 return E_FAIL;
0092
0093 if (m_hModule)
0094 ::FreeLibrary(m_hModule);
0095
0096 return S_OK;
0097 }
0098
0099 /* [id] */ HRESULT STDMETHODCALLTYPE IHTMLWindow2Vtable::open(IHTMLWindow2* This,
0100 /* [in][defaultvalue] */ BSTR url,
0101 /* [in][defaultvalue] */ BSTR name,
0102 /* [in][defaultvalue] */ BSTR features,
0103 /* [in][defaultvalue] */ VARIANT_BOOL replace,
0104 /* [out][retval] */ IHTMLWindow2 __RPC_FAR *__RPC_FAR *pomWindowResult)
0105 {
0106 HWND hwnd = GetRecvUrlWnd();
0107 if (hwnd)
0108 {
0109 int len = WideCharToMultiByte(CP_ACP, 0, url, -1, NULL, NULL, NULL, NULL);
0110 char *buffer = new char[len];
0111 memset(buffer, 0, len);
0112 WideCharToMultiByte(CP_ACP, 0, url, -1, buffer, len, NULL, NULL);
0113 SendUrl(buffer);
0114 delete buffer;
0115 return S_OK;
0116 }
0117 else
0118 {
0119 PFN_OPEN pFn = (PFN_OPEN)m_open_OLD;
0120 return pFn(This, url, name, features, replace, pomWindowResult);
0121 }
0122 }
0123
0124 // IWMPCoreVtable.h
0125
0126 #include <atlbase.h>
0127 #include <wmp.h>
0128 #include <Exdisp.h>
0129
0130 typedef HRESULT (STDMETHODCALLTYPE* PFN_LAUNCHURL) (void* This, BSTR url);
0131
0132 #if defined WIN64
0133 typedef DWORD64* MEMADDR_PTR;
0134 typedef DWORD64 MEMADDR_VAL;
0135 #else
0136 typedef DWORD* MEMADDR_PTR;
0137 typedef DWORD MEMADDR_VAL;
0138 #endif
0139
0140 class IWMPCoreVtable
0141 {
0142 public:
0143
0144 static HRESULT HookMethod(/*LPDWORD lpVtable*/);
0145 static HRESULT UnHookMethod();
0146
0147 static HRESULT STDMETHODCALLTYPE launchURL(void* This, BSTR url);
0148
0149 static MEMADDR_PTR m_launchURL_OLD;
0150
0151 static MEMADDR_PTR m_lpVtable;
0152 static HMODULE m_hModule;
0153 };
0154
0155 // IWMPCoreVtable.cpp
0156
0157 MEMADDR_PTR IWMPCoreVtable::m_launchURL_OLD = NULL;
0158 MEMADDR_PTR IWMPCoreVtable::m_lpVtable = NULL;
0159 HMODULE IWMPCoreVtable::m_hModule = NULL;
0160
0161 HRESULT IWMPCoreVtable::HookMethod(/*LPDWORD lpVtable*/)
0162 {
0163 m_hModule = ::LoadLibrary("wmp.dll");
0164
0165 HRESULT hr ;
0166
0167 CLSID clsWMP; // coclass WindowsMediaPlayer
0168 hr = CLSIDFromString(L"{6bf52a52-394a-11d3-b153-00c04f79faa6}", &clsWMP);
0169 if (FAILED(hr)) return hr;
0170
0171 IID idWMPCore; // interface IWMPCore
0172 hr = CLSIDFromString(L"{d84cca99-cce2-11d2-9ecc-0000f8085981}", &idWMPCore);
0173 if (FAILED(hr)) return hr;
0174
0175 IWMPCore* pIWMPCore = NULL;
0176 hr = ::CoCreateInstance(clsWMP, NULL, CLSCTX_INPROC_SERVER, /*IID_IUnknown*/idWMPCore, (void**)&pIWMPCore);
0177 if (FAILED(hr) || pIWMPCore == NULL) return hr;
0178
0179 MEMADDR_VAL VtableAddr = *((MEMADDR_PTR)(LPVOID)pIWMPCore);
0180 m_lpVtable = (MEMADDR_PTR)VtableAddr;
0181
0182 DWORD dwVtableMaxLen = 20;
0183 DWORD dwOld = 0;
0184 if(!::VirtualProtect(m_lpVtable, sizeof(MEMADDR_VAL) * dwVtableMaxLen,PAGE_READWRITE, &dwOld))
0185 return E_FAIL;
0186
0187 m_launchURL_OLD = (MEMADDR_PTR)m_lpVtable[19];
0188
0189 m_lpVtable[19] = (MEMADDR_VAL)(MEMADDR_PTR)IWMPCoreVtable::launchURL;
0190
0191 if(!::VirtualProtect(m_lpVtable, sizeof(MEMADDR_VAL) * dwVtableMaxLen,dwOld, &dwOld))
0192 return E_FAIL;
0193
0194 pIWMPCore->Release();
0195 pIWMPCore = NULL;
0196 return S_OK;
0197 }
0198
0199 HRESULT IWMPCoreVtable::UnHookMethod()
0200 {
0201 if (!m_lpVtable)
0202 return E_FAIL;
0203
0204 DWORD dwVtableMaxLen = 20;
0205
0206 if (0 != IsBadReadPtr(m_lpVtable, sizeof(MEMADDR_VAL) * dwVtableMaxLen))
0207 return E_FAIL;
0208
0209 DWORD dwOld = 0;
0210 if(!::VirtualProtect(m_lpVtable, sizeof(MEMADDR_VAL) * dwVtableMaxLen, PAGE_READWRITE, &dwOld))
0211 return E_FAIL;
0212
0213 m_lpVtable[19] = (MEMADDR_VAL)(MEMADDR_PTR)m_launchURL_OLD;
0214
0215 if(!::VirtualProtect(m_lpVtable, sizeof(MEMADDR_VAL) * dwVtableMaxLen, dwOld, &dwOld))
0216 return E_FAIL;
0217
0218 //if (m_hModule) // do not free the module from the host process
0219 // ::FreeLibrary(m_hModule);
0220
0221 return S_OK;
0222 }
0223
0224 HRESULT STDMETHODCALLTYPE IWMPCoreVtable::launchURL(void* This, BSTR url)
0225 {
0226 HWND hwnd = GetRecvUrlWnd();
0227 if (hwnd)
0228 {
0229 int len = WideCharToMultiByte(CP_ACP, 0, url, -1, NULL, NULL, NULL, NULL);
0230 char *buffer = new char[len];
0231 memset(buffer, 0, len);
0232 WideCharToMultiByte(CP_ACP, 0, url, -1, buffer, len, NULL, NULL);
0233 SendUrl(buffer);
0234 delete buffer;
0235
0236 return S_OK;
0237 }
0238 else
0239 {
0240 PFN_LAUNCHURL pFn = (PFN_LAUNCHURL)m_launchURL_OLD;
0241 return pFn(This, url);
0242 }
0243 }
0244
0245 // Kill.cpp
0246
0247 // 接口查询
0248 HRESULT FindInterface(LPDWORD lpVtable)
0249 {
0250 CComPtr< IShellWindows > spShellWin;
0251 HRESULT hr = spShellWin.CoCreateInstance(CLSID_ShellWindows);
0252 if (FAILED(hr)) return hr;
0253
0254 long nCount = 0;
0255 spShellWin->get_Count(&nCount);
0256 if (0 == nCount) return E_FAIL;
0257
0258 for (long i = 0; i < nCount; i++)
0259 {
0260 CComPtr< IDispatch > spDispIE;
0261 hr = spShellWin->Item(CComVariant((long)i), &spDispIE);
0262 if (FAILED(hr)) continue;
0263
0264 CComQIPtr< IWebBrowser2 > spBrowser = spDispIE;
0265 if (!spBrowser) continue;
0266
0267 CComPtr < IDispatch > spDispDoc;
0268 hr = spBrowser->get_Document(&spDispDoc);
0269 if (FAILED(hr)) continue;
0270
0271 CComQIPtr< IHTMLDocument2 > spDocument2 = spDispDoc;
0272 if (!spDocument2) continue;
0273 IID_IHTMLWindow2;
0274 IHTMLWindow2* lpIHTMLWindow2 = NULL;
0275 hr = spDocument2->get_parentWindow(&lpIHTMLWindow2);
0276 if (FAILED(hr)) continue;
0277
0278 *lpVtable = *((LPDWORD)(LPVOID)lpIHTMLWindow2);
0279 lpIHTMLWindow2->Release();
0280
0281 return TRUE;
0282 break;
0283 }
0284
0285 return FALSE;
0286 }
0287
0288 BOOL CheckModuleName1()
0289 {
0290 ...
0291 }
0292
0293 BOOL CheckModuleName2()
0294 {
0295 ...
0296 }
0297
0298 DWORD CALLBACK ThreadProc(LPVOID lpParam)
0299 {
0300 // COM环境是针对线程的
0301 ::CoInitialize(NULL);
0302
0303 int count = 10;
0304 while (count >= 0)
0305 {
0306 DWORD Vtable = NULL;
0307 if (FindInterface(&Vtable))
0308 {
0309 IHTMLWindow2Vtable::HookMethod((MEMADDR_PTR)Vtable);
0310 break;
0311 }
0312
0313 count--;
0314 Sleep(1000);
0315 }
0316
0317 return 0;
0318 }
0319
0320 DWORD CALLBACK ThreadProc2(LPVOID lpParam)
0321 {
0322 // COM环境是针对线程的
0323 ::CoInitialize(NULL);
0324 Sleep(1000);
0325 IWMPCoreVtable::HookMethod();
0326 return 0;
0327 }
0328
0329 BOOL APIENTRY DllMain(HANDLE hModule,
0330 DWORD ul_reason_for_call,
0331 LPVOID lpReserved)
0332 {
0333 g_hModule = hModule;
0334
0335 switch (ul_reason_for_call)
0336 {
0337 case DLL_PROCESS_ATTACH:
0338
0339 if (CheckModuleName1())
0340 return TRUE;
0341
0342 if (CheckModuleName2())
0343 {
0344 {
0345 HANDLE handle = CreateThread(NULL, 0, ThreadProc, 0, 0, 0);
0346 CloseHandle(handle);
0347 }
0348
0349 {
0350 HANDLE handle = CreateThread(NULL, 0, ThreadProc2, 0, 0, 0);
0351 CloseHandle(handle);
0352 }
0353
0354 return TRUE;
0355 }
0356
0357 return FALSE;
0358 break;
0359 case DLL_PROCESS_DETACH:
0360 if (CheckModuleName2())
0361 {
0362 IHTMLWindow2Vtable::UnHookMethod();
0363 IWMPCoreVtable::UnHookMethod();
0364 }
0365 break;
0366 }
0367
0368 return TRUE;
0369 }
在该实例中,直接将弹出的窗口进行的阻断式的拦截。这和前面的若干实验不一样,前面的实验中,在挂钩到相关函数时,仅仅是获取到一些信息后,就将函数的控制权返回给了原始函数,而本实例,在挂钩到IWMPCore::launchURL,IHTMLWindow2::open两个接口函数后,不再向原始函数返回。即,当目标进程调用这两个接口函数时,是无效的,这样也就达到了我们拦截弹窗的目的。但是,考虑到有一些弹窗是非常重要的,利如银行通过弹窗来通知用户某个时段要进行服务器维护等。所以,在拦截到弹窗口,还需要将拦截到的弹窗URL告之用户。本实例中,将所以拦截到的URL显示在程序主界面的一个多行文本框之中。在Kill.dll中,通过向主程序窗口发送一个WM_SETTEXT消息来通知已经拦截到的弹窗的URL。WM_SETTEXT消息对于,一个普通窗口来说,是设置其标题,而对于控件窗口来说,是设置控件上的文字。本实例的主程序在收到WM_SETTEXT并不是将消息中指定的字符串显示的标题栏上,而是输出到主程序界面是的文本框之中。所以在主程序中,通过窗口子类化技术,程序拦截自身的消息,在收到WM_SETTEXT时,直接将该消息中指定的字符串输出到文本框中。
窗口子类化,是进程内拦截消息的一种较为官方的方式。众所周知,在Windows环境下编程,程序是通过一个消息循环来决定生命周期的,即程序在不断的处理由Windows系统发送来的各消息。Windows要求,每一个窗口都有一个对应的“窗口过程函数”,原型如下:
LRESULT CALLBACK WindowProc(
HWND hwnd, // 发生消息的窗口句柄
UINT uMsg, // 消息类型
WPARAM wParam, // 消息参数,不同消息对应不同的含意
LPARAM lParam // 消息参数,不同消息对应不同的含意
);
窗口子类化,就是将指定窗口的窗口过程函数地址修改为开发者提供的新的函数地址。从而使得能够在原窗口在凋用窗口过程函数之前,收到Windows发送来的消息。窗口子类化需要用到的函数是SetWindowLong,窗口子类化仅是该函数的功能之一,该函数原型如下:
LONG SetWindowLong(
HWND hWnd, // 将要子类化的窗口句柄
int nIndex, // 子类化窗口时,需指定该值为GWL_WNDPROC
LONG dwNewLong // 子类化窗口时,需指定该值为新的窗口过程函数的地址
);
子类化窗口后,若函数正确执行,将旧的窗口过程函数的地址通过返回值返返回给调用者。这个返回值通常用于将函数控制权返回给原始函数,但是将控制权返回给原始函数时,不必再一次进行子类化。调用CallWindowProc函数即可,该函数原型如下:
LRESULT CallWindowProc(
WNDPROC lpPrevWndFunc, // 窗口过程地址,即SetWindowLong的返回值
HWND hWnd, // 窗口句柄
UINT Msg, // 窗口消息
WPARAM wParam, // 消息参数
LPARAM lParam // 消息参数
);
窗口子类化请参见附档源码中的PopTerminatorDlg.cpp。
从一个DLL向窗口中传递数据的办法除了Windows消息外,还可以使用自定义消息,管道,邮槽,socket,剪贴板,共享内存等等方式。当然,最简单的就是Windows消息。这也是本实例中所使用的方法。
除此之外,在传递数据的时候,还要考虑到DLL和窗口之间的线程同步问题。本实例使用SendMessage发送消息,而该函数本身就是一个同步函数。SendMessage的另一个版本PostMessage则是异步的,在使用此函数时,如果WPARAM或LPARAM中携带的数据是分配在堆中的,尤其要注意与窗口同步的问题。关于SendMessage和PostMessage两个函数的使用请参见MSDN。
该实例,使用了前文中的虚表补丁法,来挂钩IWMPCore::launchURL,IHTMLWindow2::open两个接口函数。同样的,我们将要自己获取IWMPCore接口和IHTMLWindow2接口的接口函数表。这里着重说一下IHTMLWindow2接口的获得,因该接口在创建前,需要注册到系统Shell中,所以它的创建过程较为复杂。在此实例中,我们不是直接手工创建该接口。而是启动一个线程,不断查询系统Shell中已注册的IE窗口,然后使用Windows提供的方法来获得IHTMLWindow2接口指针。这段代码详见上文中的FindInterface函数。
该程序运行结果如图4-3所示:
图4-3 网页弹窗拦截器运行实例
Dll入口函数(DllMain)需要注意的问题
Kill.dll程序中,在入口函数中(DllMain)不要直接调用挂钩函数,因为在获取或创建接口的过程中,还要加载DLL文件。如果在DllMain中再加载其他DLL文件,极易造成死锁,而影响目标进程的稳定性。所以,在该程序的入口函数中,创建新的线程以完成自动挂钩过程。而且还要注意的是,在新的线程中要使用CoInitializeEx函数初始化COM环境。注意,这个函数初始的COM环境是针对线程的。在任何线程中要调用COM接口,均需要进行初始化COM环境。
另外,在由于在Kill.dll中安装了全局钩子,为了使该程序不注入到非目标进程中,在入口函数中也进行了宿主的进程名称判断。如果不是目标进行,直接在入口函数中返回FALSE即可。
本章小结
本章通过一个真正实例,拦截了两个工作在IE浏览器进程内的COM接口函数,IWMPCore::launchURL,IHTMLWindow2::open,使得IE浏览器在使用这两个方法弹窗的,会被我们的程序所拦截到,并且通过自定议消息,将拦截到的URL到发送挂钩程序的主窗口中以进行显示给用户。
第5章 结论与展望
参考文献
1. 潘爱民. COM原理与应用[M],北京:机械工业出版社,2006.4.
2. 梁肇新.编程高手箴言[M],电子工业出版社,2003.11,p.201-219
3. Jeffrey Richter,Christophe Nasarre,Windows核心编程,清华大学出版社,2008
4. Microsoft. Microsoft Developer Network[DB/OL],Microsoft,2008
致谢
这篇论文能够顺利的完成,首先要感谢我的论文指导老师吕老师,以及班主任郭老师,是他们悉心的指导及无微不致的关怀才得以使论文顺利完成。
我要真诚的感谢我的妻子和可爱的女儿,论文写作期间,妻子承担了所有家务,才使得我能够专心于课题的研究。宝贝女儿天真的眼神给了我莫大的写作动力。
真诚的感谢所有参加论文评审的各位专家,感谢你们在百忙之中对我的论文给予批评指正。
最后,真的的感谢这世上,所有我爱的,和爱我的人。正是这些爱,使我在软件开发这条道路上使终坚定的自己的信念。并将为中国的软件事业尽一份绵薄之力。
附录(术语对照表)
COM:Component Object Mode,组件对象模型。
Hook:挂钩,挂接,钩子。
API:Application Programming Interface,应用程序编程接口。
DLL:Dynamic Link Library,动态链接库。
Interface:接口,本文中特指COM指口及C++接口。
原创声明
本论文同步发表在本人的CSDN博客上,以及看雪论坛上,以发表时间戳来保护这篇论文的原创立场。
若在互联网上或者论文库中找到与本文相似的文章且时间早于本人发表的时间,即可认定该论文非本人原创。
源码下载地址: 稍后更新...
原文地址:http://blog.csdn.net/zhangluduo/article/details/41810619