diff --git a/README.md b/README.md index 4613da5..54a3f88 100644 --- a/README.md +++ b/README.md @@ -274,14 +274,14 @@ what you do with them on someone else's server is between you and that server's ### Building -Requires Zig 0.16 and a checkout of [zhook](https://codeberg.org/marcelinevq/zhook) -as a sibling directory - `build.zig.zon` refers to it by relative path: +Requires Zig 0.16. The only dependency is +[zhook](https://codeberg.org/marcelinevq/zhook), the x86 inline hooking library, +pinned in `build.zig.zon` and fetched automatically: ```sh -git clone https://codeberg.org/marcelinevq/zhook git clone https://codeberg.org/MarcelineVQ/WeirdUtils cd WeirdUtils -zig build # zig-out/bin/weirdutils.dll +zig build # zig-out/bin/weirdutils.dll zig build all-variants -Doptimize=ReleaseSmall # + one DLL per module ``` diff --git a/build.zig.zon b/build.zig.zon index 101ba6a..223cc6c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,7 +4,8 @@ .fingerprint = 0x54f0a9542562d318, .dependencies = .{ .zhook = .{ - .path = "../zhook", + .url = "https://codeberg.org/marcelinevq/zhook/archive/f1b252ed61ad839f00310c386761d068f293ad0f.tar.gz", + .hash = "zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov", }, }, .paths = .{ diff --git a/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/build.zig b/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/build.zig new file mode 100644 index 0000000..ea7821c --- /dev/null +++ b/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/build.zig @@ -0,0 +1,21 @@ +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 new file mode 100644 index 0000000..ab3a97a --- /dev/null +++ b/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .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 new file mode 100644 index 0000000..9522720 --- /dev/null +++ b/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/src/x86dis.zig @@ -0,0 +1,432 @@ +//! 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 new file mode 100644 index 0000000..cadb828 --- /dev/null +++ b/zig-pkg/zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov/src/zhook.zig @@ -0,0 +1,415 @@ +//! 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; +}