11 Commits

Author SHA1 Message Date
lirent 51feb8d39c Bump version to 0.2.0 2026-06-28 21:52:22 +03:00
lirent 1b06206043 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.
2026-06-28 21:51:47 +03:00
lirent 8ca84a5861 Bump version to 0.1.6 2026-06-26 07:57:36 +03:00
lirent 91e5e05de4 Scope gva_code_xref by an explicit prot_any argument
It hard-coded VR_X, so a target whose page tables expose no executable
region silently yielded zero xrefs. Callers now pass the scope (VR_X to
restrict to code, VR_R when the X bit is unavailable); the decoder and
exact target match remain the correctness gate. Mirrors the prot_any
parameter gva_imm_xref already carries.
2026-06-26 07:57:35 +03:00
lirent 32440c7104 Drop the user-half top-level NX bit when deriving VR_X
Under KVA shadow (KPTI) the kernel CR3 marks user-half PML4Es NX as
hardening, so OR-ing that top-level NX down the walk made every user
region non-executable - even live code (ntdll .text read back r--u).
Ring 3 executes those pages via the user/shadow CR3, which reaches the
same shared lower tables through an NX-clear PML4E, so the kernel
table's user-half PML4E NX is not what enforces user execution. Ignore
it on the user half; sub-PML4E NX (shared between both CR3s) stays
authoritative.
2026-06-26 07:57:26 +03:00
lirent d26f6c0bf0 Install file(1) in the Debian CI job so shlibdeps can run
cpack's CPACK_DEBIAN_PACKAGE_SHLIBDEPS runs dpkg-shlibdeps, which needs
the file utility to detect ELF binaries and derive the runtime libc
dependency. The slim CI image does not ship it, so packaging failed at
the cpack step. Add file to the job toolchain.
2026-06-22 16:32:47 +03:00
lirent 0c3aa5ef25 Build a shared library and ship it as Debian packages
Factor the source list into an OBJECT library so the same objects back
both the existing static lib and a new shared libvmie.so (SONAME from the
project version; the version comes from the tag, falling back to a cached
default). Add install rules and a CPack DEB config that produce two
packages: libvmie0 (the runtime .so + SONAME symlink) and libvmie-dev
(public headers under include/vmie + the linker symlink), with the dev
package depending on the exact runtime version.

Add a CI job that, on a v* tag, builds the shared library, runs cpack,
and uploads both .deb files to the Gitea Debian package registry.
2026-06-22 15:52:39 +03:00
lirent 3028a4eb11 Take the kernel cr3 in the read-only win32 constructor
vmie_win32_open_ro_fd now takes a kcr3 and builds the struct-offset
profile read-only from the image (find ntoskrnl, resolve
PsInitialSystemProcess, build the profile, derive the System _EPROCESS),
so a read-only context has the full read surface - including
proc_list/proc_modules, not just the cr3-parameterised PE walks.

The read-only bring-up tail is factored out of host_bootstrap into a
shared helper; host_bootstrap keeps the beacon find / cr3 recovery / ACK
around it, so its behaviour and return codes are unchanged.
2026-06-20 17:02:29 +03:00
lirent 02e63f2ff0 Add a read-only win32 context over a file descriptor
vmie_win32_open_ro_fd maps the RAM backing file PROT_READ (via
gpa_from_ro_fd) and does NOT bootstrap; the caller passes a cr3 to each
read call. The read surfaces work this way - sections / functions /
imports / exports / callgraph / hooks, gva_read_text, and the generic
gva_* and scanner surfaces via vmie_win32_mem() - while any write returns
-1 because the mapping is read-only. The RW twin vmie_win32_open_fd and
host_bootstrap are unchanged.
2026-06-20 16:39:08 +03:00
lirent 5ea9a3785f Construct the memory source from a file descriptor
vmie_mem could only be built from a path. Add a second input: an
already-open file descriptor, dup()'d internally (borrowed - the
caller's fd stays valid, core owns and closes its copy).

  - vmie_mem_from_fd / vmie_mem_from_fd_segs   (read-write, as the path
    constructors: PROT_RW, MAP_SHARED)
  - vmie_mem_from_ro_fd / vmie_mem_from_ro_fd_segs   (read-only: map
    PROT_READ, mark the source ro; gpa_write/gva_write return -1, every
    read path is unchanged; accepts an O_RDONLY fd)
  - vmie_win32_open_fd   (the win32 context over an fd backing file)

Factor the mmap/validate tail and the single-low segment map out of the
path constructors into shared helpers, so the path and fd inputs go
through one mmap site and one map builder each.
2026-06-20 11:20:33 +03:00
lirent f401738702 Fix CI: install libc6-dev for the native compiler check
CMake's project(... C) check compiles AND links a test binary, which needs glibc's startup objects (Scrt1.o/crti.o). gcc only Recommends libc6-dev, so --no-install-recommends dropped it and configure failed before the cross-build. Install it explicitly.
2026-06-17 18:54:49 +03:00
17 changed files with 1401 additions and 62 deletions
+57 -1
View File
@@ -22,7 +22,7 @@ jobs:
apt-get update
apt-get install -y --no-install-recommends \
cmake make zip jq curl ca-certificates \
gcc gcc-mingw-w64-x86-64
gcc libc6-dev gcc-mingw-w64-x86-64
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@@ -70,3 +70,59 @@ jobs:
curl -fsSL -X POST -H "$auth" \
-F "attachment=@dist/${asset};type=application/zip" \
"${api}/releases/${rid}/assets?name=${asset}"
deb:
runs-on: ubuntu-latest
container:
image: node:20-bookworm-slim
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install toolchain
run: |
apt-get update
apt-get install -y --no-install-recommends \
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} -DVMIE_DISASM=zydis
- name: Build shared library
run: cmake --build build -j --target vmie_shared
- name: Package
run: |
set -euo pipefail
(cd build && cpack -G DEB)
ls -l build/*.deb
- name: Publish to Debian registry
env:
TOKEN: ${{ secrets.PUBLISH_TOKEN }} # requires scope: package:write
SERVER: ${{ github.server_url }}
OWNER: ${{ github.repository_owner }}
DISTRIBUTION: stable
COMPONENT: main
run: |
set -euo pipefail
url="${SERVER}/api/packages/${OWNER}/debian/pool/${DISTRIBUTION}/${COMPONENT}/upload"
auth="Authorization: token ${TOKEN}"
for deb in build/*.deb; do
# 201 Created = uploaded; 409 Conflict = this version already present (re-run).
# Both are success; anything else fails. Do not use curl -f (it would fail 409).
code=$(curl -s -o /dev/null -w '%{http_code}' \
-X PUT -H "$auth" -T "$deb" "$url")
echo "$deb -> HTTP $code"
if [ "$code" != 201 ] && [ "$code" != 409 ]; then
echo "upload failed: $deb (HTTP $code)" >&2
exit 1
fi
done
+142 -7
View File
@@ -1,15 +1,31 @@
cmake_minimum_required(VERSION 3.18) # find_program(... REQUIRED)
project(vmi-engine C)
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
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS ON) # deliberate: strnlen (POSIX) + void* arithmetic (GNU)
set(CMAKE_POSITION_INDEPENDENT_CODE ON) # -fPIC on all objects: static lib stays linkable into a shared object
include(GNUInstallDirs)
option(VMIE_LTO "Enable LTO" OFF) # build-only; shipped default is -O2, no LTO
# ---- host: VMI core as a static library ---------------------------------
add_library(vmie STATIC
# ---- 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
src/engine/gva.c
src/engine/sigphys.c
@@ -27,15 +43,93 @@ add_library(vmie STATIC
src/handlers/x86dec.c
src/handlers/pmap.c
src/handlers/snapdiff.c)
target_include_directories(vmie
# 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
${CMAKE_CURRENT_SOURCE_DIR}/src/engine/include # private: engine-arch.h, pe.h
${CMAKE_CURRENT_SOURCE_DIR}/src/engine/win32) # private: engine-win32.h, contract.h
target_compile_options(vmie PRIVATE -O2 -Wall -Wextra)
target_compile_options(vmie_obj PRIVATE -O2 -Wall -Wextra)
if(VMIE_LTO)
target_compile_options(vmie PRIVATE -flto)
target_link_options(vmie PRIVATE -flto)
target_compile_options(vmie_obj PRIVATE -flto)
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>)
target_include_directories(vmie_shared PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
set_target_properties(vmie_shared PROPERTIES
OUTPUT_NAME vmie
SOVERSION ${PROJECT_VERSION_MAJOR}
VERSION ${PROJECT_VERSION})
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 ----------------------------
@@ -61,3 +155,44 @@ add_custom_command(
COMMENT "Cross-compiling vmie-startup.exe (mingw-w64, x86-64)"
VERBATIM)
add_custom_target(vmie-startup ALL DEPENDS ${VMIE_STARTUP})
# ---- install: shared library + public headers ---------------------------
# Versioned file + SONAME symlink -> runtime; unversioned linker symlink -> dev.
install(TARGETS vmie_shared
LIBRARY
DESTINATION ${CMAKE_INSTALL_LIBDIR}
COMPONENT runtime
NAMELINK_COMPONENT dev)
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/vmie
COMPONENT dev
FILES_MATCHING PATTERN "*.h")
# ---- package: two Debian packages (runtime + dev) -----------------------
set(CPACK_GENERATOR "DEB")
set(CPACK_DEB_COMPONENT_INSTALL ON)
set(CPACK_COMPONENTS_ALL runtime dev)
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
# component -> Debian package name (Debian shared-library convention)
set(CPACK_DEBIAN_RUNTIME_PACKAGE_NAME "libvmie${PROJECT_VERSION_MAJOR}")
set(CPACK_DEBIAN_DEV_PACKAGE_NAME "libvmie-dev")
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64")
set(VMIE_DEB_MAINTAINER "Gregory Lirent <opensource@lirent.ru>"
CACHE STRING "Debian package Maintainer field")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${VMIE_DEB_MAINTAINER}")
set(CPACK_DEBIAN_PACKAGE_SECTION "libs")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_DEBIAN_FILE_NAME "DEB-DEFAULT")
set(CPACK_COMPONENT_RUNTIME_DESCRIPTION
"VM introspection engine (Windows-guest memory introspection) - shared library")
set(CPACK_COMPONENT_DEV_DESCRIPTION
"VM introspection engine (Windows-guest memory introspection) - development files")
# dev depends on the exact runtime version; runtime libc dependency via shlibdeps
set(CPACK_DEBIAN_DEV_PACKAGE_DEPENDS "libvmie${PROJECT_VERSION_MAJOR} (= ${PROJECT_VERSION})")
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
include(CPack)
+46 -2
View File
@@ -62,7 +62,47 @@ vmie_mem* vmie_mem_open(const char* path, uint64_t low);
* Returns a heap-owned handle, or NULL on failure. Release with vmie_mem_close(). */
vmie_mem* vmie_mem_open_segs(const char* path, const gpa_seg* segs, int nseg);
/* Unmap, close, and free a handle from vmie_mem_open*. Safe on NULL. */
/* Build a source from an already-open file descriptor `fd` instead of a path,
* with the single-`low` map (as vmie_mem_open). `fd` is BORROWED: it is dup()'d
* internally, so the source owns its own copy and the caller's `fd` stays open
* and valid both after this call AND after vmie_mem_close(). `fd` must reference
* an mmap-able, read-write object (a regular file, memfd_create, or POSIX shm) -
* a pipe/socket/tty is not mmap-able and yields NULL. The fd's file position is
* not read or changed (the mapping starts at offset 0). Returns a heap-owned
* handle, or NULL on dup/fstat/mmap failure. Release with vmie_mem_close(). */
vmie_mem* vmie_mem_from_fd(int fd, uint64_t low);
/* As vmie_mem_from_fd, but with an explicit segment map (`nseg` entries; see
* gpa_seg), well-formed against the file size. Same dup-borrow semantics: the
* caller's `fd` stays valid after this call and after vmie_mem_close(); `fd`
* must be an mmap-able RW object and its position is untouched. Returns a
* heap-owned handle, or NULL on failure. Release with vmie_mem_close(). */
vmie_mem* vmie_mem_from_fd_segs(int fd, const gpa_seg* segs, int nseg);
/* Read-only twin of vmie_mem_from_fd: build a source from `fd` with the
* single-`low` map, but map it PROT_READ and mark it read-only. On the returned
* handle gpa_write/gva_write (and any write through the engine) return -1; all
* reads - gpa_read/gva_read, gpa_ptr/gva_ptr, gva_regions, sig_scan_mem - behave
* exactly as on an RW source. Because only read access is needed, `fd` may be
* opened O_RDONLY (an O_RDWR fd is also accepted). Same dup-borrow semantics: the
* fd is dup()'d internally, so the caller's `fd` stays valid after this call AND
* after vmie_mem_close(); its file position is not read or changed. NOTE: writing
* THROUGH a pointer returned by gpa_ptr/gva_ptr on a read-only source is a
* contract violation (the pages are read-only and the store will fault) - use
* such pointers for reading only. Returns a heap-owned handle, or NULL on
* dup/fstat/mmap failure. Release with vmie_mem_close(). */
vmie_mem* vmie_mem_from_ro_fd(int fd, uint64_t low);
/* As vmie_mem_from_ro_fd, but with an explicit segment map (`nseg` entries; see
* gpa_seg), well-formed against the file size. Same read-only contract
* (writes -> -1; no writes through gpa_ptr/gva_ptr) and dup-borrow semantics
* (caller's `fd` stays valid after this call and after vmie_mem_close(); accepts
* an O_RDONLY or O_RDWR fd; position untouched). Returns a heap-owned handle, or
* NULL on failure. Release with vmie_mem_close(). */
vmie_mem* vmie_mem_from_ro_fd_segs(int fd, const gpa_seg* segs, int nseg);
/* Unmap, close, and free a handle from vmie_mem_open*, vmie_mem_from_fd*, or
* vmie_mem_from_ro_fd*. Safe on NULL. */
void vmie_mem_close(vmie_mem* m);
/* ---- flat memory view (single owner) ------------------------------------- *
@@ -85,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
View File
@@ -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
+126
View File
@@ -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 */
+54
View File
@@ -83,6 +83,29 @@ typedef struct {
* failure. Free with vmie_win32_close(). */
vmie_win32* vmie_win32_open(const char* ram_path, uint64_t low);
/* As vmie_win32_open, but over an already-open file descriptor `fd` for the RAM
* backing file instead of a path. `fd` is BORROWED: it is dup()'d internally, so
* the context owns its own copy and the caller's `fd` stays open and valid after
* this call AND after vmie_win32_close(). `fd` must reference an mmap-able,
* read-write object (regular file / memfd_create / POSIX shm); its file position
* is not read or changed. Returns a new context (call host_bootstrap() next), or
* NULL on dup/fstat/mmap failure. Free with vmie_win32_close(). */
vmie_win32* vmie_win32_open_fd(int fd, uint64_t low);
/* Open a READ-ONLY context over the RAM backing file `fd`, for a consumer that
* only reads. It takes a caller-supplied `kcr3` (kernel cr3 / System DirectoryTableBase)
* and builds the struct-offset profile read-only from the image (no bootstrap
* handshake - beacon and ACK are not used). The result has the FULL read
* capability: proc_list/proc_modules AND the section / function / import / export
* / callgraph / hook surfaces, gva_read_text, and the generic gva_* and scanner
* surfaces via vmie_win32_mem(). Any write (gva_write) returns -1 because the
* mapping is PROT_READ. `fd` is BORROWED (dup()'d internally; the caller's fd
* stays valid after this call AND after vmie_win32_close()); an O_RDONLY fd is
* accepted. `low` is the below-4G split, as in vmie_win32_open_fd. Returns a new
* context, or NULL if `kcr3` does not resolve a kernel (ntoskrnl not found under
* it) or on dup/fstat/mmap failure. Free with vmie_win32_close(). */
vmie_win32* vmie_win32_open_ro_fd(int fd, uint64_t low, uint64_t kcr3);
/* Unmap, close, and free a context. Safe on NULL. After this, every pointer
* into guest memory obtained through this context is invalid. */
void vmie_win32_close(vmie_win32* v);
@@ -438,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
+154 -18
View File
@@ -52,6 +52,9 @@ int gpa_read(vmie_mem* m, uintptr_t offs, void* buf, const size_t nmemb) {
}
int gpa_write(vmie_mem* m, uintptr_t offs, const void* src, const size_t nmemb) {
if (m->ro) {
return -1;
}
if (out_of_bounds(m, &offs, nmemb)) {
return -1;
}
@@ -88,37 +91,64 @@ static int segs_valid(const gpa_seg* segs, int nseg, uint64_t fsize) {
return 1;
}
/* Finish a vmie_mem over an ALREADY-OWNED fd (from open() or dup()): fstat it,
* validate the seg map against its real size, mmap it shared (PROT_READ when
* `ro`, else PROT_RW), and fill `m`. On ANY failure close `fd` and clean `m`
* (no partial state); on success store the fd in `m->fd`, record `ro`, and
* return 0. The fd's lifetime is now owned by `m` (closed in gpa_close). The
* single mmap/validate site shared by every constructor. */
__attribute__((cold))
int gpa_open_segs(vmie_mem* m, const char* path, const gpa_seg* segs, int nseg) {
static int gpa_map_owned_fd(vmie_mem* m, int fd, const gpa_seg* segs, int nseg, int ro) {
struct stat st;
if ((m->fd = open(path, O_RDWR)) < 0) {
goto ret_;
}
if (fstat(m->fd, &st) || !segs_valid(segs, nseg, (uint64_t)st.st_size)) {
if (fstat(fd, &st) || !segs_valid(segs, nseg, (uint64_t)st.st_size)) {
goto close_;
}
if ((m->pa = mmap(NULL, st.st_size, PROT_RW, MAP_SHARED, m->fd, 0)) == MAP_FAILED) {
const int prot = ro ? PROT_READ : PROT_RW;
if ((m->pa = mmap(NULL, st.st_size, prot, MAP_SHARED, fd, 0)) == MAP_FAILED) {
close_:
close(m->fd);
ret_:
close(fd);
clean_ctx(m);
return -1;
}
m->fd = fd;
m->fsize = st.st_size;
m->nseg = nseg;
m->ro = ro;
memcpy(m->seg, segs, (size_t)nseg * sizeof *segs);
return 0;
}
/* Convenience: the classic single-`low` QEMU map. Below the 4 GiB PCI hole the
* file maps 1:1 ([0,low)->file[0,low)); at and above 4 GiB it resumes at file
* offset low. When low >= fsize the hole is never reached, so one inert identity
* seg covering the whole image suffices. */
/* Build the classic single-`low` QEMU map into out[0..nseg) and return nseg.
* Below the 4 GiB PCI hole the file maps 1:1 ([0,low)->file[0,low)); at and
* above 4 GiB it resumes at file offset low. When low >= fsize the hole is never
* reached, so one inert identity seg covering the whole image suffices (nseg=1).
* The single low-map site shared by gpa_open (fsize from stat) and gpa_from_fd
* (fsize from fstat). */
__attribute__((cold))
static int low_segs(uint64_t fsize, uint64_t low, gpa_seg out[2]) {
if (low >= fsize) {
out[0] = (gpa_seg){ 0, fsize, 0 };
return 1;
}
out[0] = (gpa_seg){ 0, low, 0 };
out[1] = (gpa_seg){ RAM_H, fsize - low, low };
return 2;
}
__attribute__((cold))
int gpa_open_segs(vmie_mem* m, const char* path, const gpa_seg* segs, int nseg) {
const int fd = open(path, O_RDWR);
if (fd < 0) {
clean_ctx(m);
return -1;
}
return gpa_map_owned_fd(m, fd, segs, nseg, 0);
}
__attribute__((cold))
int gpa_open(vmie_mem* m, const char* path, uintptr_t low) {
struct stat st;
@@ -126,16 +156,70 @@ int gpa_open(vmie_mem* m, const char* path, uintptr_t low) {
clean_ctx(m);
return -1;
}
const uint64_t fsize = (uint64_t)st.st_size;
const gpa_seg one[1] = { { 0, fsize, 0 } };
const gpa_seg two[2] = { { 0, low, 0 }, { RAM_H, fsize > low ? fsize - low : 0, low } };
const gpa_seg* segs = low >= fsize ? one : two;
const int nseg = low >= fsize ? 1 : 2;
gpa_seg segs[2];
const int nseg = low_segs((uint64_t)st.st_size, low, segs);
return gpa_open_segs(m, path, segs, nseg);
}
/* fd-backed constructors: build a vmie_mem from an already-open file descriptor
* instead of a path. The fd is BORROWED via dup() - core owns the copy (closed
* in gpa_close); the caller's fd stays valid. The fd must reference an mmap-able
* RW object (regular file / memfd / shm); its file position is not used. */
__attribute__((cold))
int gpa_from_fd_segs(vmie_mem* m, int fd, const gpa_seg* segs, int nseg) {
const int dupfd = dup(fd);
if (dupfd < 0) {
clean_ctx(m);
return -1;
}
return gpa_map_owned_fd(m, dupfd, segs, nseg, 0);
}
__attribute__((cold))
int gpa_from_fd(vmie_mem* m, int fd, uintptr_t low) {
struct stat st;
if (fstat(fd, &st)) {
clean_ctx(m);
return -1;
}
gpa_seg segs[2];
const int nseg = low_segs((uint64_t)st.st_size, low, segs);
return gpa_from_fd_segs(m, fd, segs, nseg);
}
/* read-only twins of gpa_from_fd*: map the borrowed fd PROT_READ and mark the
* source read-only (m->ro), so gpa_write/gva_write return -1. Same dup-borrow
* semantics (core owns the copy, caller's fd stays valid). Accept O_RDONLY as
* well as O_RDWR fds - only read access is required for a PROT_READ mapping.
* Reads/gpa_ptr/scan behave exactly as on an RW source. */
__attribute__((cold))
int gpa_from_ro_fd_segs(vmie_mem* m, int fd, const gpa_seg* segs, int nseg) {
const int dupfd = dup(fd);
if (dupfd < 0) {
clean_ctx(m);
return -1;
}
return gpa_map_owned_fd(m, dupfd, segs, nseg, 1);
}
__attribute__((cold))
int gpa_from_ro_fd(vmie_mem* m, int fd, uintptr_t low) {
struct stat st;
if (fstat(fd, &st)) {
clean_ctx(m);
return -1;
}
gpa_seg segs[2];
const int nseg = low_segs((uint64_t)st.st_size, low, segs);
return gpa_from_ro_fd_segs(m, fd, segs, nseg);
}
__attribute__((cold))
void gpa_close(vmie_mem* m) {
if (m->pa) {
@@ -180,6 +264,58 @@ vmie_mem* vmie_mem_open_segs(const char* path, const gpa_seg* segs, int nseg) {
return m;
}
__attribute__((cold))
vmie_mem* vmie_mem_from_fd(int fd, uint64_t low) {
vmie_mem* m = calloc(1, sizeof *m);
if (!m) {
return NULL;
}
if (gpa_from_fd(m, fd, (uintptr_t)low)) {
free(m);
return NULL;
}
return m;
}
__attribute__((cold))
vmie_mem* vmie_mem_from_fd_segs(int fd, const gpa_seg* segs, int nseg) {
vmie_mem* m = calloc(1, sizeof *m);
if (!m) {
return NULL;
}
if (gpa_from_fd_segs(m, fd, segs, nseg)) {
free(m);
return NULL;
}
return m;
}
__attribute__((cold))
vmie_mem* vmie_mem_from_ro_fd(int fd, uint64_t low) {
vmie_mem* m = calloc(1, sizeof *m);
if (!m) {
return NULL;
}
if (gpa_from_ro_fd(m, fd, (uintptr_t)low)) {
free(m);
return NULL;
}
return m;
}
__attribute__((cold))
vmie_mem* vmie_mem_from_ro_fd_segs(int fd, const gpa_seg* segs, int nseg) {
vmie_mem* m = calloc(1, sizeof *m);
if (!m) {
return NULL;
}
if (gpa_from_ro_fd_segs(m, fd, segs, nseg)) {
free(m);
return NULL;
}
return m;
}
__attribute__((cold))
void vmie_mem_close(vmie_mem* m) {
if (!m) {
+10
View File
@@ -20,6 +20,7 @@ typedef struct vmie_mem {
int fd;
int nseg;
gpa_seg seg[VMIE_MAX_SEGS];
int ro; /* source is read-only (gpa_write/gva_write -> -1); 0 = RW */
} vmie_mem;
/* GPA <-> file-offset converters over the segment map.
@@ -40,6 +41,15 @@ static inline uintptr_t offset_gpa(const vmie_mem* m, uintptr_t off) {
/* guest-physical lifecycle + primitives (gpa.c) */
int gpa_open_segs(vmie_mem* m, const char* path, const gpa_seg* segs, int nseg);
int gpa_open (vmie_mem* m, const char* path, uintptr_t low);
/* fd-backed twins of gpa_open*: build from a borrowed fd (dup'd internally,
* caller's fd stays valid) instead of a path. fd must be an mmap-able RW object. */
int gpa_from_fd_segs(vmie_mem* m, int fd, const gpa_seg* segs, int nseg);
int gpa_from_fd (vmie_mem* m, int fd, uintptr_t low);
/* read-only twins of gpa_from_fd*: map the borrowed fd PROT_READ and mark the
* source read-only (gpa_write/gva_write -> -1). Accept O_RDONLY as well as
* O_RDWR fds (only read access is required). Reads/gpa_ptr/scan behave as RW. */
int gpa_from_ro_fd_segs(vmie_mem* m, int fd, const gpa_seg* segs, int nseg);
int gpa_from_ro_fd (vmie_mem* m, int fd, uintptr_t low);
void gpa_close(vmie_mem* m);
int gpa_read (vmie_mem* m, uintptr_t offs, void* dst, size_t nmemb);
int gpa_write(vmie_mem* m, uintptr_t offs, const void* src, size_t nmemb);
+10 -1
View File
@@ -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;
+65 -22
View File
@@ -6,6 +6,9 @@
#define MZ 0x5A4Du
__attribute__((cold))
static int host_profile_from_cr3(vmie_win32* v, uintptr_t cr3);
/* ---- lifecycle (cold) ---------------------------------------------------- */
__attribute__((cold))
@@ -21,6 +24,40 @@ vmie_win32* vmie_win32_open(const char* ram_path, uint64_t low) {
return v;
}
__attribute__((cold))
vmie_win32* vmie_win32_open_fd(int fd, uint64_t low) {
vmie_win32* v = calloc(1, sizeof *v);
if (!v) {
return NULL;
}
if (gpa_from_fd(&v->mem, fd, low)) {
free(v);
return NULL;
}
return v;
}
/* Read-only consumer context: maps the backing file PROT_READ and builds the
* struct-offset profile from the image under the caller-supplied kcr3 (no
* beacon, no cr3 recovery, no ACK write). All read APIs work, incl.
* proc_list/proc_modules; writes return -1. */
__attribute__((cold))
vmie_win32* vmie_win32_open_ro_fd(int fd, uint64_t low, uint64_t kcr3) {
vmie_win32* v = calloc(1, sizeof *v);
if (!v) {
return NULL;
}
if (gpa_from_ro_fd(&v->mem, fd, low)) {
free(v);
return NULL;
}
if (host_profile_from_cr3(v, (uintptr_t)kcr3)) {
vmie_win32_close(v);
return NULL;
}
return v;
}
__attribute__((cold))
void vmie_win32_close(vmie_win32* v) {
if (!v) {
@@ -153,6 +190,31 @@ static uint32_t ko_export_rva(vmie_mem* m, uintptr_t cr3, uint64_t kbase, const
return 0;
}
__attribute__((cold))
static int host_profile_from_cr3(vmie_win32* v, uintptr_t cr3) {
vmie_mem* m = &v->mem;
uint8_t guid[16];
uint32_t age, rva;
uint64_t sys_ep, dtb;
if (find_ntoskrnl(m, cr3, &v->kbase, guid, &age)) {
return -3;
}
rva = ko_export_rva(m, cr3, v->kbase, "PsInitialSystemProcess");
if (!rva || gva_read(m, cr3, v->kbase + rva, &sys_ep, 8)) {
return -4;
}
if (profile_build(v, cr3, sys_ep, guid, age)) {
return -5;
}
if (gva_read(m, cr3, sys_ep + v->prof.ep_dtb, &dtb, 8)) {
return -6;
}
v->kcr3 = dtb & PFN_MASK;
v->sysproc = sys_ep;
return 0;
}
static void beacon_ack(vmie_mem* m, uint64_t anchor_pa) {
uint64_t ack = CONTRACT_MAGIC_R;
gpa_write(m, anchor_pa + offsetof(contract, ack), &ack, 8);
@@ -167,10 +229,6 @@ int host_bootstrap(vmie_win32* v) {
vmie_mem* m = &v->mem;
uint64_t anchor_pa, va_self;
uintptr_t cr3boot;
uint32_t rva;
uint8_t guid[16];
uint32_t age;
uint64_t sys_ep;
if (beacon_find(m, &anchor_pa, &va_self)) {
return -1;
@@ -180,26 +238,11 @@ int host_bootstrap(vmie_win32* v) {
return -2;
}
if (find_ntoskrnl(m, cr3boot, &v->kbase, guid, &age)) {
return -3;
const int rc = host_profile_from_cr3(v, cr3boot);
if (rc) {
return rc;
}
rva = ko_export_rva(m, cr3boot, v->kbase, "PsInitialSystemProcess");
if (!rva || gva_read(m, cr3boot, v->kbase + rva, &sys_ep, 8)) {
return -4;
}
if (profile_build(v, cr3boot, sys_ep, guid, age)) {
return -5;
}
uint64_t dtb;
if (gva_read(m, cr3boot, sys_ep + v->prof.ep_dtb, &dtb, 8)) {
return -6;
}
v->kcr3 = dtb & PFN_MASK;
v->sysproc = sys_ep;
beacon_ack(m, anchor_pa);
return 0;
}
+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;
}
+2 -2
View File
@@ -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;
+181
View File
@@ -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;
}
+93
View File
@@ -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 */
+189
View File
@@ -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";
}
+185
View File
@@ -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";
}
+26
View File
@@ -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";
}