我试图动态地找到一个程序在x86_64(英特尔语法)运行时调用和返回的函数的数量。
为此,我使用了ptrace(不带PTRACE_SYSCALL),并检查了RIP寄存器(包含下一个指令地址)和操作码。我知道如果LSB等于0xE8(根据Intel文档,或http://icube-avr.unistra.fr/fr/images/4/41/253666.pdf第105页),则可以找到函数CALL。
我在http://ref.x86asm.net/coder64.html上找到了每条指令,因此在我的程序中,每次找到0xE8、0x9A、0xF1等时,我都找到了函数入口(CALL或INT指令),如果是0xC2、0xC3等,则是函数离开(RET指令)。
我们的目标是在每个程序运行时找到它,我不能访问测试程序的编译,检测或使用gcc的魔术工具。
我做了一个小程序,可以用gcc -Wall -Wextra your_file.c
编译,输入./a.out a_program
即可启动。
下面是我的代码:
#include <sys/ptrace.h>
#include <sys/signal.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <stdint.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
typedef struct user_regs_struct reg_t;
static int8_t increase(pid_t pid, int32_t *status)
{
if (WIFEXITED(*status) || WIFSIGNALED(*status))
return (-1);
if (WIFSTOPPED(*status) && (WSTOPSIG(*status) == SIGINT))
return (-1);
if (ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL) == -1)
return (-1);
return (0);
}
int main(int argc, char *argv[])
{
size_t pid = fork();
long address_rip;
uint16_t call = 0;
uint16_t ret = 0;
int32_t status;
reg_t regs;
if (!pid) {
if ((status = ptrace(PTRACE_TRACEME, 0, NULL, NULL)) == -1)
return (1);
kill(getpid(), SIGSTOP);
execvp(argv[1], argv + 1);
} else {
while (42) {
waitpid(pid, &status, 0);
ptrace(PTRACE_GETREGS, pid, NULL, ®s);
address_rip = ptrace(PTRACE_PEEKDATA, pid, regs.rip, NULL);
address_rip &= 0xFFFF;
if ((address_rip & 0x00FF) == 0xC2 || (address_rip & 0x00FF) == 0xC3 ||
(address_rip & 0x00FF) == 0xCA || (address_rip & 0x00FF) == 0xCB ||
(address_rip & 0x00FF) == 0xCF)
ret += 1;
else if ((address_rip & 0x00FF) == 0xE8 || (address_rip & 0x00FF) == 0xF1 ||
(address_rip & 0x00FF) == 0x9A || (address_rip & 0x00FF) == 0xCC ||
(address_rip & 0x00FF) == 0xCD || (address_rip & 0x00FF) == 0xCF)
call += 1;
if (increase(pid, &status) == -1) {
printf("call: %i\tret: %i\n", call, ret);
return (0);
}
}
}
return (0);
}
当我运行它与a_program
(这是一个自定义程序谁只是进入一些本地函数,并做一些写系统调用,目标只是跟踪这个程序的进入/离开函数的数量),没有发生错误,它的工作很好,但我没有相同数量的调用和RET.例如:
user〉./a.输出基本程序
电话:636回复:651
(The大量的调用和重试是由LibC在启动程序之前进入大量函数引起的,请参见Parsing Call and Ret with ptrace.)
实际上,我的程序返回的比函数调用的多,但我发现0xFF指令用于(r/m64或r/m16/m32)中的CALL或CALLF,但也用于其他指令,如DEC、INC或JMP(非常常见的指令)。
那么,我如何区分它呢?根据http://ref.x86asm.net/coder64.html和"操作码字段",但我如何找到它呢?
如果我将0xFF添加到条件中:
else if ((address_rip & 0x00FF) == 0xE8 || (address_rip & 0x00FF) == 0xF1 ||
(address_rip & 0x00FF) == 0x9A || (address_rip & 0x00FF) == 0xCC ||
(address_rip & 0x00FF) == 0xCD || (address_rip & 0x00FF) == 0xCF ||
(address_rip & 0x00FF) == 0xFF)
call += 1;
如果我启动它:
user〉./a.输出基本程序
电话:1152回复:651
我觉得这很正常,因为它会对每个JMP、DEC或INC进行计数,所以我需要区分每个0xFF指令。
else if ((address_rip & 0x00FF) == 0xE8 || (address_rip & 0x00FF) == 0xF1 ||
(address_rip & 0x00FF) == 0x9A || (address_rip & 0x00FF) == 0xCC ||
(address_rip & 0x00FF) == 0xCD || (address_rip & 0x00FF) == 0xCF ||
((address_rip & 0x00FF) == 0xFF && ((address_rip & 0x0F00) == 0X02 ||
(address_rip & 0X0F00) == 0X03)))
call += 1;
但是它给了我同样的结果,我是不是哪里错了,我怎么才能找到相同的call和ret号码呢?
2条答案
按热度按时间crcmnpdw1#
下面是一个编程示例。注意,由于x86指令最长可达16个字节,因此必须查看16个字节以确保获得完整的指令。由于每次查看读取8个字节,这意味着您需要查看两次,一次在
regs.rip
,另一次在8个字节之后:请注意,这段代码忽略了很多关于如何处理前缀的细节,并检测到一堆无效指令作为函数调用。进一步注意,如果您想将其用于32位代码,则需要更改代码,以纳入更多CALL指令,并删除对雷克斯前缀的检测:
flvlnr442#
我个人会“晚”运行跟踪一条指令,保留上一步中的
rip
和rsp
。为了简单起见,假设curr_rip
和curr_rsp
是从最近的PTRACE_GETREGS
获得的rip
和rsp
寄存器,以及从上一步获得的prev_rip
和prev_rsp
寄存器。如果
(curr_rip < prev_rip || curr_rip > prev_rip + 16)
,则指令指针向后或向前移动的距离大于最长有效指令的长度。如果是,则:(curr_rsp > prev_rsp)
,则最后一条指令是某种类型的ret
,因为数据也从堆栈中弹出。(curr_rsp < prev_rsp)
,则最后一条指令是某种类型的call
,因为数据也被压入堆栈。(curr_rsp == prev_rsp)
,则该指令是某种跳转;无条件跳转或分支。换句话说,您只需要检查从
prev_rip
开始的指令(curr_rip - prev_rip
字节,介于1和16之间,包括1和16),当(curr_rsp != prev_rsp && curr_rip > prev_rip && curr_rip <= prev_rip + 16
)时,我将使用Intel XED,当然,您可以实现自己的call/ret指令识别器。