BlockCTF 2024 100% Speedrun WR (7:38:22)
Yesterday we participated in BlockCTF 2024, a quick midweek 24-hour jeopardy event. It was a nice change of pace from our usual college app and college grind that we are all busy with now.
In the end, we were not only able to place 1st, but we also full-cleared the event in just 7 hours and 38 minutes!! This left us the remaining 16 hours to make these writeups! We had a great time and thanks a bunch to the organizers for making these chals and being so kind and gracious throughout!
Now here are our speedrun splits for the event:
00:10 - Welcome (Misc)
Ultimate network connectivity speedrun challenge
flag{h0w_m4ny_bl0ckh3ads_d1d_1t_t4k3_t0_run_th1s?}
05:34 - echo (Pwn)
Standard 64 bit buffer overflow with no protections except for NX. They were even so kind as to provide us with a win function 😊
flag{curs3d_are_y0ur_eyes_for_they_see_the_fl4g}
Solvers:
10:40 - Only Ws (Pwn)
The program reads in 4096 bytes of input and then directly executes it as shellcode. Seccomp restricts us to only using the exit
and write
syscalls, but luckily the flag is loaded into memory and its address is leaked to us. All we have to do is setup one easy write syscall!
flag{kinda_like_orw_but_only_ws}
Solvers:
26:48 - Where’s Mey Key? (Crypto)
x25519 is implementing a scheme that involves scalar multiplication of a point, hence we can submit a point that is 0 (b\x00
*32), and the secret key will be 0 (b’\x00’*32)
flag{0000_wh0_knew_pub_keys_c0uld_be_bad_0000}
Solvers:
35:13 - echo2 (Pwn)
Almost identical source as the first echo challenge, but this time every protection is enabled. Luckily, the former puts
call in echo is now a printf
and this logic is wrapped in a while-true loop. This lets us take all the time we need to get leaks and then build our payload. The only leaks necessary are the canary and any ELF address, then it’s another standard buffer overflow!
flag{aslr_and_canari3s_are_n0_match_f0r_l3aky_stacks}
Solvers:
37:24 - Glitch in the Crypt: Exploiting Faulty RSA Decryption (Crypto)
This computes, for an input , the crt of and , and takes the crt to find .
When there is a fault, it randomizes . If this is the case, , but , hence . To exploit this, we submit 0x0
repeatedly to the server, until we get a decryption that is not 0x0
:
We can then take the gcd of this and n to get the factorization of the secret key:
We then grab a flag encryption from the server:
And decrypt using our recovered :
flag{cr4ck1ng_RS4_w1th_f4ul7s}
Solvers:
42:37 - Sizer Cipher (Crypto)
After messing around a bit, we can notice the following:
- it encrypts pairs of bytes, starting from the end (if odd length first byte is encrypted alone) (also does some weird leading 0 stripping)
- the output for 2 chars together is first char * 0x40 + second char
So we write a script to extract mapping for char -> number then decrypt flag
Solvers:
57:07 - 2048 hacker 0 (Pwn)
By some stroke of luck, I had solved a nearly identical challenge in my Binary Exploitation lab a few weeks back. It’s a regular 2048 game where the goal is to reach… 2048! There are no flaws in the implementation of the game itself, but there lies a vulnerability in the printf
call that occurs when an invalid command is entered.
Unfortunately, our input is read in with getline
meaning it will be stored on the heap rather than the stack, preventing us from getting a trivial arbitrary read/write or using pwntools’ fmtstr_payload
function. However, we still get infinite printf
calls and can use a pointer-chain instead. Also, the lack of PIE lets us directly write any addresses we may want to read or write onto the stack using %{value}c%{offset}lln
as the addresses are so small and all the characters can be reasonably written in one go.
In situations like this, my go-to pointer chain is that of argv which then points to the program name. This is easy to recognize even just by looking at a dump of pointers as it is usually the most repeated stack pointer closest to the top of the stack.
You can also always double check this using a combination of %p and %s. In this case:
- %31$p = stack pointer to argv
- %31$s = stack pointer to program name
- %61$p = stack pointer to program name
- %61$s = program name string
Now that we know that offset 31 points to offset 61 the rest is easy. I first wrote the address of the FLAG
global variable to 31 and then leaked it using 61 which gave me the actual address of the flag string on remote.
This turned out to always be 0x404018
(4210712 in decimal), so we can do the same to get the flag.
flag{u_r_b3st_numb3r_0ne_f4st3st_2048_champi0n_0f_all_tim3}
Solvers:
1:15:45 - 2048 hacker 1 (Pwn)
The challenge file is identical to 2048 hacker 0, but this time we must get a shell. We already have our pointer chain to use from the first challenge making this quite simple. First we’ll need a libc leak in order to get the address of something more useful like system
or a one_gadget
. Also, we were not provided a libc file, so we first leaked some known libc addresses and then used https://libc.rip/ to find the version was 2.35.
Next, we need to decide where to write this. Fortunately enough, the binary only has Partial RELRO allowing us to attack any of the GOT entries. These are also much more desirable to utilize as their addresses are small and like last time can be written in one go, if we wanted to write somewhere like libc this would take some extra work.
I eventually chose to target free’s GOT entry as it was called with a single argument, our input, and it was only called if we typed in q
to quit. This is important as it lets us write our desired address using multiple writes which decreases the complexity of our payloads.
Here’s my final script that leaks libc and then writes the address of system to free’s GOT entry 2 bytes at a time. To get a shell all we have to do is type something like: q;/bin/sh
flag{s00p3r_d00p3r_h4cker_sup3ri0rity}
Solvers:
1:20:10 - Nothin But Stringz (Rev)
Download the nothin_but_stringz.c.o
Judging by the file name, we strings
the file:
And of course nothing interesting in there. Tried to use objdump:
Was not it. Doing some research online you can find to decompile the LLVM bitcode you need the llvm-dis. And then it would output a ll file:
And flag:
flag{al1_th3_h0miez_l0v3_llvm_643e5f4a}
Solvers:
1:20:31 - I Have No Syscalls And I Must Scream (Pwn) 🩸
I Have No Syscalls And I Must Scream was the second hardest pwn challenge (by point value) in Block CTF 2024. As of writing, it has 25 solves, of which we (.;,;.) were the first solve.
This was a fairly straightforward shellcoding problem. The binary first writes the flag to a randomly-generated address in memory. Next, it prompts the user to input the syscall number of the syscall they wanted to use. Afterwards, it would prompt the user for shellcode, and then execute the shellcode. The challenge adds some seccomp rules to prevent the user from accessing any syscalls other than the syscall selected (and exit).
The flag is written at a randomly-generated address within a page mmap’ed at a random address in memory. That address is some offset away from HAYSTACK (0x4200000). When asked to write out content from an invalid address in memory, write will return EFAULT instead of segfaulting/otherwise crashing. Therefore, we can use write as an oracle to find the page in memory where the flag has been stored. Since there are no “dummy pages” between HAYSTACK and the flag page where the flag is not stored, we can repeatedly call write on each possible page location above HAYSTACK. If write is successful (it returns a non-zero number of bytes), then we have found the page where the flag has been written
We start at HAYSTACK and attempt to write out one byte. If the write was unsuccessful, then we increment the write source by 0x1000 and try again. If the write was successful, then we write out the entire page we found (which should just be the flag and a bunch of 0s).
The challenge title is inspired by the sci-fi horror novel I Have No Mouth, and I Must Scream. Kudos to the challenge author for being an avid reader!
Solvers:
1:58:30 - Red Flags (Rev) 🩸
Using GDRE Tools, we can extract the entire Godot project. Looking at the source, we see this script in arena.tscn
:
There are 1024 unique possible states since there are 10 “Flag_*” objects, so we can brute force all possible states and pick the one where the characters lie close together on the Y axis.
This gives our flag: flag{now_wishlist_me_on_steam}
Solvers:
2:03:56 - Juggl3r (Web)
The description provide a link to http://54.85.45.101:8080/
. It redirect us to http://54.85.45.101:8080/login.php with a classic login.
Initially we suspect it’s something about apache(Apache/2.4.54 ). However looking at the title we know it’s a php juggler thing.
By sending a array in the password we can reasonaly assume the structure like this:
Where if we try to make hash start with 0e, PHP interprets this as scientific notation and the hash is treated as a float in comparison operations as referenced on Here, it is also called Magic Hashes.
So we just submited TyNOQHUS
as the password, which redirects us to /admin.php?user_id=1
. Then uses user_id[]=1
we can see this error
which tells us it’s a SQL injection. The rest is eazy sqlmap and flag!
sqlmap -r ./a.txt --level 5 --risk 3 --tamper=charencode -T flags -C flag --dump
flag{juggl3_inject}
Solvers:
2:18:56 - An Elf on a Shelf (Rev)
The bounds of the inner image are (215, 215, 335, 333)
. Looking at the inner image itself, we notice the top right corner has b'\x7fELF
right-to-left. We can then extract the entire ELF like so:
Then, after reversing the binary, we see that it is embedding the flag into the red pixels of elf.png
after passing through an SBOX. We can extract the sbox and the red pixels and then reverse the sbox to get the flag.
Solvers:
2:54:19 - WannaLaugh (Rev) 🩸
We are only interested in the most recent layer of the Docker image, so we can extract b174a0967b592f30503d3a16a04f1f145f010625aa999a28ff7b2bbdb247a96f
from the blobs and look at it seperately.
Since it is a Docker image, this file is a POSIX tar image, and extracting gives us two files, flag.txt.enc
and ransom
.
Reversing ransom
, we see it uses srand(time(null))
to generate a random AES key and IV. Thankfully, since tar
preserves file modification date, we can recover the time the file was created like so:
We can then take that time and use it to regenerate the key and IV to decrypt the flag
Running this prints out the flag
Solvers:
3:00:45 - 2048 ai (Misc)
This challenge is very similar to the pwn challenges, but there are no vulnerabilities to exploit. We must simply reach 2048 in a legitimate manner, the only caveat being that we have a timeout, so we must do it quickly with a script.
At first I tried to do a bruteforcey solution where I would spam s
and a
, typically the safest moves, and only use d
or w
when absolutely necessary, but this only resulted in a high of 512 and the board would get quickly cluttered. Eventually, I caved and used an AI solution as the challenge name intended.
I utilized the following tool: https://github.com/nneonneo/2048-ai. The setup was very simple as it was just a single python script to run and using its manual mode, it would receive the board state line by line and print out the next best move. Then, individual adjustments can be made on where the new block appears, but I instead opted to rerun the script everytime which was possible due to the VERY generous timeout. The rest was just some parsing and prompting the script.
flag{f4st3r_th4n_hum4nly_p0ssibl3}
Solvers:
3:00:58 - Flower App (Rev) 🩸
Looking through the binary, we notice three suspicious base64 strings.
Then in the flowerapp.load
function, we see it initializes some data with initWithBase64EncodedString
, probably the three strings from earlier.
Going down through that function, we eventually get a call to CCCrypt
. With a bit of googling, it turns out CCCrypt
is just AES-CBC
. Decoding the base64 strings shows that the first string is 32 bytes long and the second string is 16 bytes long, clearly the key and IV.
We can then just decrypt the last string which is just the encrypted flag.
The decrypted flag is flag{congrats_on_swiftly_decoding_this}
Solvers:
3:03:01 - go-moth (Web) 🩸
Challenge description contains only a URL to http://54.85.45.101:8004/
.
Opening it, we see a mostly blank page with a (bad) quote, and a footer filled with various links. Off to the side, there’s a GitHub icon though, maybe that will give us more information.
https://github.com/alokmenghrajani/go-moth
Looks like the source of the challenge!
Looking through the source, we can see the main code where the quotes are coming from:
api/moth.go
Where’s state
coming from, and what’s plaintextMoths
, though?
api/api.go
Ok, so it reads from moth.json
. Opening moth.json
on GitHub, we see there was a commit saying “Oops, we forgot the flag.” and it adds a 24th element to the array.
But wait, going back to how it’s used in moth.go
, it’s indexed by hours in the day:
You might know time goes to 23:59:59 before going to 0:00:00. There is no 24th hour, so we will never see the plaintextMoth
corresponding to the flag. This is when we shifted focus to other parts of the source.
Maybe focusing on how it encrypts and decrypts will help? api.go
was calling decrypt
from utils
, how does it decrypt? Opening utils.go
we see this:
So a file called aes.key
is embedded into the Go file as a variable called AesKey
. AesKey
is then used in the Encrypt
and Decrypt
functions. Maybe we can extract the AesKey
now somehow? However, there’s no local file inclusion or anything. Let’s keep looking.
For some reason, the web server library used by the app, gorilla/mux
, was all contained in the vendor/
directory.
Because of the unusual inclusion of the dependency directly in the source, we decided to diff the actual source from https://github.com/gorilla/mux
with the source contained in this repo. This will show us if any malicious changes were made.
After running the diff, we can clearly see the web server code is modified to send the AesKey
when certain conditions are met!
vendor/github.com/gorilla/mux/route.go
Those conditions boil down to:
Throwing that into WolframAlpha, we get some solutions we can try to throw at the server:
Ignoring ones that obviously don’t match the regex, 9864629288998116810
and 1155587244919948276
worked, satisfying those conditions and giving us the AesKey
:
Now all that’s left to do is decrypt the 24th encryptedMoth
in moth.json
:
And we get the flag!
flag{85d680bbb5103431d31d9413e29c7f6f}
Solvers:
4:56:27 - Protect your API Key (Rev) 🩸
Opening up the APK in jadx
, we see it immediately loads a native library with not much other functionality.
Unfortunately, the native library that is loaded is full of obfuscation.
Fortunately, it turns out this obfuscation is only used to decrypt strings that are later then used to access and call certain functions.
I wrote a small unicorn
script to emulate the decrypt, since all it does is perform a bunch of operations on fixed bytes from the binary.
Using this script, we can recover the string names to see what functions are being called.
Going through the function and decrypting the strings, we see that it opens a file called 8.data
in the APK assets, then initializes an javax.crypto.Cipher
with AES-ECB and a decrypted key, then decrypts the data in 8.data
.
After extracting the data myself, I found that it was a DEX file containing more code.
After converting the DEX file to a JAR using dex2jar
, we see the real internal APK.
Decrypting the base64 in start
following the code gives us the flag
flag{3fh9.gkf29j.7}
Solvers:
7:38:22 - Custom Keyboard (Rev) 🩸
Right before the CTF started, I fell asleep and woke up 4-ish hours after the CTF started. After a bit, I realize “Oh shoot, I should probably help with the CTF.” So I hop in VC and realize there’s only one challenge left 💀. I decide that I might as well help as much as I can, so I download the files and get started.
The challenge hints towards the idea that the binary has something to do with a keyboard, and the only idea that really makes sense is for it to be some sort of firmware. Running file
on the binary is misleading, telling us that it’s TTComp archive data, binary, 1K dictionary
. However, there are some ways to help confirm that this is some sort of firmware.
Running strings
on the binary gives some interesting information, one specific string which helped us was:
Searching for this string online leads us to [wear_leveling] efl updates (#22489) · 75d11e0421 - varl/qmk_firmware - meatbag. This tells us that the binary is probably firmware for a QMK keyboard.
When opening the binary in Binary Ninja, it predicts that the architecture is thumb2
.
Looking through the disassembly, everything seems correct and there aren’t many unsupported instructions, which tells us that Binary Ninja was most likely right and that this really is firmware.
There are 1000+ functions defined by Binary Ninja, which is too many to manually look through for the relevant code. In this situation, it’s a good idea to look through any interesting strings and see where it’s being used. Looking through the strings in Binary Ninja, there’s one that catches my eye.
A lot of seemingly random uppercase letters in a row? Remembering that the flag is also in all caps (given in the description of the challenge), it seems like this string will be useful. However, Binary Ninja can’t find any cross-references for it. Strange.
Carefully reviewing some of the decompiled code, we see many references to some value 0x80XXXXX
which gets dereferenced. This makes me think that the binary is supposed to be mapped at some static address, which is very easy to change with Binary Ninja. Going to Analysis > Rebase…, we can set the address we want the beginning of the binary to be mapped at.
After rebasing, we finally see our interesting string being used in the code.
Going to the function that the string is being used in, we see code that definitely doesn’t look like your typical keyboard firmware.
Taking a look at the parent function, we see some more interesting code.
From this code, it becomes somewhat more clear what’s going on. The case where some argument is subtracted by 0x41
and then later encrypted seems very suspicous, especially once you realize that 0x41
is A
in ASCII. We can assume that it’s probably passing some character, most likely our keyboard input, into the function we discovered earlier and then encrypting it.
We’re able to clean this decompilation up a bit more through some little hacks. In the code, we see many dereferences to addresses like 0x2000XXXX
. This area is actually the RAM for the microcontroller, which is 32kb in size. We can make Binary Ninja map this section by adding 32kb of null bytes to the end of the image and manually entering the section information when opening the binary with options. I used the following options when opening the modified binary.
The encryption function now looks like this:
Carefully reading over the function, I see that it’s very similar to the Enigma cipher. If you’re not familiar with how the Enigma machine works, read about it on its wikipedia page. In this function, we can see what looks like our 3 rings, plus some fourth set of characters. Initially, we assumed that this was the reflector. However, this function does not properly use this string as a reflector. Instead of having a pair of letters which would switch with each other, it indexes into the string as if it functioned as another ring. This means that you can’t simply throw the strings into a tool like CyberChef and solve it.
Another challenge is that the offsets of the rings may not be initialized to 0 at the start of encryption, so we would need to bruteforce the offsets. The following script reimplements the encryption function and bruteforces for the flag.
To be honest, most of the time spent on this challenge wasn’t on figuring out what the code was doing, but rather getting the script to work. I accidentally wasn’t converting back to a string after the math in the if statements, so the following if statements would try to compare a string to an integer, which would always return false. After fixing this mistake, the script worked as intended and was able to decipher the flag.
FLAG{DIETASTATURISTMACHTIGERALSDASSCHWERT}
Solvers:
Conclusion
Thanks again to the organizers for a great event! Winning this garnered us over 100 ctftime points putting us at 11th globally. More importantly, we were finally given a good reason to make another blog post on this site! Hope you found something useful here 🙂