使用 ptrace 动态获取以字节为单位的指令大小
Dynamically get instruction size in bytes using ptrace
我正在使用 ptrace
来跟踪流程并监控其行为。在某些时候,我想在点击下一条指令之前获得下一个 rip
地址。事实上,我想在 callq
指令之后调用获取指令的地址。有几种不同的此类指令(近、远、相对、绝对等),它们的长度并不都相同。
有没有一种方法可以在检索到指令后使用 ptrace
获取指令的字节大小。类似于以下内容:
int ip = ptrace(PTRACE_PEEKUSER, t_pid, ipoffs, 0); // some addr where ip points
long isntruction = ptrace(PTRACE_PEEKTEXT, t_pid, ip, NULL); // e8 ae 72 f8 ff (relative call)
printf("Instruction is %d bytes", get_instruction_size(instruction)); // Instrction is 5 bytes
我猜测实现 get_instruction_size
的一种方法是获取指令的操作码(前 1 或 2 个字节),然后根据 x86 architecture/manual 确定它应该有多长.但我觉得会有很多特殊情况需要考虑,并且需要大量阅读才能找到价值 + 这将从一种 CPU 架构变为另一种架构。另一方面,动态查找大小似乎更方便。我还没有找到答案。
------ 编辑 ------
尝试在调用后立即从 rsp 检索 return 值:
#define M_OFFSETOF(STRUCT, ELEMENT) \
(unsigned long) &((STRUCT *)NULL)->ELEMENT;
...
ipoffs = M_OFFSETOF(struct user, regs.rip);
spoffs = M_OFFSETOF(struct user, regs.rsp);
...
while(1) {
// exec one instruction
if(ptrace(PTRACE_SINGLESTEP, t_pid, 0, signo) < 0){
perror("ptrace single step error\n");
exit(EXIT_FAILURE);
}
ip = ptrace(PTRACE_PEEKUSER, t_pid, ipoffs, 0);
full_instruction = ptrace(PTRACE_PEEKTEXT, t_pid, ip, NULL);
opcode = (unsigned)0xFF & full_instruction;
if(opcode == ADDR32){
opcode = ((unsigned)0xFF00 & full_instruction) >> 8;
}
if(call_found){
sp = ptrace(PTRACE_PEEKUSER, t_pid, spoffs, 0);
// print sp ...
call_found = false;
}
if(opcode == CALL)
call_found = true;
}
ptrace 在内核中没有反汇编程序1,硬件本身不会告诉你这一点,直到 call
指令执行后。
如果您可以等到指令执行之后,最好的选择可能是 PTRACE_SINGLESTEP
然后读取压入堆栈的 return 地址 call
。 (ESP/RSP会指向它2)。
另一种选择当然是自己解码(包括任何可能用于填充的前缀,例如 ld
将 6 字节 call [got_entry]
放宽为 1+5 字节 addr32 call rel32
).使用反汇编程序库,或者通过扫描前缀直到到达 call
操作码之一,然后您可以从它(E8 call rel32
)或从解码 ModRM 字节获得间接长度FF /2 call [r/m32]
。 (https://www.felixcloutier.com/x86/call).
如果你想移植到非 x86 ISA,许多人使用 link 寄存器而不是压入 return 地址,所以它不相同;您不能只是一般地取消引用具有 uintptr_t
宽度的堆栈指针。
并且其中许多 ISA 具有固定的指令宽度,因此您可以只向前执行一条指令,而不是单步执行和读取寄存器。 (尽管许多支持使用 2 或 4 字节指令的紧凑编码,例如 ARM Thumb、MIPS 和 RISC-V)。
一些 ISA 上还有其他问题,例如 MIPS 有一个分支延迟槽,所以 return 地址实际上在 next 指令之后 [=20] =].
脚注 1:(有趣的事实:ARM Linux 内核曾经有一个支持 某些 指令的反汇编程序,因此它可以为您模拟单步执行,但是 that hack was removed).
脚注 2:即使对于带有 far
调用的手写 asm,CS:[ER]IP return 地址的偏移部分也会位于最低地址,即指向ESP/RSP。当然,远调用使用不同的操作码,所以你可以单独对待它们或忽略它们。
我不确定前缀是否有可能以某种方式覆盖大小,从而使 call
推送不同大小的 return 地址。 (例如 32 位模式下的 16 位)。可能不是,即使是这样,它也只是恶意二进制文件试图故意欺骗您的跟踪器的一个问题。即使是 GNU/Linux 的手写 asm 也不太可能出于任何正常原因这样做。
我正在使用 ptrace
来跟踪流程并监控其行为。在某些时候,我想在点击下一条指令之前获得下一个 rip
地址。事实上,我想在 callq
指令之后调用获取指令的地址。有几种不同的此类指令(近、远、相对、绝对等),它们的长度并不都相同。
有没有一种方法可以在检索到指令后使用 ptrace
获取指令的字节大小。类似于以下内容:
int ip = ptrace(PTRACE_PEEKUSER, t_pid, ipoffs, 0); // some addr where ip points
long isntruction = ptrace(PTRACE_PEEKTEXT, t_pid, ip, NULL); // e8 ae 72 f8 ff (relative call)
printf("Instruction is %d bytes", get_instruction_size(instruction)); // Instrction is 5 bytes
我猜测实现 get_instruction_size
的一种方法是获取指令的操作码(前 1 或 2 个字节),然后根据 x86 architecture/manual 确定它应该有多长.但我觉得会有很多特殊情况需要考虑,并且需要大量阅读才能找到价值 + 这将从一种 CPU 架构变为另一种架构。另一方面,动态查找大小似乎更方便。我还没有找到答案。
------ 编辑 ------
尝试在调用后立即从 rsp 检索 return 值:
#define M_OFFSETOF(STRUCT, ELEMENT) \
(unsigned long) &((STRUCT *)NULL)->ELEMENT;
...
ipoffs = M_OFFSETOF(struct user, regs.rip);
spoffs = M_OFFSETOF(struct user, regs.rsp);
...
while(1) {
// exec one instruction
if(ptrace(PTRACE_SINGLESTEP, t_pid, 0, signo) < 0){
perror("ptrace single step error\n");
exit(EXIT_FAILURE);
}
ip = ptrace(PTRACE_PEEKUSER, t_pid, ipoffs, 0);
full_instruction = ptrace(PTRACE_PEEKTEXT, t_pid, ip, NULL);
opcode = (unsigned)0xFF & full_instruction;
if(opcode == ADDR32){
opcode = ((unsigned)0xFF00 & full_instruction) >> 8;
}
if(call_found){
sp = ptrace(PTRACE_PEEKUSER, t_pid, spoffs, 0);
// print sp ...
call_found = false;
}
if(opcode == CALL)
call_found = true;
}
ptrace 在内核中没有反汇编程序1,硬件本身不会告诉你这一点,直到 call
指令执行后。
如果您可以等到指令执行之后,最好的选择可能是 PTRACE_SINGLESTEP
然后读取压入堆栈的 return 地址 call
。 (ESP/RSP会指向它2)。
另一种选择当然是自己解码(包括任何可能用于填充的前缀,例如 ld
将 6 字节 call [got_entry]
放宽为 1+5 字节 addr32 call rel32
).使用反汇编程序库,或者通过扫描前缀直到到达 call
操作码之一,然后您可以从它(E8 call rel32
)或从解码 ModRM 字节获得间接长度FF /2 call [r/m32]
。 (https://www.felixcloutier.com/x86/call).
如果你想移植到非 x86 ISA,许多人使用 link 寄存器而不是压入 return 地址,所以它不相同;您不能只是一般地取消引用具有 uintptr_t
宽度的堆栈指针。
并且其中许多 ISA 具有固定的指令宽度,因此您可以只向前执行一条指令,而不是单步执行和读取寄存器。 (尽管许多支持使用 2 或 4 字节指令的紧凑编码,例如 ARM Thumb、MIPS 和 RISC-V)。
一些 ISA 上还有其他问题,例如 MIPS 有一个分支延迟槽,所以 return 地址实际上在 next 指令之后 [=20] =].
脚注 1:(有趣的事实:ARM Linux 内核曾经有一个支持 某些 指令的反汇编程序,因此它可以为您模拟单步执行,但是 that hack was removed).
脚注 2:即使对于带有 far
调用的手写 asm,CS:[ER]IP return 地址的偏移部分也会位于最低地址,即指向ESP/RSP。当然,远调用使用不同的操作码,所以你可以单独对待它们或忽略它们。
我不确定前缀是否有可能以某种方式覆盖大小,从而使 call
推送不同大小的 return 地址。 (例如 32 位模式下的 16 位)。可能不是,即使是这样,它也只是恶意二进制文件试图故意欺骗您的跟踪器的一个问题。即使是 GNU/Linux 的手写 asm 也不太可能出于任何正常原因这样做。