mirror of
https://dev.lirent.ru/Vatrog/vm-introspection-engine.git
synced 2026-07-09 01:46:38 +03:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
51feb8d39c
|
|||
|
1b06206043
|
|||
|
8ca84a5861
|
|||
|
91e5e05de4
|
|||
|
32440c7104
|
|||
|
d26f6c0bf0
|
@@ -86,13 +86,14 @@ jobs:
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
cmake make gcc libc6-dev dpkg-dev \
|
||||
cmake make gcc libc6-dev dpkg-dev file \
|
||||
libzydis-dev \
|
||||
gcc-mingw-w64-x86-64 ca-certificates curl
|
||||
|
||||
- name: Configure
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DVMIE_VERSION=${TAG#v}
|
||||
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DVMIE_VERSION=${TAG#v} -DVMIE_DISASM=zydis
|
||||
|
||||
- name: Build shared library
|
||||
run: cmake --build build -j --target vmie_shared
|
||||
|
||||
+74
-1
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.18) # find_program(... REQUIRED)
|
||||
|
||||
set(VMIE_VERSION "0.1.4" CACHE STRING "Library version (MAJOR.MINOR.PATCH); CI passes the tag")
|
||||
set(VMIE_VERSION "0.2.0" CACHE STRING "Library version (MAJOR.MINOR.PATCH); CI passes the tag")
|
||||
project(vmi-engine VERSION ${VMIE_VERSION} LANGUAGES C)
|
||||
|
||||
set(CMAKE_C_STANDARD 17) # generation B uses no C23 feature
|
||||
@@ -12,6 +12,18 @@ include(GNUInstallDirs)
|
||||
|
||||
option(VMIE_LTO "Enable LTO" OFF) # build-only; shipped default is -O2, no LTO
|
||||
|
||||
# ---- optional semantic-signature backend (operand disassembler) ----------
|
||||
# VMIE_DISASM selects the OPTIONAL operand-decode backend for the semantic
|
||||
# signature feature (semsig.h). OFF (default) ships the 0.1.6 behaviour: the
|
||||
# generic symbol exists via semsig_stub.c, returns 0, VMIE_HAVE_DISASM=0, and no
|
||||
# external library is pulled - CI and the .deb packages are unchanged. A backend
|
||||
# (zydis|capstone) is NEVER vendored: it is brought from OUTSIDE the tree, either
|
||||
# by VMIE_DISASM_SRC (a path to the backend's own source tree, add_subdirectory'd)
|
||||
# or, failing that, by find_package of an already-installed library.
|
||||
set(VMIE_DISASM "OFF" CACHE STRING "Semantic-signature backend: OFF | zydis | capstone")
|
||||
set_property(CACHE VMIE_DISASM PROPERTY STRINGS OFF zydis capstone)
|
||||
set(VMIE_DISASM_SRC "" CACHE PATH "Path to the disassembler backend source tree (external; add_subdirectory'd)")
|
||||
|
||||
# ---- host: VMI core objects (single owner of the source list) -----------
|
||||
add_library(vmie_obj OBJECT
|
||||
src/core/gpa.c
|
||||
@@ -31,6 +43,23 @@ add_library(vmie_obj OBJECT
|
||||
src/handlers/x86dec.c
|
||||
src/handlers/pmap.c
|
||||
src/handlers/snapdiff.c)
|
||||
|
||||
# semantic-signature TUs: OFF compiles ONLY the stub (which carries the whole
|
||||
# generic symbol and returns 0); a backend compiles the core (semsig.c) + the one
|
||||
# backend TU that talks to the disassembler. semsig.c is never built in OFF (it
|
||||
# calls the backend decode, absent there) - so there is no dead/unresolved call.
|
||||
if(VMIE_DISASM STREQUAL "OFF")
|
||||
target_sources(vmie_obj PRIVATE src/handlers/semsig_stub.c)
|
||||
elseif(VMIE_DISASM STREQUAL "zydis")
|
||||
target_sources(vmie_obj PRIVATE src/handlers/semsig.c
|
||||
src/handlers/semsig_backend_zydis.c)
|
||||
elseif(VMIE_DISASM STREQUAL "capstone")
|
||||
target_sources(vmie_obj PRIVATE src/handlers/semsig.c
|
||||
src/handlers/semsig_backend_capstone.c)
|
||||
else()
|
||||
message(FATAL_ERROR "VMIE_DISASM must be OFF, zydis, or capstone (got '${VMIE_DISASM}')")
|
||||
endif()
|
||||
|
||||
target_include_directories(vmie_obj
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # public API: include/*.h
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/core/include # private: core.h
|
||||
@@ -42,11 +71,49 @@ if(VMIE_LTO)
|
||||
target_link_options(vmie_obj PRIVATE -flto)
|
||||
endif()
|
||||
|
||||
# Resolve the backend target/package name (the actual link/define is applied to
|
||||
# vmie_obj here AND re-applied to the static/shared libs below, because
|
||||
# $<TARGET_OBJECTS:vmie_obj> carries object files only - not link/define usage
|
||||
# requirements). The backend is PRIVATE (an implementation detail), while
|
||||
# VMIE_HAVE_DISASM is PUBLIC (visible through semsig.h). The backend comes from
|
||||
# VMIE_DISASM_SRC (external sources) or find_package - never from in-tree sources.
|
||||
if(NOT VMIE_DISASM STREQUAL "OFF")
|
||||
if(VMIE_DISASM STREQUAL "zydis")
|
||||
set(VMIE_DISASM_LINK "Zydis::Zydis")
|
||||
set(_vmie_disasm_pkg "Zydis")
|
||||
else()
|
||||
set(VMIE_DISASM_LINK "capstone::capstone")
|
||||
set(_vmie_disasm_pkg "capstone")
|
||||
endif()
|
||||
|
||||
if(VMIE_DISASM_SRC)
|
||||
# Build the backend from its OWN external source tree (kept out of this
|
||||
# repo). Its CMake exports the imported target linked below.
|
||||
add_subdirectory(${VMIE_DISASM_SRC} ${CMAKE_CURRENT_BINARY_DIR}/_disasm_backend)
|
||||
else()
|
||||
find_package(${_vmie_disasm_pkg} REQUIRED)
|
||||
endif()
|
||||
|
||||
target_link_libraries(vmie_obj PRIVATE ${VMIE_DISASM_LINK})
|
||||
target_compile_definitions(vmie_obj PUBLIC VMIE_HAVE_DISASM=1)
|
||||
else()
|
||||
set(VMIE_DISASM_LINK "")
|
||||
target_compile_definitions(vmie_obj PUBLIC VMIE_HAVE_DISASM=0)
|
||||
endif()
|
||||
|
||||
# ---- host: static library (in-tree consumers: CLIs + test) --------------
|
||||
# $<TARGET_OBJECTS:> carries object files only, not usage requirements:
|
||||
# re-declare the PUBLIC include scope so vmie_cli/vmie_scan still see include/.
|
||||
add_library(vmie STATIC $<TARGET_OBJECTS:vmie_obj>)
|
||||
target_include_directories(vmie PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
# Re-apply the feature macro (PUBLIC: seen by in-tree consumers + the test) and
|
||||
# the backend link (PRIVATE), which $<TARGET_OBJECTS> does not carry over.
|
||||
if(VMIE_DISASM STREQUAL "OFF")
|
||||
target_compile_definitions(vmie PUBLIC VMIE_HAVE_DISASM=0)
|
||||
else()
|
||||
target_compile_definitions(vmie PUBLIC VMIE_HAVE_DISASM=1)
|
||||
target_link_libraries(vmie PRIVATE ${VMIE_DISASM_LINK})
|
||||
endif()
|
||||
|
||||
# ---- host: shared library (external delivery: libvmie.so.*) -------------
|
||||
add_library(vmie_shared SHARED $<TARGET_OBJECTS:vmie_obj>)
|
||||
@@ -58,6 +125,12 @@ set_target_properties(vmie_shared PROPERTIES
|
||||
if(VMIE_LTO)
|
||||
target_link_options(vmie_shared PRIVATE -flto)
|
||||
endif()
|
||||
if(VMIE_DISASM STREQUAL "OFF")
|
||||
target_compile_definitions(vmie_shared PUBLIC VMIE_HAVE_DISASM=0)
|
||||
else()
|
||||
target_compile_definitions(vmie_shared PUBLIC VMIE_HAVE_DISASM=1)
|
||||
target_link_libraries(vmie_shared PRIVATE ${VMIE_DISASM_LINK})
|
||||
endif()
|
||||
|
||||
# ---- host: CLI demonstrator over the library ----------------------------
|
||||
add_executable(vmie_cli src/cli.c)
|
||||
|
||||
+5
-1
@@ -125,7 +125,11 @@ typedef struct {
|
||||
* x86-64 has no read bit: a present page is readable, so VR_R is always set on a
|
||||
* returned region. Write/execute/user are the EFFECTIVE rights along the whole
|
||||
* page-table path (RW & US are AND-ed across levels, NX is OR-ed), not just the
|
||||
* leaf entry, so they reflect what the guest CPU actually enforces. */
|
||||
* leaf entry, so they reflect what the guest CPU actually enforces. One
|
||||
* exception keeps VR_X honest under KVA shadow (KPTI): the kernel CR3 marks
|
||||
* user-half PML4Es NX as hardening, but ring 3 executes those pages via the
|
||||
* user/shadow CR3 (same shared lower tables, NX-clear PML4E), so the user-half
|
||||
* top-level NX bit is ignored when deriving VR_X. */
|
||||
#ifndef VMIE_VREGION_DEFINED
|
||||
#define VMIE_VREGION_DEFINED
|
||||
#define VR_R 0x1u /* readable (present => always set) */
|
||||
|
||||
+12
-9
@@ -70,18 +70,21 @@ int gva_sig_scan_multi(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
||||
uint32_t prot_any, const sigset* s,
|
||||
sig_multi_hit* out, int max);
|
||||
|
||||
/* code-xref: every instruction in the X-regions of [lo,hi] whose near rel
|
||||
* branch or RIP-relative memory operand resolves to `target_va`. Brute-scans
|
||||
* each byte offset with the light x86-64 decoder (x86dec.h, NOT a full
|
||||
* disassembler): an E8/E9/EB/Jcc rel branch matches when next_rip + rel ==
|
||||
/* code-xref: every instruction in the regions of [lo,hi] kept by `prot_any`
|
||||
* whose near rel branch or RIP-relative memory operand resolves to `target_va`.
|
||||
* Brute-scans each byte offset with the light x86-64 decoder (x86dec.h, NOT a
|
||||
* full disassembler): an E8/E9/EB/Jcc rel branch matches when next_rip + rel ==
|
||||
* target_va, and any RIP-relative operand (ModRM mod=00, rm=101) matches when
|
||||
* next_rip + disp32 == target_va (this covers lea/mov and any other rip-rel
|
||||
* form). Records each matching instruction-start VA. The sweep forces VR_X and
|
||||
* carries a >=15-byte overlap (max x86 instruction length) so no instruction is
|
||||
* cut at a window seam. Writes up to `max` VAs to `out` (NULL to count only) and
|
||||
* returns the TOTAL number of matches, or -1 on bad input. */
|
||||
* form). Records each matching instruction-start VA. The sweep is scoped by
|
||||
* `prot_any` (pass VR_X to restrict to code; pass VR_R when the target's
|
||||
* page-table X bit is unavailable - the decoder and exact target match keep the
|
||||
* result correct, only more bytes are decoded) and carries a >=15-byte overlap
|
||||
* (max x86 instruction length) so no instruction is cut at a window seam. Writes
|
||||
* up to `max` VAs to `out` (NULL to count only) and returns the TOTAL number of
|
||||
* matches, or -1 on bad input. */
|
||||
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);
|
||||
uint32_t prot_any, 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
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/* semsig.h - generic (OS-agnostic) semantic / normalized code signature.
|
||||
*
|
||||
* Handler layer: a NORMALIZED function fingerprint that survives what the byte
|
||||
* world cannot. func_hash (codeanalysis.h) and sig_generate (siggen.h) are
|
||||
* BYTE fingerprints - both only neutralize the rel/RIP-relative displacement
|
||||
* bytes; they still change when the compiler shuffles registers, picks a
|
||||
* different equivalent register allocation, alters an immediate/displacement
|
||||
* VALUE, or reschedules independent instructions. semsig_hash instead decodes
|
||||
* OPERANDS (registers, operand classes, sizes, mnemonic group) and folds a
|
||||
* canonical token stream, so it answers "semantically the same function" rather
|
||||
* than "byte-for-byte the same function" - a FLIRT-like recognition primitive.
|
||||
*
|
||||
* Operand decode is NOT in the light decoder (x86dec.h is deliberately pure and
|
||||
* length-only). It is supplied by an OPTIONAL external disassembler (Zydis or
|
||||
* Capstone) selected at configure time, behind a private adapter seam. So this
|
||||
* primitive is FEATURE-GATED: with no backend the symbol still exists (ABI
|
||||
* stable) but returns the "no hash" sentinel 0, and VMIE_HAVE_DISASM is 0 (see
|
||||
* below). The byte world (func_hash / sig_generate) is untouched either way.
|
||||
*
|
||||
* NOT COMPARABLE TO func_hash. semsig_hash uses a different algorithm over a
|
||||
* different (normalized) input, so its values live in their OWN domain: never
|
||||
* compare a semsig_hash to a func_hash. Compare semsig_hash to semsig_hash only
|
||||
* (equality / a known-hash table), exactly as func_hash is used for library-ID.
|
||||
*/
|
||||
#ifndef VMIE_SEMSIG_H
|
||||
#define VMIE_SEMSIG_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "memmodel.h" /* mem_view_t (the single owner of the view type) */
|
||||
|
||||
/* Compile-time feature availability. Pushed from CMake as a PUBLIC compile
|
||||
* definition so consumers of this header see the SAME value the library was
|
||||
* built with:
|
||||
* VMIE_HAVE_DISASM == 1 built with a disassembler backend; semsig_hash is
|
||||
* live, semsig_backend_name() names the backend.
|
||||
* VMIE_HAVE_DISASM == 0 built without a backend (the default); semsig_hash
|
||||
* always returns 0 and semsig_backend_name() == "none".
|
||||
* Test it BEFORE calling to tell "feature off" apart from "empty/undecodable
|
||||
* function" - both return 0, but only the latter is a real input. The default
|
||||
* here keeps the header valid when compiled outside the project build. */
|
||||
#ifndef VMIE_HAVE_DISASM
|
||||
#define VMIE_HAVE_DISASM 0
|
||||
#endif
|
||||
|
||||
/* Reorder-/register-canonical semantic hash of one function view. `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, fn.size its length. Reported in the view's own coordinate space, but the
|
||||
* hash is position-INDEPENDENT (like func_hash), so SECTION_LOCAL is enough for
|
||||
* a stable value.
|
||||
*
|
||||
* What it normalizes (the canonicalization contract):
|
||||
* - mnemonic CLASS is kept (the semantic backbone of each instruction);
|
||||
* - the concrete REGISTER identity is erased to its register CLASS, so a
|
||||
* rax<->rcx reshuffle does not change the hash;
|
||||
* - operand WIDTH is kept (mov al,_ differs from mov rax,_);
|
||||
* - immediate and displacement VALUES are erased (only "an imm of this width
|
||||
* is present" survives), as func_hash/sig_generate already do for rel/RIP;
|
||||
* - memory-operand SHAPE is kept (has_base / has_index / is_riprel flags, not
|
||||
* the register ids);
|
||||
* - operand ORDER is kept (mov dst,src differs from mov src,dst).
|
||||
*
|
||||
* REORDER INVARIANCE (v1 default behavior) AND ITS COST. The fold is two-level
|
||||
* and tied to the CFG. The function is split into basic blocks (via cfg_blocks),
|
||||
* and:
|
||||
* - WITHIN a block the per-instruction token hashes are folded ORDER-
|
||||
* INSENSITIVELY (sorted, then folded), so the compiler's intra-block
|
||||
* instruction scheduling - reordering independent instructions - does not
|
||||
* change the hash;
|
||||
* - BETWEEN blocks the order is PRESERVED (blocks are folded in cfg_blocks'
|
||||
* ascending-start order), because block order is the control-flow structure
|
||||
* and must stay significant.
|
||||
* The cost, stated plainly (this is a PROPERTY of the hash, not a free win): the
|
||||
* within-block order-insensitive fold LOWERS discrimination - two blocks with
|
||||
* the same MULTISET of normalized instructions but a different internal order
|
||||
* yield the SAME block hash, even when that order is semantically meaningful
|
||||
* (a data dependency this primitive does not model). That RAISES the false-match
|
||||
* (collision) probability versus an order-sensitive hash. It is a deliberate
|
||||
* trade for robustness against the scheduler; zero collisions are NOT promised.
|
||||
*
|
||||
* Returns the 64-bit semantic hash, or 0 on any of: `fn` empty (no data / size
|
||||
* 0), a decode desync (cfg_blocks could not split the bytes, or the backend
|
||||
* could not decode an instruction inside a block), or a build with no
|
||||
* disassembler backend (VMIE_HAVE_DISASM == 0). 0 is therefore "no hash", never
|
||||
* a valid fingerprint - the SAME sentinel as func_hash, so callers handle it the
|
||||
* same way.
|
||||
*
|
||||
* Token layout (stability contract). The per-instruction token folded by this
|
||||
* function is a fixed little-endian tuple; its layout is part of the hash's
|
||||
* stability contract (changing it changes every value). Per instruction:
|
||||
* byte 0 : mnemonic_class (the stable SEM_MN_* class id, low 8 bits)
|
||||
* byte 1 : mnemonic_class high (the SEM_MN_* class id, high 8 bits)
|
||||
* byte 2 : noperands (0..4)
|
||||
* byte 3 : flags (bit0 = is_control_flow)
|
||||
* bytes 4.. : per operand i in [0, noperands), 2 bytes each:
|
||||
* +0 : (kind & 0x0f) | ((reg_class & 0x0f) << 4)
|
||||
* +1 : (width_log2 & 0x07)
|
||||
* | (mem_has_base ? 0x08 : 0)
|
||||
* | (mem_has_index ? 0x10 : 0)
|
||||
* | (mem_is_riprel ? 0x20 : 0)
|
||||
* trailing : for the OTHER mnemonic class only, the raw opcode byte(s) so
|
||||
* unclassified instructions still differ from one another.
|
||||
* Operand order in the tuple follows the instruction's operand order (kept).
|
||||
*
|
||||
* Example - recognize a library function semantically (register-shuffle stable):
|
||||
* mem_view_t fn; // a SECTION_LOCAL sub-view of one function body
|
||||
* uint64_t h = semsig_hash(fn);
|
||||
* if (h && h == known_crt_memcpy_semsig) puts("looks like memcpy");
|
||||
*
|
||||
* Example - guard on availability before relying on the feature:
|
||||
* #if VMIE_HAVE_DISASM
|
||||
* uint64_t h = semsig_hash(fn); // live; 0 only on empty/desync
|
||||
* #else
|
||||
* // feature not built (semsig_hash would return 0)
|
||||
* #endif */
|
||||
uint64_t semsig_hash(mem_view_t fn) __attribute__((cold));
|
||||
|
||||
/* Stable identity of the compiled disassembler backend, for diagnostics and for
|
||||
* tagging which decoder produced a stored hash. Returns a static, never-NULL
|
||||
* string: the backend name (e.g. "zydis", "capstone") when VMIE_HAVE_DISASM==1,
|
||||
* or "none" when built without a backend. The normalized hash is designed to be
|
||||
* domain-stable ACROSS backends (the mnemonic classes are backend-neutral), so
|
||||
* this name is informational, not a hash-comparison key. */
|
||||
const char* semsig_backend_name(void);
|
||||
|
||||
#endif /* VMIE_SEMSIG_H */
|
||||
@@ -461,6 +461,37 @@ int vmie_win32_inline_hooks(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
int vmie_win32_func_imports(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
uint32_t func_rva, uint32_t* iat_rvas, int max);
|
||||
|
||||
/* Reorder-/register-canonical SEMANTIC hash of ONE function, addressed by its
|
||||
* func_rva (a .pdata function start, as from vmie_win32_functions or an export).
|
||||
* The win32 convenience entry point over the generic semsig_hash (semsig.h): it
|
||||
* locates the function extent from .pdata (like vmie_win32_func_imports),
|
||||
* gathers its body with gva_read, builds a SECTION_LOCAL mem_view_t over those
|
||||
* bytes, and DELEGATES to semsig_hash - it does NOT reimplement normalization or
|
||||
* folding, and it pulls in no disassembler backend itself (the feature is
|
||||
* inherited transitively via the same VMIE_HAVE_DISASM as the generic).
|
||||
*
|
||||
* v - engine handle.
|
||||
* cr3 - the process address space the module is mapped in.
|
||||
* module_base - image base VA (its PE headers must be resident).
|
||||
* func_rva - function start RVA (must match a .pdata function start).
|
||||
*
|
||||
* Returns the 64-bit semantic hash, or 0 on any of: func_rva is not a known
|
||||
* function start, the body is unreadable (paged out), a decode desync, or the
|
||||
* build has no disassembler backend (VMIE_HAVE_DISASM == 0) - the SAME "no hash"
|
||||
* sentinel as semsig_hash / func_hash. NOT COMPARABLE to func_hash: it is a
|
||||
* different (normalized) domain; compare it only to other vmie_win32_func_semsig
|
||||
* / semsig_hash values. semsig_hash is position-independent, so the SECTION_LOCAL
|
||||
* gather gives a stable value regardless of where the module is based.
|
||||
*
|
||||
* Reorder invariance and its cost (lowered intra-block discrimination => higher
|
||||
* collision risk) are properties of the underlying semsig_hash - see semsig.h.
|
||||
*
|
||||
* Example - name a function semantically against a known-hash table:
|
||||
* uint64_t h = vmie_win32_func_semsig(v, pr->cr3, m.base, fn_rva);
|
||||
* if (h && h == known_crt_strlen_semsig) puts("strlen"); */
|
||||
uint64_t vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3,
|
||||
uint64_t module_base, uint32_t func_rva);
|
||||
|
||||
/* Devirtualization (C++ vtables) needs NO dedicated symbol - it is a
|
||||
* COMPOSITION of primitives the engine already exposes:
|
||||
* - a vtable at `vtable_va` is an array of code pointers, so its METHODS are
|
||||
|
||||
+10
-1
@@ -158,7 +158,16 @@ int gva_regions(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
||||
if (!(e4 & PG_P)) continue;
|
||||
const uint64_t b4 = VA_CANON((uint64_t)i4 << 39);
|
||||
if (!rgn_hit(b4, 1ull << 39, lo, hi)) continue;
|
||||
const int rw4 = (e4 >> 1) & 1, us4 = (e4 >> 2) & 1, nx4 = (int)(e4 >> 63) & 1;
|
||||
const int rw4 = (e4 >> 1) & 1, us4 = (e4 >> 2) & 1;
|
||||
/* On a KVA-shadow (KPTI) Windows the kernel CR3 marks user-half PML4Es
|
||||
* NX as hardening: the kernel must not execute user pages through its own
|
||||
* mapping. Ring 3 runs under the user/shadow CR3, which reaches the SAME
|
||||
* lower tables (PDPT/PD/PT are shared) through a PML4E with NX clear, so
|
||||
* that top-level NX is not what executes user code. Drop it on the user
|
||||
* half; sub-PML4E NX stays authoritative (it is shared between the two
|
||||
* CR3s). On a non-KPTI guest a code-covering user PML4E already has NX
|
||||
* clear, so this only ever discards a zero. i4 < 256 == canonical user. */
|
||||
const int nx4 = (i4 < 256) ? 0 : ((int)(e4 >> 63) & 1);
|
||||
|
||||
const uint64_t* t3 = gpa_ptr(m, e4 & PFN_MASK, 4096);
|
||||
if (!t3) continue;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -115,10 +115,10 @@ static int xref_sweep_cb(void* u, const uint8_t* data, size_t len,
|
||||
}
|
||||
|
||||
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) {
|
||||
uint32_t prot_any, uint64_t target_va, uint64_t* out, int max) {
|
||||
struct xref_cb c; memset(&c, 0, sizeof c);
|
||||
c.target = target_va; c.out = out; c.max = max;
|
||||
if (gva_sweep(m, cr3, lo, hi, VR_X, X86_MAX_INSN, xref_sweep_cb, &c) < 0) {
|
||||
if (gva_sweep(m, cr3, lo, hi, prot_any, X86_MAX_INSN, xref_sweep_cb, &c) < 0) {
|
||||
return -1;
|
||||
}
|
||||
return c.n;
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/* semsig.c - normalized (semantic) code-signature core (see semsig.h).
|
||||
*
|
||||
* Built ONLY when a disassembler backend is selected (VMIE_DISASM != OFF); the
|
||||
* OFF build compiles semsig_stub.c instead. It turns one function view into a
|
||||
* register-/value-/reorder-canonical 64-bit hash:
|
||||
* 1. split the function into basic blocks - REUSING cfg_blocks (codeanalysis.h),
|
||||
* not a second splitter;
|
||||
* 2. step each block with the BACKEND decoder (semsig_backend.h), normalize
|
||||
* each instruction into a fixed token tuple (the layout documented in
|
||||
* semsig.h), and FNV-1a-fold that tuple into a per-instruction hash;
|
||||
* 3. fold a block's per-instruction hashes ORDER-INSENSITIVELY (sort, then
|
||||
* fold) so intra-block instruction scheduling does not change the result;
|
||||
* 4. fold the block hashes into the final hash IN cfg_blocks' order (block
|
||||
* order is the CFG structure and stays significant).
|
||||
*
|
||||
* Boundary: includes ONLY semsig.h, semsig_backend.h, codeanalysis.h (for
|
||||
* cfg_blocks / code_block) and the standard headers. It NEVER includes a
|
||||
* backend's own headers (Zydis/Capstone) - the seam is semsig_backend.h. The
|
||||
* pure core (x86dec, codeanalysis, ...) is reused, never modified.
|
||||
*
|
||||
* FNV-1a uses the SAME constants and scheme as codeanalysis.c so the hash world
|
||||
* stays stylistically consistent. They are NOT factored into a shared header for
|
||||
* a single second consumer (Rule of three: a tolerated 2nd appearance of two
|
||||
* short #defines); a 3rd consumer would justify a private util.
|
||||
*
|
||||
* Cold path: a one-shot pass over one function body, not a hot loop.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h> /* malloc/free (per-block hash buffer overflow only) */
|
||||
#include "semsig.h"
|
||||
#include "semsig_backend.h"
|
||||
#include "codeanalysis.h" /* cfg_blocks, code_block (REUSED splitter) */
|
||||
|
||||
#define FNV64_OFFSET 0xcbf29ce484222325ull
|
||||
#define FNV64_PRIME 0x00000100000001b3ull
|
||||
|
||||
/* Caps. CFG_MAX_BLOCKS bounds the stack block array; functions with more blocks
|
||||
* spill the block list to the heap. INSN_STACK bounds the per-block per-insn
|
||||
* hash array on the stack; a denser block spills that one buffer to the heap. */
|
||||
#define CFG_MAX_BLOCKS 512
|
||||
#define INSN_STACK 256
|
||||
|
||||
/* Fold one byte into an FNV-1a accumulator. */
|
||||
static inline uint64_t fnv_byte(uint64_t h, uint8_t b) {
|
||||
h ^= b;
|
||||
h *= FNV64_PRIME;
|
||||
return h;
|
||||
}
|
||||
|
||||
/* Normalize ONE decoded instruction into its token tuple and return the
|
||||
* per-instruction FNV-1a hash of that tuple. The tuple layout is the stability
|
||||
* contract documented in semsig.h: mnemonic class, noperands, a flags byte, then
|
||||
* 2 bytes per operand (kind|reg_class, then width_log2|mem-shape flags), plus
|
||||
* the raw opcode for the OTHER class so unclassified instructions still differ.
|
||||
* Pure data->data: no branching on a concrete register, only on its class. */
|
||||
static uint64_t token_hash(const sem_insn* in) {
|
||||
uint64_t h = FNV64_OFFSET;
|
||||
h = fnv_byte(h, (uint8_t)(in->mnemonic_class & 0xff));
|
||||
h = fnv_byte(h, (uint8_t)((in->mnemonic_class >> 8) & 0xff));
|
||||
const uint8_t nop = in->noperands <= 4 ? in->noperands : 4;
|
||||
h = fnv_byte(h, nop);
|
||||
h = fnv_byte(h, (uint8_t)(in->is_control_flow ? 0x01u : 0x00u));
|
||||
for (uint8_t i = 0; i < nop; i++) {
|
||||
const uint8_t b0 = (uint8_t)((in->op[i].kind & 0x0f) |
|
||||
((in->op[i].reg_class & 0x0f) << 4));
|
||||
const uint8_t b1 = (uint8_t)((in->op[i].width_log2 & 0x07) |
|
||||
(in->op[i].mem_has_base ? 0x08u : 0u) |
|
||||
(in->op[i].mem_has_index ? 0x10u : 0u) |
|
||||
(in->op[i].mem_is_riprel ? 0x20u : 0u));
|
||||
h = fnv_byte(h, b0);
|
||||
h = fnv_byte(h, b1);
|
||||
}
|
||||
if (in->mnemonic_class == SEM_MN_OTHER) {
|
||||
h = fnv_byte(h, in->raw_opcode); /* keep unclassified ops distinct */
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/* Ascending uint64_t comparator for the deterministic per-block sort. The sort
|
||||
* is what makes the within-block fold order-INSENSITIVE while keeping the
|
||||
* MULTISET (duplicates survive, their count is significant). A plain xor/sum was
|
||||
* REJECTED: xor cancels identical-instruction pairs (a block "loses" two equal
|
||||
* moves) and sum collides easily on multiset permutations; sort-then-fold avoids
|
||||
* both. */
|
||||
static int cmp_u64(const void* a, const void* b) {
|
||||
const uint64_t x = *(const uint64_t*)a;
|
||||
const uint64_t y = *(const uint64_t*)b;
|
||||
return (x > y) - (x < y);
|
||||
}
|
||||
|
||||
/* Fold one basic block [start, end) of `fn` into a block hash, stepping the
|
||||
* BACKEND decoder from start to end and order-insensitively folding the per-insn
|
||||
* hashes. Returns 1 on success and writes *out; returns 0 on backend desync
|
||||
* (an undecodable instruction inside the block) - the caller turns that into the
|
||||
* 0 "no hash" sentinel. */
|
||||
static int fold_block(mem_view_t fn, const code_block* blk, uint64_t* out) {
|
||||
const size_t start = blk->start;
|
||||
const size_t end = blk->end <= fn.size ? blk->end : fn.size;
|
||||
if (end <= start) { *out = FNV64_OFFSET; return 1; } /* empty block */
|
||||
|
||||
uint64_t stack_h[INSN_STACK];
|
||||
uint64_t* hh = stack_h;
|
||||
uint64_t* heap_h = NULL;
|
||||
const size_t span = end - start; /* >= 1 insn per byte */
|
||||
if (span > INSN_STACK) {
|
||||
heap_h = malloc(span * sizeof *heap_h);
|
||||
if (!heap_h) { return 0; }
|
||||
hh = heap_h;
|
||||
}
|
||||
|
||||
size_t n = 0;
|
||||
int ok = 1;
|
||||
for (size_t off = start; off < end; ) {
|
||||
sem_insn in;
|
||||
if (!semsig_backend_decode(fn.data + off, end - off, &in) ||
|
||||
in.length == 0) {
|
||||
ok = 0;
|
||||
break; /* backend desync */
|
||||
}
|
||||
hh[n++] = token_hash(&in);
|
||||
off += in.length;
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
qsort(hh, n, sizeof *hh, cmp_u64); /* order-insensitive */
|
||||
uint64_t h = FNV64_OFFSET;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const uint64_t v = hh[i];
|
||||
for (int b = 0; b < 8; b++) {
|
||||
h = fnv_byte(h, (uint8_t)((v >> (b * 8)) & 0xff));
|
||||
}
|
||||
}
|
||||
*out = h;
|
||||
}
|
||||
|
||||
free(heap_h);
|
||||
return ok;
|
||||
}
|
||||
|
||||
uint64_t semsig_hash(mem_view_t fn) __attribute__((cold));
|
||||
uint64_t semsig_hash(mem_view_t fn) {
|
||||
if (!fn.data || fn.size == 0) { return 0; }
|
||||
|
||||
/* Block partition (count then gather), REUSING cfg_blocks - -1 is a desync
|
||||
* and maps to the 0 sentinel. cfg_blocks gives ORDER-of-blocks boundaries;
|
||||
* the within-block step uses the backend, so a backend/x86_decode length
|
||||
* disagreement only ever turns into the 0 sentinel, never a wrong hash. */
|
||||
const int nb = cfg_blocks(fn, NULL, 0);
|
||||
if (nb <= 0) { return 0; }
|
||||
|
||||
code_block stack_bb[CFG_MAX_BLOCKS];
|
||||
code_block* bb = stack_bb;
|
||||
code_block* heap_bb = NULL;
|
||||
if (nb > CFG_MAX_BLOCKS) {
|
||||
heap_bb = malloc((size_t)nb * sizeof *heap_bb);
|
||||
if (!heap_bb) { return 0; }
|
||||
bb = heap_bb;
|
||||
}
|
||||
const int got = cfg_blocks(fn, bb, nb);
|
||||
if (got <= 0) { free(heap_bb); return 0; }
|
||||
const int use = got < nb ? got : nb;
|
||||
|
||||
/* Fold block hashes into the final hash IN cfg_blocks' ascending-start order:
|
||||
* block order is the CFG structure and stays significant (only WITHIN a
|
||||
* block is the fold order-insensitive). */
|
||||
uint64_t h = FNV64_OFFSET;
|
||||
for (int i = 0; i < use; i++) {
|
||||
uint64_t bh;
|
||||
if (!fold_block(fn, &bb[i], &bh)) { /* backend desync */
|
||||
free(heap_bb);
|
||||
return 0;
|
||||
}
|
||||
for (int b = 0; b < 8; b++) {
|
||||
h = fnv_byte(h, (uint8_t)((bh >> (b * 8)) & 0xff));
|
||||
}
|
||||
}
|
||||
|
||||
free(heap_bb);
|
||||
return h;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/* semsig_backend.h - backend-agnostic operand-decode boundary (PRIVATE).
|
||||
*
|
||||
* The ONE place semsig.c talks to an external disassembler. Each concrete
|
||||
* backend (Zydis, Capstone, ...) provides exactly the entities below; semsig.c
|
||||
* includes THIS header and NEVER a backend's own headers. That keeps the
|
||||
* operand-decode dependency behind a single seam so the feature core stays
|
||||
* backend-neutral and the resulting hash is domain-stable across backends.
|
||||
*
|
||||
* This header is intentionally NOT in include/ - it is an implementation detail
|
||||
* of the feature, not public API. The public surface is semsig.h.
|
||||
*/
|
||||
#ifndef VMIE_SEMSIG_BACKEND_H
|
||||
#define VMIE_SEMSIG_BACKEND_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Operand kind, normalized. Carried in sem_insn.op[].kind. */
|
||||
enum {
|
||||
SEMOP_NONE = 0, /* no operand in this slot */
|
||||
SEMOP_REG, /* register operand (identity erased, class kept) */
|
||||
SEMOP_MEM, /* memory operand (shape kept: base/index/riprel flags) */
|
||||
SEMOP_IMM /* immediate operand (value erased, width kept) */
|
||||
};
|
||||
|
||||
/* Canonical register class. Carried in sem_insn.op[].reg_class. The concrete
|
||||
* register id is NEVER carried across the seam - only its class - so a compiler
|
||||
* register reshuffle (rax<->rcx) does not change the normalized token. */
|
||||
enum {
|
||||
SEM_RC_NONE = 0, /* not a register / unknown */
|
||||
SEM_RC_GPR, /* general-purpose integer register (al..r15) */
|
||||
SEM_RC_XMM, /* vector register (xmm/ymm/zmm) */
|
||||
SEM_RC_MASK, /* AVX-512 mask register (k0..k7) */
|
||||
SEM_RC_SEG, /* segment register */
|
||||
SEM_RC_FLAGS, /* flags / control / other special register */
|
||||
SEM_RC_OTHER /* any register class not distinguished above */
|
||||
};
|
||||
|
||||
/* Backend-neutral mnemonic CLASS ids. NOT a 1:1 map of every mnemonic: only the
|
||||
* groups that matter for normalization. Each backend TU maps its own mnemonic
|
||||
* enum into these with a static table; unclassified mnemonics fall to
|
||||
* SEM_MN_OTHER, for which semsig.c additionally folds the raw opcode byte(s) so
|
||||
* they still differ from one another. These ids are part of the hash's
|
||||
* stability contract (see the token layout in semsig.h). */
|
||||
enum {
|
||||
SEM_MN_OTHER = 0, /* unclassified - raw opcode folded in (see semsig.h) */
|
||||
SEM_MN_MOV, /* register/memory moves (mov, movzx, movsx, lea, ...) */
|
||||
SEM_MN_ARITH, /* add/sub/inc/dec/neg/adc/sbb/mul/imul/div/idiv */
|
||||
SEM_MN_LOGIC, /* and/or/xor/not/shl/shr/sar/rol/ror */
|
||||
SEM_MN_CMP, /* cmp */
|
||||
SEM_MN_TEST, /* test */
|
||||
SEM_MN_BRANCH, /* conditional jump (jcc) / setcc / cmovcc */
|
||||
SEM_MN_JMP, /* unconditional jmp */
|
||||
SEM_MN_CALL, /* call */
|
||||
SEM_MN_RET, /* ret / retf / leave epilogue return */
|
||||
SEM_MN_PUSH, /* push */
|
||||
SEM_MN_POP, /* pop */
|
||||
SEM_MN_LEA, /* lea (address computation; kept distinct from mov) */
|
||||
SEM_MN_NOP, /* nop / padding */
|
||||
SEM_MN_FLOAT, /* x87/SSE/AVX float arithmetic */
|
||||
SEM_MN_VEC /* integer/other vector ops */
|
||||
};
|
||||
|
||||
/* A decoded instruction reduced to ONLY what normalization needs - no full
|
||||
* operand model leaks across the seam. */
|
||||
typedef struct {
|
||||
uint8_t length; /* full instruction length in bytes (>= 1) */
|
||||
uint16_t mnemonic_class; /* one of SEM_MN_* (backend-neutral group id) */
|
||||
uint8_t is_control_flow; /* 1 if branch/call/ret (affects normalization) */
|
||||
uint8_t noperands; /* number of meaningful operands (0..4) */
|
||||
uint8_t raw_opcode; /* for SEM_MN_OTHER: a raw opcode byte to fold,
|
||||
* so unclassified instructions still differ; 0
|
||||
* (and unused) for classified instructions */
|
||||
struct {
|
||||
uint8_t kind; /* SEMOP_NONE / SEMOP_REG / SEMOP_MEM / SEMOP_IMM */
|
||||
uint8_t reg_class; /* SEM_RC_* canonical register class (not an id) */
|
||||
uint8_t width_log2; /* operand size as log2 bytes (0=1B..4=16B), or 0 */
|
||||
uint8_t mem_has_base; /* memory operand has a base register */
|
||||
uint8_t mem_has_index;/* memory operand has an index register */
|
||||
uint8_t mem_is_riprel;/* memory operand is RIP-relative */
|
||||
} op[4];
|
||||
} sem_insn;
|
||||
|
||||
/* Decode ONE instruction at code[0..avail). Returns 1 on success (fills *out),
|
||||
* 0 on undecodable / not enough bytes. Stateless: no allocation, no I/O, no
|
||||
* shared mutable state - reentrant. */
|
||||
int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out);
|
||||
|
||||
/* Stable backend identity for diagnostics / hash-domain tagging. Static,
|
||||
* never-NULL. This declaration is shared with semsig.h's public symbol of the
|
||||
* same name; the concrete backend TU (or the stub) defines it once. */
|
||||
const char* semsig_backend_name(void);
|
||||
|
||||
#endif /* VMIE_SEMSIG_BACKEND_H */
|
||||
@@ -0,0 +1,189 @@
|
||||
/* semsig_backend_capstone.c - Capstone implementation of the operand-decode seam.
|
||||
*
|
||||
* The ONE TU that includes Capstone headers and links its library. It implements
|
||||
* the semsig_backend.h contract: decode one x86-64 instruction into a sem_insn
|
||||
* (length, backend-neutral mnemonic class, normalized operands), and report the
|
||||
* backend name. Built ONLY when VMIE_DISASM=capstone (see CMakeLists.txt).
|
||||
*
|
||||
* Stateless: a Capstone handle is opened/closed on the call stack per decode, no
|
||||
* global mutable state - reentrant. Capstone allocates internally for cs_disasm;
|
||||
* the buffer is freed before return so there is no leak across the seam.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <capstone/capstone.h>
|
||||
#include "semsig_backend.h"
|
||||
|
||||
/* Map a Capstone x86 instruction id to a backend-neutral SEM_MN_* class. STATIC
|
||||
* table, not a switch forest; unlisted ids fall through to SEM_MN_OTHER (where
|
||||
* the core folds the raw opcode). Conditional branches (Jcc) are a family folded
|
||||
* by Capstone group, handled before the table lookup. */
|
||||
typedef struct { unsigned int id; uint16_t cls; } mn_map;
|
||||
|
||||
static const mn_map MN_TABLE[] = {
|
||||
/* moves */
|
||||
{ X86_INS_MOV, SEM_MN_MOV },
|
||||
{ X86_INS_MOVZX, SEM_MN_MOV },
|
||||
{ X86_INS_MOVSX, SEM_MN_MOV },
|
||||
{ X86_INS_MOVSXD, SEM_MN_MOV },
|
||||
{ X86_INS_MOVAPS, SEM_MN_MOV },
|
||||
{ X86_INS_MOVAPD, SEM_MN_MOV },
|
||||
{ X86_INS_MOVDQA, SEM_MN_MOV },
|
||||
{ X86_INS_MOVDQU, SEM_MN_MOV },
|
||||
{ X86_INS_MOVD, SEM_MN_MOV },
|
||||
{ X86_INS_MOVQ, SEM_MN_MOV },
|
||||
{ X86_INS_XCHG, SEM_MN_MOV },
|
||||
{ X86_INS_LEA, SEM_MN_LEA },
|
||||
/* arithmetic */
|
||||
{ X86_INS_ADD, SEM_MN_ARITH },
|
||||
{ X86_INS_ADC, SEM_MN_ARITH },
|
||||
{ X86_INS_SUB, SEM_MN_ARITH },
|
||||
{ X86_INS_SBB, SEM_MN_ARITH },
|
||||
{ X86_INS_INC, SEM_MN_ARITH },
|
||||
{ X86_INS_DEC, SEM_MN_ARITH },
|
||||
{ X86_INS_NEG, SEM_MN_ARITH },
|
||||
{ X86_INS_MUL, SEM_MN_ARITH },
|
||||
{ X86_INS_IMUL, SEM_MN_ARITH },
|
||||
{ X86_INS_DIV, SEM_MN_ARITH },
|
||||
{ X86_INS_IDIV, SEM_MN_ARITH },
|
||||
/* logic / shifts */
|
||||
{ X86_INS_AND, SEM_MN_LOGIC },
|
||||
{ X86_INS_OR, SEM_MN_LOGIC },
|
||||
{ X86_INS_XOR, SEM_MN_LOGIC },
|
||||
{ X86_INS_NOT, SEM_MN_LOGIC },
|
||||
{ X86_INS_SHL, SEM_MN_LOGIC },
|
||||
{ X86_INS_SHR, SEM_MN_LOGIC },
|
||||
{ X86_INS_SAR, SEM_MN_LOGIC },
|
||||
{ X86_INS_ROL, SEM_MN_LOGIC },
|
||||
{ X86_INS_ROR, SEM_MN_LOGIC },
|
||||
/* compare / test */
|
||||
{ X86_INS_CMP, SEM_MN_CMP },
|
||||
{ X86_INS_TEST, SEM_MN_TEST },
|
||||
/* control flow */
|
||||
{ X86_INS_JMP, SEM_MN_JMP },
|
||||
{ X86_INS_CALL, SEM_MN_CALL },
|
||||
{ X86_INS_RET, SEM_MN_RET },
|
||||
{ X86_INS_RETF, SEM_MN_RET },
|
||||
{ X86_INS_RETFQ, SEM_MN_RET },
|
||||
{ X86_INS_PUSH, SEM_MN_PUSH },
|
||||
{ X86_INS_POP, SEM_MN_POP },
|
||||
{ X86_INS_NOP, SEM_MN_NOP },
|
||||
};
|
||||
|
||||
/* True if `g` is among the instruction's Capstone groups. */
|
||||
static int has_group(const cs_insn* in, uint8_t g) {
|
||||
const cs_detail* d = in->detail;
|
||||
if (!d) { return 0; }
|
||||
for (uint8_t i = 0; i < d->groups_count; i++) {
|
||||
if (d->groups[i] == g) { return 1; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint16_t classify_mnemonic(const cs_insn* in) {
|
||||
if (has_group(in, X86_GRP_CALL)) { return SEM_MN_CALL; }
|
||||
if (has_group(in, X86_GRP_RET)) { return SEM_MN_RET; }
|
||||
if (has_group(in, X86_GRP_JUMP)) {
|
||||
/* unconditional jmp vs conditional jcc: JMP id is unconditional. */
|
||||
return (in->id == X86_INS_JMP) ? SEM_MN_JMP : SEM_MN_BRANCH;
|
||||
}
|
||||
for (size_t i = 0; i < sizeof MN_TABLE / sizeof MN_TABLE[0]; i++) {
|
||||
if (MN_TABLE[i].id == in->id) { return MN_TABLE[i].cls; }
|
||||
}
|
||||
if (has_group(in, X86_GRP_FPU)) { return SEM_MN_FLOAT; }
|
||||
if (has_group(in, X86_GRP_SSE) || has_group(in, X86_GRP_SSE1) ||
|
||||
has_group(in, X86_GRP_SSE2)) { return SEM_MN_VEC; }
|
||||
return SEM_MN_OTHER;
|
||||
}
|
||||
|
||||
/* Map a Capstone x86 register id to a canonical SEM_RC_* class. Capstone has no
|
||||
* single "register class" accessor; classify by the documented id ranges. */
|
||||
static uint8_t reg_class_of(unsigned int reg) {
|
||||
if (reg == X86_REG_INVALID) { return SEM_RC_NONE; }
|
||||
if ((reg >= X86_REG_AH && reg <= X86_REG_BH) ||
|
||||
(reg >= X86_REG_AL && reg <= X86_REG_R15D) ||
|
||||
(reg >= X86_REG_RAX && reg <= X86_REG_RBP) ||
|
||||
(reg >= X86_REG_RSP && reg <= X86_REG_RDI) ||
|
||||
(reg >= X86_REG_R8 && reg <= X86_REG_R15)) { return SEM_RC_GPR; }
|
||||
if (reg >= X86_REG_XMM0 && reg <= X86_REG_ZMM31) { return SEM_RC_XMM; }
|
||||
if (reg >= X86_REG_K0 && reg <= X86_REG_K7) { return SEM_RC_MASK; }
|
||||
if (reg >= X86_REG_CS && reg <= X86_REG_SS) { return SEM_RC_SEG; }
|
||||
if (reg >= X86_REG_EFLAGS && reg <= X86_REG_RIP) { return SEM_RC_FLAGS; }
|
||||
return SEM_RC_OTHER;
|
||||
}
|
||||
|
||||
static uint8_t width_log2_of(uint8_t size_bytes) {
|
||||
uint8_t l = 0;
|
||||
uint8_t v = size_bytes;
|
||||
while (v > 1u && l < 4u) { v >>= 1; l++; }
|
||||
return l;
|
||||
}
|
||||
|
||||
int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out) {
|
||||
if (!code || !out || avail == 0) { return 0; }
|
||||
|
||||
csh h;
|
||||
if (cs_open(CS_ARCH_X86, CS_MODE_64, &h) != CS_ERR_OK) { return 0; }
|
||||
cs_option(h, CS_OPT_DETAIL, CS_OPT_ON);
|
||||
|
||||
cs_insn* insn = NULL;
|
||||
const size_t n = cs_disasm(h, code, avail, 0, 1, &insn);
|
||||
if (n < 1 || !insn) {
|
||||
if (insn) { cs_free(insn, n); }
|
||||
cs_close(&h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sizeof *out; i++) { ((uint8_t*)out)[i] = 0; }
|
||||
out->length = (uint8_t)insn->size;
|
||||
out->mnemonic_class = classify_mnemonic(insn);
|
||||
out->is_control_flow =
|
||||
(has_group(insn, X86_GRP_JUMP) || has_group(insn, X86_GRP_CALL) ||
|
||||
has_group(insn, X86_GRP_RET)) ? 1u : 0u;
|
||||
out->raw_opcode = (out->mnemonic_class == SEM_MN_OTHER && insn->detail)
|
||||
? insn->detail->x86.opcode[0] : 0u;
|
||||
|
||||
uint8_t no = 0;
|
||||
if (insn->detail) {
|
||||
const cs_x86* x = &insn->detail->x86;
|
||||
for (uint8_t i = 0; i < x->op_count && no < 4; i++) {
|
||||
const cs_x86_op* o = &x->operands[i];
|
||||
switch (o->type) {
|
||||
case X86_OP_REG:
|
||||
out->op[no].kind = SEMOP_REG;
|
||||
out->op[no].reg_class = reg_class_of(o->reg);
|
||||
out->op[no].width_log2 = width_log2_of(o->size);
|
||||
no++;
|
||||
break;
|
||||
case X86_OP_MEM:
|
||||
out->op[no].kind = SEMOP_MEM;
|
||||
out->op[no].width_log2 = width_log2_of(o->size);
|
||||
out->op[no].mem_has_base =
|
||||
(o->mem.base != X86_REG_INVALID &&
|
||||
o->mem.base != X86_REG_RIP) ? 1u : 0u;
|
||||
out->op[no].mem_has_index =
|
||||
(o->mem.index != X86_REG_INVALID) ? 1u : 0u;
|
||||
out->op[no].mem_is_riprel =
|
||||
(o->mem.base == X86_REG_RIP) ? 1u : 0u;
|
||||
no++;
|
||||
break;
|
||||
case X86_OP_IMM:
|
||||
out->op[no].kind = SEMOP_IMM;
|
||||
out->op[no].width_log2 = width_log2_of(o->size);
|
||||
no++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
out->noperands = no;
|
||||
|
||||
cs_free(insn, n);
|
||||
cs_close(&h);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* semsig_backend_name(void) {
|
||||
return "capstone";
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/* semsig_backend_zydis.c - Zydis implementation of the operand-decode seam.
|
||||
*
|
||||
* The ONE TU that includes Zydis headers and links its library. It implements
|
||||
* the semsig_backend.h contract: decode one x86-64 instruction into a sem_insn
|
||||
* (length, backend-neutral mnemonic class, normalized operands), and report the
|
||||
* backend name. Built ONLY when VMIE_DISASM=zydis (see CMakeLists.txt).
|
||||
*
|
||||
* Stateless: a ZydisDecoder is initialized on the call stack per decode, no
|
||||
* global mutable state - reentrant, no allocation, no I/O.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <Zydis/Zydis.h>
|
||||
#include "semsig_backend.h"
|
||||
|
||||
/* Map a Zydis mnemonic to a backend-neutral SEM_MN_* class. A STATIC table, not
|
||||
* a switch forest: only the groups that matter for normalization are listed;
|
||||
* everything else falls through to SEM_MN_OTHER (where the core folds the raw
|
||||
* opcode so unclassified instructions still differ). Keep entries sorted by
|
||||
* mnemonic for readability; lookup is a linear scan (cold path, one per insn). */
|
||||
typedef struct { ZydisMnemonic mn; uint16_t cls; } mn_map;
|
||||
|
||||
static const mn_map MN_TABLE[] = {
|
||||
/* moves */
|
||||
{ ZYDIS_MNEMONIC_MOV, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVZX, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVSX, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVSXD, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVAPS, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVAPD, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVDQA, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVDQU, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVD, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVQ, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_XCHG, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_LEA, SEM_MN_LEA },
|
||||
/* arithmetic */
|
||||
{ ZYDIS_MNEMONIC_ADD, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_ADC, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_SUB, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_SBB, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_INC, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_DEC, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_NEG, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_MUL, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_IMUL, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_DIV, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_IDIV, SEM_MN_ARITH },
|
||||
/* logic / shifts */
|
||||
{ ZYDIS_MNEMONIC_AND, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_OR, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_XOR, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_NOT, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_SHL, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_SHR, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_SAR, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_ROL, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_ROR, SEM_MN_LOGIC },
|
||||
/* compare / test */
|
||||
{ ZYDIS_MNEMONIC_CMP, SEM_MN_CMP },
|
||||
{ ZYDIS_MNEMONIC_TEST, SEM_MN_TEST },
|
||||
/* control flow */
|
||||
{ ZYDIS_MNEMONIC_JMP, SEM_MN_JMP },
|
||||
{ ZYDIS_MNEMONIC_CALL, SEM_MN_CALL },
|
||||
{ ZYDIS_MNEMONIC_RET, SEM_MN_RET },
|
||||
{ ZYDIS_MNEMONIC_PUSH, SEM_MN_PUSH },
|
||||
{ ZYDIS_MNEMONIC_POP, SEM_MN_POP },
|
||||
{ ZYDIS_MNEMONIC_NOP, SEM_MN_NOP },
|
||||
};
|
||||
|
||||
/* jcc / setcc / cmovcc are a wide family; classify them by Zydis meta category
|
||||
* rather than enumerating every conditional mnemonic. */
|
||||
static uint16_t classify_mnemonic(const ZydisDecodedInstruction* insn) {
|
||||
/* conditional branch family -> BRANCH */
|
||||
if (insn->meta.category == ZYDIS_CATEGORY_COND_BR) { return SEM_MN_BRANCH; }
|
||||
if (insn->meta.category == ZYDIS_CATEGORY_UNCOND_BR) { return SEM_MN_JMP; }
|
||||
if (insn->meta.category == ZYDIS_CATEGORY_CALL) { return SEM_MN_CALL; }
|
||||
if (insn->meta.category == ZYDIS_CATEGORY_RET) { return SEM_MN_RET; }
|
||||
|
||||
for (size_t i = 0; i < sizeof MN_TABLE / sizeof MN_TABLE[0]; i++) {
|
||||
if (MN_TABLE[i].mn == insn->mnemonic) { return MN_TABLE[i].cls; }
|
||||
}
|
||||
|
||||
/* float / vector fall-backs by meta category, else OTHER. */
|
||||
if (insn->meta.category == ZYDIS_CATEGORY_X87_ALU) { return SEM_MN_FLOAT; }
|
||||
if (insn->meta.isa_set >= ZYDIS_ISA_SET_SSE &&
|
||||
insn->meta.isa_set <= ZYDIS_ISA_SET_SSE4A) { return SEM_MN_VEC; }
|
||||
return SEM_MN_OTHER;
|
||||
}
|
||||
|
||||
/* Map a Zydis register to a canonical SEM_RC_* class (identity erased). */
|
||||
static uint8_t reg_class_of(ZydisRegister reg) {
|
||||
const ZydisRegisterClass rc = ZydisRegisterGetClass(reg);
|
||||
switch (rc) {
|
||||
case ZYDIS_REGCLASS_GPR8:
|
||||
case ZYDIS_REGCLASS_GPR16:
|
||||
case ZYDIS_REGCLASS_GPR32:
|
||||
case ZYDIS_REGCLASS_GPR64: return SEM_RC_GPR;
|
||||
case ZYDIS_REGCLASS_XMM:
|
||||
case ZYDIS_REGCLASS_YMM:
|
||||
case ZYDIS_REGCLASS_ZMM: return SEM_RC_XMM;
|
||||
case ZYDIS_REGCLASS_MASK: return SEM_RC_MASK;
|
||||
case ZYDIS_REGCLASS_SEGMENT: return SEM_RC_SEG;
|
||||
case ZYDIS_REGCLASS_FLAGS:
|
||||
case ZYDIS_REGCLASS_IP: return SEM_RC_FLAGS;
|
||||
case ZYDIS_REGCLASS_INVALID: return SEM_RC_NONE;
|
||||
default: return SEM_RC_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
/* width in bytes -> log2 (0..4), saturating. */
|
||||
static uint8_t width_log2_of(uint16_t size_bits) {
|
||||
const uint32_t bytes = (uint32_t)size_bits / 8u;
|
||||
uint8_t l = 0;
|
||||
uint32_t v = bytes;
|
||||
while (v > 1u && l < 4u) { v >>= 1; l++; }
|
||||
return l;
|
||||
}
|
||||
|
||||
int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out) {
|
||||
if (!code || !out || avail == 0) { return 0; }
|
||||
|
||||
ZydisDecoder dec;
|
||||
if (ZYAN_FAILED(ZydisDecoderInit(&dec, ZYDIS_MACHINE_MODE_LONG_64,
|
||||
ZYDIS_STACK_WIDTH_64))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ZydisDecodedInstruction insn;
|
||||
ZydisDecodedOperand ops[ZYDIS_MAX_OPERAND_COUNT];
|
||||
if (ZYAN_FAILED(ZydisDecoderDecodeFull(&dec, code, avail, &insn, ops))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sizeof *out; i++) { ((uint8_t*)out)[i] = 0; }
|
||||
out->length = (uint8_t)insn.length;
|
||||
out->mnemonic_class = classify_mnemonic(&insn);
|
||||
out->is_control_flow =
|
||||
(insn.meta.category == ZYDIS_CATEGORY_COND_BR ||
|
||||
insn.meta.category == ZYDIS_CATEGORY_UNCOND_BR ||
|
||||
insn.meta.category == ZYDIS_CATEGORY_CALL ||
|
||||
insn.meta.category == ZYDIS_CATEGORY_RET) ? 1u : 0u;
|
||||
out->raw_opcode = (out->mnemonic_class == SEM_MN_OTHER) ? insn.opcode : 0u;
|
||||
|
||||
/* Reduce only EXPLICIT operands (implicit ones - e.g. flags, rsp on push -
|
||||
* are decoder noise that would differ pointlessly across forms). */
|
||||
uint8_t no = 0;
|
||||
for (uint8_t i = 0; i < insn.operand_count && no < 4; i++) {
|
||||
const ZydisDecodedOperand* o = &ops[i];
|
||||
if (o->visibility != ZYDIS_OPERAND_VISIBILITY_EXPLICIT) { continue; }
|
||||
switch (o->type) {
|
||||
case ZYDIS_OPERAND_TYPE_REGISTER:
|
||||
out->op[no].kind = SEMOP_REG;
|
||||
out->op[no].reg_class = reg_class_of(o->reg.value);
|
||||
out->op[no].width_log2 = width_log2_of(o->size);
|
||||
no++;
|
||||
break;
|
||||
case ZYDIS_OPERAND_TYPE_MEMORY:
|
||||
out->op[no].kind = SEMOP_MEM;
|
||||
out->op[no].width_log2 = width_log2_of(o->size);
|
||||
out->op[no].mem_has_base =
|
||||
(o->mem.base != ZYDIS_REGISTER_NONE &&
|
||||
o->mem.base != ZYDIS_REGISTER_RIP) ? 1u : 0u;
|
||||
out->op[no].mem_has_index =
|
||||
(o->mem.index != ZYDIS_REGISTER_NONE) ? 1u : 0u;
|
||||
out->op[no].mem_is_riprel =
|
||||
(o->mem.base == ZYDIS_REGISTER_RIP) ? 1u : 0u;
|
||||
no++;
|
||||
break;
|
||||
case ZYDIS_OPERAND_TYPE_IMMEDIATE:
|
||||
out->op[no].kind = SEMOP_IMM;
|
||||
out->op[no].width_log2 = width_log2_of(o->size);
|
||||
no++;
|
||||
break;
|
||||
default:
|
||||
break; /* pointer/unused: ignored */
|
||||
}
|
||||
}
|
||||
out->noperands = no;
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* semsig_backend_name(void) {
|
||||
return "zydis";
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/* semsig_stub.c - the OFF (no-backend) path of the semantic-signature feature.
|
||||
*
|
||||
* Built into the library ONLY when VMIE_DISASM=OFF (the default). It provides
|
||||
* the full public generic surface so the ABI is stable - linking never fails
|
||||
* for a missing symbol - while the feature is not present:
|
||||
* semsig_hash -> 0 (the same "no hash" sentinel as func_hash)
|
||||
* semsig_backend_name -> "none"
|
||||
* Callers tell "feature off" from "empty/undecodable function" via the compile-
|
||||
* time macro VMIE_HAVE_DISASM (0 here), NOT via the runtime 0.
|
||||
*
|
||||
* In the OFF build semsig.c is NOT compiled (it calls the backend decode, which
|
||||
* does not exist without a backend); this stub is the whole generic symbol. See
|
||||
* CMakeLists.txt for the source-selection logic.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include "semsig.h"
|
||||
|
||||
uint64_t semsig_hash(mem_view_t fn) __attribute__((cold));
|
||||
uint64_t semsig_hash(mem_view_t fn) {
|
||||
(void)fn;
|
||||
return 0; /* feature not built: same sentinel as func_hash */
|
||||
}
|
||||
|
||||
const char* semsig_backend_name(void) {
|
||||
return "none";
|
||||
}
|
||||
Reference in New Issue
Block a user