python中普通方法与c函数的hook挂钩
前言
java可以在不改用户代码的前提下,hook一些方法,做一些扩展,在python下虽然也可以hook方法,但是需要用户插入一行代码用于触发。
普通方法hook
python中的hook可玩性高,他hook方法比较简单,下面看一个例子,这个例子最早可在stackoverflow中找到,虽然这里还有几个不明白的地方,但是秉着一句话,能跑就行。
import ctypes import user def magic_get_dict(o): dict_addr = id(o) + type(o).__dictoffset__ dict_ptr = ctypes.cast(dict_addr, ctypes.POINTER(ctypes.py_object)) return dict_ptr.contents.value def magic_flush_mro_cache(): ctypes.PyDLL(None).PyType_Modified(ctypes.cast(id(object), ctypes.py_object)) class Wrapper: def __init__(self,origin): self.origin_method=origin def __call__(self, *args, **kwargs): origin_value=self.origin_method(*args,*kwargs) if origin_value=="user1": return "user2" return origin_value def wrapper_invoke(s,b,origin): origin(s, b) origin_method =getattr(user,"get_name") dct = magic_get_dict(user) dct['get_name'] =Wrapper(origin_method) magic_flush_mro_cache() print(user.get_name())
主要用到的是ctypes模块,这个模块是用于和C沟通的一个桥梁,比起java来,python调用dll、so函数方便的要很多,实现hook的思想是:对象中有函数,函数会有一个地址,我们只要替换这个函数地址指向新的函数即可,并且保存原地址,在新函数中以便在调用原函数。
而上面的magic_get_dict
方法就是获取对象中这个函数的信息,结果是一个字典,通过dct['get_name']=xx
就可以替换原来的函数了。
最后需要执行刷新操作。
虽然上述能完成我们想要的需要,但是有两个问题,获取字典可以通过__dict__
,为什么还要通过那么复杂的方法呢,是因为用ctypes这种方法,可以做到修改内置类型中的方法,比如str,你可以替换str.count()的实现,而使用str.__dict__
获取的字典,是不允许被修改的。
而最后需要执行flush的操作,看样子意思应该是把新函数地址刷新一下,但是在linux上测试,好像有没有都一样,或者是,在某些场景下不flush真的会失效,暂时没见过这种场景。
但上面代码还有一个问题,magic_get_dict
函数需要传一个对象,我们作为hook者只知道函数完整的路径,我们不能import他,解决方法是使用__import__
函数,他可以动态地导入模块,允许我们在运行时根据字符串的内容导入一个模块,而不是在代码中直接指定模块名。
下面是修改了的代码
import ctypes import user def magic_get_dict(o): dict_addr = id(o) + type(o).__dictoffset__ dict_ptr = ctypes.cast(dict_addr, ctypes.POINTER(ctypes.py_object)) return dict_ptr.contents.value def magic_flush_mro_cache(): ctypes.PyDLL(None).PyType_Modified(ctypes.cast(id(object), ctypes.py_object)) class Wrapper: def __init__(self,origin): self.origin_method=origin def __call__(self, *args, **kwargs): origin_value=self.origin_method(*args,*kwargs) if origin_value=="user1": return "user2" return origin_value def wrapper_invoke(s,b,origin): origin(s, b) user_module=__import__("user",globals(),locals(),[],0) origin_method =getattr(user_module,"get_name") dct = magic_get_dict(user_module) dct['get_name'] =Wrapper(origin_method) magic_flush_mro_cache() print(user.get_name())
操作系统函数hook
python还可以hook c层的函数,由于python调用windll等非常方便,可能有人使用window/linux的函数,你也可以hook住window的函数,这个实现需要借助funchook这个库,github地址是https://github.com/kubo/funchook
。
下载下来进行编译,以下是linux中的例子。
$ git clone --recursive https://github.com/kubo/funchook.git $ mkdir build $ cd build $ cmake .. $ make $ make install
写一点点测试代码,首先同样是获取原函数的指针(hook最重要的一步是获取原函数地址),然后在调用funchook安装挂钩即可。
#include #include #include #include static int (*getpid_func)(); static int getpid_hook() { printf("origin getpid==%d\n",getpid_func()); return 0; } int main() { funchook_t *funchook = funchook_create(); getpid_func = getpid; funchook_prepare(funchook, (void**)&getpid_func, getpid_hook); funchook_install(funchook, 0); printf("%d\n",getpid()); return 0; }
输出如下,可以看到在调用getpid时,先进入了我们的hook函数,然后通过getpid_func指针就可以调用原函数了,想做你想做的事。
origin getpid==8809 0
那接下来看python的hook,下面这段代码是调用getpid,输出没有任何问题。
import ctypes libc = ctypes.CDLL("libc.so.6") pid = libc.getpid() print("ID:", pid)
然后在看hook,python使用funchook还是有一点点麻烦的。
import ctypes fh_lib = ctypes.cdll.LoadLibrary('/usr/local/lib/libfunchook.so') libc = ctypes.CDLL("libc.so.6") funchook_create = fh_lib.funchook_create funchook_create.restype = ctypes.c_void_p funchook_create.argtypes = [] funchook_prepare = fh_lib.funchook_prepare funchook_prepare.restype = ctypes.c_ssize_t funchook_prepare.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] funchook_install = fh_lib.funchook_install funchook_install.restype = ctypes.c_ssize_t funchook_install.argtypes = [ctypes.c_void_p, ctypes.c_int] funchook_libc_getpid = libc.getpid funchook_libc_getpid.restype = ctypes.c_int funchook_libc_getpid.argtypes=[] global origin_getpid_func, hook, origin_getpid_func_ptr hook_type = ctypes.PYFUNCTYPE(ctypes.c_int ) origin_getpid_func = None def hook_impl(): print("origin",origin_getpid_func()) return 1 hook = hook_type(hook_impl) fh = funchook_create() origin_getpid_func_ptr = ctypes.c_void_p(ctypes.c_void_p.from_address(ctypes.addressof(funchook_libc_getpid)).value) ret = funchook_prepare(fh, ctypes.addressof(origin_getpid_func_ptr), hook) ret = funchook_install(fh, 0) origin_getpid_func = hook_type.from_address(ctypes.addressof(origin_getpid_func_ptr)) pid = libc.getpid() print("ID:", pid)
首先是定义要调用函数的返回值类型和参数,如下面这段是定义getpid的参数类型和返回值类型。
funchook_libc_getpid = libc.getpid funchook_libc_getpid.restype = ctypes.c_int funchook_libc_getpid.argtypes=[]
在通过一系列的地址指针转换操作,安装挂钩即可,在调用libc.getpid()
时,首先会进入hook_impl
方法,origin_getpid_func变量保存着原来函数地址指针,可以进行调用,上面代码我们只hook一下返回一个常量1。
funchook和ctypes这部分知识用到的还蛮多的,在下一篇继续讨论。