Overview
A single binary: lander.elf. The description tells us it’s diagnostic firmware for a RISC-V lander. We need to find the activation code.
Reconnaissance
$ file lander.elf
lander.elf: ELF 64-bit LSB executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), statically linked, stripped
$ qemu-riscv64 ./lander.elf
ARES-7 LANDER DIAGNOSTIC v1.4.2
Safe mode active. Science payload locked.
Enter activation code: hello
DENIED - Safe mode maintained.
Stripped RISC-V 64-bit ELF, statically linked. Let’s load it in Ghidra.
Finding the Vulnerability
Open Ghidra, create a new project, import lander.elf. Select RISC-V:LE:64:default as the processor.
We have no symbols, so navigate to main by finding the entry point. Look for a function that:
- Calls
printfa few times (the banner and prompt) - Calls another function with our input string
- Branches on the result
That called function is verify_code. In the decompiler it looks roughly like:
undefined8 verify_code(char *input)
{
size_t len = strlen(input);
if (len != 30) return 0; // TOKEN_LEN = 30
uint8_t prev = 0xaa;
for (size_t i = 0; i < 30; i++) {
uint8_t c = input[i];
c = c ^ (uint8_t)((i * 0x1f) + 0x37); // step 1
int rot = (int)((i % 7) + 1);
c = (uint8_t)((c << rot) | (c >> (8 - rot))); // step 2
c = c ^ prev; // step 3
prev = c;
if (c != auth_token[i]) return 0;
}
return 1;
}
The auth_token[] is a 30-byte constant in the data section — Ghidra will show it inline. It’s the scrambled reference value the binary compares against.
The Three Steps
Step 1: Position-keyed XOR: Each byte is XOR’d with (i * 0x1F + 0x37) & 0xFF. XOR is self-inverse, so reversing is the same operation.
Step 2: Bit rotation: The double-shift-and-OR pattern (c << rot) | (c >> (8 - rot)) is a left-rotate by rot bits. In RISC-V assembly this is two sll/srl instructions followed by or — distinctive because x86 has a native ROL instruction, RISC-V doesn’t. Reversing a left-rotate is a right-rotate by the same amount.
Step 3: CBC chaining: Each transformed byte is XOR’d with the previous transformed byte (initial value 0xAA). Crucially, prev is updated to the post-transform value, not the original byte. This means we must reverse left-to-right (not right-to-left) using the expected array values directly as the chain.
Solution
Reverse all three steps in order:
expected = [
0x5E, 0x92, 0x9A, 0xC5, 0xDB, 0x7A, 0xA1, 0x77,
0x02, 0xF9, 0x7C, 0x80, 0x34, 0xFE, 0xE9, 0x0D,
0xE7, 0x65, 0xAF, 0x52, 0x3A, 0x01, 0x46, 0x35,
0x31, 0x5B, 0x01, 0x06, 0xD7, 0xC8,
]
prev = 0xAA
flag = []
for i, e in enumerate(expected):
c = e ^ prev # undo step 3 (prev is expected[i-1], i.e. 'e' from last round)
prev = e
rot = (i % 7) + 1
c = ((c >> rot) | (c << (8 - rot))) & 0xFF # undo step 2: rotate right
c ^= (i * 0x1F + 0x37) & 0xFF # undo step 1
flag.append(chr(c))
print("".join(flag))
$ python3 solve.py
MetaCTF{r15cv_bus1n3ss_0n_m4rs_59b7eec00092da4e9ae9780daacd850f}
Verifing against the binary passes:
$ echo -n 'MetaCTF{r15cv_bus1n3ss_0n_m4rs_59b7eec00092da4e9ae9780daacd850f}' | qemu-riscv64 ./lander.elf
ARES-7 LANDER DIAGNOSTIC v1.4.2
Safe mode active. Science payload locked.
Enter activation code:
AUTHORIZED - Science payload unlocked.
Resuming nominal operations.
Flag
MetaCTF{r15cv_bus1n3ss_0n_m4rs_59b7eec00092da4e9ae9780daacd850f}