技术文章 > 动态替换COM方法和属性

动态替换COM方法和属性

2018-09-25 05:26

文档管理软件,文档管理系统,知识管理系统,档案管理系统的技术资料:
1 替换的原因
动态替换COM方法的原因主要是:我们需要了解某个应该在干什么,并希望动态该变其行为。
95以后的Windows平台充满了COM接口,这些接口都提供了哪些功能,如何使用这些功能,应该是许多人的梦想。通过OleView.exe可以察看一些COM组件的接口,我们可以通过实验,猜想/认识这些方法的使用。但这个过程无疑是痛苦的,因为我们根本不知道这些方法的使用顺序,甚至牵扯到其他接口的使用。那末最好的方法就是看看别人的应用是怎样使用的。
另外,你想知道IE在播放视频时都干了些什么吗?
希望读完本文让你得到一点乐趣或兴奋!
2 基础知识
获得进程的控制权

Win32是进程独立的,要想动态修改其他进程空间的COM方法,首先得得到改进程的控制权,一般使用Windows的“钩子”函数,将自己的DLL注入(让进程加载)所关心的进程空间。
MSDN中有详细的文档说明,关键函数为:
SetWindowsHookEx(…)
UnhookWindowsHookEx(…)
等,另外还要会写自己的DLL。
替换进程的API入口

PE文件格式一般包含引入函数列表和输出函数列表,其中引入函数列表中包含了该应用程序(exe、dll等可执行文件)所连接的DLL的函数信息;输出函数列表包含了该执行体提供给其他应用使用的函数列表,一般只有DLL才提供输出函数。
使用SDK工具Dumpbin.exe可以察看文件的引入函数和输出函数,也可以看到所连接的DLL文件名。
Dumpbin /imports ObjFile.exe 可以察看与文件ObjFile.exe所有连接的DLL,及其使用了每个DLL中的那一些输出函数。
下面是其中的一部分,至少表明了:ObjFile.exe连接了ole32.dll文件,而且使用了该DLL中的CoRevokeClassObject、CoRegisterClassObject等函数。


Dumpbin /exports ole32.dll 可以察看ole32.dll中所有提供的输出函数。
下图是其中的一部分:至少表明了ole32.dll提供了输出方法CoCreateGuid,而且该函数的静态执行代码位于ole32.dll中的文件偏移0x00003D4C处。


可执行程序运行时,加载所连接的DLL,或通过显式LoadLibrary()等调用得到DLL实例,再通过函数的偏移,计算得到引入函数的实际地址(进程地址或虚拟地址)。当需要调用该引入函数时,实际就是调用该函数指针所存储的地址中的内容(有点拗口)。因此可以将该地址中存储的内容(函数指针)替换为我们的DLL中的某个函数的指针(前面说过,需要自己写一个DLL先注入当前进程)就好了。
当然你的DLL提供的输出函数必须与原来的DLL提供的输出函数声明完全一样,否则进行参数传递或函数返回时可能破坏堆栈,那就必死无疑了。
说起来就这么简单,但真正的操作还有很多情况必须考虑,但目前有一本《Windows核心编程》(Jeffrey Richter)(注:这是Win32下真正的好书,非一般“内幕”、“核心”可比,强烈推荐)的第22章有详细的说明,并提供一个方便实用的C++类(CAPIHook),可以直接使用,但源程序好像有点小问题,用了你就会发现的。
关键要理解PE文件结构,与DLL函数的存储结构,再就是使用CAPIHook类。
C++虚函数与COM接口

一提COM就要想到C++,当然也有可能是C程序员的原因。C++中的虚函数机制恰好揭示了COM接口的本质,即提供What,不管How。
C++编译器将决定C++ class实例化后的存储结构。我目前只能确定的说:MSVC6.0编译器生成的C++对象的结构是这样的(没有多重继承):

对于COM接口的要求是:接口的前三个函数(虚函数)一定是:
1. QueryInterface:对应上图虚函数表中的“virtual f1”,当“对象体”中包含多个接口时(使用多重继承),一般使用QueryInterface得到其他接口;
2. AddRef:对应上图虚函数表中的“virtual f2”;
3. Release:对应上图虚函数表中的“virtual f3”,将用于确认对象的注销;
对于COM接口中的其他方法,依次列于虚函数表中。
关键要理解COM接口方法与对象的存储关系,接口不关心数据。
COM的实例化

COM一般使用两种方式被实例化:
COM库函数 该COM库函数使用clsid(类标示)和iid(接口标示)创建COM实例(对象)。
CoCreateInstance(Ex)是创建COM实例的主要方法,一般通过clsid和iid两个参数,我们就可以确定那个COM接口将要被实例化。
COM类工厂 首先得到COM的IID_IClassFactory接口,再通过IClassFactory.CreateInstace创建COM实例(对象)。
一般使用CoGetClassObject得到指定clsid的COM的类工厂接口,然后再将iid作为参数传递给其类工厂接口的方法CreateInstance(),创建COM实例。如果使用这种方式创建的COM对象,有必要了解接口IClassFactory的定义(见MSDN)。
MSDN中有详细文档。
库、类、接口

从IDL文件的结构可以看出,一个包含COM实现的DLL的结构:
一个库(Library),可以包含多个类(CoClass),每个类可以包含多个接口(Interface)。
this指针

一般情况下this指针对于一个C++对象是确定的,但对于提供多个接口实现的对象(使用多重继承)却有可能不一样。对COM接口的实现对象的QueryInterface()一般可能见到如下代码:


this指向的对象将至少包含两个虚函数表指针,分别指向类IFoo1声明的虚函数表和类IFoo2声明的虚函数表,而通过上面的不同执行分支,指针ppObj将包含不同的值。
另外,非常重要的一点是:
非静态成员函数的第一个参数应该是this。
即在C观点看IClassFactory::CreateInstance(NULL, IID_IFoo, &pObj)类似
CreateInstance(pthis, NULL, IID_IFoo, &pObj)
关键了解this指针可能的变化,而且作为每一个非静态成员函数的第一个参数。
3 步骤
动态替换COM对象的方法或属性其实就是动态修改特定COM对象的特定方法的函数指针指针,使之指向自己提供的函数。
Win32的进程地址空间是线型的,即代码指针(EIP)可以指向该进程可使用的地址中代码段的任意位置,你编写的DLL注入应用进程后,其执行代码自然加载到进程的代码段地址空间。
现在关键任务是找出COM实现方法的进程地址。
3.1 发现所关心的COM接口被加载
截获应用对COM库函数CoCreateInstance()和CoGetClassObject()的调用。
注入并替换API函数。可能有些COM的创建者(应用)可能使用其它的方法(MSDN中有介绍),但目前我所关心的COM创建都是使用上面两个函数创建的。如果没有截获那末只好使用WinDBG等调试工具,在所有想到的可能的函数上下断点,总能找到吧!
在你的替换函数中,首先关心的参数是clsid和iid。比较得到的clsid和iid,是否就是关心的COM类标示和接口标示。如果替换的是CoCreateInstance(),你可能直接得到所关心的接口的iid,如果替换的是CoGetClassObject(),一般将得到该类的工厂接口,就需要看一下类工厂是如何创建接口实例的了:
pClassFactory->CreateInstance(NULL, IID_IFoo, (void **)& pFoo);
首先要注意的是CreateInstance()是一个C++的类成员函数,即实际的函数调用中,将有下面的伪代码:
push pFoo 参数压栈,按照自右向左顺序
push &IID_IFoo
push 0
push this 指向pClassFactory对象实例的指针
call CreateInstance 调用函数
因此我们替换CreateInstance()的函数R_CreateInstance()必须如下声明:
HRESULT __stdcall R_CreateInstance(
void *pthis,
IUnknown *pUnkOut,
IID iid,
void ** ppObj);
下一个关心的参数就是创建的对象指针pObj了。好,在你的替换函数中可以这样写:
HRESULT __stdcall R_CreateInstance( pthis, pUnkOut, iid, ppObj)
{
// 调用原来的CreateInstance()
hr = gfp_Old( pthis, // 正确情况下,pthis应该是指向类工厂对象的指针
pUnkOut,
iid,
ppObj);
if (IsEqualIID(iid, &IID_IFoo)) { // 检查是否为所关心之接口
if (hr == S_OK) { // 创建成功!
gObj_Foo = *ppObj; // 赶快保存下来,这就是你所希望得到的接口的实例
}
}
if (IsEqualIID(iid, &IID_IUnknown)){ // 有时类工厂的调用并不直接创建需要的接口,而是通过QueryInterface获得
if (hr == S_OK) { // 创建IUnknown接口实例成功!
gObj_Unk = *ppObj; // 赶快保存下来,需要进一步处理,见下一节
}
}
}
得到了对象的实例(指向对象的指针),就可以通过C++的对象存储结构,通过虚函数表找到目标方法的地址了!!但,有时,特别是一个类包含多个接口实现时,应用可能调用类工厂的CreateInstance()创建某个接口(如IUnknown)实例,再通过QueryInterface()得到特定的接口实例。
3.2 通过QueryInterface得到接口实例
一般情况下,一个COM的类工厂只对应一个类的创建,而不管该类中实现了多少接口,因此只要得到该类中任何一个接口的实例,都应该可以通过该接口的QueryInterface()得到其他接口的指针(static_castthis,其实只是一份C++对象实例,但返回不同的地址偏移——指向各自接口的虚函数表)。
我们知道所有COM接口的都派生于IUnknown,因此,目前任何COM实例的虚函数表中的第一项都是指向QueryInterface()的函数指针。
如果前面没有得到期望接口的实例,那么现在有两种方式得到它:
依次替换掉当前接口的QueryInterface的实现,直到得到所期望的接口的创建
麻烦,可能要写一大堆R_QueryInterface()。
直接调用QueryInterface()(在R_CreateInstance中)
得到所期望接口的实例,但一定要注意,QueryInterface是谁的成员函数,千万不要把类工厂的this指针与当前接口的this指针搞混了。但怎样调用当前接口的QueryInterface呢?
假设pFoo是当前接口的实现的指针,那么通过它的存储结构就可以知道:
pvtlb_Foo = *(PROC **)pFoo是指向虚函数表的指针。
当然*(pvtlb_Foo+0)就是QueryInterface的函数指针了!然后调用之:
hr = (*(pvtlb_Foo+0))(pFoo, // 当前对象的this指针
iid, // 所期望的接口IID
ppObj); // 如果正确的话,这就是期望的接口的实例了!

3.3 假如想替换接口所有方法…
有时我们希望了解一个接口的所有方法的工作,而有嫌一个一个替换它麻烦,那就写一个替换类吧。
在我的例子中,替换了IMediaPlayer接口,这可是一个拥有至少185个方法(虚函数)的接口,单是以最简单的实现(调用原来的方法后返回)也写了几百行代码!替换类名为CTrajonMediaPlayer。
我是这样做的:
1. 使用VC的“#import “c:\winnt\system32\msdxm.ocx””生成Mediaplayer的节口描述;
2. 声明类CTrajonMediaPlayer,使用:
class CTrajonMediaPlayer : public MediaPlayer::IMediaPlayer
3. 依次实现所有虚函数(对于IMediaPlayer接口来说太多了,以至于后来写了一堆丑陋的宏定义)。注意,所有的COM方法的第一个参数是指向原来COM对象的指针,而不是替换类的this指针。
4. 有些虚函数的实现有特殊的要求,如Release()。COM规范对Release()返回值等于0时作了确切的要求,即当该COM对象的引用计数等于0时,Release()必须返回0,因此,通过对原来Release()的调用,可以决定何时释放你的替换类。
5. 还需要提供替换和恢复虚函数表的操作,在我的代码中放到了CTrajonMediaPlayer的构造/析构函数中。注意这里使用了VirtualProtectEx()函数修改了IMediaPlayer接口对象的虚函数表处的访问权限。
6. 然后……测试,修改,再测试,再修改……………………!。
4 例子
本文所附的例子是我工作中的一部分,因此不可能将所有代码公开,只提供其中的DLL部分。但由于牵扯到对工程库的使用,因此将无法编译。
文件列表
ApiHook.cpp、ApiHook.h和ToolHelp.h来自《Windows核心编程》的源代码;
DoGetClassObject.h实现了CTrajonMediaPlayer;
WnHookEx.cpp和WnHookEx.h为DLL的启动代码,和一些替换函数。
5 补充和总结
COM技术在发展,一些优化技术可能修改COM实例化的存储结构,因此本文中的方法不适应所有COM对象。
本文介绍了一种方法,但实则鼓励大家的钻研精神,有时我们为一些现象迷惑,有时费大时间争论vritual inheritance和public inheritance的区别,其实不如自己动手试一下,说不定还挖出什么“内幕”“核心”呢!