【问题标题】:Dynamically get instruction size in bytes using ptrace使用 ptrace 动态获取指令大小(以字节为单位)
【发布时间】:2021-03-27 16:15:15
【问题描述】:

我正在使用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 架构/手册确定它应该多长时间。但我觉得会有很多特殊情况需要考虑,并且需要大量阅读以找到值+这将从一种 CPU 架构变为另一种。另一方面,动态查找大小似乎更方便。我还没有找到答案。

----- 编辑 -------

尝试在调用后立即从 rsp 检索返回值:

#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;
}

【问题讨论】:

  • 但是,如果您想要移植到非 x86 ISA,许多使用链接寄存器而不是推送返回地址。 OTOH,其中许多具有固定的指令宽度。不过,还有其他问题,比如 MIPS 有一个分支延迟槽,因此返回地址实际上是在 jal 之后的 next 指令之后。当然,这与您的标题问题相去甚远。找到通话说明后,您真正需要/想要做什么?
  • lDebug 的 P 命令会进行一些有限的反汇编以计算出调用指令的大小。这仅适用于 16 位和 32 位模式,但可以扩展到 64 位。这是实现此反汇编的the handler in run.asm。这里是the ppbytes and ppinfo tables,用于调度大部分前缀和操作码。
  • @PeterCordes 正如我所料,这比我想要的要复杂得多。现在我只需要 x86-32 上的相对(近或远)调用的返回地址,但我想尽可能地通用。检索 SP 指向的地址听起来是最好的选择。
  • re: 更新:评论说“打印 sp”,但 RSP 指向返回地址。你需要在那个地址偷看。 (此外,您在编辑中写了“返回值”,而不是“地址”。)此外,您必须在 RIP 指向调用时再执行一次 SINGLESTEP,以使其实际运行调用并推送返回地址。看起来您只检查一个 call 操作码。间接调用是FF /2,所以需要检查2个字节(屏蔽掉ModRM中的其他位)

标签: assembly x86 ptrace


【解决方案1】:

ptrace 在内核中没有反汇编程序1,硬件本身不会告诉你这一点,直到call 指令执行完毕。

如果您可以等到指令执行后,您最好的选择可能是PTRACE_SINGLESTEP 然后读取返回地址call 压入堆栈。 (ESP/RSP 将指向它2)。


当然,另一种选择是自己解码(包括任何可能用于填充的前缀,例如 ld 将 6 字节 call [got_entry] 放宽为 1+5 字节 addr32 call rel32 时)。使用反汇编程序库,或者通过扫描前缀直到您到达call 操作码之一,然后您可以从中获得长度(E8 call rel32)或从间接 FF /2 @ 解码 ModRM 字节987654331@。 (https://www.felixcloutier.com/x86/call)。


如果您想要移植到非 x86 ISA,许多人使用链接寄存器而不是推送返回地址,因此它并不相同;你不能只是一般地用uintptr_t宽度去引用堆栈指针。

其中许多 ISA 具有固定的指令宽度,因此您可以只向前执行一条指令,而不是单步执行和读取寄存器。 (尽管许多支持 2 或 4 字节指令的紧凑编码,例如 ARM Thumb、MIPS 和 RISC-V)。

在某些 ISA 上还有其他问题,例如 MIPS 有一个分支延迟槽,因此返回地址实际上是在 jal 之后的 next 指令之后。


脚注 1:(有趣的事实:ARM Linux 内核曾经有一个支持一些指令的反汇编程序,所以它可以为你模拟单步,但是that hack was removed)。

脚注 2:即使对于带有 far 调用的手写 asm,CS:[ER]IP 返回地址的偏移部分也会位于最低地址,即 ESP/RSP 指向的地址。当然,far 调用使用不同的操作码,因此您可以分别对待它们或忽略它们。

我不确定前缀是否可以覆盖大小以使call 推送不同大小的返回地址。 (例如 32 位模式下的 16 位)。可能不会,即使是这样,它也只是恶意二进制文件故意欺骗你的跟踪器的一个问题。出于任何正常原因,即使是为 GNU/Linux 手写的 asm 也几乎不可能做到这一点。

【讨论】:

  • 您能否提供一个简短的示例,用于使用 ptrace 使 rsp 的返回值达到峰值,我刚刚尝试过,我得到了一些奇怪的地址,我什至在二进制文件上使用 objdump -D 都找不到这些地址。这将非常有帮助!
  • 我确实认为 32 位 CS 中的 o16 call rel16 会推送一个字返回地址,正如您最后建议的那样。但确实会很不寻常。
  • @ecm:是的,我测试过了。
  • @Desperados:由于 ASLR,您应该期望在运行时观察到的地址与二进制文件中的内容无关。
  • @Desperados:那么您的 ptrace 几乎可以肯定是错误的(或者内核是错误的),或者没有完全按照您的描述进行操作;在函数的顶部(即在call 之后),mov rax, qword [rsp] 将加载返回地址。 ret 会将其弹出到 RIP 中;一个空函数只能包含一个ret,而ret 是我们在x86 上拼写pop rip 的方式。 (对于 uint32_t 或 uint16_t 地址,这同样适用于 32 位或 16 位模式。)
猜你喜欢
  • 2010-09-17
  • 2016-04-08
  • 2013-02-01
  • 1970-01-01
  • 2013-02-06
  • 1970-01-01
  • 1970-01-01
  • 2021-12-15
相关资源
最近更新 更多