assembly 如何挂接未知数量的函数- x86

huwehgph  于 2022-11-13  发布在  其他
关注(0)|答案(2)|浏览(144)

问题描述

在运行时,我得到了一个函数地址列表(在同一个进程中),每次调用其中任何一个函数时,我都需要记录其地址。
"我的尝试"
如果只有一个函数(在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,唯一的选项似乎是subhookfunchook,但它们似乎都不支持这个功能。

uklbhaso

uklbhaso1#

用汇编语言手工完成应该是可行的,就像你修改一个钩子库一样。覆盖原始函数开头的机器码可以在跳转到使用call将推送一个唯一的返回地址,而钩子可能并不希望实际返回到该地址。(因此,它会使返回地址预测器堆栈失衡,除非钩子使用ret和修改后的返回地址,或者它使用一些前缀作为填充,使call hookcall [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()>对象,而不是函数指针。当你这样做的时候,没有新的机器码,只有数据。

wh6knrhe

wh6knrhe2#

我的直觉是遵循调试器路径。
您需要

  • 一个uin8_t *-〉uint8_tMap,
  • 陷阱处理程序,以及
  • 单步处理程序

在广义斯托克斯中,

  • 当你收到一个监控函数的请求时,把它的地址和它指向的字节添加到Map中,用int3修补指向的字节。
  • 陷阱处理程序将从异常帧中获取一个出错地址,并将其记录下来。然后,它将从Map中用该值解补丁字节,在FLAGS中设置单步标志(TF)(同样,在异常帧中),并返回。这将执行指令,并引发单步异常。

您 * 可以 * 自己从用户空间设置TF,并捕获结果SIGTRAP,直到您清除它(在POSIX操作系统上);更常见的情况是TF仅由调试器使用,例如,由内核作为Linux的ptrace(PTRACE_SINGLESTEP)的一部分进行设置。但是设置/清除TF不是特权操作。(使用int3修补机器码字节是调试器实现软件断点的方式,而不是使用x86的dr0-7硬件调试寄存器。在您自己的进程中,在mprotect之后不需要系统调用来使其可写。)

  • 单步处理程序应重新修补int3,并返回以让程序运行,直到再次命中int3

在POSIX中,异常帧由uap参数指向sigaction处理程序。

优点:

  • 没有臃肿的二进制文件
  • 无编译时检测
    缺点:
  • 正确实现很难。重新Map文本段可写;使I-cache无效;也许还有别的。
  • 巨大的性能损失;实时系统中的不可行性。

相关问题