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.
dlog
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.
ezdsa
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.
misc:Pylindrome
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.)
Hashfun
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.
dtls
The task gives you a pcap file and this shell script:
# /usr/lib/libc.so.6|head -n 1
# GNU C Library (GNU libc) stable release version 2.31.
# clone
git clone https://github.com/eclipse/tinydtls.git 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 127.0.0.1 -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.
Yanote
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.