OKAY with this information you have: The program ...
Tạo vào: 7 tháng 12, 2025
Trả lời bằng GPT-5.1 Thinking bởi Chat01
Tạo vào: 7 tháng 12, 2025
Trả lời bằng GPT-5.1 Thinking bởi Chat01
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):
textself.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()
textsteps = 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.")
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:
pwntools, sympy and pycryptodome locally.My m4c&ch3353: <hex>My signature: <int>n = <int> – if not, you’ll hard-code or parse it some other way.)The solver does:
n if printed).obfuscateSmall by querying option 3 with inputs 2^i.n (either from the service output or from a file / constant).verify once with the real (n, factorization) so the service dumps the original factorization (and fixes self.n, self.data, self.verified).communicate to get the RSA-like ciphertext c.base (encryption exponent) from the signature.n (if needed) and follow the same sequence of next() calls as the server to get the final exponent e.φ(n) (or λ(n)), then d = e^{-1} mod φ(n).x = c^d mod n.Below is a reasonably clean implementation with a few TODO hooks where you’ll plug your “math heavy” pieces.
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()
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:
How exactly getSignature behaves.
If in the real challenge it is:
pythonreturn 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().
How you get n.
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).read_banner, or just paste it from a previous run into the script.The deobfuscate_small inverse.
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.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.