mina-nokawari’s blog

流行に便乗したかった後悔はしていない

PlaidCTF 2017 Writeup (zipper)

就職活動も一段落し、1人で張り切って参加してきたが得点は51 pt(345 位/1143 チーム中)となんとも言えない感じ
とりあえずzipperという50 pt問題だけ解いたので、Writeupを書きます

問題自体は壊れたzipからFlagを取り出せ! というシンプルなもの

世の中にはDiskInternals ZIP Repairやら『zip -F』コマンドやらがあって
簡単に壊れたzipを修復できるワザがあるらしいが、これらを試しても中のFlagは取り出せず

ツールを使っても解決できなそうなので、バイナリエディタからデータ列を引っこ抜いてやろうという方針に変更
zipファイルをバイナリエディタで見ると、以下のような感じに見える(250byteくらいしかないし初めからバイナリエディタで見ればよかった)

f:id:mina-nokawari:20170424135712p:plain

バイナリがそんなに長いわけではないので、ZIPのファイルフォーマットを見て頭から解析していく
途中までは問題なく解析できるのだが、ローカルファイルヘッダのfile name length(2329)の部分で「ファイル全体のサイズがこれより小さいからこのあたりから壊れてるのかなぁ」と思い始める

とりあえず、わかっている部分だけで解析できたことを整理すると、

f:id:mina-nokawari:20170424140150p:plain

  • ファイル圧縮形式はデフレート形式(0008 画像の緑部分)
  • 圧縮後のデータサイズは0x46バイト(画像の赤い部分)
  • 情報ヘッダ(02014B50)よりも前半部分にファイルの実データが格納されている

(つまり、データサイズを加味すると画像の青い部分の0x46バイト分のエリアが最も実データである可能性が高い?)

となります。なので、あとはコードを書くだけ。
デフレート形式で圧縮されたファイルを元に戻す方法について、JavaScriptで実装されていた方がいたので、コードはそちらの方のものを参考にさせていただきました

バイナリエディタを駆使して壊れたzipからファイルを救出する, Qiita(2015/12/8)
qiita.com


var fs = require('fs');
var zlib = require('zlib');

var start;
var end;

function unzipFile(name, start, end) {
  var rs = fs.createReadStream('Zipper.zip', {start: start, end: end});
  var inflateStream = zlib.createInflateRaw();
  var ws = fs.createWriteStream(name);
  rs.pipe(inflateStream).pipe(ws);
}

start = 0x42; 
end = 0x87;
unzipFile('DecFile', start, end);

コードの説明はほぼ不要だと思いますが、開始バイトを0x42, 終了バイトを0x87としてそのエリアを切り取って、InflateRawを使って圧縮データを元に戻すだけです。
このコードを実行するとtxtファイルが出力されて、無事にFlagが取れましたとさ

f:id:mina-nokawari:20170424142727p:plain

flag: PCTF{f0rens1cs_yay}

ALEXCTF の Writeup をそれとなく書こうと思う

2017/2/3〜2017/2/6の3日間で行われていた大会です

「TeamAZ」というチーム名で出場しましたが、参加メンバーは自分と卒論に追われた後輩ちゃん一人だけで、実質単騎出撃という寂しい状況でした

結果は 1440 pt 150位で、「一人にしては頑張ったんじゃないか!」と自己満足しています(笑)

解けた問題は以下の15問です

問題の一覧については、こちらを参照してください




Reverse Engineering

Re1: Gifted(50 pt)

64bitのelfファイルが渡されたのでとりあえずstrings叩いてみた


$ strings gifted

.....

Enter the flag:

AlexCTF{Y0u_h4v3_45t0n15h1ng_futur3_1n_r3v3r5ing}

You got it right dude!

..... 

 これは1分CTF

Re2: C++ is awesome(100 pt)

64bitのelfファイルが渡されるが、僕が普段使用しているUbuntu 14.04ではエラーを吐いて実行できなかかったため泣く泣くUbuntu 16.04を入れました

Objdumpして逆アンセンブリした結果を見ると、どうやら実行時に引数として入力した文字列分だけflagの先頭と合っているか1文字ずつ確認しているだけっぽい。(他の方の Writeup にも書いてありましたが、ltrace するとよくわかります)

f:id:mina-nokawari:20170207205732p:plain

全て合っていれば「You should have the flag by now」を、そうでないと「Better luck next time」を出力するので、『ALEXCTF{』から初めて『}』でYou sholud〜が出るまで1文字ずつ総当たりすればよいかなぁという方針のもと総当たりしてみます。

f:id:mina-nokawari:20170207205921p:plain

普通はShellでコード書くんだろうけど、ある程度文章見えるしいけるだろうと思って手動総当たりをしてしまった......というわけでflagは以下。

AlexCTF{W3_L0v3_C_W1th_CL45335}


Re4: unVM me(250 pt)

pycファイルが与えられてるが、pycってなんぞ?と思ってググる

どうやらpythonコードをコンパイルして得られるバイトコードらしいが、簡単にデコンパイルできるっぽい。

python decompiler」でググるEasy Python Decompilerなるものが出てきたので落として使ってみる。デコンパイルされて出てきたのが下のコード。


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' 

どうやらflagを5文字ずつ区切ってmd5でhash化し、それがmd5sの配列にある値に一致すれば良いっぽい(試しに『ALEXC』でmd5をとってみるとmd5s[0]と等しくなる) 

(じゃあ5文字ずつ総当たりするコード書くか)ってことで5文字総当たりを繰り返すコードを書いたが、僕の実装方法がダメすぎて全然終わる気配がなかったので、仕方なく総当たり成功した5文字だけ出力するコードを並列して回した(頭悪い)。以下、その頭悪いコード。

import md5

import itertools


if __name__ == '__main__':
l = ["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",
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "_"]
md5s = [174282896860968005525213562254350376167L,
137092044126081477479435678296496849608L,
126300127609096051658061491018211963916L,
314989972419727999226545215739316729360L,
256525866025901597224592941642385934114L,
115141138810151571209618282728408211053L,
8705973470942652577929336993839061582L,
256697681645515528548061291580728800189L,
39818552652170274340851144295913091599L,
65313561977812018046200997898904313350L,
230909080238053318105407334248228870753L,
196125799557195268866757688147870815374L,
74874145132345503095307276614727915885L]

for element in itertools.product(l, repeat=5):
if int('0x' + md5.new( element[0] + element[1] + element[2] + element[3] + element[4] ).hexdigest(), 16) == md5s[12]:

print element

まだmd5s[12]を引数とって md5s[i]にしたほうが頭良く見えますね。まぁそんなこんなで13分割された英数字をつなぎ合わせると以下のようにflagになりましたとさ。

ALEXCTF{dv5d4s2vj8nk43s8d8l6m1n5l67ds9v41n52nv37j481h3d28n4b6v3k}



Cryptography

CR1: Ultracoded(50 pt)

ここは後輩ちゃんが解いた問題なんでWriteupの執筆は一任してしまいました。

  1. とりあえずZEROとONEで構成されたtxtファイルを渡されたので0と1に置き換えて16進数
  2. 変換後のテキストがASCIIっぽかったのでASCIIコードに変換
  3. 最後が「=」で終わっていたためBase64だと仮定してデコード

という処理をすると、『. 』と『 -』で構成された文が出現。モールス信号っぽいので変換テーブルを作って変換してみると「ALEXCTFTH15O1SO5UP3RO5ECR3TOTXT」の文字列になったのでビンゴでしたとさ。だが、これに{}を入れて「ALEXCTF{TH15O1SO5UP3RO5ECR3TOTXT}」にしても通らない。実はHintに「特殊文字は入れられませんでした」と書かれていて、flagの区切り文字っぽい「O」を「_」に変えるという一工夫が必要だったようですね。以下、後輩ちゃんが実装したコード。

import base64

Morse_table = {'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':'--..','1':'.----','2':'..---','3':'...--','4':'....-','5':'.....','6':'-....','7':'--...','8':'---..','9':'----.','0':'-----','.':'.-.-.-',',':'--..--','?':'..--..','!':'-.-.--','-':'-....-','/':'-..-.','@':'.--.-.','(':'-.--.',')':'-.--.-'}
Morse_table_inv = {v:k for k, v in Morse_table.items()}

f = open('./zero_one')
ciphertext = f.read()
bintext = ciphertext.replace(' ', '').replace('ZERO', '0').replace('ONE', '1')
hextext = format(int(bintext, 2),'x')
asciitext = hextext.decode('hex')
decode_b64_text = base64.b64decode(asciitext)

words = decode_b64_text.split(" ")
flag = ""
for w in words:
	flag += Morse_table_inv[w]
print flag

というわけで、flagは ALEXCTF{TH15_1S_5UP3R_5ECR3T_TXT}


CR2: Many time secrets(100 pt)

『One time pad』と問題文にあって問題名が『Many time secrets』だからきっとMany time pad attackだよなぁとは思うが手が動かない。

後輩ちゃんに「とりあえずわかってる『ALEXCTF{(0x414c45584354467b)』でXORしてみましょう!」と言われてやってみる

すると、msgの文頭が『Dear Fri』だったため、「じゃあ次は『end』がくるだろうからこれと暗号msgでXORしてみよう!たぶんflagが使い回しの鍵だから『end』で拡張した3バイトも加えて他の行頭にもXORしよう!次にこの単語がわかったから今度は......」と、あとは推測とXORの繰り返しで鍵と平文を交互に特定していくだけでした。最終的に出てきた鍵がflag

ALEXCTF{HERE_GOES_THE_KEY}

CR3: What is this encryption?(150 pt)

問題にはp, q, e, cの値が書いてある。どう見てもRSAです。本当にありがとうございました。

p, q, eがわかっているので、拡張ユークリッドの互除法を用いて

 ed + k(p-1)(q-1) = 1
を満たすdの値を求めたあと、
 c^d (mod pq)
を求めてあげるだけでcを復号できますね。

とは言ってもこの問題は後輩ちゃんが解き方だけ伝えてくれたので、僕はそれに従ってコード書いただけです(笑) 以下、僕が書いた雑コード

import gmpy

p = 0xa6055ec186de51800ddd6fcbf0192384ff42d707a55f57af4fcfb0d1dc7bd97055e8275cd4b78ec63c5d592f567c66393a061324aa2e6a8d8fc2a910cbee1ed9
q = 0xfa0f9463ea0a93b929c099320d31c277e0b0dbc65b189ed76124f5a1218f5d91fd0102a4c8de11f28be5e4d0ae91ab319f4537e97ed74bc663e972a4a9119307

e = 0x6d1fdab4ce3217b3fc32c9ed480a31d067fd57d93a9ab52b472dc393ab7852fbcb11abbebfd6aaae8032db1316dc22d3f7c3d631e24df13ef23d3b381a1c3e04abcc745d402ee3a031ac2718fae63b240837b4f657f29ca4702da9af22a3a019d68904a969ddb01bcf941df70af042f4fae5cbeb9c2151b324f387e525094c41

c = 0x7fe1a4f743675d1987d25d38111fae0f78bbea6852cba5beda47db76d119a3efe24cb04b9449f53becd43b0b46e269826a983f832abb53b7a7e24a43ad15378344ed5c20f51e268186d24c76050c1e73647523bd5f91d9b6ad3e86bbf9126588b1dee21e6997372e36c3e74284734748891829665086e0dc523ed23c386bb520

n = (p-1)*(q-1)
N = p * q

def exgcd(a, b):
    q = a / b
    r = a % b
    (u0, v0) = exgcd(b, r)
    u = v0
    v = u0 - q * v0
    return (u, v)

d = exgcd(e,n)[0]

m = pow(c, d, N)
print ('%x' % m)

ALEXCTF{RS4_I5_E55ENT1AL_T0_D0_BY_H4ND}が出力で、復号されたcがflagでしたとさ。

CR4: Poor RSA(200 pt)

公開鍵とおそらくそれで暗号化されたメッセージが渡される。しかし、この公開鍵あまりにも短い。

          • BEGIN PUBLIC KEY-----

ME0wDQYJKoZIhvcNAQEBBQADPAAwOQIyUqmeJJ7nzzwMv5Y6AJZhdyvJzfbh4/v8
bkSgel4PiURXqfgcOuEyrFaD01soulwyQkMCAwEAAQ==

          • END PUBLIC KEY-----

このぐらいだったら素因数分解できんじゃね?ということで前に後輩くんが紹介していた素因数分解してくれるサイトにぶん投げると案の定数秒で二つの素数に分解された。

p = 863653476616376575308866344984576466644942572246900013156919
q = 965445304326998194798282228842484732438457170595999523426901

素因数分解さえできれば秘密鍵を作れてしまうので、ちょちょっとrsatoolで作って暗号文を復号しましょう!

$ python rsatool.py -f PEM -o key.pem -p 863653476616376575308866344984576466644942572246900013156919 -q 965445304326998194798282228842484732438457170595999523426901
Using (p, q) to initialise RSA instance

n =
52a99e249ee7cf3c0cbf963a009661772bc9cdf6e1e3fbfc6e44a07a5e0f894457a9f81c3ae132ac
5683d35b28ba5c324243

e = 65537 (0x10001)

d =
33ad09ca06f50f9e90b1acae71f390d6b92f1d6d3b6614ff871181c4df08da4c5f5012457a643094
05eaecd6341e43027931

p =
899683060c76b9c0de581a69e0ea9d91bed1071beb1d924a37

q = 
99cde74aedee87adffdd684cbc478e759870b4f20692f65255

Saving PEM as key.pem


$ cat flag.b64 | base64 -D | openssl rsautl -inkey key.pem -decrypt
ALEXCTF{SMALL_PRIMES_ARE_BAD}

どうやらこれとほぼ同じような問題がTrend Micro CTF 2015にあったらしいのでそれを知ってる人には簡単でしたね。
というわけでflagはALEXCTF{SMALL_PRIMES_ARE_BAD}でした。


Forensic

Fore1: Hit the core(50 pt)

64bitのelfファイルが渡されるので脳死でstrings叩く

$ strings fore1.core
CORE
code
......
AUATL
[]A\A]A^A_
cvqAeqacLtqazEigwiXobxrCrtuiTzahfFreqc{bnjrKwgk83kgd43j85e
Pgb_e_rwqr7fvbmHjklo3tews_hmkogooyf0vbnk0ii87Drfgh_n kiwutf
b0ghk9ro987k5tfb_hjiouo087ptfcv}
;*3$"
......

『cvq...』から始まる文字列があるが、『A』を始点に5文字飛ばしで読むと『L』, 『E』, 『X』......と、どうやらflagっぽい文字列が浮かび上がる。『}』まで続けて読んでみると

ALEXCTF{K33P_7H3_g00D_w0rk_up} となってそのまま通りましたとさ。

Fore3: USB probing(150 pt)

USBの通信が記録されたpcapが渡されるが、「Wiresharkで見ても何もわからんなぁ」という状況。
stringsを叩いてみると『flag.mp4』の文字が見え、(もしかしてmp4形式のファイルが流れていたりするのかな?)と思いbinwalkとforemostも追加で叩いてみたが特に抽出されるものなし!

うーむどうしようかなぁと悩んでいると同時に(Wiresharkから見ても何もわからんかなぁ)と思ってバイナリエディタでpcapを開きなおしてみた。何かあるかなぁとバイナリをぼんやり眺めているとpngマジックナンバーが見えた。

f:id:mina-nokawari:20170208014927p:plain

なので、画像の部分を抜き出してやってpngとして新たに保存。プレビューで見ると以下のような内容だった。

f:id:mina-nokawari:20170208015103p:plain

よって、flagはALEXCTF{SN1FF_TH3_FL4G_0V3R_U58}でした。


Scripting

SC1: Math bot(100 pt)

指定された通りにnetcatにつないでみると、botが次々と計算問題を出してきた。間違った答えを返さない限り出題が続くようなので、(とりあえず一定回数答え続けたら何か起こるかなぁ)という仮定のもとスクリプトを書くことにした。1時間くらい回してダメだったら次の方法を考えようと思っていたが、500回正解後にflagを吐き出そうとしてる様子が伺えたのでループの回数を500回に書き直してもう一度実行した。以下に、僕が書いた美しくないコードを一応掲載しておく。


import socket
import string

host = "195.154.53.62"
port = 1337

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((host, port))
response = client.recv(762)

def answer(count):
	ans=0
	a = client.recv(128)
	a = a.strip().replace(" ","")
	strcount = str(count)
	a = a.replace("Question"+ strcount +":","")
	a = a.replace("=", "")
	print a

	if (a.find('+')!=-1):
		list = a.split("+")
		ans = int(list[0]) + int(list[1])
	elif (a.find('-')!=-1):
		list = a.split("-")
		ans = int(list[0]) - int(list[1])
	elif (a.find('*')!=-1):
		list = a.split("*")
		ans = int(list[0]) * int(list[1])
	elif (a.find('/')!=-1):
		list = a.split("/")
		ans = int(list[0]) / int(list[1])
	elif (a.find('%')!=-1):
		list = a.split("%")
		ans = int(list[0]) % int(list[1])

	print ans

	client.send(str(ans)+"\n")

roop=1

count = 1
while (roop==1):
	answer(count)
	count = count + 1
	if count == 501:
		roop = 0 

flag = client.recv(256)
print flag

(計算問題は加法・減法・乗法・除法の四則演算+剰余演算があるだけだし場合わけでいけるかなぁ)という方針で書いた結果、同じ処理の記述が繰り返される雑なコードがこの世に生まれてしまった。

結果は実行して5分くらいで出ました(スクショ撮りなおすのが面倒だったので、「ニコ動見ているときに結果が出て急いで撮ったスクショ」で我慢してください)。

f:id:mina-nokawari:20170208022233p:plain

とまぁこんな感じで、flagはALEXCTF{1_4M_l33t_b0t}と無事取れました。


SC2: Cutie cat(150 pt)

可愛らしい猫の画像が渡されるが、画像中に他のファイルが混入しているわけではないようでbinwalkやforemostを叩いた程度では何もわからない。

(ある特定の色調で見えるように文字が埋め込まれている?)と思いStegpyを使ってあれこれ試してみても文字らしきものは全く見えない。

(これはRGBの下位数bitとかに情報を分散させた電子透かしのようなタイプかなぁこれはそうとう面倒だなぁ)とか思っていたら、けっこう難しくてみんな解けていないらしく以下のようなHintが提示されていた。

Hint: It scripting because we need a python library to solve the challenge, one that is made in japan.

日本人の方が作ったSteganoのツールを使用して情報が埋め込まれてるっぽい。なので、Documentが日本語で書かれているようなPython製のSteganoツールをあれこれ探した。

すると、steganographyという日本人の方が作ったSteganoのツールが見つかる。秘密情報のInput, Output方法が書かれたサンプルコードも置かれていたため、物は試しということで以下のようなコードを書いて実行してみた。

# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from steganography.steganography import Steganography

# hide text to image
path = "./cat_with_secrets.png"

# read secret text from image
secret_text = Steganography.decode(path)

print (secret_text)
$ python test.py 
ALEXCTF{CATS_HIDE_SECRETS_DONT_THEY}

ツールがわかれば一瞬というタイプの問題ですね。
というわけで、flagはALEXCTF{CATS_HIDE_SECRETS_DONT_THEY}でした。


Trivia

最後は雑学なのでサクッといきます

TR1: Hello there(10 pt)

一番ポイントが低い問題ですが、僕にはどこにflagがあるのかわからなくて後輩ちゃんに pt入れてもらった問題です(笑)

どうやらIRCチャットが用意されていたらしく、alexctfのチャンネルに入ればflagが書かれていたようですね。(下の画像参照)

f:id:mina-nokawari:20170208131859p:plain
なので、flagは ALEXCTF{W3_w15h_y0u_g00d_luck}でした。


TR2: SSL 0day(20 pt)

サーバとクライアント間の通信からメモリリークを発生させ、秘密鍵を盗むことができる脆弱性といえば......
そう、Heartbleedですね。
この問題は「One wordで答えろ」という指定があったのですが、気づかずに『ALEXCTF{}』で囲ってしまい無駄に失敗数を増やしてしまいました。(今後は要注意ですね)
少し脇道に逸れますが、もしHeartbleedに興味がある人は、DEFCON 2016の xkcdをやってみると面白いと感じるかもしれません。
というわけで、正解はHeartbleed でした。


TR3: CA(30 pt)

「ALEXCTFのWebサイトにHTTPS証明書を発行した認証局(CA)はどーこだ?」という問題だったので、ブラウザ左上のURLバーにある鍵アイコンからCAを見に行きました。
どうやら「Let's Encrypt」のようなので「let'sencrypt」と打ち込んでみても不正解。
なんでかなーと唸っているときに、(そういえばflagの形式には「'」の文字がないってどこかに書いてあったなぁ)と思い「'」を取り除いてみると正解できました。

よって、正解は letsencryptでした。


TR4: Doesn't our logo look cool ?(40 pt)

というわけで最後の問題ですね。

問題が「僕らのロゴはクールだろ?」なので、ロゴ(下図)の中に何か隠れてるのかなーくらいの想像は容易につきますね。

f:id:mina-nokawari:20170208133948p:plain

目を凝らして見ると「A」やら「{」やらが隠れているので、vimなりコマンドなりで「 @, + : ;」といったflagに関係ない文字列を取り除いていきます。

そうすれば、flagである ALEXCTF{0UR_L0G0_R0CKS} を見つけるのは難しくないはずです。


後語り

僕はCTFのチームでバイナリ班を担当していますが、Reversingの他の方々のWriteupを見てみるとまだまだだなぁと痛感させられますね。

RE2なんかは総当たりをしなくても文字列と変換テーブルを見つけてくれば総当たりなんてしなくてもすみますし、RE3については(「rand」は乱数だから毎回Password変わるよなぁ)とかいう擬似乱数まるで理解してない思考をしていました。

これからも精進していきます