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:
0x88ffas the version field so the binary recognizes it as version 3.0x0789as the sender, which decodes toTUTTI.0x11das the session token sosession_token_decode()returns success.0xffffin the timestamp field so the parser takes the branch that performs the unalignedlw 0x1a(slot).
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
- Select packet slot
7. - Choose
Encode packetand send an invalid recipient to trigger thememset(slot - 1, '*', 0x401)underflow. - Post the crafted raw packet into slot 7 so the slot body now holds the shellcode.
- Choose
Extract telemetry datato parse that slot and hit the misalignedlw 0x1a(slot). - 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:
solve.py
Full remote solver that assembles the shellcode, crafts the packet, and extracts the flag.
shellcode.S
Minimal RISC-V UART shellcode that prints the bytes in the .flag ELF section.
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}