ふるつき

私は素直に思ったことを書いてるけど、上から目線だって言われる

高専セキュリティコンテスト #SCKOSEN で優勝した

奈良高専は insecure というチームで、 theoldmoon0602、 thrust2799、 kyumina、 miwpayou の四人で、高専セキュリティコンテストに参加しました。現地行きたかったけど申し込み失敗しました。

あと師匠がいねぇ。

これは二日間(実質24時間程度)の Jeopardy + King of the Hill なCTFで、 Jeopardy が 22問くらい、 KoH が 2問ありました。 KoH では 30点が 10分ごとに与えられます。

私チーム insecure はチームワークを遺憾なく発揮して、 3930 点で 1位(35チーム中)でした。私はそのうち、 1400 + 630 点を入れましたので、解いたものについて 簡単な Write up をします。

f:id:Furutsuki:20171022123605p:plain

jeopardy Binary[100] フラグを答えろ

バイナリが降ってくるので strings する

jeopardy Binary[500] OreNoFS

途中で追加された問題。31M くらいのバイナリが渡されて、これが独自FSらしい。めっちゃよなべしたけど解けなかった。最終的には解いた。

バイナリは GPT でパーティショニングされているということ。1クラスタ4096byte 単位で管理されていること。AllocationTableのサイズ、DirectoryEntry 構造体が示されていた。

最初は binwalk などしていたが、 何かしらの zip ファイルが存在していて、 flag.png_MAXOSX/.flag.png が入ってることしかわからず、 unzip はできなかった。

二日目になってやっと参考になるサイトを見つけて ( http://memes.sakura.ne.jp/memes/?page_id=2303 ) 、AllocationTable がどのような存在かを理解したので、ごちゃっとパーサを書いたらunzippable な zip を取り出せました。

方針としては、

  1. DirectoryEntry をみつける
  2. これは ほしいzipのEntryだって知ってるので、 offset を見て、バイナリを読み、AllocationTable から次のクラスタのオフセットを手に入れる
  3. これ以上なくなったらそれを吐く

です。

import struct
import os
import codecs
import sys
from io import SEEK_CUR,SEEK_SET

LBA_SIZE = 512


f = open("raw.dmg", "rb")

f.seek(512, SEEK_CUR) # skip mbr
f.seek(512, SEEK_CUR) # gpt header (primary)

f.seek(32, SEEK_CUR) # partition entry

first_sector = struct.unpack('<Q', f.read(8))[0]
last_sector = struct.unpack('<Q', f.read(8))[0]
f.seek(8, SEEK_CUR)

partition_name = f.read(72)
partition_name_str = codecs.decode(partition_name, encoding='utf_16_le')
if len(partition_name_str)>0:
    print("------------")
    print("first sector: {}".format(first_sector))
    print("last sector:  {}".format(last_sector))
    print("name: {}".format(partition_name_str))
    print()

f.seek( first_sector*LBA_SIZE, SEEK_SET)



AT = f.tell()
print("ALLOCATION TABLE IS {:0X}".format(AT))

allocationTable = []
for _ in range(8192):
    v = f.read(2)
    v = struct.unpack("<H", v)[0]
    allocationTable.append(v)

print("----------------")
print("Allocation Table");
for i, v in enumerate(allocationTable):
    if v == 0:
        continue
    # print("[{0}]   {1:0X}( {1} )".format(i, v));
    pass

DIRENTS = f.tell()
print("DIRECTORY ENTRIES AT: {:0X}".format(DIRENTS))

binvalue = []

while f.tell() < (last_sector+1)*LBA_SIZE:
# read directory entry
    tell = f.tell()
    magic = f.read(1)
    if magic != b'\x0f':
        f.seek(31, SEEK_CUR)
        continue

    name = f.read(8)
    ext = f.read(3)
    size = struct.unpack("<I", f.read(4))[0]
    offset = struct.unpack("<H", f.read(2))[0]
    attr = f.read(12)
    reserved = f.read(2)


    print("-----")
    print("@ {:0X}".format(tell))
    print("magic: {}".format(magic))
    print("name: {}".format(name))
    print("ext: {}".format(ext))
    print("size: {0}({0:0X})".format(size))
    print("offset: {0}({0:0X})".format(offset))
    print("attr: {}".format(attr))
    print("resv: {}".format(reserved))

    nextoffset = offset
    while nextoffset != 0xffff:
        f.seek(0x1000 * nextoffset + AT, SEEK_SET)
        b = f.read(0x1000)
        binvalue.append(b)

        print("atvalue: ", end="")
        print(allocationTable[nextoffset])
        nextoffset = allocationTable[nextoffset]
        if nextoffset >= len(allocationTable):
            break

        
    print()

    break
o = open("kore", "wb")
for b in binvalue:
    o.write(b)

きったねぇ

jeopardy Crypto[100] かんたんな符号化2

これはほとんど thrust2799 が解いた。

わけのわからん文字列が与えられるけど、 base64 -d すると16進値っぽい数字列になる。これを decode('hex') してやると KP から始まる文字列になるので、エンディアンをひっくり返してあげて、保存する。 file をかけると Word 2007 と出るので、 libre Office Writer で開くと 縦にフラグが書いてある。コピーして適当なファイルに保存して cat | tr -d '\n' した。

jeopardy Network[100] 寝坊気味のコンピュータ

pcap が降ってくるので strings してうまいことつなぎ合わせる

jeopardy Network[100] ログインしたいんだ!

pcap が降ってくるので strings してBase64っぽい文字列を見かけたら base64 -d する

jeopardy Web[100] ログインせよ

ログインフォームがあるのでユーザ名 admin、パスワード 'or'a'='a と入力する

jeopardy Web[100] 灯台下暗し

ログインフォームと、ログインのロジックをPHPで書いたものが表示されるので ざっと眺めて login.db みたいな名前がひっかかる。アクセスすると sqlite dbファイルが降ってくるので apt install sqlite3 して sqlite3 login.db する。 .schema で users というテーブルだけがあることがわかるので select * from usersすると admin のパスワードがフラグになっている。

jeopardy Web[200] Web1

難読化された javascript がある。適当にBeautifyして処理を一つずつ追ってやる。すると DOM を書き換えるしょりと、別のことをしている処理が存在することがわかる。 別のことをしている処理が怪しいので単体で実行するように書き換え、余計な分岐をはずしてやるとフラグが手に入る。

jeopardy Web[200] Web2

いわゆる Javascript puzzle。解き方が何処かに乗ってないかと思って調べたら burningCTF でまったく同じ問題が出題されていることがわかったのでWriteUpをみながら真似をした。

KoH 通信を解析しろ

pcap ファイルが降ってくる。サイズはそこそこ。みると、幾つかの ssh ログインとたくさんの TCP SYN がある。ssh はよくわからないIP同士で通信していることが多い。TCP SYNは 10.201.1.101から10.201.1.102 へのもの。最初はポートスキャンかなと思っていた。

ちょっとだけある HTTP パケットにフラグが書いてあり、さらにバイナリが存在することが示唆される。フラグに port knocking とあるので TCP SYN のうちACKが帰ってきたものを真似て port knocking してやると a.out が手に入る(port knocking しなくてもいいという説がある→作問者がいるって言ってた)。

フラグは thrust2799 が手に入れ、私が port knocking してみれば、と言った。 thrust2799 はバイナリ読めないと開始前から何度も言っていたので、私が解析することにしてファイルを貰った。

radare2 で見ていたがわかりにくかったので Bokken を入れてる miwpayou にも解析してもらった。どうやら a.out は 8081番で待ち受ける fork型のサーバで、fork した先でstrncmp や strlen を使い、一部で popen していることがわかった。

gdb が存在することを思い出したので set follow-fork-mode child して b do_workして、fork後の処理を追う。 strncmp では recv した文字列を SCK と比較していることがわかった。さらに次では recvした文字列+3を popenに突っ込んでいることがわかった。

そこで試しに SCKwhoami と送ってみたところ ubuntu というレスポンスが来た。 SCKls → SCKcat README → SCKecho insecure >> flag してKoHになった。

余談だけど SCKkillall a.out したらサーバからのれすぽんすがなくなった。これを運営に報告したらしばらく経って復活した。復活して SCKwhoami したら root だった。


SECCON まわりのコンテストって教育的な問題が多いですよね。高専生のレベルが上ってもっとヤバゲな問題も出てきてほしいです。

それにしても楽しいコンテストでした。来年も参加したいものです。

あと #procon28 で悔しい思いをしたぶんこっちで晴らせてよかった。

最後ですけど

  運営の皆さんお疲れ様です。ありがとうございました。

  弊学のお方、休日出勤させてしまい申し訳ありません。優勝したので許して。

  ちーむめいとの人々。さいこう