$ cat writeup.md…
$ cat writeup.md…
fortid2026
Task: a C review-manager binary with a use-after-free — favourite aliases a freed review chunk whose embedded display function pointer gets reclaimed by a same-size draft allocation. Solution: free the review, allocate a 40-byte draft to reclaim the chunk, overwrite the display pointer with win() (fixed non-PIE address), then trigger the indirect call via preview_favourite().
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
Everyone's opinion matters to us. Submit a review, save your thoughts for later, and make sure your favourite feedback is never forgotten. Can you leave one last review?
A handout.zip contained chall.c (source) and chall (ELF 64-bit). The service was reachable over TLS (ncat --ssl ... 1337). Goal: hijack a dangling function pointer through a use-after-free to call the built-in win() function and read flag.txt.
Binary facts (readelf -h / checksec):
e_type = EXEC), so function addresses are fixed — no leak needed.win() at 0x40135f — opens flag.txt and prints its contents.show_review() at 0x401336 — the normal display handler.Vulnerable data model (from chall.c):
#define REVIEW_TEXT_SIZE 32 struct review { char text[REVIEW_TEXT_SIZE]; // offset 0, 32 bytes void (*display)(review_t *review); // offset 32, function pointer }; // draft is a raw byte buffer of EXACTLY the same size as review_t (40 bytes) typedef struct { char contents[sizeof(review_t)]; } draft_t; static review_t *review; static review_t *favourite; static draft_t *draft;
Menu behaviour:
submit_review() — review = malloc(sizeof(review_t)), reads text, sets review->display = show_review.mark_favourite() — favourite = review (just aliases the pointer, no copy).delete_review() — free(review); review = NULL; but favourite is left dangling.write_draft() — draft = malloc(sizeof(draft_t)), user supplies a length 1..40 and that exact many bytes via fread (so NUL bytes are allowed).preview_favourite() — favourite->display(favourite) — an indirect call through a possibly-freed chunk.Root cause — Use-After-Free + pointer aliasing:
delete_review() frees the review chunk and nulls review, but mark_favourite()
earlier copied the pointer into favourite, which is never cleared. favourite
therefore dangles into a freed chunk.
...