---
# System prepended metadata

title: Secret Base MKII
tags: [CTFWriteups]

---

# 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



