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
7 changes: 6 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ jobs:
version: 0.16.0

- name: Build
run: ./build.sh --zig-targets "${{ matrix.zig-target }}" --no-dotnet
run: >-
zig build
-Dtarget=${{ matrix.zig-target }}
-Doptimize=ReleaseFast
-Dinjector-optimize=ReleaseSmall
--prefix bin

- name: Build payload
run: dotnet build Hauyne.Payload -c Release -p:TargetFramework=net${{ matrix.dotnet }} --nologo
Expand Down
36 changes: 0 additions & 36 deletions Hauyne.Bootstrap/build.zig

This file was deleted.

34 changes: 0 additions & 34 deletions Hauyne.Injector/build.zig

This file was deleted.

51 changes: 26 additions & 25 deletions Hauyne.Injector/injector.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ const builtin = @import("builtin");

const is_windows = builtin.os.tag == .windows;

const max_matches = 64;

fn println(allocator: std.mem.Allocator, comptime fmt: []const u8, args: anytype) void {
const msg = std.fmt.allocPrint(allocator, fmt, args) catch return;
defer allocator.free(msg);
Expand All @@ -23,7 +21,11 @@ pub fn main(init: std.process.Init) u8 {

const args = init.minimal.args.toSlice(allocator) catch return 1;

if (args.len < 2 or std.mem.eql(u8, args[1], "--help") or std.mem.eql(u8, args[1], "-h")) {
const wants_help = for (args) |a| {
if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) break true;
} else false;

if (args.len < 2 or wants_help) {
const usage =
\\Usage: {s} <process-name|pid> [payload-path] [options]
\\
Expand All @@ -36,7 +38,7 @@ pub fn main(init: std.process.Init) u8 {
\\Options:
\\ --type <name> Fully qualified type name (default: Hauyne.Payload.Entrypoint, Hauyne.Payload)
\\ --method <name> Entry method name (default: Initialize)
\\ -h, --help Hello
\\ -h, --help Show this help
\\
;
println(allocator, usage, .{args[0]});
Expand Down Expand Up @@ -65,6 +67,9 @@ pub fn main(init: std.process.Init) u8 {
return 1;
}
method_name = args[i];
} else if (std.mem.startsWith(u8, a, "--")) {
std.debug.print("Unknown option: {s}\n", .{a});
return 1;
} else if (payload_path == null) {
payload_path = a;
} else {
Expand Down Expand Up @@ -144,11 +149,9 @@ fn resolveTarget(io: std.Io, allocator: std.mem.Allocator, spec: []const u8) !u3
return pid;
} else |_| {}

var matches: [max_matches]u32 = undefined;
var n: usize = 0;
try collectMatches(io, spec, &matches, &n, &inaccessible);
const matches = try collectMatches(io, allocator, spec, &inaccessible);

if (n == 0) {
if (matches.len == 0) {
if (inaccessible > 0) {
std.debug.print("No process matches '{s}' ({d} process(es) unreadable — try root or ptrace_scope=0)\n", .{ spec, inaccessible });
} else {
Expand All @@ -157,19 +160,17 @@ fn resolveTarget(io: std.Io, allocator: std.mem.Allocator, spec: []const u8) !u3
return error.NotFound;
}

// Compact .NET-valid PIDs over the front of `matches`. If vn == 0 no writes
// happen and matches[0..n] stays intact for the "none loaded hostfxr" list.
var vn: usize = 0;
for (matches[0..n]) |pid| {
for (matches) |pid| {
if (isDotNetProcess(io, allocator, pid, &inaccessible) catch false) {
matches[vn] = pid;
vn += 1;
}
}

if (vn == 0) {
std.debug.print("'{s}' matched {d} process(es) but none loaded hostfxr: ", .{ spec, n });
printPidList(matches[0..n]);
std.debug.print("'{s}' matched {d} process(es) but none loaded hostfxr: ", .{ spec, matches.len });
printPidList(matches);
return error.NoDotNetMatch;
}
if (vn > 1) {
Expand All @@ -188,12 +189,13 @@ fn printPidList(pids: []const u32) void {
std.debug.print("\n", .{});
}

fn collectMatches(io: std.Io, name: []const u8, out: []u32, count: *usize, inaccessible: *usize) !void {
if (is_windows) return collectMatchesWindows(name, out, count);
return collectMatchesLinux(io, name, out, count, inaccessible);
fn collectMatches(io: std.Io, allocator: std.mem.Allocator, name: []const u8, inaccessible: *usize) ![]u32 {
if (is_windows) return collectMatchesWindows(allocator, name);
return collectMatchesLinux(io, allocator, name, inaccessible);
}

fn collectMatchesLinux(io: std.Io, name: []const u8, out: []u32, count: *usize, inaccessible: *usize) !void {
fn collectMatchesLinux(io: std.Io, allocator: std.mem.Allocator, name: []const u8, inaccessible: *usize) ![]u32 {
var matches: std.ArrayList(u32) = .empty;
const self_pid: u32 = @intCast(std.posix.system.getpid());

var proc_dir = try std.Io.Dir.openDirAbsolute(io, "/proc", .{ .iterate = true });
Expand All @@ -207,11 +209,10 @@ fn collectMatchesLinux(io: std.Io, name: []const u8, out: []u32, count: *usize,
if (pid == self_pid) continue;

if (pidMatchesName(io, pid, name, inaccessible)) {
if (count.* >= out.len) return;
out[count.*] = pid;
count.* += 1;
try matches.append(allocator, pid);
}
}
return matches.items;
}

fn pidMatchesName(io: std.Io, pid: u32, name: []const u8, inaccessible: *usize) bool {
Expand Down Expand Up @@ -257,7 +258,8 @@ fn nameMatches(candidate: []const u8, name: []const u8) bool {
return false;
}

fn collectMatchesWindows(name: []const u8, out: []u32, count: *usize) !void {
fn collectMatchesWindows(allocator: std.mem.Allocator, name: []const u8) ![]u32 {
var matches: std.ArrayList(u32) = .empty;
const windows = std.os.windows;

const TH32CS_SNAPPROCESS: windows.DWORD = 0x00000002;
Expand Down Expand Up @@ -301,7 +303,7 @@ fn collectMatchesWindows(name: []const u8, out: []u32, count: *usize) !void {
.library_name = "kernel32",
});

if (Process32FirstW(snapshot, &entry) == .FALSE) return;
if (Process32FirstW(snapshot, &entry) == .FALSE) return matches.items;

while (true) {
if (entry.th32ProcessID == self_pid) {
Expand All @@ -320,13 +322,12 @@ fn collectMatchesWindows(name: []const u8, out: []u32, count: *usize) !void {
exe_name;

if (std.ascii.eqlIgnoreCase(stem, name) or std.ascii.eqlIgnoreCase(exe_name, name)) {
if (count.* >= out.len) return;
out[count.*] = entry.th32ProcessID;
count.* += 1;
try matches.append(allocator, entry.th32ProcessID);
}

if (Process32NextW(snapshot, &entry) == .FALSE) break;
}
return matches.items;
}

fn isDotNetProcess(io: std.Io, allocator: std.mem.Allocator, pid: u32, inaccessible: *usize) !bool {
Expand Down
19 changes: 19 additions & 0 deletions Hauyne.Injector/linux/arch.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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 builtin = @import("builtin");

pub const cpu = switch (builtin.cpu.arch) {
.x86_64 => @import("arch/x86_64.zig"),
.aarch64 => @import("arch/aarch64.zig"),
else => @compileError("unsupported architecture"),
};

pub const emitter = switch (builtin.cpu.arch) {
.x86_64 => @import("shim/x86_64.zig"),
.aarch64 => @import("shim/aarch64.zig"),
else => @compileError("unsupported architecture"),
};
15 changes: 6 additions & 9 deletions Hauyne.Injector/linux/linux.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ const shim = @import("shim.zig");
const symbols = @import("symbols.zig");
const victim_mod = @import("victim.zig");

const arch = switch (builtin.cpu.arch) {
.x86_64 => @import("arch/x86_64.zig"),
.aarch64 => @import("arch/aarch64.zig"),
else => @compileError("unsupported architecture"),
};
const arch = @import("arch.zig").cpu;

const UserRegsStruct = ptrace_mod.UserRegsStruct;

Expand Down Expand Up @@ -114,10 +110,11 @@ pub fn inject(

const victim = try victim_mod.pickVictimThread(io, allocator, tgid);

const dlopen_addr = try symbols.findSymbolInTarget(io, allocator, tgid, "dlopen");
const dlsym_addr = try symbols.findSymbolInTarget(io, allocator, tgid, "dlsym");
const pthread_create_addr = try symbols.findSymbolInTarget(io, allocator, tgid, "pthread_create");
const pthread_detach_addr = try symbols.findSymbolInTarget(io, allocator, tgid, "pthread_detach");
const sym_addrs = try symbols.findSymbolsInTarget(io, allocator, tgid, &.{ "dlopen", "dlsym", "pthread_create", "pthread_detach" });
const dlopen_addr = sym_addrs[0];
const dlsym_addr = sym_addrs[1];
const pthread_create_addr = sym_addrs[2];
const pthread_detach_addr = sym_addrs[3];

std.debug.print("[hauyne] victim tid={d} (tgid={d})\n", .{ victim, tgid });
if (debug) {
Expand Down
1 change: 1 addition & 0 deletions Hauyne.Injector/linux/procfs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

const std = @import("std");

// /proc pseudo-files report st_size=0, so std.Io readFileAlloc returns empty.
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);
Expand Down
6 changes: 1 addition & 5 deletions Hauyne.Injector/linux/ptrace.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
const std = @import("std");
const builtin = @import("builtin");

const arch = switch (builtin.cpu.arch) {
.x86_64 => @import("arch/x86_64.zig"),
.aarch64 => @import("arch/aarch64.zig"),
else => @compileError("unsupported architecture"),
};
const arch = @import("arch.zig").cpu;

pub const UserRegsStruct = arch.UserRegsStruct;

Expand Down
7 changes: 1 addition & 6 deletions Hauyne.Injector/linux/shim.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// Mozilla Public License, v. 2.0.

const std = @import("std");
const builtin = @import("builtin");

pub const ScratchSize: usize = 0x2000; // 8 KiB (two pages)
pub const PathOffset: usize = 0x40; // bootstrap .so path (1984)
Expand All @@ -14,11 +13,7 @@ pub const SymbolOffset: usize = 0x1800; // "hauyne_start\0" (256)
pub const VictimShimOff: usize = 0x1900; // pthread_create + pthread_detach (256)
pub const PayloadShimOff: usize = 0x1A00; // dlopen + dlsym + hauyne_start (1536)

const arch = switch (builtin.cpu.arch) {
.x86_64 => @import("shim/x86_64.zig"),
.aarch64 => @import("shim/aarch64.zig"),
else => @compileError("unsupported architecture"),
};
const arch = @import("arch.zig").emitter;

pub const InputError = error{
BootstrapPathTooLong,
Expand Down
35 changes: 28 additions & 7 deletions Hauyne.Injector/linux/symbols.zig
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ const MapsRow = struct {
const exacts = [_][]const u8{ "libc.so.6", "libpthread.so.0", "libdl.so.2" };
const prefix = [_][]const u8{"libc.musl-"};

pub fn findSymbolInTarget(io: std.Io, allocator: std.mem.Allocator, pid: i32, symbol: []const u8) !usize {
pub fn findSymbolsInTarget(
io: std.Io,
allocator: std.mem.Allocator,
pid: i32,
comptime names: []const []const u8,
) ![names.len]usize {
var results = std.mem.zeroes([names.len]usize);
var found: usize = 0;

const maps_path = try std.fmt.allocPrint(allocator, "/proc/{d}/maps", .{pid});
defer allocator.free(maps_path);

Expand All @@ -25,20 +33,33 @@ pub fn findSymbolInTarget(io: std.Io, allocator: std.mem.Allocator, pid: i32, sy

var lines = std.mem.splitScalar(u8, maps_text, '\n');
while (lines.next()) |line| {
if (found == names.len) break;

const row = parseMapsRow(line) orelse continue;
if (!std.mem.eql(u8, row.offset, "00000000")) continue;
if (!isLibcCandidate(std.fs.path.basename(row.path))) continue;

const data = readTargetFile(io, allocator, pid, row.path) catch continue;
defer allocator.free(data);

const sym_off = lookupDynsym(data, symbol) catch |err| switch (err) {
error.SymbolNotFound => continue,
else => return err,
};
return row.start + sym_off;
for (0..names.len) |i| {
if (results[i] != 0) continue;
const sym_off = lookupDynsym(data, names[i]) catch |err| switch (err) {
error.SymbolNotFound => continue,
else => return err,
};
results[i] = row.start + sym_off;
found += 1;
}
}
return error.SymbolNotFound;

if (found < names.len) return error.SymbolNotFound;
return results;
}

pub fn findSymbolInTarget(io: std.Io, allocator: std.mem.Allocator, pid: i32, comptime symbol: []const u8) !usize {
const result = try findSymbolsInTarget(io, allocator, pid, &.{symbol});
return result[0];
}

fn isLibcCandidate(basename: []const u8) bool {
Expand Down
6 changes: 1 addition & 5 deletions Hauyne.Injector/linux/victim.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
const std = @import("std");
const procfs = @import("procfs.zig");

const arch = switch (@import("builtin").cpu.arch) {
.x86_64 => @import("arch/x86_64.zig"),
.aarch64 => @import("arch/aarch64.zig"),
else => @compileError("unsupported architecture"),
};
const arch = @import("arch.zig").cpu;

// Falls back to the main thread, but main thread holds EE locks,
// and will probably just suicide bomb if hijacked
Expand Down
Loading
Loading