riscy business picoMini by redpwn Solution

Published: April 2, 2026

Description

RISC-V binary analysis: reverse engineer a stripped RISC-V executable that encrypts your input with a custom stream cipher before comparing against a stored ciphertext.

Download the RISC-V binary from the challenge page.

Install RISC-V toolchain and QEMU user-mode emulation: sudo apt install binutils-riscv64-linux-gnu qemu-user.

bash
file riscy-business   # confirm RISC-V ELF
bash
qemu-riscv64 ./riscy-business   # test run under emulation
  1. Step 1Identify the architecture and load into a disassembler
    Run file to confirm the binary is a statically linked, stripped RISC-V 64-bit ELF with RVC (compressed instructions). Load it into Ghidra using the RISCV:LE:64:default language, or use riscv64-linux-gnu-objdump -d for a quick look at the raw assembly.
    bash
    file riscy-business
    bash
    riscv64-linux-gnu-objdump -d riscy-business | head -100
    bash
    strings riscy-business   # spot the failure message and any embedded data
    Learn more

    RISC-V is an open-source instruction set architecture (ISA) based on reduced instruction set computing (RISC) principles. The standard integer register file has 32 registers named x0-x31, with conventional aliases: a0-a7 for function arguments, s0-s11 for saved registers, and ra for the return address.

    Ghidra 9.2+ has built-in RISC-V support. When importing the binary, select RISCV:LE:64:default. Because the binary is stripped (no symbol names), you will need to rename functions manually as you identify them. The decompiler output will still be readable pseudo-C once you orient yourself in the call graph.

    The binary is statically linked, so objdump output is large. Focus on the .text section and look for the function that reads from stdin and the two helper routines that implement the cipher.

  2. Step 2Understand the two-stage stream cipher
    The binary implements a custom stream cipher that closely resembles RC4. Two key functions carry the work: init_shuffle (or generate_shuffle) builds a 256-element permutation array using your input as the key - this is RC4's key-scheduling algorithm (KSA). shuffle_and_fetch (or step) advances two index pointers through the permutation, swaps two bytes, and returns one derived byte - this is RC4's pseudo-random generation algorithm (PRGA). The main validation loop calls step once per flag character and XORs (or otherwise combines) the result against a hardcoded expected ciphertext array embedded in the binary. Your raw input is never compared directly against plaintext bytes.
    Learn more

    RC4-like key scheduling: The KSA initializes S[0..255] = 0..255, then for each index i, computes j = (j + S[i] + key[i % keylen]) % 256 and swaps S[i] with S[j]. Because the entire permutation depends on every byte of the input key, changing one character of your guess changes all subsequent stream bytes - you cannot simply read the answer from hardcoded immediates in the comparison instructions.

    Why static analysis alone is hard: You could port the cipher to Python, initialize it with the known prefix picoCTF{, and then for each subsequent position XOR the expected ciphertext byte with the next stream byte to recover the plaintext. This works because the flag prefix is known and the cipher is deterministic. Some solvers took this approach after disassembling the cipher logic in Ghidra.

    Alternative - dynamic analysis: Run the binary under QEMU + GDB and set a breakpoint at the comparison site (around address 0x101c0 in common builds). At that breakpoint, register a5 holds the expected ciphertext byte and s1 holds the encrypted byte of your guess. By brute-forcing one character at a time and checking whether those registers match, you can recover each flag character without understanding the cipher internals at all.

  3. Step 3Brute force the flag character by character under QEMU + GDB
    The practical solving path is dynamic: emulate the binary with QEMU user-mode in debug mode (-g 1234), attach GDB-multiarch, and brute force each flag position. Because the cipher's state after the first N characters only depends on those N characters and the validation is sequential, a correct partial guess extends the accepted prefix by one - a classic oracle-based brute force.
    bash
    # Terminal 1: start the binary under QEMU with a GDB stub on port 1234
    bash
    qemu-riscv64 -g 1234 ./riscy-business
    bash
    # Terminal 2: attach GDB-multiarch
    bash
    gdb-multiarch ./riscy-business
    bash
      (gdb) target remote localhost:1234
    bash
      (gdb) break *0x101c0   # comparison site
    bash
      (gdb) continue

    Rather than stepping through GDB manually for every candidate character, use a pwntools script to automate the loop. The script below spawns QEMU in debug mode, attaches via the GDB stub, and iterates over all printable ASCII characters for each flag position, accepting the one whose ciphertext register value matches the expected register value at the breakpoint.

    Learn more

    Pwntools skeleton for the brute force:

    from pwn import *
    import string
    
    BINARY = "./riscy-business"
    CHARSET = string.printable.strip()
    BREAK_ADDR = 0x101c0   # adjust to match your build
    
    known = "picoCTF{"
    
    while not known.endswith("}"):
        for c in CHARSET:
            guess = known + c
            # spawn QEMU stub
            io = process(["qemu-riscv64", "-g", "1234", BINARY])
            # attach GDB
            gdb = remote("localhost", 1234)
            # ... set breakpoint, send guess, read registers a5 and s1
            # if s1 == a5: known += c; break
            io.close()
    

    Both the GitHub Gist by spezifisch and the Owlgorithm writeup published complete working scripts for this approach. The key insight is that each correct character keeps the comparison breakpoint reachable for the next position, while a wrong character causes an early exit - making the oracle reliable.

Flag

picoCTF{4ny0n3_g0t_r1scv_h4rdw4r3?_LGUfwl8xyMUlpgvz}

The flag is validated via an RC4-like stream cipher: your input acts as the key, the cipher produces a ciphertext stream, and that stream is compared against hardcoded expected bytes. The flag is recovered by dynamic brute force under QEMU emulation, not by reading plaintext immediates from the disassembly.

Want more picoMini by redpwn writeups?

Useful tools for Reverse Engineering

Related reading

What to try next