Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 30 additions & 14 deletions Hauyne.Bootstrap/bootstrap.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 '/';
Expand Down Expand Up @@ -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;
};
};

Expand All @@ -109,42 +109,42 @@ 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);

var ctx: t.HostfxrHandle = null;
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);

var load_asm_ptr: ?*anyopaque = null;
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.?));

Expand All @@ -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 {
Expand All @@ -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;
}
};
Expand Down
4 changes: 4 additions & 0 deletions Hauyne.Bootstrap/types/linux.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
44 changes: 29 additions & 15 deletions Hauyne.Injector/injector.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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 => {},
Expand All @@ -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;
Expand Down
34 changes: 32 additions & 2 deletions Hauyne.Injector/linux/linux.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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");
Expand Down Expand Up @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions Hauyne.Injector/linux/procfs.zig
Original file line number Diff line number Diff line change
@@ -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];
}
12 changes: 9 additions & 3 deletions Hauyne.Injector/linux/shim.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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;
}
15 changes: 2 additions & 13 deletions Hauyne.Injector/linux/symbols.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand Down Expand Up @@ -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];
}
Loading
Loading