Может ли ptrace заставить отслеживаемый процесс выполнять системный вызов без доступа к исполняемой инструкции системного вызова? - PullRequest
0 голосов
/ 27 октября 2018

Рассмотрим эту простую программу, которая просто бесконечно зацикливается:

int main(void) {
        for(;;);
}

Достаточно просто использовать ptrace для ввода в нее системного вызова, например:

#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/user.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
        struct user_regs_struct regs;
        pid_t pid = strtol(argv[1], NULL, 10);
        ptrace(PTRACE_ATTACH, pid, 0, 0);
        waitid(P_PID, pid, NULL, WSTOPPED);
        ptrace(PTRACE_GETREGS, pid, 0, &regs);
        if(ptrace(PTRACE_POKETEXT, pid, (void*)regs.rip, (void*)0x050f /* the "syscall" instruction, in little-endian */)) {
                perror("PTRACE_POKETEXT");
                return 1;
        }
        regs.rax = SYS_exit;
        regs.rdi = 42;
        ptrace(PTRACE_SETREGS, pid, 0, &regs);
        ptrace(PTRACE_DETACH, pid, 0, 0);
        return 0;
}

Это введет системный вызов _exit(42); через бесконечный цикл. Это также возможно сделать, ища существующую инструкцию syscall, вместо того, чтобы просто перезаписывать, где указатель инструкции оказывается.

Теперь рассмотрим эту программу, которая также (после некоторой настройки) просто бесконечно зацикливается:

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/syscall.h>

struct mapping_list {
    void *start;
    size_t len;
    struct mapping_list *next;
};

typedef void unmap_all_t(struct mapping_list *list, void *start, size_t len);
extern unmap_all_t unmap_all;
extern const char unmap_all_end[];
__asm__("\n"
    "unmap_all:\n"
    "  movq %rsi, %r8 # save start\n"
    "  movq %rdi, %r9 # save list\n"
    ".unmap_list_element:\n"
    "  movq (%r9), %rdi # pass list->start as addr\n"
    "  movq 8(%r9), %rsi # pass list->len as length\n"
    "  movl $11, %eax # SYS_munmap\n"
    "  syscall\n"
    "  movq 16(%r9), %r9 # advance to the next list element\n"
    "  testq %r9, %r9\n"
    "  jne .unmap_list_element\n"
    "  movl $11, %eax # SYS_munmap\n"
    "  movq %r8, %rdi # pass start as addr\n"
    "  movq %rdx, %rsi # pass len as length\n"
    "  jmp .final_syscall\n"
    "  .org unmap_all+4094 # make sure the upcoming syscall instruction is at the very end of the page,\n"
    ".final_syscall:       # given that unmap_all started at the very beginning of it\n"
    "  syscall\n"
    ".loop_forever:\n"
    "  jmp .loop_forever\n"
    "unmap_all_end:\n"
);

int main(void) {
    FILE *maps = fopen("/proc/self/maps", "r");
    if(!maps) {
        perror("fopen");
        return 1;
    }

    struct mapping_list *list = NULL;
    unsigned long start, end;
    char r, w, x;
    while(fscanf(maps, "%lx-%lx %c%c%c", &start, &end, &r, &w, &x) == 5) {
        while(fgetc(maps) != '\n');
        if(x != 'x') continue;
        struct mapping_list *new_list = malloc(sizeof(struct mapping_list));
        new_list->start = (void*)start;
        new_list->len = end - start;
        new_list->next = list;
        list = new_list;
    }

    if(fclose(maps)) {
        perror("fclose");
        return 1;
    }

    int memfd = syscall(SYS_memfd_create, "unmap_all", 2 /* MFD_ALLOW_SEALING */);
    if(memfd == -1) {
        perror("memfd_create");
        return 1;
    }

    if(ftruncate(memfd, 8192)) {
        perror("ftruncate");
        return 1;
    }

    char *pages = mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, memfd, 0);
    if(pages == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    memcpy(pages, unmap_all, unmap_all_end - (const char*)unmap_all);

    if(munmap(pages, 8192)) {
        perror("munmap");
        return 1;
    }

    char *path;
    if(asprintf(&path, "/proc/self/fd/%d", memfd) == -1) {
        perror("asprintf");
        return 1;
    }

    int memfd_ro = open(path, O_RDONLY);
    if(memfd_ro == -1) {
        perror("open");
        return 1;
    }

    free(path);

    if(fcntl(memfd, 1033 /* F_ADD_SEALS */, 15 /* F_SEAL_SEAL|F_SEAL_SHRINK|F_SEAL_GROW|F_SEAL_WRITE */)) {
        perror("fcntl");
        return 1;
    }

    if(close(memfd)) {
        perror("close");
        return 1;
    }

    pages = mmap(NULL, 8192, PROT_READ|PROT_EXEC, MAP_SHARED, memfd_ro, 0);
    if(pages == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    if(close(memfd_ro)) {
        perror("close");
        return 1;
    }

    ((unmap_all_t*)pages)(list, pages, 4096);

    __builtin_unreachable();
}

Когда я пытаюсь использовать на нем свою программу ptrace, шаг PTRACE_POKETEXT для записи инструкции syscall завершается неудачно с ошибкой EIO, поскольку содержащая страница является общим отображением файла только для чтения. У меня также нет возможности найти существующую инструкцию syscall, поскольку все исполняемые страницы, кроме одной, не были отображены, а единственная оставшаяся нигде не содержит эту инструкцию.

Есть ли другой способ использовать ptrace, чтобы заставить эту программу выполнить системный вызов, или я сделал это совершенно невозможным? (Если это имеет значение, предположим, что Linux 4.19 на x86_64.)

Ответы [ 2 ]

0 голосов
/ 28 октября 2018

Может ли ptrace заставить отслеживаемый процесс выполнить системный вызов без доступа к исполняемой инструкции системного вызова?

Только если трассировщик может сгенерировать его, используя POKETEXT, используя текущие ядра основной линии имодули ядра.


Возможно, пришло время перечитать первый абзац в man 2 ptrace:

Системный вызов ptrace () предоставляет средствос помощью которого один процесс («трассировщик») может наблюдать и контролировать выполнение другого процесса («трассировщик»), а также проверять и изменять память и регистры трассировки.Он в основном используется для реализации отладки точек останова и трассировки системных вызовов.

Это инструмент для наблюдения за трассировкой, а не какой-то тюрьмой или злоумышленником, от которого должен защищаться процесс.

Возможно, есть и другие способы, с помощью которых трассировщик может установить не записываемое отображение исполняемого файла, удалить все остальные страницы и гарантировать, что на оставшихся исполняемых страницах ничего не будет содержаться последовательность, которую можно использовать для создания системного вызова.

И что?Такие случаи еще не появились на практике, или мы бы изменили средства трассировки , чтобы охватить и этот случай.

Если это реальная проблема, мне кажется, чтоНаилучшим подходом было бы добавить явное средство системного вызова в ptrace.Есть несколько вариантов, как это можно реализовать.

Таким образом, любой "Нет" ответ на поставленный вопрос должен быть изменен на ", мы можем добавить эту функцию, если это необходимоХотя ".Мне даже не нужно изменять какие-либо существующие ядра, я думаю;просто напишите вспомогательный модуль ядра, обеспечивающий необходимые возможности.

0 голосов
/ 28 октября 2018

Смысл печати в том, чтобы «доказать», что ptrace не должен автоматически разрешать запись страниц, доступных только для чтения

Печати связаны с обычным доступом к разделяемой памяти между процессами.

Как я уже упоминал в вашем другом вопросе относительно источника ядра:

ptrace доступ через PTRACE_POKETEXT отличается.Это полностью обходит защиту на данной странице.(т. е.) не ссылается на все, что связано с печатями.

Операция poketext обрабатывается совершенно другим кодом в ядре, а [своего рода] просто делает это через вызовы доступа к ВМ.

Я бы не слишком волновался об этом.

Возможно, вы посмотрите на CONFIG_HAVE_IOREMAP_PROT

...