In this challenge, we’re given chall.py and the output.
# chall.py
assert len(flag) == 64
m1 = bytes_to_long(flag[:32].encode())
m2 = bytes_to_long(flag[32:].encode())
p = getPrime(256)
def release(m):
print(hex((m \* pow(2, m, p)) % p)[2:].rjust(64, '0'))
print(hex(p)[2:])
release(m1)
release(m2)
release(m2-m1)
release(m2+m1)
So m1 converts the first 32 bytes of the flag to a long integer, and m2 converts the last 32 bytes of the flag to a long integer.
release(m) is a function that takes an integer m, computes (m * pow(2, m, p)) % p, and prints the result in hexadecimal format, padded to 64 characters with leading zeros.
Outputs:
hex(p)[2:]: The prime p in hex, without the ‘0x’ prefix. (call thisp)release(m1): Output for m1. (call thisc1)release(m2): Output for m2. (call thisc2)release(m2 - m1): Output for the difference. (call thisc3)release(m2 + m1): Output for the sum. (call thisc4)
So we get the following equations:
c1 = (m1 * pow(2, m1, p)) % pc2 = (m2 * pow(2, m2, p)) % pc3 = ((m2 - m1) * pow(2, m2 - m1, p)) % pc4 = ((m2 + m1) * pow(2, m2 + m1, p)) % p
The goal is to find the flag, which is m1 and m2.
An initial exploit idea which doesn’t work is to assume that m2 - m1 in c3 is small enough to be brute-forced. And then once we get d, we can get m1 and m2 from c1 and c2. But this doesn’t work.
So nvm, let’s try to solve the equations.
With
a = 2^{m1} mod pb = 2^{m2} mod p
We can rewrite the equations as:
c1 = (m1 * a) % pc2 = (m2 * b) % pc3 = (m2 - m1) * (b/a) % pc4 = (m2 + m1) * (b * a) % p
From equation 1, we can rearrange to get m1 = (c1 * a^{-1}) % p.
From equation 2, we can rearrange to get m2 = (c2 * b^{-1}) % p.
Equation 4,
c4 = (m2 + m1) * (b * a) % p
= (c1 * a^{-1} + c2 * b^{-1}) * (b * a) % p
= (c1 * b + c2 * a) % p
Equation 3,
c3 = (m2 - m1) * (b/a) % p
= ((c2 * b^{-1}) - (c1 * a^{-1})) * (b/a) % p
= (c2 * a - c1 * b) / a^{2} % p
c3 * a^{2} = (c2 * a - c1 * b) % p
So we need to solve
c3 * a^{2} = (c2 * a - c1 * b) % p
c4 = (c1 * b + c2 * a) % p
Rearranging the second equation, we get
b = (c4 - c2 * a) / c1 % p where c1 is not zero.
Substituting this into the first equation gives us a quadratic equation in a:
c3 * a^{2}
= c2 * a - (c4 - c2 * a)
= c2 * a - c4 + c2 * a
= 2 * c2 * a - c4
So we have c3 * a^{2} - 2 * c2 * a + c4 = 0 mod p.
This is a quadratic equation in a.
-
Form
c3 * a^{2} + b * a + c = 0 mod p -
Discriminant (b^2 - 4ac)
D = (-2 * c2)^{2} - 4 * c3 * c4 = 4 * c2^{2} - 4 * c3 * c4 mod p -
Roots: x = (-b ± sqrt(D)) / 2a mod p
a = [2 * c2 ± sqrt(D)] / (2 * c3) mod p
Sagemath has a nice built-in function to solve quadratic equations in finite fields. So we run this in Sage:
# solve.sage
p = 0xafe4dfec75d05b8204f949749dce9d69eaee982528f7e2c177862b4f12b635d9
c1 = 0x6d04f0ebde78ca72c0a65629cd6f2cc337319c05b266ed789843ea2bdf11551f
c2 = 0x61483d050ad72a0e6dda11e3f683fbac20ab17b4a26615ac3eb4fbaecef519bd
c3 = 0x13c9395628b7f90ff1675d73cc97ae24ea5c9993366364627d20f9f52b19fabb
c4 = 0x75e04f3f38420029fa57934de57b6fb59f9615e4be32eaa4460c57a47c2842ae
# Define the finite field
F = GF(p)
# discriminant
D = F(4 * (c2^2 - c3 * c4))
# Check if D is a quadratic residue
if D.is_square():
sqrt_D = D.sqrt()
inv2 = F(2)^(-1) # Inverse of 2 mod p
inv_c3 = F(c3)^(-1) # Inverse of c3 mod p
# possible values of a
a1 = (F(c2) + sqrt_D * inv2) * inv_c3
a2 = (F(c2) - sqrt_D * inv2) * inv_c3
# for each possible a, compute b, m1, m2
for a in [a1, a2]:
b = (F(c4) - F(c2) * a) * F(c1)^(-1)
m1 = F(c1) * a^(-1)
m2 = F(c2) * b^(-1)
m1_int = Integer(m1)
m2_int = Integer(m2)
if m1_int < 2^256 and m2_int < 2^256:
flag_part1 = m1_int.to_bytes(32, byteorder='big')
flag_part2 = m2_int.to_bytes(32, byteorder='big')
flag = flag_part1 + flag_part2
print("Possible flag:", flag.decode('utf-8', errors='ignore'))
else:
print("Discriminant is not a square, no solutions.")
Then we get the flag. Tbh I think the intended solution has to do with some recurrence relation, but I couldn’t find it so owells.