Skip to content

Commit 7f124fa

Browse files
authored
Small-fry refactors and generalising (#14)
1 parent 8f9102e commit 7f124fa

13 files changed

Lines changed: 156 additions & 156 deletions

File tree

.github/workflows/test.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@ jobs:
3636
version: 0.16.0
3737

3838
- name: Build
39-
run: ./build.sh --zig-targets "${{ matrix.zig-target }}" --no-dotnet
39+
run: >-
40+
zig build
41+
-Dtarget=${{ matrix.zig-target }}
42+
-Doptimize=ReleaseFast
43+
-Dinjector-optimize=ReleaseSmall
44+
--prefix bin
4045
4146
- name: Build payload
4247
run: dotnet build Hauyne.Payload -c Release -p:TargetFramework=net${{ matrix.dotnet }} --nologo

Hauyne.Bootstrap/build.zig

Lines changed: 0 additions & 36 deletions
This file was deleted.

Hauyne.Injector/build.zig

Lines changed: 0 additions & 34 deletions
This file was deleted.

Hauyne.Injector/injector.zig

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ const builtin = @import("builtin");
99

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

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

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

26-
if (args.len < 2 or std.mem.eql(u8, args[1], "--help") or std.mem.eql(u8, args[1], "-h")) {
24+
const wants_help = for (args) |a| {
25+
if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) break true;
26+
} else false;
27+
28+
if (args.len < 2 or wants_help) {
2729
const usage =
2830
\\Usage: {s} <process-name|pid> [payload-path] [options]
2931
\\
@@ -36,7 +38,7 @@ pub fn main(init: std.process.Init) u8 {
3638
\\Options:
3739
\\ --type <name> Fully qualified type name (default: Hauyne.Payload.Entrypoint, Hauyne.Payload)
3840
\\ --method <name> Entry method name (default: Initialize)
39-
\\ -h, --help Hello
41+
\\ -h, --help Show this help
4042
\\
4143
;
4244
println(allocator, usage, .{args[0]});
@@ -65,6 +67,9 @@ pub fn main(init: std.process.Init) u8 {
6567
return 1;
6668
}
6769
method_name = args[i];
70+
} else if (std.mem.startsWith(u8, a, "--")) {
71+
std.debug.print("Unknown option: {s}\n", .{a});
72+
return 1;
6873
} else if (payload_path == null) {
6974
payload_path = a;
7075
} else {
@@ -144,11 +149,9 @@ fn resolveTarget(io: std.Io, allocator: std.mem.Allocator, spec: []const u8) !u3
144149
return pid;
145150
} else |_| {}
146151

147-
var matches: [max_matches]u32 = undefined;
148-
var n: usize = 0;
149-
try collectMatches(io, spec, &matches, &n, &inaccessible);
152+
const matches = try collectMatches(io, allocator, spec, &inaccessible);
150153

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

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

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

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

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

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

209211
if (pidMatchesName(io, pid, name, inaccessible)) {
210-
if (count.* >= out.len) return;
211-
out[count.*] = pid;
212-
count.* += 1;
212+
try matches.append(allocator, pid);
213213
}
214214
}
215+
return matches.items;
215216
}
216217

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

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

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

304-
if (Process32FirstW(snapshot, &entry) == .FALSE) return;
306+
if (Process32FirstW(snapshot, &entry) == .FALSE) return matches.items;
305307

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

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

328328
if (Process32NextW(snapshot, &entry) == .FALSE) break;
329329
}
330+
return matches.items;
330331
}
331332

332333
fn isDotNetProcess(io: std.Io, allocator: std.mem.Allocator, pid: u32, inaccessible: *usize) !bool {

Hauyne.Injector/linux/arch.zig

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
2+
// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
3+
//
4+
// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the
5+
// Mozilla Public License, v. 2.0.
6+
7+
const builtin = @import("builtin");
8+
9+
pub const cpu = switch (builtin.cpu.arch) {
10+
.x86_64 => @import("arch/x86_64.zig"),
11+
.aarch64 => @import("arch/aarch64.zig"),
12+
else => @compileError("unsupported architecture"),
13+
};
14+
15+
pub const emitter = switch (builtin.cpu.arch) {
16+
.x86_64 => @import("shim/x86_64.zig"),
17+
.aarch64 => @import("shim/aarch64.zig"),
18+
else => @compileError("unsupported architecture"),
19+
};

Hauyne.Injector/linux/linux.zig

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@ const shim = @import("shim.zig");
1212
const symbols = @import("symbols.zig");
1313
const victim_mod = @import("victim.zig");
1414

15-
const arch = switch (builtin.cpu.arch) {
16-
.x86_64 => @import("arch/x86_64.zig"),
17-
.aarch64 => @import("arch/aarch64.zig"),
18-
else => @compileError("unsupported architecture"),
19-
};
15+
const arch = @import("arch.zig").cpu;
2016

2117
const UserRegsStruct = ptrace_mod.UserRegsStruct;
2218

@@ -114,10 +110,11 @@ pub fn inject(
114110

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

117-
const dlopen_addr = try symbols.findSymbolInTarget(io, allocator, tgid, "dlopen");
118-
const dlsym_addr = try symbols.findSymbolInTarget(io, allocator, tgid, "dlsym");
119-
const pthread_create_addr = try symbols.findSymbolInTarget(io, allocator, tgid, "pthread_create");
120-
const pthread_detach_addr = try symbols.findSymbolInTarget(io, allocator, tgid, "pthread_detach");
113+
const sym_addrs = try symbols.findSymbolsInTarget(io, allocator, tgid, &.{ "dlopen", "dlsym", "pthread_create", "pthread_detach" });
114+
const dlopen_addr = sym_addrs[0];
115+
const dlsym_addr = sym_addrs[1];
116+
const pthread_create_addr = sym_addrs[2];
117+
const pthread_detach_addr = sym_addrs[3];
121118

122119
std.debug.print("[hauyne] victim tid={d} (tgid={d})\n", .{ victim, tgid });
123120
if (debug) {

Hauyne.Injector/linux/procfs.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
const std = @import("std");
88

9+
// /proc pseudo-files report st_size=0, so std.Io readFileAlloc returns empty.
910
pub fn readFileAlloc(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
1011
const fd = try std.posix.openat(std.posix.AT.FDCWD, path, .{ .ACCMODE = .RDONLY }, 0);
1112
defer _ = std.c.close(fd);

Hauyne.Injector/linux/ptrace.zig

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@
77
const std = @import("std");
88
const builtin = @import("builtin");
99

10-
const arch = switch (builtin.cpu.arch) {
11-
.x86_64 => @import("arch/x86_64.zig"),
12-
.aarch64 => @import("arch/aarch64.zig"),
13-
else => @compileError("unsupported architecture"),
14-
};
10+
const arch = @import("arch.zig").cpu;
1511

1612
pub const UserRegsStruct = arch.UserRegsStruct;
1713

Hauyne.Injector/linux/shim.zig

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
// Mozilla Public License, v. 2.0.
66

77
const std = @import("std");
8-
const builtin = @import("builtin");
98

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

17-
const arch = switch (builtin.cpu.arch) {
18-
.x86_64 => @import("shim/x86_64.zig"),
19-
.aarch64 => @import("shim/aarch64.zig"),
20-
else => @compileError("unsupported architecture"),
21-
};
16+
const arch = @import("arch.zig").emitter;
2217

2318
pub const InputError = error{
2419
BootstrapPathTooLong,

Hauyne.Injector/linux/symbols.zig

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@ const MapsRow = struct {
1616
const exacts = [_][]const u8{ "libc.so.6", "libpthread.so.0", "libdl.so.2" };
1717
const prefix = [_][]const u8{"libc.musl-"};
1818

19-
pub fn findSymbolInTarget(io: std.Io, allocator: std.mem.Allocator, pid: i32, symbol: []const u8) !usize {
19+
pub fn findSymbolsInTarget(
20+
io: std.Io,
21+
allocator: std.mem.Allocator,
22+
pid: i32,
23+
comptime names: []const []const u8,
24+
) ![names.len]usize {
25+
var results = std.mem.zeroes([names.len]usize);
26+
var found: usize = 0;
27+
2028
const maps_path = try std.fmt.allocPrint(allocator, "/proc/{d}/maps", .{pid});
2129
defer allocator.free(maps_path);
2230

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

2634
var lines = std.mem.splitScalar(u8, maps_text, '\n');
2735
while (lines.next()) |line| {
36+
if (found == names.len) break;
37+
2838
const row = parseMapsRow(line) orelse continue;
2939
if (!std.mem.eql(u8, row.offset, "00000000")) continue;
3040
if (!isLibcCandidate(std.fs.path.basename(row.path))) continue;
3141

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

35-
const sym_off = lookupDynsym(data, symbol) catch |err| switch (err) {
36-
error.SymbolNotFound => continue,
37-
else => return err,
38-
};
39-
return row.start + sym_off;
45+
for (0..names.len) |i| {
46+
if (results[i] != 0) continue;
47+
const sym_off = lookupDynsym(data, names[i]) catch |err| switch (err) {
48+
error.SymbolNotFound => continue,
49+
else => return err,
50+
};
51+
results[i] = row.start + sym_off;
52+
found += 1;
53+
}
4054
}
41-
return error.SymbolNotFound;
55+
56+
if (found < names.len) return error.SymbolNotFound;
57+
return results;
58+
}
59+
60+
pub fn findSymbolInTarget(io: std.Io, allocator: std.mem.Allocator, pid: i32, comptime symbol: []const u8) !usize {
61+
const result = try findSymbolsInTarget(io, allocator, pid, &.{symbol});
62+
return result[0];
4263
}
4364

4465
fn isLibcCandidate(basename: []const u8) bool {

0 commit comments

Comments
 (0)