Как я могу найти номер CALL и RET, используя ptrace? - PullRequest
0 голосов
/ 04 мая 2018

Я пытаюсь динамически найти номер функции, вызываемой и возвращаемой программой во время выполнения в x86_64 (синтаксис intel).

Для этого я использую ptrace (без PTRACE_SYSCALL), и я проверяю регистратор RIP (который содержит следующий адрес инструкции), и я проверяю его код операции. Я знаю, что функция CALL может быть найдена, если LSB равен 0xE8 (согласно документации Intel или http://icube -avr.unistra.fr / fr / images / 4/41 / 253666.pdf page 105).

Я нашел каждую инструкцию в 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, &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);
}

Когда я запустил его с a_program (это пользовательская программа, которая просто входит в какую-то локальную функцию и делает какую-то запись в системном вызове, цель состоит в том, чтобы просто отследить количество введенных / оставленных функций этой программы), ошибки не возникает , это работает нормально, НО у меня не одинаковое количество CALL и RET. Exemple:

пользователь> ./a.out basic_program

вызов: 636, рет: 651

(Большое количество вызовов и ретрансляторов вызвано тем, что LibC выполняет множество функций перед запуском вашей программы, см. Парсинг вызовов и ретрансляторов с помощью 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;

Если я запускаю его:

пользователь> ./a.out basic_program

вызов: 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;

Но это дало мне тот же результат. Я где то не прав? Как я могу найти один и тот же номер звонка и ответа?

Ответы [ 2 ]

0 голосов
/ 08 мая 2018

Я бы лично запустил трассировку одной инструкции "поздно", сохранив rip и rsp с предыдущего шага. Для простоты, скажем, curr_rip и curr_rsp - это регистры rip и rsp, полученные из самых последних PTRACE_GETREGS, а также 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), инструкция была своего рода прыжком; либо безусловный переход, либо ветка.

Другими словами, вам нужно только проверить инструкцию (из 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.

0 голосов
/ 07 мая 2018

Вот пример того, как это запрограммировать. Обратите внимание, что, поскольку инструкция x86 может иметь длину до 16 байтов, необходимо просмотреть 16 байтов, чтобы получить полную инструкцию. Поскольку каждый цикл считывает 8 байтов, это означает, что вам нужно просматривать дважды, один раз на regs.rip и один раз на 8 байтов позже:

peek1 = ptrace(PTRACE_PEEKDATA, pid, regs.rip, NULL);
peek2 = ptrace(PTRACE_PEEKDATA, pid, regs.rip + sizeof(long), NULL);

Обратите внимание, что в этом коде скрыты многие подробности о том, как обрабатываются префиксы, и обнаруживается множество неверных инструкций в виде вызовов функций. Также обратите внимание, что код необходимо изменить, чтобы включить в него еще несколько инструкций CALL и удалить обнаружение префиксов REX, если вы хотите использовать его для 32-битного кода:

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);
}
...