Add operand-normalized semantic code signatures

New semsig_hash(mem_view_t) primitive (include/semsig.h): a position-,
register-, immediate- and instruction-order-invariant function fingerprint.
It folds per-instruction operand-canonical tokens, splitting the body into
basic blocks via cfg_blocks and using a within-block order-insensitive
sort-then-fold (between-block order preserved), so it survives compiler
register reallocation and instruction scheduling that the byte-mask
func_hash/sig_generate cannot. Distinct hash domain from func_hash.

The operand decode is delegated to an OPTIONAL external disassembler behind
a private adapter seam (src/handlers/semsig_backend.h); semsig.c stays
backend-neutral and includes no backend header. VMIE_DISASM={OFF|zydis|
capstone} gates the build: OFF (default) compiles only semsig_stub.c and
keeps the zero-dependency build (VMIE_HAVE_DISASM=0, packages unchanged).
The backend is brought from VMIE_DISASM_SRC or find_package - never
vendored. Adds the vmie_win32_func_semsig wrapper.

The CI deb job builds with the Zydis backend (libzydis-dev); the runtime
package dependency on libzydis is derived automatically by dpkg-shlibdeps.
This commit is contained in:
2026-06-28 21:51:47 +03:00
parent 8ca84a5861
commit 1b06206043
10 changed files with 955 additions and 1 deletions
+49
View File
@@ -6,6 +6,7 @@
#include "sigscan.h" /* mem_sub (pure matcher; engine may use it) */
#include "win32.h" /* public surface: vmie_win32, section_desc, view_base */
#include "x86dec.h" /* x86_decode / x86_branch_target (call-graph step) */
#include "semsig.h" /* semsig_hash (generic; no backend header pulled here) */
/* IMAGE_SECTION_HEADER: 8-byte Name, then Misc.VirtualSize(+8), VirtualAddress
* (+12), and Characteristics(+36); the header is 40 bytes wide. */
@@ -775,3 +776,51 @@ int vmie_win32_func_imports(vmie_win32* v, uint64_t cr3, uint64_t module_base,
free(fb);
return total;
}
/* Semantic hash of one function by func_rva: reuse the func_imports gather (find
* the extent from .pdata, read the body), then delegate to the generic
* semsig_hash over a SECTION_LOCAL view. No normalization/fold is duplicated
* here, and no backend header is included - the feature is inherited via the
* generic (a stub returning 0 in an OFF build). Cold: one-shot per function. */
uint64_t vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3,
uint64_t module_base, uint32_t func_rva)
__attribute__((cold));
uint64_t vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3,
uint64_t module_base, uint32_t func_rva) {
vmie_mem* m = vmie_win32_mem(v);
if (!m) { return 0; }
/* locate the function's extent from .pdata (count then gather). */
const int nfn = vmie_win32_functions(v, cr3, module_base, NULL, 0);
if (nfn < 0) { return 0; }
func_range stack_fr[256];
func_range* fr = stack_fr;
func_range* heap_fr = NULL;
if (nfn > (int)(sizeof stack_fr / sizeof stack_fr[0])) {
heap_fr = malloc((size_t)nfn * sizeof *heap_fr);
if (!heap_fr) { return 0; }
fr = heap_fr;
}
const int got = vmie_win32_functions(v, cr3, module_base, fr, nfn);
if (got < 0) { free(heap_fr); return 0; }
uint32_t fn_size = 0;
for (int f = 0; f < got; f++) {
if (fr[f].rva == func_rva) { fn_size = fr[f].size; break; }
}
free(heap_fr);
if (fn_size == 0) { return 0; } /* not a known function start */
/* gather the body and view it SECTION_LOCAL (base_va = 0): semsig_hash is
* position-independent, so the local base is enough for a stable value. */
uint8_t* fb = malloc(fn_size);
if (!fb) { return 0; }
if (gva_read(m, cr3, module_base + func_rva, fb, fn_size)) {
free(fb);
return 0;
}
const mem_view_t view = { .data = fb, .size = fn_size, .base_va = 0 };
const uint64_t h = semsig_hash(view);
free(fb);
return h;
}