OKAY with this information you have: The program ...

Created on: December 7, 2025

Answered using GPT-5.1 Thinking by Chat01

Question

OKAY with this information you have:
The program provided is a contrived cryptographic challenge. It is not “ordinary” AES or RSA, but rather an interactive mix of several ideas from elementary number theory, public‑key encryption and message authentication codes. At a high level, the components work as follows:

generator() builds a composite modulus n whose bit‑length is twice the security parameter (≈2 × 1024 = 2048 bits). The modulus is a product of many randomly chosen primes (each ~128 bits) and a power of 2. Because each prime is fairly small, n is unusual – it’s neither a simple RSA modulus nor a safe‑prime modulus.

process() computes an AES‑CMAC tag of n. The key used for the CMAC is the first 16 bytes of the big‑endian representation of n and the message is the entire byte string of n. This tag is printed once at start‑up and is later used as an integrity check when the user attempts to “verify” a new modulus.

getSignature() leaks information about base by returning base² modulo a large power of 2. Because the signature uses a power‑of‑two modulus, it reveals all but a few of the most significant bits of base – this essentially leaks the exponent being used.

power_tower_mod() (a hidden function) returns the value of an infinite tetration
𝑥
𝑥
𝑥


x
x
x


modulo n. In modular arithmetic, such power towers eventually stabilise because the exponent reduces modulo the Carmichael function of n; if b is relatively prime to n, repeated application of the Carmichael function eventually yields 1, so the infinite power tower converges modulo n
math.stackexchange.com
. This mechanism is used to update the exponent for each round.

verify() allows the user to replace the original modulus with a new one if, and only if, the new modulus has the correct bit‑length, its factorisation into primes (provided by the user) is correct, every prime factor is “small” (≤ ≈200 bits), and the CMAC tag for the new modulus matches the tag printed at start‑up. Since forging an AES‑CMAC under an unknown key is assumed to be infeasible, the only practical way to supply a valid factorisation is to re‑use the original modulus.

obfuscateSmall() splits the plaintext into high bits and 96 low bits, clears the low bits and then adds a sum of 96 secret primes corresponding to the low‑bit pattern. Because the secret “keys” are independent primes and the operation is linear (bits are turned into a sum of primes), one can recover them by querying obfuscateSmall(2^i) for each bit i, which returns exactly the i‑th secret prime.

The overall goal is to obtain the flag from the RSA‑like ciphertext printed by communicate(). To achieve this one must:

Recover the random primes used in obfuscateSmall(). Before verification, the function can be queried on chosen inputs. By inputting numbers with exactly one low bit set (e.g., m = 1<<i), the function returns a sum containing only the i‑th secret prime; this reveals all 96 secret primes and allows the obfuscation to be reversed.

Determine the exponent base. The signature base² mod 2^k leaks almost all of the exponent’s bits. Using the signature along with known techniques (Hensel lifting) one can reconstruct base.

Compute the totient of n. The infinite‐tower update uses the Carmichael‑function chain, so inverting the RSA operation requires knowing φ(n). Because n is a product of many comparatively small primes, it is amenable to special‑purpose factorization algorithms such as the elliptic‑curve method (ECM). ECM is particularly effective for finding small factors: it runs in sub‑exponential time and its running time is governed by the size of the smallest factor rather than the size of the whole number
en.wikipedia.org
. Thus, factoring a 2048‑bit number composed of ~128‑bit primes is within reach of ECM.

Invert the RSA‑like encryption. Once φ(n) is known, one can compute the modular inverse of base modulo φ(n) and raise the ciphertext to that inverse to recover the obfuscated message. Using the recovered secret primes from step 1, the low bits can be removed and the original plaintext (the flag) is obtained.

In summary, this problem is a custom cryptography challenge where the solver must combine several mathematical techniques:

modular arithmetic and infinite exponent towers (tetration) converge modulo n because exponents reduce via the Carmichael function
math.stackexchange.com
,

integer factorization to recover φ(n), with elliptic‑curve factorisation being particularly appropriate for the many ~128‑bit prime factors
en.wikipedia.org
, and

bitwise analysis and linear algebra to undo the obfuscation.

Elliptic curves are used here not in the sense of elliptic‑curve cryptography but as an efficient factorisation method. The solver is expected to leverage these concepts rather than standard RSA or elliptic‑curve encryption.

Can you create a solver script
live instance: nc remote.infoseciitr.in 4001

Its for a homelab crypto ctf challenge created by my brother

chall.py:
import math
from Crypto.Hash import CMAC
from Crypto.Util.number import long_to_bytes, getPrime, bytes_to_long, isPrime
from Crypto.Cipher import AES
from hidden import power_tower_mod, flag

assert bytes_to_long(flag).bit_length() == 1263

"""
power_tower_mod -> takes x, n and some data and returns
.
.
.
x
x
x
x
x mod n
i.e. infinite power tower of x modulo n
x^(x^(x^(x^(x.......))))) mod n
There are no vulnerabilities in that function trust me!!
"""

class bigMac:
def init(self, security = 1024):

text
self.security = security self.n, self.data = self.generator() self.base = getPrime(security) * getPrime(security) self.message = bytes_to_long(flag) self.process() self.verified = False self.bits = 96 self.keys = [] for i in range(self.bits): self.keys.append(getPrime(self.bits)) print("My m4c&ch3353:", self.mac) print("My signature: ", self.getSignature(self.base)) self.next() def generator(self): chunk = 128 while 1: data = [] n = 1 for i in range(2 * self.security // chunk): data.append([getPrime(chunk), 1]) n *= data[-1][0] data.append([2, 2 * self.security - n.bit_length()]) while n.bit_length() < 2 * self.security: n *= 2 if n.bit_length() == 2 * self.security: return n, data def process(self): x = long_to_bytes(self.n) cc = CMAC.new(x[:16], ciphermod=AES) self.mac = cc.update(x).hexdigest() def getSignature(self, toSign): return (toSign * toSign) % (1 << (toSign.bit_length() - (self.security // 250))) def verify(self, N, data): self.next() if self.verified: print("ALREADY VERIFIED") return False if N.bit_length() != 2 * self.security: print("size of N is not correct.") return False prev = self.n mac = self.mac self.n = N self.process() x = 1 maxPrime = 0 for i in range(len(data)): data[i][0] = int(data[i][0]) data[i][1] = int(data[i][1]) if not isPrime(data[i][0]): self.n = prev self.mac = mac print("Gimme primesssssss onlyyyy!!") return False x *= pow(data[i][0], data[i][1]) maxPrime = max(maxPrime, data[i][0]) if self.mac != mac or x != N or maxPrime.bit_length() > self.security // 5: self.n = prev self.mac = mac print("Failed to verify.") return False print("Yayyyyyyyy! big mac got verified! for n =", prev) print("Data =", self.data) self.data = data self.n = N self.verified = True return True def next(self): self.base = power_tower_mod(self.base, self.data, self.n) def obfuscateSmall(self, m): obs = m & ((1 << self.bits) - 1) m ^= obs final = 0 for i in range(self.bits): if ((obs >> i) & 1): final += self.keys[i] return m + final def communicate(self): self.next() if self.verified: x = self.obfuscateSmall(bytes_to_long(flag)) while math.gcd(x, n) != 1: x += 1 while math.gcd(self.base, self.n) != 1: self.base += 1 print(f"Here is your obfuscated c: {pow(x, self.base, self.n)}") else: print("Verification needed.") def power_tower(self, x): self.next() if self.verified: print("WTF(What a Freak), you have n do it yourself.") return -1 return power_tower_mod(x, self.data, self.n)

if name == "main":
big = bigMac()

text
steps = 90 while steps > 0: print("1: Communicate.") print("2: Verify.") print("3: Obfuscating.") print("4: Quit.") steps -= 1 x = int(input("Choose: ")) if x == 1: big.communicate() elif x == 2: n = int(input("Give me the MY modulus : ")) *inp, = input("Enter prime factorization in format [prime1, count1]-[prime2, count2]-[...: ").split('-') data = [] for i in inp: curr = i[1:-1].split(", ") data.append([int(curr[0]), int(curr[1])]) big.verify(n, data) elif x == 3: x = int(input("Enter your message : ")) print("Here is your obfuscated message : ", big.obfuscateSmall(x)) elif x == 4: print("Goodbye.") quit() else: print("Wrong input.")

Thought

Reviewing the solution approach

Hmm, maybe I'm misusing something, but it's possible that the remote reference is offline. Regardless, the user’s description gives enough for a high-level approach: they say the getSignature leaks the exponent by returning base² mod 2^k and using Hensel's lifting to reconstruct b, assuming b is odd. This gives us a clear structure to build the solver.

Clarifying solver approach

They omitted some details, but I think we can still work with it. By treating the base as unknown, we can reconstruct it from the signature. Essentially, because base² has enough bits truncated, we can reconstruct the base exactly. I'll just outline a function solve_base_from_signature(sig), referencing Hensel’s lemma to recover the base modulo 2^k.

Reconstructing base from signature

The details are missing, but we can treat the base as unknown and reconstruct it from the signature. By using the truncated bits of base², we can recover base but only up to a certain modulus. We can use Hensel's lemma to find square roots mod 2^k. However, we'd still need to know the top 4 bits to fully reconstruct the base.

Clarifying signature leakage

It seems I was confused earlier. If the implementation didn't use base², getSignature could have returned base mod 2^(base.bit_length() - (self.security // 250)), revealing everything except the top 4 bits. Their description might have been inaccurate or miswritten, and perhaps the actual code used base instead of base².

Reconciling signature and base reconstruction

If we used signature = base mod 2^(L-4), it would leak all but the top 4 bits of base. We could brute-force 16 possibilities for the top 4 bits and identify the correct one, but factoring 2048-bit numbers is impractical. They might have intentionally used base² mod 2^k for design reasons. Our solver can adapt to handle both situations, and we can leave a placeholder in the script for reconstruction.

Handling instructions for incomplete tasks

The system isn’t asking for perfection, just progress. I can provide a solver script with a stub for base reconstruction, along with clear comments referencing the algorithm. While the user may want fully functional code, I can still offer a practical solution given the base is computed properly elsewhere.

Considering the bug in code and its impact

The code mentions gcd(x, n), but if n isn't defined globally or within the class, it would cause an error. If n is global, maybe it’s something we control. Still, it’s unclear if that’s the case. The code has some uncertainties, but we're assuming the actual challenge has accounted for them.

Structuring script and considerations

The user’s high-level approach focuses on factoring ECM and a base leak, which I trust. My script would involve using pwntools for server interaction. The first step is to parse the signature. After that, we might need to fetch 'n', but it's unclear if 'n' is printed or prompted by the server. We can factor offline if necessary.

Structuring a solver for the challenge

Since the user is building a homelab and may modify the challenge, it’s safer to provide the solver in two parts: an interactive portion for retrieving data and an offline portion for functions like 'recover_keys', 'factor_modulus', and 'decrypt'. Factoring can be done using Sage, as factoring 2048-bit numbers with sympy might be slow. I'll highlight the option for them to handle the factoring separately in Sage.

Creating solver options for user’s homelab CTF

Given the user's request for a single solver script, I'll provide a Python solution using libraries like pwntools and pyecm (or gmpy). However, factoring might be slower in pure Python. For efficiency, I’ll suggest using SageMath for large-number factoring and incorporate the result into the script.

Clarifying steps for solver script

The solver will handle the 96 queries to obfuscateSmall and process the challenge while factoring n using available methods. The approach involves using ECM (via PyECM or sympy) for factoring large numbers, recovering prime factors, and ensuring the input format matches expectations for verification. After verification, the script communicates and obfuscates the flag as described, while respecting the challenge's logic and base updates.

Reasoning through signature leak and base reconstruction

The main challenge here is reconstructing the original base (base0) from the leaked signature. This is crucial because it's tied to the exponent (e). We know base0's square modulo a huge number, and could use advanced techniques like Hensel's lifting to try and recover it. Still, the ability to replicate the base updates is tricky since key functions are hidden or unavailable.

Reconstructing the base from signature and square modulus

Given the signature leaks information about the base, we potentially reconstruct the base using the square modulus method. While it's tricky without knowing the full modulus, we could try using Hensel's lift for the 2-adic square root, applying iterative decompositions of the data. But we're treating y as modulo 2^k, and may get a unique base from this.

Planning solver skeleton with factoring and recovery steps

Given all the uncertainty, I’m leaning toward providing a skeleton solver with parts like key recovery and base recovery marked as TODO. I'll outline the process, noting that retrieving base from the signature requires non-trivial 2-adic reconstruction, which I’ll skip for now. I'll focus on a Python3+pwntools outline for key functions.

Implementing decryption and obfuscation removal

I'll implement the decryption flow starting with computing the exponent for the modulus using tetration, followed by exponent chain computation to ensure we get the final base. After decryption, I’ll address obfuscation removal by handling the odd factors affecting the message, factoring in key values and iteratively adjusting to ensure proper GCD. I'll test potential bit patterns to retrieve the original flag.

Inverting the obfuscation process

To reverse the obfuscation, I'll leverage the fact that the key set is made of independent primes. Using the congruence constraints, I aim to reconstruct the low bits of the flag through a subset-sum problem. The direct prime retrieval process with queries allows me to systematically reverse the obfuscation for each bit, using the known keys.

Reassessing de-obfuscation steps

It looks like I'm stuck, trying to reverse-engineer obfuscation using the known primes. The math is tough, as the mapping isn’t easily invertible without extra data. I need a more efficient way—possibly by recovering the low 96 bits directly. Using multiple queries might help, but there's still the consideration of time complexity and the limits of the system.

Rethinking approach to obfuscation and equations

I realize there's a misunderstanding in treating the keys as a direct path to recovering the flag. The method described with obfuscateSmall is both tricky and computationally intense. Even though we can work with lots of equations, the challenge still feels intractable because of the unknowns—especially since the high bits of the final mapping stay hidden.

Exploring base reconstruction and deobfuscation strategy

I’m considering multiple ways to approach the encryption challenge here, focusing on using multiple ciphertexts or treating the base as a simpler factor. The complexity, especially with modulus and exponents, adds difficulty, but if I carefully break down the pseudocode approach and focus on general mathematical principles, I can help guide the solution effectively.

Considering deobfuscation approach challenges

I'm revisiting how to reverse the obfuscation process. The complexity of dealing with large bit lengths, primes, and subset-sum methods makes things tricky. The approach seems to rely on linear algebra or dynamic programming, both of which are difficult given the size. I may need to experiment with clever techniques or rethink the problem based on user-provided insights.

Breaking down deobfuscation strategy

The approach involves solving a system of 96 linear equations modulo 2^96 using dynamic programming. The complexity comes from the carry across bits. While it seems manageable, the full solution is computationally heavy. The user may only need a general structure or skeleton for solving this problem, not the complete implementation.

Creating script structure for solver

I’ll start by giving a brief explanation—they likely know this, but a short reminder helps. Then, I’ll lay out the Python script with crucial elements: network code for interactions, key retrieval, factoring nn, deobfuscation, and pseudocode hints for algorithms. I’ll provide a general structure while keeping the explanation moderate, focusing on practical implementation details.

Analyzing power tower and exponent behavior

The key challenge is understanding that exponent updates via a power tower process may not need to mimic directly. I need to derive the final exponent from base and modulus λ(n) without deeper recursion or factorization. The encryption seems to use the final base, which we can calculate without needing full exponent updates.

Designing a generic solver for the challenge

Given the ambiguity of the data, our final answer will outline the key steps for building a solver. We can't definitively validate parameters or behaviors without more clarity on how the remote operates. However, I’ll create a high-level Python skeleton, mentioning areas needing adaption, such as factoring and base recovery. I'll provide pseudocode with placeholders for unknown functions.

Designing solver script with assumptions

The solver script expects the remote to print the modulus n at the start. I'll make sure to mention in the comments that it assumes this. The script will include parsing for "mac" and "signature", with placeholders for factors and verification. I’ll use general pseudocode and clear structure to guide the user on what to adjust.

Thought for 6m 41s

Answer

Here’s a solver skeleton that follows exactly the attack you described and talks to

nc remote.infoseciitr.in 4001

You’ll have to fill in a couple of mathematically-heavy parts (factoring with ECM / base recovery / de-obfuscation) depending on the exact version of your brother’s challenge, but all the plumbing around the menu, oracles, and data-flow is done.

I’ll assume:

  • Python ≥3.8
  • You’re okay installing pwntools, sympy and pycryptodome locally.
  • The remote prints:
    • My m4c&ch3353: <hex>
    • My signature: <int>
    • (Possibly also n = <int> – if not, you’ll hard-code or parse it some other way.)

High-level structure

The solver does:

  1. Connect to the service, read MAC + signature (+ optionally n if printed).
  2. Leak all 96 primes used in obfuscateSmall by querying option 3 with inputs 2^i.
  3. Obtain / factor n (either from the service output or from a file / constant).
  4. Call verify once with the real (n, factorization) so the service dumps the original factorization (and fixes self.n, self.data, self.verified).
  5. Call communicate to get the RSA-like ciphertext c.
  6. Offline:
    • Reconstruct base (encryption exponent) from the signature.
    • Re-implement the infinite power tower modulo n (if needed) and follow the same sequence of next() calls as the server to get the final exponent e.
    • Compute φ(n) (or λ(n)), then d = e^{-1} mod φ(n).
    • Decrypt: x = c^d mod n.
    • Undo obfuscation using the 96 primes to recover the flag.

Below is a reasonably clean implementation with a few TODO hooks where you’ll plug your “math heavy” pieces.


Full solver skeleton (Python)

python
#!/usr/bin/env python3 # # bigMac solver skeleton # # Needs: # pip install pwntools pycryptodome sympy # from pwn import remote from Crypto.Util.number import inverse, long_to_bytes from math import gcd, lcm from sympy import factorint HOST = "remote.infoseciitr.in" PORT = 4001 SECURITY = 1024 SMALL_BITS = 96 # --------------------------------------------------------------------------- # Helpers for number theory # --------------------------------------------------------------------------- def phi_from_factors(factors): """ Compute Euler's totient φ(n) from prime factorization. factors: dict {p: e} """ phi = 1 for p, e in factors.items(): if p == 2: # φ(2^e) = 2^(e-1) for e >= 1 phi *= 1 << (e - 1) else: phi *= (p - 1) * (p ** (e - 1)) return phi def carmichael_from_factors(factors): """ Compute Carmichael λ(n) from prime factorization. λ(p^k) = φ(p^k) for odd p λ(2) = 1 λ(4) = 2 λ(2^k) = 2^(k-2) for k >= 3 λ(n) = lcm(λ(p_i^e_i)) """ def lam_prime_power(p, e): if p == 2: if e == 1: return 1 if e == 2: return 2 return 1 << (e - 2) # odd prime return (p - 1) * (p ** (e - 1)) curr = 1 for p, e in factors.items(): curr = lcm(curr, lam_prime_power(p, e)) return curr # --------------------------------------------------------------------------- # Infinite power tower (tetration) modulo n # --------------------------------------------------------------------------- def infinite_power_tower_mod(a, n, lam_cache=None): """ Compute ^∞ a mod n, using the Carmichael-chain idea. This is a generic implementation; in the real challenge, 'hidden.power_tower_mod' presumably does something equivalent but more optimized, possibly using 'data'. Assumes gcd(a, n) == 1 for correctness of the λ-reduction. """ if n == 1: return 0 if lam_cache is None: lam_cache = {} def lam(m): if m in lam_cache: return lam_cache[m] # For this problem, m will always be of the form λ^k(n) where n # is a product of small primes -> factoring is feasible. fac = factorint(m) val = carmichael_from_factors(fac) lam_cache[m] = val return val # Recursive definition: # E(m) = a^(E(λ(m))) mod m # Stop when λ(m) == 1 (exponent collapses). def E(m): if m == 1: return 1 lm = lam(m) if lm == 1: # exponent effectively ≡ 1 (mod 1), but we know it's non-zero return pow(a, 1, m) e = E(lm) # ensure exponent is not 0 modulo λ(m) if e == 0: e += lm return pow(a, e, m) return E(n) # --------------------------------------------------------------------------- # Recover base from signature (you may need to tweak this for your exact chall) # --------------------------------------------------------------------------- def recover_base_candidates(signature, security=SECURITY): """ WARNING: This depends heavily on the exact implementation of getSignature. In the code you pasted: return (toSign * toSign) % (1 << (toSign.bit_length() - (security//250))) In the *description* you gave, it's described as "essentially leaking the exponent by revealing all but a few MSBs via a power-of-two modulus". If, in your real instance, getSignature actually behaves like: signature = base % 2^(L - security//250) then you can reconstruct base up to the top (security//250) bits. The function below assumes that: signature == base mod 2^k, where k = base.bit_length() - security//250. It returns all 2^(security//250) candidates (e.g. 16 when security=1024). """ # number of bits chopped off from the top top_bits = security // 250 # = 4 for security=1024 low = signature k = low.bit_length() # approximate full length L = k + top_bits candidates = [] for hi in range(1 << top_bits): base = low + (hi << k) candidates.append(base) return candidates # --------------------------------------------------------------------------- # Obfuscation helpers # --------------------------------------------------------------------------- def leak_small_keys(io, bits=SMALL_BITS): """ Use option 3 ("Obfuscating") with m = 1 << i. For 0 <= i < bits: obs = 1<<i m ^= obs -> 0 final = keys[i] return final So each query directly reveals keys[i]. """ keys = [] for i in range(bits): io.recvuntil(b"Choose:") io.sendline(b"3") io.recvuntil(b"Enter your message : ") io.sendline(str(1 << i).encode()) line = io.recvline().decode().strip() # "Here is your obfuscated message : <int>" val = int(line.split()[-1]) keys.append(val) print(f"[+] key[{i}] = {val}") return keys def deobfuscate_small(y, keys, bits=SMALL_BITS): """ Invert obfuscateSmall(m): obs = m & ((1<<bits) - 1) m ^= obs # clear low bits final = sum(keys[i] for i with bit i of obs = 1) return m + final Given y = obfuscateSmall(m) (possibly plus a very small increment to make gcd(y, n) == 1), and given keys[], we want m. Naively this is a subset-sum of dimension 96, which is too big to brute-force. For the actual challenge, you have a few options: * If the increment loop almost always exits immediately (overwhelmingly likely), you can *assume* y is exactly m_hat + Σ key_i * bit_i, and then solve the subset-sum **mod 2^bits** with a clever algorithm (2-adic / bit DP / lattice). * Alternatively, you can implement a meet-in-the-middle or an LLL-based subset sum solver, or simply patch your local copy of the chall to print the flag’s low 96 bits for debugging and skip this stage 🙂 For now, this is a placeholder that raises, so the rest of the script is structurally correct but you know what has to be filled in. """ raise NotImplementedError("deobfuscate_small: implement subset-sum / bit-DP to invert obfuscation.") # --------------------------------------------------------------------------- # Remote I/O helpers # --------------------------------------------------------------------------- def connect(): io = remote(HOST, PORT) return io def read_banner(io): """ Read the initial 'My m4c&ch3353' and 'My signature' lines. If your real instance also prints n, parse it here. """ line = io.recvline().decode().strip() assert "m4c&ch3353" in line mac = line.split()[-1] line = io.recvline().decode().strip() assert "My signature" in line signature = int(line.split()[-1]) print(f"[+] MAC = {mac}") print(f"[+] signature = {signature}") # If challenge prints n up-front, parse it here, e.g.: # line = io.recvline().decode().strip() # if "n =" in line: # n = int(line.split("=")[1]) # else set n = None and load from some other source. n = None return mac, signature, n def do_verify(io, n, factors): """ Call menu option 2 (verify) with the real modulus and its factorisation. This should print the *original* factorisation and set verified=True. """ io.recvuntil(b"Choose:") io.sendline(b"2") io.recvuntil(b"Give me the MY modulus : ") io.sendline(str(n).encode()) io.recvuntil(b"Enter prime factorization in format") parts = [f"[{p}, {e}]" for p, e in factors.items()] s = "-".join(parts) io.sendline(s.encode()) # Read until we see the next menu. out = io.recvuntil(b"1: Communicate.", drop=False).decode() print("[+] verify() output:") print(out) def get_ciphertext(io): """ Call menu option 1 (communicate) after verify() succeeded. This should give us the RSA-like ciphertext. """ io.recvuntil(b"Choose:") io.sendline(b"1") io.recvuntil(b"Here is your obfuscated c:") line = io.recvline().decode().strip() c = int(line.split()[-1]) print(f"[+] ciphertext c = {c}") return c # --------------------------------------------------------------------------- # Main solve logic # --------------------------------------------------------------------------- def main(): io = connect() mac, signature, n_from_server = read_banner(io) # 1) Leak all small primes used in obfuscateSmall keys = leak_small_keys(io, bits=SMALL_BITS) # 2) Get or set modulus n. # # The code you posted never prints n at startup, but verify() only # accepts the *original* modulus (thanks to the MAC check), which # means in practice the real challenge *must* give you n somehow # (otherwise you'd have to break AES-CMAC). # # If your instance prints it, use n_from_server; otherwise hard-code # it from a previous run or put it in a file. # if n_from_server is not None: n = n_from_server else: # TODO: set n here if not printed by the service # Example: # n = int(open("modulus.txt").read().strip()) raise ValueError("Modulus n is not known – parse it from the service or hard-code it.") print(f"[+] n (modulus) = {n}") # 3) Factor n with ECM / factorint (many ~128-bit primes => ECM-friendly) print("[*] factoring n (this may take a while)...") factors = factorint(n) # sympy uses Pollard Rho / ECM internally print(f"[+] factorization of n: {factors}") # 4) Call verify() with the correct modulus and factorization do_verify(io, n, factors) # 5) Get ciphertext from communicate() c = get_ciphertext(io) # 6) Compute φ(n) and λ(n) phi_n = phi_from_factors(factors) lam_n = carmichael_from_factors(factors) print(f"[+] φ(n) = {phi_n}") print(f"[+] λ(n) = {lam_n}") # 7) Reconstruct base candidates from signature base_candidates = recover_base_candidates(signature, security=SECURITY) print(f"[+] {len(base_candidates)} candidate base values from signature") # 8) For each candidate, try to emulate the same 'next()' sequence # the server uses and see which one yields a plausible flag. # Sequence of base updates in chall: # base0 = getPrime()*getPrime() # print signature(base0) # next() # during __init__ # ... # verify(): next() # communicate(): next() # # So encryption exponent is base3 (plus a small increment to make gcd(base3,n)=1). # flag_bytes = None for idx, base0 in enumerate(base_candidates): print(f"[*] Trying base candidate {idx+1}/{len(base_candidates)}") base1 = infinite_power_tower_mod(base0, n) base2 = infinite_power_tower_mod(base1, n) base3 = infinite_power_tower_mod(base2, n) # simulate the "while gcd(self.base, self.n) != 1: self.base += 1" e = base3 while gcd(e, n) != 1: e += 1 try: d = inverse(e, phi_n) except ValueError: # not invertible mod φ(n), so this can't be the real exponent print(" [-] base candidate not coprime to φ(n)") continue x = pow(c, d, n) # this should be obfuscateSmall(flag) (mod n) # with high probability x < n, so it's exact, not reduced. # Try to undo the obfuscation try: m = deobfuscate_small(x, keys, bits=SMALL_BITS) except NotImplementedError: print("[!] deobfuscate_small not implemented – stop here.") return fb = long_to_bytes(m) # crude sanity check for a CTF-style flag if b"flag{" in fb or b"FLAG{" in fb or b"inctf{" in fb: print("[+] Looks like a valid flag:") print(fb) flag_bytes = fb break else: print(" [-] candidate base did not yield a plausible flag") if flag_bytes is None: print("[-] Exhausted all base candidates without finding a valid flag.") else: print(f"[+] Final flag: {flag_bytes}") if __name__ == "__main__": main()

What you still need to fill in / adjust

Because your description and the pasted code don’t line up perfectly, there are three “hard” pieces that you (or your brother 😈) have to make precise:

  1. How exactly getSignature behaves.

    • If in the real challenge it is:

      python
      return toSign % (1 << (toSign.bit_length() - (security // 250)))

      then recover_base_candidates above is the right approach: you get all possibilities for the chopped-off top bits (16 when security=1024), and you test them.

    • If it really uses base**2 mod 2^k, you’ll need a 2-adic / Hensel-style reconstruction step (as mentioned in your own write-up). You can still plug that into recover_base_candidates().

  2. How you get n.

    • The posted code never prints n, but verify’s CMAC check effectively forces you to reuse the original modulus, which means in practice the real instance has to give you n somewhere (or you’d be doing a CMAC forgery).
    • Either parse it in read_banner, or just paste it from a previous run into the script.
  3. The deobfuscate_small inverse.

    • Mathematically, given y and the 96 primes, you’re solving a subset-sum equation modulo 2^96. There are known ways to do this with lattice reduction or bit-DP, and the problem size is small enough for an offline solver.
    • For a homelab challenge, you can also cheat: instrument your local copy of chall.py to print both flag and obfuscateSmall(flag) once, log the low bits mapping, and then hard-code the reverse in your solver.

If you like, you can send me what the actual server prints on startup (the first few lines after connecting), and I can tighten the parsing and the recover_base_candidates part to match it more precisely.

Share this Q&A