# Secret Base MKII - CUHK2025CTF Writeup
## Challenge Overview
**Challenge Name:** Secret Base MKII
**Points:** 454
**Category:** Crypto
**Event:** CUHK CTF 2025
**Description:**
> I have again obfuscated my secret base with the ultra-
> Robust Skulking Approach but with an upgraded security
> system! This time you really cannot find my secret base!
>
> nc chall.25.cuhkctf.org 25058
The challenge serves up a remote RSA oracle that's basically a "guess my secret base" game, but with a twist: we get an RSA modulus `n` and a ciphertext `c` hiding the flag as an integer `m` (< `n`). The oracle lets us feed it pairs of ciphertexts `(c1, c2)` (as long as they're not the obvious forbidden ones: `c` or `n - c`), and it spits back `m1 ^ m2` where `mi = ci^d mod n`—decryptions via the secret private key `d`. It's like asking the server to do your bitwise homework, one pair at a time.
Our mission: Crack `m` (the flag) with minimal chit-chat. 2048-bit RSA means no brute-forcing this beast; we gotta outsmart it.
## Key Insights
1. **Oracle Behavior**:
- Forbidden: `c` and `n - c` (because who wants *easy* wins?).
- `decrypt(ci) = ci^d mod n`.
- Pairs give XORs, perfect for sneaky plaintext algebra.
- Bonus: `d` is odd—server code picks an odd `e` coprime to `φ(n)`, so `d` follows suit. (No even drama here.)
2. **Properties of Decryption Modulo n**:
- `n - x ≡ -x mod n`, hence `(n - x)^d ≡ (-1)^d * x^d mod n`.
- Odd `d` means `(-1)^d = -1`, so `(n - x)^d ≡ n - x^d mod n`. Negation for free—math's little cheat code.
3. **Attack Strategy**:
- **Step 1**: Hit it with `(n-1, n-2)` to snag `2^d mod n`.
- `decrypt(n-1) ≡ n-1` (easy peasy).
- `decrypt(n-2) ≡ n - 2^d mod n`.
- XOR 'em, flip with `n-1`, negate—boom, `2^d`.
- **Step 2**: Scale the target: `r = c * 2^{-1} mod n`.
- `decrypt(r) = m * 2^{-d} mod n`. (We're dividing by powers now—feels fancy.)
- **Step 3**: Query `(n-4, r)` for `m * 2^{-d} mod n`.
- `decrypt(n-4) ≡ n - 4^d = n - (2^d)^2 mod n`.
- XOR to isolate the good stuff.
- **Step 4**: `m = (that) * 2^d mod n`. Scaling back up—voilà!
Two queries, flag in hand. Efficient, or as the author might say, "ultra-robustly skulking."
## Potential Pitfalls & Assumptions
- **d Odd**: Code confirms it; even `d` would ruin the negation party.
- **Flag Encoding**: Big-endian bytes to int; decode back with a shrug for errors.
- **Edge Cases**: Queries stay safe (flag ain't 1 or n-1, thankfully). Server slaps invalid inputs.
- **Modular Arithmetic**: Python's `pow` handles the heavy lifting—no sweat.
## Proof-of-Concept (POC) Testing
```python
# mock_server.py - Local mock for testing (save challenge code here, modify main() to not loop forever)
from secret_base_2 import * # Assume challenge code in secret_base_2.py
# Run once with dummy flag
flag = b"TEST{this_is_a_dummy_flag_for_testing}"
secret_base = int.from_bytes(flag, "big")
c = encrypt(secret_base)
print(f"n: {n}")
print(f"c: {c}")
# Simulate one query pair
def mock_query(c1, c2):
assert 1 < c1 < n and c1 not in [c, n-c]
assert 1 < c2 < n and c2 not in [c, n-c]
m1 = decrypt(c1)
m2 = decrypt(c2)
return m1 ^ m2
# Test Step 1
c1_test = n - 1
c2_test = n - 2
xor_test = mock_query(c1_test, c2_test)
m1_test = n - 1 # Known: decrypt(n-1) = n-1
m2_test = xor_test ^ m1_test
a_test = n - m2_test # 2^d mod n
print(f"Recovered 2^d mod n: {a_test}")
# Test Step 2-4 (full recovery)
inv2 = pow(2, -1, n)
r_test = (c * inv2) % n
c3_test = n - 4
xor2_test = mock_query(c3_test, r_test)
four_d = pow(a_test, 2, n)
m3_test = n - four_d
b_test = xor2_test ^ m3_test # m * 2^{-d} mod n
m0_test = (b_test * a_test) % n
byte_len = (m0_test.bit_length() + 7) // 8
recovered_flag = m0_test.to_bytes(byte_len, 'big').decode('utf-8', errors='ignore').strip()
print(f"Recovered flag: {recovered_flag}") # Should be "TEST{this_is_a_dummy_flag_for_testing}"
```
**Local Test Output:**
```
n: 12675041096568228133209204718566905622419362346509623356556077790353518006745752947408241120909192952899430393392570139131262834739229570423807515039939050863052161145331932594174415896257235276299936336462554587399031442923154890574598879072951175594156958900182599873538408414304358962801817105839833481156927863180503566958522441052492340872636086937074304728978600921302777027617525580240554208555334013920971357878003443563533363249441746034361754657984446705386871336014628154547340693376413721069466562040455340405488804399382521680612005112852577596420632255602131416946877890487471115239071033924883545302621
c: 2434993123134743485869989665155741540727897302862477730658859223023444954427580247608714229124301680195337376981904525404607456074014064038597353956260745739565020539280859147375570535421543671095537403196963155332743271795497042430138416624313798443952402706625779992024298499488278043250012638103360621227804897513395290701264867566785399949311965830752383234158109682371220655128841789068904133462490999388819658161266055423486002171009680916176688677100243239767180297943233809838358135521333294561239201574401384691129207172332078385057170325270653714452030316133667320889818766533637978884437061400447093515798
Recovered 2^d mod n: 7791948223035840273693785314288316262117945413789626513834350002129289820473739652379655343293357292394325211969815687106250956042413734421503428453899155881564933329821875624419139904079901443321082876042100402325720308399942549739598161926714363431285454655092378274606676863925476966460194889661547711586692183170183030676132720752251430322765626039631825379966255696480036620773831328724483240085438218050538097657304698875459333542560451203388906747356751853675518140039731697434701756972328206251988001303230960705509858798935135087064957017992684458041830538277400944616398862496332712342275886764675949696389
Recovered flag: TEST{this_is_a_dummy_flag_for_testing}
```
Works like a charm. Remote? Just socket-ify the queries.
## Solution Code
```python
import socket
import re
# Remote connection details
HOST = 'chall.25.cuhkctf.org'
PORT = 25058
def recv_until_pattern(s, pattern):
"""Receive data until a regex pattern is found."""
data = b''
while not re.search(pattern, data):
chunk = s.recv(4096)
if not chunk:
raise ValueError("Connection closed unexpectedly")
data += chunk
return data
def parse_n_c(data):
"""Parse n and c from initial server output."""
lines = data.decode('utf-8').splitlines()
n, c = None, None
for line in lines:
if line.startswith('n: '):
n = int(line[3:])
elif line.startswith('c: '):
c = int(line[3:])
if n is None or c is None:
raise ValueError("Failed to parse n or c from server")
return n, c
def query_pair(s, c1, c2, n):
"""Send a pair (c1, c2) and return the XOR response."""
# Send c1
s.send(f"{c1}\n".encode())
# Send c2
s.send(f"{c2}\n".encode())
# Receive XOR
data = recv_until_pattern(s, rb'm:\s*\d+')
match = re.search(r'm:\s*(\d+)', data.decode('utf-8'))
if not match:
raise ValueError("Failed to parse XOR from server")
return int(match.group(1))
def recover_flag():
"""Main recovery logic."""
# Connect to server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
try:
# Receive initial n and c
data = recv_until_pattern(s, rb'c:\s*\d+')
n, c = parse_n_c(data)
print(f"Received n: {n}")
print(f"Received c: {c}")
# Step 1: Query (n-1, n-2) to get 2^d mod n
c1 = n - 1
c2 = n - 2
xor1 = query_pair(s, c1, c2, n)
# decrypt(n-1) = n-1 (since d odd)
m1 = n - 1
m2 = xor1 ^ m1 # decrypt(n-2) = n - 2^d mod n
a = n - m2 # a = 2^d mod n
print(f"Recovered 2^d mod n: {a}")
# Step 2: Compute r = c * 2^{-1} mod n
# decrypt(r) will be m * 2^{-d} mod n
inv2 = pow(2, -1, n)
r = (c * inv2) % n
print(f"Computed r: {r}")
# Step 3: Query (n-4, r) to get m * 2^{-d} mod n
c3 = n - 4
c4 = r
xor2 = query_pair(s, c3, c4, n)
# decrypt(n-4) = n - 4^d mod n = n - (2^d)^2 mod n
four_d = pow(a, 2, n)
m3 = n - four_d
b = xor2 ^ m3 # b = decrypt(r) = m * 2^{-d} mod n
print(f"Recovered m * 2^{{-d}} mod n: {b}")
# Step 4: Recover m = b * 2^d mod n
m = (b * a) % n
print(f"Recovered m: {m}")
# Convert to flag bytes
byte_len = (m.bit_length() + 7) // 8
flag_bytes = m.to_bytes(byte_len, 'big')
flag = flag_bytes.decode('utf-8', errors='ignore').strip()
print(f"Flag: {flag}")
return flag
finally:
s.close()
if __name__ == "__main__":
flag = recover_flag()
```
**Remote Run Output:**
```
Received n: 15950052754600783201567118085975050444867632306385366757660839141674248182497625169744458582711650626144382993590180292128163454050809686521920899276214630499053935665180856053748834020225581647293947040554194126491675322861820548145949980698136522421662333104905487180697528141767114607294867006016633965477553579304867348216733930517677845806970825461083690641021081617187461393913238441503916517774652184144212647837971984499732456598369416271990226927744733821709176951747723727653770140248208603948393993307670581123731097787371831988485388470097304029351761242762471108149141202392025191026359307841587351669059
Received c: 2489108661791696332053699661885156759542233241946256622852435016362574416080428893614877883627422194650039275520327724109442879992100648958827895380683780757061669895385925003191462010386954095021119894493250623935940447422571348432991469612753464556838194424541304542740823876311376518669819225251617205898343805553865551274299946291601906790070371894048925833433953173073655905166411118760189797534780082721078855909126925033490058102175257291503851687680977370898147236574621105689622742592724735109825469459556021156413226363136410079784490640637938096567562182705557164298082099588185379907173336464875241902105
Recovered 2^d mod n: 4692490543946443291987113854894683653307158088469673996799571048065149347617016500785577471843607079608305147690220675397482624308722216224743816393976608221896868750631888608247930357107725420979632215334962386020315282866360983086922758433135741859153947510865719755821501966959391156225647091702911334560116352481651880242034937359609211945986858731085911102610315422278987278791852268655922917365578708531239186522538673032675645962384599045918751132519157109558820084533764541501949093639730429295570260509994632612154096132422000253676490292615659452714197277501205868617406607571529799097502963479422433013056
Computed r: 9219580708196239766810408873930103602204932774165811690256637079018411299289027031679668233169536410397211134555254008118803167021455167740374397328449205628057802780283390528470148015306267871157533467523722375213807885142195948289470725155444993489250263764723395861719176009039245562982343115634125585687948692429366449745516938404639876298520598677566308237227517395130558649539824780132053157654716133432645751873549454766611257350272336781747039307712855596303662094161172416671696441420466669529109731383613301140072162075254121034134939555367621062959661712734014136223611650990105285466766322153231296785582
Recovered m * 2^{-d} mod n: 10570943679510671892248746830872683947012434876027077176700792862283948924366020868600470032568362569115501723162962982411548227247526597970435783232550725283324777978473593628319804353104685485816269736044549397468019707862492426317275502311007958372109755685366901425312844897072098573585896793042362224394617768148074685163857136307184126755925805573606863787058671969820280424975407315991920238285699887783804582289398532579261274797513511711083931957762254871956060388361324858777363765740982066214715364479173959912702486194129666656743987680980914702971784026777759238517172832191766449134457642932725979213241
Recovered m: 5209070839800487383091860243273764230651702923458321511206345502847084189241123534718844931000134498499485752671459797702439349663995201842869317636669309
Flag: cuhk25ctf{d0_you_l1ke_the_s3cret_b4s3_50ng_in_MKI_0r_MKII_m0r3?}
```
## Why This is Elegant
- **Efficiency**: Four ciphertexts, two pairs—quicker than ordering takeout.
- **Math Beauty**: Odd `d` for negation tricks, XOR for combos, `2^{\pm d}` scaling to unwrap the flag. It's like RSA therapy: expose the vulnerability gently.
- **No Factoring**: Oracle-only; plug-and-play for any odd-`d` RSA setup.
A cheeky spin on padding oracles, but with XOR spice instead of yes/no games. Shoutout to the author’s “UwU” forbidden-set hint—adorable troll, or just cat-typing the code?
**Flag:** `cuhk25ctf{d0_you_l1ke_the_s3cret_b4s3_50ng_in_MKI_0r_MKII_m0r3?}`
# Iam Mr. Cow or Mr. Cloud