Introduction
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!
Here are our speedrun splits for the event if you wish to view by solve time:
Challenge Name | Category | Time to Solve |
---|---|---|
echo | pwn | 00:05:34 |
Only Ws | pwn | 00:10:40 |
Where’s My Key? | crypto | 00:26:48 |
echo2 | pwn | 00:35:13 |
Glitch in the Crypt | crypto | 00:37:24 |
Sizer Cipher | crypto | 00:42:37 |
2048 hacker 0 | pwn | 00:57:07 |
2048 hacker 1 | pwn | 01:15:45 |
I Have No Syscalls And I Must Scream | pwn | 01:20:31 |
Nothin But Stringz | rev | 01:20:10 |
Red Flags | rev | 01:58:30 |
Juggl3r | web | 02:03:56 |
An Elf on a Shelf | rev | 02:18:56 |
WannaLaugh | rev | 02:54:19 |
2048 ai | misc | 03:00:45 |
Flower App | rev | 03:00:58 |
go-moth | web | 03:03:01 |
Protect your API Key | rev | 04:56:27 |
Custom Keyboard | rev | 07:38:22 |
Binary Exploitation
[00:05:34] echo
This simple server echos back what it hears. Can you make it say so̸met̸hing
ḍ̣͂̅i̮̓f̆f̛ḙ̊rḙ̊nt̮̏? nc 54.85.45.101 8008
Standard 64 bit buffer overflow with no protections except for NX. They were even so kind as to provide us with a win
function 😊:
[00:10:40] Only Ws
The flag is write there. All you have to do is get it. nc 54.85.45.101 8005
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!
[00:35:13] echo2
I fixed my echo server by enabling lots of protections. Now what can you do?
nc 54.85.45.101 8009
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!
[00:57:07] 2048 hacker 0
The game is rigged against you. nc 54.85.45.101 8007
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.
[01:15:45] 2048 hacker 1
If you solved 2048 hacker 0 you know the game is actually rigged in your
favor. Go get a shell! nc 54.85.45.101 8007
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
[01:20:31] I Have No Syscalls And I Must Scream 🩸
write(1, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH", 64);
note: solving this does not require tens of thousands of repeat connections, and trying to do so will probably kill our infrastructure before you get your flag. Please don’t attack the Infrastructure! nc 54.85.45.101 8002
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!
Reverse Engineering
[01:20:10] Nothin But Stringz
Someone sent me this as a test of friendship, but I can’t make heads or tails out of it. Can you help?
Running file
on the file gives us:
Judging by the file name, we strings
the file:
And of course, there’s nothing interesting in there. Tried to use objdump
:
This didn’t work either. Doing some research online, you can find to decompile the LLVM bitcode you need the llvm-dis
tool.
And then it would output an ll
file:
[01:58:30] Red Flags 🩸
I made a video game, its really hard!
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.
[02:18:56] An Elf on a Shelf
What’s going on here?
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.
[02:54:19] WannaLaugh 🩸
Grandpa downloaded a weird program on the Internet and now his files don’t work. Knowing you’re good with computers, he asked for your help. The program is asking for Bitcoin to get his files back, but his wallet password was encrypted as well. Note: The program will only encrypt .txt files in the same directory as the executable and won’t delete any files so nothing bad should happen…
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:
[03:00:58] Flower App 🩸
I swiftly drew some flowers!
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:
[04:56:27] Protect your API Key 🩸
Bob is creating an app in which he needs to include a particular SDK. To use the SDK, he applied for an API Key which needs to pass to the SDK in the runtime. Considering the Key is not free, Bob is trying multiple approaches to protect it.
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:
[07:38:22] Custom Keyboard 🩸
I got a sweet deal on this keyboard at a garage sale, but I think whoever used it before me messed with the settings. It came with an instruction manual that just says: KKPE{FJZBSVNBYWWOKOJIPNWGPGASCVVYPAYLAHTX}
NOTE: The flag will be in ALL CAPS, like FLAG{...}
NOTE: The flag was typed continuously 🎹
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.
Cryptography
[00:26:48] Where’s My Key?
The flag server is providing encrypted flags. It looks like there might be a
bug in the code and I can’t figure out how to decrypt it. nc 54.85.45.101 8001
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
)
[00:37:24] Glitch in the Crypt: Exploiting Faulty RSA Decryption
A high-security server, SecureVault Inc., uses RSA encryption to protect
sensitive data. The server utilizes a 1024-bit RSA key and employs the
CRT optimization during decryption to improve performance. Due to a
hardware fault, the server occasionally introduces errors during the
decryption process. Specifically, the fault affects the computation of during the CRT step, causing to be incorrect, while
remains correct. As an experienced cryptanalyst, you
have managed to obtain access to the server’s decryption service. Your goal is
to exploit the faulty decryption results to recover the prime factors and
of the RSA modulus , effectively breaking the encryption scheme and
recovering the private key. nc 54.85.45.101 8010
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 to get the factorization of the secret key:
We then grab a flag encryption from the server:
And decrypt using our recovered :
[00:42:37] Sizer Cipher
I made my own encryption scheme- check it out!
052c01be88c7f52cbdc3c084c7b1313828034370034dd13778342dff
nc 54.85.45.101
8003
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 to number, then decrypt the flag:
Web Exploitation
[02:03:56] Juggl3r
The admin panel seems locked behind some odd logic, and the flag is hidden
deep. Can you bypass the checks and uncover the secret?
http://54.85.45.101:8080/
The description provides a link to http://54.85.45.101:8080/
. It redirects us to http://54.85.45.101:8080/login.php
with a classic login interface:
Initially, we suspected it was something about Apache (Apache/2.4.54). However, looking at the title we know it’s a PHP juggler issue. By sending a array in the password, we can reasonagly assume the structure looks 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 here. This is also called “magic hashes.”
We just submited TyNOQHUS
as the password, which redirects us to /admin.php?user_id=1
and then using user_id[]=1
we can see this error:
This tells us it’s a SQL injection. The rest is an easy sqlmap
and flag:
[03:03:01] go-moth 🩸
The challenge description only contains 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 finding this link, we can see the source of the challenge!
Looking through the source, we can see the main code where the quotes are coming from:
Where’s state
coming from, and what’s plaintextMoths
, though?:
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 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!
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!
Miscellaneous
[03:00:45] 2048 ai
Can you speedrun 2048? nc 54.85.45.101 8006
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. 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:
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 🙂