今年も
id:nanuyokakinuさんによる年賀状CTFが開催されていたので参加した.
reversing問題が全部で3問.すべて解くと最後のフラグにAmazonのギフト券が書いてあり,お年玉が貰える.今年はなんと0x1337円.ありがとうございました :)
nanuyokakinu.hatenablog.jp
以下,解いた問題の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___
が鍵の復号処理があるので,それを流用した.
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文字ずつ総当たりして求めた.
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
...
デコードするとギフト券番号が書いてある画像が出てくる.