From 06230ac680bc4b684bc2d4cf4b7abe113908f5e1 Mon Sep 17 00:00:00 2001 From: Gregory Lirent Date: Tue, 16 Jun 2026 19:06:59 +0300 Subject: [PATCH] Add PE section enumeration and section views (section-local / RVA / absolute) vmie_win32_sections lists a module's PE sections (name, RVA, virtual size, VR_* protection) for any image base in a process address space - including a base found by scanning, not only loader-list modules. vmie_win32_section_view gathers a section's bytes into a caller buffer and returns a mem_view_t whose base_va is chosen by view_base: SECTION_LOCAL (0, section-relative offsets), MODULE_RVA (ASLR-stable module RVAs), or ABSOLUTE_VA (live VA). Because the pure scanners report base_va + offset, the mode directly selects the coordinate space of every hit - feeding a view to sig_all or x86_decode yields section-relative, RVA, or absolute results with no extra work. The MZ/PE header walk is factored into one helper that both pe_find_section and the new enumerator share - no second parser. The whole public surface is documented with the operational nuances (coordinate stability, borrowed-buffer lifetime, truncation, residency) and worked examples. --- include/win32.h | 121 +++++++++++++++++++++++++++++++++ src/engine/include/pe.h | 17 +++++ src/engine/win32/pe.c | 146 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 273 insertions(+), 11 deletions(-) diff --git a/include/win32.h b/include/win32.h index cf0faf0..29501f4 100644 --- a/include/win32.h +++ b/include/win32.h @@ -146,4 +146,125 @@ scan* scan_new(vmie_win32* v, const process* pr, scan_type t, const void* value, int vmie_scan_pointer(vmie_win32* v, const process* pr, uint64_t target, int max_depth, uint32_t max_off, scan_ptr_path* out, int max); +/* ---- PE sections + section views ----------------------------------------- * + * A section is a PE-image concept, so it is keyed by (vmie_win32*, cr3, + * module_base): the address space and where the image is based in it. The + * module need NOT be in the loader list - any valid PE base works, including + * one found by scanning for MZ/PE (a manually-mapped or hidden module). */ + +/* Coordinate space of a section view's mem_view_t.base_va. The pure scanners + * (sig_all/sig_each, x86_decode callers, ...) report every result as + * base_va + offset, so this enum decides what coordinate the hits/targets come + * back in: + * SECTION_LOCAL - base_va = 0 => results are section-relative + * offsets [0, vsize). The most stable form: independent of + * the image base AND of where the section sits in the image. + * Use when you only care about positions inside one section. + * MODULE_RVA - base_va = section RVA => results are module-relative (RVA). + * ASLR-stable across runs of the same binary; the canonical + * form for portable signatures and for correlating across + * sections of one module. Recommended for sig-gen. + * ABSOLUTE_VA - base_va = module_base + RVA => results are live guest + * virtual addresses, valid for gva_read/gva_ptr under this + * cr3 NOW. NOT stable across runs (ASLR). Use when you must + * dereference a hit immediately in the live process. */ +typedef enum { SECTION_LOCAL, MODULE_RVA, ABSOLUTE_VA } view_base; + +/* One PE section, as enumerated by vmie_win32_sections. + * name - section name, NUL-terminated; PE names are <= 8 bytes (name[8] is + * the NUL slot). Names are NOT unique in a malformed/packed image - + * prefer iterating by index over matching by name. + * rva - section RVA: byte offset of the section from the module base + * (so ABSOLUTE_VA = module_base + rva). + * vsize - virtual size in bytes (the in-memory size; may exceed the on-disk + * raw size). Size the buffer for vmie_win32_section_view from THIS. + * prot - effective protection as VR_* flags (VR_R/VR_W/VR_X), derived from + * the section Characteristics (IMAGE_SCN_MEM_READ/WRITE/EXECUTE). + * VR_U is never set: these are image semantics, not live PTE rights. */ +typedef struct { char name[9]; uint32_t rva; uint32_t vsize; uint32_t prot; } section_desc; + +/* Enumerate the sections of the PE image based at `module_base` in the address + * space `cr3`. + * v - engine handle + * cr3 - the process address space the module is mapped in (e.g. a + * process->cr3). The module need NOT be in the loader list - + * any valid PE base works (e.g. one found by scanning for + * MZ/PE, i.e. a manually-mapped / hidden module). + * module_base - image base VA; only the PE headers (first page) must be + * resident and readable under `cr3`. + * out, max - caller array receiving up to `max` section_desc; `out` may be + * NULL to count only (then `max` is ignored). + * Returns the TOTAL section count (may exceed `max` => enlarge and retry), or + * -1 if the headers are absent/unreadable or `module_base` is not a PE. + * + * The returned `rva`/`vsize` are ASLR-independent (image-relative): stable + * across runs of the same binary. The absolute placement is module_base + rva. + * + * Example - list the sections of the first module of a process: + * pmodule m; proc_modules(v, pr, &m, 1); + * section_desc s[32]; + * int n = vmie_win32_sections(v, pr->cr3, m.base, s, 32); + * for (int i = 0; i < n && i < 32; i++) + * printf("%-8s rva=%#x vsize=%#x %c%c%c\n", s[i].name, s[i].rva, s[i].vsize, + * (s[i].prot & VR_R) ? 'R' : '-', (s[i].prot & VR_W) ? 'W' : '-', + * (s[i].prot & VR_X) ? 'X' : '-'); */ +int vmie_win32_sections(vmie_win32* v, uint64_t cr3, uint64_t module_base, + section_desc* out, int max); + +/* Gather a section's bytes from the live process into `buf` and return a flat + * mem_view_t over them in the coordinate space chosen by `mode`. This is the + * "section memory, addressed from 0" entry point. + * v, cr3, module_base - as in vmie_win32_sections. + * sec - the section to open (from vmie_win32_sections; carries + * rva/vsize). Must be non-NULL. + * mode - view_base: sets out->base_va (see view_base). + * SECTION_LOCAL => 0, MODULE_RVA => sec->rva, + * ABSOLUTE_VA => module_base + sec->rva. + * buf, bufcap - caller-owned destination; size it to sec->vsize (from + * enumeration). If bufcap < sec->vsize the section is TRUNCATED: + * out->size = bufcap (this is NOT an error). The returned view + * BORROWS buf - it is valid only while `buf` lives and is left + * unmodified; the library retains no pointer past this call. + * out - on success: out->data = buf, out->size = min(sec->vsize, + * bufcap), out->base_va per `mode`. Must be non-NULL. + * Returns 0 on success, or -1 if `sec`/`buf`/`out` is NULL, the headers are + * unreadable, or the section bytes are not fully resident (paged out / sparse) + * so the read fails. + * + * Reversing nuance: .text/.rdata are normally fully resident; a section that is + * partly paged out yields -1 - re-try when resident, or sweep the live VA range + * with gva_sweep instead. The base-mode also picks the coordinate stability: + * SECTION_LOCAL/MODULE_RVA are ASLR-stable (offset / RVA), ABSOLUTE_VA is the + * live VA for this run only. + * + * Example - find an IDA pattern in .text as RVAs (ASLR-stable across runs): + * section_desc s[32]; + * int n = vmie_win32_sections(v, cr3, base, s, 32); + * for (int i = 0; i < n && i < 32; i++) if (!strcmp(s[i].name, ".text")) { + * uint8_t* buf = malloc(s[i].vsize); + * mem_view_t tv; + * if (vmie_win32_section_view(v, cr3, base, &s[i], MODULE_RVA, + * buf, s[i].vsize, &tv) == 0) { + * sig_pattern_t p; sig_parse_ida("48 8B 05 ? ? ? ?", &p); + * uint64_t rvas[64]; + * int h = sig_all(tv, &p, rvas, 64); // rvas[] are module RVAs, stable + * sig_free(&p); + * } + * free(buf); + * } + * + * Example - step instructions section-locally (offset 0 == section start): + * mem_view_t tv; + * vmie_win32_section_view(v, cr3, base, &text, SECTION_LOCAL, + * buf, text.vsize, &tv); + * for (size_t off = 0; off < tv.size; ) { + * x86_insn in; + * int len = x86_decode(tv.data + off, tv.size - off, &in); // off == section-local addr + * if (len <= 0) { off++; continue; } + * off += (size_t)len; + * } */ +int vmie_win32_section_view(vmie_win32* v, uint64_t cr3, uint64_t module_base, + const section_desc* sec, view_base mode, + uint8_t* buf, size_t bufcap, mem_view_t* out); + #endif /* VMIE_WIN32_H */ diff --git a/src/engine/include/pe.h b/src/engine/include/pe.h index 112e724..823b017 100644 --- a/src/engine/include/pe.h +++ b/src/engine/include/pe.h @@ -14,6 +14,23 @@ #include #include "memmodel.h" /* mem_view_t, vmie_mem */ +/* One enumerated PE section header, decoded by pe_sections. Mirrors the public + * win32 section_desc, but stays engine-private (this header is engine-only). + * name - section name, NUL-terminated (PE names are <= 8 bytes; name[8] NUL) + * rva - section RVA (relative to module_base) + * vsize - virtual size in bytes + * prot - VR_R/VR_W/VR_X from the section Characteristics (VR_U never set) */ +typedef struct { char name[9]; uint32_t rva; uint32_t vsize; uint32_t prot; } pe_secrec; + +/* Enumerate the section headers of the PE image based at `module_base` inside a + * view holding at least the image headers (the first page is enough). + * out, max - caller array receiving up to `max` pe_secrec; out may be NULL to + * count only. Headers truncated by the view end are not reported. + * Returns the TOTAL section count (may exceed `max`), or -1 if `v` does not hold + * a parseable PE at `module_base`. Shares the section-table walk with + * pe_find_section (one header parser, no duplication). */ +int pe_sections(mem_view_t v, uint64_t module_base, pe_secrec* out, int max); + /* Locate a PE section by name within a view that contains at least the image * headers at `module_base` (the first page is enough). * module_base - image base VA, must be >= v.base_va and inside `v` diff --git a/src/engine/win32/pe.c b/src/engine/win32/pe.c index 4237c8e..b8c3bdd 100644 --- a/src/engine/win32/pe.c +++ b/src/engine/win32/pe.c @@ -1,12 +1,30 @@ #include "pe.h" #include -#include "memmodel.h" /* gva_read */ +#include "memmodel.h" /* gva_read, VR_* */ #include "sigscan.h" /* mem_sub (pure matcher; engine may use it) */ +#include "win32.h" /* public surface: vmie_win32, section_desc, view_base */ -bool pe_find_section(mem_view_t v, uint64_t module_base, const char* name, - uint64_t* rva_out, uint32_t* vsize_out) { - if (!v.data || !name || module_base < v.base_va) return false; +/* IMAGE_SECTION_HEADER: 8-byte Name, then Misc.VirtualSize(+8), VirtualAddress + * (+12), and Characteristics(+36); the header is 40 bytes wide. */ +#define SH_SIZE 40u +#define SH_VSIZE_OFF 8u +#define SH_VADDR_OFF 12u +#define SH_CHAR_OFF 36u + +/* IMAGE_SCN_MEM_* protection bits in IMAGE_SECTION_HEADER.Characteristics. */ +#define SCN_MEM_EXECUTE 0x20000000u +#define SCN_MEM_READ 0x40000000u +#define SCN_MEM_WRITE 0x80000000u + +/* Common PE section-table walk: validate the MZ/PE headers reachable inside `v` + * at `module_base`, then locate the section-header array. On success fills + * *sec_off (byte offset into v.data of the first IMAGE_SECTION_HEADER) and + * *nsec (NumberOfSections), and returns true. This is the single header parse + * shared by pe_find_section and pe_sections - there is no second PE parser. */ +static bool pe_section_table(mem_view_t v, uint64_t module_base, + size_t* sec_off, uint16_t* nsec) { + if (!v.data || module_base < v.base_va) return false; const size_t mo = (size_t)(module_base - v.base_va); if (mo + 0x40 > v.size) return false; if (v.data[mo] != 'M' || v.data[mo + 1] != 'Z') return false; @@ -17,23 +35,42 @@ bool pe_find_section(mem_view_t v, uint64_t module_base, const char* name, if (nt + 0x18 > v.size) return false; if (memcmp(v.data + nt, "PE\0\0", 4) != 0) return false; - uint16_t nsec, opt_size; - memcpy(&nsec, v.data + nt + 6, 2); /* NumberOfSections */ + uint16_t n, opt_size; + memcpy(&n, v.data + nt + 6, 2); /* NumberOfSections */ memcpy(&opt_size, v.data + nt + 20, 2); /* SizeOfOptionalHeader */ - const size_t sec = nt + 24 + opt_size; /* first section header */ + *sec_off = nt + 24 + opt_size; /* first section header */ + *nsec = n; + return true; +} + +/* Map IMAGE_SCN_MEM_READ/WRITE/EXECUTE -> VR_R/VR_W/VR_X. Image semantics, not + * live PTEs: VR_U is never set here (see section_desc.prot in win32.h). */ +static uint32_t pe_prot(uint32_t characteristics) { + uint32_t prot = 0; + if (characteristics & SCN_MEM_READ) { prot |= VR_R; } + if (characteristics & SCN_MEM_WRITE) { prot |= VR_W; } + if (characteristics & SCN_MEM_EXECUTE) { prot |= VR_X; } + return prot; +} + +bool pe_find_section(mem_view_t v, uint64_t module_base, const char* name, + uint64_t* rva_out, uint32_t* vsize_out) { + size_t sec; uint16_t nsec; + if (!name || !pe_section_table(v, module_base, &sec, &nsec)) return false; + size_t want = strlen(name); if (want > 8) want = 8; for (uint16_t i = 0; i < nsec; i++) { - const size_t sh = sec + (size_t)i * 40; - if (sh + 40 > v.size) break; + const size_t sh = sec + (size_t)i * SH_SIZE; + if (sh + SH_SIZE > v.size) break; char nm[9] = {0}; memcpy(nm, v.data + sh, 8); if (strncmp(nm, name, want) == 0 && (want == 8 || nm[want] == '\0')) { uint32_t vsize, vaddr; - memcpy(&vsize, v.data + sh + 8, 4); /* Misc.VirtualSize */ - memcpy(&vaddr, v.data + sh + 12, 4); /* VirtualAddress */ + memcpy(&vsize, v.data + sh + SH_VSIZE_OFF, 4); /* Misc.VirtualSize */ + memcpy(&vaddr, v.data + sh + SH_VADDR_OFF, 4); /* VirtualAddress */ if (rva_out) *rva_out = vaddr; if (vsize_out) *vsize_out = vsize; return true; @@ -42,6 +79,29 @@ bool pe_find_section(mem_view_t v, uint64_t module_base, const char* name, return false; } +int pe_sections(mem_view_t v, uint64_t module_base, pe_secrec* out, int max) { + size_t sec; uint16_t nsec; + if (!pe_section_table(v, module_base, &sec, &nsec)) return -1; + + int total = 0; + for (uint16_t i = 0; i < nsec; i++) { + const size_t sh = sec + (size_t)i * SH_SIZE; + if (sh + SH_SIZE > v.size) break; /* headers truncated in view */ + if (out && total < max) { + pe_secrec* r = &out[total]; + memset(r->name, 0, sizeof r->name); + memcpy(r->name, v.data + sh, 8); /* name[8] stays the NUL slot */ + uint32_t ch; + memcpy(&r->vsize, v.data + sh + SH_VSIZE_OFF, 4); + memcpy(&r->rva, v.data + sh + SH_VADDR_OFF, 4); + memcpy(&ch, v.data + sh + SH_CHAR_OFF, 4); + r->prot = pe_prot(ch); + } + total++; + } + return total; +} + bool pe_section(mem_view_t v, uint64_t module_base, const char* name, mem_view_t* out) { uint64_t rva; uint32_t vsize; if (!out || !pe_find_section(v, module_base, name, &rva, &vsize)) return false; @@ -61,3 +121,67 @@ int vmie_pe_section(vmie_mem* m, uintptr_t cr3, uint64_t module_base, out->data = buf; out->size = n; out->base_va = module_base + rva; return 0; } + +/* ---- public win32 surface: section enumeration + section views ----------- * + * Cold paths (one-shot header parse / section gather, not a hot loop). They + * reuse pe_sections / the shared section-table walk above - no second parser - + * and never allocate: the section_view buffer is caller-owned. */ + +int vmie_win32_sections(vmie_win32* v, uint64_t cr3, uint64_t module_base, + section_desc* out, int max) __attribute__((cold)); +int vmie_win32_sections(vmie_win32* v, uint64_t cr3, uint64_t module_base, + section_desc* out, int max) { + vmie_mem* m = vmie_win32_mem(v); + if (!m) { return -1; } + + uint8_t hdr[0x1000]; + if (gva_read(m, cr3, module_base, hdr, sizeof hdr)) { return -1; } + const mem_view_t hv = { hdr, sizeof hdr, module_base }; + + /* count first via pe_sections(out=NULL); if the caller only wants the count + * (out==NULL) we are done. */ + const int total = pe_sections(hv, module_base, NULL, 0); + if (total < 0 || !out) { return total; } + + pe_secrec recs[96]; + int cap = max; + if (cap < 0) { cap = 0; } + if (cap > (int)(sizeof recs / sizeof recs[0])) { + cap = (int)(sizeof recs / sizeof recs[0]); + } + const int got = pe_sections(hv, module_base, recs, cap); + if (got < 0) { return got; } + + int n = got < cap ? got : cap; + for (int i = 0; i < n; i++) { + memcpy(out[i].name, recs[i].name, sizeof out[i].name); + out[i].rva = recs[i].rva; + out[i].vsize = recs[i].vsize; + out[i].prot = recs[i].prot; + } + return total; /* total count, even if it exceeded `max` */ +} + +int vmie_win32_section_view(vmie_win32* v, uint64_t cr3, uint64_t module_base, + const section_desc* sec, view_base mode, + uint8_t* buf, size_t bufcap, mem_view_t* out) + __attribute__((cold)); +int vmie_win32_section_view(vmie_win32* v, uint64_t cr3, uint64_t module_base, + const section_desc* sec, view_base mode, + uint8_t* buf, size_t bufcap, mem_view_t* out) { + vmie_mem* m = vmie_win32_mem(v); + if (!m || !sec || !buf || !out) { return -1; } + + const size_t n = sec->vsize < bufcap ? sec->vsize : bufcap; + if (gva_read(m, cr3, module_base + sec->rva, buf, n)) { return -1; } + + uint64_t base_va = 0; + switch (mode) { + case MODULE_RVA: base_va = sec->rva; break; + case ABSOLUTE_VA: base_va = module_base + sec->rva; break; + case SECTION_LOCAL: + default: base_va = 0; break; + } + out->data = buf; out->size = n; out->base_va = base_va; + return 0; +}