|
|
|
@@ -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");
|
|
|
|
@@ -17,15 +18,11 @@ const Range = @import("Range.zig");
|
|
|
|
|
|
|
|
|
|
|
|
const assert = std.debug.assert;
|
|
|
|
const assert = std.debug.assert;
|
|
|
|
|
|
|
|
|
|
|
|
const page_size = 4096;
|
|
|
|
const page_size = std.heap.pageSize();
|
|
|
|
const jump_rel32: u8 = 0xe9;
|
|
|
|
const jump_rel32: u8 = 0xe9;
|
|
|
|
const jump_rel32_size = 5;
|
|
|
|
const jump_rel32_size = 5;
|
|
|
|
const jump_rel8: u8 = 0xeb;
|
|
|
|
const jump_rel8: u8 = 0xeb;
|
|
|
|
const jump_rel8_size = 2;
|
|
|
|
const jump_rel8_size = 2;
|
|
|
|
const max_ins_bytes = 15;
|
|
|
|
|
|
|
|
// Based on the paper 'x86-64 Instruction Usage among C/C++ Applications' by 'Akshintala et al.'
|
|
|
|
|
|
|
|
// it's '4.25' bytes, so 4 is good enough. (https://oscarlab.github.io/papers/instrpop-systor19.pdf)
|
|
|
|
|
|
|
|
const avg_ins_bytes = 4;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: Find an invalid instruction to use.
|
|
|
|
// TODO: Find an invalid instruction to use.
|
|
|
|
// const invalid: u8 = 0xaa;
|
|
|
|
// const invalid: u8 = 0xaa;
|
|
|
|
@@ -33,42 +30,53 @@ const int3: u8 = 0xcc;
|
|
|
|
const nop: u8 = 0x90;
|
|
|
|
const nop: u8 = 0x90;
|
|
|
|
|
|
|
|
|
|
|
|
// Prefixes for Padded Jumps (Tactic T1)
|
|
|
|
// Prefixes for Padded Jumps (Tactic T1)
|
|
|
|
const prefix_fs: u8 = 0x64;
|
|
|
|
const prefixes = [_]u8{
|
|
|
|
const prefix_gs: u8 = 0x65;
|
|
|
|
// prefix_fs,
|
|
|
|
const prefix_ss: u8 = 0x36;
|
|
|
|
0x64,
|
|
|
|
const prefixes = [_]u8{ prefix_fs, prefix_gs, prefix_ss };
|
|
|
|
// prefix_gs,
|
|
|
|
|
|
|
|
0x65,
|
|
|
|
|
|
|
|
// prefix_ss,
|
|
|
|
|
|
|
|
0x36,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const Patcher = @This();
|
|
|
|
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
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
gpa: mem.Allocator,
|
|
|
|
pub var gpa: mem.Allocator = undefined;
|
|
|
|
flicken: std.StringArrayHashMapUnmanaged(Flicken) = .empty,
|
|
|
|
pub var flicken_templates: std.StringArrayHashMapUnmanaged(Flicken) = .empty;
|
|
|
|
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.
|
|
|
|
allocated_pages: std.AutoHashMapUnmanaged(u64, void) = .empty,
|
|
|
|
pub var allocated_pages: std.AutoHashMapUnmanaged(u64, void) = .empty;
|
|
|
|
|
|
|
|
pub var mutex: std.Thread.Mutex = .{};
|
|
|
|
|
|
|
|
|
|
|
|
pub fn init(gpa: mem.Allocator) !Patcher {
|
|
|
|
var init_once = std.once(initInner);
|
|
|
|
var flicken: std.StringArrayHashMapUnmanaged(Flicken) = .empty;
|
|
|
|
pub fn init() void {
|
|
|
|
try flicken.ensureTotalCapacity(gpa, 8);
|
|
|
|
init_once.call();
|
|
|
|
flicken.putAssumeCapacity("nop", .{ .name = "nop", .bytes = &.{} });
|
|
|
|
|
|
|
|
return .{
|
|
|
|
|
|
|
|
.gpa = gpa,
|
|
|
|
|
|
|
|
.flicken = flicken,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn initInner() void {
|
|
|
|
pub fn deinit(patcher: *Patcher) void {
|
|
|
|
gpa = std.heap.page_allocator;
|
|
|
|
_ = patcher;
|
|
|
|
flicken_templates.ensureTotalCapacity(
|
|
|
|
|
|
|
|
std.heap.page_allocator,
|
|
|
|
|
|
|
|
page_size / @sizeOf(Flicken),
|
|
|
|
|
|
|
|
) catch @panic("failed initializing patcher");
|
|
|
|
|
|
|
|
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
|
|
|
|
/// name is already registered it gets overwritten.
|
|
|
|
/// name is already registered it gets overwritten.
|
|
|
|
/// 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(patcher: *Patcher, trampoline: Flicken) !FlickenId {
|
|
|
|
pub fn addFlicken(trampoline: Flicken) !FlickenId {
|
|
|
|
assert(!mem.eql(u8, "nop", trampoline.name));
|
|
|
|
assert(!mem.eql(u8, "nop", trampoline.name));
|
|
|
|
try patcher.flicken.ensureUnusedCapacity(patcher.gpa, 1);
|
|
|
|
assert(!mem.eql(u8, "syscall", trampoline.name));
|
|
|
|
|
|
|
|
try flicken_templates.ensureUnusedCapacity(gpa, 1);
|
|
|
|
errdefer comptime unreachable;
|
|
|
|
errdefer comptime unreachable;
|
|
|
|
|
|
|
|
|
|
|
|
const gop = patcher.flicken.getOrPutAssumeCapacity(trampoline.name);
|
|
|
|
const gop = flicken_templates.getOrPutAssumeCapacity(trampoline.name);
|
|
|
|
if (gop.found_existing) {
|
|
|
|
if (gop.found_existing) {
|
|
|
|
log.warn("addTrampoline: Overwriting existing trampoline: {s}", .{trampoline.name});
|
|
|
|
log.warn("addTrampoline: Overwriting existing trampoline: {s}", .{trampoline.name});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -93,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,
|
|
|
|
_,
|
|
|
|
_,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
@@ -169,18 +179,28 @@ pub const Statistics = struct {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
pub fn patchRegion(patcher: *Patcher, region: []align(page_size) u8) !void {
|
|
|
|
/// Scans a memory region for instructions that require patching and applies the patches
|
|
|
|
|
|
|
|
/// using a hierarchy of tactics (Direct/Punning -> Successor Eviction -> Neighbor Eviction).
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// The region is processed Back-to-Front to ensure that modifications (punning) only
|
|
|
|
|
|
|
|
/// constrain instructions that have already been processed or are locked.
|
|
|
|
|
|
|
|
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));
|
|
|
|
try patcher.address_allocator.block(
|
|
|
|
try address_allocator.block(
|
|
|
|
patcher.gpa,
|
|
|
|
gpa,
|
|
|
|
.{ .start = start, .end = start + @as(i64, @intCast(region.len)) },
|
|
|
|
.{ .start = start, .end = start + @as(i64, @intCast(region.len)) },
|
|
|
|
page_size,
|
|
|
|
page_size,
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var arena_impl = std.heap.ArenaAllocator.init(patcher.gpa);
|
|
|
|
var arena_impl = std.heap.ArenaAllocator.init(gpa);
|
|
|
|
const arena = arena_impl.allocator();
|
|
|
|
const arena = arena_impl.allocator();
|
|
|
|
defer arena_impl.deinit();
|
|
|
|
defer arena_impl.deinit();
|
|
|
|
|
|
|
|
|
|
|
|
@@ -200,11 +220,12 @@ pub fn patchRegion(patcher: *Patcher, 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..],
|
|
|
|
@@ -234,7 +255,7 @@ pub fn patchRegion(patcher: *Patcher, region: []align(page_size) u8) !void {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
last_offset = request.offset;
|
|
|
|
last_offset = request.offset;
|
|
|
|
|
|
|
|
|
|
|
|
if (@as(u64, @intFromEnum(request.flicken)) >= patcher.flicken.count()) {
|
|
|
|
if (@as(u64, @intFromEnum(request.flicken)) >= flicken_templates.count()) {
|
|
|
|
const fmt = dis.formatBytes(request.bytes[0..request.size]);
|
|
|
|
const fmt = dis.formatBytes(request.bytes[0..request.size]);
|
|
|
|
log.err(
|
|
|
|
log.err(
|
|
|
|
"patchRegion: Usage of undefined flicken in request {f} for instruction: {s}",
|
|
|
|
"patchRegion: Usage of undefined flicken in request {f} for instruction: {s}",
|
|
|
|
@@ -269,7 +290,7 @@ pub fn patchRegion(patcher: *Patcher, region: []align(page_size) u8) !void {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (try patcher.attemptDirectOrPunning(
|
|
|
|
if (try attemptDirectOrPunning(
|
|
|
|
request,
|
|
|
|
request,
|
|
|
|
arena,
|
|
|
|
arena,
|
|
|
|
&locked_bytes,
|
|
|
|
&locked_bytes,
|
|
|
|
@@ -279,7 +300,7 @@ pub fn patchRegion(patcher: *Patcher, region: []align(page_size) u8) !void {
|
|
|
|
continue :requests;
|
|
|
|
continue :requests;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (try patcher.attemptSuccessorEviction(
|
|
|
|
if (try attemptSuccessorEviction(
|
|
|
|
request,
|
|
|
|
request,
|
|
|
|
arena,
|
|
|
|
arena,
|
|
|
|
&locked_bytes,
|
|
|
|
&locked_bytes,
|
|
|
|
@@ -289,7 +310,7 @@ pub fn patchRegion(patcher: *Patcher, region: []align(page_size) u8) !void {
|
|
|
|
continue :requests;
|
|
|
|
continue :requests;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (try patcher.attemptNeighborEviction(
|
|
|
|
if (try attemptNeighborEviction(
|
|
|
|
request,
|
|
|
|
request,
|
|
|
|
arena,
|
|
|
|
arena,
|
|
|
|
&locked_bytes,
|
|
|
|
&locked_bytes,
|
|
|
|
@@ -323,7 +344,6 @@ pub fn patchRegion(patcher: *Patcher, region: []align(page_size) u8) !void {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn attemptDirectOrPunning(
|
|
|
|
fn attemptDirectOrPunning(
|
|
|
|
patcher: *Patcher,
|
|
|
|
|
|
|
|
request: PatchRequest,
|
|
|
|
request: PatchRequest,
|
|
|
|
arena: mem.Allocator,
|
|
|
|
arena: mem.Allocator,
|
|
|
|
locked_bytes: *std.DynamicBitSetUnmanaged,
|
|
|
|
locked_bytes: *std.DynamicBitSetUnmanaged,
|
|
|
|
@@ -333,7 +353,7 @@ fn attemptDirectOrPunning(
|
|
|
|
const flicken: Flicken = if (request.flicken == .nop)
|
|
|
|
const flicken: Flicken = if (request.flicken == .nop)
|
|
|
|
.{ .name = "nop", .bytes = request.bytes[0..request.size] }
|
|
|
|
.{ .name = "nop", .bytes = request.bytes[0..request.size] }
|
|
|
|
else
|
|
|
|
else
|
|
|
|
patcher.flicken.entries.get(@intFromEnum(request.flicken)).value;
|
|
|
|
flicken_templates.entries.get(@intFromEnum(request.flicken)).value;
|
|
|
|
|
|
|
|
|
|
|
|
var pii = PatchInstructionIterator.init(
|
|
|
|
var pii = PatchInstructionIterator.init(
|
|
|
|
request.bytes,
|
|
|
|
request.bytes,
|
|
|
|
@@ -346,9 +366,9 @@ fn attemptDirectOrPunning(
|
|
|
|
// mapped. While harmless (it becomes an unused executable page), it is technically a
|
|
|
|
// mapped. While harmless (it becomes an unused executable page), it is technically a
|
|
|
|
// memory leak. A future fix should track "current attempt" pages separately and unmap
|
|
|
|
// memory leak. A future fix should track "current attempt" pages separately and unmap
|
|
|
|
// them on failure.
|
|
|
|
// them on failure.
|
|
|
|
while (pii.next(&patcher.address_allocator)) |allocated_range| {
|
|
|
|
while (pii.next(.{ .count = 256 })) |allocated_range| {
|
|
|
|
try pages_made_writable.ensureUnusedCapacity(arena, touchedPageCount(allocated_range));
|
|
|
|
try pages_made_writable.ensureUnusedCapacity(arena, touchedPageCount(allocated_range));
|
|
|
|
patcher.ensureRangeWritable(
|
|
|
|
ensureRangeWritable(
|
|
|
|
allocated_range,
|
|
|
|
allocated_range,
|
|
|
|
pages_made_writable,
|
|
|
|
pages_made_writable,
|
|
|
|
) catch |err| switch (err) {
|
|
|
|
) catch |err| switch (err) {
|
|
|
|
@@ -366,7 +386,7 @@ fn attemptDirectOrPunning(
|
|
|
|
else => return err,
|
|
|
|
else => return err,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try patcher.address_allocator.block(patcher.gpa, allocated_range, 0);
|
|
|
|
try address_allocator.block(gpa, allocated_range, 0);
|
|
|
|
const lock_size = jump_rel32_size + pii.num_prefixes;
|
|
|
|
const lock_size = jump_rel32_size + pii.num_prefixes;
|
|
|
|
locked_bytes.setRangeValue(
|
|
|
|
locked_bytes.setRangeValue(
|
|
|
|
.{ .start = request.offset, .end = request.offset + lock_size },
|
|
|
|
.{ .start = request.offset, .end = request.offset + lock_size },
|
|
|
|
@@ -374,7 +394,7 @@ fn attemptDirectOrPunning(
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (request.size >= 5) {
|
|
|
|
if (request.size >= 5) {
|
|
|
|
assert(pii.num_prefixes == 0);
|
|
|
|
// assert(pii.num_prefixes == 0);
|
|
|
|
stats.jump += 1;
|
|
|
|
stats.jump += 1;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
stats.punning[pii.num_prefixes] += 1;
|
|
|
|
stats.punning[pii.num_prefixes] += 1;
|
|
|
|
@@ -385,7 +405,6 @@ fn attemptDirectOrPunning(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn attemptSuccessorEviction(
|
|
|
|
fn attemptSuccessorEviction(
|
|
|
|
patcher: *Patcher,
|
|
|
|
|
|
|
|
request: PatchRequest,
|
|
|
|
request: PatchRequest,
|
|
|
|
arena: mem.Allocator,
|
|
|
|
arena: mem.Allocator,
|
|
|
|
locked_bytes: *std.DynamicBitSetUnmanaged,
|
|
|
|
locked_bytes: *std.DynamicBitSetUnmanaged,
|
|
|
|
@@ -421,7 +440,7 @@ fn attemptSuccessorEviction(
|
|
|
|
succ_request.size,
|
|
|
|
succ_request.size,
|
|
|
|
succ_flicken.size(),
|
|
|
|
succ_flicken.size(),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
while (succ_pii.next(&patcher.address_allocator)) |succ_range| {
|
|
|
|
while (succ_pii.next(.{ .count = 16 })) |succ_range| {
|
|
|
|
// Ensure bytes match original before retry.
|
|
|
|
// Ensure bytes match original before retry.
|
|
|
|
assert(mem.eql(
|
|
|
|
assert(mem.eql(
|
|
|
|
u8,
|
|
|
|
u8,
|
|
|
|
@@ -430,7 +449,7 @@ fn attemptSuccessorEviction(
|
|
|
|
));
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
try pages_made_writable.ensureUnusedCapacity(arena, touchedPageCount(succ_range));
|
|
|
|
try pages_made_writable.ensureUnusedCapacity(arena, touchedPageCount(succ_range));
|
|
|
|
patcher.ensureRangeWritable(
|
|
|
|
ensureRangeWritable(
|
|
|
|
succ_range,
|
|
|
|
succ_range,
|
|
|
|
pages_made_writable,
|
|
|
|
pages_made_writable,
|
|
|
|
) catch |err| switch (err) {
|
|
|
|
) catch |err| switch (err) {
|
|
|
|
@@ -452,17 +471,17 @@ fn attemptSuccessorEviction(
|
|
|
|
const flicken: Flicken = if (request.flicken == .nop)
|
|
|
|
const flicken: Flicken = if (request.flicken == .nop)
|
|
|
|
.{ .name = "nop", .bytes = request.bytes[0..request.size] }
|
|
|
|
.{ .name = "nop", .bytes = request.bytes[0..request.size] }
|
|
|
|
else
|
|
|
|
else
|
|
|
|
patcher.flicken.entries.get(@intFromEnum(request.flicken)).value;
|
|
|
|
flicken_templates.entries.get(@intFromEnum(request.flicken)).value;
|
|
|
|
|
|
|
|
|
|
|
|
var orig_pii = PatchInstructionIterator.init(
|
|
|
|
var orig_pii = PatchInstructionIterator.init(
|
|
|
|
request.bytes,
|
|
|
|
request.bytes,
|
|
|
|
request.size,
|
|
|
|
request.size,
|
|
|
|
flicken.size(),
|
|
|
|
flicken.size(),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
while (orig_pii.next(&patcher.address_allocator)) |orig_range| {
|
|
|
|
while (orig_pii.next(.{ .count = 16 })) |orig_range| {
|
|
|
|
if (succ_range.touches(orig_range)) continue;
|
|
|
|
if (succ_range.touches(orig_range)) continue;
|
|
|
|
try pages_made_writable.ensureUnusedCapacity(arena, touchedPageCount(orig_range));
|
|
|
|
try pages_made_writable.ensureUnusedCapacity(arena, touchedPageCount(orig_range));
|
|
|
|
patcher.ensureRangeWritable(
|
|
|
|
ensureRangeWritable(
|
|
|
|
orig_range,
|
|
|
|
orig_range,
|
|
|
|
pages_made_writable,
|
|
|
|
pages_made_writable,
|
|
|
|
) catch |err| switch (err) {
|
|
|
|
) catch |err| switch (err) {
|
|
|
|
@@ -480,8 +499,8 @@ fn attemptSuccessorEviction(
|
|
|
|
else => return err,
|
|
|
|
else => return err,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try patcher.address_allocator.block(patcher.gpa, succ_range, 0);
|
|
|
|
try address_allocator.block(gpa, succ_range, 0);
|
|
|
|
try patcher.address_allocator.block(patcher.gpa, orig_range, 0);
|
|
|
|
try address_allocator.block(gpa, orig_range, 0);
|
|
|
|
const lock_size = request.size + jump_rel32_size + succ_pii.num_prefixes;
|
|
|
|
const lock_size = request.size + jump_rel32_size + succ_pii.num_prefixes;
|
|
|
|
locked_bytes.setRangeValue(
|
|
|
|
locked_bytes.setRangeValue(
|
|
|
|
.{ .start = request.offset, .end = request.offset + lock_size },
|
|
|
|
.{ .start = request.offset, .end = request.offset + lock_size },
|
|
|
|
@@ -501,7 +520,6 @@ fn attemptSuccessorEviction(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn attemptNeighborEviction(
|
|
|
|
fn attemptNeighborEviction(
|
|
|
|
patcher: *Patcher,
|
|
|
|
|
|
|
|
request: PatchRequest,
|
|
|
|
request: PatchRequest,
|
|
|
|
arena: mem.Allocator,
|
|
|
|
arena: mem.Allocator,
|
|
|
|
locked_bytes: *std.DynamicBitSetUnmanaged,
|
|
|
|
locked_bytes: *std.DynamicBitSetUnmanaged,
|
|
|
|
@@ -509,56 +527,48 @@ fn attemptNeighborEviction(
|
|
|
|
instruction_starts: *const std.DynamicBitSetUnmanaged,
|
|
|
|
instruction_starts: *const std.DynamicBitSetUnmanaged,
|
|
|
|
stats: *Statistics,
|
|
|
|
stats: *Statistics,
|
|
|
|
) !bool {
|
|
|
|
) !bool {
|
|
|
|
// Iterate valid neighbors.
|
|
|
|
// Valid neighbors must be within [-128, 127] range for a short jump.
|
|
|
|
// Neighbors must be within [-128, 127] range for a short jump.
|
|
|
|
|
|
|
|
// Since we patch back-to-front, we only look at neighbors *after* the current instruction
|
|
|
|
// Since we patch back-to-front, we only look at neighbors *after* the current instruction
|
|
|
|
// (higher address) to avoid evicting an instruction we haven't processed/patched yet.
|
|
|
|
// (higher address) to avoid evicting an instruction we haven't processed/patched yet.
|
|
|
|
// Short jump is 2 bytes (EB xx). Target is IP + 2 + xx.
|
|
|
|
|
|
|
|
// So min offset is +2 (xx=0). Max offset is +2+127 = +129.
|
|
|
|
|
|
|
|
const start_offset = request.offset + 2;
|
|
|
|
const start_offset = request.offset + 2;
|
|
|
|
const end_offset = @min(
|
|
|
|
const end_offset = @min(
|
|
|
|
start_offset + 128, // 2 + 128
|
|
|
|
start_offset + 128,
|
|
|
|
request.bytes.len + request.offset,
|
|
|
|
request.bytes.len + request.offset,
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
neighbor: for (start_offset..end_offset) |neighbor_offset| {
|
|
|
|
neighbor: for (start_offset..end_offset) |neighbor_offset| {
|
|
|
|
if (!instruction_starts.isSet(neighbor_offset)) continue;
|
|
|
|
if (!instruction_starts.isSet(neighbor_offset)) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Found a candidate victim instruction.
|
|
|
|
|
|
|
|
// We must access it relative to the request bytes slice.
|
|
|
|
|
|
|
|
const victim_bytes_all = request.bytes[neighbor_offset - request.offset ..];
|
|
|
|
const victim_bytes_all = request.bytes[neighbor_offset - request.offset ..];
|
|
|
|
|
|
|
|
|
|
|
|
// Disassemble to get size.
|
|
|
|
|
|
|
|
// PERF: We could also search for the next set bit in instruction_starts
|
|
|
|
// PERF: We could also search for the next set bit in instruction_starts
|
|
|
|
const victim_instr = dis.disassembleInstruction(victim_bytes_all) orelse continue;
|
|
|
|
const victim_instr = dis.disassembleInstruction(victim_bytes_all) orelse continue;
|
|
|
|
const victim_size = victim_instr.instruction.length;
|
|
|
|
const victim_size = victim_instr.instruction.length;
|
|
|
|
const victim_bytes = victim_bytes_all[0..victim_size];
|
|
|
|
const victim_bytes = victim_bytes_all[0..victim_size];
|
|
|
|
|
|
|
|
|
|
|
|
// Check locks for victim.
|
|
|
|
|
|
|
|
for (0..victim_size) |i| {
|
|
|
|
for (0..victim_size) |i| {
|
|
|
|
if (locked_bytes.isSet(neighbor_offset + i)) {
|
|
|
|
if (locked_bytes.isSet(neighbor_offset + i)) {
|
|
|
|
continue :neighbor;
|
|
|
|
continue :neighbor;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Save original bytes to revert.
|
|
|
|
// Save original bytes to revert if constraints cannot be solved.
|
|
|
|
var victim_orig_bytes: [15]u8 = undefined;
|
|
|
|
var victim_orig_bytes: [15]u8 = undefined;
|
|
|
|
@memcpy(victim_orig_bytes[0..victim_size], victim_bytes);
|
|
|
|
@memcpy(victim_orig_bytes[0..victim_size], victim_bytes);
|
|
|
|
|
|
|
|
|
|
|
|
// OUTER LOOP: J_Patch
|
|
|
|
// OUTER LOOP: J_Patch
|
|
|
|
// Iterate possible offsets 'k' inside the victim for the patch jump.
|
|
|
|
// Iterate possible offsets 'k' inside the victim for the patch jump.
|
|
|
|
// J_Patch is 5 bytes. It can extend beyond victim.
|
|
|
|
var k: u8 = 1;
|
|
|
|
for (1..victim_size) |k| {
|
|
|
|
while (k < victim_size) : (k += 1) {
|
|
|
|
// Check if short jump from P reaches V+k
|
|
|
|
|
|
|
|
const target: i64 = @intCast(neighbor_offset + k);
|
|
|
|
const target: i64 = @intCast(neighbor_offset + k);
|
|
|
|
const source: i64 = @intCast(request.offset + 2);
|
|
|
|
const source: i64 = @intCast(request.offset + 2);
|
|
|
|
const disp = target - source;
|
|
|
|
const disp = target - source;
|
|
|
|
if (disp > 127 or disp < -128) continue; // Should be covered by loop bounds, but be safe.
|
|
|
|
if (disp > 127 or disp < -128) continue;
|
|
|
|
|
|
|
|
|
|
|
|
const patch_flicken: Flicken = if (request.flicken == .nop)
|
|
|
|
const patch_flicken: Flicken = if (request.flicken == .nop)
|
|
|
|
.{ .name = "nop", .bytes = request.bytes[0..request.size] }
|
|
|
|
.{ .name = "nop", .bytes = request.bytes[0..request.size] }
|
|
|
|
else
|
|
|
|
else
|
|
|
|
patcher.flicken.entries.get(@intFromEnum(request.flicken)).value;
|
|
|
|
flicken_templates.entries.get(@intFromEnum(request.flicken)).value;
|
|
|
|
|
|
|
|
|
|
|
|
// Constraints for J_Patch:
|
|
|
|
// Constraints for J_Patch:
|
|
|
|
// Bytes [0 .. victim_size - k] are free (inside victim).
|
|
|
|
// Bytes [0 .. victim_size - k] are free (inside victim).
|
|
|
|
@@ -569,19 +579,18 @@ fn attemptNeighborEviction(
|
|
|
|
patch_flicken.size(),
|
|
|
|
patch_flicken.size(),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
while (patch_pii.next(&patcher.address_allocator)) |patch_range| {
|
|
|
|
while (patch_pii.next(.{ .count = 16 })) |patch_range| {
|
|
|
|
// J_Patch MUST NOT use prefixes, because it's punned inside J_Victim.
|
|
|
|
// J_Patch MUST NOT use prefixes, because it's punned inside J_Victim.
|
|
|
|
// Adding prefixes would shift J_Patch relative to J_Victim, making constraints harder.
|
|
|
|
// Adding prefixes would shift J_Patch relative to J_Victim, making constraints harder.
|
|
|
|
if (patch_pii.num_prefixes > 0) break;
|
|
|
|
if (patch_pii.num_prefixes > 0) break;
|
|
|
|
|
|
|
|
|
|
|
|
try pages_made_writable.ensureUnusedCapacity(arena, touchedPageCount(patch_range));
|
|
|
|
try pages_made_writable.ensureUnusedCapacity(arena, touchedPageCount(patch_range));
|
|
|
|
patcher.ensureRangeWritable(patch_range, pages_made_writable) catch |err| switch (err) {
|
|
|
|
ensureRangeWritable(patch_range, pages_made_writable) catch |err| switch (err) {
|
|
|
|
error.MappingAlreadyExists => continue,
|
|
|
|
error.MappingAlreadyExists => continue,
|
|
|
|
else => return err,
|
|
|
|
else => return err,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Tentatively write J_Patch to memory to set constraints for J_Victim.
|
|
|
|
// Tentatively write J_Patch to memory to set constraints for J_Victim.
|
|
|
|
// We must perform the write logic manually because applyPatch assumes request struct.
|
|
|
|
|
|
|
|
// We only need to write the bytes of J_Patch that land inside the victim.
|
|
|
|
// We only need to write the bytes of J_Patch that land inside the victim.
|
|
|
|
{
|
|
|
|
{
|
|
|
|
const jmp_target = patch_range.start;
|
|
|
|
const jmp_target = patch_range.start;
|
|
|
|
@@ -602,15 +611,15 @@ fn attemptNeighborEviction(
|
|
|
|
|
|
|
|
|
|
|
|
var victim_pii = PatchInstructionIterator.init(
|
|
|
|
var victim_pii = PatchInstructionIterator.init(
|
|
|
|
victim_bytes_all,
|
|
|
|
victim_bytes_all,
|
|
|
|
@intCast(k),
|
|
|
|
k,
|
|
|
|
victim_flicken.size(),
|
|
|
|
victim_flicken.size(),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
while (victim_pii.next(&patcher.address_allocator)) |victim_range| {
|
|
|
|
while (victim_pii.next(.{ .count = 16 })) |victim_range| {
|
|
|
|
if (patch_range.touches(victim_range)) continue;
|
|
|
|
if (patch_range.touches(victim_range)) continue;
|
|
|
|
|
|
|
|
|
|
|
|
try pages_made_writable.ensureUnusedCapacity(arena, touchedPageCount(victim_range));
|
|
|
|
try pages_made_writable.ensureUnusedCapacity(arena, touchedPageCount(victim_range));
|
|
|
|
patcher.ensureRangeWritable(victim_range, pages_made_writable) catch |err| switch (err) {
|
|
|
|
ensureRangeWritable(victim_range, pages_made_writable) catch |err| switch (err) {
|
|
|
|
error.MappingAlreadyExists => continue,
|
|
|
|
error.MappingAlreadyExists => continue,
|
|
|
|
else => return err,
|
|
|
|
else => return err,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@@ -620,55 +629,48 @@ fn attemptNeighborEviction(
|
|
|
|
// 1. Write Patch Trampoline (J_Patch target)
|
|
|
|
// 1. Write Patch Trampoline (J_Patch target)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
const trampoline: [*]u8 = @ptrFromInt(patch_range.getStart(u64));
|
|
|
|
const trampoline: [*]u8 = @ptrFromInt(patch_range.getStart(u64));
|
|
|
|
@memcpy(trampoline, patch_flicken.bytes);
|
|
|
|
var reloc_info: ?RelocInfo = null;
|
|
|
|
if (request.flicken == .nop) {
|
|
|
|
if (request.flicken == .nop) {
|
|
|
|
const instr = dis.disassembleInstruction(patch_flicken.bytes).?;
|
|
|
|
reloc_info = .{
|
|
|
|
try relocateInstruction(
|
|
|
|
.instr = dis.disassembleInstruction(patch_flicken.bytes).?,
|
|
|
|
instr,
|
|
|
|
.old_addr = @intFromPtr(request.bytes.ptr),
|
|
|
|
@intCast(patch_range.start),
|
|
|
|
};
|
|
|
|
trampoline[0..patch_flicken.bytes.len],
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Jmp back from Patch Trampoline to original code (after request)
|
|
|
|
commitTrampoline(
|
|
|
|
trampoline[patch_flicken.bytes.len] = jump_rel32;
|
|
|
|
trampoline,
|
|
|
|
const ret_addr: i64 = @intCast(@intFromPtr(&request.bytes[request.size]));
|
|
|
|
patch_flicken.bytes,
|
|
|
|
const from = patch_range.end;
|
|
|
|
reloc_info,
|
|
|
|
const jmp_back_disp: i32 = @intCast(ret_addr - from);
|
|
|
|
@intFromPtr(request.bytes.ptr) + request.size,
|
|
|
|
mem.writeInt(i32, trampoline[patch_flicken.bytes.len + 1 ..][0..4], jmp_back_disp, .little);
|
|
|
|
) catch |err| switch (err) {
|
|
|
|
|
|
|
|
error.RelocationOverflow => continue,
|
|
|
|
|
|
|
|
else => return err,
|
|
|
|
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Write Victim Trampoline (J_Victim target)
|
|
|
|
// 2. Write Victim Trampoline (J_Victim target)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
const trampoline: [*]u8 = @ptrFromInt(victim_range.getStart(u64));
|
|
|
|
const trampoline: [*]u8 = @ptrFromInt(victim_range.getStart(u64));
|
|
|
|
@memcpy(trampoline, victim_orig_bytes[0..victim_size]);
|
|
|
|
commitTrampoline(
|
|
|
|
// Relocate victim instruction
|
|
|
|
trampoline,
|
|
|
|
const instr = dis.disassembleInstruction(victim_orig_bytes[0..victim_size]).?;
|
|
|
|
victim_orig_bytes[0..victim_size],
|
|
|
|
try relocateInstruction(
|
|
|
|
.{
|
|
|
|
instr,
|
|
|
|
.instr = dis.disassembleInstruction(victim_orig_bytes[0..victim_size]).?,
|
|
|
|
@intCast(victim_range.start),
|
|
|
|
.old_addr = @intFromPtr(victim_bytes_all.ptr),
|
|
|
|
trampoline[0..victim_size],
|
|
|
|
},
|
|
|
|
);
|
|
|
|
@intFromPtr(victim_bytes_all.ptr) + victim_size,
|
|
|
|
// Jmp back from Victim Trampoline to original code (after victim)
|
|
|
|
) catch |err| switch (err) {
|
|
|
|
trampoline[victim_size] = jump_rel32;
|
|
|
|
error.RelocationOverflow => continue,
|
|
|
|
const ret_addr: i64 = @intCast(@intFromPtr(&victim_bytes_all[victim_size]));
|
|
|
|
else => return err,
|
|
|
|
const from = victim_range.end;
|
|
|
|
};
|
|
|
|
const jmp_back_disp: i32 = @intCast(ret_addr - from);
|
|
|
|
|
|
|
|
mem.writeInt(i32, trampoline[victim_size + 1 ..][0..4], jmp_back_disp, .little);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Write J_Victim (overwrites head of J_Patch which is fine, we just used it for constraints)
|
|
|
|
// 3. Write J_Victim (overwrites head of J_Patch which is fine)
|
|
|
|
applyPatch(
|
|
|
|
commitJump(
|
|
|
|
// Create a fake request for the victim part
|
|
|
|
victim_bytes_all.ptr,
|
|
|
|
.{
|
|
|
|
@intCast(victim_range.start),
|
|
|
|
.flicken = .nop, // Irrelevant, unused by applyPatch for jump writing
|
|
|
|
|
|
|
|
.offset = neighbor_offset,
|
|
|
|
|
|
|
|
.size = @intCast(victim_size),
|
|
|
|
|
|
|
|
.bytes = victim_bytes_all,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
victim_flicken, // Unused by applyPatch for jump writing
|
|
|
|
|
|
|
|
victim_range,
|
|
|
|
|
|
|
|
victim_pii.num_prefixes,
|
|
|
|
victim_pii.num_prefixes,
|
|
|
|
) catch unreachable; // Should fit because we allocated it
|
|
|
|
k, // Total size for padding is limited to k to preserve J_Patch tail
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. Write J_Short at request
|
|
|
|
// 4. Write J_Short at request
|
|
|
|
request.bytes[0] = jump_rel8;
|
|
|
|
request.bytes[0] = jump_rel8;
|
|
|
|
@@ -678,8 +680,8 @@ fn attemptNeighborEviction(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. Locking
|
|
|
|
// 5. Locking
|
|
|
|
try patcher.address_allocator.block(patcher.gpa, patch_range, 0);
|
|
|
|
try address_allocator.block(gpa, patch_range, 0);
|
|
|
|
try patcher.address_allocator.block(patcher.gpa, victim_range, 0);
|
|
|
|
try address_allocator.block(gpa, victim_range, 0);
|
|
|
|
|
|
|
|
|
|
|
|
locked_bytes.setRangeValue(
|
|
|
|
locked_bytes.setRangeValue(
|
|
|
|
.{ .start = request.offset, .end = request.offset + request.size },
|
|
|
|
.{ .start = request.offset, .end = request.offset + request.size },
|
|
|
|
@@ -706,6 +708,10 @@ fn attemptNeighborEviction(
|
|
|
|
return false;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Applies a standard patch (T1/B1/B2) where the instruction is replaced by a jump to a trampoline.
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// This handles the logic of writing the trampoline content (including relocation) and
|
|
|
|
|
|
|
|
/// overwriting the original instruction with a `JMP` (plus prefixes/padding).
|
|
|
|
fn applyPatch(
|
|
|
|
fn applyPatch(
|
|
|
|
request: PatchRequest,
|
|
|
|
request: PatchRequest,
|
|
|
|
flicken: Flicken,
|
|
|
|
flicken: Flicken,
|
|
|
|
@@ -713,51 +719,78 @@ fn applyPatch(
|
|
|
|
num_prefixes: u8,
|
|
|
|
num_prefixes: u8,
|
|
|
|
) !void {
|
|
|
|
) !void {
|
|
|
|
const flicken_addr: [*]u8 = @ptrFromInt(allocated_range.getStart(u64));
|
|
|
|
const flicken_addr: [*]u8 = @ptrFromInt(allocated_range.getStart(u64));
|
|
|
|
const flicken_slice = flicken_addr[0..flicken.size()];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const jump_to_offset: i32 = blk: {
|
|
|
|
// Commit Trampoline
|
|
|
|
const from: i64 = @intCast(@intFromPtr(&request.bytes[
|
|
|
|
var reloc_info: ?RelocInfo = null;
|
|
|
|
num_prefixes + jump_rel32_size
|
|
|
|
|
|
|
|
]));
|
|
|
|
|
|
|
|
const to = allocated_range.start;
|
|
|
|
|
|
|
|
break :blk @intCast(to - from);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const jump_back_offset: i32 = blk: {
|
|
|
|
|
|
|
|
const from = allocated_range.end;
|
|
|
|
|
|
|
|
const to: i64 = @intCast(@intFromPtr(&request.bytes[request.size]));
|
|
|
|
|
|
|
|
break :blk @intCast(to - from);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
// The jumps have to be in the opposite direction.
|
|
|
|
|
|
|
|
assert(math.sign(jump_to_offset) * math.sign(jump_back_offset) < 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Write to the trampoline first, because for the `nop` flicken `flicken.bytes` points to
|
|
|
|
|
|
|
|
// `request.bytes` which we overwrite in the next step.
|
|
|
|
|
|
|
|
@memcpy(flicken_addr, flicken.bytes);
|
|
|
|
|
|
|
|
if (request.flicken == .nop) {
|
|
|
|
if (request.flicken == .nop) {
|
|
|
|
const instr_bytes = request.bytes[0..request.size];
|
|
|
|
reloc_info = .{
|
|
|
|
const instr = dis.disassembleInstruction(instr_bytes).?;
|
|
|
|
.instr = dis.disassembleInstruction(request.bytes[0..request.size]).?,
|
|
|
|
|
|
|
|
.old_addr = @intFromPtr(request.bytes.ptr),
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const ret_addr = @intFromPtr(request.bytes.ptr) + request.size;
|
|
|
|
|
|
|
|
try commitTrampoline(flicken_addr, flicken.bytes, reloc_info, ret_addr);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Commit Jump (Patch)
|
|
|
|
|
|
|
|
commitJump(request.bytes.ptr, @intCast(allocated_range.start), num_prefixes, request.size);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const RelocInfo = struct {
|
|
|
|
|
|
|
|
instr: dis.BundledInstruction,
|
|
|
|
|
|
|
|
old_addr: u64,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Helper to write code into a trampoline.
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// It copies the original bytes (or flicken content), relocates any RIP-relative instructions
|
|
|
|
|
|
|
|
/// to be valid at the new address, and appends a jump back to the instruction stream.
|
|
|
|
|
|
|
|
fn commitTrampoline(
|
|
|
|
|
|
|
|
trampoline_ptr: [*]u8,
|
|
|
|
|
|
|
|
content: []const u8,
|
|
|
|
|
|
|
|
reloc_info: ?RelocInfo,
|
|
|
|
|
|
|
|
return_addr: u64,
|
|
|
|
|
|
|
|
) !void {
|
|
|
|
|
|
|
|
@memcpy(trampoline_ptr[0..content.len], content);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (reloc_info) |info| {
|
|
|
|
try relocateInstruction(
|
|
|
|
try relocateInstruction(
|
|
|
|
instr,
|
|
|
|
info.instr,
|
|
|
|
@intCast(allocated_range.start),
|
|
|
|
@intFromPtr(trampoline_ptr),
|
|
|
|
flicken_slice[0..request.size],
|
|
|
|
trampoline_ptr[0..content.len],
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
flicken_slice[flicken.bytes.len] = jump_rel32;
|
|
|
|
|
|
|
|
const jump_back_location = flicken_slice[flicken.bytes.len + 1 ..][0..4];
|
|
|
|
|
|
|
|
mem.writeInt(i32, jump_back_location, jump_back_offset, .little);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@memcpy(request.bytes[0..num_prefixes], prefixes[0..num_prefixes]);
|
|
|
|
// Write jump back
|
|
|
|
request.bytes[num_prefixes] = jump_rel32;
|
|
|
|
trampoline_ptr[content.len] = jump_rel32;
|
|
|
|
mem.writeInt(
|
|
|
|
const jump_src = @intFromPtr(trampoline_ptr) + content.len + jump_rel32_size;
|
|
|
|
i32,
|
|
|
|
const jump_disp: i32 = @intCast(@as(i64, @intCast(return_addr)) - @as(i64, @intCast(jump_src)));
|
|
|
|
request.bytes[num_prefixes + 1 ..][0..4],
|
|
|
|
mem.writeInt(i32, trampoline_ptr[content.len + 1 ..][0..4], jump_disp, .little);
|
|
|
|
jump_to_offset,
|
|
|
|
}
|
|
|
|
.little,
|
|
|
|
|
|
|
|
);
|
|
|
|
/// Helper to overwrite an instruction with a jump to a trampoline.
|
|
|
|
// Pad remaining with int3.
|
|
|
|
///
|
|
|
|
|
|
|
|
/// It handles writing optional prefixes (padding), the `0xE9` opcode, the relative offset,
|
|
|
|
|
|
|
|
/// and fills any remaining bytes of the original instruction with `INT3` to prevent
|
|
|
|
|
|
|
|
/// execution of garbage bytes.
|
|
|
|
|
|
|
|
fn commitJump(
|
|
|
|
|
|
|
|
from_ptr: [*]u8,
|
|
|
|
|
|
|
|
to_addr: u64,
|
|
|
|
|
|
|
|
num_prefixes: u8,
|
|
|
|
|
|
|
|
total_size: usize,
|
|
|
|
|
|
|
|
) void {
|
|
|
|
|
|
|
|
const prefixes_slice = from_ptr[0..num_prefixes];
|
|
|
|
|
|
|
|
@memcpy(prefixes_slice, prefixes[0..num_prefixes]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from_ptr[num_prefixes] = jump_rel32;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const jump_src = @intFromPtr(from_ptr) + num_prefixes + jump_rel32_size;
|
|
|
|
|
|
|
|
const jump_disp: i32 = @intCast(@as(i64, @intCast(to_addr)) - @as(i64, @intCast(jump_src)));
|
|
|
|
|
|
|
|
mem.writeInt(i32, from_ptr[num_prefixes + 1 ..][0..4], jump_disp, .little);
|
|
|
|
|
|
|
|
|
|
|
|
const patch_end_index = num_prefixes + jump_rel32_size;
|
|
|
|
const patch_end_index = num_prefixes + jump_rel32_size;
|
|
|
|
if (patch_end_index < request.size) {
|
|
|
|
if (patch_end_index < total_size) {
|
|
|
|
@memset(request.bytes[patch_end_index..request.size], int3);
|
|
|
|
@memset(from_ptr[patch_end_index..total_size], int3);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -780,7 +813,6 @@ fn touchedPageCount(range: Range) u32 {
|
|
|
|
|
|
|
|
|
|
|
|
/// Ensure `range` is mapped R|W. Assumes `pages_made_writable` has enough free capacity.
|
|
|
|
/// Ensure `range` is mapped R|W. Assumes `pages_made_writable` has enough free capacity.
|
|
|
|
fn ensureRangeWritable(
|
|
|
|
fn ensureRangeWritable(
|
|
|
|
patcher: *Patcher,
|
|
|
|
|
|
|
|
range: Range,
|
|
|
|
range: Range,
|
|
|
|
pages_made_writable: *std.AutoHashMapUnmanaged(u64, void),
|
|
|
|
pages_made_writable: *std.AutoHashMapUnmanaged(u64, void),
|
|
|
|
) !void {
|
|
|
|
) !void {
|
|
|
|
@@ -792,7 +824,7 @@ fn ensureRangeWritable(
|
|
|
|
// If the page is already writable, skip it.
|
|
|
|
// If the page is already writable, skip it.
|
|
|
|
if (pages_made_writable.get(page_addr)) |_| continue;
|
|
|
|
if (pages_made_writable.get(page_addr)) |_| continue;
|
|
|
|
// If we mapped it already we have to do mprotect, else mmap.
|
|
|
|
// If we mapped it already we have to do mprotect, else mmap.
|
|
|
|
const gop = try patcher.allocated_pages.getOrPut(patcher.gpa, page_addr);
|
|
|
|
const gop = try allocated_pages.getOrPut(gpa, page_addr);
|
|
|
|
if (gop.found_existing) {
|
|
|
|
if (gop.found_existing) {
|
|
|
|
const ptr: [*]align(page_size) u8 = @ptrFromInt(page_addr);
|
|
|
|
const ptr: [*]align(page_size) u8 = @ptrFromInt(page_addr);
|
|
|
|
try posix.mprotect(ptr[0..page_addr], protection);
|
|
|
|
try posix.mprotect(ptr[0..page_addr], protection);
|
|
|
|
@@ -810,8 +842,8 @@ fn ensureRangeWritable(
|
|
|
|
// (executable, OS, dynamic loader,...) allocated something there.
|
|
|
|
// (executable, OS, dynamic loader,...) allocated something there.
|
|
|
|
// We block this so we don't try this page again in the future,
|
|
|
|
// We block this so we don't try this page again in the future,
|
|
|
|
// saving a bunch of syscalls.
|
|
|
|
// saving a bunch of syscalls.
|
|
|
|
try patcher.address_allocator.block(
|
|
|
|
try address_allocator.block(
|
|
|
|
patcher.gpa,
|
|
|
|
gpa,
|
|
|
|
.{ .start = @intCast(page_addr), .end = @intCast(page_addr + page_size) },
|
|
|
|
.{ .start = @intCast(page_addr), .end = @intCast(page_addr + page_size) },
|
|
|
|
page_size,
|
|
|
|
page_size,
|
|
|
|
);
|
|
|
|
);
|
|
|
|
@@ -835,6 +867,7 @@ const PatchInstructionIterator = struct {
|
|
|
|
num_prefixes: u8,
|
|
|
|
num_prefixes: u8,
|
|
|
|
pli: PatchLocationIterator,
|
|
|
|
pli: PatchLocationIterator,
|
|
|
|
valid_range: Range,
|
|
|
|
valid_range: Range,
|
|
|
|
|
|
|
|
allocated_count: u64,
|
|
|
|
|
|
|
|
|
|
|
|
fn init(
|
|
|
|
fn init(
|
|
|
|
bytes: []const u8,
|
|
|
|
bytes: []const u8,
|
|
|
|
@@ -851,12 +884,26 @@ const PatchInstructionIterator = struct {
|
|
|
|
.num_prefixes = 0,
|
|
|
|
.num_prefixes = 0,
|
|
|
|
.pli = pli,
|
|
|
|
.pli = pli,
|
|
|
|
.valid_range = valid_range,
|
|
|
|
.valid_range = valid_range,
|
|
|
|
|
|
|
|
.allocated_count = 0,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub const Strategy = union(enum) {
|
|
|
|
|
|
|
|
/// Iterates through all possible ranges.
|
|
|
|
|
|
|
|
/// Useful for finding the optimal allocation (fewest prefixes).
|
|
|
|
|
|
|
|
exhaustive: void,
|
|
|
|
|
|
|
|
/// Limits the search to `count` allocation attempts per valid constraint range found by the
|
|
|
|
|
|
|
|
/// PatchLocationIterator.
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// This acts as a heuristic to prevent worst-case performance (scanning every byte of a 2GB
|
|
|
|
|
|
|
|
/// gap) while still offering better density than a purely greedy approach. A count of 1 is
|
|
|
|
|
|
|
|
/// equivalent to a greedy strategy.
|
|
|
|
|
|
|
|
count: u64,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
fn next(
|
|
|
|
fn next(
|
|
|
|
pii: *PatchInstructionIterator,
|
|
|
|
pii: *PatchInstructionIterator,
|
|
|
|
address_allocator: *AddressAllocator,
|
|
|
|
strategy: Strategy,
|
|
|
|
) ?Range {
|
|
|
|
) ?Range {
|
|
|
|
const State = enum {
|
|
|
|
const State = enum {
|
|
|
|
allocation,
|
|
|
|
allocation,
|
|
|
|
@@ -870,11 +917,23 @@ const PatchInstructionIterator = struct {
|
|
|
|
pii.valid_range,
|
|
|
|
pii.valid_range,
|
|
|
|
)) |allocated_range| {
|
|
|
|
)) |allocated_range| {
|
|
|
|
assert(allocated_range.size() == pii.flicken_size);
|
|
|
|
assert(allocated_range.size() == pii.flicken_size);
|
|
|
|
|
|
|
|
pii.allocated_count += 1;
|
|
|
|
// Advancing the valid range, such that the next call to `findAllocation` won't
|
|
|
|
// Advancing the valid range, such that the next call to `findAllocation` won't
|
|
|
|
// find the same range again.
|
|
|
|
// find the same range again.
|
|
|
|
pii.valid_range.start = allocated_range.start + 1;
|
|
|
|
switch (strategy) {
|
|
|
|
|
|
|
|
.exhaustive => pii.valid_range.start = allocated_range.start + 1,
|
|
|
|
|
|
|
|
.count => |c| {
|
|
|
|
|
|
|
|
if (pii.allocated_count >= c) {
|
|
|
|
|
|
|
|
pii.valid_range.start = pii.valid_range.end;
|
|
|
|
|
|
|
|
pii.allocated_count = 0;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
pii.valid_range.start = allocated_range.start + 1;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
}
|
|
|
|
return allocated_range;
|
|
|
|
return allocated_range;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
|
|
|
|
pii.allocated_count = 0;
|
|
|
|
continue :blk .range;
|
|
|
|
continue :blk .range;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|