Initial commit: WeirdUtils DLL for WoW 1.12.1
Lua protection bypass, embedded addon loading, async PNG screenshots with self-contained deflate compressor (store + fixed Huffman). 15KB ReleaseSmall.
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
//! x86 inline hooking library for Windows DLL injection.
|
||||
//!
|
||||
//! Provides a `Hook` struct for patching function prologues with JMP detours,
|
||||
//! building trampolines to call the original, and chaining with other hooks.
|
||||
//! Also includes memory read/write helpers, rel32 arithmetic, a generic
|
||||
//! `fastcall` caller, and a fastcall-to-cdecl thunk builder.
|
||||
//!
|
||||
//! ## Quick start
|
||||
//!
|
||||
//! ```zig
|
||||
//! const hook = @import("hook.zig");
|
||||
//!
|
||||
//! var my_hook = hook.Hook{};
|
||||
//!
|
||||
//! // One-shot: prepare trampoline + patch in one call.
|
||||
//! // The last arg is a slice of opcode offsets within the prologue that
|
||||
//! // contain E8/E9 (CALL/JMP rel32) instructions needing fixup.
|
||||
//! _ = my_hook.install(target_addr, prologue_size, @intFromPtr(&detour), &.{1});
|
||||
//!
|
||||
//! // Two-phase (when you need the alloc block before patching, e.g. for a thunk):
|
||||
//! _ = my_hook.prepare(target_addr, prologue_size, &.{});
|
||||
//! const thunk_buf = my_hook.mem.? + 32;
|
||||
//! _ = hook.buildFastcallToCdeclThunk(thunk_buf, @intFromPtr(&hookImpl), 1);
|
||||
//! my_hook.activate(@intFromPtr(thunk_buf));
|
||||
//!
|
||||
//! // Call the original from inside the detour:
|
||||
//! const orig = my_hook.getTrampoline(*const fn () callconv(.{ .x86_stdcall = .{} }) void);
|
||||
//! orig();
|
||||
//!
|
||||
//! // Unhook (restore original bytes, free memory):
|
||||
//! my_hook.remove();
|
||||
//! ```
|
||||
|
||||
const std = @import("std");
|
||||
const use_hwbp = @import("config").use_hwbp;
|
||||
|
||||
// =============================================================================
|
||||
// Windows API
|
||||
// =============================================================================
|
||||
|
||||
const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
|
||||
pub const PAGE_EXECUTE_READWRITE: u32 = 0x40;
|
||||
pub const MEM_COMMIT: u32 = 0x1000;
|
||||
pub const MEM_RELEASE: u32 = 0x8000;
|
||||
|
||||
extern "kernel32" fn VirtualProtect(
|
||||
lpAddress: *anyopaque,
|
||||
dwSize: usize,
|
||||
flNewProtect: u32,
|
||||
lpflOldProtect: *u32,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
extern "kernel32" fn VirtualAlloc(
|
||||
lpAddress: ?*anyopaque,
|
||||
dwSize: usize,
|
||||
flAllocationType: u32,
|
||||
flProtect: u32,
|
||||
) callconv(WINAPI) ?[*]u8;
|
||||
|
||||
extern "kernel32" fn VirtualFree(
|
||||
lpAddress: *anyopaque,
|
||||
dwSize: usize,
|
||||
dwFreeType: u32,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
// =============================================================================
|
||||
// Hardware breakpoint support (DR0-DR3 + Vectored Exception Handler)
|
||||
// =============================================================================
|
||||
|
||||
const CONTEXT_DEBUG_REGISTERS: u32 = 0x00010010;
|
||||
const EXCEPTION_SINGLE_STEP: u32 = 0x80000004;
|
||||
const EXCEPTION_CONTINUE_EXECUTION: i32 = -1;
|
||||
const EXCEPTION_CONTINUE_SEARCH: i32 = 0;
|
||||
|
||||
extern "kernel32" fn GetCurrentThread() callconv(WINAPI) *anyopaque;
|
||||
|
||||
extern "kernel32" fn GetThreadContext(
|
||||
hThread: *anyopaque,
|
||||
lpContext: *CONTEXT,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
extern "kernel32" fn SetThreadContext(
|
||||
hThread: *anyopaque,
|
||||
lpContext: *const CONTEXT,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
extern "kernel32" fn AddVectoredExceptionHandler(
|
||||
First: u32,
|
||||
Handler: *const fn (*EXCEPTION_POINTERS) callconv(WINAPI) i32,
|
||||
) callconv(WINAPI) ?*anyopaque;
|
||||
|
||||
extern "kernel32" fn RemoveVectoredExceptionHandler(
|
||||
Handle: *anyopaque,
|
||||
) callconv(WINAPI) u32;
|
||||
|
||||
const CONTEXT = extern struct {
|
||||
ContextFlags: u32,
|
||||
Dr0: u32,
|
||||
Dr1: u32,
|
||||
Dr2: u32,
|
||||
Dr3: u32,
|
||||
Dr6: u32,
|
||||
Dr7: u32,
|
||||
FloatSave: [112]u8,
|
||||
SegGs: u32,
|
||||
SegFs: u32,
|
||||
SegEs: u32,
|
||||
SegDs: u32,
|
||||
Edi: u32,
|
||||
Esi: u32,
|
||||
Ebx: u32,
|
||||
Edx: u32,
|
||||
Ecx: u32,
|
||||
Eax: u32,
|
||||
Ebp: u32,
|
||||
Eip: u32,
|
||||
SegCs: u32,
|
||||
EFlags: u32,
|
||||
Esp: u32,
|
||||
SegSs: u32,
|
||||
ExtendedRegisters: [512]u8,
|
||||
};
|
||||
|
||||
const EXCEPTION_RECORD = extern struct {
|
||||
ExceptionCode: u32,
|
||||
ExceptionFlags: u32,
|
||||
ExceptionRecord: ?*EXCEPTION_RECORD,
|
||||
ExceptionAddress: ?*anyopaque,
|
||||
NumberParameters: u32,
|
||||
ExceptionInformation: [15]usize,
|
||||
};
|
||||
|
||||
const EXCEPTION_POINTERS = extern struct {
|
||||
ExceptionRecord: *EXCEPTION_RECORD,
|
||||
ContextRecord: *CONTEXT,
|
||||
};
|
||||
|
||||
var hwbp_slots: [4]?*Hook = .{ null, null, null, null };
|
||||
var hwbp_detours: [4]usize = .{ 0, 0, 0, 0 };
|
||||
var veh_handle: ?*anyopaque = null;
|
||||
|
||||
fn vehHandler(info: *EXCEPTION_POINTERS) callconv(WINAPI) i32 {
|
||||
if (info.ExceptionRecord.ExceptionCode != EXCEPTION_SINGLE_STEP)
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
|
||||
const eip = info.ContextRecord.Eip;
|
||||
for (0..4) |i| {
|
||||
if (hwbp_slots[i]) |h| {
|
||||
if (h.target == eip) {
|
||||
info.ContextRecord.Eip = @intCast(hwbp_detours[i]);
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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 helper
|
||||
// =============================================================================
|
||||
|
||||
/// Call a __fastcall function at `addr` with ECX and EDX arguments.
|
||||
/// Dispatches on return type: void, f64 (x87 ST0), or integer/pointer (EAX).
|
||||
pub fn fastcall(comptime R: type, addr: usize, ecx: anytype, edx: anytype) R {
|
||||
if (R == f64) {
|
||||
return asm volatile ("call *%[func]"
|
||||
: [ret] "={st}" (-> f64),
|
||||
: [_] "{ecx}" (ecx), [_] "{edx}" (edx), [func] "r" (addr),
|
||||
: .{ .eax = true, .memory = true, .cc = true }
|
||||
);
|
||||
} else if (R == void) {
|
||||
asm volatile ("call *%[func]"
|
||||
:
|
||||
: [_] "{ecx}" (ecx), [_] "{edx}" (edx), [func] "r" (addr),
|
||||
: .{ .eax = true, .memory = true, .cc = true }
|
||||
);
|
||||
} else {
|
||||
return asm volatile ("call *%[func]"
|
||||
: [ret] "={eax}" (-> R),
|
||||
: [_] "{ecx}" (ecx), [_] "{edx}" (edx), [func] "r" (addr),
|
||||
: .{ .memory = true, .cc = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook struct
|
||||
// =============================================================================
|
||||
|
||||
const ALLOC_SIZE: usize = 64;
|
||||
const TRAMPOLINE_RESERVE: usize = 32;
|
||||
const MAX_PROLOGUE: usize = 16;
|
||||
|
||||
pub const Hook = struct {
|
||||
mem: ?[*]u8 = null,
|
||||
trampoline: usize = 0,
|
||||
target: usize = 0,
|
||||
prologue_size: usize = 0,
|
||||
saved_bytes: [MAX_PROLOGUE]u8 = undefined,
|
||||
|
||||
/// Allocate executable memory, save current bytes at target, and build the
|
||||
/// trampoline. Does NOT patch the target yet — call activate() after.
|
||||
/// `rel32_fixups` is a slice of opcode offsets within the prologue that
|
||||
/// contain E8/E9 instructions needing rel32 adjustment.
|
||||
pub fn prepare(
|
||||
self: *Hook,
|
||||
target: usize,
|
||||
prologue_size: usize,
|
||||
rel32_fixups: []const usize,
|
||||
) bool {
|
||||
if (self.mem != null) return true;
|
||||
|
||||
const mem = VirtualAlloc(null, ALLOC_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE) orelse return false;
|
||||
self.mem = mem;
|
||||
self.target = target;
|
||||
self.prologue_size = prologue_size;
|
||||
|
||||
// Save current bytes for remove()
|
||||
const src: [*]const u8 = @ptrFromInt(target);
|
||||
@memcpy(self.saved_bytes[0..prologue_size], src[0..prologue_size]);
|
||||
|
||||
// Build trampoline
|
||||
self.trampoline = @intFromPtr(mem);
|
||||
|
||||
if (src[0] == 0xE9) {
|
||||
// Another DLL already hooked — resolve their JMP and chain through it
|
||||
const other_detour = rel32Target(target);
|
||||
mem[0] = 0xE9;
|
||||
writeRel32(mem + 1, self.trampoline + 1, other_detour);
|
||||
} else {
|
||||
// Original prologue — copy bytes, fix up any relative instructions, JMP back
|
||||
@memcpy(mem[0..prologue_size], src[0..prologue_size]);
|
||||
|
||||
for (rel32_fixups) |opcode_offset| {
|
||||
const abs_target = rel32Target(target + opcode_offset);
|
||||
const tramp_operand = self.trampoline + opcode_offset + 1;
|
||||
writeRel32(mem + opcode_offset + 1, tramp_operand, abs_target);
|
||||
}
|
||||
|
||||
mem[prologue_size] = 0xE9;
|
||||
writeRel32(
|
||||
mem + prologue_size + 1,
|
||||
self.trampoline + prologue_size + 1,
|
||||
target + prologue_size,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Write the E9 JMP patch (inline mode) or set a hardware breakpoint
|
||||
/// (HWBP mode) to redirect target → detour_addr.
|
||||
pub fn activate(self: *Hook, detour_addr: usize) void {
|
||||
if (use_hwbp) {
|
||||
// Register VEH on first use
|
||||
if (veh_handle == null) {
|
||||
veh_handle = AddVectoredExceptionHandler(1, &vehHandler);
|
||||
}
|
||||
// Find free DR slot
|
||||
const slot: u2 = for (0..4) |i| {
|
||||
if (hwbp_slots[i] == null) break @as(u2, @intCast(i));
|
||||
} else return; // all 4 slots occupied
|
||||
|
||||
hwbp_slots[slot] = self;
|
||||
hwbp_detours[slot] = detour_addr;
|
||||
|
||||
const thread = GetCurrentThread();
|
||||
var ctx: CONTEXT = std.mem.zeroes(CONTEXT);
|
||||
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
||||
_ = GetThreadContext(thread, &ctx);
|
||||
|
||||
// Set DRn to target address
|
||||
switch (slot) {
|
||||
0 => { ctx.Dr0 = @intCast(self.target); },
|
||||
1 => { ctx.Dr1 = @intCast(self.target); },
|
||||
2 => { ctx.Dr2 = @intCast(self.target); },
|
||||
3 => { ctx.Dr3 = @intCast(self.target); },
|
||||
}
|
||||
|
||||
// Enable local execute breakpoint in DR7
|
||||
const s: u5 = slot;
|
||||
ctx.Dr7 |= @as(u32, 1) << (s * 2);
|
||||
ctx.Dr7 &= ~(@as(u32, 0xF) << (16 + s * 4));
|
||||
|
||||
_ = SetThreadContext(thread, &ctx);
|
||||
} else {
|
||||
var patch: [MAX_PROLOGUE]u8 = .{0x90} ** MAX_PROLOGUE;
|
||||
patch[0] = 0xE9;
|
||||
writeRel32(patch[1..5], self.target + 1, detour_addr);
|
||||
writeProtected(self.target, patch[0..self.prologue_size]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience: prepare + activate in one call.
|
||||
pub fn install(
|
||||
self: *Hook,
|
||||
target: usize,
|
||||
prologue_size: usize,
|
||||
detour_addr: usize,
|
||||
rel32_fixups: []const usize,
|
||||
) bool {
|
||||
if (!self.prepare(target, prologue_size, rel32_fixups)) return false;
|
||||
self.activate(detour_addr);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Restore original bytes (inline) or clear the DR slot (HWBP), then free
|
||||
/// the trampoline memory.
|
||||
pub fn remove(self: *Hook) void {
|
||||
if (self.mem == null) return;
|
||||
|
||||
if (use_hwbp) {
|
||||
for (0..4) |i| {
|
||||
if (hwbp_slots[i]) |h| {
|
||||
if (h == self) {
|
||||
const thread = GetCurrentThread();
|
||||
var ctx: CONTEXT = std.mem.zeroes(CONTEXT);
|
||||
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
||||
_ = GetThreadContext(thread, &ctx);
|
||||
|
||||
switch (@as(u2, @intCast(i))) {
|
||||
0 => { ctx.Dr0 = 0; },
|
||||
1 => { ctx.Dr1 = 0; },
|
||||
2 => { ctx.Dr2 = 0; },
|
||||
3 => { ctx.Dr3 = 0; },
|
||||
}
|
||||
const s: u5 = @intCast(i);
|
||||
ctx.Dr7 &= ~(@as(u32, 1) << (s * 2));
|
||||
ctx.Dr7 &= ~(@as(u32, 0xF) << (16 + s * 4));
|
||||
|
||||
_ = SetThreadContext(thread, &ctx);
|
||||
|
||||
hwbp_slots[i] = null;
|
||||
hwbp_detours[i] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
writeProtected(self.target, self.saved_bytes[0..self.prologue_size]);
|
||||
}
|
||||
|
||||
_ = VirtualFree(@ptrFromInt(@intFromPtr(self.mem.?)), 0, MEM_RELEASE);
|
||||
self.mem = null;
|
||||
}
|
||||
|
||||
/// Cast trampoline address to a typed function pointer for calling the original.
|
||||
pub fn getTrampoline(self: *const Hook, comptime T: type) T {
|
||||
return @ptrFromInt(self.trampoline);
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Thunk builder: fastcall(ECX, EDX, stack...) → cdecl(stack, stack, stack...)
|
||||
// =============================================================================
|
||||
|
||||
/// Build a fastcall-to-cdecl bridge thunk in `buf`.
|
||||
/// `cdecl_fn` is the address of the cdecl target function.
|
||||
/// `stack_arg_count` is the number of extra stack arguments beyond ECX/EDX.
|
||||
/// Returns the total thunk size in bytes.
|
||||
///
|
||||
/// Generated code:
|
||||
/// push dword [esp + 4*N] ; for each stack arg, right-to-left
|
||||
/// ...
|
||||
/// push edx ; arg 2
|
||||
/// push ecx ; arg 1
|
||||
/// mov eax, <cdecl_fn>
|
||||
/// call eax
|
||||
/// add esp, (2 + stack_arg_count) * 4
|
||||
/// ret (stack_arg_count * 4)
|
||||
pub fn buildFastcallToCdeclThunk(buf: [*]u8, cdecl_fn: usize, stack_arg_count: u8) usize {
|
||||
var pos: usize = 0;
|
||||
|
||||
// Push stack args right-to-left. At entry, [esp] = return addr,
|
||||
// [esp+4] = first stack arg, [esp+8] = second, etc.
|
||||
// But each push shifts esp, so we always read from [esp + 4 * stack_arg_count]
|
||||
// (the offset stays constant because we push the same number of times as the depth grows).
|
||||
var i: u8 = stack_arg_count;
|
||||
while (i > 0) : (i -= 1) {
|
||||
// push dword ptr [esp + 4 * stack_arg_count]
|
||||
buf[pos] = 0xFF;
|
||||
buf[pos + 1] = 0x74;
|
||||
buf[pos + 2] = 0x24;
|
||||
buf[pos + 3] = stack_arg_count * 4;
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
// push edx (arg 2)
|
||||
buf[pos] = 0x52;
|
||||
pos += 1;
|
||||
|
||||
// push ecx (arg 1)
|
||||
buf[pos] = 0x51;
|
||||
pos += 1;
|
||||
|
||||
// mov eax, <cdecl_fn>
|
||||
buf[pos] = 0xB8;
|
||||
std.mem.writeInt(u32, buf[pos + 1 ..][0..4], @intCast(cdecl_fn), .little);
|
||||
pos += 5;
|
||||
|
||||
// call eax
|
||||
buf[pos] = 0xFF;
|
||||
buf[pos + 1] = 0xD0;
|
||||
pos += 2;
|
||||
|
||||
// add esp, (2 + stack_arg_count) * 4 (cdecl caller cleanup)
|
||||
const cleanup: u8 = (2 + stack_arg_count) * 4;
|
||||
buf[pos] = 0x83;
|
||||
buf[pos + 1] = 0xC4;
|
||||
buf[pos + 2] = cleanup;
|
||||
pos += 3;
|
||||
|
||||
// ret (stack_arg_count * 4) (fastcall callee cleans stack args)
|
||||
if (stack_arg_count == 0) {
|
||||
buf[pos] = 0xC3; // ret
|
||||
pos += 1;
|
||||
} else {
|
||||
buf[pos] = 0xC2; // ret imm16
|
||||
std.mem.writeInt(u16, buf[pos + 1 ..][0..2], @as(u16, stack_arg_count) * 4, .little);
|
||||
pos += 3;
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
Reference in New Issue
Block a user