ひよこになりたい

Programming Server Network Security and so on

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アニメのよう。

f:id:zipsan:20150214060921g:plain

どう見ても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で予習してたので調度良かった。

f:id:zipsan:20150214061125p:plain

出力がこれです。読み取ると

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を開いてみる。

f:id:zipsan:20150214145008p:plain

目が痛くなる。。。

なんか文字が書かれています。(解いた後の画像をあげているので、上でデコードしたものとは違うかも)

パスワードらしきものが加工されています。画像をいじれば文字が読めそうなので、きれいな形にぐりぐりいじってから、画像文字認識(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')

出力されたleft.pngとright.pngはこちら

f:id:zipsan:20150214145016p:plain f:id:zipsan:20150214145018p:plain

(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')

出力されたものがこちら

f:id:zipsan:20150214145432p:plain

右半分と合わせればちょうどいい感じになりそうです。

(3) 黒抽出

目が痛くなるようなカラフルフレームを外したいので、黒色だけ抽出します。

黒色を抽出するために、元画像からimage.getpixel()でピクセルの色を取得し、その色が(100, 100, 100)以下ならimage.putpixel()でキャンバスに書き込んでいます。

閾値を設定しているのは、ジャギーを出来るだけ減らしたかったためです。image.getpixel() == (0, 0, 0)とすると、文字が欠けてしまったのでこのような処理にしています。

# 黒抽出
>>> def checkblack(I, V):
... if I[0] &lt; V[0] and I[1] &lt; V[1] and I[2] &lt; 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')

出力されたものがこちら。

f:id:zipsan:20150214145438p:plain f:id:zipsan:20150214145441p:plain

(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')

出力結果はこちら

f:id:zipsan:20150214145724p:plain

これなら読めそう。横のままだと読みづらいので回転します

>>> lastimage = blendimage.transpose(Image.ROTATE_270)
>>> lastimage.save('last.png', 'PNG')

f:id:zipsan:20150214145728p:plain

これならOCRにかけれそうです。

(5) OCRにかける

他アプリケーションを使って手動でOCRにかけるのはさすがにきついので、PythonOCRモジュールを使用します。

今回は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']
.....(略)

多分あってると思う(たぶん)

競技プログラミング勢は簡単に解いていたみたい。つよい。