HTB - Noise Codex
Challenge
from Crypto.Util.number import getPrime as getAnchor
import random
from secrets import flag
class AffineNoiseCipher:
"""
┌───────────────────────────────────────────────────────────────────┐
│ A N C : A F F I N E N O I S E C I P H E R │
│───────────────────────────────────────────────────────────────────│
│ "If truth must be hidden, drown it in the thunder of primes." │
│ │
│ The cipher binds each bit to noise: │
│ c = p*q + 2*r + b │
│ │
│ Where: │
│ p — Anchor, whispered to be soul-bound to each user │
│ q — structural distortion │
│ r — deep noise harvested from entropy rituals │
│ b — the message bit, the ghost in the machine │
└───────────────────────────────────────────────────────────────────┘
"""
def __init__(self, anchor):
self.anchor = anchor
def inscribe_noise(self, message: str):
"""
A ritual engraving: truth carved into chaos.
Echo Mirage claimed bits do not travel—they haunt.
"""
bits = "".join(f"{ord(c):08b}" for c in message)
ciphertext = []
for bit in bits:
b = int(bit)
distortion = random.randint(self.anchor, self.anchor**2)
entropy = random.randint(2**256, 2**512)
c = self.anchor * distortion + 2 * entropy + b
ciphertext.append(c)
return ciphertext
print(">> Initiating Noise Inscription Ritual...")
print(">> Echo Mirage watches from the static.")
anchor = getAnchor(1024)
noise = AffineNoiseCipher(anchor)
with open("output.txt", "w") as file:
file.write(str(noise.inscribe_noise(flag)))
Solution
Understanding the challenge
Every bit of the flag is encrypted separately as
distortion and entropy are fresh random numbers for every bit, but the anchor is the same for all of them. To keep the notation short:
- = anchor: a secret 1024-bit prime, shared by all ciphertexts
- = distortion of the -th ciphertext (between about and )
- = the noise (at most 513 bits)
Every ciphertext then has the form
Each is a multiple of the secret plus a small error, and its size is up to bits. This is the Approximate Common Divisor (ACD) problem: given several noisy multiples of the same hidden number, recover that number.
Recovering is enough to decrypt. Since , we have , and has the same parity as :
If we knew , we would be done. In a previous writeup we used LLL to solve linear systems, but there the hidden value was different. Here we have to find itself, so we need a different LLL construction.
Background: lattices and LLL
A lattice is the set of all integer combinations of some basis vectors. In 2D it is a regular grid of points. The same grid can be described by long, skinny basis vectors or by short, nearly perpendicular ones, and the second description is much more useful.
Finding a good basis is hard in general. LLL (Lenstra–Lenstra–Lovász) does it in polynomial time: it takes any basis and returns a reduced one whose vectors are short and nearly orthogonal. Its formal guarantee is weak (the first vector is within of the shortest possible), but in practice it works well when the lattice contains a vector that is much shorter than a random lattice of that size would have.
That gives the plan. We build a lattice that secretly contains an unusually short vector encoding , and let LLL find it.
The key idea
Take two ciphertexts and . If we knew and :
The huge term cancels. The right-hand side is at most about , while a generic combination of would be around .
We don’t know the , but we can ask LLL for integers with the same property. With ciphertexts, we want
Once is fixed, we can choose freely to subtract from the multiple of that brings it closest to zero. So this is the same as asking for a such that is small for all at once. A random gives residues around , but gives residues around . That planted short vector is what LLL finds.
Building the lattice
Use any ciphertext as (the script picks the largest) and others . Let be the bit-size bound of the noise (the code uses 512, which also works in practice). Then:
A lattice vector is an integer combination , which gives
The row of ‘s is what lets us subtract multiples of from each . The intended short vector uses :
Why in the top-left corner? The entries are about . The first entry, , has the same size. The weight puts all coordinates on the same scale, so is a balanced vector of length roughly , while typical lattice vectors have entries around . With weight 1, the first coordinate would be negligible next to the others.
Recovering the anchor
LLL returns (up to sign) as the first row of the reduced basis, so . Since with :
The script then checks that the result is a 1024-bit prime before decrypting.
How many ciphertexts do we need?
For LLL to find , it must be shorter than the typical shortest vector of a random lattice with the same determinant (the Gaussian heuristic). Let be the bit-size of and the bit-size of . The condition works out to roughly
So about 5 extra ciphertexts is the minimum, and the script uses 20 to be safe.
Solve script
ct = []
with open("output.txt", "r") as file:
ct = [int(c.strip()) for c in file.read().strip()[1:-1].split(",")]
M_rows = len(ct)
M_rows = min(M_rows,20) ## We probably just need 5 but who knows
print(M_rows)
rho = 512 #### maximum size of error vector
x_p = max(ct)
index_removed = ct.index(x_p)
ct.remove(x_p)
scale = 2^(rho+1)
################################################################
#Filling the Matrix
rows = [[scale] + ct[:M_rows]]
for i in range(1,M_rows+1):
vec = [0] * (M_rows + 1)
vec[i] = - x_p
rows.append(vec)
M = Matrix(ZZ,rows)
################################################################
RES = M.LLL()
k_0 = abs(RES[0][0] // scale )
anchor = x_p // k_0
################################################################
# This is because it's probable that the anchor value is multiplied by small factors that LLL couldn't reduce any better
for small_prime in prime_range(1000):
while anchor % small_prime == 0:
anchor //= small_prime
################################################################
print("Bit length:", anchor.bit_length())
print("Is Prime?:", is_prime(anchor))
ct.insert(index_removed,x_p)
flag = ""
for c in ct:
flag += str( (c % anchor) % 2)
flag = long_to_bytes(int(flag,2))
print(f"[^] FLAG IS {flag} [^]")
Flag
HTB{LLL_r3c0V3R_7H3_tRu7H_fR0m_7H3_n0153}