如何使用ptrace找到CALL和RET编号?

Vol*_*ple 6 c assembly ptrace elf opcode

我试图在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指令),如果它是0xC2,0XC3等......它是一个函数leave(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, &regs);
                    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);
}
Run Code Online (Sandbox Code Playgroud)

当我运行它时a_program(它是一个自定义程序,只需输入某些本地函数并执行一些编写系统调用,目标只是跟踪此程序的输入/左侧函数的数量),没有错误发生,它工作正常,但是我没有相同数量的CALL和RET.为例:

user> ./a.out basic_program

电话:636 ret:651

(大量的call和ret是由LibC引起的,他们在启动你的程序之前会进入很多功能,请参阅Parsing Call和Ret with ptrace.)

实际上,就像我的程序比函数调用更多的返回,但我发现0xFF指令用于CALL或CALLF(r/m64或r/m16/m32),但也用于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;
Run Code Online (Sandbox Code Playgroud)

如果我启动它:

user> ./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;
Run Code Online (Sandbox Code Playgroud)

但它给了我同样的结果.我错了吗?如何找到相同数量的呼叫和转发?

fuz*_*fuz 5

这是一个如何编程的示例。请注意,由于 x86 指令最长可达 16 个字节,因此必须查看 16 个字节才能确保获得完整的指令。由于每次 peek 读取 8 个字节,这意味着您需要 peek 两次,一次一次,regs.rip然后一次 8 个字节:

peek1 = ptrace(PTRACE_PEEKDATA, pid, regs.rip, NULL);
peek2 = ptrace(PTRACE_PEEKDATA, pid, regs.rip + sizeof(long), NULL);
Run Code Online (Sandbox Code Playgroud)

请注意,这段代码掩盖了很多关于如何处理前缀的细节,并将一堆无效指令检测为函数调用。进一步注意,如果要将其用于 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);
}
Run Code Online (Sandbox Code Playgroud)

  • 如果在您链接的目标文件中找到“foo”,则“ld”会将“gcc -fno-plt”的“call *foo@GOTPCREL(%rip)”放宽为“67 call foo”。请参阅“67 e8”的[Can't call C standard library function on 64-bit Linux from assembly (yasm) code](/sf/answers/3649176611/)底部的示例作为开头一个“调用 rel32”。(@保罗-玛丽)。在我的 Arch GNU/Linux 系统上,该机器代码在反汇编一些真实二进制文件(如 objdump -drwC -Mintel /bin/bash | )时并不罕见。grep '67 e8'`。但在其他二进制文件中则更为罕见;取决于编译/链接选项,也许还有源。 (2认同)
  • @Paul-Marie:我只是联系了你,以防你仍在使用这个确切的代码来做任何事情,并且想要修复可能的误报。如果您对这个问题不再感兴趣,也不必担心;主要针对模糊和未来的读者。 (2认同)