mirror of
https://dev.lirent.ru/Vatrog/vm-introspection-engine.git
synced 2026-06-18 02:06:36 +03:00
Add code-structure analysis: call graph, jump tables, basic blocks, constant xref
Wave 1 of the code-analysis layer, built on the x86-64 decoder: - vmie_win32_callgraph walks each .pdata function with the decoder and emits an edge for every direct call/jmp whose target lands in the module - the intra-module call graph. Indirect edges are left to the IAT and jump tables. - gva_jumptable recovers a switch's case targets from an indirect jump's table: consecutive pointer entries that land in an executable region. - cfg_blocks splits one function view into basic blocks (a generic handler: leaders from intra-function branch targets, cut after jmp/jcc/ret). - gva_imm_xref finds the instructions whose immediate operand equals a constant - the dual of code-xref for magic values, error codes, syscall numbers. The decoder now also reports imm_off/imm_len so a caller can read or match the immediate operand. The generic primitives live in the new codeanalysis.h (jump tables, basic blocks) and scan.h (constant xref); the .pdata-bound call graph stays on the win32 surface and reuses the existing function/section/decode primitives - no second PE or instruction parser.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
/* codeanalysis.h - generic (OS-agnostic) x86-64 code-structure analysis.
|
||||
*
|
||||
* Handler layer: built on the generic memory model (memmodel.h: cr3 + VA, the
|
||||
* region map, gva_read) and the light x86-64 decoder (x86dec.h). It names no
|
||||
* Windows object - jump-table recovery and basic-block splitting are properties
|
||||
* of code and the address space, not of any particular OS. The win32-specific
|
||||
* call graph (which needs .pdata) lives in win32.h instead.
|
||||
*
|
||||
* These are the structure-recovery primitives that sit above the decoder and
|
||||
* gva_code_xref / gva_imm_xref (scan.h): given a function body or an indirect
|
||||
* jump's table, reconstruct the control flow the linear scanners cannot see.
|
||||
*/
|
||||
#ifndef VMIE_CODEANALYSIS_H
|
||||
#define VMIE_CODEANALYSIS_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "memmodel.h" /* vmie_mem, cr3+VA, vregion/VR_*, gva_read/gva_regions */
|
||||
#include "sigscan.h" /* mem_view_t (the single owner of the view type) */
|
||||
#include "x86dec.h" /* x86_decode, x86_insn, x86_branch_target */
|
||||
|
||||
/* Jump-table recovery. From `table_va`, read consecutive 8-byte entries and
|
||||
* keep those that point into an EXECUTABLE region under `cr3` (membership tested
|
||||
* against the live region map, i.e. a VR_X run from gva_regions); stop at the
|
||||
* first entry that is not a code pointer, at a read failure, or at `max`. The
|
||||
* entries are absolute 64-bit code VAs (the common /CASE jump-table form a
|
||||
* compiler emits for a switch). Writes up to `max` recovered targets to
|
||||
* `targets` (NULL to count only) and returns the number recovered.
|
||||
*
|
||||
* Feed it the table address taken from an indirect jump's memory operand - e.g.
|
||||
* `jmp qword [rip+disp]` => rip+disp (x86_riprel_target), or the base of a
|
||||
* `jmp qword [base + idx*8]` SIB table - to recover a switch's case targets and
|
||||
* complete the control-flow graph that the linear decoders (cfg_blocks,
|
||||
* vmie_win32_callgraph) leave dangling at the indirect jump.
|
||||
*
|
||||
* Returns 0 when the first entry is already not a code pointer (an empty/absent
|
||||
* table), so a 0 return is "no table here", not an error.
|
||||
*
|
||||
* Example - resolve a switch reached by `jmp qword [rip+disp]`:
|
||||
* x86_insn in; x86_decode(code, avail, &in); // the indirect jmp
|
||||
* uint64_t tbl = x86_riprel_target(jmp_va, &in); // table base VA
|
||||
* uint64_t cases[64];
|
||||
* int n = gva_jumptable(m, cr3, tbl, cases, 64); // case target VAs */
|
||||
int gva_jumptable(vmie_mem* m, uintptr_t cr3, uint64_t table_va,
|
||||
uint64_t* targets, int max);
|
||||
|
||||
/* One basic block inside a function view. The offsets are in the VIEW's own
|
||||
* coordinate space (mem_view_t.base_va + offset): for a SECTION_LOCAL view they
|
||||
* are section-local byte offsets, for a MODULE_RVA view they are RVAs.
|
||||
* start - byte offset of the block's first instruction (inclusive)
|
||||
* end - byte offset just past the block's last instruction (exclusive), so
|
||||
* the block spans [start, end) and its length is end - start. */
|
||||
typedef struct { uint32_t start; uint32_t end; } code_block;
|
||||
|
||||
/* Split one function's bytes into basic blocks. `fn` is a view spanning exactly
|
||||
* one function (e.g. a section-view sub-range covering a func_range from
|
||||
* vmie_win32_functions): fn.data[0] is the function's first byte and fn.size its
|
||||
* length. Two linear passes over the bytes with the decoder:
|
||||
* 1. collect intra-function branch targets (the destinations of jmp/jcc whose
|
||||
* target lands inside [0, fn.size)) - these are leaders;
|
||||
* 2. cut a block after every jmp/jcc/ret and before every leader. A CALL is
|
||||
* treated as fall-through (it returns), so it does NOT end a block. A
|
||||
* branch whose target is OUTSIDE `fn` (a tail call or inter-procedural jmp)
|
||||
* ends the block but starts no new one inside `fn`.
|
||||
*
|
||||
* Blocks are emitted in ascending start order, partition [0, fn.size) with no
|
||||
* gaps or overlaps, and are reported in the view's coordinate space (start/end
|
||||
* are offsets from fn.base_va). Writes up to `max` blocks to `out` (NULL to
|
||||
* count only) and returns the TOTAL block count, or -1 if the bytes do not
|
||||
* decode cleanly (a desync: the linear walk hit an undecodable byte). Pure: it
|
||||
* touches only the view and the decoder, no vmie_mem / no I/O.
|
||||
*
|
||||
* Example - block count and extents of one function:
|
||||
* mem_view_t fn; // a SECTION_LOCAL/RVA sub-view of one function
|
||||
* code_block bb[256];
|
||||
* int n = cfg_blocks(fn, bb, 256);
|
||||
* for (int i = 0; i < n && i < 256; i++)
|
||||
* printf("block %d: [%#x, %#x)\n", i, bb[i].start, bb[i].end); */
|
||||
int cfg_blocks(mem_view_t fn, code_block* out, int max);
|
||||
|
||||
#endif /* VMIE_CODEANALYSIS_H */
|
||||
@@ -83,6 +83,34 @@ int gva_sig_scan_multi(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
||||
int gva_code_xref(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
||||
uint64_t target_va, uint64_t* out, int max);
|
||||
|
||||
/* immediate / constant xref: every instruction in [lo,hi] (kept by the
|
||||
* protection filter `prot_any`; pass VR_X to restrict to code) whose IMMEDIATE
|
||||
* operand equals `value`, compared over the low `width` bytes (width is 1, 2, 4,
|
||||
* or 8). Like gva_code_xref it brute-scans each byte offset with the light
|
||||
* x86-64 decoder (x86dec.h, NOT a full disassembler) and carries a >=15-byte
|
||||
* (max x86 instruction length) sweep overlap so no instruction is cut at a
|
||||
* window seam; the same SEAM and INTERIOR de-duplications apply (a match
|
||||
* starting in a non-last window's trailing overlap is left to the next window,
|
||||
* and an interior alias falling inside an already-accepted match is dropped).
|
||||
*
|
||||
* An instruction matches when it carries an immediate (imm_len > 0) at least
|
||||
* `width` bytes wide and its low `width` bytes equal `value & mask(width)`. The
|
||||
* rel/RIP-relative DISPLACEMENT of a branch is NOT an immediate and never
|
||||
* matches here - use gva_code_xref for displacement targets.
|
||||
*
|
||||
* Records each matching instruction-start VA in the view's coordinate space.
|
||||
* Writes up to `max` VAs to `out` (NULL to count only) and returns the TOTAL
|
||||
* number of matches, or -1 on bad input (a NULL m, an unswept range, or a width
|
||||
* that is not 1/2/4/8). Use it to answer "what code uses the constant N" - error
|
||||
* codes, magic values, syscall numbers, table sizes, struct sizes.
|
||||
*
|
||||
* Example - sites that load the NTSTATUS 0xC0000022 (ACCESS_DENIED) as a dword:
|
||||
* uint64_t sites[64];
|
||||
* int n = gva_imm_xref(m, cr3, lo, hi, VR_X, 0xC0000022ull, 4, sites, 64); */
|
||||
int gva_imm_xref(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
||||
uint32_t prot_any, uint64_t value, int width,
|
||||
uint64_t* out, int max);
|
||||
|
||||
/* gva bridges to the signature matcher: build mem_view from guest memory and feed sigscan.h */
|
||||
int gva_sig_scan (vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
||||
uint32_t prot_any, const sig_pattern_t* p, uint64_t* out, int max);
|
||||
|
||||
@@ -295,6 +295,42 @@ typedef struct { uint32_t rva; uint32_t size; } func_range;
|
||||
int vmie_win32_functions(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
func_range* out, int max);
|
||||
|
||||
/* One call-graph edge, with both endpoints as RVAs relative to the module base
|
||||
* (absolute VA = module_base + rva).
|
||||
* from - RVA of the function that contains the call/jmp site (a .pdata
|
||||
* function start)
|
||||
* to - RVA of the branch target (inside the same module image)
|
||||
* kind - 0 = call (E8 / direct CALL), 1 = direct jmp (E9/EB, including a tail
|
||||
* call to another function). */
|
||||
typedef struct { uint32_t from; uint32_t to; uint8_t kind; } call_edge;
|
||||
|
||||
/* Build the intra-module call graph of the image at `module_base` (in the `cr3`
|
||||
* address space). Reuses the existing primitives - vmie_win32_functions to
|
||||
* enumerate the .pdata function starts, vmie_win32_section_view to gather the
|
||||
* .text bytes, and x86_decode to step each function - and emits one edge for
|
||||
* every DIRECT call/jmp (has_rel) whose resolved target lands inside the module
|
||||
* image [module_base, module_base + SizeOfImage). `from` is the containing
|
||||
* function's RVA, `to` is the target's RVA.
|
||||
*
|
||||
* INDIRECT calls/jmps (through a register or memory, e.g. `call [rip+disp]` or
|
||||
* `jmp rax`) are SKIPPED here - they carry no static rel target. Resolve those
|
||||
* separately: switch tables via gva_jumptable, import thunks via the IAT (a
|
||||
* wave-2 concern). A direct branch whose target falls OUTSIDE the image (an
|
||||
* inter-module jmp/call) is also skipped - the graph is intra-module by
|
||||
* construction.
|
||||
*
|
||||
* Writes up to `max` edges to `out` (NULL to count only) and returns the TOTAL
|
||||
* edge count, or -1 if the .pdata/.text directory is missing or unreadable.
|
||||
* Edges are grouped by source function (all of one function's edges are
|
||||
* contiguous), in ascending function order.
|
||||
*
|
||||
* Example - out-degree of each function:
|
||||
* call_edge e[4096];
|
||||
* int n = vmie_win32_callgraph(v, pr->cr3, m.base, e, 4096);
|
||||
* // group by e[i].from to get each function's callees */
|
||||
int vmie_win32_callgraph(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
call_edge* out, int max);
|
||||
|
||||
/* One exported symbol from the module export directory (EAT).
|
||||
* rva - export target RVA (absolute VA = module_base + rva). Forwarder
|
||||
* exports report the forwarder-string RVA; see `forwarded`.
|
||||
|
||||
+29
-1
@@ -46,6 +46,26 @@ typedef struct {
|
||||
uint8_t disp_len; /* displacement length: 1 (rel8), 4 (rel32 or RIP-rel
|
||||
* disp32), else 0 (no displacement). The wildcard span is
|
||||
* [disp_off, disp_off + disp_len). */
|
||||
uint8_t imm_off; /* byte offset, within the instruction, of the IMMEDIATE
|
||||
* operand (the trailing constant: imm8/16/32/64 of mov
|
||||
* reg,imm / cmp r/m,imm / push imm / test / add ...), or
|
||||
* 0 if the instruction carries no immediate
|
||||
* (imm_len == 0). This is distinct from disp_off: disp_*
|
||||
* is the rel/RIP-relative DISPLACEMENT (an address that
|
||||
* floats with the load address), imm_* is the encoded
|
||||
* CONSTANT operand. An instruction can have neither, one,
|
||||
* or - for a few forms (e.g. a RIP-relative store of an
|
||||
* immediate) - both. The immediate value lives at
|
||||
* code[imm_off .. imm_off + imm_len), little-endian. */
|
||||
uint8_t imm_len; /* immediate length in bytes: 1, 2, 4, or 8 (resolved
|
||||
* against the effective operand size: the 66 prefix and
|
||||
* REX.W are honoured, so e.g. mov r,imm is 2/4/8 and
|
||||
* push imm / cmp r/m,imm32 is 2/4). 0 when the
|
||||
* instruction has no single immediate operand; the rare
|
||||
* combined-immediate forms (ENTER imm16,imm8; far ptr)
|
||||
* also report 0 here - they are not a clean constant.
|
||||
* The constant-xref scanner (gva_imm_xref) reads the low
|
||||
* `width` bytes at imm_off when imm_len >= width. */
|
||||
} x86_insn;
|
||||
|
||||
/* Decode ONE 64-bit-mode instruction at `code` (`avail` readable bytes). Fills
|
||||
@@ -59,7 +79,15 @@ typedef struct {
|
||||
* byte position and length of the rel/RIP-relative displacement field within the
|
||||
* instruction (0/0 when there is none). These are exactly the bytes that float
|
||||
* with the load address / relocation, so a signature generator wildcards
|
||||
* [disp_off, disp_off+disp_len) and keeps the rest as must-match. */
|
||||
* [disp_off, disp_off+disp_len) and keeps the rest as must-match.
|
||||
*
|
||||
* It also reports out->imm_off / out->imm_len: the position and length of the
|
||||
* trailing IMMEDIATE constant operand (imm8/16/32/64), or 0/0 when there is
|
||||
* none. The immediate is the encoded literal (a magic value, error code, table
|
||||
* size, syscall number, ...) - distinct from the rel/RIP displacement. The
|
||||
* length honours the 66 prefix and REX.W (so mov r,imm is 2/4/8); combined-
|
||||
* immediate forms (ENTER, far ptr) report imm_len 0. This is what the
|
||||
* constant-xref scanner (gva_imm_xref) compares against a wanted value. */
|
||||
int x86_decode(const uint8_t* code, size_t avail, x86_insn* out);
|
||||
|
||||
/* Absolute target of a rel branch: ip + insn->len + insn->rel (0 unless has_rel). */
|
||||
|
||||
Reference in New Issue
Block a user