问题描述
在运行时,我得到了一个函数地址列表(在同一个进程中),每次调用其中任何一个函数时,我都需要记录其地址。
"我的尝试"
如果只有一个函数(在subhook这样的挂钩库的帮助下),我可以创建一个挂钩:
create_hook(function_to_be_hooked, intermediate)
intermediate(args...):
log("function with address {&function_to_be_hooked} got called")
remove_hook(function_to_be_hooked)
ret = function_to_be_hooked(args...)
create_hook(function_to_be_hooked, intermediate)
return ret
这种方法并不是简单的扩展。我可以在编译时添加任意数量的函数,但我只知道在运行时需要多少。如果我用同一个intermediate
挂接多个函数,它不知道是谁调用了它。
详细数据
看起来这个问题应该通过挂钩库来解决。我使用的是C/C++和Linux,唯一的选项似乎是subhook和funchook,但它们似乎都不支持这个功能。
2条答案
按热度按时间uklbhaso1#
用汇编语言手工完成应该是可行的,就像你修改一个钩子库一样。覆盖原始函数开头的机器码可以在跳转到使用
call
将推送一个唯一的返回地址,而钩子可能并不希望实际返回到该地址。(因此,它会使返回地址预测器堆栈失衡,除非钩子使用ret
和修改后的返回地址,或者它使用一些前缀作为填充,使call hook
或call [rel hook_ptr]
或任何其他字符在原始代码的指令边界处结束,以便它可以ret
。)如果函数在x86-64 System V调用约定中不是变元函数,则类似于
mov al, imm8
;如果函数在x86 - 64 System V调用约定中不是变元函数,则类似于mov r11b, imm8
。或者mov ah, imm8
在x86-64 SysV中可以工作,而不会干扰变元函数的XMM参数的AL= #,并且仍然只有2个字节。或者使用push imm8
。如果钩子函数本身是用asm编写的,那么它可以直接查找寄存器和额外的堆栈参数,或者只是从
call
中返回一个地址,作为额外的参数,而不会影响它为钩子函数查找参数的能力。如果它是用C编写的,那么查找全局(或线程局部)变量就不需要定制的调用约定。但是对于现有的钩子库,假设你是对的,它们不会传递
int id
使用这个库接口,你似乎需要生成未知数量的可作为函数指针调用的唯一对象?这不是ISO C能做到的。它可以被严格地提前编译,不需要在运行时生成任何新的机器码。它与严格的哈佛体系结构兼容。
你可以定义一个指向
hook1()
、hook2()
等的函数指针数组,每个函数指针都在数组的另一个结构体成员中寻找自己的边数据。足够多的钩子函数,无论你在运行时需要多少,你都已经足够了。每个钩子函数都可以硬编码它应该访问的数组元素,以获得它的唯一字符串。您可以使用一些C预处理器宏来定义一些数量足够多的钩子,并单独获取一个用结构体初始化的数组,该结构体包含指向它们的函数指针。一些CPP技巧可能允许对名称进行迭代,因此您不必手动写出
define_hook(0)
define_hook(1)
...define_hook(MAX_HOOKS-1)
。或者,可能有一个计数器作为CPP宏,它使#defined
达到一个新的更高值。未使用的钩子将位于内存和磁盘上的可执行文件中,但不会被调用,因此它们不会在缓存中很热。那些不与任何其他代码共享页面的钩子根本不需要被分页到RAM中。指针数组和边数据数组的后面部分也是如此。它不优雅、笨拙,并且不允许无限的数字。但如果你能合理地说1024或8000“对每个人来说应该足够了”,那么这是可行的。
另一种方法也有很多缺点,与上面的方法不同,但比上面的方法更糟。特别是它需要从递归的底部调用程序的其余部分(* 而不仅仅是 * 调用一个正常返回的init函数),并且使用了大量的堆栈空间。(你可能会使用
ulimit -s
来增加堆栈大小限制,超过Linux通常的8 MiB。)此外,它还需要GNU扩展。GNU C nested functions可以用生成新的可调用实体,当你获取嵌套函数的地址时,在堆栈上生成“trampoline”机器码。这将是你的堆栈可执行文件,所以有一个安全强化的缺点。对于嵌套函数,将有一个实际机器码的副本。而是
n
个trampoline代码的副本,它设置了一个指向正确堆栈帧的指针。所以你可以使用一个递归函数遍历你的钩子数组,比如
foo(counter+1, hooks+1)
,并且让钩子成为一个嵌套函数,读取counter
。或者,它可以不是一个计数器,而是一个char*
或者任何你喜欢的东西;您只需在函数的调用中设置它。这是相当令人讨厌的(钩子机器代码和数据都在堆栈上),并且可能会为程序的其余部分使用大量的堆栈空间。**你不能从这个递归返回,否则你的钩子会中断。所以递归基本情况必须(tail)调用一个实现程序其余部分的函数,直到程序结束才返回到最终调用者。
C有一些
std::
可调用对象,比如某个特定对象的成员函数的std::function = std::bind
,但是它们与函数指针的类型不兼容。您不能将
std::function *
指针传递给需要空void (*fptr)(void)
函数指针的函数;要实现这一点,可能需要库分配一些可执行内存并在其中生成机器代码。**但ISO C的设计是严格提前编译的,因此它们不支持这种情况。std::function<void(void)> f = std::bind(&Class::member, hooks[i]);
编译,但是产生的std::function<void(void)>
对象不能转换成void (*)()
函数指针。(https://godbolt.org/z/TnYM6MYTP)。调用者需要知道它调用的是std::function<void()>
对象,而不是函数指针。当你这样做的时候,没有新的机器码,只有数据。wh6knrhe2#
我的直觉是遵循调试器路径。
您需要
uin8_t *
-〉uint8_t
Map,在广义斯托克斯中,
int3
修补指向的字节。您 * 可以 * 自己从用户空间设置TF,并捕获结果
SIGTRAP
,直到您清除它(在POSIX操作系统上);更常见的情况是TF仅由调试器使用,例如,由内核作为Linux的ptrace(PTRACE_SINGLESTEP)
的一部分进行设置。但是设置/清除TF不是特权操作。(使用int3
修补机器码字节是调试器实现软件断点的方式,而不是使用x86的dr0-7
硬件调试寄存器。在您自己的进程中,在mprotect
之后不需要系统调用来使其可写。)int3
,并返回以让程序运行,直到再次命中int3
。在POSIX中,异常帧由
uap
参数指向sigaction
处理程序。优点:
缺点: