Flash CTF – RISC-V Business

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 printf a 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}