pwnfreehard

transmutation

b01lersc

Task: 28-line C program exposing a single byte-write primitive into the first 73 bytes of chall(), whose page is mprotect'd RWX. Solution: patch the final ret to nop so chall falls through into main (creating an infinite write loop), neutralize the LEN bounds check by zeroing a jge displacement, plant execve shellcode after _fini, and flip one byte to transmute main's call chall into jmp short → trampoline → shellcode.

$ ls tags/ techniques/
single_byte_write_primitiveret_to_nop_fallthroughcall_to_jmp_opcode_flipjge_neutralizationinfinite_write_looppost_fini_shellcode_placement

$ cat /etc/rate-limit

Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.

transmutation — b01lersc (BCTF 2026)

Description

To turn one program into another... is it even possible?

ncat --ssl transmutation.opus4-7.b01le.rs 8443

We are given a tiny C program (28 lines) together with its compiled binary (chall, no PIE, partial RELRO) and loader/libc. Each TCP connection spawns a fresh process, reads exactly two bytes (a value c and an offset i), and if i < LEN writes c into the function chall itself. The whole code page is made RWX beforehand. The goal is to execute code.

File list

FilePurpose
chall.c28-line source of the binary (shown below)
challCompiled ELF, no PIE, partial RELRO
libc.so.6, ld-linux-x86-64.so.2Runtime libraries (unused by our exploit)
DockerfileRemote runs under pwn.red/jail (chroot, uid 1000, cwd /app)
flag.txtFlag file at /app/flag.txt on the remote

Source

#include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #define MAIN ((char *)main) #define CHALL ((char *)chall) #define LEN (MAIN - CHALL) int main(void); void chall(void) { char c = getchar(); unsigned char i = getchar(); if (i < LEN) { CHALL[i] = c; } } int main(void) { setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); mprotect((char *)((long)CHALL & ~0xfff), 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC); chall(); return 0; }

Two properties jump out immediately:

  1. main calls chall exactly once. No loop. So a single TCP connection naively gives us one byte-write and then exit(0).
  2. LEN = main - chall = 0x49 (73 bytes). The bounds check restricts us to writing inside chall itself — we cannot write into main, _fini or anywhere else (initially).

...