【问题标题】:Can't trace a subprocess's syscalls which calls execve using ptrace and seccomp无法跟踪使用 ptrace 和 seccomp 调用 execve 的子进程的系统调用
【发布时间】:2019-09-25 05:29:08
【问题描述】:

我正在创建一个syscall tracer using seccomp。我没有更改系统调用中的任何内容,我只是将其记录在我的结构中,当进程完成时 - 我将这个结构转储到磁盘上。

当我像这样运行我的程序时(它被称为 tracer):

跟踪环境

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

tracer watch -n1 env

tracer strace -o /tmp/log 环境

标准输出失败

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 规则都被复制并且孩子中没有跟踪器,所以子孩子中的每个系统调用都返回 -ENOSYS 因为没有tracer 那里,但是,规则被复制。

【问题讨论】:

  • 可能是 message traffic from execve needs to be handled 导致问题的方式。
  • @ryyker 谢谢帮忙,但是不知道这两个问题是怎么联系的?请你澄清一下好吗?
  • 他们可能不是。这是在黑暗中拍摄的。鉴于您的环境和条件不太可能轻易重现,剩下的就是试图从用于描述问题的词语中推断出因果关系,例如:however ... with execve,它在您帖子的第一部分失败。我看到您编辑了似乎是关于问题性质的伪结论?

标签: c linux


【解决方案1】:

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

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_O_TRACEVFORK 被设置,PTRACE_EVENT_VFORK 将被传递;否则,如果被跟踪者调用 clone(2) 并将退出信号设置为SIGCHLD,如果设置了PTRACE_O_TRACE‐FORK,则PTRACE_EVENT_FORK 将被传递。

它在我的情况下起作用的原因是,在简单的克隆之后,seccomp 规则被复制到克隆的进程中,但跟踪器没有。通过指定这些标志,父进程自动成为每个子进程的跟踪器,因此,随着规则被复制和跟踪器被指定,一切都像魅力一样工作。

注意 由于使用这种方式父进程成为跟踪器,您还需要等待所有子进程和子子进程,而不仅仅是您实际生成的进程。为此,请在 waitpid 或类似的系统调用中使用 -1 作为 pid 参数:

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

【讨论】:

    猜你喜欢
    • 2012-04-03
    • 2011-07-16
    • 2012-06-20
    • 1970-01-01
    • 2012-11-11
    • 1970-01-01
    • 2012-04-03
    • 2016-11-13
    • 2017-01-04
    相关资源
    最近更新 更多