diff --git a/.gitignore b/.gitignore index 7d1fe9a..0743a95 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ __pycache__/ # fetched at runtime by tools/stormlib.py tools/libstorm.so tools/lib/ + +# zig package cache +zig-pkg/ diff --git a/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/build.zig b/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/build.zig deleted file mode 100644 index ea7821c..0000000 --- a/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/build.zig +++ /dev/null @@ -1,21 +0,0 @@ -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - // x86 length disassembler — standalone, no dependencies - const x86dis_mod = b.addModule("x86dis", .{ - .root_source_file = b.path("src/x86dis.zig"), - .target = target, - .optimize = optimize, - }); - - // zhook — x86-32 inline hooking with auto-sizing trampolines - const zhook_mod = b.addModule("zhook", .{ - .root_source_file = b.path("src/zhook.zig"), - .target = target, - .optimize = optimize, - }); - zhook_mod.addImport("x86dis", x86dis_mod); -} diff --git a/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/build.zig.zon b/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/build.zig.zon deleted file mode 100644 index ab3a97a..0000000 --- a/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/build.zig.zon +++ /dev/null @@ -1,10 +0,0 @@ -.{ - .name = .zhook, - .version = "0.1.0", - .fingerprint = 0xf05c933a601259a4, - .paths = .{ - "build.zig", - "build.zig.zon", - "src", - }, -} diff --git a/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/src/x86dis.zig b/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/src/x86dis.zig deleted file mode 100644 index 9522720..0000000 --- a/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/src/x86dis.zig +++ /dev/null @@ -1,432 +0,0 @@ -//! Minimal x86 (32-bit) length disassembler. -//! -//! Faithful port of Vyacheslav Patkov's Hacker Disassembler Engine 32 (HDE32) -//! Only computes instruction length + flags needed for relocation (F_RELATIVE). -//! ~470 bytes of table data, compiles to ~1-2 KB. -//! -//! Standalone module with no OS dependencies — pure computation on byte slices. -//! Can be used independently of the hook library for any x86 disassembly task. - -const std = @import("std"); - -// ── public flags ─────────────────────────────────────────────────────── - -/// Instruction has a ModR/M byte. -pub const F_MODRM: u32 = 0x00000001; -/// Instruction has a SIB (Scale-Index-Base) byte. -pub const F_SIB: u32 = 0x00000002; -/// Instruction has an 8-bit immediate operand. -pub const F_IMM8: u32 = 0x00000004; -/// Instruction has a 16-bit immediate operand. -pub const F_IMM16: u32 = 0x00000008; -/// Instruction has a 32-bit immediate operand. -pub const F_IMM32: u32 = 0x00000010; -/// Instruction has an 8-bit displacement. -pub const F_DISP8: u32 = 0x00000020; -/// Instruction has a 16-bit displacement. -pub const F_DISP16: u32 = 0x00000040; -/// Instruction has a 32-bit displacement. -pub const F_DISP32: u32 = 0x00000080; -/// Instruction contains a relative offset (CALL/JMP/Jcc). Indicates the -/// immediate is PC-relative and must be relocated if the instruction is moved. -pub const F_RELATIVE: u32 = 0x00000100; -/// Decoding failed — the byte sequence is not a valid x86 instruction. -pub const F_ERROR: u32 = 0x00001000; - -// ── internal cflags ──────────────────────────────────────────────────── -const C_MODRM: u8 = 0x01; -const C_IMM8: u8 = 0x02; -const C_IMM16: u8 = 0x04; -const C_IMM_P66: u8 = 0x10; -const C_REL8: u8 = 0x20; -const C_REL32: u8 = 0x40; -const C_GROUP: u8 = 0x80; -const C_ERROR: u8 = 0xff; - -const PRE_NONE: u8 = 0x01; -const PRE_66: u8 = 0x08; -const PRE_67: u8 = 0x10; - -const DELTA_OPCODES: usize = 0x4a; - -// HDE32 opcode table — verbatim from table32.h -const hde32_table = [_]u8{ - 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, - 0xa8, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xac, 0xaa, 0xb2, 0xaa, 0x9f, 0x9f, - 0x9f, 0x9f, 0xb5, 0xa3, 0xa3, 0xa4, 0xaa, 0xaa, 0xba, 0xaa, 0x96, 0xaa, 0xa8, 0xaa, 0xc3, - 0xc3, 0x96, 0x96, 0xb7, 0xae, 0xd6, 0xbd, 0xa3, 0xc5, 0xa3, 0xa3, 0x9f, 0xc3, 0x9c, 0xaa, - 0xaa, 0xac, 0xaa, 0xbf, 0x03, 0x7f, 0x11, 0x7f, 0x01, 0x7f, 0x01, 0x3f, 0x01, 0x01, 0x90, - 0x82, 0x7d, 0x97, 0x59, 0x59, 0x59, 0x59, 0x59, 0x7f, 0x59, 0x59, 0x60, 0x7d, 0x7f, 0x7f, - 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x9a, 0x88, 0x7d, - 0x59, 0x50, 0x50, 0x50, 0x50, 0x59, 0x59, 0x59, 0x59, 0x61, 0x94, 0x61, 0x9e, 0x59, 0x59, - 0x85, 0x59, 0x92, 0xa3, 0x60, 0x60, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, - 0x59, 0x59, 0x9f, 0x01, 0x03, 0x01, 0x04, 0x03, 0xd5, 0x03, 0xcc, 0x01, 0xbc, 0x03, 0xf0, - 0x10, 0x10, 0x10, 0x10, 0x50, 0x50, 0x50, 0x50, 0x14, 0x20, 0x20, 0x20, 0x20, 0x01, 0x01, - 0x01, 0x01, 0xc4, 0x02, 0x10, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0xc0, 0xc2, 0x10, 0x11, - 0x02, 0x03, 0x11, 0x03, 0x03, 0x04, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, 0x00, 0xc6, 0xc8, - 0x02, 0x02, 0x02, 0x02, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0xca, - 0x01, 0x01, 0x01, 0x00, 0x06, 0x00, 0x04, 0x00, 0xc0, 0xc2, 0x01, 0x01, 0x03, 0x01, 0xff, - 0xff, 0x01, 0x00, 0x03, 0xc4, 0xc4, 0xc6, 0x03, 0x01, 0x01, 0x01, 0xff, 0x03, 0x03, 0x03, - 0xc8, 0x40, 0x00, 0x0a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x33, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xbf, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, - 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x7f, 0x00, 0x00, 0xff, 0x4a, 0x4a, 0x4a, 0x4a, 0x4b, 0x52, 0x4a, 0x4a, 0x4a, 0x4a, 0x4f, - 0x4c, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x55, 0x45, 0x40, 0x4a, 0x4a, 0x4a, - 0x45, 0x59, 0x4d, 0x46, 0x4a, 0x5d, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, - 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x61, 0x63, 0x67, 0x4e, 0x4a, 0x4a, 0x6b, 0x6d, 0x4a, 0x4a, - 0x45, 0x6d, 0x4a, 0x4a, 0x44, 0x45, 0x4a, 0x4a, 0x00, 0x00, 0x00, 0x02, 0x0d, 0x06, 0x06, - 0x06, 0x06, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06, 0x06, 0x00, 0x06, 0x06, 0x02, 0x06, - 0x00, 0x0a, 0x0a, 0x07, 0x07, 0x06, 0x02, 0x05, 0x05, 0x02, 0x02, 0x00, 0x00, 0x04, 0x04, - 0x04, 0x04, 0x00, 0x00, 0x00, 0x0e, 0x05, 0x06, 0x06, 0x06, 0x01, 0x06, 0x00, 0x00, 0x08, - 0x00, 0x10, 0x00, 0x18, 0x00, 0x20, 0x00, 0x28, 0x00, 0x30, 0x00, 0x80, 0x01, 0x82, 0x01, - 0x86, 0x00, 0xf6, 0xcf, 0xfe, 0x3f, 0xab, 0x00, 0xb0, 0x00, 0xb1, 0x00, 0xb3, 0x00, 0xba, - 0xf8, 0xbb, 0x00, 0xc0, 0x00, 0xc1, 0x00, 0xc7, 0xbf, 0x62, 0xff, 0x00, 0x8d, 0xff, 0x00, - 0xc4, 0xff, 0x00, 0xc5, 0xff, 0x00, -}; - -/// Decoded instruction descriptor. Contains the length and metadata flags -/// needed for instruction relocation — no operand values are extracted. -pub const Insn = struct { - /// Total instruction length in bytes (including prefixes, opcode, ModRM, SIB, disp, imm). - len: u8, - /// Bitmask of `F_*` flags describing the instruction's encoding. - flags: u32, - /// Primary opcode byte (after any prefixes). For two-byte opcodes (0F xx), this is 0x0F. - opcode: u8, - /// Secondary opcode byte for two-byte instructions (the byte after 0F). Zero for single-byte opcodes. - opcode2: u8, -}; - -/// Decode the instruction at `code`, returning its length and flags. -/// -/// Handles all x86-32 prefixes, one- and two-byte opcodes, ModRM/SIB, -/// displacement, and immediate fields. Sets `F_RELATIVE` on instructions -/// with PC-relative operands (E8 CALL, E9 JMP, 0F 8x Jcc, short jumps). -/// -/// Does not read beyond the instruction boundary — safe to use on -/// pointers to live code without over-reading into adjacent instructions. -/// Returns `F_ERROR` in flags if the byte sequence is invalid; `len` is -/// clamped to 15 (the x86 maximum instruction length). -pub fn decode(code: [*]const u8) Insn { - var result = Insn{ .len = 0, .flags = 0, .opcode = 0, .opcode2 = 0 }; - - var p: usize = 0; - var pref: u8 = 0; - var disp_size: u8 = 0; - - // ── prefixes ── - var prefix_count: u8 = 16; - prefix_loop: while (prefix_count > 0) : (prefix_count -= 1) { - switch (code[p]) { - 0xf3, 0xf2 => pref |= if (code[p] == 0xf3) 0x04 else 0x02, - 0xf0 => pref |= 0x20, // PRE_LOCK - 0x26, 0x2e, 0x36, 0x3e, 0x64, 0x65 => pref |= 0x40, // PRE_SEG - 0x66 => pref |= PRE_66, - 0x67 => pref |= PRE_67, - else => break :prefix_loop, - } - p += 1; - } - - result.flags = @as(u32, pref) << 23; - - if (pref == 0) pref |= PRE_NONE; - - // ── opcode ── - var ht_base: usize = 0; - var c = code[p]; - p += 1; - result.opcode = c; - - if (c == 0x0f) { - // two-byte opcode - result.opcode2 = code[p]; - c = code[p]; - p += 1; - ht_base = DELTA_OPCODES; - } else if (c >= 0xa0 and c <= 0xa3) { - // MOV moffs — address-size prefix swaps operand-size behavior - if (pref & PRE_67 != 0) - pref |= PRE_66 - else - pref &= ~PRE_66; - } - - const opcode = c; - - // ── two-level table lookup: ht[ht[opcode/4] + (opcode%4)] ── - var cflags: u8 = blk: { - const idx1 = ht_base + @as(usize, opcode / 4); - if (idx1 >= hde32_table.len) break :blk C_ERROR; - const idx2 = ht_base + @as(usize, hde32_table[idx1]) + @as(usize, opcode % 4); - if (idx2 >= hde32_table.len) break :blk C_ERROR; - break :blk hde32_table[idx2]; - }; - - if (cflags == C_ERROR) { - result.flags |= F_ERROR; - cflags = 0; - if ((opcode & 0xfd) == 0x24) // (opcode & -3) == 0x24 - cflags +%= 1; - } - - // ── group resolution ── - var x: u8 = 0; - if (cflags & C_GROUP != 0) { - const group_idx = ht_base + @as(usize, cflags & 0x7f); - if (group_idx + 1 < hde32_table.len) { - const t = std.mem.readInt(u16, hde32_table[group_idx..][0..2], .little); - cflags = @truncate(t); - x = @truncate(t >> 8); - } - } - - // ── modrm ── - if (cflags & C_MODRM != 0) { - result.flags |= F_MODRM; - const modrm = code[p]; - p += 1; - const m_mod = modrm >> 6; - const m_rm: u8 = modrm & 7; - const m_reg: u3 = @truncate((modrm & 0x3f) >> 3); - - // F6 TEST imm8 / F7 TEST imm16/32 - if (m_reg <= 1) { - if (opcode == 0xf6) - cflags |= C_IMM8; - if (opcode == 0xf7) - cflags |= C_IMM_P66; - } - - // displacement - switch (m_mod) { - 0 => { - if (pref & PRE_67 != 0) { - if (m_rm == 6) disp_size = 2; - } else { - if (m_rm == 5) disp_size = 4; - } - }, - 1 => disp_size = 1, - 2 => { - disp_size = 2; - if (pref & PRE_67 == 0) - disp_size = 4; - }, - else => {}, - } - - // SIB byte - if (m_mod != 3 and m_rm == 4 and (pref & PRE_67 == 0)) { - result.flags |= F_SIB; - const sib = code[p]; - p += 1; - if ((sib & 7) == 5 and (m_mod & 1) == 0) - disp_size = 4; - } - - // displacement bytes - switch (disp_size) { - 1 => { - result.flags |= F_DISP8; - p += 1; - }, - 2 => { - result.flags |= F_DISP16; - p += 2; - }, - 4 => { - result.flags |= F_DISP32; - p += 4; - }, - else => {}, - } - } - - // ── immediates ── - if (cflags & C_IMM_P66 != 0) { - if (cflags & C_REL32 != 0) { - if (pref & PRE_66 != 0) { - result.flags |= F_IMM16 | F_RELATIVE; - p += 2; - // disasm_done — skip remaining immediate checks - result.len = @intCast(p); - if (result.len > 15) { - result.flags |= F_ERROR; - result.len = 15; - } - return result; - } - // fall through to rel32_ok below - } else { - if (pref & PRE_66 != 0) { - result.flags |= F_IMM16; - p += 2; - } else { - result.flags |= F_IMM32; - p += 4; - } - } - } - - if (cflags & C_IMM16 != 0) { - if (result.flags & F_IMM32 != 0) { - result.flags |= F_IMM16; - } else if (result.flags & F_IMM16 != 0) { - // F_2IMM16 - } else { - result.flags |= F_IMM16; - } - p += 2; - } - if (cflags & C_IMM8 != 0) { - result.flags |= F_IMM8; - p += 1; - } - - if (cflags & C_REL32 != 0) { - result.flags |= F_IMM32 | F_RELATIVE; - p += 4; - } else if (cflags & C_REL8 != 0) { - result.flags |= F_IMM8 | F_RELATIVE; - p += 1; - } - - result.len = @intCast(p); - if (result.len > 15) { - result.flags |= F_ERROR; - result.len = 15; - } - return result; -} - -// ── tests ────────────────────────────────────────────────────────────── - -test "push ebp" { - const d = decode(&[_]u8{ 0x55, 0xCC }); - try std.testing.expectEqual(@as(u8, 1), d.len); -} - -test "mov ebp, esp" { - // 8B EC (or 89 E5) - const d = decode(&[_]u8{ 0x8B, 0xEC }); - try std.testing.expectEqual(@as(u8, 2), d.len); - try std.testing.expect(d.flags & F_MODRM != 0); -} - -test "call rel32" { - const d = decode(&[_]u8{ 0xE8, 0x78, 0x56, 0x34, 0x12 }); - try std.testing.expectEqual(@as(u8, 5), d.len); - try std.testing.expect(d.flags & F_RELATIVE != 0); - try std.testing.expect(d.flags & F_IMM32 != 0); -} - -test "jmp rel32" { - const d = decode(&[_]u8{ 0xE9, 0x00, 0x00, 0x00, 0x00 }); - try std.testing.expectEqual(@as(u8, 5), d.len); - try std.testing.expect(d.flags & F_RELATIVE != 0); -} - -test "sub esp, imm8" { - // 83 EC 10 - const d = decode(&[_]u8{ 0x83, 0xEC, 0x10 }); - try std.testing.expectEqual(@as(u8, 3), d.len); - try std.testing.expect(d.flags & F_MODRM != 0); - try std.testing.expect(d.flags & F_IMM8 != 0); -} - -test "mov eax, [ebp+8]" { - // 8B 45 08 - const d = decode(&[_]u8{ 0x8B, 0x45, 0x08 }); - try std.testing.expectEqual(@as(u8, 3), d.len); - try std.testing.expect(d.flags & F_MODRM != 0); - try std.testing.expect(d.flags & F_DISP8 != 0); -} - -test "jz rel32 (0F 84)" { - const d = decode(&[_]u8{ 0x0F, 0x84, 0x10, 0x00, 0x00, 0x00 }); - try std.testing.expectEqual(@as(u8, 6), d.len); - try std.testing.expect(d.flags & F_RELATIVE != 0); -} - -test "nop" { - const d = decode(&[_]u8{0x90}); - try std.testing.expectEqual(@as(u8, 1), d.len); -} - -test "ret" { - const d = decode(&[_]u8{0xC3}); - try std.testing.expectEqual(@as(u8, 1), d.len); -} - -test "short jmp EB" { - const d = decode(&[_]u8{ 0xEB, 0x05 }); - try std.testing.expectEqual(@as(u8, 2), d.len); - try std.testing.expect(d.flags & F_RELATIVE != 0); - try std.testing.expect(d.flags & F_IMM8 != 0); -} - -test "short jcc 74 (jz rel8)" { - const d = decode(&[_]u8{ 0x74, 0x0A }); - try std.testing.expectEqual(@as(u8, 2), d.len); - try std.testing.expect(d.flags & F_RELATIVE != 0); -} - -test "mov eax, imm32" { - const d = decode(&[_]u8{ 0xB8, 0x44, 0x33, 0x22, 0x11 }); - try std.testing.expectEqual(@as(u8, 5), d.len); -} - -test "push imm32" { - const d = decode(&[_]u8{ 0x68, 0x44, 0x33, 0x22, 0x11 }); - try std.testing.expectEqual(@as(u8, 5), d.len); -} - -test "push imm8" { - // 6A 01 - const d = decode(&[_]u8{ 0x6A, 0x01 }); - try std.testing.expectEqual(@as(u8, 2), d.len); -} - -test "mov [ebp-4], eax" { - // 89 45 FC - const d = decode(&[_]u8{ 0x89, 0x45, 0xFC }); - try std.testing.expectEqual(@as(u8, 3), d.len); - try std.testing.expect(d.flags & F_MODRM != 0); - try std.testing.expect(d.flags & F_DISP8 != 0); -} - -test "lea eax, [ecx+edx*4+8]" { - // 8D 44 91 08 - const d = decode(&[_]u8{ 0x8D, 0x44, 0x91, 0x08 }); - try std.testing.expectEqual(@as(u8, 4), d.len); - try std.testing.expect(d.flags & F_MODRM != 0); - try std.testing.expect(d.flags & F_SIB != 0); - try std.testing.expect(d.flags & F_DISP8 != 0); -} - -test "mov [disp32], eax" { - // A3 xx xx xx xx - const d = decode(&[_]u8{ 0xA3, 0x00, 0x10, 0x40, 0x00 }); - try std.testing.expectEqual(@as(u8, 5), d.len); -} - -test "sub esp, imm32" { - // 81 EC 00 01 00 00 - const d = decode(&[_]u8{ 0x81, 0xEC, 0x00, 0x01, 0x00, 0x00 }); - try std.testing.expectEqual(@as(u8, 6), d.len); - try std.testing.expect(d.flags & F_MODRM != 0); -} - -test "test eax, imm32 (F7 C0)" { - // F7 C0 FF 00 00 00 = test eax, 0xFF - const d = decode(&[_]u8{ 0xF7, 0xC0, 0xFF, 0x00, 0x00, 0x00 }); - try std.testing.expectEqual(@as(u8, 6), d.len); -} - -test "ret imm16" { - // C2 04 00 - const d = decode(&[_]u8{ 0xC2, 0x04, 0x00 }); - try std.testing.expectEqual(@as(u8, 3), d.len); -} diff --git a/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/src/zhook.zig b/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/src/zhook.zig deleted file mode 100644 index cadb828..0000000 --- a/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/src/zhook.zig +++ /dev/null @@ -1,415 +0,0 @@ -//! x86-32 inline hooking library for Windows DLL injection. -//! -//! Provides auto-sizing detours with instruction relocation, type-safe -//! `Detour(FnType)` wrappers, memory read/write helpers, rel32 arithmetic, -//! a generic `fastcall` caller, and Windows virtual memory API re-exports. -//! -//! ## Type-safe API (Detour) — recommended -//! -//! ```zig -//! const fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} }; -//! const CheckFile = fn (u32, u32, u32) callconv(fc) u32; -//! var hook: Detour(CheckFile) = .{}; -//! hook.attach(0x654DD0, &myDetour); -//! // Inside detour: hook.callOriginal(.{ filename, flags, output }); -//! hook.detach(); -//! ``` -//! -//! ## Low-level API (GenericHook) -//! -//! ```zig -//! var my_hook: GenericHook = .{}; -//! if (my_hook.install(0x401000, @intFromPtr(&myDetour)) == .ok) { -//! const orig = my_hook.getTrampoline(OrigFnType); -//! _ = orig(); -//! } -//! my_hook.remove(); -//! ``` - -const std = @import("std"); -const x86dis = @import("x86dis"); - -// ============================================================================= -// Windows API -// ============================================================================= - -const WINAPI = std.builtin.CallingConvention.winapi; - -/// Memory protection constant: page is readable, writable, and executable. -pub const PAGE_EXECUTE_READWRITE: u32 = 0x40; -/// Memory allocation type: commit physical storage for the region. -pub const MEM_COMMIT: u32 = 0x1000; -/// Memory free type: release the region (decommit + free address space). -pub const MEM_RELEASE: u32 = 0x8000; - -extern "kernel32" fn VirtualProtect( - lpAddress: *anyopaque, - dwSize: usize, - flNewProtect: u32, - lpflOldProtect: *u32, -) callconv(WINAPI) i32; - -/// Allocate or reserve virtual memory. Used to create RWX trampoline pages. -pub extern "kernel32" fn VirtualAlloc( - lpAddress: ?*anyopaque, - dwSize: usize, - flAllocationType: u32, - flProtect: u32, -) callconv(WINAPI) ?[*]u8; - -/// Release virtual memory previously allocated with `VirtualAlloc`. -pub extern "kernel32" fn VirtualFree( - lpAddress: *anyopaque, - dwSize: usize, - dwFreeType: u32, -) callconv(WINAPI) i32; - -// ============================================================================= -// Memory helpers -// ============================================================================= - -/// Read a value of type `T` from an arbitrary memory address (unaligned). -pub fn readMem(comptime T: type, addr: usize) T { - return @as(*align(1) const T, @ptrFromInt(addr)).*; -} - -/// Write raw bytes to an arbitrary memory address. No protection change — -/// caller must ensure the page is writable (or use `writeProtected`). -pub fn writeMem(addr: usize, bytes: []const u8) void { - const dest: [*]u8 = @ptrFromInt(addr); - for (bytes, 0..) |b, i| { - dest[i] = b; - } -} - -/// Write bytes to a potentially read-only/executable page. Temporarily sets -/// PAGE_EXECUTE_READWRITE, writes, then restores the original protection. -pub fn writeProtected(addr: usize, bytes: []const u8) void { - var old: u32 = 0; - _ = VirtualProtect(@ptrFromInt(addr), bytes.len, PAGE_EXECUTE_READWRITE, &old); - writeMem(addr, bytes); - _ = VirtualProtect(@ptrFromInt(addr), bytes.len, old, &old); -} - -// ============================================================================= -// Rel32 helpers -// ============================================================================= - -/// Resolve the absolute target of an E8 (CALL) or E9 (JMP) at `addr`. -/// Reads the signed rel32 operand at addr+1 and computes addr+5+offset. -pub fn rel32Target(addr: usize) usize { - const offset: u32 = @bitCast(@as(*align(1) const i32, @ptrFromInt(addr + 1)).*); - return (addr + 5) +% offset; -} - -/// Write a rel32 displacement into dest[0..4] such that a JMP/CALL -/// from address `from` reaches `to`. Displacement = to - (from + 4). -pub fn writeRel32(dest: [*]u8, from: usize, to: usize) void { - std.mem.writeInt(u32, dest[0..4], to -% (from + 4), .little); -} - -// ============================================================================= -// Calling convention aliases and call helper -// ============================================================================= - -/// x86-32 calling convention shorthands for use with `callconv()`. -/// -/// Once Zig supports `@Type` for function types, these can be used to build -/// convention-specific wrappers (stdcall/thiscall/fastcall/cdecl) that inject -/// the callconv automatically, removing the need for `callconv(hook.cc.*)`. -/// -/// ```zig -/// const result = hook.call(fn (u32, u32) callconv(hook.cc.thiscall) i32, 0x6061E0, .{ player, unit }); -/// ``` -pub const cc = struct { - pub const stdcall: std.builtin.CallingConvention = .{ .x86_stdcall = .{} }; - pub const thiscall: std.builtin.CallingConvention = .{ .x86_thiscall = .{} }; - pub const fastcall: std.builtin.CallingConvention = .{ .x86_fastcall = .{} }; - pub const cdecl: std.builtin.CallingConvention = .c; -}; - -/// Call a function at `addr` using a typed function pointer cast. -/// -/// The function type `F` must include an explicit `callconv`. Supports all -/// x86-32 conventions (stdcall, thiscall, fastcall, cdecl). Uses `.never_tail` -/// to prevent tail-call optimization that would corrupt callee-cleanup stacks. -/// -/// ```zig -/// const result = hook.call(fn (u32, u32) callconv(hook.cc.stdcall) u32, 0x464870, .{ lo, hi }); -/// ``` -pub fn call(comptime F: type, addr: usize, args: anytype) FnReturnType(F) { - const func: *const F = @ptrFromInt(addr); - return @call(.never_tail, func, args); -} - -fn FnReturnType(comptime F: type) type { - return @typeInfo(F).@"fn".return_type orelse void; -} - -// ═══════════════════════════════════════════════════════════════════════ -// GenericHook — low-level auto-sizing hook -// ═══════════════════════════════════════════════════════════════════════ - -const JMP_SIZE: usize = 5; // E9 + rel32 -const MAX_STOLEN: usize = 32; -const ALLOC_SIZE: usize = 64; // trampoline only - -/// Auto-sizing inline hook that uses the x86 length disassembler to determine -/// how many prologue bytes to steal, then copies and relocates them into a -/// trampoline. Handles hook chaining (detects an existing E9 JMP at the target) -/// and expands short jumps to near jumps during relocation. -/// -/// For type-safe hooking with automatic calling convention handling, use -/// `Detour(FnType)` instead — it wraps `GenericHook` and adds compile-time -/// type checking. -pub const GenericHook = struct { - /// Base of the allocated RWX page, or null if not yet prepared. - mem: ?[*]u8 = null, - /// Address of the trampoline entry point (calls the original code). - trampoline: usize = 0, - /// Address of the hooked function's entry point. - target: usize = 0, - /// Number of bytes stolen from the target prologue (>= 5 for E9 JMP). - stolen_size: usize = 0, - /// Original prologue bytes, saved for restoration on `remove()`. - saved_bytes: [MAX_STOLEN]u8 = undefined, - - /// Result of a hook operation. - pub const Error = enum { - /// Success. - ok, - /// `VirtualAlloc` failed to allocate executable memory. - alloc_failed, - /// The length disassembler returned `F_ERROR` or a zero-length instruction. - disasm_error, - /// Could not steal enough bytes for a 5-byte JMP before hitting `MAX_STOLEN`. - prologue_too_short, - /// Prologue contains a relative instruction that cannot be relocated - /// (e.g. LOOP, JECXZ — only CALL/JMP/Jcc are supported). - unsupported_relocation, - }; - - /// Convenience: `prepare` + `activate` in one call. - pub fn install(self: *GenericHook, target: usize, detour_addr: usize) Error { - const err = self.prepare(target); - if (err != .ok) return err; - self.activate(detour_addr); - return .ok; - } - - /// Phase 1: disassemble prologue, allocate trampoline, copy + relocate. - /// - /// Walks the target's prologue with `x86dis.decode` until at least 5 bytes - /// are covered, allocates an RWX page, copies the stolen bytes into a - /// trampoline, relocates any relative instructions (E8/E9/Jcc/short jumps), - /// and appends a JMP back to the remainder of the original function. - /// - /// If the target already starts with an E9 JMP (another hook), chains - /// through it: the trampoline jumps to the existing detour rather than - /// copying the prologue. - /// - /// Does NOT patch the target — call `activate()` after to write the JMP. - pub fn prepare(self: *GenericHook, target: usize) Error { - if (self.mem != null) return .ok; - - const src: [*]const u8 = @ptrFromInt(target); - - // ── determine how many bytes to steal ── - var stolen: usize = 0; - while (stolen < JMP_SIZE) { - const insn = x86dis.decode(src + stolen); - if (insn.flags & x86dis.F_ERROR != 0) return .disasm_error; - if (insn.len == 0) return .disasm_error; - stolen += insn.len; - if (stolen > MAX_STOLEN) return .prologue_too_short; - } - - // ── allocate ── - const mem = VirtualAlloc(null, ALLOC_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE) orelse return .alloc_failed; - - self.mem = mem; - self.target = target; - self.stolen_size = stolen; - self.trampoline = @intFromPtr(mem); - - @memcpy(self.saved_bytes[0..stolen], src[0..stolen]); - - // ── check if already hooked (E9 at target) — chain through ── - if (src[0] == 0xE9) { - const other_detour = rel32Target(target); - mem[0] = 0xE9; - writeRel32(mem + 1, self.trampoline + 1, other_detour); - return .ok; - } - - // ── build trampoline: copy + relocate ── - var t_pos: usize = 0; - var s_pos: usize = 0; - - while (s_pos < stolen) { - const insn = x86dis.decode(src + s_pos); - const op = insn.opcode; - const src_addr = target + s_pos; - const dst_addr = self.trampoline + t_pos; - - if (insn.flags & x86dis.F_RELATIVE != 0) { - if (op == 0xE8 or op == 0xE9) { - // CALL/JMP rel32 - const abs = rel32Target(src_addr); - mem[t_pos] = op; - writeRel32(mem + t_pos + 1, dst_addr + 1, abs); - t_pos += 5; - } else if (op == 0x0F and insn.opcode2 >= 0x80 and insn.opcode2 <= 0x8F) { - // Jcc rel32 (0F 80-8F) - const abs = jcc32Target(src_addr); - mem[t_pos] = 0x0F; - mem[t_pos + 1] = insn.opcode2; - writeRel32(mem + t_pos + 2, dst_addr + 2, abs); - t_pos += 6; - } else if (op >= 0x70 and op <= 0x7F) { - // Short Jcc → expand to near Jcc (0F 8x) - const offset = @as(i8, @bitCast(src[s_pos + 1])); - const abs: usize = @bitCast(@as(isize, @intCast(src_addr + 2)) + offset); - mem[t_pos] = 0x0F; - mem[t_pos + 1] = op + 0x10; - writeRel32(mem + t_pos + 2, dst_addr + 2, abs); - t_pos += 6; - } else if (op == 0xEB) { - // Short JMP → expand to near JMP (E9) - const offset = @as(i8, @bitCast(src[s_pos + 1])); - const abs: usize = @bitCast(@as(isize, @intCast(src_addr + 2)) + offset); - mem[t_pos] = 0xE9; - writeRel32(mem + t_pos + 1, dst_addr + 1, abs); - t_pos += 5; - } else { - // LOOP/JECXZ or unknown — cannot trivially expand - self.cleanup(); - return .unsupported_relocation; - } - } else { - // Non-relative — copy verbatim - @memcpy(mem[t_pos .. t_pos + insn.len], src[s_pos .. s_pos + insn.len]); - t_pos += insn.len; - } - s_pos += insn.len; - } - - // ── JMP back to original code after stolen bytes ── - mem[t_pos] = 0xE9; - writeRel32( - mem + t_pos + 1, - self.trampoline + t_pos + 1, - target + stolen, - ); - - return .ok; - } - - /// Phase 2: write the E9 JMP patch at the target. - /// Any excess stolen bytes beyond the 5-byte JMP are filled with NOPs. - pub fn activate(self: *GenericHook, detour_addr: usize) void { - var patch: [MAX_STOLEN]u8 = .{0x90} ** MAX_STOLEN; - patch[0] = 0xE9; - writeRel32(patch[1..5], self.target + 1, detour_addr); - writeProtected(self.target, patch[0..self.stolen_size]); - } - - /// Restore original bytes and free trampoline memory. - pub fn remove(self: *GenericHook) void { - if (self.mem == null) return; - writeProtected(self.target, self.saved_bytes[0..self.stolen_size]); - _ = VirtualFree(@ptrFromInt(@intFromPtr(self.mem.?)), 0, MEM_RELEASE); - self.mem = null; - } - - /// Get trampoline as a typed function pointer. - pub fn getTrampoline(self: *const GenericHook, comptime T: type) T { - return @ptrFromInt(self.trampoline); - } - - fn cleanup(self: *GenericHook) void { - if (self.mem) |m| { - _ = VirtualFree(@ptrFromInt(@intFromPtr(m)), 0, MEM_RELEASE); - self.mem = null; - } - } -}; - -// ═══════════════════════════════════════════════════════════════════════ -// Detour(FnType) — HadesMem-style type-safe generic hook -// ═══════════════════════════════════════════════════════════════════════ - -/// Comptime-generic typed detour — the recommended primary API for hooking. -/// -/// Declare the target function's type once; get type-checked `attach`, -/// `callOriginal`, and `detach` with no manual pointer casts. Wraps -/// `GenericHook` for auto-sizing and relocation. -/// -/// Supports all x86-32 calling conventions: `stdcall`, `cdecl`, `thiscall`, -/// and `fastcall` (requires patched Zig 0.16 with `inreg` fix for correct -/// x86 fastcall codegen). Uses `.never_tail` internally to prevent tail-call -/// optimization that would corrupt the stack with callee-cleanup conventions. -/// -/// ```zig -/// const fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} }; -/// const CheckFile = fn (u32, u32, u32) callconv(fc) u32; -/// var hook: Detour(CheckFile) = .{}; -/// _ = hook.attach(0x654DD0, &myDetour); -/// // in detour: hook.callOriginal(.{ filename, flags, output }); -/// hook.detach(); -/// ``` -pub fn Detour(comptime TargetFnType: type) type { - const FnInfo = @typeInfo(TargetFnType).@"fn"; - const TargetFnPtr = *const TargetFnType; - const ReturnType = FnInfo.return_type orelse void; - - return struct { - /// The underlying `GenericHook` managing the trampoline and patch. - inner: GenericHook = .{}, - - const Self = @This(); - - /// Hook the function at `target` to redirect to `detour`. - /// The `detour` function pointer must match `TargetFnType` exactly. - pub fn attach(self: *Self, target: usize, detour: TargetFnPtr) GenericHook.Error { - const err = self.inner.prepare(target); - if (err != .ok) return err; - self.inner.activate(@intFromPtr(detour)); - return .ok; - } - - /// Call the original (pre-hook) function through the trampoline. - /// Pass args as a tuple: `.{ arg1, arg2, arg3 }`. - /// - /// Uses `.never_tail` to prevent tail-call optimization, which would - /// corrupt the stack with callee-cleanup conventions (stdcall/fastcall/ - /// thiscall) — a tail call reuses the caller's stack frame, but the - /// callee pops args itself, leaving ESP pointing at garbage on return. - pub fn callOriginal(self: *const Self, args: anytype) ReturnType { - const orig: *const TargetFnType = @ptrFromInt(self.inner.trampoline); - return @call(.never_tail, orig, args); - } - - /// Unhook: restore original bytes, free trampoline. - pub fn detach(self: *Self) void { - self.inner.remove(); - } - - /// Get the trampoline as a raw function pointer of the target type. - /// - /// Unlike `callOriginal`, calling through this pointer does NOT force - /// `.never_tail` — the compiler may tail-call optimize, which corrupts - /// the stack for callee-cleanup conventions. Prefer `callOriginal` - /// unless you specifically need the function pointer (e.g. to pass as - /// a callback). - pub fn original(self: *const Self) *const TargetFnType { - return @ptrFromInt(self.inner.trampoline); - } - }; -} - -/// Resolve absolute target of a Jcc rel32 (0F 8x xx xx xx xx) — 6 byte insn. -fn jcc32Target(addr: usize) usize { - const disp: u32 = @bitCast(@as(*align(1) const i32, @ptrFromInt(addr + 2)).*); - return (addr + 6) +% disp; -}