SECCON 2014 quals Online あみだくじ Writeup
先日のSECCON 2014 quals OnlineのProgramming 300 あみだくじを試合終了後にときましたので、置いておきます。
二時間程度かかりました。もうちょっと早く綺麗に解けるようにならないとな・・・。
Prog300 あみだくじ
問題ファイル:amida
実行ファイルが渡されるので、解いてねと言う問題。
書いたプログラムが以下。(python3)
# -*- coding: utf-8 -*- import sys from subprocess import Popen, PIPE # 元の文章を渡すとhead(数字行), mondai(あみだ本体), tail(*行) のリストを返す def transform(original): # 最初のNo~と最後の空白行を削除 # top => 最初の行, bottom => 最後の行 original = original[1:-1] top = list(original[0]) bottom = list(original[-1]) # あみだが横に倒れている場合 if "".join(top).find('--') != -1: trans = [] for m in original: t = m.replace('-', '#') t = t.replace('|', '-') t = t.replace('#', '|') trans.append(t) trans = list(map(list, zip(*trans))) # 転置 original = ["".join(tr) for tr in trans] top = list(original[0]) bottom = list(original[-1]) # 最終行が数字で始まっていたら上下を反転 if bottom[0] == '1' or bottom[0] == '8': trans = [] for m in original: trans.insert(0, m) original = trans top = list(original[0]) bottom = list(original[-1]) # 1 2 3 4 5 6 7 8 のように空白が開いている場合は、各行から冗長部分を削除 if (top[0] == '1' or top[0] == '8') and "".join(top).find(' ') != -1: i = 0 j = 0 while i < len(top) - 1: while top[j:j + 2] == [' ', ' ']: for k in range(len(original)): tmp = list(original[k]) del tmp[j + 1] original[k] = "".join(tmp) del top[j + 1] i += 1 j += 1 head = list(original[0]) body = original[1:-1] # headとtailを除いた部分 tail = list(original[-1]) return (head, body, tail) def main(): p = Popen(["./amida"], stdout=PIPE, stdin=PIPE, shell=True) while True: print('=' * 50) # 標準出力から問題を取得する msg = "" while True: c = p.stdout.read(1) if not c or c == b'?': break msg += c.decode('utf-8') print(msg) original = ["".join(list(m.replace('\x00', ''))) for m in msg.split('\n')] # 整形処理 head, body, tail = transform(original) print("#" * 30) print("".join(head)) print("\n".join(body)) print("".join(tail)) print("#" * 30) # solve for i in range(len(body)): for j in range(len(body[0])): if body[i][j] == '-': # swap tmp = head[j - 1] head[j - 1] = head[j + 1] head[j + 1] = tmp answer = head[tail.index('*')] print("Answer:", answer) # 答えを投げる p.stdin.write((answer + '\n').encode()) p.stdin.flush() if __name__ == '__main__': sys.exit(main())
渡された問題を標準出力から読み取り、整形して、解いて、標準入力から渡しています。
結局あみだくじは橋が出てきたら数字を入れ替えて行って、入れ替え終わったら*の位置と照合するだけでいいので、整形のほうがメインになってきます。
この問題、問題が進んでいくたびに上下が逆になったり、橋の間隔が大きくなったり、横になったりするので、それぞれに応じて整形してあげなければいけませんでした(ここが一番つらかった)
問題の出現順序が変わらなかったので、「一問あたりの制限時間あるけどどうせ100くらいなら手作業でいけるっしょ〜w」と思い、競技中は順番に入力して解いてたんですが、100問超えくらいから徐々に怪しくなって、160問くらいでつらくなったのでやめました。
その後、やっぱスクリプト書こう!!ってことで書き始めたんですが、もう考える気力と集中力は残ってなかった。結局解けず・・・(あたがわさんに任せとけばよかったなぁ・・・)
実はPythonのsubprocess.Popen使って標準入出力を制御する方法がわからず、ぐぐっても見つけきれなかったので始めは
$ netcat -l -p 40000 -e ./amida
して、それに対してTelnetlibから入出力を受け取って解いてました w
メンバーさんに聞いたらやっぱりPopenでできるみたいだよ!とURL付きで教えてくれたので、それを参考に組みました。勉強が足りないな・・・・。
SECCON 2014 quals Online 重ねてみよう Writeup
SECCON 2014 quals Onlineに参加しました。今回はビギナーでも解ける問題をたくさん出題されるということで、少しは貢献できるかな?と思いましたが、人権消滅しかけてます。
チームはwasamusumeです。今回もチームメンバーの皆さんがスパッと解決していました。すごい。
amidaも中途半端だし、他の問題はメンバーさんが強すぎてタイミングなかったので本当は呼吸Writeupになりそうだったんですが、スキマを縫ってFlag提出したものがあるので書いておこうと思います。
Prog100
重ねてみよう
ジャンル:プログラミング
点数:100
問題文:afterimage.gif
afterimage.gifがもらえるので、開くとGIFアニメのよう。
どう見てもQRコードなので、Pythonでスクリプト書きました。
# -*- coding: utf-8 -*- from PIL import Image X = 164 Y = 164 canvas = Image.new("RGB", (X, Y), (255, 255, 255)) for i in range(1, 50): print("img/img_{0:02d}.png".format(i)) base = Image.open("./img/img_{0:02d}.png".format(i)) pixels = base.load() for x in range(X): for y in range(Y): if pixels[x, y] == 1: canvas.putpixel((x, y), (0,0,0)) canvas.save('ans.png', 'PNG')
GIFアニメを適当なツール(今回はGiam)でつるっと抽出して、img/ディレクトリに保存。
PILライブラリを使って一個一個開きつつ、白い部分だけキャンバスに転写していきます。(上のスクリプトは白い部分を黒にして転写しています)
前回の記事や他のCTFで予習してたので調度良かった。
出力がこれです。読み取ると
FLAG{Many dot makes a QR code}
がでました。
今回はいくつかの問題しか手を付けていないので、復習しておかなきゃなと思ってます。
とくにあみだくじはめちゃくちゃ悔しいのでリベンジしておきます!解けたら記事にしようかな。
Python3でOCR(pytesseract)を使えるようにする
CTFを解いていると時々OCRが欲しい場面が出てくるのですが、Python用のOCRライブラリはPython2系列のものばかりで、Python3でのOCRライブラリは検索してもヒットしません。
私はPython3系列派なので、どうしてもOCRをPython3で使いたい!ということで、Python2用のOCRライブラリであるpytesseractをPython3で使えるように書きなおしてみたのでメモしておきます。
意外に簡単に使えるようになったので、使いたい人は試してみてね。
pytesseractはpipで入れることができます。
# pip install pytesseract
しかし、pip3を使用しても入るのはPython2系列用のコードで、Python3では動きません。。(たまにあるよね)
ということで、pipで入れたpytesseract(ver0.1)に修正を加えます
# pip install pytesseract $ cd /Path/to/site-packages $ cd pytesseract $ vim pytesseract.py
/Path/to/site-packagesの部分はPython3のsite-packageを指定してください。私はMacを使っているので、/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/site-packages/にありました。
以下の部分に修正を加えます。先に2to3コマンドで変換しておくと少しだけラクになるかも。
$ 2to3 -w pytesseract
変更箇所はこちら(diff)
47,48c47,48 < import Image < import StringIO --- > from PIL import Image > import io 51a52 > import tempfile 102,103c103,104 < sys.stderr = StringIO.StringIO() < return os.tempnam(None, 'tess_') --- > sys.stderr = io.StringIO() > return tempfile.mkstemp("", 'tess_') 120,122c121,122 < < input_file_name = '%s.bmp' % tempnam() < output_file_name_base = tempnam() --- > input_file_name = '{0}.bmp'.format(tempnam()[1]) > output_file_name_base = tempnam()[1] 124c124 < output_file_name = '%s.txt' % output_file_name_base --- > output_file_name = '{0}.txt'.format(output_file_name_base) 126c126 < output_file_name = '%s.box' % output_file_name_base --- > output_file_name = '{0}.box'.format(output_file_name_base) 136c138 < f = file(output_file_name) --- > f = open(output_file_name) 151c153 < sys.stderr.write('ERROR: Could not open file "%s"\n' % filename) --- > sys.stderr.write('ERROR: Could not open file "{0}"\n'.format(filename)) 153c155 < print image_to_string(image) --- > print(image_to_string(image)) 160c162 < sys.stderr.write('ERROR: Could not open file "%s"\n' % filename) --- > sys.stderr.write('ERROR: Could not open file "{0}"\n'.format(filename)) 162c164 < print image_to_string(image, lang=lang) --- > print(image_to_string(image, lang=lang))
< の行を > に置き換えです。
保存して終わり。実行してみると
>>> import pytesseract >>> from PIL import Image >>> img = Image.open("/Users/hinanawi/tenshi.png") >>> message = pytesseract.image_to_string(img) >>> print(message) 'tenshityanaishiteru'
こんな感じに出力できます:)
% 追記:pyocrはPython3対応らしいです。
pwniumCTF ROT (Prog 300) Writeup
pwniumCTF に少しだけ手をつけたので1問かいておきます。
順を追って書いていくので、全体だけ欲しい方は最後のプログラムをどうぞ
ROT (Programming 300)
Rot 90, Rot -90, Rot 90, Rot -90... nc 41.231.53.40 9090
Hint : No hints for this task yet 与えられたサーバーに接続すると、base64が降ってきます。
iVBORw0KGgoAAAANSUhEUgA(中略)nuxIpAAAAAElFTkSuQmCC Answer:
(こちらに置いておくのでどうぞ→pwnium300base64.txt)
Answer:で、答えを聞かれるのでどうにかデコードして答えてねーと言う問題。
とりあえず与えられたデータをデコードしてみる。
# Python3 >>> fp = open('pwnium300base64.txt', 'r', encoding='utf-8') >>> msg = fp.read() >>> import base64 >>> print(base64.b64decode(msg)) b'x89PNGrnx1anx00x00x00rIHDRx00x00x00xc8x00x00x00(以下略)
PNGっぽいので、保存してみる。
# Python3 >>> fp = open('x.png', 'wb') >>> decmsg = base64.b64decode(raw.encode('utf-8')) >>> fp.write(decmsg) >>> fp.close()
保存したx.pngを開いてみる。
目が痛くなる。。。
なんか文字が書かれています。(解いた後の画像をあげているので、上でデコードしたものとは違うかも)
パスワードらしきものが加工されています。画像をいじれば文字が読めそうなので、きれいな形にぐりぐりいじってから、画像文字認識(OCR)にかけてみる方針でやってみます。以下のスクリプトはPython2.7で書いています。
(1) 半分にスライス
今回は画像加工に、Pythonの画像加工ライブラリ、Pillowを使用しました。
# pip install pillow
でモジュールを追加することができます。
2つにわかれた文字を合成する必要があるので、2つにスライスをします。
image.clop()に矩形を表すタプルを渡してあげればスライスすることができます。保存はimage.save(filename, format)です。
>>> from PIL import Image # ファイルを開く >>> image = Image.open('x.png', 'r') # 半分にスライス >>> boxl = (0, 0, int(image.size[0] / 2), image.size[1]) >>> boxr = (int(image.size[0] / 2), 0, image.size[0], image.size[1]) >>> leftimage = image.crop(boxl) >>> leftimage.save('left.png', 'PNG') >>> rightimage = image.crop(boxr) >>> rightimage.save('right.png', 'PNG')
(2) 反転
文字と文字をちょうどよく合わせるために、左半分を対角反転します。
image.transpose()にimage.FLIP_TOP_BOTTOMで上下反転とimage.FLIP_LEFT_RIGHTで左右反転を同時に行っています。
# 左半分を対角反転 >>> leftRevimage = leftimage.transpose(Image.FLIP_TOP_BOTTOM).transpose(Image.FLIP_LEFT_RIGHT) >>> leftRevimage.save('leftRev.png', 'PNG')
出力されたものがこちら
右半分と合わせればちょうどいい感じになりそうです。
(3) 黒抽出
目が痛くなるようなカラフルフレームを外したいので、黒色だけ抽出します。
黒色を抽出するために、元画像からimage.getpixel()でピクセルの色を取得し、その色が(100, 100, 100)以下ならimage.putpixel()でキャンバスに書き込んでいます。
閾値を設定しているのは、ジャギーを出来るだけ減らしたかったためです。image.getpixel() == (0, 0, 0)とすると、文字が欠けてしまったのでこのような処理にしています。
# 黒抽出 >>> def checkblack(I, V): ... if I[0] < V[0] and I[1] < V[1] and I[2] < V[2]: ... return True ... return False >>> leftBlackimage = Image.new("RGB", leftRevimage.size, (255, 255, 255)) >>> for x in range(leftRevimage.size[0]): ... for y in range(leftRevimage.size[1]): ... if checkblack(leftRevimage.getpixel((x, y)), (100, 100, 100)): ... leftBlackimage.putpixel((x, y), (0, 0, 0)) >>> leftBlackimage.save('leftblack.png', 'PNG') >>> rightBlackimage = Image.new("RGB", rightimage.size, (255, 255, 255)) >>> for x in range(rightimage.size[0]): ... for y in range(rightimage.size[1]): ... if checkblack(rightimage.getpixel((x, y)), (100, 100, 100)): ... rightBlackimage.putpixel((x, y), (0, 0, 0)) >>> rightBlackimage.save('rightblack.png', 'PNG')
出力されたものがこちら。
(4) 合成・回転
左右の画像が出来上がったので合成します。見た感じ単純合成で良さそうなので、左右両方のデータを読んでいき、黒ならキャンバスに書き込んでいます。
>>> blendimage = Image.new("RGB", rightBlackimage.size, (255, 255, 255)) >>> for x in range(rightBlackimage.size[0]): ... for y in range(rightBlackimage.size[1]): ... if rightBlackimage.getpixel((x, y)) != (255, 255, 255): ... blendimage.putpixel((x, y), rightBlackimage.getpixel((x, y))) ... if leftBlackimage.getpixel((x, y)) != (255, 255, 255): ... blendimage.putpixel((x, y), leftBlackimage.getpixel((x, y))) >>> blendimage.save('blend.png', 'PNG')
出力結果はこちら
これなら読めそう。横のままだと読みづらいので回転します
>>> lastimage = blendimage.transpose(Image.ROTATE_270) >>> lastimage.save('last.png', 'PNG')
これならOCRにかけれそうです。
(5) OCRにかける
他アプリケーションを使って手動でOCRにかけるのはさすがにきついので、PythonのOCRモジュールを使用します。
今回はpytesseractというOCRモジュールを使用しました。(python3では使えません)
余談:上のスクリプトはPython2.7になっていますが、最初に解いた時はPython3.4で書いていました。pytesseractはPython2.7用のモジュールでそのままでは使えないので、Python3→Python2へ変換する必要があります。今回は解く途中で2to3コマンドで一括で変換してpytesseractを使用しています。べんり。
(誰かPython3用のOCRモジュール知ってる人いませんか?(切実))
インストールはpipから行えます。
# pip2.7 install pytesseract
上手く行けばそのまま使用可能です。
ということでOCRモジュールを使用してみます。
# OCR >>> import pytesseract >>> message = pytesseract.image_to_string(lastimage) >>> print message
すごい!これだけでOCRにかけることができちゃうんですね。(Python3でもつかえたらいいなぁ)
これでN24P2QV64Qという文字が取得出来ました。この手順を自動化すればよさそうです。
(6) 接続からAnswer提出まで
接続はTelnetlibを使用しました。接続関係のPythonスクリプトはsocketとか使用する人が多いみたいですが、Telnetlibで簡単に記述することができます。(そのうちまとめたい)
import telnetlib tn = telnetlib.Telnet("41.231.53.40", "9090") raw = tn.read_until("\n") ~解析コード~ tn.read_until("Answer:") tn.write(message + '\n') print tn.read_all()
このスクリプトを走らせるとフラグが降ってきます。
Flag: Pwnium{b1a371c90da6a1d2deba2f6ebcfe3fc0}
以上です。
こんなに簡単に300ptもらえていいのだろうか・・・・
ソースコード投げておきます。(Link(text):pwnium-ROT-solve)
3Dマルバツゲームの残骸
DEFCON 22 CTF Qualifications で出題された3dtttを解いてたのですが、惜しくもなく解けなかったので、途中まで組んで出来上がった残骸を放り投げておきます。
3dtttは3D Tic Tac Toe(3Dマルバツゲーム)を指定された条件で解いていく問題でした。WriteUpは __math氏のWriteUpが参考になります。
3dttt(@__math氏) - https://gist.github.com/math314/34ff1da0b4e169b1ed33
プログラム(Python3)
3Dの盤面をランダムに生成して指定した個数だけ○と×が揃っている盤面を抽出する。何かに使えるかもしれない(?)
# -*- coding: utf-8 -*- import random ox = ('o', 'x') # generate 3d tic tac def makeline(): return [random.choice(ox), random.choice(ox), random.choice(ox)] def makeaspect(): return [makeline(), makeline(), makeline()] def maketic(): return [makeaspect(), makeaspect(), makeaspect()] # print 3d tic tac def printtic(tic): for aspect in tic: for line in aspect: print(line) print('') # print aspect #def printasp(tic): # for aspect in tic: # print(aspect) # 横チェック def check_c(aspect): win = [0, 0] for i, c in enumerate(ox): win[i] += sum([1 if line.count(c) == 3 else 0 for line in aspect]) return tuple(win) # 縦チェック def check_r(aspect): win = [0, 0] for i, c in enumerate(ox): win[i] += sum([1 if line.count(c) == 3 else 0 for line in list(map(list, zip(*aspect)))]) return tuple(win) # 斜めチェック def check_d(aspect): win = [0, 0] for i, c in enumerate(ox): win[i] += 1 if [aspect[j][j] for j in range(3)].count(c) == 3 else 0 win[i] += 1 if [aspect[j][2 - j] for j in range(3)].count(c) == 3 else 0 return tuple(win) # 3Dの盤面の斜めチェック def check_tic_d(tic): win = [0, 0] for i, c in enumerate(ox): if tic[0][0][0] == tic[1][1][1] == tic[2][2][2] == c: win[i] += 1 if tic[2][0][0] == tic[1][1][1] == tic[0][2][2] == c: win[i] += 1 if tic[0][2][0] == tic[1][1][1] == tic[2][0][2] == c: win[i] += 1 if tic[2][2][0] == tic[1][1][1] == tic[0][0][2] == c: win[i] += 1 return tuple(win) if __name__ == '__main__': o = 4 x = 2 count = 0 while True: if count % 10000 == 0: print(count) count += 1 # 3D空間にランダムプロット tic = maketic() # ticのスライス1(面を変える) tictrans1 = [] asptrans = [] for i in range(3): for aspect in tic: asptrans.append(aspect[i]) tictrans1.append(asptrans) asptrans = [] # ticのスライス2(面を変える) tictrans2 = [] asptrans = [] for i in range(3): for aspect in tic: aspect = list(map(list, zip(*aspect))) asptrans.append(aspect[i]) tictrans2.append(asptrans) asptrans = [] # 縦横チェック (通常状態 8x3=24通り) chk = [] for aspect in tic: chk.append(check_r(aspect)) chk.append(check_c(aspect)) chk.append(check_d(aspect)) # 別の面からの判定1(縦方向への向き3x3=9通り + 斜め2x3=6通り) for aspect in tictrans1: chk.append(check_r(aspect)) chk.append(check_d(aspect)) # 別の面からの判定2(斜め2x3=6通り) for aspect in tictrans2: chk.append(check_d(aspect)) # 斜め(3Dの対角線 4通り) chk.append(check_tic_d(tic)) # 個数 sumO = sum([X for X, Y in chk]) sumX = sum([Y for X, Y in chk]) if sumO == o and sumX == x: print("Found!: ({0}, {1})".format(o, x)) printtic(tic)
出力
$ python 3dtictac.py 0 Found!: (4, 2) ['x', 'o', 'x'] ['o', 'o', 'x'] ['o', 'o', 'o'] ['o', 'o', 'x'] ['x', 'o', 'o'] ['o', 'x', 'x'] ['x', 'x', 'x'] ['o', 'o', 'x'] ['o', 'x', 'o'] .....(略)
多分あってると思う(たぶん)
競技プログラミング勢は簡単に解いていたみたい。つよい。