Boston Key Party 2015 'Airport' writeup
Airport was a cryptography challenge worth 500 points at the Boston Key Party CTF 2015.
We’re provided access to a remote server that implements a custom crypto protocol:
- Let p = 2q + 1 = 2732…5727 be a safe prime (i.e. q is prime too).
- Choose a random integer s from the range [1, (p-1)/2).
- Read an integer b from the client.
- If bs ≡ 4 (mod p), send the flag to the client. Otherwise, send bs mod p to the client and go to step 3.
Reconstruction s from only a set of (b, s) pairs is hard according to the Discrete logarithm conjecture. However, the server also leaks timing information to the client because it uses the following square-and-multiply implementation for step 4:
def slowpower(base, power, mod):
accum = 1
for bit in bin(power)[2:]:
if accum == 4:
time.sleep(1.0)
accum = accum*accum % mod
if bit == '1':
accum = accum*base % mod
return accum
From now on, we assume that we can compute arbitrary roots modulo p, i.e. we can compute a function root(n, y, p) with the property that (root(n, y, p))n ≡ y (mod p). We will go into the details of how to implement this function below.
Equipped with this, we can:
- Given s, compute b such that bs ≡ 4 (mod p), using b = root(s, 4, p). This will allow us to solve the task once we figure out s.
- Check whether s starts with a given bit string P. Let’s say P = 101.
If s = 101…2 (the subscribed 2 means base 2, i.e. binary), then
after 3 iterations of the
slowpower
loop, we have accum ≡ base1012. If base = root(1012, 4, p), then this will cause the sleep function to be called, which we can measure. If the binary representation of s does not start with 101, then it is very likely that no sleep will occur.
Using the second primitive, we can reconstruct s bit by bit using the following algorithm:
s = socket.create_connection((SERVER, PORT))
# ... solve proof-of-work test
def oracle(b):
""" Let the server compute b^s mod p. If the result is 4, print
the flag. Otherwise, return the result and the timing. """
t0 = time.time()
s.send(str(b) + '\n')
ans = s.recv(4096)
t = time.time() - t0
if any(c not in "0123456789" for c in ans):
print ans
sys.exit(0)
return int(ans), t
prefix = 1
while True:
prefix <<= 1
# check the two possible hypotheses on the extension of s
_, a = oracle(root(prefix | 0, 4, p))
_, b = oracle(root(prefix | 1, 4, p))
print bin(prefix>>1), a, b
# no difference in timing, we are at the last bit
if abs(a - b) < 0.5:
break
if b > a:
# the version with an appended 1 bit took longer, so it is the correct guess
prefix |= 1
# there are two possible options for the last bit, check them using the
# exponentiation oracle
for c in (prefix | 0, prefix | 1):
if oracle(13)[0] == pow(13, c, p):
s = c
break
else:
# something went wrong
assert 0
# send the final guess and get the flag
oracle(root(s, 4, p))
And, after apprixmately 2048 seconds of waiting:
0b1 1.10669398308 0.115952014923
0b10 0.0756859779358 1.15882515907
0b101 0.160202026367 1.19808292389
0b1011 0.161967039108 1.16554093361
0b10111 1.18343901634 0.153500080109
0b101110 0.153550863266 1.1789689064
0b1011101 1.18081712723 0.135214090347
...
FLAG{diffie_hellman_im_awfully_fond_of_youuuuuu}
All that remains is filling in the implementation of the root function and the captcha solver.
Computing arbitrary roots modulo p
The problem is, given n and y, find an x such that xn ≡ y (mod p).
Assume that n = 2k + 1 is an odd number. We then have gcd(n, p - 1) = 1 because p - 1 = 2q with q prime. Thus, the modular inverse of n modulo p - 1 exists. Let’s call it u. It can be computed using the extended Euclidean algorithm. By definition, we have u · n ≡ 1 (mod p - 1). With x = yu we then have *xn ≡ yu · n ≡ y1 + k(p - 1) ≡ y · (yp
- 1</sup>)k (mod p)* for some k. Now according to Fermat’s little theorem, this means that xn ≡ y · 1k ≡ y (mod p). Thus x is the n-th root of y that we were looking for.
The case with n = 2k is a bit more complicated: First note that if z is a square root of y, i.e. z2 ≡ y (mod p), then x = root(k, z, p) is the solution we are looking for. Luckily for us, computing square roots modulo the given p is not hard. Since p ≡ 3 (mod 4), we can use a theorem due to Lagrange to compute a square root z: It says that z = y(p+1)/4 mod p is a possible solution.
For the case where n is even, we can alternatively use the fact that we always have y = 4 to make our lives a bit easier: If gcd(n, p - 1) = 2, we can find a v such that v · n ≡ 2 (mod p - 1). Then we have x2 ≡ xv · n ≡ yv ≡ 4v and thus x = 2v is a possible solution.
In Python, the implementation of the more generic approach may look like this:
def ex_gcd(a, b):
x1 = 0; y1 = 1
x = 1; y = 0
while b:
q = a / b; r = a % b
x2 = x - q * x1; y2 = y - q * y1
a = b; b = r; x = x1; x1 = x2; y = y1; y1 = y2
return a, x, y
def root2(y, p):
""" Solve x^2 == y (mod p) """
assert p % 4 == 3
x = pow(y, (p+1)/4, p)
assert pow(x, 2, p) == y
return x
def root(n, y, p):
""" Solve x^n == y (mod p) """
g, u, _ = ex_gcd(n, p-1)
if g == 2:
return root(n/2, root2(y, p), p)
assert g == 1
u %= p-1
x = pow(y, u, p)
assert pow(x, n, p) == y
return x
Solving the captcha
This part is very standard, we just brute-force the suffix until we find a match:
pref = s.recv(4096)
assert len(pref) == 12
print "Solving challenge %s" % pref
i = 0
while True:
answer = pref + ''.join(chr(random.randint(0,0xff)) for _ in xrange(8))
assert len(answer) == 20
ha = hashlib.sha1()
ha.update(answer)
if ha.digest().endswith('\xff\xff\xff'):
break
i += 1
if i % 100000 == 0:
print i
print "Done!"
s.send(answer)
The final exploit code can be found in our Github repository.