spelling-bee
b01lersc
Task: a tiny Forth interpreter with user-defined words, deletion, and a startup leak of dosys(). Solution: exploit a use-after-free in forget, groom tcache to reclaim a stale word_t as name chunks, forge a fake word_t, and redirect execution to dosys(\"sh\").
$ 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.
spelling-bee — b01lersc
Description
The original organizer description was not preserved in the task directory.
We are given a small Forth-like interpreter implemented in C. The goal is to study how user-defined words are compiled and deleted, then turn a memory-management bug into code execution and read the flag.
Analysis
The core runtime object is:
typedef struct word { long flags; long length; long referenced_by; void (*code)(void *); void *param; } word_t;
Dictionary entries are stored separately as:
typedef struct dictionary { char *name; word_t *word; struct dictionary *next; bool alloc_name; } dict_t;
When the interpreter compiles a new word, it appends raw word_t * pointers into compile_def. There is no indirection layer: compiled words directly reference the original word_t objects.
That would still be safe if deletion respected ownership, but delete_word() does not check referenced_by before freeing the target word. So if word a was compiled to call word b, then forget b frees b even though a still contains a pointer to b.word_t. This is a classic dangling-pointer bug and gives us a use-after-free primitive.
The challenge also prints the runtime address of dosys() at startup:
printf("%p\n", dosys);
That immediately leaks the PIE base, so we can compute both dosys and the address of g_stack.
The heap layout is especially convenient:
sizeof(word_t)lands in the0x30tcache bin.sizeof(dict_t)also lands in the0x30tcache bin.- The initial
compile_defallocation for a freshly compiled word lands in the0x90tcache bin.
...