ptrace可以使被跟踪的进程执行syscall而不访问可执行的syscall指令吗?

时间:2018-10-27 16:50:04

标签: c linux system-calls ptrace

考虑这个无限循环的简单程序:

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程序时,写syscall指令的PTRACE_POKETEXT步骤失败,并显示错误EIO,因为包含的页面是只读文件的共享映射。我也没有选择找到现有的syscall指令的选项,因为除一个可执行页面外,所有可执行页面均未映射,并且剩余的唯一可执行页面在任何地方都不包含该指令。

是否还有其他方法可以使用ptrace来使该程序执行系统调用,或者使我完全无法做到这一点? (如果重要的话,请在x86_64上使用Linux 4.19。)

2 个答案:

答案 0 :(得分:1)

  

ptrace是否可以使被跟踪的进程执行系统调用而不访问可执行的系统调用指令?

仅当跟踪器可以使用POKETEXT并使用当前的主线内核和内核模块来生成跟踪器时。


也许是时候重新阅读man 2 ptrace中的第一段了:

  

ptrace()系统调用提供了一种方法,通过该方法一个进程(     “示踪剂”)可以观察并控制另一个进程的执行     (“跟踪”),并检查和更改跟踪的内存,以及     寄存器。它主要用于实现断点调试     和系统调用跟踪。

它是一种用于观察控制示踪的工具,而不是某种监狱,也不是进程应保护自己免受攻击的攻击者。

示踪者可能还有其他方法可以设置不可写的可执行程序映射,取消所有其他页面的映射,并确保剩余的可执行页面中没有任何内容包含可用于构造系统调用的序列。

那又怎样?这样的案例在实践中还没有出现,或者我们已经修改了ptrace facilities来涵盖该案例。

如果这是一个实际问题,在我看来,最好的方法是在ptrace中添加一个显式的syscall工具。有多种方法可以实现它。

因此,对上述问题的任何“否” 答案都必须修改为“尽管需要,我们也可以添加该功能” 。我认为,我们甚至不需要修改任何现有内核。只需编写一个提供所需功能的辅助内核模块即可。

答案 1 :(得分:0)

  

印章的目的是“证明” ptrace不应自动允许写入只读页面

密封与进程之间的正常共享内存访问有关。

正如我在另一个问题上提到的,关于内核源代码:

通过ptrace

PTRACE_POKETEXT访问是不同的。它完全绕过给定页面上的保护。 (即)它引用与印章有关的任何内容。

poketext操作由内核中完全不同的代码处理,[某种]只是通过对VM的访问来完成的。

我不会太担心。

您可以看看CONFIG_HAVE_IOREMAP_PROT