diff --git a/Hauyne.Bootstrap/bootstrap.zig b/Hauyne.Bootstrap/bootstrap.zig index d2aa1a2..0930868 100644 --- a/Hauyne.Bootstrap/bootstrap.zig +++ b/Hauyne.Bootstrap/bootstrap.zig @@ -55,10 +55,10 @@ fn ownModulePath(buf: []t.CharT) ?[]const t.CharT { return info.dli_fname[0..p.charTLen(info.dli_fname)]; } -fn loadPayload(param: ?*anyopaque) void { +fn loadPayload(param: ?*anyopaque) bool { if (!loadHostfxr()) { p.appendLog("hauyne.log", "hauyne: load_hostfxr failed"); - return; + return false; } const sep: t.CharT = if (t.is_windows) '\\' else '/'; @@ -90,7 +90,7 @@ fn loadPayload(param: ?*anyopaque) void { if (assembly_opt) |ap| break :blk ap[0..p.charTLen(ap)]; break :blk ownModulePath(&source_buf) orelse { p.appendLog("hauyne.log", "hauyne: ownModulePath failed"); - return; + return false; }; }; @@ -109,12 +109,12 @@ fn loadPayload(param: ?*anyopaque) void { if (assembly_path == null) { p.appendLog(log_path_u8, "hauyne: failed to determine payload path"); - return; + return false; } const config_path = rc_mod.synthesize(&config_buf) orelse { p.appendLog(log_path_u8, "hauyne: synthesize runtimeconfig failed"); - return; + return false; }; defer rc_mod.unlink(config_path); @@ -122,7 +122,7 @@ fn loadPayload(param: ?*anyopaque) void { var rc = hostfxr_init.?(config_path, null, &ctx); if (ctx == null) { logRc(log_path_u8, "hauyne: hostfxr_init failed, ctx is null", rc); - return; + return false; } defer _ = hostfxr_close.?(ctx); @@ -130,21 +130,21 @@ fn loadPayload(param: ?*anyopaque) void { rc = hostfxr_get_delegate.?(ctx, t.HDT_LOAD_ASSEMBLY, &load_asm_ptr); if (rc != 0 or load_asm_ptr == null) { logRc(log_path_u8, "hauyne: get_delegate(load_assembly) failed", rc); - return; + return false; } const load_asm: t.LoadAssemblyFn = @ptrCast(@alignCast(load_asm_ptr.?)); rc = load_asm(assembly_path.?, null, null); if (rc != 0) { logRc(log_path_u8, "hauyne: load_asm failed", rc); - return; + return false; } var get_fn_ptr: ?*anyopaque = null; rc = hostfxr_get_delegate.?(ctx, t.HDT_GET_FUNCTION_POINTER, &get_fn_ptr); if (rc != 0 or get_fn_ptr == null) { logRc(log_path_u8, "hauyne: get_delegate(get_function_pointer) failed", rc); - return; + return false; } const get_fn: t.GetFunctionPointerFn = @ptrCast(@alignCast(get_fn_ptr.?)); @@ -159,12 +159,13 @@ fn loadPayload(param: ?*anyopaque) void { ); if (rc != 0 or entry_ptr == null) { logRc(log_path_u8, "hauyne: get_function_pointer(Initialize) failed", rc); - return; + return false; } const entry: t.EntryPointFn = @ptrCast(@alignCast(entry_ptr.?)); entry(); p.appendLog(log_path_u8, "hauyne: payload loaded ok"); + return true; } const platform_entry = if (t.is_windows) struct { @@ -178,13 +179,28 @@ const platform_entry = if (t.is_windows) struct { } pub export fn hauyne_start(param: ?*anyopaque) callconv(t.CC) win.DWORD { - loadPayload(param); - if (g_hModule) |hmod| win.FreeLibraryAndExitThread(hmod, 0); - return 0; + const ok = loadPayload(param); + const code: win.DWORD = if (ok) 0 else 1; + if (g_hModule) |hmod| win.FreeLibraryAndExitThread(hmod, code); + return code; } } else struct { + const Header = extern struct { + payload_ptr: u64, + injector_pid: i32, + }; + pub export fn hauyne_start(param: ?*anyopaque) callconv(t.CC) ?*anyopaque { - loadPayload(param); + if (param) |raw| { + const header: *const Header = @ptrCast(@alignCast(raw)); + const payload: ?*anyopaque = if (header.payload_ptr != 0) @ptrFromInt(header.payload_ptr) else null; + const ok = loadPayload(payload); + if (header.injector_pid > 0) { + _ = lin.kill(header.injector_pid, if (ok) lin.SIGUSR1 else lin.SIGUSR2); + } + } else { + _ = loadPayload(null); + } return null; } }; diff --git a/Hauyne.Bootstrap/types/linux.zig b/Hauyne.Bootstrap/types/linux.zig index e5e308d..3ff21eb 100644 --- a/Hauyne.Bootstrap/types/linux.zig +++ b/Hauyne.Bootstrap/types/linux.zig @@ -24,3 +24,7 @@ pub extern "c" fn dlclose(handle: ?*anyopaque) c_int; pub extern "c" fn dladdr(addr: ?*anyopaque, info: *DlInfo) c_int; pub extern "c" fn pthread_create(thread: *std.c.pthread_t, attr: ?*anyopaque, start_routine: *const fn (?*anyopaque) callconv(CC) ?*anyopaque, arg: ?*anyopaque) c_int; pub extern "c" fn pthread_detach(thread: std.c.pthread_t) c_int; +pub extern "c" fn kill(pid: c_int, sig: c_int) c_int; + +pub const SIGUSR1: c_int = 10; +pub const SIGUSR2: c_int = 12; diff --git a/Hauyne.Injector/injector.zig b/Hauyne.Injector/injector.zig index 68b71f9..96d8107 100644 --- a/Hauyne.Injector/injector.zig +++ b/Hauyne.Injector/injector.zig @@ -90,16 +90,26 @@ pub fn main(init: std.process.Init) u8 { if (is_windows) { const windows = @import("windows.zig"); - windows.inject(allocator, @intCast(pid), bootstrap_path, payload_path, type_name, method_name) catch |err| { + const payload_ok = windows.inject(allocator, @intCast(pid), bootstrap_path, payload_path, type_name, method_name) catch |err| { std.debug.print("Injection failed: {}\n", .{err}); return 1; }; + if (!payload_ok) { + std.debug.print("Injected into PID {d}, but payload failed to load\n", .{pid}); + printLastLogLine(allocator, bootstrap_path); + return 1; + } } else if (builtin.os.tag == .linux) { const linux = @import("linux/linux.zig"); - linux.inject(io, allocator, @intCast(pid), bootstrap_path, payload_path, type_name, method_name) catch |err| { + const payload_ok = linux.inject(io, allocator, @intCast(pid), bootstrap_path, payload_path, type_name, method_name) catch |err| { std.debug.print("Injection failed: {}\n", .{err}); return 1; }; + if (!payload_ok) { + std.debug.print("Injected into PID {d}, but payload failed to load\n", .{pid}); + printLastLogLine(allocator, bootstrap_path); + return 1; + } } else { std.debug.print("Unsupported platform\n", .{}); return 1; @@ -109,6 +119,21 @@ pub fn main(init: std.process.Init) u8 { return 0; } +const fseek = @extern(*const fn (*std.c.FILE, c_long, c_int) callconv(.c) c_int, .{ .name = "fseek" }); + +fn printLastLogLine(allocator: std.mem.Allocator, bootstrap_path: []const u8) void { + const dir = std.fs.path.dirname(bootstrap_path) orelse "."; + const log_path_z = std.fs.path.joinZ(allocator, &.{ dir, "hauyne.log" }) catch return; + const fp = std.c.fopen(log_path_z, "r") orelse return; + defer _ = std.c.fclose(fp); + _ = fseek(fp, -256, 2); + var buf: [256]u8 = undefined; + const n = std.c.fread(&buf, 1, buf.len, fp); + const text = std.mem.trimEnd(u8, buf[0..n], "\n"); + const last = if (std.mem.lastIndexOfScalar(u8, text, '\n')) |i| text[i + 1 ..] else text; + if (last.len > 0) std.debug.print(" {s}\n", .{last}); +} + fn resolveTarget(io: std.Io, allocator: std.mem.Allocator, spec: []const u8) !u32 { var inaccessible: usize = 0; @@ -319,7 +344,8 @@ fn isDotNetProcessLinux(io: std.Io, allocator: std.mem.Allocator, pid: u32, inac const maps_path = try std.fmt.allocPrint(allocator, "/proc/{}/maps", .{pid}); defer allocator.free(maps_path); - const data = readProcFileAlloc(allocator, maps_path) catch |err| { + const procfs = @import("linux/procfs.zig"); + const data = procfs.readFileAlloc(allocator, maps_path) catch |err| { switch (err) { error.AccessDenied, error.PermissionDenied => inaccessible.* += 1, else => {}, @@ -335,18 +361,6 @@ fn isDotNetProcessLinux(io: std.Io, allocator: std.mem.Allocator, pid: u32, inac return false; } -fn readProcFileAlloc(allocator: std.mem.Allocator, path: []const u8) ![]u8 { - const fd = try std.posix.openat(std.posix.AT.FDCWD, path, .{ .ACCMODE = .RDONLY }, 0); - defer _ = std.c.close(fd); - var buf = try allocator.alloc(u8, 256 * 1024); - var n: usize = 0; - while (n < buf.len) { - const r = try std.posix.read(fd, buf[n..]); - if (r == 0) break; - n += r; - } - return buf[0..n]; -} fn isDotNetProcessWindows(pid: u32) bool { const windows = std.os.windows; diff --git a/Hauyne.Injector/linux/linux.zig b/Hauyne.Injector/linux/linux.zig index 950d5b0..f1cf4e2 100644 --- a/Hauyne.Injector/linux/linux.zig +++ b/Hauyne.Injector/linux/linux.zig @@ -28,6 +28,22 @@ const MAP_ANONYMOUS: u64 = 0x20; var debug: bool = false; +const SIG_BLOCK: c_int = 0; +const SIG_UNBLOCK: c_int = 1; +const SIGUSR1: c_int = 10; +const SIGUSR2: c_int = 12; + +const sigset_t = extern struct { val: [32]u32 = .{0} ** 32 }; +const timespec_t = extern struct { sec: c_long, nsec: c_long }; + +extern "c" fn sigprocmask(how: c_int, set: *const sigset_t, oldset: ?*sigset_t) callconv(.c) c_int; +extern "c" fn sigtimedwait(set: *const sigset_t, info: ?*anyopaque, timeout: *const timespec_t) callconv(.c) c_int; + +fn sigaddset(set: *sigset_t, sig: c_int) void { + const s: u32 = @intCast(sig - 1); + set.val[s / 32] |= @as(u32, 1) << @intCast(s % 32); +} + pub fn inject( io: std.Io, allocator: std.mem.Allocator, @@ -36,12 +52,17 @@ pub fn inject( payload_path: ?[]const u8, type_name: ?[]const u8, method_name: ?[]const u8, -) !void { +) !bool { debug = blk: { const val = std.c.getenv("HAUYNE_DEBUG") orelse break :blk false; break :blk val[0] == '1'; }; + var mask = sigset_t{}; + sigaddset(&mask, SIGUSR1); + sigaddset(&mask, SIGUSR2); + _ = sigprocmask(SIG_BLOCK, &mask, null); + const victim = try victim_mod.pickVictimThread(io, allocator, tgid); const dlopen_addr = try symbols.findSymbolInTarget(io, allocator, tgid, "dlopen"); @@ -81,12 +102,21 @@ pub fn inject( const scratch = try bootstrapMmap(victim, saved); if (debug) std.debug.print("[hauyne] scratch=0x{x}\n", .{scratch}); - var page = shim.buildScratchPage(so_path, payload_path, type_name, method_name, dlopen_addr, dlsym_addr, pthread_create_addr, pthread_detach_addr, scratch); + const self_pid: i32 = @intCast(std.posix.system.getpid()); + var page = shim.buildScratchPage(so_path, payload_path, type_name, method_name, dlopen_addr, dlsym_addr, pthread_create_addr, pthread_detach_addr, scratch, self_pid); try ptrace_mod.writeMemory(victim, scratch, &page); try runVictimShim(victim, saved, scratch + shim.VictimShimOff); try ptrace_mod.setRegs(victim, saved); + + const timeout = timespec_t{ .sec = 5, .nsec = 0 }; + const sig = sigtimedwait(&mask, null, &timeout); + _ = sigprocmask(SIG_UNBLOCK, &mask, null); + + if (sig == SIGUSR1) return true; + if (sig == SIGUSR2) return false; + return error.BootstrapTimeout; } fn bootstrapMmap(pid: i32, saved: UserRegsStruct) !usize { diff --git a/Hauyne.Injector/linux/procfs.zig b/Hauyne.Injector/linux/procfs.zig new file mode 100644 index 0000000..05d0c60 --- /dev/null +++ b/Hauyne.Injector/linux/procfs.zig @@ -0,0 +1,21 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the +// Mozilla Public License, v. 2.0. + +const std = @import("std"); + +pub fn readFileAlloc(allocator: std.mem.Allocator, path: []const u8) ![]u8 { + const fd = try std.posix.openat(std.posix.AT.FDCWD, path, .{ .ACCMODE = .RDONLY }, 0); + defer _ = std.c.close(fd); + var buf = try allocator.alloc(u8, 4096); + var n: usize = 0; + while (true) { + const r = try std.posix.read(fd, buf[n..]); + if (r == 0) break; + n += r; + if (n == buf.len) buf = try allocator.realloc(buf, buf.len * 2); + } + return buf[0..n]; +} diff --git a/Hauyne.Injector/linux/shim.zig b/Hauyne.Injector/linux/shim.zig index c6349be..028f53d 100644 --- a/Hauyne.Injector/linux/shim.zig +++ b/Hauyne.Injector/linux/shim.zig @@ -30,6 +30,7 @@ pub fn buildScratchPage( pthread_create_addr: usize, pthread_detach_addr: usize, scratch_base: usize, + injector_pid: i32, ) [ScratchSize]u8 { var page = std.mem.zeroes([ScratchSize]u8); @@ -63,13 +64,18 @@ pub fn buildScratchPage( const any_custom = payload_path != null or type_name != null or method_name != null; - const pthread_handle_addr: u64 = @intCast(scratch_base); + // Header at offset 0x00: { payload_ptr: u64, injector_pid: i32 } + const payload_ptr: u64 = if (any_custom) @intCast(scratch_base + PayloadOffset) else 0; + std.mem.writeInt(u64, page[0..8], payload_ptr, .little); + std.mem.writeInt(i32, page[8..12], injector_pid, .little); + + const pthread_handle_addr: u64 = @intCast(scratch_base + 16); const path_addr: u64 = @intCast(scratch_base + PathOffset); - const payload_addr: u64 = if (any_custom) @intCast(scratch_base + PayloadOffset) else 0; + const hauyne_arg: u64 = @intCast(scratch_base); const symbol_addr: u64 = @intCast(scratch_base + SymbolOffset); const payload_shim_addr: u64 = @intCast(scratch_base + PayloadShimOff); - arch.emit(&page, pthread_handle_addr, path_addr, payload_addr, symbol_addr, payload_shim_addr, dlopen_addr, dlsym_addr, pthread_create_addr, pthread_detach_addr); + arch.emit(&page, pthread_handle_addr, path_addr, hauyne_arg, symbol_addr, payload_shim_addr, dlopen_addr, dlsym_addr, pthread_create_addr, pthread_detach_addr); return page; } diff --git a/Hauyne.Injector/linux/symbols.zig b/Hauyne.Injector/linux/symbols.zig index f0174ad..b82510d 100644 --- a/Hauyne.Injector/linux/symbols.zig +++ b/Hauyne.Injector/linux/symbols.zig @@ -20,7 +20,8 @@ pub fn findSymbolInTarget(io: std.Io, allocator: std.mem.Allocator, pid: i32, sy const maps_path = try std.fmt.allocPrint(allocator, "/proc/{d}/maps", .{pid}); defer allocator.free(maps_path); - const maps_text = try readProcFileAlloc(allocator, maps_path); + const procfs = @import("procfs.zig"); + const maps_text = try procfs.readFileAlloc(allocator, maps_path); var lines = std.mem.splitScalar(u8, maps_text, '\n'); while (lines.next()) |line| { @@ -139,15 +140,3 @@ fn parseMapsRow(line: []const u8) ?MapsRow { return .{ .start = start, .offset = offset_str, .path = path_str }; } -fn readProcFileAlloc(allocator: std.mem.Allocator, path: []const u8) ![]u8 { - const fd = try std.posix.openat(std.posix.AT.FDCWD, path, .{ .ACCMODE = .RDONLY }, 0); - defer _ = std.c.close(fd); - var buf = try allocator.alloc(u8, 256 * 1024); - var n: usize = 0; - while (n < buf.len) { - const r = try std.posix.read(fd, buf[n..]); - if (r == 0) break; - n += r; - } - return buf[0..n]; -} diff --git a/Hauyne.Injector/linux/victim.zig b/Hauyne.Injector/linux/victim.zig index d4d1f76..429c5cb 100644 --- a/Hauyne.Injector/linux/victim.zig +++ b/Hauyne.Injector/linux/victim.zig @@ -31,7 +31,8 @@ pub fn pickVictimThread(io: std.Io, allocator: std.mem.Allocator, tgid: i32) !i3 const syscall_path = std.fmt.allocPrint(allocator, "/proc/{d}/task/{d}/syscall", .{ tgid, tid }) catch continue; defer allocator.free(syscall_path); - const syscall_text = readProcFileAlloc(allocator, syscall_path) catch continue; + const procfs = @import("procfs.zig"); + const syscall_text = procfs.readFileAlloc(allocator, syscall_path) catch continue; const trimmed = std.mem.trimEnd(u8, syscall_text, "\n\r \t"); if (std.mem.eql(u8, trimmed, "running")) continue; @@ -49,33 +50,16 @@ pub fn pickVictimThread(io: std.Io, allocator: std.mem.Allocator, tgid: i32) !i3 } if (!found_idle) continue; - const status_path = std.fmt.allocPrint(allocator, "/proc/{d}/task/{d}/status", .{ tgid, tid }) catch continue; - defer allocator.free(status_path); + const stat_path = std.fmt.allocPrint(allocator, "/proc/{d}/task/{d}/stat", .{ tgid, tid }) catch continue; + defer allocator.free(stat_path); - const status_text = readProcFileAlloc(allocator, status_path) catch continue; + const stat_text = procfs.readFileAlloc(allocator, stat_path) catch continue; - var lines = std.mem.splitScalar(u8, status_text, '\n'); - while (lines.next()) |line| { - if (!std.mem.startsWith(u8, line, "State:")) continue; - if (std.mem.indexOf(u8, line, "S (sleeping)") != null or - std.mem.indexOf(u8, line, "D (disk sleep)") != null) - return tid; - break; - } + const last_paren = std.mem.lastIndexOfScalar(u8, stat_text, ')') orelse continue; + const rest = stat_text[last_paren + 1 ..]; + const state = std.mem.trimStart(u8, rest, " "); + if (state.len > 0 and state[0] == 'S') return tid; } return tgid; } - -fn readProcFileAlloc(allocator: std.mem.Allocator, path: []const u8) ![]u8 { - const fd = try std.posix.openat(std.posix.AT.FDCWD, path, .{ .ACCMODE = .RDONLY }, 0); - defer _ = std.c.close(fd); - var buf = try allocator.alloc(u8, 4096); - var n: usize = 0; - while (n < buf.len) { - const r = try std.posix.read(fd, buf[n..]); - if (r == 0) break; - n += r; - } - return buf[0..n]; -} diff --git a/Hauyne.Injector/windows.zig b/Hauyne.Injector/windows.zig index 5f46cf1..5f426ab 100644 --- a/Hauyne.Injector/windows.zig +++ b/Hauyne.Injector/windows.zig @@ -25,6 +25,7 @@ extern "kernel32" fn GetModuleHandleW(lpModuleName: ?[*:0]const u16) callconv(CC extern "kernel32" fn GetProcAddress(hModule: HMODULE, lpProcName: [*:0]const u8) callconv(CC) ?*anyopaque; extern "kernel32" fn LoadLibraryW(lpLibFileName: [*:0]const u16) callconv(CC) ?HMODULE; extern "kernel32" fn WaitForSingleObject(hHandle: HANDLE, dwMilliseconds: DWORD) callconv(CC) DWORD; +extern "kernel32" fn GetExitCodeThread(hThread: HANDLE, lpExitCode: *DWORD) callconv(CC) BOOL; extern "kernel32" fn CloseHandle(hObject: HANDLE) callconv(CC) BOOL; extern "kernel32" fn CreateToolhelp32Snapshot(dwFlags: DWORD, th32ProcessID: DWORD) callconv(CC) HANDLE; extern "kernel32" fn Module32FirstW(hSnapshot: HANDLE, lpme: *MODULEENTRY32W) callconv(CC) BOOL; @@ -41,6 +42,7 @@ const MEM_RESERVE: DWORD = 0x2000; const MEM_RELEASE: DWORD = 0x8000; const PAGE_READWRITE: DWORD = 0x04; +const WAIT_TIMEOUT: DWORD = 0x102; const TH32CS_SNAPMODULE: DWORD = 0x00000008; @@ -64,7 +66,7 @@ pub fn inject( payload_path: ?[]const u8, type_name: ?[]const u8, method_name: ?[]const u8, -) !void { +) !bool { const path_utf16 = try std.unicode.utf8ToUtf16LeAllocZ(allocator, dll_path); defer allocator.free(path_utf16); @@ -116,7 +118,8 @@ pub fn inject( const load_thread = CreateRemoteThread(hProcess, null, 0, load_library_addr, bs_remote, 0, null) orelse return error.CreateRemoteThreadFailed; - _ = WaitForSingleObject(load_thread, 5000); + if (WaitForSingleObject(load_thread, 5000) == WAIT_TIMEOUT) + return error.LoadLibraryTimeout; _ = CloseHandle(load_thread); const target_base = try findBootstrapBaseInTarget(pid, dll_path); @@ -130,7 +133,12 @@ pub fn inject( return error.CreateRemoteThreadFailed; defer _ = CloseHandle(call_thread); - _ = WaitForSingleObject(call_thread, 5000); + if (WaitForSingleObject(call_thread, 5000) == WAIT_TIMEOUT) + return error.PayloadTimeout; + + var exit_code: DWORD = 0; + _ = GetExitCodeThread(call_thread, &exit_code); + return exit_code == 0; } fn buildTripleUtf16(