GreHack 2018 write ups
Introduction
I was at the GreHack security conference these past days, and my team, Greunion, travelled to France so that we can play together in the on-site CTF. Big up for them!
As usual, I focused on cryptography challenges, and you will find the write-ups for the challenged I solved below.
I would like to mention that this is the first CTF where a separate category for challenges mixing reverse engineer and cryptography was listed. Usually, you could find cryptography challenges in almost any other category (most notably: web and reverse engineering). I'm not sure about the need to create such a new category, but it was unusual enough to be mentioned.
Finally, I did not manage to solve the ransomware-like challenge, but from what saw it was close enough to a real ransomware, while at the same time being simple enough to be able to be solved during the CTF. And definitively a mix of reverse engineering was needed before starting working on the cryptography part, so it made sense to be in that new category.
Keccak is not always better (crypto 250)
TL;DR: reverse the order of deterministic steps.
This challenge starts with a pretty long description, telling us about an HMAC function, and how it was used to sign session cookies on a website. Our aim is to recover the secret key, in order to be able to sign any cookie we want and get admin privileges on the server.
Except that there is no web server involved in the challenge, instead we are only given a valid signature for a single session cookie. Oh and the HMAC function is not really an HMAC, but some custom code based on Keccak (known to be chosen for SHA-3).
Anyway, we have some C++ code that performs the HMAC, a good pointer on how to link that code against a given library. After looking into it, it does something like the following:
flag = '...' # 48 bytes
message = 'the-cookie-with-some-padding' # 48 bytes
state0 = flag + '\x00' * 2
state1 = keccakF(state0)
state2 = xor(state1, message + '\x00' * 2)
state3 = keccakF(state2)
hmac = state3[:48]
With keccakF
been taken from the library, but as far as we are concerned we
can be seen as some deterministic function.
Without looking too far, it is easy to see that the keecakF
function is in
fact a permutation, so it is bijective and all the steps can be reversed to get
the flag. In fact, the library implements the reverse permutation, so there is
not much to do. We are only missing the last 2 bytes to go from the output
hmac
to the state3
, so let's just bruteforce them:
#include "Keccak-f.h"
void bruteforce(UINT8 a, UINT8 b) {
KeccakF keccakF(400);
// end state: dc6b4bd3...
UINT8 state[50] = {220,107,75,211,82,66,218,84,62,202,18,101,66,5,193,
27,188,206,118,225,73,98,59,56,127,205,148,18,214,61,0,46,122,141,
195,211,162,215,125,189,115,102,104,249,179,20,208,32, a, b};
// "username=Nics,admin=false" followed by \x01 and null bytes
UINT8 message[50] = {117,115,101,114,110,97,109,101,61,78,105,99,115,
44,97,100,109,105,110,61,102,97,108,115,101,1,0,0,0,0,0,0,0,0,0,0,
0,0, 0,0,0,0,0,0,0,0,0,0};
// reverse the steps
keccakF.inverse(state);
for (int i = 0; i < 48; i++) {
state[i] ^= message[i];
}
keccakF.inverse(state);
// check initial state
if(state[48] || state[49]) {
return;
}
// print initial state
for (int i = 0; i < 50; i++) {
printf("%02x", state[i]);
}
printf("\n");
}
int main() {
for(int a = 0; a < 256; a++) {
for(int b = 0; b < 256; b++) {
bruteforce(a, b);
}
}
return 0;
}
Let's compile and run this code to get the flag:
$ g++ -Wall solve.cpp -IKeccakTools/Sources KeccakTools/bin/Keccak-f.o -o solve
$ ./solve | xxd -r -p | strings -n 48
GH18{Sponge_with_not_enough_capacity-->bad_idea}
Backdoored DH (crypto 300)
TL;DR: recover pre-master key when DHE's P is a product of two primes, use wireshark to decrypt the TLS 1.2 capture.
In this challenge, we are given a network capture of a TLS connexion to
decrypt. To help us, we also have the server code as well as a secret file
containing to primes (p
and q
) and their product (n
), RSA-style.
Reading the server code, we can see that the dhparam.pem
file used is custom,
but we don't have it. However, we can open the network capture, and we notice:
- The TLS connexion is using TLS 1.2 with the cipher suite
TLS_DHE_RSA_WITH_AES_128_CBC_SHA256
, meaning that a Diffie-Hellman key exchange is indeed performed; - That Diffie-Hellman
P
parameter is equal ton
(from the secret file).
After looking online for know backdoor in Diffie-Hellman exchanges, we find out that it is exactly the situation described in the fourth section of this paper (page 8). By performing the attack, we are able to recover the shared secret (i.e. the pre-master secret) out of the Diffie-Hellman exchanges.
Then, we can add it in Wireshark to be able to decrypt the network capture. To do so, we use the following resources:
- Dragon Sector's write-up of RuCTF 2014 Quals crypto 300, which explains the general idea;
- The Wireshark wiki to know how to create a key log file.
And of course, because TLS version 1.2 is used, a special version of the PRF function has to be used as well (RFC 5246 and not RFC 2246).
After a lot of determination, we are able to create a key log file able to decrypt the network capture in Wireshark:
from Crypto.Util.number import long_to_bytes
from tlslite.mathtls import PRF_1_2
from sage.all import GF, crt
# get inputs
with open('s3cr3t.txt') as f:
exec(f.read()) # p, q, n
g = 2
gx = 0x7e56137f...0154aa0c
gy = 0xb2fcf1b2...97673045
client_random = 0x835ee473...ea2a1234
server_random = 0x9a6c56b8...e71ebae4
# find g**(x*y) i.e. pre-master secret
a1 = GF(p)(gx).log(2) # discrete log mod p
a2 = GF(q)(gx).log(2) # discrete log mod q
x = crt(a1, a2, p - 1, q - 1) # may not be real x...
gxy = pow(gy, x, n) # ... but still real g**(x*y)
# compute prf (TLS 1.2 is used)
master_secret = str(PRF_1_2(
long_to_bytes(gxy), # pre-master secret
'master secret',
long_to_bytes(client_random) + long_to_bytes(server_random),
48
))
# wireshark compatible output
print 'CLIENT_RANDOM {} {}'.format(
long_to_bytes(client_random).encode('hex'),
master_secret.encode('hex'))
Even after all the time alternating between despair and trying harder, we were the first team to score that flag… first blood yeah!
HashMcQueen (re-crypto 400)
TL;DR: integer overflow.
This is a remote challenge for which we have the binary. Fortunately for us, the binary is quite easy to reverse with only a few functions not being obfuscated in any way.
In a loop, we provide:
- A string up to 256 characters (set to an empty string if not provided);
- A position in that string (cannot start with a
-
character).
And on each iteration, the SHA-256 of the provided string starting from the
provided position (up to strlen
of the remaining) is output back.
But before all that, an initialisation function reads the flag and put it in memory.
Without going into more reversing the binary, we could imagine that our attack will look like the following:
- Set a negative position so that the resulting string gives the last character of the flag;
- Get the SHA-256 of that character, bruteforce it;
- Repeat with the previous character of the flag.
To bypass the check against the minus character, we use a simple integer
overflow on the atoi
function.
And instead of trying to find the exact position of the flag, we start from
-1
downwards until we obtain the SHA-256 hash of the }
character (the last
char of the flag).
Creating the input:
# pre.py
for i in xrange(0x100000000, 0x100000000 - 1337, -1):
print 'h:{}'.format(i)
Filtering / bruteforcing the output:
# post.py
from hashlib import sha256
flag = '}' # avoid false-positives
while True:
print flag
h = raw_input().strip().lower()
for c in map(chr, xrange(128)):
if sha256(c + flag).hexdigest() == h:
flag = c + flag
break
Chaining the pre and post processings with some bash-fu:
$ ./pre.py | nc ... | egrep -o '[0-9A-F]{64}' | ./post.py
--<snip>--
18{4t_l3457_17_w45_n0t_crypt0}
H18{4t_l3457_17_w45_n0t_crypt0}
GH18{4t_l3457_17_w45_n0t_crypt0}
For a challenge in the re-crypto category, I must say that there was not much crypto in it, by I can be missing something (also: see flag value).
Note: when submitting this flag, the organisers asked me if I feel like it was worth 400 points, and indeed it was quite easy for a 400 points flag. Especially comparing it to the previous challenge which was worth 300 points…
Conclusion
It was a real pleasure to be able to attend the GreHack, and thanks to my teamates we were able to get the sweet victory with the first place!
As I'm writing these words on the train back home after a all-nighter, I must say that I am still quite happy about that!