foo函数内部调用了一个sum函数。有一个扫描器函数可以读取 foo 函数中的指令。当扫描器遇到 FF 操作码时,它会转到操作数寄存器。如何从该寄存器获取函数地址。还是有不同的方法? 简而言之,我想检测传递给扫描仪函数的函数中是否调用了特定函数。例如检测 pf 中是否调用了 malloc。
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
//the function to call in foo
int sum(int x, int y){
return x + y;
}
//function pointer for absolute address
int(*funsum)(int, int) = sum;
//the function where funsum is called
void foo(){
int result = funsum(1, 1);
}
//To read insturactions in foo
void scanner(void *pf){
unsigned char *f = pf;
while (*f != 0xc3)//the loop continues until ret is encountered
{
if(*f == 0xff){//call encountered
unsigned char *reg = f + 1;
//in here how can i access the address of funsum()
}
f++;
}
}
int main(){
scanner(foo);
}
处理间接函数调用时,面临的挑战是 CALL 指令的地址存储在寄存器中,这使得它比直接函数调用更难以跟踪。让我们通过检查程序生成的汇编代码来深入了解如何在扫描仪功能中处理此问题。
在您的程序中,对 main 中的 Scanner() 的直接函数调用很容易跟踪。例如:
00000000000011a7 <main>:
11a7: f3 0f 1e fa endbr64
11ab: 55 push %rbp
11ac: 48 89 e5 mov %rsp,%rbp
11af: 48 8d 05 8b ff ff ff lea -0x75(%rip),%rax # 1141 <foo>
11b6: 48 89 c7 mov %rax,%rdi
11b9: e8 a8 ff ff ff call 1166 <scanner>
直接引用CALL指令中的地址。然而,在 foo 中,函数调用是间接的:
0000000000001141 <foo>:
1141: f3 0f 1e fa endbr64
1145: 55 push %rbp
1146: 48 89 e5 mov %rsp,%rbp
1149: 48 83 ec 10 sub $0x10,%rsp
114d: 48 8b 05 bc 2e 00 00 mov 0x2ebc(%rip),%rax # 4010 <funsum>
<some instructions omitted for clarity>
115e: ff d0 call *%rax
这里,地址在 CALL 指令之前加载到 RAX 中。为了追踪这一点,您需要计算被调用函数的有效地址。
scanner
来处理间接调用以下是如何修改扫描仪功能来处理此问题:
void scanner(void *pf) {
unsigned char *f = (unsigned char *)pf;
uintptr_t address_in_rax = 0;
while (*f != 0xc3) { // Loop continues until RET (0xC3) is encountered
// Check for "MOV RAX, [RIP+offset]" instruction
if (*f == 0x48 && *(f + 1) == 0x8b && *(f + 2) == 0x05) {
// Calculate the offset (signed 32-bit value) from the next instruction
int32_t offset = *(int32_t *)(f + 3);
uintptr_t effective_address = (uintptr_t)(f + 7) + offset;
// Dereference to get the address being loaded into RAX
address_in_rax = *(uintptr_t *)effective_address;
printf("Address loaded into RAX (funsum address): %p\n", (void *)address_in_rax);
// Verify the address matches the actual function pointer
if (address_in_rax == (uintptr_t)funsum) {
printf("Verified: Address matches the funsum function pointer.\n");
} else {
printf("Warning: Address does not match the funsum function pointer!\n");
}
}
// Check for "CALL RAX" instruction
if (*f == 0xff && (*(f + 1) & 0xf8) == 0xd0) {
printf("CALL instruction found. Address in RAX: %p\n", (void *)address_in_rax);
}
f++;
}
}
此代码检查将地址加载到 RAX 中然后调用它的特定指令序列。它处理相对于 RIP 计算地址的情况。当然,您可以进一步扩展它以匹配不同的情况。
请记住,编译器优化可以改变函数的调用方式。例如,在不同的优化级别(-O1、-O2、-O3)下,
foo
中的函数调用可能会被优化掉或替换为不同的指令(我用 gcc 11 for x64 检查了这一点):
在-O1处,直接在CALL指令中计算偏移量。 在 -O2 及以上,该函数可能会被内联或替换为 JMP 指令。
为了处理这些情况,您可能需要进一步扩展扫描器功能,但随后您就可以有效地构建反编译器。
显然,检测特定函数是否被直接或间接调用可能非常具有挑战性。以下是您可以考虑的一些方法:
使用调试符号:如果二进制文件包含调试符号,您可以使用 gdb 等调试器在感兴趣的函数上设置断点。如果函数被优化或者二进制文件是在没有调试符号的情况下编译的,那么这将不起作用。
解释汇编:如果无法通过调试符号直接跟踪函数,则需要解释汇编代码,正如我们之前讨论的那样。这涉及手动跟踪代码、跟踪寄存器值以及了解函数指针的使用方式。此过程可能会变得复杂,尤其是在大型程序中或启用高级编译器优化时。
处理混淆:如果二进制文件被故意混淆,检测函数调用就会变得更加困难。混淆技术可能会伪装函数调用、模糊控制流,甚至修改函数本身。在这种情况下,标准调试和静态分析工具可能不够。可能需要先进的逆向工程技术或专用工具来分析二进制文件。
二进制分析工具:有一些专门为静态和动态二进制分析而设计的工具,例如 Ghidra,可以帮助识别函数调用,即使在优化或混淆的二进制文件中也是如此。这些工具可以自动化解释汇编代码所涉及的一些手动工作,从而更容易识别函数是否被调用。
通往罗马的方式有很多种,所以这完全取决于您从哪里开始。希望这有帮助,祝你好运!