ptrace 可以使被跟踪的进程执行系统调用吗,即使它没有访问可执行的系统调用指令?

3

考虑这个简单的程序,它只是无限循环:

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步骤写入系统调用指令时会出现错误EIO,因为所包含的页面是只读文件的共享映射。我也没有找到现有syscall指令的选项,因为除了一个可执行页面之外的所有页面都已被取消映射,而唯一剩下的页面并不包含任何该指令。
是否有其他方法使用ptrace使该程序执行系统调用,或者我已经完全做不到了?(如果有关系,请假设是Linux 4.19在x86_64上。)

2
从被追踪进程的内存中节省一些字节,将您的指令写入其中,执行系统调用,恢复保存的字节。 - n. m.
根据n.m.的指南,在附加到进程后,可以使用PTRACE_PEEKTEXTPTRACE_POKETEXTPTRACE_GETREGSPTRACE_SETREGS来设置系统调用。之后,在实际注入的syscall指令上执行PTRACE_SINGLESTEP。然后,恢复所有内容。这里有一个帮助库:https://github.com/emptymonkey/ptrace_do,其中包含一些代码。请注意,您不需要查找现有的`syscall`,只需要在文本段中找到足够的空间来注入一个(例如两个字节就足够了)。 - Craig Estey
@n.m. 唯一可执行的页面是不可写的。ptrace 是否提供了一些方法来写入 PROT_READ|PROT_EXECMAP_PRIVATE|MAP_ANONYMOUS 页面? - Joseph Sible-Reinstate Monica
@CraigEstey 我稍微修改了示例程序。现在,如果PTRACE_POKETEXT能够写入可执行页面,那么它将违反F_SEAL_WRITE提供的保证。 - Joseph Sible-Reinstate Monica
@CraigEstey,被追踪程序隐式地信任追踪程序,但有可能(尽管此程序不这样做)与另一个程序共享memfd,其中一个程序不会信任追踪程序(或追踪程序本身,因此存在密封的原因)。密封的目的是“证明”ptrace不应自动允许写只读页面。 - Joseph Sible-Reinstate Monica
显示剩余6条评论
2个回答

1
封印的目的是“证明”ptrace不应自动允许写入只读页面。
封印与进程之间的普通共享内存访问有关。
正如我在你的另一个问题中提到的那样,关于内核源代码:
通过PTRACE_POKETEXT访问ptrace是不同的。它完全绕过了给定页面上的保护。(即)它不涉及任何与封印相关的内容。 poketext操作由内核中完全不同的代码处理,并且通过对VM的访问调用来实现。
不必太担心。
您可以查看CONFIG_HAVE_IOREMAP_PROT。

1
使用seals会导致PTRACE_POKETEXT技巧写入syscall指令失败,返回EIO。我将我的尝试细节编辑到问题中,并成功地对一个不使用seals的程序进行了测试,证明正是seals破坏了它。 - Joseph Sible-Reinstate Monica

0
“ptrace”能否使被跟踪的进程执行系统调用,而无需访问可执行的系统调用指令?
只有当跟踪程序可以使用“POKETEXT”生成一个系统调用指令时,才能实现该功能。这适用于当前主线内核和内核模块。
也许现在是重新阅读man 2 ptrace第一段的时候了:

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

这是一种观察和控制tracee的工具,而不是某种监狱,也不是进程应该保护自己免受攻击者的工具。
可能还有其他方法可以设置一个不可写的可执行映射,取消所有其他页面的映射,并确保可执行页面中没有留下可用于构造系统调用的序列。
那又怎样?这种情况在实践中还没有出现过,否则我们会修改ptrace facilities以涵盖这种情况。
如果这是一个真正的问题,我认为最好的方法是将一个显式的系统调用功能添加到ptrace中。有许多选项可以实现它。
所以,对于所述问题的任何“否”答案都必须加上“如果需要,我们可以添加该功能”。我认为我们甚至不需要修改任何现有的内核;只需编写一个帮助程序内核模块即可提供所需的功能。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接