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

相关推荐
许野平1 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
齐 飞3 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod3 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。4 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man4 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*4 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu4 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s4 小时前
Golang--协程和管道
开发语言·后端·golang
为什么这亚子4 小时前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算
想进大厂的小王5 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构