我正在创建一个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 }},因为那里没有追踪器,但是规则被复制了。
答案 0 :(得分:0)
我找到了解决此问题的方法。要也为子进程设置跟踪程序,或者至少避免子子进程的ENOSYS
问题,我们可以在设置 ptrace <时指定PTRACE_O_TRACEFORK
和PTRACE_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);