10-25: Ultimate Calculator 3000
Writeup for Ultimate Calculator 3000 (Web/Rev) - CryptoCat CTF Challenge (2025) ๐
Video Walkthrough
Challenge Description
New calculator dropped! The pro version is a work in progress - stay tuned ๐
It's been a while since I made a challenge and I felt like doing something a little different to the last one (pure web). I hosted some parts of it on my domain but the downloadable file to get started was (is) available on my discord server. It's a good place to be! In future, I'll pre-release challenges/hints there and provide [limited] technical support ๐
Solution
Basic File Checks
After downloading the ultimate_calculator binary, we can run it and see the usage options.
Option 3 and 4 work as advertised - no interesting functionality. Option 1 functions as a traditional calculator.
Option 2 says it's still "under construction". When it comes to CTFs, features that are under construction are typically worth investigating.
Before we start reversing the binary, let's check its properties. Firstly, we'll see it is stripped - this will make reverse engineering more challenging.
Most of the binary protections are enabled, reducing the likelihood that binary exploitation is the intended path.
The cURL library is included so we should be on the lookout for HTTP traffic.
We might try to run the binary with strace or leave wireshark running in the background to analyse the network traffic. However, in this case we wouldn't see anything useful - let's take a look under the hood.
Static Analysis
When we open the binary in ghidra we'll see that the serial activation function doesn't do anything - it just prints the message and loops back to the menu.
There are no curl commands in there either. We could browse through the other functions or look for interesting strings.
There's clearly some hidden functionality. Clicking one of those entries those will take us to the code in question. It starts with some cryptographic routines.
Next, it sets up what looks like some URL endpoints (/auth + /check) and takes a 32-character hex string from the user.
Scrolling down a little (there's ~300 LOC in this function), curl is called (FUN_00101530).
We can confirm it's curl by reviewing the pseudocode.
We could proceed with static analysis, reversing this function fully (trivial with LLM assistance) and recreating the crypto and HTTP interactions. However, some obfuscation has been applied in an attempt to hinder reverse engineering efforts. Another approach would be to attach a debugger and let the program do the work for us.
Note that if we check the references to this serial validation function (to find which function calls it), we'll realise that there is no execution path. Therefore, we'll want to patch the binary (swap the function call that loops the menu with the hidden one) or change the flow of execution manually using a debugger.
Binary Patching
The most effective technique here would be to patch the binary, it will save manually restarting the program and jumping to the breakpoint each time. Right click on the instruction, select Patch Instruction and insert the correct function address (0x001019a0).
You'll see the altered instruction in the disassembler.
Next, go to File > Export Program, set the Format to Original File and click "OK".
Now if we test the patched binary it will ask for serial number ๐ If we enter an invalid serial, it loops back to the main menu so we can try again indefinitely.
Dynamic Analysis
If you patched the binary already, that's fine. If not (or you want to try another way), lets attach a debugger to the original (unpatched) binary. PIE is enabled and the function is at offset 0x19a0.
We can run the binary with pwndbg and then hit ctrl + C to pause execution. From here, we'll jump right to the target address.
We triggered the functionality. Let's try again with wireshark running in the background, then we'll use the display filter to show only HTTP traffic.
We can see two POST requests; one to /auth and then one to /check. Let's follow the HTTP stream for a better view.
The authentication is successful (cat:MeowSec159102374) and we receive a token. Next, we send the serial but get a 400 error - probably because it's invalid ๐ค
Hidden Subdomain (Link Header)
Notice that the domain is 9cdfb439c7876e703e307864c9167a15.cryptocat.me but then the response from /check includes a Link HTTP header with a different subdomain ๐ค
What happens when we view this address in the browser? Unfortunately, it's not what I intended ๐
The problem is that requests are automatically upgraded to HTTPS while the server only accepts HTTP. I considered making it HTTPS but that would result in the subdomains showing on sites like crt.sh, so players could uncover them without reversing the binary. Additionally, I wanted players to be able to easily monitor traffic without worrying about SSL.
Anyway, it's easy to get around this. If we try to curl the HTTP site, we'll see the expected response.
Therefore, we can write a script to automate the HTTP requests or we can use burp and just downgrade to HTTP.
The login page contains a POST form. Let's try to reuse the creds we received earlier.
It works! We get a new session for the subdomain.
We'll need to insert the cookie before we follow the redirection. Once we do, we'll see a download link for an "offline validator" tool used by the support team.
If we visit the /download/validator endpoint using our bk_sess cookie, we'll receive the file. You can either save this as a file, or right click the request in burp and copy as cURL command (we'll need to remove the -i flag to skip printing the response headers, and add -o to output to file).
Offline Serial Validator
Checking the file type, we'll see it's a Go binary which has not been stripped.
When the program is executed, it asks for the serial.
Opening ghidra, we'll see the verifySerialHex() function.
The function has a series of hex values which appear to be in the ASCII range, but the convert option in ghidra doesn't produce meaningful output.
That's because of the endianess within Go binaries. Let's convert them and you'll see what I mean.
Notice some of the password (eowSec159102374) in reverse (and slightly jumbled) order? You could reverse the functionality from here (ghidra has some plugins for Go), or do some "vibe reversing" aka paste the decompiled code into ChatGPT and ask for the original Go code.
Looks much better, we just need to find the salt. Double click the reference in ghidra.
It will jump to the pointer and we can follow this again by double-clicking on DAT_004a8111
We'll find the 32 byte hex salt.
A quick way to grab this is to highlight the block in Ghidra, right-click โ Copy Special โ Byte String (No Spaces)
That will give us the hex string, which we can unhex for the salt.
Now we have all the pieces we need to build a serial generator. A reminder of the verification code:
Creates HMAC-SHA256 using the salt as a key
Feeds
cat:MeowSec159102374:into the HMACFeeds the first 4 bytes (nonce) of the decoded serial into the HMAC
Finalise HMAC and get the digest
Compare the first 12 bytes of the digest with the final 12 bytes of the decoded serial
Essentially, the serial layout is P (4 bytes) || HMAC_SHA256(KEY_SALT, USERPASS || P)[:12]
We have all these pieces (username, password, key) required to generate our own valid serials. The nonce (P) can be any 4 random bytes, it's included to ensure uniqueness.
Offline Generator (keygen)
Let's put all the functionality into a new Go binary.
Compile, then run it a few times to generate some [hopefully] valid serial numbers.
We'll jump to the verification function again (you could also use burp, curl, custom script etc) and provide one of the keys.
Flag: flag{4_bi7_0f_r3v_4_bi7_0f_w3b}
Bonus: Solve Script
Using everything we've learnt, we can make a solve script to get the flag. This probably looks overkill, but I reused a template from OSWE prep where I would typically stick to this format. You could also integrate the keygen functionality directly to the script (homework).
Hope you enjoyed this one, see you for "monthly" challenge #3 next year! ๐๐ฅฐ
Community Writeups
Last updated
