assembly 修补arm64二进制文件以替换所有指向特定函数的'call'指令

nbewdwxp  于 2022-11-30  发布在  其他
关注(0)|答案(1)|浏览(135)

如何将一个arm64二进制文件中的所有函数调用替换为对一个特定函数的调用。目的是“插入”一个间接寻址,这样我就可以记录所有函数调用。
示例:
第一个
替换函数打印指向函数的指针,然后用提供的参数调用被调用者。我也不确定这种转发在所有情况下都能工作,但其目的是这样的:

template<class F, class... Args>
void my_func(F&& f, Args&&... args) {
    printf("calling: %p", f);
    std::invoke(std::forward<F>(f), std::forward<Args>(args));
}
ev7lccsx

ev7lccsx1#

TL:DR:编写调用C++ void logger(void *fptr)并返回的asm Package 函数。不要尝试从C进行尾调用,因为这在一般情况下是不可能的。
另一种方法可能是“挂钩”每个被调用者,而不是在调用点重定向,但是这样你会错过对库中你没有插装的函数的调用。
我不认为C
允许你在不知道参数是什么的情况下转发任何/所有参数。在asm中,对于特定的调用约定,这很容易做到,因为真实的函数的最后调用可以是一个tailcall跳转,返回地址和所有参数传递寄存器都设置好了,堆栈指针也设置好了。但前提是你不想删除一个参数。
因此,与其让C++对真实的函数进行尾调用,不如让asm Package 器直接调用一个日志函数。要么直接printf,要么像extern "C" void log_call(void *fptr);这样返回的函数。它是正常编译的,所以它会遵循ABI,所以手工编写的asm trampoline / Package 器函数知道在跳转之前需要恢复什么。

捕获目标地址

bl my_func不会将bar的地址放在任何地方。
对于直接呼叫,您可以使用回邮地址(在lr中)来查找目标,例如在哈希表中。否则,你需要为每个挂钩的函数使用一个单独的蹦床。(修改代码以在目标函数而不是调用点挂钩就不会有这个问题,但是你必须把第一条指令替换成一个跳转,它记录然后返回,它做的和被替换的第一条指令做的一样。或者将前两条指令替换为保存返回地址然后调用的指令。)
但是像blr x8这样的间接调用需要一个特殊的stub,可能每个不同的寄存器都有一个trampoline stub来保存一个函数地址。
这些存根需要用asm编写。
如果你想用C来调用一个 Package 器,那就有点麻烦了,因为真实的的参数可能会占用所有的寄存器参数槽。改变栈指针来添加一个栈参数会使它成为一个新的第五个参数或者其他奇怪的东西。所以,只调用一个C函数来做日志记录会更好。然后恢复您保存在堆栈上的所有arg-passing寄存器。(使用stp一次恢复16个字节。)
这也避免了试图用C++创建透明函数的问题

删除一个参数并转发其余参数

您的设计要求my_func删除一个参数,然后将未知数量的未知类型的其他参数转发给另一个函数。这在ARM 64 asm中甚至是不可能的,因此C没有要求编译器执行此操作的语法也就不足为奇了。
如果arg实际上是一个void*或函数指针,它将占用一个寄存器,因此删除它将向下移动接下来的3个寄存器(x1到x 0,等等),然后第一个堆栈arg将进入x3。但堆栈必须保持16字节对齐,因此您不能只加载它,而将后面的堆栈arg留在正确的位置。
在 * 某些 * 情况下,一个解决方法是将f参数设置为16字节,这样它就占用了两个寄存器。然后你可以将x3、x2向下移动到x 0、x1和ldp 16字节的堆栈参数。除非该参数总是在内存中传递,而不是寄存器中传递,例如,更大对象的一部分。、或非POD、或任何用于C
ABI的标准,以确保它总是具有地址。
因此,f可能是32字节,因此它会进入堆栈,并且可以在不触及传递参数的寄存器或不需要将任何堆栈参数拉回到寄存器的情况下删除。
当然,在真实的情况中,你没有一个C函数可以添加一个新的第一个参数,然后传递所有其他参数,这也是你只能在特殊情况下做的,比如传递一个f.
这是您 * 可以 * 在32位x86上的asm中使用纯堆栈参数调用约定并且没有堆栈对齐要求来实现的;您可以将返回地址向上移动一个槽并跳转,这样您最终返回到原始调用点,堆栈指针恢复到调用trampoline之前的状态,trampoline添加了一个新的第一个参数并将返回地址复制到更低的位置。
但是C
不会有任何对ABI施加超出C所做的要求的构造。

扫描二进制文件中的bl指令

这将错过任何使用b而不是bl的尾调用。这可能是好的,但如果不是这样,我看不到一个方法来修复它。无条件bl将在函数中到处都是。(使用一些启发式方法来识别函数,可以假定当前函数之外的b是tailcall,而其他函数不是,这是因为编译器通常会将单个函数的所有代码都连续起来(除非编译器认为某些代码块不太可能进入.text.cold部分)。

与x86不同,AArch 64具有需要对齐的固定宽度指令,因此编译器生成的指令的一致反汇编很容易。因此,您可以识别所有的bl指令。

但是,如果AArch 64编译器在函数之间混合了任何常量数据,就像32位ARM编译器所做的那样(用于PC相对加载的文字池),误报是可能的,即使您将其限制为查看可执行ELF部分中的二进制文件部分。(或者,如果部分标题已被剥离,则为程序段。)
我不认为bl会被用于编译器生成的代码中的函数调用以外的任何事情。(例如,不用于编译器发明的私有帮助函数。)
你可能需要一个库来帮助解析ELF头文件并找到正确的二进制偏移量。寻找bl指令可能是你通过扫描机器码而不是反汇编来完成的。
如果在汇编之前修改编译器的asm输出,那会使事情变得更容易;您可以添加指令作为调用点。但是对于现有的二进制文件,您不能从源代码编译。

相关问题