UEFIでEject専用機を作ろうとした

Eject専用PCを作りたい衝動に駆られたので、EjectするだけのUEFI Applicationを書いた。
6f70/efieject · GitHub
某所で話した時にはEFI_EXT_SCSI_PASS_THRU_PROTOCOLを使っていたが、EFI_SCSI_IO_PROTOCOLの方がシンプルなので切り替えた。

これをブートローダーに設定すると、起動画面はこんな感じになる。
f:id:xoreaxeax:20131212231103p:plain

VirtualBox 4.3.4上でパススルーのホストドライブをeject出来る事を確認した。binary/にefiファイルとisoファイルを入れておいたので手軽にejectできる。
ただ、残念なことに手元の実機(M/B: GA-H77-D3H)では動かなかった。LocateHandleBufferでEFI_NOT_FOUNDが返ってくる。EFI_SCSI_IO_PROTOCOLが実装されていないか、何か勘違いしているのかもしれない。

Hack.lu 2013 CTF FluxArchiv 1&2 Write-up

気が向いたのでWrite-upを書く。

FluxArchiv (Part 1) (Reversing 400)

These funny humans try to exclude us from the delicious beer of the Oktoberfest! They made up a passcode for everyone who wants to enter the Festzelt. Sadly, our human informant friend could not learn the passcode for us. But he heard a conversation between two drunken humans, that they were using the same passcode for this intercepted archive file. They claimed that the format is is absolutely secure and solves any kind of security issue. It's written by this funny hacker group named FluxFingers. Real jerks if you ask me. Anyway, it seems that the capability of drunken humans to remember things is limited. So they just used a 6 character passcode with only numbers and upper-case letters. So crack this passcode and get our ticket to their delicious german beer!
Here is the challenge: https://ctf.fluxfingers.net/static/downloads/fluxarchiv/hacklu2013_archiv_challenge1.tar.gz

こんな感じ。

% tar zxvf hacklu2013_archiv_challenge1.tar.gz
hacklu2013_archiv_challenge1/
hacklu2013_archiv_challenge1/archiv
hacklu2013_archiv_challenge1/FluxArchiv.arc
% file hacklu2013_archiv_challenge1/archiv
hacklu2013_archiv_challenge1/archiv: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=0x0fa7f6566e2ff973c4d045b10f6f751898c70053, not stripped
% file hacklu2013_archiv_challenge1/FluxArchiv.arc
hacklu2013_archiv_challenge1/FluxArchiv.arc: data
% xxd -g 1 hacklu2013_archiv_challenge1/FluxArchiv.arc | head
0000000: 46 6c 75 58 41 72 43 68 69 56 31 33 37 29 42 df  FluXArChiV137)B.
0000010: 27 12 82 45 05 d8 17 1f 4f 0b cb 14 15 3d 39 ba  '..E....O....=9.
0000020: 94 75 7c 42 2d e5 a1 d1 7f 19 09 1a 61 d4 d2 85  .u|B-.......a...
0000030: d3 19 09 1a 61 d4 d2 85 c4 19 09 1a 61 d4 d2 85  ....a.......a...
0000040: 2d f3 33 48 35 14 8a ff 24 8a e9 fc 98 66 29 76  -.3H5...$....f)v
0000050: b3 6d 7d 7f 0f a0 bb ea 9f 42 6a 79 e1 e7 f6 ed  .m}......Bjy....
0000060: 52 5a 92 fb 17 a9 7d 8f 0e 48 09 af ff 4d b4 04  RZ....}..H...M..
0000070: e1 6f 19 5a a9 60 e8 20 25 6a 93 27 ed 09 b6 88  .o.Z.`. %j.'....
0000080: 93 f6 26 1d 27 26 3f 67 6b 77 54 8c cf cb 72 9c  ..&.'&?gkwT...r.
0000090: 70 1e bf 00 51 84 8f bd 54 d8 94 5b c5 81 17 73  p...Q...T..[...s

archivというのが独自形式のアーカイバで、そのアーカイバで作成されたFluxArchiv.arcというアーカイブファイルのパスワードを求めればいいらしい。

IDAで処理の流れを追った所、hash_of_passwordという配列にパスワードのSHA1ハッシュが格納されていて、そのハッシュを秘密鍵としてRC4でファイルを暗号化していた。hash_of_passwordを参照している部分を中心に見ていくと、verifyArchiv関数が以下の様な処理をしている事を発見した。

#!/usr/bin/env python
import sys
import hashlib

archive = ""
hash_of_password = ""

def verifyArchiv():
    SHUFFLE_TABLE = [0, 7, 14, 1, 8, 15, 2, 9, 16, 3, 10, 17, 4, 11, 18, 5, 12, 19, 6, 13]

    hash1_correct = ""
    with open(archive) as f:
        hash1_correct = f.read()[0x0C:0x20]

    hash0 = hash_of_password
    hash0_shuffled = ''.join([hash0[SHUFFLE_TABLE[i]] for i in xrange(20)])

    sha1 = hashlib.sha1()
    sha1.update(hash0_shuffled)
    hash1 = sha1.digest()

    return 1 if hash1 == hash1_correct else 0

def main(argv):
    global archive, hash_of_password
    archive = argv[1]
    password = argv[2]
    sha1 = hashlib.sha1()
    sha1.update(password)
    hash_of_password = sha1.digest()

    print 'OK' if verifyArchiv() else 'NG'

if __name__ == '__main__':
    main(sys.argv)

問題文によればパスワードは英数字大文字6文字という事だから、36 ** 6 = 約22億通りなのでなんとか総当りできるか。こんな感じのコードを書いた。

#!/usr/bin/env python
import hashlib
import string
import itertools
import multiprocessing
import sys

HASH1_CORRECT = open('FluxArchiv.arc').read()[0x0C:0x20]
CHARS = string.ascii_uppercase + string.digits
SHUFFLE_TABLE = [0, 7, 14, 1, 8, 15, 2, 9, 16, 3, 10, 17, 4, 11, 18, 5, 12, 19, 6, 13]

def test_password(pw):
    sha1 = hashlib.sha1()
    sha1.update(pw)
    hash0 = sha1.digest()

    hash0_shuffled = ''.join([hash0[SHUFFLE_TABLE[i]] for i in xrange(20)])

    sha1 = hashlib.sha1()
    sha1.update(hash0_shuffled)
    hash1 = sha1.digest()

    return hash1 == HASH1_CORRECT

def process(headch):
    for _pw in itertools.product(CHARS, repeat=5):
        if test_password(headch + ''.join(_pw)):
            print headch + ''.join(_pw)
            break

def main(argv):
    num_pool = int(argv[1])

    for i in xrange(0, len(CHARS), num_pool):
        pool = multiprocessing.Pool(processes=num_pool)
        pool.map(process, CHARS[i:min(i + num_pool, len(CHARS))])
        pool.close()

if __name__ == '__main__':
    main(sys.argv)

(multiprocessingの使い方これでいいのか知らない)

最初はシングルプロセスのコードだった。i5-3317U上のシングルプロセスで2~3時間程度回していたらヒットした。

% ./bruteforce.py
PWF41L

マルチプロセスのコードも書いて他の環境でも回してみたところ、Xeon X5570 * 2上の16プロセスとi7-3770上の8プロセスで、どちらも15分程度でヒットした。

FluxArchiv (Part 2) (Reversing 500)

These sneaky humans! They do not just use one passcode, but two to enter the Festzelt. We heard that the passcode is hidden inside the archive file. It seems that the FluxFingers overrated their programming skill and had a major logical flaw in the archive file structure. Some of the drunken Oktoberfest humans found it and abused this flaw in order to transfer hidden messages. Find this passcode so we can finally drink their beer!

(only solvable when FluxArchiv (Part 1) was solved)
Here is the challenge: https://ctf.fluxfingers.net/static/downloads/fluxarchiv/hacklu2013_archiv_challenge1.tar.gz

archivのロジックの欠陥を利用してFluxArchiv.arcの中にフラグが隠してあるという事らしい。FluxArchiv.arcに格納されているファイル4つを全てarchivで展開して調べたが、もちろん何も無かった。

FluxArchiv.arcは先頭32bytesのヘッダと1040bytesのブロックの繰り返しで構成されているので、全てのブロックの中にどのファイルでも使われていないブロックが有るのではないかという推測が立った。格納されているファイルが使用しているブロックの一覧等の情報を出力するコードを書いた。

#!/usr/bin/env python
import hashlib
import sys
from struct import pack, unpack
from Crypto.Cipher import ARC4

# 'FluXL1sT'
MAGIC_FILEENTRY = pack('<Q', 0x5473314C58756C46)
# sha1('PWF41L')
KEY = '328042503975adb0c62f536d8416b5f78babea15'.decode('hex')
ARCFILE = open(sys.argv[1])
BLOCKSIZE = 0x410

def read_archive(length):
    rc4 = ARC4.new(KEY)
    dec = rc4.decrypt(ARCFILE.read(length))
    return dec

def list_blocks(blocki, blocknum):
    blocklist = [blocki]

    for blockcnt in xrange(blocknum - 1):
        ARCFILE.seek(0x20 + blocki * BLOCKSIZE)
        blocki = unpack('<Q', read_archive(0x08))[0]
        blocklist.append(blocki)

    return blocklist

def main(argv):
    blocki = 0
    ARCFILE.seek(0x20)
    while read_archive(0x08) == MAGIC_FILEENTRY:
        for filei in xrange(8):
            ARCFILE.seek((0x20 + 0x10 + blocki * BLOCKSIZE) + filei * 0x80)
            blocki_start = unpack('<Q', read_archive(0x08))[0]
            blocknum = unpack('<Q', read_archive(0x08))[0]
            if blocki_start != 0:
                print 'FILE %d:' % filei
                print '  index of start block: 0x%08x' % blocki_start
                print '  number of blocks: 0x%08x' % blocknum
                print '  md5sum: %s' % read_archive(0x10).encode('hex')
                print '  filename: %s' % read_archive(0x60).rstrip('\x00')
                print '  blocks: %s' % ', '.join(map(str, list_blocks(blocki_start, blocknum)))

        # next file entry
        ARCFILE.seek(0x20 + 0x08 + blocki * BLOCKSIZE)
        blocki = unpack('<Q', read_archive(0x08))[0]
        if blocki != 0:
            print '\nindex of next file entry block: 0x%08x\n' % blocki
            ARCFILE.seek(0x20 + blocki * BLOCKSIZE)
        else:
            break

if __name__ == '__main__':
    main(sys.argv)

(元のコードが残念だったので終わってからかなり書き直した。)

出力結果。

FILE 0:
  index of start block: 0x00000001
  number of blocks: 0x00000016
  md5sum: ffea3a5254c0587ad5b2ece81be8bab5
  filename: attentionzombie.mp3
  blocks: 1, 3, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157
FILE 1:
  index of start block: 0x00000002
  number of blocks: 0x00000086
  md5sum: ec5638da4810f75e0735a3df9e30c1b0
  filename: Did_You_Know.jpg
  blocks: 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136
FILE 3:
  index of start block: 0x00000089
  number of blocks: 0x00000009
  md5sum: 2bee8bec6c1a8ad658dffbe14bc14792
  filename: fluxfingers.png
  blocks: 137, 162, 163, 164, 165, 166, 167, 168, 169

index of next file entry block: 0x000000ad

FILE 4:
  index of start block: 0x000000b2
  number of blocks: 0x0000007e
  md5sum: f1df033f0179fe101d11700fe796b4fd
  filename: th_oh-noes-everybody-panic.gif
  blocks: 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303

使用されているブロックを調べるとブロック158-161, 170-177のがどのファイルからも使われていない事が分かったので、適当に復号して調べたら、ブロック160でflagを見つけた。

#!/usr/bin/env python
import sys
from struct import pack, unpack
from Crypto.Cipher import ARC4

# sha1('PWF41L')
KEY = '328042503975adb0c62f536d8416b5f78babea15'.decode('hex')
ARCFILE = open(sys.argv[1])
BLOCKSIZE = 0x410

def read_archive(length):
    rc4 = ARC4.new(KEY)
    dec = rc4.decrypt(ARCFILE.read(length))
    return dec

def main(argv):
    blocki = int(argv[2])
    outfile = argv[3]

    ARCFILE.seek(0x20 + blocki * BLOCKSIZE)
    with open(outfile, 'w') as f:
        f.write(read_archive(0x08) + read_archive(BLOCKSIZE - 0x08))

if __name__ == '__main__':
    main(sys.argv)
% xxd -g 1 block160.bin
0000000: 00 00 00 00 00 00 00 00 65 20 65 6c 65 63 74 72  ........e electr
0000010: 6f 6e 20 61 6e 64 20 74 68 65 20 73 77 69 74 63  on and the switc
0000020: 68 2c 20 74 68 65 0a 62 65 61 75 74 79 20 6f 66  h, the.beauty of
...
00003a0: 20 4d 65 6e 74 6f 72 2b 2b 2b 0a 0a 46 6c 61 67   Mentor+++..Flag
00003b0: 3a 20 44 33 6c 65 74 69 6e 47 2d 31 6e 64 33 78  : D3letinG-1nd3x
00003c0: 5f 46 34 69 4c 0a 0a 1f 16 f8 73 37 72 64 c5 5c  _F4iL.....s7rd.\
...

どうでもいいけどこんなのとかもあった。

% xxd -g 1 block161.bin | head -n 2
0000000: 00 00 00 00 00 00 00 00 44 69 64 20 59 6f 75 20  ........Did You
0000010: 4b 6e 6f 77 00 00 00 00 00 00 00 00 00 00 00 00  Know............
% xxd -g 1 block170.bin | head -n 2
0000000: 00 00 00 00 00 00 00 00 54 68 61 74 20 47 6f 64  ........That God
0000010: 73 20 4f 66 20 44 65 61 74 68 00 00 00 00 00 00  s Of Death......
% xxd -g 1 block171.bin | head -n 2
0000000: 00 00 00 00 00 00 00 00 4c 6f 76 65 73 20 41 70  ........Loves Ap
0000010: 70 6c 65 73 3f 00 00 00 00 00 00 00 00 00 00 00  ples?...........

SECCON信州大会CTF Write-up

解いた順。(多分)

パスワードを探せ (バイナリ, 100pt)

stamp.exe

exeのダウンロードリンクのみが与えられた。exeを起動すると以下のウィンドウが表示された。
f:id:xoreaxeax:20131010105129p:plain
IDAに読み込ませて眺めていると、以下の様な関数を発見した。
f:id:xoreaxeax:20131010105150p:plain
2つのループでそれぞれ別々にTextOutAを繰り返し呼んでいる。片方では座標を配列から決定していて、もう片方ではrandから決定している。
怪しい。OllyDbgで両方のTextOutAにブレークポイントを仕掛けて実行し、何度かポチポチしていたらフラグが出てきた。
f:id:xoreaxeax:20131010105202p:plain

解答

ST4MPS3CURITY

2012/4/27 11:08:54 に作成されたエントリの名前は何ですか? (フォレンジック, 100pt)

ファイル Filesystem001.bin の内容を確認し、次の設問に回答してください。
2012/4/27 11:08:54 に作成されたエントリの名前は何ですか?

Filesystem001.bin

こんな感じ。

0000000: 2e 20 20 20 20 20 20 20 20 20 20 10 00 59 dc 58  .          ..Y.X
0000010: 9b 40 9b 40 00 00 dd 58 9b 40 05 00 00 00 00 00  .@.@...X.@......
0000020: 2e 2e 20 20 20 20 20 20 20 20 20 10 00 59 dc 58  ..         ..Y.X
0000030: 9b 40 9b 40 00 00 dd 58 9b 40 00 00 00 00 00 00  .@.@...X.@......
0000040: 42 2e 00 6a 00 70 00 67 00 00 00 0f 00 c7 ff ff  B..j.p.g........
0000050: ff ff ff ff ff ff ff ff ff ff 00 00 ff ff ff ff  ................
0000060: 01 43 00 68 00 72 00 79 00 73 00 0f 00 c7 61 00  .C.h.r.y.s....a.
0000070: 6e 00 74 00 68 00 65 00 6d 00 00 00 75 00 6d 00  n.t.h.e.m...u.m.
0000080: 43 48 52 59 53 41 7e 31 4a 50 47 20 00 1d e3 58  CHRYSA~1JPG ...X
0000090: 9b 40 9b 40 00 00 8d 6e ee 3a 06 00 22 6b 0d 00  .@.@...n.:.."k..
00000a0: e5 44 00 65 00 73 00 65 00 72 00 0f 00 e3 74 00  .D.e.s.e.r....t.
00000b0: 2e 00 6a 00 70 00 67 00 00 00 00 00 ff ff ff ff  ..j.p.g.........
00000c0: e5 45 53 45 52 54 20 20 4a 50 47 20 00 20 e3 58  .ESERT  JPG . .X
00000d0: 9b 40 9b 40 00 00 8d 6e ee 3a dd 00 75 e8 0c 00  .@.@...n.:..u...
00000e0: e5 67 00 00 00 ff ff ff ff ff ff 0f 00 1d ff ff  .g..............
00000f0: ff ff ff ff ff ff ff ff ff ff 00 00 ff ff ff ff  ................
......
0000b80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0000b90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0000ba0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0000bb0: 00 00 00 00 00 00 00 00                          ........

8.3ファイル名が見えるのでFATの一部を切り出したバイナリっぽい。適当なFATのイメージを作って同じタイムスタンプのファイルを作ることにする。

dd if=/dev/zero of=fat16.bin count=65536
mkfs.vfat -F 16 fat16.bin
sudo mount -o loop fat16.bin /mnt/work0
sudo touch -t 1204271108.54 /mnt/work0/MIKU
sudo umount /mnt/work0

作成したファイル名でfat16.bin中を検索。

00107f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0010800: 4d 49 4b 55 20 20 20 20 20 20 20 20 00 00 2d 5e  MIKU        ..-^
0010810: 46 43 9b 40 00 00 1b 59 9b 40 00 00 00 00 00 00  FC.@...Y.@......
0010820: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

0x0010816:0x001081aの1b 59 9b 40がなんとなくタイムスタンプっぽかったので、その値でFilesystem001.bin中を検索した。

00004a0: 41 50 00 61 00 6e 00 64 00 61 00 0f 00 90 2e 00  AP.a.n.d.a......
00004b0: 6a 00 70 00 67 00 00 00 ff ff 00 00 ff ff ff ff  j.p.g...........
00004c0: 50 41 4e 44 41 20 20 20 4a 50 47 10 00 04 1b 59  PANDA   JPG....Y
00004d0: 9b 40 9b 40 00 00 1c 59 9b 40 9b 05 00 00 00 00  .@.@...Y.@......

解答

Panda.jpg

RXでアセンブラ短歌 (プログラミング, 100pt)

以下のアセンブラ短歌で出力される文字列は?

fb 7e 67 6d 62
fb 62 15 08 0c 65 03
fc 37 67 7e a7
66 11 66 43 66 55 03
ef 02 75 60 ff 67 01

( ´_ゝ`)……。

IDAではRXシリーズは逆アセンブルできなかった。objdumpで逆アセンブルしようと思って "objdump rx renesas" 等のキーワードで検索したがヒットしない。まだ対応していないのかと思いルネサスの開発キットやマニュアル等を当たっていた所、隣で同じ問題を解いていた@maytheplicが「RXシリーズ用のgccならあった」 と言ってRX用gccについてのWebページを見せてくれた。これで気付いた。gccがあるならbinutilsもあるはずであり、検索するならobjdumpではなくbinutilsで検索すべきだった。
"binutils rx"等のキーワードで検索をして、このページに行き着いた。アーキテクチャ欄に"elf32-rx-be"等があるので使えるはず。早速逆アセンブル

>all-objdump.exe -D -b binary -mrx rxasm.bin

rxasm.bin:     file format binary


Disassembly of section .data:

00000000 <.data>:
   0:   fb 7e 67 6d 62                  mov.l   #0x626d67, r7
   5:   fb 62 15 08 0c 65               mov.l   #0x650c0815, r6
   b:   03                              nop
   c:   fc 37 67                        xor     r6, r7
   f:   7e a7                           push.l  r7
  11:   66 11                           mov.l   #1, r1
  13:   66 43                           mov.l   #4, r3
  15:   66 55                           mov.l   #5, r5
  17:   03                              nop
  18:   ef 02                           mov.l   r0, r2
  1a:   75 60 ff                        int #255
  1d:   67 01                           rtsd    #4

解答

% python -c "from struct import pack; print pack('<I', 0x626d67 ^ 0x650c0815)"
rene

キーワードは番組の中で。 (ネットワーク, 200pt)

10.0.2.3 2222/tcpでサーバが動作しています。
パーツを集めて正解文字列を特定してください。
答えは正解文字列のsha1ハッシュ値です。

10.0.2.3:2222にncで接続すると4~5桁の数字が送られてきて数秒間(?)停止した後に通信が切れる という様な挙動を示した。送られてくる数字は以下の6つの数字のいずれかで接続毎(1秒毎?)に不規則に変わっていた。

7372 8271 9812 16212 39812 54721

数字の規則性などを見出そうとしばらく悩んだ後、この問題のジャンルがネットワークである事からこれらの数字はポート番号ではないかと気付いた。通信が停止している最中にncでそのポートに接続してみた所、文字が1文字だけ送られてきて通信が切れた。ポート番号毎に送られてくる文字は決まっていて、以下のように対応していた。

{7372: 's', 8271: '3', 9812: 'C', 16212: 'C', 39812: '0', 54721: 'N'}

解答

% echo -n "s3Cc0N" | sha1sum
fdc6a976e1971ca680ed5328814333cbff321881  -

キーワードは番組の中で(2) (ネットワーク, 300pt)

10.0.2.3 3333/tcpでサーバが動作しています。
パーツを集めて答えを見つけてください。
今回はたぶん人力では無理です、かしこ。

基本は1と同じ。ポート番号を送ってくる接続がすぐ切れる点と、送られてきたポート番号で接続しに行った時に降ってくるデータがやたらデカい点が違っていた。
3333/tcpにポート番号を聞きに行って各ポートからの受信データを全てつなぎ合わせて保存するPythonスクリプトを適当に書いた。

#!/usr/bin/env python
import subprocess
import time

def set_nonblocking(fh):
  import fcntl
  import os
  fd = fh.fileno()
  fl = fcntl.fcntl(fd, fcntl.F_GETFL)
  fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)

ports = {}
for _ in xrange(50):
    target0 = subprocess.Popen(['nc', '10.0.2.3', '3333'], stdout=subprocess.PIPE)
    set_nonblocking(target0.stdout)
    time.sleep(0.1)
    portnum = target0.stdout.read().rstrip()
    target1 = subprocess.Popen(['nc', '10.0.2.3', portnum], stdout=subprocess.PIPE)
    set_nonblocking(target1.stdout)
    time.sleep(0.3)
    recv = target1.stdout.read()
    target0.kill()
    target1.kill()
    if int(portnum) not in ports or len(ports[int(portnum)]) < len(recv):
        ports[int(portnum)] = recv

recvdata = ""
for key in sorted(ports.iterkeys()):
    recvdata += ports[key]

with open('recvdata.bin', 'w') as f:
    f.write(recvdata)

(接続周りが変だったので変なことをしている)

% file recvdata.bin
recvdata.bin: Zip archive data, at least v2.0 to extract
% unzip recvdata.bin
Archive:  recvdata.bin
  inflating: q3.exe

解答

>q3.exe
key = sashimi-umai

反省とか

4時間の短期決戦だったので、ピンと来ない問題を避けて解ける問題に集中した。正しい選択だったと思う。
"We are still here"が解けなかったのが残念。実行ファイルではなくSECCON横浜の"King of 目grep"的な問題かと思っていたがWriteupを読んで納得した。Web問の "管理者ページからキーを探せ" は解けなかったがチームメイトが頑張ってくれていた。お疲れ様と言いたい。

参加者の皆さん、今回のCTFを企画して下さったSECCON実行委員会の皆さんお疲れ様でした。ありがとうございました。(メール対応お手数おかけしました。ありがとうございました)

新春パズル 延長戦らいとあっぷ

  • 問題

http://www1.axfc.net/uploader/so/2743059.zip

  • 1問目

ヤサヤサ ソゥウールニプソ ゲヌ
チークァミウマ トオオ シハハモンシモンチュフド
アルキゾニューツマワニヤヘス
アキヨ

  • 解答

FFXアルベド語。(アルベド語 - Wikipedia)
翻訳すると以下の文章となる。

またまた とぅるーくりぱと ぜす
きーふぁいるは おのの にななよんによんきゅうぼ
かくしごりゅーむはありませぬ
かしこ

Axfcの2742495をkeyfileとして、同梱のQUEST_EX1をTrueCryptでマウント。

  • 2問目

0バイトのtxtと真っ白pngが入ったFATなディスクがマウントされる。
そちらには情報は無く、ディスク内にQUEST_Ex3.zipというファイルが削除済みで残っているので適当なユーティリティで復元

  • 3問目

最終問題です。

iesys_src.7zを格納した記憶媒体を、私の友達の裕(ゆたか)くんに預けました。
江ノ島に住む地域ネコです。(私が名前をつけました。)
ゆたかくんをさがしてください。

写真4枚が同梱されている。Exifは無し。内1枚に1月4日の神奈川新聞が写っている。

DEFCON CTF Quals grab400 うろ覚えwriteup

2012年6月2日, 3日(UTC)に行われたDEFCON CTF Qualsに参加しました。その内の一問 grab bag 400 のwriteupです。

ペネトレテスターの辻さんもblogで同じ問題のwriteupを書かれていますが、僕は少し違う解法で解きました。スクリーンショットとか色々取るのを失念していたので、その辺は辻さんのblogを参照してください。

問題

What is Jeff Moss' checking account balance?
Bank site => http://140.197.217.85:8080/boa_bank
User:blacksheep, Password:luvMeSomeSheep

ざっとサイトを調べた所、任意のパラメータを与えられそうなのはログイン画面のID, Passと支店検索フォームのZIP codeの3つでした。支店検索フォームにシングルクオートを入力したらSQLのエラーメッセージが表示されたので、ここから攻める事にします。

まず、テーブル名を列挙しました。辻さんはinformation_schemaを使っていますが、僕は確かpg_databaseテーブルからdatnameを引っ張って来ました。クエリはこんな感じ。

0 UNION SELECT datname,'','','','','' FROM pg_database

列挙されたテーブル一覧を眺めるとaccountというテーブルがありました。怪しいですね。調べます。それっぽいカラム名を入力してみたりHAVINGとGROUP BYでカラム名をエラーメッセージとして表示させてみた所、使えそうなカラムが以下のような感じである事が判明しました。

account.type => 'checking', 'saving'
account.balance => Integer
account.owner => Integer
account.name => (以上3カラムの内容その他をカンマで区切って連結したもの)

そこで、以下のようなクエリを投げて(うろ覚え)、全checking口座のbalanceとownerを列挙した所、balanceが全て0.00であることが判明。(!?)

0 UNION SELECT account.name,'','','','','' FROM account WHERE account.type = 'checking'

この時点で0.00と解答しても良かったのですが、どうも判然としないのでJeff Mossの口座を突き止める事にします。accountテーブルでは口座所有者の情報はownerの数値しか見つからなかったので、別のテーブルの情報と結びつける事を考えました。今一度テーブル一覧を眺めると、customerなるテーブルがあるようです。調べます。

accountテーブルと同様にcustomerテーブルも調べた所、customer.id, customer.firstname, customer.lastnameがあるようなので列挙させました。"Jeff", "Moss"でページを検索しましたが、彼らしき人物はヒットしなかったので"Jeff Moss"で検索した所、Wikipediaに以下のような記述を発見。

Jeff Moss, also known as The Dark Tangent, is the founder of the Black Hat and DEF CON computer hacker conferences.

"Tangent"でページ検索をかけたら、居ました。Dark Tangentさん。(どんな名前だ…)彼のcustomer.idが分かったので、account.ownerと結びつけた上でaccount.balanceを今一度確認しましたが、やっぱり0.00です。仕方がないので、"0.00"と解答した所、"CORRECT!"となり400ptをゲットしました。

反省

非効率極まりないですねorz
account.ownerとcustomer.idが同一の値であるという前提も勘に寄ったものですし、これは好ましくない解法です。全ユーザーのbalanceが0.00だったのは、何かの間違いだったのか、それとも意図されたものだったのか…。SQL Injectionはあまり知らなかったので、いい勉強になりました。
forensic 300も解きましたが、概ね辻さんと同じアプローチでした。

反転したのはどのbit? - SECCON CTF福岡大会 問題レポート

2012年2月18日, 19日に開催されたSECCON CTF福岡大会のレポートを、実行委員の園田さんが@ITで執筆されていました。僕は残念ながら参加する事が出来なかったのですが、大会で出題された問題の内の一題がレポート中で引用されていたので、解いてみました。
以下、そのレポート(とオマケ)です。ネタバレを含むので、これから解こうと思っている人は注意してください。


問題は

「以下のファイルダンプは宇宙からの中性子線で1bitのデータが反転したものである。(略)1bitだけ異なる正常なデータファイルを復元し、秘密の鍵を答えよ。」

というもの。実行委員長の竹迫さん作成の問題です。

まずは、ダンプのテキストを素のバイナリに戻します。テキストを corrupted.txt との名で保存し、毎行頭のアドレスを削除した上で、

cat corrupted.txt | xxd -r -p > corrupted.bin

とすると、素のバイナリがcorrupted.binとして保存されます。
次に、corrupted.binがどういうファイルなのか調べます。fileコマンドに入力したところ、出力は以下の通りでした。

corrupted.bin: gzip compressed data, was "z.txt", from Unix, last modified: Tue Feb 14 00:28:39 2012, max compression

これにより、このファイルはどうやらgzipで圧縮されたものらしいという事が分かったので、拡張子をgzに変えてgzip -dで解凍を試みたところ、

 incomplete distance tree

gzip: corrupted.gz: invalid compressed data--format violated

となりました。確かにこのファイルは破損しているようです。
corrupted.gzの大きさは357bytes = 2856bitsで、反転したビットは1ビットのみである事が分かっているので、総当りで1bitずつ反転させて解凍を試みる方法が現実的に実行可能であると推測できます。後は、corrupted.gzを読み込んで全ビットを1つずつ反転させながら保存していくプログラムを書き、保存された2856個のgzファイル全てに対しシェルスクリプトでgzip -dを試みて、そのうちの1つ、ファイル先頭から383bit目を反転させたファイルの解凍に成功しました。その中身がまさに「秘密の鍵」だったのですが、それは敢えてここには書きません。是非自分でやってみましょう。

オマケ

とりあえず上の方法で解くことは出来たものの、この流れではおそらく定石であろう総当たりだけをして終わるのは、もったいない。
というわけで、エラーメッセージを手がかりにした総当りによらない反転箇所の絞り込みを試みました。以下詳細。

出力されたエラーメッセージを眺めてみると

incomplete distance tree

という文がいかにも反転箇所の手がかりになりそうです。"distance tree"が何処にあるのか突き止めるため、まずはgzipのファイルフォーマットについて調べます。"gzip ヘッダ"等でぐぐって、以下のページを発見しました。
[Studying HTTP] gzip
gzip format
上のページの内容とcorrupted.gzのバイナリを突き合わせた所、corrupted.gzの中身は以下のようになっている事が分かりました。

+-----------+-------------+---------+---------+
|header(16B)|payload(333B)|CRC32(4B)|ISIZE(4B)|
+-----------+-------------+---------+---------+

括弧内の数字はその部分のバイト数を表しています。ヘッダにも末尾のデータにも"distance tree"らしき物が無いので、どうやらペイロードが怪しいです。ヘッダによれば、このペイロードにはDeflateで圧縮されたデータが格納されているとの事なので、今度はDeflateについて調べます。ぐぐって出てきた以下のページを参考に考えてみることにします。
デフレート圧縮(LZ77圧縮)処理の概要 - ウェブで用いられる画像形式。
RFC 1951 DEFLATE Compressed Data Format Specification version 1.3 日本語訳 - futomi's CGI Cafe
Notes on Deflate in PNG files
上のページ曰く、Deflateのバイナリの構造はこんな感じ。

+----------+--------+---------+---------+
|header(3b)|HLIT(5b)|HDIST(5b)|HCLEN(4b)| ->
+----------+--------+---------+---------+
   +-----------------------------+-------------------+--------------------+
-> |len for len((HCLEN + 4) * 3b)|len for literal(?b)|len for distance(?b)| ->
   +-----------------------------+-------------------+--------------------+
   +---------------+---+
-> |compressed data|EOD|
   +---------------+---+

今度は、括弧内の数字はビット数を表しています。各部分の名称は適当に略してありますが、ここで言う"len for distance"がエラーメッセージの"distance tree"を指しているようです。
反転していると思しき部分はこれで判明しましたが、問題はその範囲です。上のページや図を見るとわかるように、Deflateのバイナリは符号化されたビット単位の可変長データが複数組み合わさって構成されており、"len for distance"の範囲を求めるのは容易ではありません。もしかするとこの世の何処かにはDeflateのバイナリをそのまま読んで解読できるイカれたよく訓練されたバイナリアンが居るのかもしれませんが、僕には無理です。
そこで、gzipコマンドのソースコードを弄って、解凍中に当該部分の範囲を出力させてみる事にしました。gzipソースコード(gzip-1.4.tar.gz)を落としてきて、まずは元々のエラーメッセージでgrepします。inflate.cのinflate_dynamic関数中でエラーとなっているようです。エラー箇所の前後や定義されているマクロを調べると、inflate_dynamic関数中で、DUMPBITSというマクロを用いて、"HLIT"から"len for distance"までを読み込んでいる事が分かりました。そこで、

<       DUMPBITS(j)
---
>       DUMPBITS(j);printf("%d,",j);

という風にinflate_dynamic関数中のDUMPBITSマクロを置換し、読み込みが発生する度にそのビット長を出力させる事にしました。また、"len for literal"と"len for distance"が同一while文中(/* read in literal and distance code lengths */)でまとめて読み込まれているようなので、while文先頭に

if (i == nl) printf("D,");

というコードを挟み、"len for distance"部分に入ったら"D"という文字が出力されるようにしました。以上の改造を施したgzipでcorrupted.gzの解凍を試みると

5,5,4,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3,3,3,3,4,7,4,4,3,2,4,7,4,7,2,4,3,3,3,3,3,2,2,2,2,4,2,6,4,4,2,4,2,2,2,2,2,3,3,6,D,2,4,3,3,3,2,3,2,6,2,4,2,6,2,2,3,2,3,2,2,2,

という文字列が出力されました。後は、この文字列を表計算ソフトに読み込ませて、ヘッダのサイズに留意しつつ"len for distance"の範囲を求めると、"len for distance"はgz全体で342 - 401bitの範囲にある事が分かりました。実際に反転していた箇所は383bit目なので、求まった範囲とも合致します。これにより、総当りによらず、反転していると推測される箇所の範囲を60/2856まで絞り込むことが出来ました。

まとめ

この問題については、範囲絞り込みの手間より総当りの手間のほうが明らかに少ないので、総当たりを選ぶべきです。しかし、問題によっては、何かしらの方法で範囲を絞り込む事が必要になることもあるでしょう。
誤りがあれば指摘お願いします。