无法跟踪使用ptrace和seccomp调用execve的子进程的系统调用

时间:2019-05-07 14:08:00

标签: c linux

我正在创建一个syscall tracer using seccomp。我不会在系统调用中进行任何更改,只是将其记录在我的结构中,并且在过程完成时-我将此结构转储到磁盘上。

当我这样运行程序时(称为 tracer ):

  

tracer env

一切正常,之后我在文件中看到了日志。 但是,如果我尝试跟踪一个在内部调用execve的程序,它将失败:

  

tracer watch -n1 env

  

tracer strace -o / tmp / log env

使用标准输出失败

  

env:加载共享库时出错:无法为搜索路径创建缓存:无法分配内存

和日志:

$ cat /tmp/log
execve("/usr/bin/env", ["env"], [/* 19 vars */]) = 0
brk(NULL)                               = 0x415000
mmap(0xffffffffffffffda, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x2
writev(103, [{iov_base="env", iov_len=3}, {iov_base=": ", iov_len=2}, {iov_base="error while loading shared libraries", iov_len=36}, {iov_base=": ", iov_len=2}, {iov_base="", iov_len=0}, {iov_base="", iov_len=0}, {iov_base="cannot create cache for search path", iov_len=35}, {iov_base=": ", iov_len=2}, {iov_base="Cannot allocate memory", iov_len=22}, {iov_base="\n", iov_len=1}], 10) = 127
+++ exited with 127 +++

请注意奇怪的mmap地址及其返回值。我不明白哪里出了问题以及为什么会这样。任何其他程序都可以正常工作,所以我想问题出在将seccomp过滤器复制到调用execve的派生进程中。

这是我的seccomp规则:

struct sock_filter filter[] = {
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(struct seccomp_data, nr)),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_openat, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_write, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_mmap, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_mprotect, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_close, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
};

我没有列出整个代码,因为显而易见,它只能以一种方式编写,而且它是在我上面提到的文章中编写的。也知道该问题in the Internet,但是我找不到任何解决方案。如果您仍然坚持使用完整的代码(我对此表示怀疑)或MCVE,则可以提供。

此外,当我添加execve跟踪时,我有不同的行为:

struct sock_filter filter[] = {
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(struct seccomp_data, nr)),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_openat, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_write, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_mmap, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_mprotect, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_close, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_execve, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
};

日志变为:

$ cat /tmp/log
execve(0xffffffffffffffda, ["env"], [/* 19 vars */]) = -1 ENOSYS (Function not implemented)
getpid()                                = 15535
exit_group(1)                           = ?
+++ exited with 1 +++
  

Linux 4.4 aarch64,Linux 4.15 x86-64

我花在这个问题上的时间越多,我越意识到问题实际上出在内核的源代码中。 It copies the filters from one process to another,一个孩子,但是他们不复制实现,因此所有SECCOMP_RET_TRACE规则都被复制,并且该孩子中没有跟踪程序,因此子孩子中的每个系统调用都返回{{1 }},因为那里没有追踪器,但是规则被复制了。

1 个答案:

答案 0 :(得分:0)

我找到了解决此问题的方法。要也为子进程设置跟踪程序,或者至少避免子子进程的ENOSYS问题,我们可以在设置 ptrace <时指定PTRACE_O_TRACEFORKPTRACE_O_TRACECLONE标志/ strong>这样的选项:

ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACESECCOMP | PTRACE_O_TRACEFORK | PTRACE_O_TRACECLONE);

我们需要同时添加两者的原因不容易简要地解释。首先,体系结构和 libc 依赖于系统中存在哪些系统调用,以及哪些程序被程序使用(通常通过 libc 实现)。也许,即使这个列表还不完整:我们可能还必须跟踪VFORK以及与克隆(或产生)线程或进程有关的其他方式(请记住,线程是Linux中的轻量级进程)。因此,这些选项的作用在man中指定:

  

PTRACE_O_TRACECLONE(从Linux 2.5.46开始)   在下一个克隆(2)上自动停止跟踪   开始跟踪新克隆的过程,该过程将   以SIGSTOP开头,如果为PTRACE_EVENT_STOP   使用了PTRACE_SEIZE。跟踪程序的waitpid(2)将   返回这样的状态值

status>>8 == (SIGTRAP | (PTRACE_EVENT_CLONE<<8))
     

可以使用PTRACE_GETEVENTMSG来检索新进程的PID。此选项可能无法在所有情况下都捕获clone(2)调用。如果被跟踪者使用CLONE_VFORK标志调用clone(2),则将设置PTRACE_EVENT_VFORK来传递PTRACE_O_TRACEVFORK;否则,如果跟踪者在退出信号设置为SIGCHLD的情况下调用clone(2),则在设置PTRACE_EVENT_FORK的情况下将传递PTRACE_O_TRACE‐FORK

在我的情况下它起作用的原因是,在简单克隆之后,seccomp规则被复制到了所克隆的进程中,而示踪剂却没有。通过指定这些标志,父进程将自动成为每个子进程的跟踪器,因此,当复制规则并指定跟踪器时,一切都像一个超级按钮一样工作。

注意 使用父方法成为跟踪程序后,您还需要等待所有子代和子子代,而不仅是实际生成的过程。为此,请在-1或类似的系统调用中将waitpid用作pid参数:

const pid_t childWaited = waitpid(-1, &status, 0);
// but not const pid_t result = waitpid(myChildPid, &status, 0);