← Back to all writeups
Pwn

tuttis

409 points • RISC-V memory corruption • Solved during pingCTF 2026

This challenge ships a 32-bit RISC-V telemetry station binary that runs under Spike. The real flag is injected into a dedicated ELF section before launch, and the bug chain is a neat mix of a one-byte underflow, reversed packet-slot indexing, and a deliberate misaligned load that can be turned into code execution.

Challenge setup

The archive contains a statically linked RISC-V ELF called tutti, a local spike binary, and a Docker setup that patches the real flag into the executable right before the service starts:

COPY tutti flag.txt .
RUN ./tools/objcopy_rv --update-section .flag=flag.txt tutti

So the flag is not read from the filesystem at runtime. It lives in the program image itself, in section .flag at virtual address 0x80003e00. That immediately tells us the end goal: get the program counter under control and print bytes from that address.

First reverse pass

The binary still has symbols, which makes the interesting parts easy to spot: main, encode_packet, decode_packet, session_token_decode, and a trap handler at 0x80000000.

The menu works with eight packet slots. The important detail is that the slots are indexed backwards:

slot_ptr = SLOTS + ((7 - current_slot) << 10)

That means slot 7 is the very first slot in memory, starting at 0x80000160. The trap handler ends at 0x8000015f, so slot 7 begins immediately after it.

The bug

The packet encoder asks for a recipient and validates it with decode_sender(). On failure it does this:

puts("Invalid recipient");
memset(slot - 1, '*', 0x401);
return;

That is a one-byte underflow followed by a full-slot overwrite. For most slots it is just a nearby clobber, but for slot 7 the write starts at 0x8000015f, which is the final byte of the trap handler.

The exploit plan is to select slot 7, trigger the invalid-recipient path once to corrupt the trap handler, refill slot 7 with attacker-controlled bytes, and then force a trap so execution enters the slot buffer.

Why a trap is easy to trigger

decode_packet() contains a convenient bug on the normal parsing path. With a crafted version-3 packet and a valid sender, it can reach this load:

lw a1, 0x1a(slot)

The offset 0x1a is not 4-byte aligned, so on RISC-V that raises a misaligned-load exception. The trap handler prints register state and then, after our one-byte corruption, falls through into slot 7, which now contains shellcode.

Crafting the malicious packet

The slot payload needs to pass enough validation to reach the misaligned load. The fields I used were:

After the 0x20-byte packet header, I placed a tiny RISC-V stub that writes the bytes at 0x80003e00 to the UART until it hits the terminating zero byte.

Shellcode

The shellcode does not need to be fancy. It just points one register at the UART MMIO base 0x10000000 and another at the flag section:

lui a3, 0x10000
addi a4, a3, 5
lui a1, 0x80004
addi a1, a1, -0x200   # 0x80003e00

loop:
  lbu a0, 0(a1)
  beqz a0, done

wait_tx:
  lbu a5, 0(a4)
  andi a5, a5, 0x20
  beqz a5, wait_tx
  sb a0, 0(a3)
  addi a1, a1, 1
  j loop

Because the flag is stored directly in the ELF image, printing from 0x80003e00 is enough to recover it.

Exploit flow

  1. Select packet slot 7.
  2. Choose Encode packet and send an invalid recipient to trigger the memset(slot - 1, '*', 0x401) underflow.
  3. Post the crafted raw packet into slot 7 so the slot body now holds the shellcode.
  4. Choose Extract telemetry data to parse that slot and hit the misaligned lw 0x1a(slot).
  5. Let the corrupted trap handler transfer control into slot 7 and print the flag.

Solver sketch

set_slot(7)
patch_trap_handler_with_invalid_recipient()
post_raw(crafted_packet_with_shellcode)
trigger_decode()

# shellcode prints bytes from 0x80003e00

Downloads

The exact exploit files used for this solve are available here:

Download solve.py

Full remote solver that assembles the shellcode, crafts the packet, and extracts the flag.

Download shellcode.S

Minimal RISC-V UART shellcode that prints the bytes in the .flag ELF section.

Download README.md

Short notes on what is included in the artifact bundle and what toolchain the solver expects.

Flag

ping{1_w1ll_m154l1gn_y0ur_b0n35_190985765989}