在Python中调用未注册的COM对象

背景

迫于集团的强制要求,Windows下的客户端需要接入一个安全组件。启动时主程序通过这个安全组件加载其他动态库,在加载动态库时则会检查文件的签名和签发人,确保加载的动态库没有被篡改。
安全组件提供了动态库、头文件及msvc调用的例子。本以为用Python的ctypes直接调用动态库导出的函数就能搞定。结果发现事情并没有那么简单。动态库只导出了根据IID获取COM对象的接口。实际的功能函数都在对应的COM对象的成员函数中。而头文件则对这一系列操作做了封装,将获取COM对象,调用相关成员函数等一系列操作,封装在一个内联函数中。
对于用C/C++开发的程序,直接调用include头文件,调用这些内联函数就行了。但我们的客户端是使用Python开发的,没法这么做。最简单的方法应该是再实现一个动态库,里面实现调用这些内联函数的函数,并把自己实现的函数导出,在Python中通过ctypes去调用。然而这么套娃,总感觉很奇怪。于是就想着,能不能在Python中直接获取并使用这些COM对象(作死开始)。

pythoncom

先尝试了如下代码:

1
2
import pythoncom
com_object = pythoncom.ObjectFromAddress(object_pointer_from_dll.value, '{class_id}')

执行后报错:

1
TypeError: There is no interface object registered that supports this IID

一通Google之后,大概找到了问题的原因。pythoncom需要COM对象继承并实现IDispatch的相关接口,否则就不知道COM对象实现的函数和返回值类型。很明显,安全组件提供的COM组件并没有继承和实现IDipatch接口。

通过虚函数表调用

仔细想想,COM对象,本质上也是一个C++对象,应该能通过获取虚表中各个函数的函数指针,直接调用成员函数。
再次Google一番之后,找到了一些相关的资料。The layout of a COM object中提到:

  • COM对象的开头为指向虚函数表的指针。
  • 虚表中存放着调用方式为__stdcall的函数指针,第一个参数为对象本身,之后为函数入参。
  • 同一个类中的函数按照声明的先后顺序排列。基类的函数指针在派生类之前。
  • 如果没有出现了菱形继承,开头只有一个虚表指针。否则,开头会出现多个虚表指针。需要根据被调用函数所在的基类,去判断需要使用的虚表。

实现的代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from ctypes import addressof, cast, c_long, c_void_p, POINTER, WINFUNCTYPE
from typing import List, Type, Union


class IUnknown:
def __init__(self, address: c_void_p):
self._address = address
self._v_table = cast(self._address, POINTER(POINTER(WINFUNCTYPE(None))))

def __del__(self):
self._release()

def _release(self):
return int(self._get_func(0, 2, [c_void_p], c_long)(self._address))

def _get_func(self, v_table_index: int, function_index: int, arg_types: List[Type], result_type: Union[Type, None]):
return WINFUNCTYPE(result_type, *arg_types).from_address(addressof(self._v_table[v_table_index][function_index]))


class ICustomObject(IUnknown):
def __init__(self, address: c_void_p):
super(ICustomObject, self).__init__(address)

def custom_function(self) -> bool:
return bool(self._get_func(0, 3, [c_void_p], c_long)(self._address))

其他

平心而论,通过这种方式使用COM对象,可维护性极差。如果可以选,类似场景下,还是建议使用背景中提到的方案。