简体   繁体   English

如何挂钩未知数量的函数 - x86

[英]How to hook an unknown number of functions - x86

Problem description问题描述

At runtime, I am given a list of addresses of functions (in the same process).在运行时,我得到了一个函数地址列表(在同一进程中)。 Each time any of them is called, I need to log its address.每次调用它们中的任何一个时,我都需要记录它的地址。

My attempt我的尝试

If there was just one function (with help of a hooking library like subhook ) I could create a hook:如果只有一个 function(借助像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

This approach does not trivially extend.这种方法不会轻易扩展。 I could add any number of functions at compile-time, but I only know how many I need at runtime.我可以在编译时添加任意数量的函数,但我只知道在运行时我需要多少。 If I hook multiple functions with the same intermediate , it doesn't know who called it.如果我用同一个intermediate挂钩多个函数,它不知道是谁调用了它。

Details细节

It seems like this problem should be solved by a hooking library.看来这个问题应该通过挂钩库来解决。 I am using C/C++ and Linux and the only options seem to be subhook and funchook , but none of them seem to support this functionality.我正在使用 C/C++ 和 Linux,唯一的选项似乎是subhookfunchook ,但它们似乎都不支持此功能。

This should be fairly doable with assembly language manually, like if you were modifying a hook library.手动使用汇编语言应该是相当可行的,就像您正在修改挂钩库一样。 The machine code that overwrites the start of the original function can set a register or global variable before jumping to (or call ing) the hook.覆盖原始 function 开头的机器代码可以在跳转到(或call )钩子之前设置寄存器或全局变量。 Using call would push a unique return address which the hook likely wouldn't want to actually return to.使用call将推送一个唯一的返回地址,该钩子可能不想实际返回。 (So it unbalances the return-address predictor stack, unless the hook uses ret with a modified return address, or it uses some prefixes as padding to make the call hook or call [rel hook_ptr] or whatever end at an instruction boundary of the original code so it can ret .) (所以它不平衡返回地址预测器堆栈,除非钩子使用带有修改的返回地址的ret ,或者它使用一些前缀作为填充来使call hookcall [rel hook_ptr]或任何在原始指令边界处结束代码,所以它可以ret 。)

Like mov al, imm8 if the function isn't variadic in the x86-64 System V calling convention, or mov r11b, imm8 in x86-64.mov al, imm8如果 function 在 x86-64 System V 调用约定中不是可变参数,或者在 x86-64 中是mov r11b, imm8 Or mov ah, imm8 would work in x86-64 SysV without disturbing the AL= # of XMM args for a variadic function and still only be 2 bytes.或者mov ah, imm8可以在 x86-64 SysV 中工作,而不会干扰可变参数 function 的 AL= # of XMM args 并且仍然只有 2 个字节。 Or use push imm8 .或使用push imm8

If the hook function itself was written in asm, it would be straightforward for it to look for a register, and extra stack arg, or just a return address from a call , as an extra arg without disturbing its ability to find the args for the hooked function.如果钩子 function 本身是用 asm 编写的,那么它可以直接查找寄存器和额外的堆栈 arg,或者只是一个call的返回地址,作为一个额外的 arg,而不会影响它为钩住 function。 If it's written in C, looking in a global (or thread-local) variable avoids needing a custom calling convention.如果它是用 C 编写的,则查看全局(或线程局部)变量可避免需要自定义调用约定。


But with existing hook libraries, assuming you're right they don't pass an int id但是对于现有的钩子库,假设你是对的,它们不会传递一个int id

Using that library interface, it seems you'd need to generate an unknown number of unique things that are callable as a function pointer?使用该库接口,您似乎需要生成未知数量的可作为 function 指针调用的独特事物? That's not something ISO C can do.这不是 ISO C 可以做到的。 It can be strictly ahead-of-time compiled, not needing to generate any new machine code at run-time.它可以严格提前编译,不需要在运行时生成任何新的机器代码。 It's compatible with a strict Harvard architecture.它与严格的哈佛架构兼容。

You could define a huge array of function pointers to hook1() , hook2() , etc. which each look for their own piece of side data in another struct member of that array.您可以定义一个巨大的 function 指针数组,指向hook1()hook2()等,每个都在该数组的另一个结构成员中查找自己的边数据。 Enough hook functions that however many you need at run-time, you'll already have enough.足够多的钩子函数,无论你在运行时需要多少,你已经拥有了足够多的函数。 Each one can hard-code the array element it should access for its unique string.每个人都可以硬编码它应该为其唯一字符串访问的数组元素。

You could use some C preprocessor macros to define some large more-than-enough number of hooks, and separately get an array initialized with structs containing function pointers to them.您可以使用一些 C 预处理器宏来定义一些足够多的钩子,并分别使用包含指向它们的 function 指针的结构初始化数组。 Some CPP tricks may allow iterating over names so you don't have to manually write out define_hook(0) define_hook(1) ... define_hook(MAX_HOOKS-1) .一些 CPP 技巧可能允许迭代名称,因此您不必手动写出define_hook(0) define_hook(1) ... define_hook(MAX_HOOKS-1) Or maybe have a counter as a CPP macro that gets #defined to a new higher value.或者可能有一个计数器作为 CPP 宏,它可以将#defined设置为新的更高值。

Unused hooks would be sitting in memory and in your executable on disk, but wouldn't ever be called so they wouldn't be hot in cache.未使用的挂钩将位于 memory 和磁盘上的可执行文件中,但永远不会被调用,因此它们在缓存中不会很热。 Ones that didn't share a page with any other code wouldn't ever need to get paged in to RAM at all.那些不与任何其他代码共享页面的代码根本不需要分页到 RAM。 Same for later parts of the array of pointers and side-data.指针和边数据数组的后面部分也是如此。 It's inelegant and clunky, and doesn't allow an unbounded number, but if you can reasonably say that 1024 or 8000 "should be enough for everyone", then this can work.它不优雅且笨拙,并且不允许无限的数字,但是如果您可以合理地说 1024 或 8000 “应该对每个人都足够了”,那么这可以工作。


Another way also has many downsides, different but worse than the above.另一种方式也有许多缺点,与上述不同但更糟。 Especially that it requires calling the rest of your program from the bottom of a recursion ( not just calling an init function that returns normally), and uses a lot of stack space.尤其是它需要从递归的底部调用程序的 rest(不仅仅是调用正常返回的 init function),并且使用了大量的堆栈空间。 (You might ulimit -s to bump up your stack size limit over Linux's usual 8MiB.) Also it requires GNU extensions. (你可以使用ulimit -s来提高你的堆栈大小限制,超过 Linux 通常的 8MiB。)它还需要 GNU 扩展。

GNU C nested functions can make new callable entities with, making "trampoline" machine code on the stack when you take the address of a nested function. GNU C 嵌套函数可以创建新的可调用实体,当您获取嵌套 function 的地址时,在堆栈上生成“蹦床”机器代码。 This would your stack executable, so there's a security hardening downside.这将是您的堆栈可执行文件,因此存在安全强化缺点。 There'd be one copy of the actual machine code for the nested function, but n copies of trampoline code that sets up a pointer to the right stack frame.嵌套的 function 将有一份实际机器代码的副本,但有n个用于设置指向右侧堆栈帧的指针的蹦床代码副本。 And n instances of a local variable that you can arrange to have different values.并且您可以安排具有不同值的局部变量的n实例。

So you could use a recursive function that went through your array of hooks like foo(counter+1, hooks+1) , and have the hook be a nested function that reads counter .因此,您可以使用递归 function 遍历您的钩子数组,例如foo(counter+1, hooks+1) ,并让钩子成为嵌套的 function 读取counter Or instead of a counter, it can be a char* or whatever you like;或者代替计数器,它可以是char*或任何你喜欢的; you just set it in this invocation of the function.您只需在 function 的调用中设置它。

This is pretty nasty (the hook machine code and data is all on the stack) and uses potentially a lot of stack space for the rest of your program.这非常讨厌(钩子机器代码和数据都在堆栈上)并且可能为您的程序的 rest 使用大量堆栈空间。 You can't return from this recursion or your hooks will break.你不能从这个递归中返回,否则你的钩子会坏掉。 So the recursion base-case will have to be (tail) calling a function that implements the rest of your program, not returning to your ultimate caller until the program is ending.因此,递归基本情况必须(尾)调用实现程序的 rest 的 function,在程序结束之前不返回最终调用者。


C++ has some std:: callable objects, like std::function = std::bind of a member function of a specific object, but they're not type-compatible with function pointers. C++ has some std:: callable objects, like std::function = std::bind of a member function of a specific object, but they're not type-compatible with function pointers.

You can't pass a std::function * pointer to a function expecting a bare void (*fptr)(void) function pointer;您不能将std::function *指向 function 的指针传递给期望一个裸void (*fptr)(void) function 指针making that happen would potentially require the library to allocate some executable memory and generate machine code in it.实现这一点可能需要库分配一些可执行的 memory 并在其中生成机器代码。 But ISO C++ is designed to be strictly ahead-of-time compilable , so they don't support that.但是 ISO C++ 被设计为严格提前编译,所以他们不支持。

std::function<void(void)> f = std::bind(&Class::member, hooks[i]); compiles, but the resulting std::function<void(void)> object can't convert to a void (*)() function pointer.编译,但生成的std::function<void(void)> object 无法转换为void (*)() function 指针。 ( https://godbolt.org/z/TnYM6MYTP ). https://godbolt.org/z/TnYM6MYTP )。 The caller needs to know it's invoking a std::function<void()> object, not a function pointer.调用者需要知道它正在调用std::function<void()> object,而不是 function 指针。 There is no new machine code, just data, when you do this.当你这样做时,没有新的机器代码,只有数据。

My instinct is to follow a debugger path.我的直觉是遵循调试器路径。

You would need你需要

  • a uin8_t * -> uint8_t map,一个uin8_t * -> uint8_t map,
  • a trap handler, and陷阱处理程序,和
  • a single step handler单步处理程序

In broad stokes,概括地说,

  • When you get a request to monitor a function, add its address, and the byte pointed by it to the map.当您收到监控 function 的请求时,将其地址及其指向的字节添加到 map。 Patch the pointed-to byte with int3 .int3修补指向的字节。

  • The trap handler shall get an offending address from the exception frame, and log it.陷阱处理程序应从异常帧中获取违规地址,并将其记录下来。 Then It shall unpatch the byte with the value from the map, set a single-step flag in the PSW (again, in the exception frame), and return.然后它应该使用来自 map 的值对字节进行修补,在 PSW 中设置一个单步标志(同样,在异常帧中),然后返回。 That will execute the instruction, and raise a single-step exception.这将执行指令,并引发单步异常。

  • The single-step handler shall re-patch int3 , and return to let the program run until it hits int3 again.单步处理程序应重新修补int3 ,并返回以让程序运行,直到它再次命中int3

In POSIX, the exception frame is pointed by uap argument to a sigaction handler.在 POSIX 中,异常帧由uap参数指向sigaction处理程序。

PROS:优点:

  • No bloated binary没有臃肿的二进制文件
  • No compile-time instrumentation没有编译时检测

CONS:缺点:

  • Tricky to implement correctly.很难正确实施。 Remapping text segment writable;重新映射文本段可写; invalidating I-cache;使 I-cache 无效; perhaps something more.也许更多。
  • Huge performance penalty;巨大的性能损失; a no-go in real-time system.在实时系统中不可行。

Funchook now implements this functionality (on master branch, to be released with 2.0.0). Funchook现在实现了此功能(在 master 分支上,将与 2.0.0 一起发布)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM