我试图在x86_64(intel语法)中动态查找运行时调用和返回的函数的数量。
要做到这一点我使用ptrace(没有PTRACE_SYSCALL),并且我正在检查RIP注册表(包含下一个指令地址)并且我正在检查他的操作码。我知道如果LSB等于0xE8,则可以找到函数CALL(根据英特尔文档或http://icube-avr.unistra.fr/fr/images/4/41/253666.pdf第105页)。
我在http://ref.x86asm.net/coder64.html找到了每条指令,所以在我的程序中,每次我找到0xE8,0x9A,0xF1等...我找到了一个函数入口(CALL或INT指令),如果它&#39 ; sa 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
运行它时(它是一个自定义程序,只需输入某些本地函数并执行一些编写系统调用,目的只是跟踪此程序的输入/左侧函数的数量) ),没有错误发生,它工作正常,但我没有相同数量的CALL和RET。
为例:
使用者&gt; ./a.out basic_program
致电:636 ret:651
(大量的调用和撤销是由LibC引起的,他们在开始你的程序之前会有很多功能,请参阅Parsing Call and Ret with ptrace.)
实际上,就像我的程序比函数调用更多的返回,但我发现0xFF指令用于CALL或CALLF(r / m64或r / m16 / m32),但也用于其他像DEC,INC或JMP这样的指令(非常普通的指令)。
那么,我该如何区分呢?根据{{3}}使用&#34;操作码字段&#34;,但我怎样才能找到它?
如果我在我的条件中添加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;
如果我发布它:
使用者&gt; ./a.out basic_program
致电:1152 ret: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;
但它给了我同样的结果。我错了吗?如何找到相同数量的电话和转发?
答案 0 :(得分:4)
以下是如何编程的示例。请注意,由于x86指令最长可达16个字节,因此必须查看16个字节以确保获得完整的指令。每个peek读取8个字节,这意味着您需要查看两次,一次在regs.rip
,一次在8字节之后:
peek1 = ptrace(PTRACE_PEEKDATA, pid, regs.rip, NULL);
peek2 = ptrace(PTRACE_PEEKDATA, pid, regs.rip + sizeof(long), NULL);
请注意,此代码掩盖了有关如何处理前缀的大量详细信息,并将一堆无效指令检测为函数调用。进一步注意,如果要将其用于32位代码,则需要更改代码以包含更多CALL指令并删除REX前缀的检测:
int iscall(long peek1, long peek2)
{
union {
long longs[2];
unsigned char bytes[16];
} data;
int opcode, reg;
size_t offset;
/* turn peeked longs into bytes */
data.longs[0] = peek1;
data.longs[1] = peek2;
/* ignore relevant prefixes */
for (offset = 0; offset < sizeof data.bytes &&
((data.bytes[offset] & 0xe7) == 0x26 /* cs, ds, ss, es override */
|| (data.bytes[offset] & 0xfc) == 0x64 /* fs, gs, addr32, data16 override */
|| (data.bytes[offset] & 0xf0) == 0x40); /* REX prefix */
offset++)
;
/* instruction is composed of all prefixes */
if (offset > 15)
return (0);
opcode = data.bytes[offset];
/* E8: CALL NEAR rel32? */
if (opcode == 0xe8)
return (1);
/* sufficient space for modr/m byte? */
if (offset > 14)
return (0);
reg = data.bytes[offset + 1] & 0070; /* modr/m byte, reg field */
if (opcode == 0xff) {
/* FF /2: CALL NEAR r/m64? */
if (reg == 0020)
return (1);
/* FF /3: CALL FAR r/m32 or r/m64? */
if (reg == 0030)
return (1);
}
/* not a CALL instruction */
return (0);
}
答案 1 :(得分:0)
我会亲自运行跟踪一条指令“迟到”,保留上一步中的rip
和rsp
。为简单起见,我们说curr_rip
和curr_rsp
是从rip
和rsp
以及{{PTRACE_GETREGS
获得的prev_rip
和prev_rsp
个寄存器1}}来自前一个。
如果(curr_rip < prev_rip || curr_rip > prev_rip + 16)
,则指令指针向后或向前移动超过最长有效指令的长度。如果是,那么:
如果(curr_rsp > prev_rsp)
,最后一条指令是某种ret
,因为数据也会从堆栈中弹出。
如果(curr_rsp < prev_rsp)
,最后一条指令是某种call
,因为数据也被推送到堆栈。
如果(curr_rsp == prev_rsp)
,指令是某种跳跃;无条件跳跃或分支。
换句话说,您只需要检查指令(curr_rip - prev_rip
字节,包括1到16之间的字节),从prev_rip
开始,(curr_rsp != prev_rsp && curr_rip > prev_rip && curr_rip <= prev_rip + 16
时。为此,我使用Intel XED,但您可以自由地实现自己的call / ret指令识别器。