BlackAlps 2025 CTF - s4f3
s4f3
With Phil242, we concentrated on this interesting hardware challenge of Karim.
Description

A binary challenge.elf is provided.
Reconnaissance with AI
| |
I try to get some initial thoughts with r2mcp. It doesn’t help me very much, but gives me some idea about the context:
- The flag is formatted
BA25{.....}and it is redacted in the binary - The binary was developed using Arduino
My initial prompt:
| |
The LLM tries to list functions with the following names. It’s a good idea, but there are no matches:
| |
Then, it searches for typical Arduino functions - which is an excellent idea (calls r2mcp::list_functions). For a reason I don’t understand, it doesn’t find them (the analysis with aa was done previously, it should have succeeded).
| |
The rest of its attempts are quite unsuccessful, I stop and resort to manual radare2 investigation. I find both the loop() and the setup() function.
| |
loop() is located at 0x42002ed0 - we locate a few typical calls at the end of it to handleTouch(), i2cRead() etc - and loopTask()is actually a system function that callssetup()and thenloop()`.
| Name | Address |
|---|---|
| loop | 0x42002ed0 |
| setup | 0x42002c50 |
Decompiling setup
With r2ai, I decompile setup. As this is presumed C code generated by AI, it might not be entirely correct (it turns out to be quite good).
We see that:
- It prints a MAC address
- Does some typical Arduino / ESP32 setup (
Wire.beginetc) - Calls
drawScene
| |
Decompiling drawScene
Same, I decompile drawScene with r2ai. This function turns out to be quite central for the challenge.
- Do some manipulation with the MAC address to take only some portions.
- XOR the 4 byte MAC address with a secret, and check they match a value on a cylinder
- Toast the “Access Granted” or “Access Denied” message.
Seeing the device helps understand what the cylinders are. The device is a small ESP32 device with an LCD screen on which there are 4 cylinder-based entries to select a hexadecimal value. Once all 4 hex values have been selected, we can validate the entry and get the access message.

| |
By the way, we have no doubt this function is specifically written for the challenge, because Radare2 (and Ghidra) displays the source filename:
blackalps25.ino.
Where is the secret?
The MAC address is XORed with a 4 secret bytes. In Radare2, those 4 bytes are referenced as a global variable. But we can’t see their value.
| |
With Ghidra, a label has been created with value 0x3713300b (0b 30 13 37):

The reference to l33t (1337) gives us a good hint this is the correct secret value.
MAC address
We reboot the device (there’s a boot button) and the slash screen displays the MAC address for the device. There are 2 devices. Each one has (as expected) a slightly different MAC address.
0000F86D46BA20100000E86D46BA2010
We are a bit uncertain about what the program does with the MAC address in drawScene. The following code was generated by AI, and as such, is likely to contain errors.
| |
A MAC address consists of 3 bytes that identify the vendor, and 3 other bytes that identify the device. The vendor Id is assigned by IEEE, the 3 vendor bytes are assigned by the vendor itself.
So, we have the MAC address right, but the real issue that confused us is endianess. How is that stored? Big Endian or Little Endian?
In mac_low, do we have 46BA2010, 1020BA46, 6DF80000 or 0000F86D?
During the CTF, we weren’t alert/wise enough to simply lookup the vendor id – although it just seems to be the right thing to do. If you look up 1020BA, you find out the bytes are assigned to Espressif, which is perfect for an ESP32. So our MAC address (high) 10:20:BA:46:6D:F8 (low). Yes, it seems so easy when you sleep :(
Ghidra’s decompiled code isn’t that helpful either:

Seleting the correct bytes of the MAC to XOR
What happens when you have no brain left, and not that much time either? You use AI, or brute force, or both.
AI - when you have no brain left
Claude tells us we should absolutely select F8 6D 46 BA. Actually, this is correct.

Bruteforce
We are still all messed up with endianness in our brains, so we brute force the solution. We have 4 codes to try on each device.
| |
To speed it up a little, we try the codes in parallel, Phil242 and I, one starting from the top values, the other starting with the bottom ones.
And… we flag.

Conclusion
This was a fun challenge, we like challenges which involve devices in CTFs. Thanks Karim! We enjoyed exploring it (and flagging), but our solution isn’t fully satisfactory.
The algorithm that selects the given bytes of the MAC address still gives me headaches with its ssai and src instructions.
ssai sets the shift amount for the next operation, and src combines it with a right shift, which is not natural to me… nor to LLMs apparently and decompilers provide a mess. I wonder if some other teams who flagged went through each step of assembly, or selected the continuous bytes of the MAC address from the end … a bit by luck.
In our case, we had thought to XOR with 0x0b301337 instead of 0x3713300b, we would have flagged an hour earlier.
Remaining questions
- When I load the binary with r2, I get a warning, I don’t know why:
WARN: Cannot find asm.parser for xtensa - I have no idea how to get the secret value with r2. Anybody?
- Would love to read a write-up that clearly explains the successive
srli, src, s8ietc. So, far I always ended up with an error at some point.