CyberSecurityRumble 2020

General Comments

Overall a pretty good CTF. Decent number of tasks, variety, difficulty level and so forth. The stuff I looked at seemed alright, though I got entangled in some web business toward the end that soured things but that was my own fault.

We placed a cool 10th tho (much thanks to poiko spamming flags left and right the last day).

There wasn‘t an abundance of crypto or mathematical misc tasks, but then I don‘t really come to expect that much, it felt like there was enough. I kinda think of crypto/math as the awkward little sister that‘s there only because the organizers‘ mother forced them to bring her along. The cool kids are talking about real world stuff, hacking the Gibson, phreaking the phone lines, doing bug bounties, whatever it is that real hackermen do, while she‘s just sitting there arranging her fries to count out the Stirling numbers. Most of the time I‘m just glad she‘s invited at all; if CTFs were to reflect real world industry, I suspect it‘d be like 30 web-tasks and 2 revs.

I solved most of the easy crypto on the first evening, then did dtls the following morning, but the two remaining tasks were labelled web. Ugh. I looked at the task named blow and just thought “fuck no.“ Some kind of hellish JWS/JWT JavaScript task they kept adding hints to because it got no solves. From the hints alone I surmised that it has some trivial solution in crypto terms, but it was probably a pain in the ass (aka the “web“ part) to even get started or progress to where it‘s relevant? So I worked on Yanote instead, but failed that task miserably and eventually went off to nurse my imposter syndrome in the corner.

Pady McPadface

A live server that knows the flag traverses it bit by bit and gives for a fixed RSA modulus using the standard . is an ephemeral random number a few bits than so .

So basically I wanted to discover if the ciphertexts were quadratic residues or not under the given (composite) modulus. I knew what to look up, and it turned out to be pretty easy, but I was actually surprised to learn the Jacobi symbol is easily calculable without knowing the factorization of . Huh! (It‘s also surprising that this has never come up before.) I‘m glad to have learnt it, it does seem like a neat trick.

def trailing_zeros(a):
  return (a ^ a - 1).bit_length() - 1

def jacobi(a,n):
  assert n & 1 # n must be odd
  sign = 1

  a = a % n
  while a > 0:
    r = trailing_zeros(a)

    # 2 divides an odd number of times into a, then flip the sign if n is not
    # on the form 8k±1
    if n % 8 in (3,5) and r & 1:
      sign = -sign

    a,n = n, a >> r
    if a % 4 == 3 and n % 4 == 3:
      sign = -sign
    a = a % n

  if n != 1:
    # a divides into n
    return 0

  return sign

So then it was a matter of connecting to the server and getting several ciphertext instances and calculating their Jacobi symbols . If you find an instance where it is -1 then you know the number is not a quadratic residue, so must be 1 in that position. After some collected data you set the others to 0 and get the flag.


The task implements some basic code for working with the secp256k1 elliptic curve (). The ECC implementation is “home-made“ with code mostly taken from Wikipedia. It asks for a point, multiplies it with a random secret number and asks you to recover this secret. No check is done on whether or not the point you give is actually on the secp256k1 curve though, so it‘s a classic invalid curve attack.

Basically you can set the parameter in to whatever you want. (If you give the point it will use the curve .) I‘ve solved some task before using a singular curve but couldn‘t remember how the mapping worked off the top of my head and didn‘t find my old code for it, so instead I idly looked for other parameters where I could solve the discrete log with Pohlig-Hellman as I had the tools for that more readily available. I think there‘s 7 (?) different Frobenius traces among these curves, corresponding to different orders of the resulting elliptic curve, but each had a very large factor (133-135 bits) so this turned out to be a dead end.

I went back to pen and paper to see if I could manually work out the singular mapping, partly as self-punishment for forgetting. Given one of the simplest non-trivial points, , the server will use the singular non-elliptic curve . The point-doubling formula then does the following: and similarly . Hm! That gave me the needed deja vu: indeed it seems that , so it worked out nicely, and I didn‘t have to wade through the full algebra. So yeah, just feed it (1,1) and then do to get the secret.

It‘s probably supposed to be a pretty trivial task, but I made it overly complicated by investigating all the curve-stuff.


A server is signing messages using the Python ecdsa library. I don‘t remember the exact details but I think you were supposed to forge a signature. Immediately there‘s was a glaring red flag with a entropy=sony_rand parameter where sony_rand was a function that returned random bytes using Python‘s random module. ecdsa uses this for generating the ephemeral in its DSA signature generation.

At first I thought this was going to be a very challenging task, because even though the randomness of getrandbits isn‘t of cryptographic quality, it‘s very hard to actually mirror it unless given at least some white (fully revealed) output. I know it‘s seeded in Python from PID and two different clock functions, so it might be bruteable, but it‘s hard to predict how far into the stream the server is and so on and so forth; it seemed very daunting to brute all that. I wondered if you could get startup time or restart the server, or maybe the Mersenne Twister output had some cool linear relationship I wasn‘t aware of…

I was just being dumb. I didn‘t notice it was a forking TCP server… It will clone the state and basically just re-use the same ephemeral every single time. This becomes immediately obvious when you set to actually collect some signatures and see the being repeated. I got pretty lucky that it was so obvious once you start collecting data, if not it‘d be screwed.

So from two signatures and where you can calculate the hashes , recovering the private multiplier is trivial and you can sign anything. I think I just set sk.privkey.secret_multiplier = x and used ecdsa to do it in REPL.


A server will run any Python code that‘s a valid palindrome (x == x[::-1]) in a Python subprocess (of a user that can‘t read the flag file), but then exec the stdout from that process (in the process that can read the flag file). There‘s a limited attempt at preventing the task from becoming trivial: it will remove any of the substrings "#", '"""', "'''" from the input. The removal happens only once, in order. So, the a-ha: you can actually use """ by giving something like "'''"". The server will remove the single quotes and keep the double ones.

Unfortunately I don‘t have the actual solution at hand since I can‘t find it in my REPL history, but the basic idea was to just use a simple payload like print("__import__('os').system('sh')") and then figure out how to make that into a palindrome. I think I settled on something like """;";""";<code>;""";<reverse of code>;""";";""". Notice how """;";""" is interpreted to be the string ';";' when evaluated normally, but also provides an end for the triple-quote: ..."""; ";" "".

(Another idea would be to use some trickery involving the escape character r"\" which could potentially escape/unescape strings depending on order, but I didn‘t think of that at the time.)


Trivial welcome-task I had missed until poiko reminded me. It basically does print(flag ^ flag[4:]) (if one were able to do that in Python). You know flag starts with CSR{ which gives you the next four bytes, and those gives the next four, and so on.


The task gives you a pcap file and this shell script:

# /usr/lib/|head -n 1
# GNU C Library (GNU libc) stable release version 2.31.

# clone
git clone dtls

# build
cd dtls && autoconf && autoheader && ./configure && make && cd ..

# listen
tcpdump -i lo  port 31337 -w dump.pcap &

# Wait for tcpdump...
sleep 2

# serve
dtls/tests/dtls-server -p 31337 &

# send flag
cat flag.txt|dtls/tests/dtls-client -p 31337

I got the repo and looked around. I know it implements basic TLS over UDP, but ignored all that. The first thing I searched for was RNG. It doesn‘t use cryptographic source of random numbers but instead will just use srand() and rand() from libc (as hinted to in the script above). That‘s a problem in and by itself.

But it also turns out that the code is buggy, for example it initializes the RNG with dtls_prng_init((unsigned long)*buf), where buf was read to from /dev/urandom. It‘s probably intending to use a long for the seed, but guess what? buf is a char array, so it‘s actually just using a single byte for the seed to generate all those important crypto bytes.

Now, how to actually attack it. I knew I didn‘t want to actually interact with TLS, because TLS is web and web is a hell run by committees, so instead I did the following:

  • Make it print out the random bytes it generates. Run the script and compare it with the given pcap to figure out where those numbers go and what they should actually be.
  • Trivially brute the seed.
  • Modify the library to load its seed from an environment variable so I could run the client and server with fixed seeds.
  • Force its internal time function to return 0, as I can see from the pcap that‘s what it used there. (It actually tries to use integer seconds since the start of the program, thus 0 since the transfer happens immediately.)

I ran the script again and compared my pcap with the one given. It‘s generating and using the same numbers, uses the same cookie, etc. So then I simply copied the final encrypted packet from the pcap file and put it into the source code with an ugly hack like this:

static char bufzzz[] = "...stuff...";

static int
decrypt_verify(dtls_peer_t *peer, uint8 *packet, size_t length,
           uint8 **cleartext)
  if (packet[0] == 0x17) {
    printf("pulling the old switcharoo\n");
    packet = bufzzz;
    length = 84;

  // ...

For some reason (that I don‘t care about) this causes TinyDTLS‘ own debug printing of the message to get messed up, so I also had to print the cleartext myself at the end of the function. Out comes the flag. No TLS needed (thank God).

This was a pretty cool task. It was practical and “real-world“ but without feeling contrived and without there being JavaScript or any web frameworks involved. It was possibly the “cleanest“ real-world-y task I‘ve seen. Well done to the task author.


I didn‘t solve this one. I failed it. Not only did I fail it technically, but I failed it mentally, which is probably worse. But I worked on it for a while so I‘m still going to write about it. It triggered all sorts of feelings of inadequacy and failure that you wouldn‘t believe.

All because of this “web“ tag.

The “web“ tag usually means “use educated guesses for any blackbox behavior“ but to me it usually reads as “realize that nobody agrees with what you think is reasonable.“ It reads “smell that? Take a deep whiff, that‘s the smell of learned helplessness, baby.“

Clue: the server says “invalid CRC-HMAC“ if you give it an invalid cookie.

Ignore the pedantic fact that it would normally be called HMAC-<name> and “CRC“ is very non-specific, kind of like just saying “HASH.“

However CRCs are more fun than hashes in that they‘re easier to play around with computationally. So for a second I dared to dream about some sort of semi-fun task of trying to figure out the modulus and key and all the other constants of some unknown HMAC(key, ipad, opad, H=CRC(modulus, a, b)) given an oracle(x) = HMAC(key, A+x+B). That would have been pretty nice actually.

So my approach was all wrong. Because I started to hear this “looOOooOOoOool“ coming from a dark corner of my mind: the “web“ tag coming back to haunt me. So maybe none of that cool stuff. Maybe it‘s just…straight up CRC-32…? Well, yes and no.

See, I hate this. Instead of “figuring something out“ you have to guess what they mean about what that something is. Since it‘s “web“ and in web they tend to not be too particular about things, HMAC might mean something else. Potentially it could be a homegrown custom HMAC, potentially it could be something else entirely. It‘s not even given that it does HMAC(<key>, <pickle-data>). For all you know it could do F(<key>, <username> + ":" + <permission>) or whatever. Hell, maybe it‘s not even a CRC! Maybe it‘s just adler32(<data> + <secret>)

Okay, relax. Start testing some basic stuff.

I figured out it had normal CRC behavior at least. It was, as they say, affine-ish. Working with polynomials over , where is a constant that depends on the byte-lengths of x and y (technically on their xor difference?). You‘d expect this to work for any HMAC based on a CRC too. This allows you to (easily) log in as admin (but on the admin page I just found people doing “test post pls ignore“ and being generally “????????“).

But when I actually tried to “solve“ the blackbox function completely, I kept getting wrong constants everywhere. And I didn‘t know what was wrong, didn‘t know if it‘s because I had bugs in the old code I was using (some GF2[X] implementation from way back), or had some wrong assumption.

After a while I suspected there was no HMAC and maybe just a straight up unknown CRC, which frustrated me even more because if so, then this should be easy, no? What if something else is different? I spammed random stuff trying to figure out the modulus for the CRC from the constants I was getting out, double-checked my math… I even wondered if you were supposed to birthday-hammer user-create to find a duplicate checksum, and maybe that would finally reveal what was going on, got completely lost…

Getting full control over the cookie is trickier, but in my more clear-headed state I think it‘s possible without solving the black-box function as I was trying to do. I believe all you need is in the relationship. You can get all the constant K terms you need by making three users that xor to zero for any given length. For an unknown CRC, for three constants , and (that you‘d get from ("0","q","A") for example, since they xor to 0), then you can find the modulus as a factor of (does that work? Now I‘m not sure, I‘m copying a note…), then the relationship between the various as (TODO: math might be wrong), and you should have everything you need.

Then you make usernames that xor together to be your pickle payload (the server accepted non-printables in username) and change the byte for the username length (this last part is a pen-and-paper mess, so it‘s not something I‘d actually try unless I was sure). The stuff after the byte 0x2e (".") in the pickle data is ignored, so it‘s fine if the cookie ends with garbage. This was probably the intended solution, though it‘s a bug-prone approach, maybe a bit painful to implement.

However I basically just got so frustrated with not getting the math to work out (I mean, I couldn‘t even figure out the damn modulus![^10]) and debugging my own code that I gave up. I have an issue with committing to guesses unless I have a reasonable idea it will work. Failure-sensitive. Besides, engaging with guessy stuff feels dirty unless you vibe some level of mental harmony with the task author (which I didn‘t).

Dirty confession: I even debased myself and tried to brute force a 32-bit key against HMAC-CRC32 using the usual constants, but it didn‘t work. That was the last straw. After that I signed off and went for a long walk to hang out with the ducks by the river (not a metaphor—there are actual ducks).

Addendum: it was plain CRC-32, and the “HMAC“ just a salt being added to the data bytestring. Prefix or postfix salt? Length of salt? Well, it‘s web, so the answer could be to just guess and try it, and if that doesn‘t work, guess something else.