前言
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这部分知识用到的还蛮多的,在下一篇继续讨论。