python中普通方法与c函数的hook挂钩

前言

java可以在不改用户代码的前提下,hook一些方法,做一些扩展,在python下虽然也可以hook方法,但是需要用户插入一行代码用于触发。

普通方法hook

python中的hook可玩性高,他hook方法比较简单,下面看一个例子,这个例子最早可在stackoverflow中找到,虽然这里还有几个不明白的地方,但是秉着一句话,能跑就行。

python 复制代码
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__函数,他可以动态地导入模块,允许我们在运行时根据字符串的内容导入一个模块,而不是在代码中直接指定模块名。

下面是修改了的代码

python 复制代码
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中的例子。

java 复制代码
$ git clone --recursive https://github.com/kubo/funchook.git
$ mkdir build
$ cd build
$ cmake ..
$ make
$ make install

写一点点测试代码,首先同样是获取原函数的指针(hook最重要的一步是获取原函数地址),然后在调用funchook安装挂钩即可。

c 复制代码
#include <funchook.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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指针就可以调用原函数了,想做你想做的事。

c 复制代码
origin getpid==8809  
0

那接下来看python的hook,下面这段代码是调用getpid,输出没有任何问题。

python 复制代码
import ctypes
libc = ctypes.CDLL("libc.so.6")
pid = libc.getpid()
print("ID:", pid)

然后在看hook,python使用funchook还是有一点点麻烦的。

python 复制代码
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的参数类型和返回值类型。

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

相关推荐
0和1的舞者14 分钟前
基于Spring的论坛系统-前置知识
java·后端·spring·系统·开发·知识
invicinble1 小时前
对于springboot
java·spring boot·后端
码界奇点2 小时前
基于Spring Boot与Vue的校园后台管理系统设计与实现
vue.js·spring boot·后端·毕业设计·源代码管理
爱编程的小庄2 小时前
Rust 发行版本及工具介绍
开发语言·后端·rust
Apifox.3 小时前
测试用例越堆越多?用 Apifox 测试套件让自动化回归更易维护
运维·前端·后端·测试工具·单元测试·自动化·测试用例
sunnyday04264 小时前
Nginx与Spring Cloud Gateway QPS统计全攻略
java·spring boot·后端·nginx
康王有点困4 小时前
Link入门
后端·flink
海南java第二人4 小时前
Spring Boot全局异常处理终极指南:打造优雅的API错误响应体系
java·spring boot·后端
小楼v5 小时前
消息队列的核心概念与应用(RabbitMQ快速入门)
java·后端·消息队列·rabbitmq·死信队列·交换机·安装步骤
小北方城市网5 小时前
接口性能优化实战:从秒级到毫秒级
java·spring boot·redis·后端·python·性能优化