July 29, 2026
Reverse Engineering: Cinderbound - HTB Cyber Apocalypse CTF 2026: The Salt Crown (WriteUp)
Challenge Description: We are provided with a file named “cinderbound.mpy”. The goal is to reverse the file and retrieve the flag.

By EVEX Security
1 min read
1. Initial Analysis
Examining the file header ("4d 06 00 1f" in hex) confirms that it is a compiled MicroPython bytecode file. The 'M' byte at the beginning is a classic signature. Since decompilers for specific MicroPython versions can be unreliable, the best approach is to extract the raw opcodes using the official "mpy-tool.py" script from the MicroPython repository.
Running the tool yields the bytecode structure:
python3 mpy-tool.py -d cinderbound.mpypython3 mpy-tool.py -d cinderbound.mpy2. Dissecting the Bytecode
The disassembly reveals a single validation function named "judge(syllable)", which takes our input (the flag) and verifies it against a hardcoded target array.
By analyzing the "LOAD", "STORE", and "BINARY_OP" instructions, we can reconstruct the original Python logic:
- Target Array (FAST 1): (57, 129, 154, 31, 199, 192, 73, 243, 43, 176, 255, 173, 54, 203, 67, 15)
- Initial Key (FAST 2): k = 90
- The Loop: Iterates over the input string, encrypting it character by character.
The reconstructed encryption routine looks like this:
def judge(syllable):
target = [57, 129, 154, 31, 199, 192, 73, 243, 43, 176, 255, 173, 54, 203, 67, 15]
k = 90
out = []
for i in range(len(syllable)):
c = ord(syllable[i])
# val = c ^ k ^ ((i * 13) & 255)
val = c ^ k ^ ((i * 13) & 0xFF)
out.append(val)
# k = (k + c) & 255
k = (k + c) & 0xFF
return out == targetdef judge(syllable):
target = [57, 129, 154, 31, 199, 192, 73, 243, 43, 176, 255, 173, 54, 203, 67, 15]
k = 90
out = []
for i in range(len(syllable)):
c = ord(syllable[i])
# val = c ^ k ^ ((i * 13) & 255)
val = c ^ k ^ ((i * 13) & 0xFF)
out.append(val)
# k = (k + c) & 255
k = (k + c) & 0xFF
return out == target3. Writing the Decryptor
The encryption is sequential. The current character is XORed with a rolling key 'k' and a position-based modifier (i * 13) & 0xFF. Crucially, the rolling key 'k' is updated using the plaintext character 'c'.
Because XOR is perfectly reversible, we can iterate through the "target" array and solve for 'c' directly.
target = [57, 129, 154, 31, 199, 192, 73, 243, 43, 176, 255, 173, 54, 203, 67, 15]
k = 90
flag = []
for i, expected in enumerate(target):
# expected = c ^ k ^ ((i * 13) & 0xFF)
# c = expected ^ k ^ ((i * 13) & 0xFF)
char_code = expected ^ k ^ ((i * 13) & 0xFF)
flag.append(chr(char_code))
# update the rolling key with the recovered plaintext character
k = (k + char_code) & 0xFF
print("Flag:", "".join(flag))target = [57, 129, 154, 31, 199, 192, 73, 243, 43, 176, 255, 173, 54, 203, 67, 15]
k = 90
flag = []
for i, expected in enumerate(target):
# expected = c ^ k ^ ((i * 13) & 0xFF)
# c = expected ^ k ^ ((i * 13) & 0xFF)
char_code = expected ^ k ^ ((i * 13) & 0xFF)
flag.append(chr(char_code))
# update the rolling key with the recovered plaintext character
k = (k + char_code) & 0xFF
print("Flag:", "".join(flag))Output: c1nd3rbound_v0w5
Flag: HTB{c1nd3rbound_v0w5}
Authored by EVEX Security.