syscall tracing skeleton
This commit is contained in:
@@ -6,6 +6,7 @@ const mem = std.mem;
|
|||||||
const posix = std.posix;
|
const posix = std.posix;
|
||||||
const zydis = @import("zydis").zydis;
|
const zydis = @import("zydis").zydis;
|
||||||
const dis = @import("disassembler.zig");
|
const dis = @import("disassembler.zig");
|
||||||
|
const syscalls = @import("syscalls.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.patcher);
|
const log = std.log.scoped(.patcher);
|
||||||
const AddressAllocator = @import("AddressAllocator.zig");
|
const AddressAllocator = @import("AddressAllocator.zig");
|
||||||
@@ -38,11 +39,18 @@ const prefixes = [_]u8{
|
|||||||
0x36,
|
0x36,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var syscall_flicken_bytes = [13]u8{
|
||||||
|
0x49, 0xBB, // mov r11
|
||||||
|
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, // 8byte immediate
|
||||||
|
0x41, 0xff, 0xd3, // call r11
|
||||||
|
};
|
||||||
|
|
||||||
pub var gpa: mem.Allocator = undefined;
|
pub var gpa: mem.Allocator = undefined;
|
||||||
pub var flicken_templates: std.StringArrayHashMapUnmanaged(Flicken) = .empty;
|
pub var flicken_templates: std.StringArrayHashMapUnmanaged(Flicken) = .empty;
|
||||||
pub var address_allocator: AddressAllocator = .empty;
|
pub var address_allocator: AddressAllocator = .empty;
|
||||||
/// Tracks the base addresses of pages we have mmap'd for Flicken.
|
/// Tracks the base addresses of pages we have mmap'd for Flicken.
|
||||||
pub var allocated_pages: std.AutoHashMapUnmanaged(u64, void) = .empty;
|
pub var allocated_pages: std.AutoHashMapUnmanaged(u64, void) = .empty;
|
||||||
|
pub var mutex: std.Thread.Mutex = .{};
|
||||||
|
|
||||||
var init_once = std.once(initInner);
|
var init_once = std.once(initInner);
|
||||||
pub fn init() void {
|
pub fn init() void {
|
||||||
@@ -55,6 +63,8 @@ fn initInner() void {
|
|||||||
page_size / @sizeOf(Flicken),
|
page_size / @sizeOf(Flicken),
|
||||||
) catch @panic("failed initializing patcher");
|
) catch @panic("failed initializing patcher");
|
||||||
flicken_templates.putAssumeCapacity("nop", .{ .name = "nop", .bytes = &.{} });
|
flicken_templates.putAssumeCapacity("nop", .{ .name = "nop", .bytes = &.{} });
|
||||||
|
mem.writeInt(u64, syscall_flicken_bytes[2..][0..8], @intFromPtr(&syscalls.syscall_entry), .little);
|
||||||
|
flicken_templates.putAssumeCapacity("syscall", .{ .name = "syscall", .bytes = &syscall_flicken_bytes });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flicken name and bytes have to be valid for the lifetime it's used. If a trampoline with the
|
/// Flicken name and bytes have to be valid for the lifetime it's used. If a trampoline with the
|
||||||
@@ -62,6 +72,7 @@ fn initInner() void {
|
|||||||
/// NOTE: The name "nop" is reserved and always has the ID 0.
|
/// NOTE: The name "nop" is reserved and always has the ID 0.
|
||||||
pub fn addFlicken(trampoline: Flicken) !FlickenId {
|
pub fn addFlicken(trampoline: Flicken) !FlickenId {
|
||||||
assert(!mem.eql(u8, "nop", trampoline.name));
|
assert(!mem.eql(u8, "nop", trampoline.name));
|
||||||
|
assert(!mem.eql(u8, "syscall", trampoline.name));
|
||||||
try flicken_templates.ensureUnusedCapacity(gpa, 1);
|
try flicken_templates.ensureUnusedCapacity(gpa, 1);
|
||||||
errdefer comptime unreachable;
|
errdefer comptime unreachable;
|
||||||
|
|
||||||
@@ -90,6 +101,8 @@ pub const FlickenId = enum(u64) {
|
|||||||
/// It also needs special handling when constructing the patches, because it's different for
|
/// It also needs special handling when constructing the patches, because it's different for
|
||||||
/// each instruction.
|
/// each instruction.
|
||||||
nop = 0,
|
nop = 0,
|
||||||
|
/// TODO: docs
|
||||||
|
syscall = 1,
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,6 +185,11 @@ pub const Statistics = struct {
|
|||||||
/// The region is processed Back-to-Front to ensure that modifications (punning) only
|
/// The region is processed Back-to-Front to ensure that modifications (punning) only
|
||||||
/// constrain instructions that have already been processed or are locked.
|
/// constrain instructions that have already been processed or are locked.
|
||||||
pub fn patchRegion(region: []align(page_size) u8) !void {
|
pub fn patchRegion(region: []align(page_size) u8) !void {
|
||||||
|
// For now just do a coarse lock.
|
||||||
|
// TODO: should we make this more fine grained?
|
||||||
|
mutex.lock();
|
||||||
|
defer mutex.unlock();
|
||||||
|
|
||||||
{
|
{
|
||||||
// Block the region, such that we don't try to allocate there anymore.
|
// Block the region, such that we don't try to allocate there anymore.
|
||||||
const start: i64 = @intCast(@intFromPtr(region.ptr));
|
const start: i64 = @intCast(@intFromPtr(region.ptr));
|
||||||
@@ -202,11 +220,12 @@ pub fn patchRegion(region: []align(page_size) u8) !void {
|
|||||||
const offset = instruction.address - @intFromPtr(region.ptr);
|
const offset = instruction.address - @intFromPtr(region.ptr);
|
||||||
instruction_starts.set(offset);
|
instruction_starts.set(offset);
|
||||||
|
|
||||||
const should_patch = instruction.instruction.mnemonic == zydis.ZYDIS_MNEMONIC_SYSCALL or
|
const is_syscall = instruction.instruction.mnemonic == zydis.ZYDIS_MNEMONIC_SYSCALL;
|
||||||
|
const should_patch = is_syscall or
|
||||||
instruction.instruction.attributes & zydis.ZYDIS_ATTRIB_HAS_LOCK > 0;
|
instruction.instruction.attributes & zydis.ZYDIS_ATTRIB_HAS_LOCK > 0;
|
||||||
if (should_patch) {
|
if (should_patch) {
|
||||||
const request: PatchRequest = .{
|
const request: PatchRequest = .{
|
||||||
.flicken = .nop,
|
.flicken = if (is_syscall) .syscall else .nop,
|
||||||
.offset = offset,
|
.offset = offset,
|
||||||
.size = instruction.instruction.length,
|
.size = instruction.instruction.length,
|
||||||
.bytes = region[offset..],
|
.bytes = region[offset..],
|
||||||
|
|||||||
113
src/syscalls.zig
Normal file
113
src/syscalls.zig
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const linux = std.os.linux;
|
||||||
|
|
||||||
|
/// Represents the stack layout pushed by `syscall_entry` before calling the handler.
|
||||||
|
pub const UserRegs = extern struct {
|
||||||
|
padding: u64, // Result of `sub $8, %rsp` for alignment
|
||||||
|
rflags: u64,
|
||||||
|
rax: u64,
|
||||||
|
rbx: u64,
|
||||||
|
rcx: u64,
|
||||||
|
rdx: u64,
|
||||||
|
rsi: u64,
|
||||||
|
rdi: u64,
|
||||||
|
rbp: u64,
|
||||||
|
r8: u64,
|
||||||
|
r9: u64,
|
||||||
|
r10: u64,
|
||||||
|
r11: u64,
|
||||||
|
r12: u64,
|
||||||
|
r13: u64,
|
||||||
|
r14: u64,
|
||||||
|
r15: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The main entry point for intercepted syscalls.
|
||||||
|
///
|
||||||
|
/// This function is called from `syscall_entry` with a pointer to the saved registers.
|
||||||
|
/// It effectively emulates the syscall instruction while allowing for interception.
|
||||||
|
export fn syscall_handler(regs: *UserRegs) void {
|
||||||
|
// TODO: Handle signals (masking) to prevent re-entrancy issues if we touch global state.
|
||||||
|
// TODO: Handle `clone` specially because the child thread wakes up with a fresh stack
|
||||||
|
// and cannot pop the registers we saved here.
|
||||||
|
|
||||||
|
const sys_nr = regs.rax;
|
||||||
|
const sys: linux.SYS = @enumFromInt(sys_nr);
|
||||||
|
const arg1 = regs.rdi;
|
||||||
|
const arg2 = regs.rsi;
|
||||||
|
const arg3 = regs.rdx;
|
||||||
|
const arg4 = regs.r10;
|
||||||
|
const arg5 = regs.r8;
|
||||||
|
const arg6 = regs.r9;
|
||||||
|
|
||||||
|
std.debug.print("Got syscall {s}\n", .{@tagName(sys)});
|
||||||
|
// For now, we just pass through everything.
|
||||||
|
// In the future, we will switch on `sys` to handle mmap, mprotect, etc.
|
||||||
|
const result = std.os.linux.syscall6(sys, arg1, arg2, arg3, arg4, arg5, arg6);
|
||||||
|
|
||||||
|
// Write result back to the saved RAX so it is restored to the application.
|
||||||
|
regs.rax = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assembly trampoline that saves state and calls the Zig handler.
|
||||||
|
pub fn syscall_entry() callconv(.naked) void {
|
||||||
|
asm volatile (
|
||||||
|
\\ .global syscall_entry
|
||||||
|
\\ .type syscall_entry, @function
|
||||||
|
\\ syscall_entry:
|
||||||
|
\\ # Respect the Red Zone (128 bytes)
|
||||||
|
\\ sub $128, %rsp
|
||||||
|
\\
|
||||||
|
\\ # Save all GPRs that must be preserved or are arguments
|
||||||
|
\\ push %r15
|
||||||
|
\\ push %r14
|
||||||
|
\\ push %r13
|
||||||
|
\\ push %r12
|
||||||
|
\\ push %r11
|
||||||
|
\\ push %r10
|
||||||
|
\\ push %r9
|
||||||
|
\\ push %r8
|
||||||
|
\\ push %rbp
|
||||||
|
\\ push %rdi
|
||||||
|
\\ push %rsi
|
||||||
|
\\ push %rdx
|
||||||
|
\\ push %rcx
|
||||||
|
\\ push %rbx
|
||||||
|
\\ push %rax
|
||||||
|
\\ pushfq # Save Flags
|
||||||
|
\\
|
||||||
|
\\ # Align stack
|
||||||
|
\\ # Current pushes: 16 * 8 = 128 bytes.
|
||||||
|
\\ # Red zone sub: 128 bytes.
|
||||||
|
\\ # Trampoline call pushed ret addr: 8 bytes.
|
||||||
|
\\ # Total misalign: 8 bytes. We need 16-byte alignment for 'call'.
|
||||||
|
\\ sub $8, %rsp
|
||||||
|
\\
|
||||||
|
\\ # Pass pointer to regs (current rsp) as 1st argument (rdi) and call handler.
|
||||||
|
\\ mov %rsp, %rdi
|
||||||
|
\\ call syscall_handler
|
||||||
|
\\
|
||||||
|
\\ # Restore State
|
||||||
|
\\ add $8, %rsp
|
||||||
|
\\ popfq
|
||||||
|
\\ pop %rax
|
||||||
|
\\ pop %rbx
|
||||||
|
\\ pop %rcx
|
||||||
|
\\ pop %rdx
|
||||||
|
\\ pop %rsi
|
||||||
|
\\ pop %rdi
|
||||||
|
\\ pop %rbp
|
||||||
|
\\ pop %r8
|
||||||
|
\\ pop %r9
|
||||||
|
\\ pop %r10
|
||||||
|
\\ pop %r11
|
||||||
|
\\ pop %r12
|
||||||
|
\\ pop %r13
|
||||||
|
\\ pop %r14
|
||||||
|
\\ pop %r15
|
||||||
|
\\
|
||||||
|
\\ # Restore Red Zone and Return
|
||||||
|
\\ add $128, %rsp
|
||||||
|
\\ ret
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user