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/
$ 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
| File | Purpose |
|---|---|
chall.c | 28-line source of the binary (shown below) |
chall | Compiled ELF, no PIE, partial RELRO |
libc.so.6, ld-linux-x86-64.so.2 | Runtime libraries (unused by our exploit) |
Dockerfile | Remote runs under pwn.red/jail (chroot, uid 1000, cwd /app) |
flag.txt | Flag 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:
maincallschallexactly once. No loop. So a single TCP connection naively gives us one byte-write and thenexit(0).LEN = main - chall = 0x49(73 bytes). The bounds check restricts us to writing insidechallitself — we cannot write intomain,_finior anywhere else (initially).
...