diff options
author | 2025-08-11 18:17:40 -0400 | |
---|---|---|
committer | 2025-08-11 20:46:00 -0400 | |
commit | 89055b019e2e2d49f8813d3578f6bc338326ca47 (patch) | |
tree | cdd097e134454937ccd35324b55d4055c2c2f4ed | |
download | rt2-89055b019e2e2d49f8813d3578f6bc338326ca47.tar.gz rt2-89055b019e2e2d49f8813d3578f6bc338326ca47.zip |
-rw-r--r-- | build.zig | 39 | ||||
-rw-r--r-- | build.zig.zon | 16 | ||||
-rw-r--r-- | hook/LICENSE | 12 | ||||
-rw-r--r-- | hook/build.zig | 21 | ||||
-rw-r--r-- | hook/build.zig.zon | 12 | ||||
-rw-r--r-- | hook/src/Hook.zig | 237 | ||||
-rw-r--r-- | hook/src/HookManager.zig | 80 | ||||
-rw-r--r-- | hook/src/mem.zig | 199 | ||||
-rw-r--r-- | hook/src/utils.zig | 198 | ||||
-rw-r--r-- | hook/src/x86.zig | 670 | ||||
-rw-r--r-- | hook/src/zhook.zig | 9 | ||||
-rw-r--r-- | src/main.zig | 608 | ||||
-rw-r--r-- | src/root.zig | 24 |
13 files changed, 2125 insertions, 0 deletions
diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..f2ab70d --- /dev/null +++ b/build.zig @@ -0,0 +1,39 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + // for now, this only supports 32 bit windows builds, so just build for that + // by default + const target = b.standardTargetOptions(.{ .default_target = .{ + .os_tag = .windows, + .cpu_arch = .x86, + .abi = .msvc, + } }); + const optimize = b.standardOptimizeOption(.{}); + + const hook = b.dependency("hook", .{}); + + const exe = b.addExecutable(.{ + .name = "rt2", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), + }); + + b.installArtifact(exe); + exe.root_module.addImport("hook", hook.module("hook")); + exe.linkSystemLibrary("kernel32"); + exe.linkLibC(); + + exe.addIncludePath(b.path("../../../scoop/persist/vcpkg/installed/x86-windows-static/include")); + + const system_libraries = [_][]const u8 { + "avcodec", "avutil", "avformat", "swscale", "vorbisfile", "libx264", "x265-static", + "swresample", "ws2_32", "mfuuid", "strmiids", "ole32", "user32", "bcrypt", "secur32", + "aom", "opus", "libmp3lame-static", "opus", "libmpghip-static", + }; + for (system_libraries) |library| { + exe.linkSystemLibrary2(library, .{.preferred_link_mode = .static}); + } +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..8ad8cbd --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,16 @@ +.{ + .name = .rt2, + .version = "0.0.0", + .fingerprint = 0x2bb0885edd13d89c, // Changing this has security and trust implications. + .minimum_zig_version = "0.15.0-dev.1092+d772c0627", + .dependencies = .{ + .hook = .{ + .path = "hook/", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/hook/LICENSE b/hook/LICENSE new file mode 100644 index 0000000..ec7482e --- /dev/null +++ b/hook/LICENSE @@ -0,0 +1,12 @@ +Copyright (c) 2025 evanlin96069
+
+Permission to use, copy, modify, and/or distribute this software for any purpose
+with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+THIS SOFTWARE.
diff --git a/hook/build.zig b/hook/build.zig new file mode 100644 index 0000000..6e13e7c --- /dev/null +++ b/hook/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(.{}); + + const mod = b.addModule("hook", .{ + .root_source_file = b.path("src/zhook.zig"), + .target = target, + .optimize = optimize, + }); + if (target.result.os.tag == .windows) + mod.linkSystemLibrary("kernel32", .{}); + + const mod_tests = b.addTest(.{ + .root_module = mod, + }); + const run_mod_tests = b.addRunArtifact(mod_tests); + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_mod_tests.step); +} diff --git a/hook/build.zig.zon b/hook/build.zig.zon new file mode 100644 index 0000000..f9c531b --- /dev/null +++ b/hook/build.zig.zon @@ -0,0 +1,12 @@ +.{ + .name = .hook, + .version = "0.0.0", + .fingerprint = 0xa45843556d3689d1, // Changing this has security and trust implications. + .minimum_zig_version = "0.15.0-dev.1149+4e6a04929", + .dependencies = .{}, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/hook/src/Hook.zig b/hook/src/Hook.zig new file mode 100644 index 0000000..7a8ca06 --- /dev/null +++ b/hook/src/Hook.zig @@ -0,0 +1,237 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const x86 = @import("x86.zig"); +const utils = @import("utils.zig"); + +const loadValue = @import("mem.zig").loadValue; + +const Hook = @This(); + +const HookType = enum { + vmt, + detour, +}; + +const Rel32Patch = struct { + offset: u32, // offset into the trampoline + dest: u32, // absolute address rel32 points to + orig: u32, // original data in the instruction +}; + +// Windows doesn't use PIC +const PICPatch = switch (builtin.os.tag) { + .linux => struct { + offset: u32, // offset into the original function + orig: u32, // original data in the instruction + }, + .windows => void, + else => @compileError("Unsupported OS"), +}; + +const HookData = union(HookType) { + const HookVMTResult = struct { + vt: [*]*const anyopaque, + index: u32, + }; + + const HookDetourResult = struct { + func: [*]u8, + trampoline: []u8, + rel32_patch: ?Rel32Patch = null, + pic_patch: ?PICPatch = null, + }; + + vmt: HookVMTResult, + detour: HookDetourResult, +}; + +orig: ?*const anyopaque, +data: HookData, + +pub fn hookVMT(vt: [*]*const anyopaque, index: usize, target: *const anyopaque) !Hook { + const orig: *const anyopaque = vt[index]; + const entry_ptr: [*]u8 = @ptrCast(vt + index); + + const bytes = std.mem.toBytes(target); + try utils.patchCode(entry_ptr, &bytes, 0b001); // restore to read-only + + return Hook{ + .orig = orig, + .data = .{ + .vmt = .{ + .vt = vt, + .index = index, + }, + }, + }; +} + +// Trampoline memory must have rwx permissions +pub fn hookDetour(func: *anyopaque, target: *const anyopaque, trampoline: []u8) !Hook { + var mem: [*]u8 = @ptrCast(func); + + // Hook the underlying thing if the function jmp immediately. + while (mem[0] == x86.Opcode.Op1IW.jmpiw) { + const offset = loadValue(u32, mem + 1); + mem = @ptrFromInt(@intFromPtr(mem + 5) +% offset); + } + + var rel32_patch: ?Rel32Patch = null; + var pic_patch: ?PICPatch = null; + + var len: usize = 0; + while (true) : (len += try x86.x86_len(mem + len)) { + if (len >= 5) break; + + // No checks for rel16 at all. I don't think we will encounter them. + const op0 = mem[len]; + switch (op0) { + x86.Opcode.Op1I8.jmpi8, + x86.Opcode.Op1I8.jcxz, + x86.Opcode.Op1I8.jo, + x86.Opcode.Op1I8.jno, + x86.Opcode.Op1I8.jb, + x86.Opcode.Op1I8.jnb, + x86.Opcode.Op1I8.jz, + x86.Opcode.Op1I8.jnz, + x86.Opcode.Op1I8.jna, + x86.Opcode.Op1I8.ja, + x86.Opcode.Op1I8.js, + x86.Opcode.Op1I8.jns, + x86.Opcode.Op1I8.jp, + x86.Opcode.Op1I8.jnp, + x86.Opcode.Op1I8.jl, + x86.Opcode.Op1I8.jnl, + x86.Opcode.Op1I8.jng, + x86.Opcode.Op1I8.jg, + => { + // TODO: Make it rel32 jump in the trampoline + return error.BadInstruction; + }, + + x86.Opcode.Op1IW.jmpiw, + x86.Opcode.Op1IW.call, + => { + const offset = loadValue(u32, mem + len + 1); + rel32_patch = .{ + .offset = len + 1, + .dest = @intFromPtr(mem + len + 5) +% offset, + .orig = offset, + }; + + if (op0 == x86.Opcode.Op1IW.call and builtin.os.tag == .linux) { + // Look for PIC pattern: + // call __i686.get_pc_thunk.reg + // add reg, imm32 + if (utils.matchPIC(mem + len)) |off| { + const imm32 = loadValue(u32, mem + len + off); + pic_patch = .{ + .offset = len + off, + .orig = imm32, + }; + } + } + }, + + x86.Opcode.op2_byte => { + const op1 = mem[len + 1]; + switch (op1) { + x86.Opcode.Op2IW.joii, + x86.Opcode.Op2IW.jnoii, + x86.Opcode.Op2IW.jbii, + x86.Opcode.Op2IW.jnbii, + x86.Opcode.Op2IW.jzii, + x86.Opcode.Op2IW.jnzii, + x86.Opcode.Op2IW.jnaii, + x86.Opcode.Op2IW.jaii, + x86.Opcode.Op2IW.jsii, + x86.Opcode.Op2IW.jnsii, + x86.Opcode.Op2IW.jpii, + x86.Opcode.Op2IW.jnpii, + x86.Opcode.Op2IW.jlii, + x86.Opcode.Op2IW.jnlii, + x86.Opcode.Op2IW.jngii, + x86.Opcode.Op2IW.jgii, + => { + const offset = loadValue(u32, mem + len + 2); + rel32_patch = .{ + .offset = len + 2, + .dest = @intFromPtr(mem + len + 6) +% offset, + .orig = offset, + }; + }, + else => {}, + } + }, + else => {}, + } + } + + const trampoline_size = len + 5; + if (trampoline.len < trampoline_size) { + return error.OutOfTrampoline; + } + + @memcpy(trampoline[0..len], mem); + trampoline[len] = x86.Opcode.Op1IW.jmpiw; + const jmp1_offset: *align(1) u32 = @ptrCast(trampoline.ptr + len + 1); + jmp1_offset.* = @intFromPtr(mem + len) -% @intFromPtr(trampoline.ptr + len + 5); + + if (rel32_patch) |r| { + const rel_patch: *align(1) u32 = @ptrCast(trampoline.ptr + r.offset); + rel_patch.* = r.dest -% (@intFromPtr(trampoline.ptr + r.offset + 4)); + } + + var detour: [5]u8 = undefined; + detour[0] = x86.Opcode.Op1IW.jmpiw; + const jmp2_offset: *align(1) u32 = @ptrCast(&detour[1]); + jmp2_offset.* = @intFromPtr(target) -% @intFromPtr(mem + 5); + + try utils.patchCode(mem, detour[0..], 0b101); + + if (builtin.os.tag == .linux) { + if (pic_patch) |p| { + const delta: u32 = @intFromPtr(trampoline.ptr) -% @intFromPtr(mem); + const new_value: u32 = p.orig -% delta; + + const bytes = std.mem.toBytes(new_value); + try utils.patchCode(mem + p.offset, &bytes, 0b101); + } + } + + return Hook{ + .orig = trampoline.ptr, + .data = .{ .detour = .{ + .func = mem, + .trampoline = trampoline[0..trampoline_size], + .rel32_patch = rel32_patch, + .pic_patch = pic_patch, + } }, + }; +} + +pub fn unhook(self: *Hook) !void { + const orig = self.orig orelse return; + switch (self.data) { + .vmt => |v| { + const entry_ptr: [*]u8 = @ptrCast(v.vt + v.index); + const bytes = std.mem.toBytes(orig); + try utils.patchCode(entry_ptr, &bytes, 0b001); // restore to read-only + }, + .detour => |v| { + if (v.rel32_patch) |r| { + const orig_patch: *align(1) u32 = @ptrCast(v.trampoline.ptr + r.offset); + orig_patch.* = r.orig; + } + try utils.patchCode(v.func, v.trampoline[0 .. v.trampoline.len - 5], 0b101); + if (builtin.os.tag == .linux) { + if (v.pic_patch) |p| { + const bytes = std.mem.toBytes(p.orig); + try utils.patchCode(v.func + p.offset, &bytes, 0b101); + } + } + }, + } + self.orig = null; +} diff --git a/hook/src/HookManager.zig b/hook/src/HookManager.zig new file mode 100644 index 0000000..259fc3e --- /dev/null +++ b/hook/src/HookManager.zig @@ -0,0 +1,80 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const Hook = @import("Hook.zig"); +const mem = @import("mem.zig"); +const utils = @import("utils.zig"); + +const HookManager = @This(); + +hooks: std.ArrayList(Hook), +exec_page: []u8, + +pub fn init(alloc: std.mem.Allocator) !HookManager { + // create our exectuable page to store trampolines + const page_size = std.heap.page_size_min; + const exec_page: []u8 = try alloc.alignedAlloc(u8, .fromByteUnits(page_size), page_size); + if (builtin.os.tag == .windows) { + var oldprotect: u32 = undefined; + try std.os.windows.VirtualProtect(exec_page.ptr, page_size, std.os.windows.PAGE_EXECUTE_READWRITE, &oldprotect); + } else if (builtin.os.tag == .linux) { + const PROT = std.os.linux.PROT; + try std.os.linux.mprotect(exec_page.ptr, page_size, PROT.EXEC | PROT.WRITE | PROT.READ); + } else { + @compileError("unsupported os"); + } + return HookManager{ + .hooks = std.ArrayList(Hook).init(alloc), + .exec_page = exec_page[0..], + }; +} + +pub fn deinit(self: *HookManager) usize { + var count: usize = 0; + for (self.hooks.items) |*hook| { + hook.unhook() catch continue; + count += 1; + } + + self.hooks.deinit(); + return count; +} + +pub fn findAndHook(self: *HookManager, T: type, module: []const u8, patterns: []const []const ?u8, target: *const anyopaque) !T { + const match = mem.scanUniquePatterns(module, patterns) orelse { + return error.PatternNotFound; + }; + + return self.hookDetour(T, match.ptr, target); +} + +pub fn hookVMT(self: *HookManager, vt: [*]*const anyopaque, index: usize, target: anytype) !@TypeOf(target) { + var hook = try Hook.hookVMT(vt, index, target); + errdefer hook.unhook() catch {}; + + try self.hooks.append(hook); + + return @ptrCast(hook.orig.?); +} + +pub fn hookDetour(self: *HookManager, func: anytype, target: @TypeOf(func)) !@TypeOf(func) { + var hook = try Hook.hookDetour(@constCast(func), target, self.exec_page); + errdefer hook.unhook() catch {}; + + try self.hooks.append(hook); + + self.exec_page = self.exec_page[hook.data.detour.trampoline.len..]; + + return @ptrCast(hook.orig.?); +} + +pub fn hookSymbol(self: *HookManager, module_name: []const u8, func: [:0]const u8, target: anytype) !@TypeOf(target) { + comptime std.debug.assert(@typeInfo(@TypeOf(target)) == .pointer); + + var module = try std.DynLib.open(module_name); + // this just decreases refcount by 1, we don't ever hook things that aren't + // already loaded so it doesn't matter + defer module.close(); + const func_ptr = module.lookup(@TypeOf(target), func) orelse return error.NoSuchSymbol; + return self.hookDetour(func_ptr, target); +} diff --git a/hook/src/mem.zig b/hook/src/mem.zig new file mode 100644 index 0000000..62aec00 --- /dev/null +++ b/hook/src/mem.zig @@ -0,0 +1,199 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const testing = std.testing; + +const utils = @import("utils.zig"); + +const isHex = utils.isHex; +const makeHex = utils.makeHex; + +pub fn makePattern(comptime str: []const u8) []const ?u8 { + return comptime blk: { + @setEvalBranchQuota(10000); + var it = std.mem.splitSequence(u8, str, " "); + var pat: []const ?u8 = &.{}; + + while (it.next()) |byte| { + if (byte.len != 2) { + @compileError("Each byte should be 2 characters"); + } + if (byte[0] == '?') { + if (byte[1] != '?') { + @compileError("The second question mark is missing"); + } + pat = pat ++ .{null}; + } else if (isHex(byte[0])) { + if (!isHex(byte[1])) { + @compileError("The second hex digit is missing"); + } + const n = try std.fmt.parseInt(u8, byte, 16); + pat = pat ++ .{n}; + } else { + @compileError("Only hex digits, spaces and question marks are allowed"); + } + } + break :blk pat; + }; +} + +pub fn makePatterns(comptime arr: anytype) []const []const ?u8 { + return comptime blk: { + var patterns: []const []const ?u8 = &.{}; + for (arr) |str| { + const pat: []const []const ?u8 = &.{makePattern(str)}; + patterns = patterns ++ pat; + } + break :blk patterns; + }; +} + +pub fn scanFirst(mem: []const u8, pattern: []const ?u8) ?usize { + if (mem.len < pattern.len) { + return null; + } + + var offset: usize = 0; + outer: while (offset < mem.len - pattern.len + 1) : (offset += 1) { + for (pattern, 0..) |byte, j| { + if (byte) |b| { + if (b != mem[offset + j]) { + continue :outer; + } + } + } + return offset; + } + + return null; +} + +pub fn scanUnique(mem: []const u8, pattern: []const ?u8) ?usize { + if (scanFirst(mem, pattern)) |offset| { + if (scanFirst(mem[offset + pattern.len ..], pattern) != null) { + return null; + } + return offset; + } + + return null; +} + +pub const MatchedPattern = struct { + index: usize, + ptr: [*]const u8, +}; + +pub fn scanAllPatterns(mem: []const u8, patterns: []const []const ?u8, data: *std.ArrayList(MatchedPattern)) !void { + for (patterns, 0..) |pattern, i| { + var base: usize = 0; + while (scanFirst(mem[base..], pattern)) |offset| { + try data.append(MatchedPattern{ + .index = i, + .ptr = mem.ptr + base + offset, + }); + base += offset + pattern.len; + } + } +} + +pub fn scanUniquePatterns(mem: []const u8, patterns: []const []const ?u8) ?MatchedPattern { + var match: ?MatchedPattern = null; + for (patterns, 0..) |pattern, i| { + if (scanFirst(mem, pattern)) |offset| { + if (scanFirst(mem[offset + pattern.len ..], pattern) != null) { + return null; + } + + if (match != null) { + return null; + } + + match = .{ + .index = i, + .ptr = mem.ptr + offset, + }; + } + } + + return match; +} + +test "Scan first pattern" { + const mem = makeHex("F6 05 12 34 56 78 12"); + + // Match at the start + const test_pattern1 = makePattern("F6 05 12"); + const result1 = scanFirst(mem, test_pattern1); + try testing.expect(result1 != null); + if (result1) |offset| { + try testing.expectEqual(0, offset); + } + + // Match at the middle + const test_pattern2 = makePattern("12 34 56"); + const result2 = scanFirst(mem, test_pattern2); + try testing.expect(result2 != null); + if (result2) |offset| { + try testing.expectEqual(2, offset); + } + + // Match at the end + const test_pattern3 = makePattern("56 78 12"); + const result3 = scanFirst(mem, test_pattern3); + try testing.expect(result3 != null); + if (result3) |offset| { + try testing.expectEqual(4, offset); + } +} + +test "Scan unique patterns" { + const mem = makeHex("F6 05 12 34 56 78 12"); + const test_patterns = makePatterns(.{ + "00 00 ?? ?? 12", + "12 ?? 56", + "F6 05 00 34", + }); + + const result = scanUniquePatterns(mem, test_patterns); + try testing.expect(result != null); + if (result) |r| { + try testing.expectEqual(1, r.index); + try testing.expectEqual(mem.ptr + 2, r.ptr); + } +} + +test "Scan unique patterns with multiple matches" { + const mem = makeHex("12 34 56 12 34 56 78 9A BC DE"); + + const test_patterns1 = makePatterns(.{ + "12 34 56", // Non-unique match + }); + try testing.expect(scanUniquePatterns(mem, test_patterns1) == null); + + const test_patterns2 = makePatterns(.{ + "12 ?? ?? 12", // Unique match + "9A BC DE", // Unique match + }); + try testing.expect(scanUniquePatterns(mem, test_patterns2) == null); + + const test_patterns3 = makePatterns(.{ + "12 34 56", // Non-unique match + "9A BC DE", // Unique match + }); + try testing.expect(scanUniquePatterns(mem, test_patterns3) == null); +} + +pub fn loadValue(T: type, ptr: [*]const u8) T { + const val: *align(1) const T = @ptrCast(ptr); + return val.*; +} + +pub fn setValue(T: type, ptr: [*]u8, value: T) void { + const val: *align(1) T = @ptrCast(ptr); + val.* = value; +} + +test "Load value from memory" { + const mem = makeHex("E9 B1 9A 78 56"); // jmp + try testing.expectEqual(0x56789AB1, loadValue(u32, mem.ptr + 1)); +} diff --git a/hook/src/utils.zig b/hook/src/utils.zig new file mode 100644 index 0000000..57fdfec --- /dev/null +++ b/hook/src/utils.zig @@ -0,0 +1,198 @@ +const std = @import("std"); +const w = std.os.windows; +const builtin = @import("builtin"); + +const x86 = @import("x86.zig"); +const mem = @import("mem.zig"); + +const MODULEINFO = extern struct { + lpBaseOfDll: w.LPVOID, + SizeOfImage: w.DWORD, + EntryPoint: w.LPVOID, +}; + +extern "kernel32" fn FlushInstructionCache(hProcess: w.HANDLE, lpBaseAddress: w.LPCVOID, dwSize: w.SIZE_T) callconv(.winapi) w.BOOL; +extern "kernel32" fn GetModuleHandleW(lpModuleName: ?w.LPCWSTR) callconv(.winapi) w.HMODULE; +extern "kernel32" fn GetModuleInformation(hProcess: w.HANDLE, hModule: w.HMODULE, lpmodinfo: *MODULEINFO, cb: w.DWORD) callconv(.winapi) w.BOOL; +extern "kernel32" fn GetCurrentProcess() callconv(.winapi) w.HANDLE; + +pub inline fn isHex(c: u8) bool { + return (c >= '0' and c <= '9') or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F'); +} + +pub fn makeHex(comptime str: []const u8) []const u8 { + return comptime blk: { + @setEvalBranchQuota(10000); + var it = std.mem.splitSequence(u8, str, " "); + var pat: []const u8 = &.{}; + + while (it.next()) |byte| { + if (byte.len != 2) { + @compileError("Each byte should be 2 characters"); + } + if (isHex(byte[0])) { + if (!isHex(byte[1])) { + @compileError("The second hex digit is missing"); + } + const n = try std.fmt.parseInt(u8, byte, 16); + pat = pat ++ .{n}; + } else { + @compileError("Only hex digits are allowed"); + } + } + break :blk pat; + }; +} + +pub fn patchCode(addr: [*]u8, data: []const u8, restore_protect: u32) !void { + if (builtin.os.tag == .windows) { + var old_protect: w.DWORD = undefined; + + try w.VirtualProtect(addr, data.len, w.PAGE_EXECUTE_READWRITE, &old_protect); + @memcpy(addr, data); + + _ = FlushInstructionCache(GetCurrentProcess(), addr, data.len); + try w.VirtualProtect(addr, data.len, old_protect, &old_protect); + } else { + const page_size = std.heap.page_size_min; + const addr_int = @intFromPtr(addr); + const page_start = addr_int & ~(page_size - 1); + const page_end = addr_int + data.len; + const page_len = (page_end - page_start + page_size - 1) & ~(page_size - 1); + + const prot_all = 0b111; // rwx + + if (std.c.mprotect(@ptrFromInt(page_start), page_len, prot_all) != 0) + return error.MProtectWritable; + + @memcpy(addr, data); + + if (std.c.mprotect(@ptrFromInt(page_start), page_len, restore_protect) != 0) + return error.MProtectRestore; + } +} + +// Windows: Return entire module memory +// Linux: Return the code segment memory +pub fn getModule(comptime module_name: []const u8) ?[]const u8 { + return switch (builtin.os.tag) { + .windows => getModuleWindows(module_name), + .linux => getModuleLinux(module_name, 0b101) catch return null, + else => @compileError("getModule is not available for this target"), + }; +} + +// Return the entire module memory +pub fn getEntireModule(comptime module_name: []const u8) ?[]const u8 { + return switch (builtin.os.tag) { + .windows => getModuleWindows(module_name), + .linux => getModuleLinux(module_name, 0) catch return null, + else => @compileError("getModule is not available for this target"), + }; +} + +fn getModuleWindows(comptime module_name: []const u8) ?[]const u8 { + const dll_name = module_name ++ ".dll"; + const path_w = std.unicode.utf8ToUtf16LeStringLiteral(dll_name); + const dll = w.GetModuleHandleW(path_w) orelse return null; + var info: w.MODULEINFO = undefined; + if (GetModuleInformation(GetCurrentProcess(), dll, &info, @sizeOf(MODULEINFO)) == 0) { + return null; + } + const module: [*]const u8 = @ptrCast(dll); + return module[0..info.SizeOfImage]; +} + +fn getModuleLinux(comptime module_name: []const u8, permission: u32) !?[]const u8 { + const file_name = module_name ++ ".so"; + + const allocator = std.heap.page_allocator; + var file = try std.fs.openFileAbsolute("/proc/self/maps", .{ .mode = .read_only }); + defer file.close(); + var reader = file.reader(); + + var base: usize = 0; + var end: usize = 0; + var found = false; + + while (try reader.readUntilDelimiterOrEofAlloc(allocator, '\n', 4096)) |line| { + defer allocator.free(line); + + // Example format: + // de228000-de229000 r--p 00000000 00:29 1026008 /usr/lib/libstdc++.so.6.0.33 + + if (!std.mem.endsWith(u8, line, file_name)) { + if (found) break; + continue; + } + + const pos = line.len - file_name.len; + if (line[pos - 1] != '/' and line[pos - 1] != ' ') continue; + + const dash = std.mem.indexOfScalar(u8, line, '-') orelse continue; + const space = std.mem.indexOfScalarPos(u8, line, dash + 1, ' ') orelse continue; + + const perms_start = space + 1; + if (line.len < perms_start + 4) continue; + const read = line[perms_start]; + const write = line[perms_start + 1]; + const exec = line[perms_start + 2]; + if (permission & 0b001 != 0 and read == '-') continue; + if (permission & 0b010 != 0 and write == '-') continue; + if (permission & 0b100 != 0 and exec == '-') continue; + + const start_hex = line[0..dash]; + const end_hex = line[dash + 1 .. space]; + + const start_addr = try std.fmt.parseInt(usize, start_hex, 16); + const end_addr = try std.fmt.parseInt(usize, end_hex, 16); + + if (!found) { + base = start_addr; + end = end_addr; + found = true; + } else if (start_addr == end) { + end = end_addr; + } else { + break; + } + } + + if (!found) return null; + + const size = end - base; + const ptr: [*]const u8 = @ptrFromInt(base); + return ptr[0..size]; +} + +// Match call + add pattern +// If matched, inst + len will be the start of the imm32 +pub fn matchPIC(inst: [*]const u8) ?u32 { + if (inst[0] != x86.Opcode.Op1.call) return null; + if (inst[5] == x86.Opcode.Op1.alumiw) { + const modrm = inst[6]; + // mod must be 0b11 (register operand) + if ((modrm & 0b1100_0000) != 0b1100_0000) return null; + // reg/opcode must be 0b000 (ADD) + if ((modrm & 0b0011_1000) != 0b0000_0000) return null; + + // rm should not be 0b100 (ESP) + // Although it's rare, compiler occasionally uses EBP for PIC + const rm = modrm & 0b0000_0111; + if (rm == 0b100) return null; + return 7; + } else if (inst[5] == x86.Opcode.Op1.addeaxi) { + return 6; + } + return null; +} + +const GOT_pattern = mem.makePattern("E8 ?? ?? ?? ?? 05 ?? ?? ?? ?? 8D 80"); + +pub fn findGOTAddr(module: []const u8) ?u32 { + if (mem.scanFirst(module, GOT_pattern)) |offset| { + const imm32 = mem.loadValue(u32, module.ptr + offset + 6); + return @intFromPtr(module.ptr + offset + 5) +% imm32; + } + return null; +} diff --git a/hook/src/x86.zig b/hook/src/x86.zig new file mode 100644 index 0000000..60b9666 --- /dev/null +++ b/hook/src/x86.zig @@ -0,0 +1,670 @@ +const std = @import("std"); +const testing = std.testing; + +const makeHex = @import("utils.zig").makeHex; + +pub const Opcode = struct { + pub const Prefixes = struct { + pub const es = 0x26; + pub const cs = 0x2E; + pub const ss = 0x36; + pub const ds = 0x3E; + pub const fs = 0x64; + pub const gs = 0x65; + pub const opsz = 0x66; + pub const adsz = 0x67; + pub const lock = 0xF0; + pub const repn = 0xF2; + pub const rep = 0xF3; + }; + + pub const Op1No = struct { + pub const pushes = 0x06; + pub const popes = 0x07; + pub const pushcs = 0x0E; + pub const pushss = 0x16; + pub const popss = 0x17; + pub const pushds = 0x1E; + pub const popds = 0x1F; + pub const daa = 0x27; + pub const das = 0x2F; + pub const aaa = 0x37; + pub const aas = 0x3F; + pub const inceax = 0x40; + pub const incecx = 0x41; + pub const incedx = 0x42; + pub const incebx = 0x43; + pub const incesp = 0x44; + pub const incebp = 0x45; + pub const incesi = 0x46; + pub const incedi = 0x47; + pub const deceax = 0x48; + pub const dececx = 0x49; + pub const decedx = 0x4A; + pub const decebx = 0x4B; + pub const decesp = 0x4C; + pub const decebp = 0x4D; + pub const decesi = 0x4E; + pub const decedi = 0x4F; + pub const pusheax = 0x50; + pub const pushecx = 0x51; + pub const pushedx = 0x52; + pub const pushebx = 0x53; + pub const pushesp = 0x54; + pub const pushebp = 0x55; + pub const pushesi = 0x56; + pub const pushedi = 0x57; + pub const popeax = 0x58; + pub const popecx = 0x59; + pub const popedx = 0x5A; + pub const popebx = 0x5B; + pub const popesp = 0x5C; + pub const popebp = 0x5D; + pub const popesi = 0x5E; + pub const popedi = 0x5F; + pub const pusha = 0x60; + pub const popa = 0x61; + pub const nop = 0x90; + pub const xchgecxeax = 0x91; + pub const xchgedxeax = 0x92; + pub const xchgebxeax = 0x93; + pub const xchgespeax = 0x94; + pub const xchgebpeax = 0x95; + pub const xchgesieax = 0x96; + pub const xchgedieax = 0x97; + pub const cwde = 0x98; + pub const cdq = 0x99; + pub const wait = 0x9B; + pub const pushf = 0x9C; + pub const popf = 0x9D; + pub const sahf = 0x9E; + pub const lahf = 0x9F; + pub const movs8 = 0xA4; + pub const movsw = 0xA5; + pub const cmps8 = 0xA6; + pub const cmpsw = 0xA7; + pub const stos8 = 0xAA; + pub const stosd = 0xAB; + pub const lods8 = 0xAC; + pub const lodsd = 0xAD; + pub const scas8 = 0xAE; + pub const scasd = 0xAF; + pub const ret = 0xC3; + pub const leave = 0xC9; + pub const retf = 0xCB; + pub const int3 = 0xCC; + pub const into = 0xCE; + pub const xlat = 0xD7; + pub const cmc = 0xF5; + pub const clc = 0xF8; + pub const stc = 0xF9; + pub const cli = 0xFA; + pub const sti = 0xFB; + pub const cld = 0xFC; + pub const std = 0xFD; + }; + pub const Op1I8 = struct { + pub const addali = 0x04; + pub const orali = 0x0C; + pub const adcali = 0x14; + pub const sbbali = 0x1C; + pub const andali = 0x24; + pub const subali = 0x2C; + pub const xorali = 0x34; + pub const cmpali = 0x3C; + pub const pushi8 = 0x6A; + pub const testali = 0xA8; + pub const jo = 0x70; + pub const jno = 0x71; + pub const jb = 0x72; + pub const jnb = 0x73; + pub const jz = 0x74; + pub const jnz = 0x75; + pub const jna = 0x76; + pub const ja = 0x77; + pub const js = 0x78; + pub const jns = 0x79; + pub const jp = 0x7A; + pub const jnp = 0x7B; + pub const jl = 0x7C; + pub const jnl = 0x7D; + pub const jng = 0x7E; + pub const jg = 0x7F; + pub const movali = 0xB0; + pub const movcli = 0xB1; + pub const movdli = 0xB2; + pub const movbli = 0xB3; + pub const movahi = 0xB4; + pub const movchi = 0xB5; + pub const movdhi = 0xB6; + pub const movbhi = 0xB7; + pub const int = 0xCD; + pub const amx = 0xD4; + pub const adx = 0xD5; + pub const loopnz = 0xE0; + pub const loopz = 0xE1; + pub const loop = 0xE2; + pub const jcxz = 0xE3; + pub const jmpi8 = 0xEB; + }; + pub const Op1IW = struct { + pub const addeaxi = 0x05; + pub const oreaxi = 0x0D; + pub const adceaxi = 0x15; + pub const sbbeaxi = 0x1D; + pub const andeaxi = 0x25; + pub const subeaxi = 0x2D; + pub const xoreaxi = 0x35; + pub const cmpeaxi = 0x3D; + pub const pushiw = 0x68; + pub const testeaxi = 0xA9; + pub const moveaxi = 0xB8; + pub const movecxi = 0xB9; + pub const movedxi = 0xBA; + pub const movebxi = 0xBB; + pub const movespi = 0xBC; + pub const movebpi = 0xBD; + pub const movesii = 0xBE; + pub const movedii = 0xBF; + pub const call = 0xE8; + pub const jmpiw = 0xE9; + }; + pub const Op1IWI = struct { + pub const movalii = 0xA0; + pub const moveaxii = 0xA1; + pub const moviial = 0xA2; + pub const moviieax = 0xA3; + }; + pub const Op1I16 = struct { + pub const reti16 = 0xC2; + pub const retfi16 = 0xCA; + }; + pub const Op1Mrm = struct { + pub const addmr8 = 0x00; + pub const addmrw = 0x01; + pub const addrm8 = 0x02; + pub const addrmw = 0x03; + pub const ormr8 = 0x08; + pub const ormrw = 0x09; + pub const orrm8 = 0x0A; + pub const orrmw = 0x0B; + pub const adcmr8 = 0x10; + pub const adcmrw = 0x11; + pub const adcrm8 = 0x12; + pub const adcrmw = 0x13; + pub const sbbmr8 = 0x18; + pub const sbbmrw = 0x19; + pub const sbbrm8 = 0x1A; + pub const sbbrmw = 0x1B; + pub const andmr8 = 0x20; + pub const andmrw = 0x21; + pub const andrm8 = 0x22; + pub const andrmw = 0x23; + pub const submr8 = 0x28; + pub const submrw = 0x29; + pub const subrm8 = 0x2A; + pub const subrmw = 0x2B; + pub const xormr8 = 0x30; + pub const xormrw = 0x31; + pub const xorrm8 = 0x32; + pub const xorrmw = 0x33; + pub const cmpmr8 = 0x38; + pub const cmpmrw = 0x39; + pub const cmprm8 = 0x3A; + pub const cmprmw = 0x3B; + pub const arpl = 0x63; + pub const testmr8 = 0x84; + pub const testmrw = 0x85; + pub const xchgmr8 = 0x86; + pub const xchgmrw = 0x87; + pub const movmr8 = 0x88; + pub const movmrw = 0x89; + pub const movrm8 = 0x8A; + pub const movrmw = 0x8B; + pub const movms = 0x8C; + pub const lea = 0x8D; + pub const movsm = 0x8E; + pub const popm = 0x8F; + pub const shiftm18 = 0xD0; + pub const shiftm1w = 0xD1; + pub const shiftmcl8 = 0xD2; + pub const shiftmclw = 0xD3; + pub const fltblk1 = 0xD8; + pub const fltblk2 = 0xD9; + pub const fltblk3 = 0xDA; + pub const fltblk4 = 0xDB; + pub const fltblk5 = 0xDC; + pub const fltblk6 = 0xDD; + pub const fltblk7 = 0xDE; + pub const fltblk8 = 0xDF; + pub const miscm8 = 0xFE; + pub const miscmw = 0xFF; + }; + pub const Op1MrmI8 = struct { + pub const imulmi8 = 0x6B; + pub const alumi8 = 0x80; + pub const alumi8x = 0x82; + pub const alumi8s = 0x83; + pub const shiftmi8 = 0xC0; + pub const shiftmiw = 0xC1; + pub const movmi8 = 0xC6; + }; + pub const Op1MrmIW = struct { + pub const imulmiw = 0x69; + pub const alumiw = 0x81; + pub const movmiw = 0xC7; + }; + + pub const Op1Extra = struct { + const enter = 0xC8; + const crazy8 = 0xF6; + const crazyw = 0xF7; + }; + + pub const Op2No = struct { + pub const rdtsc = 0x31; + pub const rdpmd = 0x33; + pub const sysenter = 0x34; + pub const pushfs = 0xA0; + pub const popfs = 0xA1; + pub const cpuid = 0xA2; + pub const pushgs = 0xA8; + pub const popgs = 0xA9; + pub const rsm = 0xAA; + pub const bswapeax = 0xC8; + pub const bswapecx = 0xC9; + pub const bswapedx = 0xCA; + pub const bswapebx = 0xCB; + pub const bswapesp = 0xCC; + pub const bswapebp = 0xCD; + pub const bswapesi = 0xCE; + pub const bswapedi = 0xCF; + pub const emms = 0x77; + }; + pub const Op2IW = struct { + pub const joii = 0x80; + pub const jnoii = 0x81; + pub const jbii = 0x82; + pub const jnbii = 0x83; + pub const jzii = 0x84; + pub const jnzii = 0x85; + pub const jnaii = 0x86; + pub const jaii = 0x87; + pub const jsii = 0x88; + pub const jnsii = 0x89; + pub const jpii = 0x8A; + pub const jnpii = 0x8B; + pub const jlii = 0x8C; + pub const jnlii = 0x8D; + pub const jngii = 0x8E; + pub const jgii = 0x8F; + }; + pub const Op2Mrm = struct { + pub const nop = 0x0D; + pub const hints1 = 0x18; + pub const hints2 = 0x19; + pub const hints3 = 0x1A; + pub const hints4 = 0x1B; + pub const hints5 = 0x1C; + pub const hints6 = 0x1D; + pub const hints7 = 0x1E; + pub const hints8 = 0x1F; + pub const cmovo = 0x40; + pub const cmovno = 0x41; + pub const cmovb = 0x42; + pub const cmovnb = 0x43; + pub const cmovz = 0x44; + pub const cmovnz = 0x45; + pub const cmovna = 0x46; + pub const cmova = 0x47; + pub const cmovs = 0x48; + pub const cmovns = 0x49; + pub const cmovp = 0x4A; + pub const cmovnp = 0x4B; + pub const cmovl = 0x4C; + pub const cmovnl = 0x4D; + pub const cmovng = 0x4E; + pub const cmovg = 0x4F; + pub const seto = 0x90; + pub const setno = 0x91; + pub const setb = 0x92; + pub const setnb = 0x93; + pub const setz = 0x94; + pub const setnz = 0x95; + pub const setna = 0x96; + pub const seta = 0x97; + pub const sets = 0x98; + pub const setns = 0x99; + pub const setp = 0x9A; + pub const setnp = 0x9B; + pub const setl = 0x9C; + pub const setnl = 0x9D; + pub const setng = 0x9E; + pub const setg = 0x9F; + pub const btmr = 0xA3; + pub const shldmrcl = 0xA5; + pub const bts = 0xAB; + pub const shrdmrcl = 0xAD; + pub const misc = 0xAE; + pub const imul = 0xAF; + pub const cmpxchg8 = 0xB0; + pub const cmpxchgw = 0xB1; + pub const movzx8 = 0xB6; + pub const movzxw = 0xB7; + pub const popcnt = 0xB8; + pub const btcrm = 0xBB; + pub const bsf = 0xBC; + pub const bsr = 0xBD; + pub const movsx8 = 0xBE; + pub const movsxw = 0xBF; + pub const xaddrm8 = 0xC0; + pub const xaddrmw = 0xC1; + pub const cmpxchg64 = 0xC7; + pub const movrm128 = 0x10; + pub const movmr128 = 0x11; + pub const movlrm = 0x12; + pub const movlmr = 0x13; + pub const unpckl = 0x14; + pub const unpckh = 0x15; + pub const movhrm = 0x16; + pub const movhmr = 0x17; + pub const movarm = 0x28; + pub const movamr = 0x29; + pub const cvtif64 = 0x2A; + pub const movnt = 0x2B; + pub const cvtft64 = 0x2C; + pub const cvtfi64 = 0x2D; + pub const ucomi = 0x2E; + pub const comi = 0x2F; + pub const movmsk = 0x50; + pub const sqrt = 0x51; + pub const rsqrt = 0x52; + pub const rcp = 0x53; + pub const and_ = 0x54; + pub const andn = 0x55; + pub const or_ = 0x56; + pub const xor = 0x57; + pub const add = 0x58; + pub const mul = 0x59; + pub const cvtff128 = 0x5A; + pub const cvtfi128 = 0x5B; + pub const sub = 0x5C; + pub const div = 0x5D; + pub const min = 0x5E; + pub const max = 0x5F; + pub const punpcklbw = 0x60; + pub const punpcklbd = 0x61; + pub const punpckldq = 0x62; + pub const packsswb = 0x63; + pub const pcmpgtb = 0x64; + pub const pcmpgtw = 0x65; + pub const pcmpgtd = 0x66; + pub const packuswb = 0x67; + pub const punpckhbw = 0x68; + pub const punpckhwd = 0x69; + pub const punpckhdq = 0x6A; + pub const packssdw = 0x6B; + pub const punpcklqdq = 0x6C; + pub const punpckhqdq = 0x6D; + pub const movdrm = 0x6E; + pub const movqrm = 0x6F; + pub const pcmpeqb = 0x74; + pub const pcmpeqw = 0x75; + pub const pcmpeqd = 0x76; + pub const movdmr = 0x7E; + pub const movqmr = 0x7F; + pub const movnti = 0xC3; + pub const addsub = 0xD0; + pub const psrlw = 0xD1; + pub const psrld = 0xD2; + pub const psrlq = 0xD3; + pub const paddq = 0xD4; + pub const pmullw = 0xD5; + pub const movqrr = 0xD6; + pub const pmovmskb = 0xD7; + pub const psubusb = 0xD8; + pub const psubusw = 0xD9; + pub const pminub = 0xDA; + pub const pand = 0xDB; + pub const paddusb = 0xDC; + pub const paddusw = 0xDD; + pub const pmaxub = 0xDE; + pub const pandn = 0xDF; + pub const pavgb = 0xE0; + pub const psraw = 0xE1; + pub const psrad = 0xE2; + pub const pavgw = 0xE3; + pub const pmulhuw = 0xE4; + pub const pmulhw = 0xE5; + pub const cvtq = 0xE6; + pub const movntq = 0xE7; + pub const psubsb = 0xE8; + pub const psubsw = 0xE9; + pub const pminsb = 0xEA; + pub const pminsw = 0xEB; + pub const paddsb = 0xEC; + pub const paddsw = 0xED; + pub const pmaxsw = 0xEE; + pub const pxor = 0xEF; + pub const lddqu = 0xF0; + pub const psllw = 0xF1; + pub const pslld = 0xF2; + pub const psllq = 0xF3; + pub const pmuludq = 0xF4; + pub const pmaddwd = 0xF5; + pub const psabdw = 0xF6; + pub const maskmovq = 0xF7; + pub const psubb = 0xF8; + pub const psubw = 0xF9; + pub const psubd = 0xFA; + pub const psubq = 0xFB; + pub const paddb = 0xFC; + pub const paddw = 0xFD; + pub const paddd = 0xFE; + }; + pub const Op2MrmI8 = struct { + pub const shldmri = 0xA4; + pub const shrdmri = 0xAC; + pub const btxmi = 0xBA; + pub const pshuf = 0x70; + pub const pswi = 0x71; + pub const psdi = 0x72; + pub const psqi = 0x73; + pub const cmpsi = 0xC2; + pub const pinsrw = 0xC4; + pub const pextrw = 0xC5; + pub const shuf = 0xC6; + }; + + pub const op2_byte = 0x0F; + pub const op3_1 = 0x38; + pub const op3_2 = 0x3A; + pub const op3dnow = 0x0F; +}; + +// Constructs a ModRM byte +pub fn modrm(mod: u8, reg: u8, rm: u8) u8 { + return mod << 6 | reg << 3 | rm; +} + +fn mrmsib(b: [*]const u8, address_len: usize) usize { + if (address_len == 4 or b[0] & 0xC0 != 0) { + const sib: usize = if (address_len == 4 and b[0] < 0xC0 and (b[0] & 7) == 4) 1 else 0; + if ((b[0] & 0xC0) == 0x40) { + return 2 + sib; + } + if ((b[0] & 0xC0) == 0x00) { + if ((b[0] & 7) != 5) { + if (sib == 1 and (b[1] & 7) == 5) { + return if (b[0] & 0x40 != 0) 3 else 6; + } + return 1 + sib; + } + return 1 + address_len + sib; + } + if ((b[0] & 0xC0) == 0x80) { + return 1 + address_len + sib; + } + } + if (address_len == 2 and (b[0] & 0xC7) == 0x06) { + return 3; + } + return 1; +} + +fn is_field(comptime T: type, byte: u8) bool { + @setEvalBranchQuota(100000); + for (@typeInfo(T).@"struct".decls) |decl| { + const decl_ptr = &@field(T, decl.name); + if (decl_ptr.* == byte) { + return true; + } + } + return false; +} + +pub fn x86_len(address: [*]const u8) !usize { + var b = address; + + var prefix_len: usize = 0; + var operand_len: usize = 4; + var address_len: usize = 4; + + // prefixes + while (prefix_len < 14 and switch (b[0]) { + inline 0x00...0xFF => |byte| blk: { + break :blk comptime is_field(Opcode.Prefixes, byte); + }, + }) : ({ + prefix_len += 1; + b += 1; + }) { + if (b[0] == Opcode.Prefixes.opsz) { + operand_len = 2; + } else if (b[0] == Opcode.Prefixes.adsz) { + address_len = 2; + } + } + + // opcode + return switch (b[0]) { + Opcode.op2_byte => switch (b[1]) { + Opcode.op3_1, + Opcode.op3_2, + Opcode.op3dnow, + => error.UnsupportedInstruction, + inline else => |byte| blk: { + if (comptime is_field(Opcode.Op2No, byte)) { + break :blk prefix_len + 2; + } + if (comptime is_field(Opcode.Op2IW, byte)) { + break :blk prefix_len + 2 + operand_len; + } + if (comptime is_field(Opcode.Op2Mrm, byte)) { + break :blk prefix_len + 2 + mrmsib(b + 2, address_len); + } + if (comptime is_field(Opcode.Op2MrmI8, byte)) { + operand_len = 1; + break :blk prefix_len + 2 + operand_len + mrmsib(b + 2, address_len); + } + break :blk error.UnsupportedInstruction; + }, + }, + inline else => |byte| blk: { + if (comptime is_field(Opcode.Op1No, byte)) { + break :blk prefix_len + 1; + } + if (comptime is_field(Opcode.Op1I8, byte)) { + operand_len = 1; + break :blk prefix_len + 1 + operand_len; + } + if (comptime is_field(Opcode.Op1IW, byte)) { + break :blk prefix_len + 1 + operand_len; + } + if (comptime is_field(Opcode.Op1IWI, byte)) { + break :blk prefix_len + 1 + address_len; + } + if (comptime is_field(Opcode.Op1I16, byte)) { + break :blk prefix_len + 3; + } + if (comptime is_field(Opcode.Op1Mrm, byte)) { + break :blk prefix_len + 1 + mrmsib(b + 1, address_len); + } + if (comptime is_field(Opcode.Op1MrmI8, byte)) { + operand_len = 1; + break :blk prefix_len + 1 + operand_len + mrmsib(b + 1, address_len); + } + if (comptime is_field(Opcode.Op1MrmIW, byte)) { + break :blk prefix_len + 1 + operand_len + mrmsib(b + 1, address_len); + } + if (byte == Opcode.Op1Extra.enter) { + break :blk prefix_len + 4; + } + if (byte == Opcode.Op1Extra.crazy8 or byte == Opcode.Op1Extra.crazyw) { + if (byte == Opcode.Op1Extra.crazy8) { + operand_len = 1; + } + + if ((b[1] & 0x38) >= 0x10) { + operand_len = 0; + } + + break :blk prefix_len + 1 + operand_len + mrmsib(b + 1, address_len); + } + break :blk error.UnsupportedInstruction; + }, + }; +} + +test "Simple x86 instruction lengths" { + const nop = makeHex("90"); + try testing.expectEqual(1, try x86_len(nop.ptr)); + const push_eax = makeHex("50"); + try testing.expectEqual(1, try x86_len(push_eax.ptr)); + const mov_eax = makeHex("B8 78 56 34 12"); + try testing.expectEqual(5, try x86_len(mov_eax.ptr)); + const add_mem_eax = makeHex("00 00"); + try testing.expectEqual(2, try x86_len(add_mem_eax.ptr)); + const mov_ax = makeHex("66 B8 34 12"); + try testing.expectEqual(4, try x86_len(mov_ax.ptr)); + const add_mem_disp32 = makeHex("00 80 78 56 34 12"); + try testing.expectEqual(6, try x86_len(add_mem_disp32.ptr)); + const add_eax_imm = makeHex("05 78 56 34 12"); + try testing.expectEqual(5, try x86_len(add_eax_imm.ptr)); +} + +test "The \"crazy\" instructions should be given correct lengths" { + const test8 = makeHex("F6 05 12 34 56 78 12"); + try testing.expectEqual(7, try x86_len(test8.ptr)); + const test16 = makeHex("66 F7 05 12 34 56 78 12"); + try testing.expectEqual(9, try x86_len(test16.ptr)); + const test32 = makeHex("F7 05 12 34 56 78 12 34 56 78"); + try testing.expectEqual(10, try x86_len(test32.ptr)); + const not8 = makeHex("F6 15 12 34 56 78"); + try testing.expectEqual(6, try x86_len(not8.ptr)); + const not16 = makeHex("66 F7 15 12 34 56 78"); + try testing.expectEqual(7, try x86_len(not16.ptr)); + const not32 = makeHex("F7 15 12 34 56 78"); + try testing.expectEqual(6, try x86_len(not32.ptr)); +} + +test "SIB bytes should be decoded correctly" { + const fstp = makeHex("D9 1C 24"); + try testing.expectEqual(3, try x86_len(fstp.ptr)); +} + +test "mov AL, moff8 instructions should be decoded correctly" { + const mov_moff8_al = makeHex("A2 DA 78 B4 0D"); + try testing.expectEqual(5, try x86_len(mov_moff8_al.ptr)); + const mov_al_moff8 = makeHex("A0 28 DF 5C 66"); + try testing.expectEqual(5, try x86_len(mov_al_moff8.ptr)); +} + +test "16-bit MRM instructions should be decoded correctly" { + const fiadd_off16 = makeHex("67 DA 06 DF 11"); + try testing.expectEqual(5, try x86_len(fiadd_off16.ptr)); + const fld_tword = makeHex("67 DB 2E 99 C4"); + try testing.expectEqual(5, try x86_len(fld_tword.ptr)); + const add_off16_bl = makeHex("67 00 1E F5 BB"); + try testing.expectEqual(5, try x86_len(add_off16_bl.ptr)); +} diff --git a/hook/src/zhook.zig b/hook/src/zhook.zig new file mode 100644 index 0000000..82e184b --- /dev/null +++ b/hook/src/zhook.zig @@ -0,0 +1,9 @@ +pub const x86 = @import("x86.zig"); +pub const mem = @import("mem.zig"); +pub const Hook = @import("Hook.zig"); +pub const HookManager = @import("HookManager.zig"); +pub const utils = @import("utils.zig"); + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..3d40078 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,608 @@ +const std = @import("std"); +const w = std.os.windows; +const hook = @import("hook"); + +const c = @cImport({ + @cInclude("libavcodec/avcodec.h"); + @cInclude("libavformat/avformat.h"); + @cInclude("libswscale/swscale.h"); + @cInclude("libswresample/swresample.h"); + @cInclude("libavutil/opt.h"); +}); + +// for whatever reason, winsock just doesn't export this but libsrt expects it +// so we can just throw it in here +export const in6addr_any: [32]u8 = .{ 0 } ** 32; + +fn virtualFunc(This: type, comptime func_name: []const u8, Params: []const type, RT: type) fn (*This, anytype) RT { + var params: [Params.len + 1]std.builtin.Type.Fn.Param = undefined; + params[0] = .{ .is_generic = false, .is_noalias = false, .type = *This }; + for (params[1..], Params) |*param, Param| param.* = .{ + .is_generic = false, + .is_noalias = false, + .type = Param, + }; + const Fn = *@Type(.{ + .@"fn" = .{ + .is_generic = false, + .is_var_args = false, + .params = ¶ms, + .return_type = RT, + .calling_convention = .{ .x86_thiscall = .{} }, + }, + }); + + return struct { + pub fn func(this: *This, args: anytype) RT { + const vtable: *[*]Fn = @ptrCast(@constCast(this)); + const type_name = comptime name: { + const fqn = @typeName(This); + // find the last . + var iter = std.mem.splitScalar(u8, fqn, '.'); + var shortName: []const u8 = undefined; + while (iter.next()) |seg| shortName = seg; + break :name shortName; + }; + const idx = @field(@field(gamedata, type_name), "vtidx_" ++ func_name); + const target_location = vtable.*[idx]; + std.log.debug("calling {s}::{s} @ 0x{x}", + .{ type_name, func_name, @intFromPtr(target_location) }); + return @call(.auto, target_location, .{this} ++ args); + } + }.func; +} + +const CreateInterfaceFn = *const fn ([*:0]const u8, ?*c_int) callconv(.c) *align(4) anyopaque; + +const GameData = struct { + const HL2PRE20TH = GameData{ + .IVEngineClient = .{ + .vtidx_IsPlayingDemo = 76, + .vtidx_ClientCmd = 7, + .vtidx_Time = 14, + .vtidx_GetAppID = 97, + .iface_name = "VEngineClient014", + }, + .off_S_TransferStereo16 = 0x7fac0, + }; + + IVEngineClient: struct { + vtidx_IsPlayingDemo: usize, + vtidx_Time: usize, + vtidx_GetAppID: usize, + vtidx_ClientCmd: usize, + iface_name: [:0]const u8, + }, + CDemoPlayer: struct { + vtidx_GetDemoFile: usize = 2, + vtidx_GetPlaybackTicks: usize = 3, + vtidx_GetTotalTicks: usize = 4, + vtidx_StartPlayback: usize = 5, + vtidx_IsPlayingBack: usize = 6, + } = .{}, + CEngineAPI: struct { + iface_name: [:0]const u8 = "VENGINE_LAUNCHER_API_VERSION004", + vtidx_SetEngineWindow: usize = 7, + } = .{}, + CEngineVGui: struct { + iface_name: [:0]const u8 = "VEngineVGui001", + vtidx_Init: usize = 3, + } = .{}, + CEngineTool: struct { + iface_name: [:0]const u8 = "VENGINETOOL003", + vtidx_StartMovieRecording: usize = 65, + } = .{}, + // TODO: implement a good way to find this + off_S_TransferStereo16: usize, + CVideoMode_MaterialSystem: struct { + vtidx_WriteMovieFrame: usize = 27, + vtidx_ReadScreenPixels: usize = 30, + } = .{}, +}; + +const IVEngineClient = extern struct { + vt: [*]usize, + const isRecordingDemo = + virtualFunc(IVEngineClient, "IsPlayingDemo", &.{}, bool); + const clientCmd = + virtualFunc(IVEngineClient, "ClientCmd", &.{[*:0]const u8}, void); + const time = virtualFunc(IVEngineClient, "Time", &.{}, f32); + const getAppId = virtualFunc(IVEngineClient, "GetAppID", &.{}, c_int); +}; + +const CDemoPlayer = extern struct { + vt: [*]usize, + const getPlaybackTicks = + virtualFunc(CDemoPlayer, "GetPlaybackTicks", &.{}, c_int); + const getTotalTicks = + virtualFunc(CDemoPlayer, "GetTotalTicks", &.{}, c_int); + const startPlayback = + virtualFunc(CDemoPlayer, "StartPlayback", &.{[*]const u8, bool}, bool); + const isPlayingBack = + virtualFunc(CDemoPlayer, "IsPlayingBack", &.{}, bool); +}; + +const CEngineAPI = extern struct { + vt: [*]usize, +}; + +const CEngineVGui = extern struct { + vt: [*]*const anyopaque, +}; + +const CVideoMode_MaterialSystem = extern struct { + vt: [*]*const anyopaque, + const readScreenPixels = virtualFunc(CVideoMode_MaterialSystem, "ReadScreenPixels", + &.{c_int, c_int, c_int, c_int, *anyopaque, i32}, void); +}; + +const CEngineTool = extern struct { + vt: [*]*const anyopaque, +}; + +const Sample = extern struct { left: i32, right: i32 }; + +var api: struct { + engine_factory: CreateInterfaceFn, + engclient: *IVEngineClient, + demoplayer: *CDemoPlayer, + engineapi: *CEngineAPI, + videomode: *CVideoMode_MaterialSystem, + engvgui: *CEngineVGui, + enginetool: *CEngineTool, + S_TransferStereo16: *const fn(*anyopaque, [*]Sample, u32, u32) callconv(.c) void, + movieinfo: *MovieInfo, + + fn init(this: *@This()) !void { + var engine_dll = try std.DynLib.open("engine"); + defer engine_dll.close(); + + const engine_factory = engine_dll.lookup(CreateInterfaceFn, "CreateInterface").?; + this.engclient = + @ptrCast(engine_factory(gamedata.IVEngineClient.iface_name, null)); + this.engineapi = + @ptrCast(engine_factory(gamedata.CEngineAPI.iface_name, null)); + this.engvgui = + @ptrCast(engine_factory(gamedata.CEngineVGui.iface_name, null)); + this.enginetool = + @ptrCast(engine_factory(gamedata.CEngineTool.iface_name, null)); + // CEngineClient::IsPlayingDemo immediately loads &demoplayer into ECX + const isplayingdemo: [*]u8 = + @ptrFromInt(this.engclient.vt[gamedata.IVEngineClient.vtidx_IsPlayingDemo]); + std.debug.assert(isplayingdemo[0] == hook.x86.Opcode.Op1Mrm.movrmw); + std.debug.assert(isplayingdemo[1] == hook.x86.modrm(0, 1, 5)); + // +2 is the first byte past the mov opcode and modr/m byte + this.demoplayer = @as(*align(1) **CDemoPlayer, @ptrCast(isplayingdemo + 2)).*.*; + std.log.debug("demoplayer: {x}", .{@intFromPtr(this.demoplayer)}); + // CEngineAPI::SetWindow does 2 virtual calls, one to unset the window + // and one to apply the new one. videomode is loaded into ECX for the + // second one + var p: [*]u8 = + @ptrFromInt(this.engineapi.vt[gamedata.CEngineAPI.vtidx_SetEngineWindow]); + var mov_ecx_count: u8 = 0; + while (hook.x86.x86_len(p)) |len| { + if (p[0] == hook.x86.Opcode.Op1Mrm.movrmw and + p[1] == hook.x86.modrm(0, 1, 5)) + mov_ecx_count += 1; + if (mov_ecx_count == 2) { + this.videomode = @as(*align(1) **CVideoMode_MaterialSystem, @ptrCast(p + 2)).*.*; + break; + } + p += len; + } else |err| return err; + std.log.debug("videomode: {x}", .{ @intFromPtr(this.videomode) }); + // CEngineTool::StartMovieRecording first checks if there's currently a + // movie playing. Check first call. + p = @constCast(@ptrCast(this.enginetool.vt[gamedata.CEngineTool.vtidx_StartMovieRecording])); + while (hook.x86.x86_len(p)) |len| { + if (p[0] == hook.x86.Opcode.Op1IW.call) { + const offset = @as(*align(1) usize, @ptrCast(p + 1)).*; + p = @ptrFromInt(@addWithOverflow(@intFromPtr(p + 5), offset)[0]); + break; + } + p += len; + } else |err| return err; + // CL_IsRecordingMovie checks the first byte of the first field of + // movieinfo, which, well, is just the pointer to the whole thing. + std.log.debug("CL_IsRecordingMovie: {*}", .{p}); + std.debug.assert(p[0] == hook.x86.Opcode.Op1MrmI8.alumi8); + std.debug.assert(p[1] == hook.x86.modrm(0, 7, 5)); + this.movieinfo = @as(*align(1) *MovieInfo, @ptrCast(p + 2)).*; + std.log.debug("movieinfo: {x}", .{ @intFromPtr(this.movieinfo) }); + } +} = undefined; + +// example +const gamedata = GameData.HL2PRE20TH; + +const Config = struct { + pub const GameBuild = enum { Hl2Pre20th }; + + build: GameBuild, + width: u16 = 1920, + height: u16 = 1080, + framerate: u9 = 60, + bitrate: u32 = 20000, + mod: []const u8 = "hl2", + video_codec: [:0]const u8 = "libx264", + extraargs: []const u8 = "", + + pub fn readFromFile(path: []const u8, allocator: std.mem.Allocator) !Config { + const cfg = try std.fs.cwd() + .readFileAllocOptions(allocator, path, 4096, null, .@"1", 0); + var diag: std.zon.parse.Diagnostics = .{}; + return std.zon.parse.fromSlice(Config, allocator, cfg, &diag, .{}) catch |err| + switch (err) { + error.ParseZon => { + var stderr = std.fs.File.stderr().writer(&.{}); + try diag.format(&stderr.interface); + return err; + }, + else => return err, + }; + } +}; + +const MovieInfo = extern struct { + name: [256:0]u8, + curframe: c_int, + kind: c_int, + jpeg_quality: c_int, +}; + +var render: struct { + orig_WriteMovieFrame: *const @TypeOf(hook_WriteMovieFrame), + orig_S_TransferStereo16: *const @TypeOf(hook_S_TransferStereo16), + + v: struct { + codec: *const c.AVCodec, + ctx: *c.AVCodecContext, + stream: *c.AVStream, + yuv_frame: *c.AVFrame, + sws: *c.SwsContext, + nextpts: u32, + }, + + a: struct { + codec: *const c.AVCodec, + ctx: *c.AVCodecContext, + swr: *c.SwrContext, + frame: *c.AVFrame, + pcm_frame: *c.AVFrame, + stream: *c.AVStream, + nextpts: u32, + frame_idx: u32, + }, + format: *c.AVFormatContext, + pixels: []Bgr, + + const Bgr = extern struct { b: u8, g: u8, r: u8 }; + + // HEY!! 'this' in this function is NOT the render struct! + fn hook_WriteMovieFrame(this: *CVideoMode_MaterialSystem, + _: *MovieInfo) callconv(.{ .x86_thiscall = .{} }) void { + std.log.debug("frame!", .{}); + // 3: source engine IMAGE_FORMAT_BGR888 + this.readScreenPixels(.{0, 0, config.width, config.height, render.pixels.ptr, 3}); + const stride: c_int = config.width * @sizeOf(Bgr); + // convert to yuv + _ = c.sws_scale(render.v.sws, @ptrCast(&render.pixels.ptr), &stride, 0, + config.height, &render.v.yuv_frame.data, &render.v.yuv_frame.linesize); + + render.v.yuv_frame.pts = render.v.nextpts; render.v.nextpts += 1; + render.v.yuv_frame.time_base = render.v.ctx.time_base; + // 1 'time_base' + render.v.yuv_frame.duration = 1; + + fftest("send a frame for encoding", + c.avcodec_send_frame(render.v.ctx, render.v.yuv_frame)) catch return; + + var pkt: ?*c.AVPacket = c.av_packet_alloc() orelse { + std.log.err("failed to alloc packet!", .{}); + return; + }; + + var r: c_int = 0; + while (true) { + r = c.avcodec_receive_packet(render.v.ctx, pkt); + if (r == c.AVERROR(c.EAGAIN) or r == c.AVERROR(c.AVERROR_EOF)) + break; + fftest("encode a frame", r) catch return; + + pkt.?.stream_index = render.v.stream.index; + c.av_packet_rescale_ts(pkt, render.v.ctx.time_base, render.v.stream.time_base); + fftest("write frame to file", c.av_interleaved_write_frame(render.format, pkt)) catch return; + } + c.av_packet_unref(pkt); + c.av_packet_free(&pkt); + } + + + fn hook_S_TransferStereo16(p: *anyopaque, samples_ptr: [*]Sample, start: u32, end: u32) callconv(.c) void { + const samples = samples_ptr[0..(end - start)/2]; + for (samples) |_| { + //render.a.pcm_frame.data[0][] + } + render.orig_S_TransferStereo16(p, samples_ptr, start, end); + } + + fn fftest(msg: []const u8, v: c_int) !void { + if (v != 0) { + var buf: [256:0]u8 = .{0} ** 256; + _ = c.av_strerror(v, &buf, 256); + std.log.err("failed to {s} ({s})", .{msg, buf[0..:0].ptr}); + return error.FFmpegError; + } + } + + pub fn init(this: *@This(), allocator: std.mem.Allocator) !void { + std.log.info("available video codecs:", .{}); + var i: ?*anyopaque = null; + while (c.av_codec_iterate(&i)) |codec| { + if (codec.*.type == c.AVMEDIA_TYPE_VIDEO) { + std.log.info("\t{s} ", .{ codec.*.name.? }); + } + } + // make sure the game knows we are rendering a video + api.movieinfo.name[0] = 'a'; + // but we don't want it to do anything else + api.movieinfo.kind = 0; + + var r: i32 = 0; + var format: ?*c.AVFormatContext = null; + r = c.avformat_alloc_output_context2(&format, null, null, "test.mp4"); + this.format = format orelse { + std.log.err("couldn't create output context! ({})", .{r}); + return error.FFmpegError; + }; + + // create video codec + this.v.codec = c.avcodec_find_encoder_by_name(config.video_codec) orelse { + std.log.err("could't find video encoder", .{}); + return error.FFmpegError; + }; + this.v.ctx = c.avcodec_alloc_context3(this.v.codec) orelse { + std.log.err("couldn't alloc video codec context", .{}); + return error.FFmpegError; + }; + this.v.ctx.width = config.width; + this.v.ctx.height = config.height; + this.v.ctx.pix_fmt = c.AV_PIX_FMT_YUV420P; + this.v.ctx.time_base = c.av_make_q(1, config.framerate); + this.v.ctx.framerate = c.av_make_q(config.framerate, 1); + this.v.ctx.gop_size = 2 * config.framerate; + this.v.ctx.bit_rate = 1000 * config.bitrate; + //this.v.ctx.rc_max_rate = this.v.ctx.bit_rate; + try fftest("set crf", c.av_opt_set_int(this.v.ctx.priv_data, "crf", 24, 0)); + // TODO: extra encoder arguments + r = c.avcodec_open2(this.v.ctx, this.v.codec, null); + if (r < 0) { + std.log.err("failed to create video encoder ({})", .{ r }); + return error.FFmpegError; + } + + // stream + const stream = c.avformat_new_stream(this.format, this.v.codec); + if (stream == null) { + std.log.err("failed to create video stream", .{}); + return error.FFmpegError; + } + this.v.stream = stream.?; + + r = c.avcodec_parameters_from_context(this.v.stream.codecpar, this.v.ctx); + if (r < 0) { + std.log.err("failed to copy params from video context ({})", .{r}); + return error.FFmpegError; + } + this.v.stream.time_base = this.v.ctx.time_base; + this.v.stream.avg_frame_rate = this.v.ctx.framerate; + + // frame + this.v.yuv_frame = c.av_frame_alloc(); + this.v.yuv_frame.width = config.width; + this.v.yuv_frame.height = config.height; + this.v.yuv_frame.color_range = c.AVCOL_RANGE_JPEG; + this.v.yuv_frame.time_base = this.v.ctx.time_base; + this.v.yuv_frame.format = this.v.ctx.pix_fmt; + + r = c.av_frame_get_buffer(this.v.yuv_frame, 0); + if (r < 0) { + std.log.err("failed to alloc frame buffer ({})", .{r}); + return error.FFmpegError; + } + + // sws + this.v.sws = c.sws_getContext(config.width, config.height, + c.AV_PIX_FMT_BGR24, config.width, config.height, + this.v.yuv_frame.format, 0, null, null, null) orelse { + std.log.err("failed to create swscontext", .{}); + return error.FFmpegError; + }; + + // audio codec + const acodec = c.avcodec_find_encoder(c.AV_CODEC_ID_AAC); + if (acodec == null) { + std.log.err("couldn't find audio encoder", .{}); + return error.FFmpegError; + } + this.a.codec = acodec; + this.a.ctx = c.avcodec_alloc_context3(this.a.codec) orelse { + std.log.err("couldn't create audio encoder context", .{}); + return error.FFmpegError; + }; + this.a.ctx.bit_rate = 256000; + this.a.ctx.sample_fmt = c.AV_SAMPLE_FMT_FLTP; + this.a.ctx.sample_rate = 44100; + this.a.ctx.profile = c.FF_PROFILE_AAC_MAIN; + this.a.ctx.time_base = c.av_make_q(1, 44100); + + this.a.ctx.ch_layout.order = c.AV_CHANNEL_ORDER_NATIVE; + this.a.ctx.ch_layout.nb_channels = 2; + this.a.ctx.ch_layout.u.map = null; // not custom channel order + this.a.ctx.ch_layout.u.mask = c.AV_CH_LAYOUT_STEREO; + this.a.ctx.ch_layout.@"opaque" = null; + + try fftest("open audio codec", c.avcodec_open2(this.a.ctx, this.a.codec, null)); + + // audio resampler + this.a.swr = c.swr_alloc() orelse { + std.log.err("failed to alloc swr context", .{}); + return error.FFmpegError; + }; + + try fftest("set audio sample rate", c.av_opt_set_int(this.a.swr, "in_sample_rate", 44100, 0)); + try fftest("set audio sample format", c.av_opt_set_sample_fmt(this.a.swr, "in_sample_fmt", c.AV_SAMPLE_FMT_S16P, 0)); + try fftest("set audio channel layout", c.av_opt_set_chlayout(this.a.swr, "in_chlayout", &this.a.ctx.ch_layout, 0)); + try fftest("set out sample rate", c.av_opt_set_int(this.a.swr, "out_sample_rate", this.a.ctx.sample_rate, 0)); + try fftest("set out sample format", c.av_opt_set_sample_fmt(this.a.swr, "out_sample_fmt", this.a.ctx.sample_fmt, 0)); + try fftest("set out channel layout", c.av_opt_set_chlayout(this.a.swr, "out_chlayout", &this.a.ctx.ch_layout, 0)); + + r = c.swr_init(this.a.swr); + if (r < 0) { + std.log.err("couldn't start audio resampler ({})", .{r}); + return error.FFmpegError; + } + + const frame = c.av_frame_alloc(); + this.a.frame = frame orelse { + std.log.err("failed to alloc audio frame", .{}); + return error.FFmpegError; + }; + this.a.frame.nb_samples = this.a.ctx.frame_size; + this.a.frame.format = this.a.ctx.sample_fmt; + this.a.frame.ch_layout = this.a.ctx.ch_layout; + r = c.av_frame_get_buffer(this.a.frame, 0); + if (r < 0) { + std.log.err("couldn't alloc frame buffer", .{}); + return error.FFmpegError; + } + + std.log.info("Audio frame nb_samples: {}", .{this.a.frame.nb_samples}); + + const astream = c.avformat_new_stream(this.format, null); + if (astream == null) { + std.log.err("failed to create audio stream", .{}); + return error.FFmpegError; + } + this.a.stream = astream.?; + try fftest("set stream parameters from ctx", + c.avcodec_parameters_from_context(this.a.stream.codecpar, this.a.ctx)); + this.a.stream.time_base = this.a.ctx.time_base; + + r = c.avcodec_open2(this.a.ctx, this.a.codec, null); + if (r < 0) { + std.log.err("couldn't open audio codec", .{}); + return error.FFmpegError; + } + + try fftest("open container file", c.avio_open(&this.format.*.pb, "test.mp4", c.AVIO_FLAG_WRITE)); + try fftest("write header", c.avformat_write_header(this.format, null)); + + this.v.nextpts = 0; + this.a.nextpts = 0; + this.a.frame_idx = 0; + const width_big: usize = config.width; + const height_big: usize = config.height; + this.pixels = try allocator.alloc(Bgr, width_big * height_big); + + //this.orig_WriteMovieFrame = try hookman.hookVMT( + // api.videomode.vt, + // gamedata.CVideoMode_MaterialSystem.vtidx_WriteMovieFrame, + // &hook_WriteMovieFrame + //); + //this.orig_S_TransferStereo16 = + // try hookman.hookDetour(api.S_TransferStereo16, &hook_S_TransferStereo16); + } +} = undefined; + +extern "kernel32" fn SetDllDirectoryA(lpPathName: [*]const u8) callconv(.winapi) c_int; + +var orig_Init: *const @TypeOf(hook_Init) = undefined; +fn hook_Init(this: *CEngineVGui) callconv(.{ .x86_thiscall = .{} }) void { + orig_Init(this); + api.init() catch |err| { + std.log.err("failed to init api! {}", .{err}); + std.process.exit(1); + }; + render.init(gpa.allocator()) catch |err| { + std.log.err("failed to init render! {}", .{err}); + std.process.exit(1); + }; + api.engclient.clientCmd(.{"sv_cheats 1"}); + api.engclient.clientCmd(.{"host_framerate 60"}); + // _ = api.demoplayer.startPlayback(.{"test.dem", false}); +} + +var hooked_engine = false; +var orig_LoadLibraryExA: *const @TypeOf(hook_LoadLibraryExA) = undefined; +fn hook_LoadLibraryExA(filename: [*:0]const u8, + handle: w.HANDLE, flags: u32) callconv(.winapi) ?w.HMODULE { + const ret = orig_LoadLibraryExA(filename, handle, flags) + orelse return null; // we only want to log things that actually load + // get the base name of the module + var iter = std.mem.splitScalar(u8, std.mem.span(filename), '\\'); + var shortname: []const u8 = undefined; + while (iter.next()) |seg| shortname = seg; + shortname = shortname[0 .. shortname.len - 4]; // cut off ".dll" + std.log.info("LoadLibrary: {s}", .{shortname}); + + // have a guard, the game likes to try and load engine multiple times and we + // don't want to try and double hook + if (std.mem.eql(u8, shortname, "engine") and !hooked_engine) { + var eng = std.DynLib { .inner = .{ .dll = ret } }; + const factory = eng.lookup(CreateInterfaceFn, "CreateInterface").?; + const engvgui: *CEngineVGui = + @ptrCast(factory(gamedata.CEngineVGui.iface_name, null)); + orig_Init = hookman.hookVMT(engvgui.vt, + gamedata.CEngineVGui.vtidx_Init, &hook_Init) catch return ret; + // since we already have the engine dll base now, just grab the sound + // offsets we need + const base = @intFromPtr(ret); + api.S_TransferStereo16 = + @ptrFromInt(base + gamedata.off_S_TransferStereo16); + hooked_engine = true; + } + + return ret; +} + +var cmdline: [128:0]u8 = .{0} ** 128; +fn hook_GetCommandLineA() callconv(.winapi) [*:0]const u8 { + return &cmdline; +} + +var hookman: hook.HookManager = undefined; +var config: Config = undefined; +var gpa: std.heap.GeneralPurposeAllocator(.{}) = undefined; +pub fn main() !void { + gpa = std.heap.GeneralPurposeAllocator(.{}).init; + + // cd to the exe + var buf: [4096]u8 = undefined; + const goalcwd = try std.fs.selfExeDirPath(&buf); + std.log.info("cd to {s}", .{goalcwd}); + try (try std.fs.openDirAbsolute(goalcwd, .{})).setAsCwd(); + + config = try Config.readFromFile("config.zon", gpa.allocator()); + std.log.info("{}", .{config}); + + _ = try std.fmt.bufPrint(&cmdline, + "hl2.exe -game {s} -w {} -h {} -window -novid -console {s}", + .{ config.mod, config.width, config.height, config.extraargs }); + + if (SetDllDirectoryA("bin/") == 0) return error.SetDllDirectoryFailed; + + hookman = try hook.HookManager.init(gpa.allocator()); + defer _ = hookman.deinit(); + + // hook loadlibrary so we know when the game is fully loaded + orig_LoadLibraryExA = + try hookman.hookSymbol("kernel32", "LoadLibraryExA", &hook_LoadLibraryExA); + // easy way to pass the game stuff like resolution, etc + _ = try hookman.hookSymbol("kernel32", "GetCommandLineA", &hook_GetCommandLineA); + + var launcher = try std.DynLib.open("bin/launcher.dll"); + const launcherMain = + launcher.lookup(*const fn (?*anyopaque, ?*anyopaque) callconv(.c) c_int, "LauncherMain"); + _ = launcherMain.?(null, null); +} diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..9afb8de --- /dev/null +++ b/src/root.zig @@ -0,0 +1,24 @@ +//! By convention, root.zig is the root source file when making a library. +const std = @import("std"); + +pub fn bufferedPrint() !void { + // Stdout is for the actual output of your application, for example if you + // are implementing gzip, then only the compressed bytes should be sent to + // stdout, not any debugging messages. + const stdout_file = std.fs.File.stdout().deprecatedWriter(); + // Buffering can improve performance significantly in print-heavy programs. + var bw = std.io.bufferedWriter(stdout_file); + const stdout = bw.writer(); + + try stdout.print("Run `zig build test` to run the tests.\n", .{}); + + try bw.flush(); // Don't forget to flush! +} + +pub fn add(a: i32, b: i32) i32 { + return a + b; +} + +test "basic add functionality" { + try std.testing.expect(add(3, 7) == 10); +} |