mirror of
https://github.com/frida/frida-core.git
synced 2026-11-09 10:09:38 -10:00
Compare commits
94
Commits
No files matched your search
@@ -3,6 +3,10 @@
|
||||
/deps/
|
||||
/src/android-helper/build/
|
||||
/src/barebone/agent/target/
|
||||
/src/barebone/agent/target-linux/
|
||||
/src/barebone/agent/target-win9x/
|
||||
/src/barebone/agent/linux/*.o
|
||||
/src/barebone/agent/linux/*.o.bin
|
||||
/src/barebone/agent/xpacks/
|
||||
/src/barebone/helpers/memory-scanner/target/
|
||||
/src/barebone/helpers/target/
|
||||
|
||||
@@ -63,7 +63,7 @@ if build_agent
|
||||
link_depends: extra_link_depends,
|
||||
vs_module_defs: 'frida-agent.def',
|
||||
override_options: ['b_asneeded=true'],
|
||||
dependencies: [gio_dep, gee_dep, gum_dep, gumjs_dep, base_dep, pipe_dep, tls_provider_dep, payload_dep],
|
||||
dependencies: [gio_dep, gee_dep, gum_dep, gumjs_dep, gumpharo_dep, base_dep, pipe_dep, tls_provider_dep, payload_dep],
|
||||
)
|
||||
|
||||
operations = ['--move', 'constructor', 'frida_libc_shim_init', 'first']
|
||||
|
||||
@@ -2274,7 +2274,11 @@ namespace Frida {
|
||||
/**
|
||||
* The V8 runtime.
|
||||
*/
|
||||
V8;
|
||||
V8,
|
||||
/**
|
||||
* The Pharo runtime.
|
||||
*/
|
||||
PHARO;
|
||||
|
||||
public static ScriptRuntime from_nick (string nick) throws Error {
|
||||
return Marshal.enum_from_nick<ScriptRuntime> (nick);
|
||||
|
||||
@@ -49,7 +49,7 @@ payload = static_library('frida-payload-' + api_version, payload_sources,
|
||||
],
|
||||
vala_header: 'frida-payload.h',
|
||||
vala_vapi: f'frida-payload-@[email protected]',
|
||||
dependencies: [gio_dep, gee_dep, json_glib_dep, gum_dep, gumjs_dep, base_dep],
|
||||
dependencies: [gio_dep, gee_dep, json_glib_dep, gum_dep, gumjs_dep, gumpharo_dep, base_dep],
|
||||
install: true,
|
||||
install_dir: [true, header_install_dir, true],
|
||||
)
|
||||
|
||||
+155
-17
@@ -91,7 +91,27 @@ namespace Frida {
|
||||
if (name == null)
|
||||
name = "script%u".printf (script_id.handle);
|
||||
|
||||
Gum.ScriptBackend backend = pick_backend (options.runtime);
|
||||
ScriptRuntime runtime = options.runtime;
|
||||
if (runtime == DEFAULT)
|
||||
runtime = preferred_runtime;
|
||||
|
||||
#if HAVE_GUMPHARO
|
||||
if (runtime == PHARO) {
|
||||
if (source == null)
|
||||
throw new Error.NOT_SUPPORTED ("The Pharo runtime cannot load precompiled scripts");
|
||||
|
||||
var instance = new PharoScriptInstance (script_id, new Gum.PharoScript (name, source));
|
||||
instances[script_id] = instance;
|
||||
|
||||
instance.closed.connect (on_instance_closed);
|
||||
instance.message.connect (on_instance_message);
|
||||
instance.debug_message.connect (on_instance_debug_message);
|
||||
|
||||
return instance;
|
||||
}
|
||||
#endif
|
||||
|
||||
Gum.ScriptBackend backend = pick_backend (runtime);
|
||||
|
||||
Gum.Script script;
|
||||
try {
|
||||
@@ -109,7 +129,8 @@ namespace Frida {
|
||||
return Source.REMOVE;
|
||||
});
|
||||
|
||||
var instance = new ScriptInstance (script_id, script);
|
||||
var js_instance = new JSScriptInstance (script_id, script);
|
||||
ScriptInstance instance = js_instance;
|
||||
instances[script_id] = instance;
|
||||
|
||||
instance.closed.connect (on_instance_closed);
|
||||
@@ -175,7 +196,9 @@ namespace Frida {
|
||||
}
|
||||
|
||||
public Gum.Script eternalize_script (AgentScriptId script_id) throws Error {
|
||||
var instance = get_instance (script_id);
|
||||
var instance = get_instance (script_id) as JSScriptInstance;
|
||||
if (instance == null)
|
||||
throw new Error.NOT_SUPPORTED ("Only JavaScript scripts may be eternalized");
|
||||
|
||||
var script = instance.eternalize ();
|
||||
|
||||
@@ -231,7 +254,7 @@ namespace Frida {
|
||||
source.attach (Gum.ScriptBackend.get_scheduler ().get_js_context ());
|
||||
}
|
||||
|
||||
public sealed class ScriptInstance : Object, RpcPeer {
|
||||
public abstract class ScriptInstance : Object, RpcPeer {
|
||||
public signal void closed ();
|
||||
public signal void message (string json, Bytes? data);
|
||||
public signal void debug_message (string message);
|
||||
@@ -241,6 +264,120 @@ namespace Frida {
|
||||
construct;
|
||||
}
|
||||
|
||||
public abstract async void close ();
|
||||
public abstract async void flush ();
|
||||
public abstract async void load () throws Error;
|
||||
public abstract void post (string json, Bytes? data) throws Error;
|
||||
public abstract async void prepare_for_termination (TerminationReason reason);
|
||||
public abstract void unprepare_for_termination ();
|
||||
|
||||
public abstract async void post_rpc_message (string json, Bytes? data, Cancellable? cancellable)
|
||||
throws Error, IOError;
|
||||
|
||||
public virtual void interrupt () throws Error {
|
||||
throw new Error.NOT_SUPPORTED ("Interrupt is not available for this runtime");
|
||||
}
|
||||
|
||||
public virtual async void terminate () {
|
||||
yield close ();
|
||||
}
|
||||
|
||||
public virtual void enable_debugger () throws Error {
|
||||
throw new Error.NOT_SUPPORTED ("Debugger is not available for this runtime");
|
||||
}
|
||||
|
||||
public virtual void disable_debugger () {
|
||||
}
|
||||
|
||||
public virtual void post_debug_message (string message) throws Error {
|
||||
throw new Error.NOT_SUPPORTED ("Debugger is not available for this runtime");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if HAVE_GUMPHARO
|
||||
public sealed class PharoScriptInstance : ScriptInstance {
|
||||
private Gum.PharoScript? script;
|
||||
|
||||
private State state = CREATED;
|
||||
|
||||
private enum State {
|
||||
CREATED,
|
||||
LOADED,
|
||||
UNLOADED,
|
||||
DESTROYED
|
||||
}
|
||||
|
||||
private RpcClient rpc_client;
|
||||
|
||||
public PharoScriptInstance (AgentScriptId script_id, Gum.PharoScript script) {
|
||||
Object (script_id: script_id);
|
||||
|
||||
this.script = script;
|
||||
script.set_message_handler (on_message);
|
||||
}
|
||||
|
||||
construct {
|
||||
rpc_client = new RpcClient (this);
|
||||
}
|
||||
|
||||
public override async void close () {
|
||||
if (state == DESTROYED)
|
||||
return;
|
||||
|
||||
if (state == LOADED) {
|
||||
script.unload ();
|
||||
state = UNLOADED;
|
||||
}
|
||||
|
||||
script = null;
|
||||
state = DESTROYED;
|
||||
|
||||
closed ();
|
||||
}
|
||||
|
||||
public override async void flush () {
|
||||
yield close ();
|
||||
}
|
||||
|
||||
public override async void load () throws Error {
|
||||
if (state != CREATED)
|
||||
throw new Error.INVALID_OPERATION ("Script cannot be loaded in its current state");
|
||||
|
||||
script.load ();
|
||||
|
||||
state = LOADED;
|
||||
}
|
||||
|
||||
public override void post (string json, Bytes? data) throws Error {
|
||||
if (state != LOADED)
|
||||
throw new Error.INVALID_OPERATION ("Only active scripts may be posted to");
|
||||
|
||||
script.post (json, data);
|
||||
}
|
||||
|
||||
public override async void prepare_for_termination (TerminationReason reason) {
|
||||
}
|
||||
|
||||
public override void unprepare_for_termination () {
|
||||
}
|
||||
|
||||
private void on_message (Gum.PharoScript script, string json, Bytes? data) {
|
||||
bool handled = rpc_client.try_handle_message (json);
|
||||
if (!handled)
|
||||
this.message (json, data);
|
||||
}
|
||||
|
||||
public override async void post_rpc_message (string json, Bytes? data, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
if (script == null)
|
||||
throw new Error.INVALID_OPERATION ("Script is destroyed");
|
||||
script.post (json, data);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public sealed class JSScriptInstance : ScriptInstance {
|
||||
public Gum.Script? script {
|
||||
get {
|
||||
return _script;
|
||||
@@ -274,7 +411,7 @@ namespace Frida {
|
||||
|
||||
private RpcClient rpc_client;
|
||||
|
||||
public ScriptInstance (AgentScriptId script_id, Gum.Script script) {
|
||||
public JSScriptInstance (AgentScriptId script_id, Gum.Script script) {
|
||||
Object (script_id: script_id, script: script);
|
||||
}
|
||||
|
||||
@@ -282,7 +419,7 @@ namespace Frida {
|
||||
rpc_client = new RpcClient (this);
|
||||
}
|
||||
|
||||
public async void close () {
|
||||
public override async void close () {
|
||||
if (close_request != null) {
|
||||
try {
|
||||
yield close_request.future.wait_async (null);
|
||||
@@ -338,7 +475,7 @@ namespace Frida {
|
||||
close_request.resolve (true);
|
||||
}
|
||||
|
||||
public async void flush () {
|
||||
public override async void flush () {
|
||||
if (close_request == null)
|
||||
close.begin ();
|
||||
|
||||
@@ -349,7 +486,7 @@ namespace Frida {
|
||||
}
|
||||
}
|
||||
|
||||
public async void load () throws Error {
|
||||
public override async void load () throws Error {
|
||||
if (state != CREATED)
|
||||
throw new Error.INVALID_OPERATION ("Script cannot be loaded in its current state");
|
||||
|
||||
@@ -362,11 +499,11 @@ namespace Frida {
|
||||
load_request.resolve (true);
|
||||
}
|
||||
|
||||
public void interrupt () {
|
||||
public override void interrupt () throws Error {
|
||||
script.interrupt ();
|
||||
}
|
||||
|
||||
public async void terminate () {
|
||||
public override async void terminate () {
|
||||
script.interrupt ();
|
||||
|
||||
yield close ();
|
||||
@@ -397,7 +534,7 @@ namespace Frida {
|
||||
return result;
|
||||
}
|
||||
|
||||
public async void prepare_for_termination (TerminationReason reason) {
|
||||
public override async void prepare_for_termination (TerminationReason reason) {
|
||||
if (state == LOADED) {
|
||||
schedule_on_js_thread (() => {
|
||||
script.get_stalker ().flush ();
|
||||
@@ -408,7 +545,7 @@ namespace Frida {
|
||||
yield ensure_dispose_called (reason);
|
||||
}
|
||||
|
||||
public void unprepare_for_termination () {
|
||||
public override void unprepare_for_termination () {
|
||||
if (state == DISPOSED) {
|
||||
state = LOADED;
|
||||
dispose_request = null;
|
||||
@@ -448,7 +585,7 @@ namespace Frida {
|
||||
dispose_request.resolve (true);
|
||||
}
|
||||
|
||||
public void post (string json, Bytes? data) throws Error {
|
||||
public override void post (string json, Bytes? data) throws Error {
|
||||
switch (state) {
|
||||
case LOADING:
|
||||
case LOADED:
|
||||
@@ -460,17 +597,17 @@ namespace Frida {
|
||||
}
|
||||
}
|
||||
|
||||
public void enable_debugger () throws Error {
|
||||
public override void enable_debugger () throws Error {
|
||||
if (_script != null)
|
||||
_script.set_debug_message_handler (on_debug_message);
|
||||
}
|
||||
|
||||
public void disable_debugger () {
|
||||
public override void disable_debugger () {
|
||||
if (_script != null)
|
||||
_script.set_debug_message_handler (null);
|
||||
}
|
||||
|
||||
public void post_debug_message (string message) {
|
||||
public override void post_debug_message (string message) throws Error {
|
||||
if (_script != null)
|
||||
_script.post_debug_message (message);
|
||||
}
|
||||
@@ -485,7 +622,8 @@ namespace Frida {
|
||||
this.debug_message (message);
|
||||
}
|
||||
|
||||
private async void post_rpc_message (string json, Bytes? data, Cancellable? cancellable) throws Error, IOError {
|
||||
public override async void post_rpc_message (string json, Bytes? data, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
if (script == null)
|
||||
throw new Error.INVALID_OPERATION ("Script is destroyed");
|
||||
script.post (json, data);
|
||||
|
||||
+15
-2
@@ -12,14 +12,27 @@ frida_pipe_get_sddl_string_for_pipe (void)
|
||||
#define DACL_ACE_EVERYONE_RW L"(A;;GRGW;;;WD)"
|
||||
#define SACL_START L"S:"
|
||||
#define SACL_ACE_LOWINTEGRITY_NORW L"(ML;;NWNR;;;LW)"
|
||||
#define DACL_ACE_SYSTEM_RW L"(A;;GRGW;;;SY)"
|
||||
#define DACL_ACE_ADMINS_RW L"(A;;GRGW;;;BA)"
|
||||
|
||||
if (frida_pipe_is_windows_8_or_greater ())
|
||||
{
|
||||
return DACL_START_NOINHERIT DACL_ACE_APPCONTAINER_RW DACL_ACE_EVERYONE_RW SACL_START SACL_ACE_LOWINTEGRITY_NORW;
|
||||
return DACL_START_NOINHERIT
|
||||
DACL_ACE_APPCONTAINER_RW
|
||||
DACL_ACE_EVERYONE_RW
|
||||
DACL_ACE_SYSTEM_RW
|
||||
DACL_ACE_ADMINS_RW
|
||||
SACL_START
|
||||
SACL_ACE_LOWINTEGRITY_NORW;
|
||||
}
|
||||
else if (frida_pipe_is_windows_vista_or_greater ())
|
||||
{
|
||||
return DACL_START_NOINHERIT DACL_ACE_EVERYONE_RW SACL_START SACL_ACE_LOWINTEGRITY_NORW;
|
||||
return DACL_START_NOINHERIT
|
||||
DACL_ACE_EVERYONE_RW
|
||||
DACL_ACE_SYSTEM_RW
|
||||
DACL_ACE_ADMINS_RW
|
||||
SACL_START
|
||||
SACL_ACE_LOWINTEGRITY_NORW;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+10
-1
@@ -382,6 +382,12 @@ if have_in_tree_payloads
|
||||
endif
|
||||
gumjs_inspector_dep = dependency('frida-gumjs-inspector-1.0', default_options: ['default_library=static'])
|
||||
|
||||
gumpharo_dep = dependency('frida-gumpharo-' + api_version, required: false)
|
||||
have_gumpharo = gumpharo_dep.found()
|
||||
if have_gumpharo
|
||||
vala_flags += '--define=HAVE_GUMPHARO'
|
||||
endif
|
||||
|
||||
gee_options = [
|
||||
'default_library=static',
|
||||
'disable-introspection=true',
|
||||
@@ -527,7 +533,7 @@ have_droidy_backend = get_option('droidy_backend') \
|
||||
have_socket_backend = get_option('socket_backend').allowed()
|
||||
have_barebone_backend = get_option('barebone_backend') \
|
||||
.disable_if(not (quickjs_dep.found() and lzfse_dep.found())) \
|
||||
.disable_auto_if(not host_docks_mobile_devices) \
|
||||
.disable_auto_if(not host_docks_mobile_devices and host_os != 'android') \
|
||||
.allowed()
|
||||
compiler_backend_opt = get_option('compiler_backend') \
|
||||
.disable_auto_if(host_os == 'watchos')
|
||||
@@ -669,6 +675,9 @@ gumjs_inspector_vala_args = ['--pkg=frida-gumjs-inspector-1.0', gum_vala_args]
|
||||
|
||||
if have_in_tree_payloads
|
||||
gumjs_vala_args = ['--pkg=frida-gumjs-1.0', '--pkg=gio-2.0', gum_vala_args]
|
||||
if have_gumpharo
|
||||
gumjs_vala_args += ['--pkg=frida-gumpharo-' + api_version]
|
||||
endif
|
||||
|
||||
have_v8 = gumjs_dep.get_variable('gumjs_v8') == 'enabled'
|
||||
if have_v8
|
||||
|
||||
+1
-1
Submodule releng updated: abb8238970...0ad9eef364.
@@ -5,17 +5,22 @@ namespace Frida.Barebone {
|
||||
|
||||
private Cancellable io_cancellable = new Cancellable ();
|
||||
|
||||
private SocketConnection hostlink;
|
||||
private IOStream hostlink;
|
||||
private BufferedInputStream input;
|
||||
private OutputStream output;
|
||||
|
||||
private ByteOrder byte_order;
|
||||
private uint pointer_size;
|
||||
|
||||
private AgentConfig agent_config;
|
||||
private VsockTransportConfig? vsock_transport;
|
||||
private ImageConfig? image_config;
|
||||
private KernelKind kernel_kind;
|
||||
private KernelRelocation? relocation;
|
||||
private uint64 kernel_base;
|
||||
private Machine machine;
|
||||
private Allocator allocator;
|
||||
private Gee.List<SymbolInfo> kernel_symbols;
|
||||
|
||||
private Allocation elf_allocation;
|
||||
private Allocation config_allocation;
|
||||
@@ -26,15 +31,18 @@ namespace Frida.Barebone {
|
||||
private const int COMMAND_TIMEOUT_MS = 25000;
|
||||
|
||||
public static async AgentConnection open (AgentConfig agent_config, ImageConfig? image_config,
|
||||
KernelRelocation? relocation, uint64 kernel_base, Machine machine, Allocator allocator,
|
||||
Cancellable? cancellable) throws Error, IOError {
|
||||
KernelKind kernel_kind, KernelRelocation? relocation, uint64 kernel_base, Machine machine,
|
||||
Allocator allocator, Gee.List<SymbolInfo> kernel_symbols, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
var connection = new AgentConnection () {
|
||||
agent_config = agent_config,
|
||||
image_config = image_config,
|
||||
kernel_kind = kernel_kind,
|
||||
relocation = relocation,
|
||||
kernel_base = kernel_base,
|
||||
machine = machine,
|
||||
allocator = allocator,
|
||||
kernel_symbols = kernel_symbols,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -46,15 +54,33 @@ namespace Frida.Barebone {
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches to an agent already resident in the target, which brought its own
|
||||
* transport with it, such as the Linux kernel module exposing /dev/frida.
|
||||
*/
|
||||
public static async AgentConnection open_resident (IOStream stream, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
var connection = new AgentConnection () {
|
||||
byte_order = ByteOrder.HOST,
|
||||
pointer_size = (uint) sizeof (void *),
|
||||
};
|
||||
|
||||
connection.adopt_hostlink_streams (stream);
|
||||
connection.process_incoming_messages.begin ();
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
private const uint8 TRANSPORT_KIND_VIRTIO = 0;
|
||||
private const uint8 TRANSPORT_KIND_VSOCK = 1;
|
||||
private const uint8 TRANSPORT_KIND_VIRTIO_PCI = 2;
|
||||
|
||||
private async bool init_async (int io_priority, Cancellable? cancellable) throws Error, IOError {
|
||||
var transport_tag = yield resolve_transport (cancellable);
|
||||
|
||||
var gdb = machine.gdb;
|
||||
ByteOrder byte_order = gdb.byte_order;
|
||||
uint pointer_size = gdb.pointer_size;
|
||||
byte_order = gdb.byte_order;
|
||||
pointer_size = gdb.pointer_size;
|
||||
|
||||
Layout layout;
|
||||
uint64 preferred_base = 0;
|
||||
@@ -73,8 +99,6 @@ namespace Frida.Barebone {
|
||||
} else {
|
||||
layout = new Layout.empty ();
|
||||
}
|
||||
if (kernel_base == 0)
|
||||
throw new Error.NOT_SUPPORTED ("Missing kernel_base");
|
||||
|
||||
var symbols = new Gee.HashMap<string, SymbolInfo> ();
|
||||
var hash_builder = new SymbolHashBuilder ();
|
||||
@@ -101,13 +125,25 @@ namespace Frida.Barebone {
|
||||
}
|
||||
}
|
||||
|
||||
SymbolInfo? thread_block = symbols["thread_block"];
|
||||
if (thread_block == null)
|
||||
throw new Error.NOT_SUPPORTED ("Missing symbol for thread_block");
|
||||
KernelKind kind = kernel_kind;
|
||||
if (kind == AUTO)
|
||||
kind = (image_config != null) ? KernelKind.XNU : KernelKind.BARE;
|
||||
|
||||
SymbolInfo? panic = symbols["panic"];
|
||||
if (panic != null && machine is Arm64Machine)
|
||||
((Arm64Machine) machine).call_landing_zone = kernel_base + panic.offset;
|
||||
foreach (var s in kernel_symbols) {
|
||||
symbols[s.name] = s;
|
||||
hash_builder.add_symbol (s);
|
||||
}
|
||||
|
||||
KernelFlavor flavor;
|
||||
if (kind == XNU) {
|
||||
if (kernel_base == 0)
|
||||
throw new Error.NOT_SUPPORTED ("Missing kernel_base");
|
||||
flavor = new XnuKernelFlavor (machine, kernel_base, symbols);
|
||||
} else if (kind == WIN9X) {
|
||||
flavor = new Win9xKernelFlavor (machine, symbols);
|
||||
} else {
|
||||
flavor = new BareKernelFlavor (machine);
|
||||
}
|
||||
|
||||
Bytes symbol_data = hash_builder.build (byte_order);
|
||||
|
||||
@@ -134,14 +170,10 @@ namespace Frida.Barebone {
|
||||
return true;
|
||||
});
|
||||
|
||||
yield machine.enter_exception_level (1, 1000, cancellable);
|
||||
|
||||
yield run_until_thread_block (kernel_base + thread_block.offset, cancellable);
|
||||
yield flavor.prepare (cancellable);
|
||||
|
||||
size_t page_size = yield machine.query_page_size (cancellable);
|
||||
|
||||
yield ((Arm64Machine) machine).learn_permission_templates (kernel_base + thread_block.offset, cancellable);
|
||||
|
||||
elf_allocation = yield inject_elf (elf, raw_elf.bytes, page_size, machine, allocator, cancellable);
|
||||
|
||||
uint64 start_address = 0;
|
||||
@@ -187,14 +219,7 @@ namespace Frida.Barebone {
|
||||
},
|
||||
cancellable);
|
||||
|
||||
// The vphone research kernel panics on any synchronous exception taken while a
|
||||
// debugger is attached, which the worker hits in the allocator during gum_init.
|
||||
var arm64 = machine as Arm64Machine;
|
||||
bool post_inject_access_uses_bridge = arm64 != null && arm64.physical_memory != null;
|
||||
if (post_inject_access_uses_bridge)
|
||||
yield gdb.detach (cancellable);
|
||||
else
|
||||
yield gdb.continue (cancellable);
|
||||
yield flavor.settle (cancellable);
|
||||
yield establish_hostlink (cancellable);
|
||||
|
||||
process_incoming_messages.begin ();
|
||||
@@ -202,19 +227,6 @@ namespace Frida.Barebone {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async void run_until_thread_block (uint64 address, Cancellable? cancellable) throws Error, IOError {
|
||||
var gdb = machine.gdb;
|
||||
var bp = yield gdb.add_breakpoint (SOFT, address, 4, cancellable);
|
||||
|
||||
GDB.Breakpoint? hit = null;
|
||||
do {
|
||||
var exception = yield gdb.continue_until_exception (cancellable);
|
||||
hit = exception.breakpoint;
|
||||
} while (hit != bp);
|
||||
|
||||
yield bp.remove (cancellable);
|
||||
}
|
||||
|
||||
private async Variant resolve_transport (Cancellable? cancellable) throws Error, IOError {
|
||||
if (agent_config.transport is HostlinkTransportConfig)
|
||||
return yield connect_virtio_transport ((HostlinkTransportConfig) agent_config.transport, cancellable);
|
||||
@@ -257,9 +269,16 @@ namespace Frida.Barebone {
|
||||
private async Variant connect_virtio_transport (HostlinkTransportConfig config, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
var qmp = yield QmpClient.open (config.qmp, 0, cancellable);
|
||||
var link = yield qmp.open_hostlink (cancellable);
|
||||
var link = yield qmp.open_hostlink (config.bus, cancellable);
|
||||
adopt_hostlink_streams (link.connection);
|
||||
|
||||
if (config.bus != null) {
|
||||
return new Variant.tuple ({
|
||||
new Variant.byte (TRANSPORT_KIND_VIRTIO_PCI),
|
||||
new Variant.variant (new Variant.tuple ({}))
|
||||
});
|
||||
}
|
||||
|
||||
Variant[] virtio_cfg = { new Variant.uint64 (link.mmio), new Variant.uint32 (link.irq) };
|
||||
return new Variant.tuple ({
|
||||
new Variant.byte (TRANSPORT_KIND_VIRTIO),
|
||||
@@ -267,7 +286,7 @@ namespace Frida.Barebone {
|
||||
});
|
||||
}
|
||||
|
||||
private void adopt_hostlink_streams (SocketConnection connection) {
|
||||
private void adopt_hostlink_streams (IOStream connection) {
|
||||
hostlink = connection;
|
||||
input = (BufferedInputStream) Object.new (typeof (BufferedInputStream),
|
||||
"base-stream", hostlink.get_input_stream (),
|
||||
@@ -290,6 +309,30 @@ namespace Frida.Barebone {
|
||||
io_cancellable.cancel ();
|
||||
}
|
||||
|
||||
public async HostProcessInfo[] enumerate_processes (Cancellable? cancellable) throws Error, IOError {
|
||||
var response = yield execute_command (Command.ENUMERATE_PROCESSES, new Variant.tuple ({}), cancellable);
|
||||
if (!response.check_format_string ("a(us)", false))
|
||||
throw new Error.PROTOCOL ("Invalid enumerate_processes response format");
|
||||
|
||||
var result = new Gee.ArrayList<HostProcessInfo?> ();
|
||||
uint32 pid;
|
||||
unowned string path;
|
||||
var it = response.iterator ();
|
||||
while (it.next ("(u&s)", out pid, out path)) {
|
||||
result.add (HostProcessInfo (pid, basename_of (path),
|
||||
new HashTable<string, Variant> (str_hash, str_equal)));
|
||||
}
|
||||
var processes = new HostProcessInfo[result.size];
|
||||
for (int i = 0; i != result.size; i++)
|
||||
processes[i] = result[i];
|
||||
return processes;
|
||||
}
|
||||
|
||||
private static string basename_of (string path) {
|
||||
int start = path.last_index_of_char ('\\');
|
||||
return (start != -1) ? path[start + 1:] : path;
|
||||
}
|
||||
|
||||
public async AgentScriptId create_script (string source, Cancellable? cancellable) throws Error, IOError {
|
||||
var payload = new Variant ("s", source);
|
||||
var response = yield execute_command (Command.CREATE_SCRIPT, payload, cancellable);
|
||||
@@ -341,16 +384,25 @@ namespace Frida.Barebone {
|
||||
});
|
||||
timeout_source.attach (MainContext.get_thread_default ());
|
||||
|
||||
Variant response = null;
|
||||
try {
|
||||
return yield promise.future.wait_async (cancellable);
|
||||
response = yield promise.future.wait_async (cancellable);
|
||||
} finally {
|
||||
timeout_source.destroy ();
|
||||
}
|
||||
|
||||
if (!response.check_format_string ("(bv)", false))
|
||||
throw new Error.PROTOCOL ("Malformed reply from agent");
|
||||
bool succeeded;
|
||||
Variant detail;
|
||||
response.get ("(bv)", out succeeded, out detail);
|
||||
if (!succeeded)
|
||||
throw new Error.NOT_SUPPORTED ("%s", detail.get_string ());
|
||||
|
||||
return detail;
|
||||
}
|
||||
|
||||
private async void process_incoming_messages () {
|
||||
var byte_order = machine.gdb.byte_order;
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
size_t header_size = 4;
|
||||
@@ -503,10 +555,10 @@ namespace Frida.Barebone {
|
||||
|
||||
private Bytes frame_message (Command command, uint16 request_id, Variant payload) {
|
||||
var message = new Variant ("(yqv)", (uint8) command, request_id, payload);
|
||||
if (machine.gdb.byte_order != ByteOrder.HOST)
|
||||
if (byte_order != ByteOrder.HOST)
|
||||
message = message.byteswap ();
|
||||
var message_bytes = message.get_data_as_bytes ();
|
||||
return machine.gdb.make_buffer_builder ()
|
||||
return new BufferBuilder (byte_order, pointer_size)
|
||||
.append_uint32 ((uint32) message_bytes.get_size ())
|
||||
.append_bytes (message_bytes)
|
||||
.build ();
|
||||
@@ -520,6 +572,7 @@ namespace Frida.Barebone {
|
||||
REMAP_WRITABLE_PAGES = 5,
|
||||
MEMORY_PROTECT = 6,
|
||||
PATCH_CODE = 7,
|
||||
ENUMERATE_PROCESSES = 8,
|
||||
REPLY = 128,
|
||||
SCRIPT_MESSAGE = 129
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -13,9 +13,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.0"
|
||||
version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cexpr",
|
||||
@@ -63,9 +63,9 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
|
||||
@@ -4,6 +4,25 @@ version = "1.0.0"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
[lib]
|
||||
name = "frida_barebone_agent"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["rlib", "staticlib"]
|
||||
|
||||
# The XNU flavour is a freestanding executable the host injects and calls into.
|
||||
# The Linux flavour is the staticlib above, linked into a .ko by linux/Kbuild.
|
||||
[[bin]]
|
||||
name = "frida-barebone-agent"
|
||||
path = "src/main.rs"
|
||||
required-features = ["blob"]
|
||||
|
||||
[features]
|
||||
# Flavors the host injects and calls into, as opposed to the module Linux loads.
|
||||
blob = []
|
||||
xnu = ["blob"]
|
||||
win9x = ["blob"]
|
||||
linux = []
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
opt-level = "z"
|
||||
|
||||
@@ -21,10 +21,29 @@
|
||||
export AR_aarch64_unknown_none=aarch64-none-elf-ar
|
||||
export RANLIB_aarch64_unknown_none=aarch64-none-elf-ranlib
|
||||
|
||||
cargo build --release --features xnu
|
||||
|
||||
## Targets
|
||||
|
||||
The agent supports two kernels. One of them has to be picked; neither is a
|
||||
default, since neither follows from the host you build on:
|
||||
|
||||
- `xnu` — a freestanding ELF executable that the Barebone backend
|
||||
injects into a running kernel from the outside, over a remote stub.
|
||||
- `linux` — a static library that becomes a kernel module, loaded from the
|
||||
inside. See `linux/README.md`.
|
||||
|
||||
Everything above the `kernel` module is shared; each backend supplies the same
|
||||
set of primitives (logging, allocation, threads, waiting, time, symbols) plus its
|
||||
half of Gum's platform backend (`gum_xnu.rs` / `gum_linux.rs`).
|
||||
|
||||
## Development loop
|
||||
|
||||
export FRIDA_BAREBONE_CONFIG=$PWD/etc/xnu.json
|
||||
cargo build --release && make -C ~/src/frida-python && killall -9 qemu-system-aarch64 && sleep 2 && frida -D barebone -p 0
|
||||
cargo build --release --features xnu \
|
||||
&& make -C ~/src/frida-python \
|
||||
&& killall -9 qemu-system-aarch64 && sleep 2 \
|
||||
&& frida -D barebone -p 0
|
||||
|
||||
## Speeding up loop
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
SECTIONS {
|
||||
.text : {
|
||||
*(.text.entrypoint)
|
||||
*(.text*)
|
||||
}
|
||||
.rodata : {
|
||||
*(.rodata*)
|
||||
}
|
||||
.kernel_addrs : ALIGN(4) {
|
||||
. = ALIGN(4);
|
||||
_Get_Cur_VM_Handle = .; . += 4;
|
||||
_Get_Sys_VM_Handle = .; . += 4;
|
||||
_Schedule_Global_Event = .; . += 4;
|
||||
_Create_Semaphore = .; . += 4;
|
||||
_Wait_Semaphore = .; . += 4;
|
||||
_Signal_Semaphore = .; . += 4;
|
||||
_Get_Next_VM_Handle = .; . += 4;
|
||||
_Release_Time_Slice = .; . += 4;
|
||||
_Set_Global_Time_Out = .; . += 4;
|
||||
_Cancel_Time_Out = .; . += 4;
|
||||
_Get_System_Time = .; . += 4;
|
||||
__HeapAllocate = .; . += 4;
|
||||
__HeapFree = .; . += 4;
|
||||
__PageAllocate = .; . += 4;
|
||||
__PageFree = .; . += 4;
|
||||
__CopyPageTable = .; . += 4;
|
||||
__MapPhysToLinear = .; . += 4;
|
||||
_Hook_VMM_Fault = .; . += 4;
|
||||
_Fatal_Error_Handler = .; . += 4;
|
||||
_VMMCreateThread = .; . += 4;
|
||||
_Get_Cur_Thread_Handle = .; . += 4;
|
||||
_Get_Initial_Thread_Handle = .; . += 4;
|
||||
_Get_Next_Thread_Handle = .; . += 4;
|
||||
__Debug_Printf_Service = .; . += 4;
|
||||
__ContextSwitch = .; . += 4;
|
||||
__PageModifyPermissions = .; . += 4;
|
||||
__GetCurrentContext = .; . += 4;
|
||||
_VPICD_Virtualize_IRQ = .; . += 4;
|
||||
_VPICD_Phys_EOI = .; . += 4;
|
||||
_VPICD_Physically_Unmask = .; . += 4;
|
||||
}
|
||||
.data.rel.ro : {
|
||||
*(.data.rel.ro*)
|
||||
}
|
||||
.got : {
|
||||
*(.got*)
|
||||
}
|
||||
.bss : {
|
||||
*(.bss*)
|
||||
}
|
||||
. = ALIGN(4);
|
||||
_heap_start = .;
|
||||
}
|
||||
|
||||
VERSION {
|
||||
{
|
||||
global:
|
||||
_start;
|
||||
|
||||
local:
|
||||
*;
|
||||
};
|
||||
}
|
||||
+47
-21
@@ -8,12 +8,17 @@ fn main() {
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let devkit_dir = PathBuf::from(env::var("GUMJS_DEVKIT_DIR").unwrap());
|
||||
|
||||
let cc_str = env::var("CC_aarch64_unknown_none")
|
||||
let target = env::var("TARGET").unwrap();
|
||||
let cc_str = env::var(format!("CC_{}", target.replace('-', "_")))
|
||||
.or_else(|_| env::var("CC"))
|
||||
.unwrap_or_else(|_| "cc".to_string());
|
||||
let cc = Path::new(&cc_str);
|
||||
let cc_include_paths = detect_gcc_include_paths(cc);
|
||||
let cc_library_paths = detect_gcc_library_paths(cc);
|
||||
// Same convention as cc-rs: the variable may carry flags, and a bare-metal
|
||||
// compiler needs them to find its libc at all.
|
||||
let mut cc_argv = cc_str.split_whitespace();
|
||||
let cc = Path::new(cc_argv.next().unwrap());
|
||||
let cc_args: Vec<&str> = cc_argv.collect();
|
||||
let cc_include_paths = detect_gcc_include_paths(cc, &cc_args);
|
||||
let cc_library_paths = detect_gcc_library_paths(cc, &cc_args);
|
||||
|
||||
let cc_clang_args: Vec<String> = cc_include_paths
|
||||
.iter()
|
||||
@@ -25,9 +30,10 @@ fn main() {
|
||||
.header(devkit_dir.join("frida-gumjs.h").to_str().unwrap())
|
||||
.clang_arg("-nostdinc")
|
||||
.clang_arg("-target")
|
||||
.clang_arg("aarch64-none-elf")
|
||||
.clang_arg(format!("{}-none-elf", target.split('-').next().unwrap()))
|
||||
.clang_args(cc_clang_args)
|
||||
.clang_arg(format!("-I{}", devkit_dir.to_str().unwrap()))
|
||||
.blocklist_type("max_align_t")
|
||||
.merge_extern_blocks(true)
|
||||
.generate()
|
||||
.expect("Unable to generate bindings");
|
||||
@@ -36,25 +42,45 @@ fn main() {
|
||||
.write_to_file(out_dir.join("bindings.rs"))
|
||||
.expect("Couldn't write bindings");
|
||||
|
||||
println!(
|
||||
"cargo:rustc-link-search=native={}",
|
||||
devkit_dir.to_str().unwrap()
|
||||
);
|
||||
println!("cargo:rustc-link-lib=static=frida-gumjs");
|
||||
for path in cc_library_paths {
|
||||
println!("cargo:rustc-link-search=native={}", path.to_string_lossy());
|
||||
// The Linux flavour is a staticlib: kbuild owns the final link, so it — not
|
||||
// us — decides where the devkit and libc archives come from.
|
||||
if env::var("CARGO_FEATURE_BLOB").is_ok() {
|
||||
println!(
|
||||
"cargo:rustc-link-search=native={}",
|
||||
devkit_dir.to_str().unwrap()
|
||||
);
|
||||
println!("cargo:rustc-link-lib=static=frida-gumjs");
|
||||
for path in cc_library_paths {
|
||||
println!("cargo:rustc-link-search=native={}", path.to_string_lossy());
|
||||
}
|
||||
for arg in &cc_args {
|
||||
if let Some(path) = arg.strip_prefix("-L") {
|
||||
println!("cargo:rustc-link-search=native={path}");
|
||||
}
|
||||
}
|
||||
println!("cargo:rustc-link-lib=static=c");
|
||||
println!("cargo:rustc-link-lib=static=m");
|
||||
println!("cargo:rustc-link-arg=--export-dynamic");
|
||||
println!("cargo:rustc-link-arg=--emit-relocs");
|
||||
let script = if env::var("CARGO_FEATURE_WIN9X").is_ok() {
|
||||
"agent-win9x.lds"
|
||||
} else {
|
||||
"agent.lds"
|
||||
};
|
||||
println!("cargo:rustc-link-arg=--script={script}");
|
||||
println!("cargo:rustc-link-arg=--gc-sections");
|
||||
// All of the agent lives in the library crate, so nothing in the binary
|
||||
// references the entrypoint the host calls; keep the linker from
|
||||
// dropping it along with the rest of the unreferenced archive members.
|
||||
println!("cargo:rustc-link-arg=--undefined=_start");
|
||||
}
|
||||
println!("cargo:rustc-link-lib=static=c");
|
||||
println!("cargo:rustc-link-lib=static=m");
|
||||
println!("cargo:rustc-link-arg=--export-dynamic");
|
||||
println!("cargo:rustc-link-arg=--emit-relocs");
|
||||
println!("cargo:rustc-link-arg=--script=agent.lds");
|
||||
println!("cargo:rustc-link-arg=--gc-sections");
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
|
||||
pub fn detect_gcc_include_paths(gcc: &Path) -> Vec<PathBuf> {
|
||||
pub fn detect_gcc_include_paths(gcc: &Path, args: &[&str]) -> Vec<PathBuf> {
|
||||
let out = Command::new(gcc)
|
||||
.args(args)
|
||||
.args(["-xc", "-E", "-v", "-"])
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
@@ -94,8 +120,8 @@ pub fn detect_gcc_include_paths(gcc: &Path) -> Vec<PathBuf> {
|
||||
paths
|
||||
}
|
||||
|
||||
pub fn detect_gcc_library_paths(gcc: &Path) -> Vec<PathBuf> {
|
||||
let out = Command::new(gcc).arg("-print-search-dirs").output()
|
||||
pub fn detect_gcc_library_paths(gcc: &Path, args: &[&str]) -> Vec<PathBuf> {
|
||||
let out = Command::new(gcc).args(args).arg("-print-search-dirs").output()
|
||||
.expect("Failed to execute GCC to detect library paths");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"agent": {
|
||||
"transport": {
|
||||
"type": "socket",
|
||||
"path": "/tmp/frida-guest.sock"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"agent": {
|
||||
"transport": {
|
||||
"type": "device",
|
||||
"path": "/dev/frida"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"connection": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 1234
|
||||
},
|
||||
"kernel": "win9x",
|
||||
"agent": {
|
||||
"path": "/home/oleavr/src/frida-core/src/barebone/agent/target-win9x/i686-unknown-none/release/frida-barebone-agent",
|
||||
"transport": {
|
||||
"type": "hostlink",
|
||||
"qmp": "unix:/tmp/claude-1000/-home-oleavr-src-frida-core/0f3e358c-8f66-4246-b793-deb2cd3727f3/scratchpad/qmp.sock",
|
||||
"bus": "frida-vserial.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"arch": "x86",
|
||||
"cpu": "pentium4",
|
||||
"crt-objects-fallback": "false",
|
||||
"data-layout": "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-i128:128-f64:32:64-f80:32-n8:16:32-S128",
|
||||
"features": "-mmx,-sse,-sse2,-sse3,-ssse3,-sse4.1,-sse4.2,-avx,-avx2,+soft-float",
|
||||
"linker": "rust-lld",
|
||||
"linker-flavor": "gnu-lld",
|
||||
"llvm-target": "i686-unknown-none-elf",
|
||||
"max-atomic-width": 64,
|
||||
"panic-strategy": "abort",
|
||||
"plt-by-default": true,
|
||||
"position-independent-executables": false,
|
||||
"relocation-model": "static",
|
||||
"rustc-abi": "softfloat",
|
||||
"stack-probes": {
|
||||
"kind": "inline"
|
||||
},
|
||||
"target-pointer-width": 32
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
MODULE_NAME ?= frida-agent
|
||||
|
||||
obj-m += $(MODULE_NAME).o
|
||||
|
||||
# frida-agent-prelinked.o is not built from source here: it is the Rust agent, the
|
||||
# GumJS devkit and the C library collapsed into a single relocatable object by the prelink
|
||||
# step in Makefile. Copy it in through if_changed rather than kbuild's _shipped rule,
|
||||
# so the .cmd file modpost expects gets written.
|
||||
$(MODULE_NAME)-y := frida-kmod.o frida-agent-prelinked.o
|
||||
|
||||
targets += frida-agent-prelinked.o
|
||||
|
||||
quiet_cmd_frida_agent_prelinked = COPY $@
|
||||
cmd_frida_agent_prelinked = cp $< $@
|
||||
|
||||
$(obj)/frida-agent-prelinked.o: $(src)/frida-agent-prelinked.o.bin FORCE
|
||||
$(call if_changed,frida_agent_prelinked)
|
||||
|
||||
# On x86 objtool validates every object going into a module and generates its ORC
|
||||
# unwind tables. It was not built to read a JavaScript runtime linked against a
|
||||
# freestanding libc: it gives up on the first Rust switch jump table it cannot
|
||||
# follow, and the tables would only ever describe frames no kernel stack trace walks
|
||||
# into. Both the blob and the module object it is linked into have to be excused,
|
||||
# since objtool runs over each.
|
||||
OBJECT_FILES_NON_STANDARD_frida-agent-prelinked.o := y
|
||||
OBJECT_FILES_NON_STANDARD_$(MODULE_NAME).o := y
|
||||
|
||||
ccflags-y += $(FRIDA_KMOD_DEFS)
|
||||
@@ -0,0 +1,205 @@
|
||||
# Builds frida-agent.ko in two steps, because kbuild insists on owning the final
|
||||
# link:
|
||||
#
|
||||
# 1. Collapse the Rust staticlib plus everything it needs — the GumJS devkit and
|
||||
# the C library — into one relocatable object, and localize every symbol except
|
||||
# the entry points frida-kmod.c calls. Localizing matters: Gum and the C library
|
||||
# define names like memcpy and malloc that must not leak into the kernel's
|
||||
# namespace.
|
||||
# 2. Hand that object to kbuild as a shipped object, so vermagic, modinfo and
|
||||
# CRCs all come from the target kernel's own build system.
|
||||
#
|
||||
# See README.md for usage.
|
||||
|
||||
# KDIR is the kernel to build against. When it is a source tree that was configured
|
||||
# elsewhere — which is what Android's modules_prepare tarball gives — point KOUT at
|
||||
# the prepared tree.
|
||||
KDIR ?= /lib/modules/$(shell uname -r)/build
|
||||
PROFILE ?= release
|
||||
|
||||
# The kernel's own name for the architecture, and the one Rust knows it by. Both
|
||||
# follow from the machine this is built for, which defaults to the one building.
|
||||
ARCH ?= $(shell uname -m \
|
||||
| sed -e 's/^i[3-6]86$$/x86/' -e 's/^x86_64$$/x86/' -e 's/^aarch64$$/arm64/')
|
||||
BITS ?= $(shell uname -m \
|
||||
| sed -e 's/^i[3-6]86$$/32/' -e 's/^x86_64$$/64/' -e 's/^aarch64$$/64/')
|
||||
|
||||
ifeq ($(ARCH)-$(BITS),x86-32)
|
||||
RUST_TARGET ?= i686-unknown-none
|
||||
AGENT_TARGET_FLAGS ?= --target $(AGENT_DIR)/$(RUST_TARGET).json -Z json-target-spec
|
||||
SDK_ARCH ?= x86
|
||||
TOOLPREFIX ?=
|
||||
AGENT_LD_EMULATION ?= elf_i386
|
||||
else ifeq ($(ARCH)-$(BITS),x86-64)
|
||||
RUST_TARGET ?= x86_64-unknown-none
|
||||
SDK_ARCH ?= x86_64
|
||||
TOOLPREFIX ?=
|
||||
AGENT_LD_EMULATION ?= elf_x86_64
|
||||
else ifeq ($(ARCH)-$(BITS),arm64-64)
|
||||
RUST_TARGET ?= aarch64-unknown-none
|
||||
SDK_ARCH ?= arm64
|
||||
TOOLPREFIX ?= aarch64-none-elf-
|
||||
else
|
||||
$(error Unsupported ARCH "$(ARCH)" and BITS "$(BITS)"; this flavour covers \
|
||||
x86/32, x86/64 and arm64/64)
|
||||
endif
|
||||
AGENT_TARGET_FLAGS ?= --target $(RUST_TARGET)
|
||||
|
||||
AGENT_DIR := $(CURDIR)/..
|
||||
AGENT_TARGET_DIR := $(AGENT_DIR)/target-linux
|
||||
AGENT_LIB := $(AGENT_TARGET_DIR)/$(RUST_TARGET)/$(PROFILE)/libfrida_barebone_agent.a
|
||||
|
||||
GUMJS_DEVKIT_DIR ?= $(error Set GUMJS_DEVKIT_DIR to the directory holding libfrida-gumjs.a)
|
||||
|
||||
# Any binutils that speak the target's ELF will do; nothing here is compiled with
|
||||
# them. Building for the host's own architecture, that is the host's binutils.
|
||||
AGENT_LD ?= $(TOOLPREFIX)ld
|
||||
AGENT_AR ?= $(TOOLPREFIX)ar
|
||||
AGENT_NM ?= $(TOOLPREFIX)nm
|
||||
AGENT_OBJCOPY ?= $(TOOLPREFIX)objcopy
|
||||
|
||||
# The ABI the kernel imposes on anything linked into it. The flags only reach crates
|
||||
# Cargo compiles, and the toolchain ships core, alloc and compiler_builtins prebuilt
|
||||
# without them, so those three have to be rebuilt from source as well. That needs the
|
||||
# rust-src component and an unstable flag, hence RUSTC_BOOTSTRAP.
|
||||
#
|
||||
# On x86_64 that is endbr64 on every address-taken function, since a kernel with
|
||||
# CONFIG_X86_KERNEL_IBT faults an indirect call landing anywhere else, and switches
|
||||
# compiled as comparisons rather than jump tables, which objtool cannot follow into.
|
||||
# Soft-float, the kernel code model and the absent red zone come with the target
|
||||
# itself. On arm64
|
||||
# it is x18, which the kernel reserves for the shadow call stack, so nothing linked in
|
||||
# may use it as a general-purpose register.
|
||||
ifeq ($(ARCH)-$(BITS),x86-32)
|
||||
AGENT_RUSTFLAGS ?=
|
||||
AGENT_CFLAGS ?= --target=i386-none-elf \
|
||||
-Xclang -target-feature -Xclang +soft-float -mno-mmx -mno-sse \
|
||||
-mlong-double-64 -fno-pic --sysroot=$(FRIDA_SDK)
|
||||
else ifeq ($(ARCH)-$(BITS),x86-64)
|
||||
AGENT_RUSTFLAGS ?= -Zcf-protection=branch \
|
||||
-Cllvm-args=--min-jump-table-entries=65536 \
|
||||
-Crelocation-model=static -Zplt=yes
|
||||
AGENT_CFLAGS ?= --target=x86_64-none-elf \
|
||||
-Xclang -target-feature -Xclang +soft-float -mno-mmx -mno-sse -mno-red-zone \
|
||||
-mlong-double-64 -mcmodel=kernel -fcf-protection=branch -fno-jump-tables \
|
||||
-fno-pic --sysroot=$(FRIDA_SDK)
|
||||
else
|
||||
AGENT_RUSTFLAGS ?= -Zfixed-x18
|
||||
AGENT_CFLAGS ?= --target=aarch64-none-elf -mabi=aapcs-soft -mgeneral-regs-only \
|
||||
-ffixed-x18 -fno-pic --sysroot=$(FRIDA_SDK)
|
||||
endif
|
||||
AGENT_BUILD_STD ?= -Z build-std=core,alloc,compiler_builtins
|
||||
|
||||
# The devkit header reaches for GLib's and capstone's, which a devkit built against
|
||||
# an SDK leaves in that SDK rather than beside itself. The sysroot's own include
|
||||
# directory is spelled out because only clang's bare-metal driver, which it selects
|
||||
# for arm64 but not for x86_64, searches there on its own.
|
||||
AGENT_CFLAGS += -isystem $(FRIDA_SDK)/include \
|
||||
-I$(FRIDA_SDK)/include/glib-2.0 \
|
||||
-I$(FRIDA_SDK)/lib/glib-2.0/include \
|
||||
-I$(FRIDA_SDK)/include/capstone \
|
||||
-I$(FRIDA_SDK)/include/json-glib-1.0
|
||||
|
||||
# The soft-float SDK, whose libc is picolibc and whose compiler runtime is
|
||||
# compiler-rt. GCC's own newlib and libgcc are hard-float and cannot be used here.
|
||||
FRIDA_SDK ?= $(error Set FRIDA_SDK to a none-$(SDK_ARCH)-softfloat SDK prefix)
|
||||
LIBM = $(FRIDA_SDK)/lib/libm.a
|
||||
LIBBUILTINS = $(FRIDA_SDK)/lib/libclang_rt.builtins.a
|
||||
|
||||
# bindgen reads the devkit header through whatever this compiler reports as its
|
||||
# include path, so it has to be one that knows where the SDK keeps its libc. It has
|
||||
# to be clang either way: -mabi=aapcs-soft landed there in 19 and GCC has it at no
|
||||
# version, and on x86_64 the soft-float ABI is reachable only as an -Xclang feature.
|
||||
AGENT_CC ?= clang
|
||||
|
||||
PRELINKED := frida-agent-prelinked.o.bin
|
||||
|
||||
AGENT_LIB_SOFTFLOAT := $(AGENT_TARGET_DIR)/libfrida-barebone-agent-softfloat.a
|
||||
|
||||
# On arm64 the toolchain's own compiler_builtins is compiled for the ordinary AAPCS,
|
||||
# where a double travels in d0: its __muldf3 opens with "fmov x14, d0". Everything
|
||||
# else there is -mabi=aapcs-soft and passes that same double in x0. Whole-archiving
|
||||
# the staticlib puts those definitions in front of the SDK's compiler-rt, which has
|
||||
# the soft-float ones, and every double the C library computes then comes back
|
||||
# garbage — quietly, since the arithmetic still runs. Drop them and let compiler-rt
|
||||
# answer instead. x86_64 needs none of this: soft-float is part of the Rust target
|
||||
# there, so the copy build-std compiles already passes doubles the way the C half
|
||||
# expects.
|
||||
ifeq ($(ARCH),arm64)
|
||||
DROP_HARDFLOAT_BUILTINS = \
|
||||
$(AGENT_AR) d $@ $$($(AGENT_NM) --print-armap $@ | sed -n 's/^__muldf3 in //p') && \
|
||||
$(AGENT_AR) s $@
|
||||
else
|
||||
DROP_HARDFLOAT_BUILTINS = :
|
||||
endif
|
||||
|
||||
# The agent puts malloc, the C library's locks and its process stubs on the kernel's
|
||||
# own facilities, so picolibc's versions are duplicate definitions rather than
|
||||
# fallbacks. Work out which members they live in instead of naming them: the set
|
||||
# follows from what the agent defines, and picolibc moves things between releases.
|
||||
LIBC := $(AGENT_TARGET_DIR)/libc-trimmed.a
|
||||
|
||||
ENTRY_POINTS := frida_agent_start frida_agent_stop \
|
||||
gum_interceptor_obtain gum_interceptor_begin_transaction \
|
||||
gum_interceptor_end_transaction gum_interceptor_replace \
|
||||
gum_interceptor_revert
|
||||
KEEP_GLOBAL := $(foreach s,$(ENTRY_POINTS),--keep-global-symbol=$(s))
|
||||
|
||||
# Two structures the reparenting path reaches into lost a member upstream, and
|
||||
# neither change announces itself as anything frida-kmod.c could test for. The
|
||||
# headers it is compiled against are the authority on both, so ask them.
|
||||
FRIDA_KMOD_DEFS := \
|
||||
$(if $(shell grep -l 'list_head[[:space:]]*thread_group;' \
|
||||
$(KDIR)/include/linux/sched.h),-DFRIDA_HAVE_TASK_THREAD_GROUP) \
|
||||
$(if $(shell grep -l 'spinlock_t[[:space:]]*lock;' \
|
||||
$(KDIR)/include/linux/fs_struct.h),-DFRIDA_HAVE_FS_STRUCT_LOCK)
|
||||
|
||||
all: modules
|
||||
|
||||
# A module that dies in its init path is left wedged under its own name until the
|
||||
# next boot, so bring-up wants a fresh one per attempt.
|
||||
MODULE_NAME ?= frida-agent
|
||||
|
||||
modules: $(PRELINKED)
|
||||
$(MAKE) -C $(KDIR) $(if $(KOUT),O=$(KOUT)) M=$(CURDIR) ARCH=$(ARCH) \
|
||||
FRIDA_KMOD_DEFS="$(FRIDA_KMOD_DEFS)" MODULE_NAME=$(MODULE_NAME) modules
|
||||
|
||||
$(PRELINKED): $(AGENT_LIB_SOFTFLOAT) $(LIBC)
|
||||
$(AGENT_LD) -r $(if $(AGENT_LD_EMULATION),-m $(AGENT_LD_EMULATION)) -o $@.tmp \
|
||||
--whole-archive $(AGENT_LIB_SOFTFLOAT) --no-whole-archive \
|
||||
--start-group \
|
||||
$(GUMJS_DEVKIT_DIR)/libfrida-gumjs.a \
|
||||
$(LIBC) \
|
||||
$(LIBM) \
|
||||
$(LIBBUILTINS) \
|
||||
--end-group
|
||||
$(AGENT_OBJCOPY) $(KEEP_GLOBAL) --strip-debug \
|
||||
--remove-section=.llvmbc --remove-section=.llvmcmd $@.tmp $@
|
||||
rm -f $@.tmp
|
||||
|
||||
$(AGENT_LIB_SOFTFLOAT): agent
|
||||
cp $(AGENT_LIB) $@
|
||||
$(DROP_HARDFLOAT_BUILTINS)
|
||||
|
||||
$(LIBC): export LC_ALL = C
|
||||
$(LIBC): $(AGENT_LIB_SOFTFLOAT)
|
||||
cp $(FRIDA_SDK)/lib/libc.a $@
|
||||
$(AGENT_NM) --defined-only $(AGENT_LIB_SOFTFLOAT) \
|
||||
| awk 'NF == 3 && $$2 ~ /^[TDBR]$$/ { print $$3 }' | sort -u > $@.agent-syms
|
||||
$(AGENT_NM) --print-armap $@ | sed -n 's/^\([^ ]*\) in \(.*\)$$/\1 \2/p' | sort -u > $@.members
|
||||
$(AGENT_AR) d $@ $$(join $@.agent-syms $@.members | awk '{ print $$2 }' | sort -u)
|
||||
$(AGENT_AR) s $@
|
||||
rm -f $@.agent-syms $@.members
|
||||
|
||||
agent:
|
||||
cd $(AGENT_DIR) && RUSTC_BOOTSTRAP=1 RUSTFLAGS="$(AGENT_RUSTFLAGS)" \
|
||||
CC_$(subst -,_,$(RUST_TARGET))="$(AGENT_CC) $(AGENT_CFLAGS)" \
|
||||
cargo build --$(PROFILE) \
|
||||
$(AGENT_TARGET_FLAGS) --features linux \
|
||||
--target-dir $(AGENT_TARGET_DIR) $(AGENT_BUILD_STD)
|
||||
|
||||
clean:
|
||||
$(MAKE) -C $(KDIR) $(if $(KOUT),O=$(KOUT)) M=$(CURDIR) ARCH=$(ARCH) clean
|
||||
rm -f $(PRELINKED) $(PRELINKED).tmp $(AGENT_LIB_SOFTFLOAT) $(LIBC)
|
||||
|
||||
.PHONY: all modules agent clean
|
||||
@@ -0,0 +1,291 @@
|
||||
# Linux kernel module
|
||||
|
||||
The same agent that the Barebone backend injects into XNU from the outside, but
|
||||
loaded from the inside as an ordinary kernel module.
|
||||
|
||||
## How it differs from the XNU flavour
|
||||
|
||||
| | XNU | Linux |
|
||||
|-------------------|--------------------------------------------------|-------------------------------------------|
|
||||
| Delivery | remote stub injects a freestanding ELF | `insmod frida-agent.ko` |
|
||||
| Kernel primitives | addresses patched into `.kernel_addrs` by the host | direct calls into `frida-kmod.c` |
|
||||
| Bootstrap config | GVariant blob written into guest memory | module parameters |
|
||||
| Transport | virtio hostlink or vsock | `/dev/frida` character device |
|
||||
| Page permissions | host rewrites descriptors over the hostlink | `set_memory_*()` |
|
||||
| Writable alias | shadow copy, committed via physical-memory bridge | `vmap()` of the same pages |
|
||||
| Symbols | table supplied by the host | kallsyms |
|
||||
|
||||
Because the Linux agent performs its own memory work, it never issues the
|
||||
`RemapWritablePages`, `MemoryProtect` or `PatchCode` host RPCs. The wire protocol
|
||||
is otherwise identical, so the host side sees the same `(yqv)` frames.
|
||||
|
||||
## Building
|
||||
|
||||
Four inputs: this repository, a GumJS devkit built for `none-<arch>-softfloat`, the
|
||||
soft-float SDK it was built against, and the kernel the module is for. `Makefile`
|
||||
covers x86_64 and arm64, and defaults to the architecture of the machine building.
|
||||
|
||||
Building for the running kernel is a good deal shorter than cross-building for a
|
||||
device, so that walkthrough comes first; the arm64 one below it is the same steps
|
||||
with the kernel obtained from elsewhere.
|
||||
|
||||
### Toolchain
|
||||
|
||||
rustup target add x86_64-unknown-none # or aarch64-unknown-none
|
||||
rustup component add rust-src
|
||||
sudo dnf install clang binutils kernel-devel-$(uname -r)
|
||||
|
||||
clang must be 19 or newer, since that is where `-mabi=aapcs-soft` landed; GCC does
|
||||
not implement it at any version, nor does it reach x86_64's soft-float ABI. The
|
||||
binutils are for the prelink, which reads the archive map that only GNU `nm` prints —
|
||||
the LLVM one does not; cross-building wants the target's, natively the host's own
|
||||
will do. The host compiler is for Cargo's own build scripts, which run on the machine
|
||||
doing the building.
|
||||
|
||||
The commands below are run from the top of this repository, with `releng` checked
|
||||
out — `git submodule update --init releng` if the clone did not recurse.
|
||||
|
||||
### For the running x86_64 kernel
|
||||
|
||||
No prebuilt bundles are published for `none-x86_64-softfloat`, so both inputs are
|
||||
built locally, against the same SDK:
|
||||
|
||||
releng/deps.py build --bundle=sdk --host=none-x86_64-softfloat
|
||||
mkdir -p ~/sdk-none-x86_64-softfloat
|
||||
tar -C ~/sdk-none-x86_64-softfloat -xf deps/sdk-none-x86_64-softfloat.tar.xz
|
||||
|
||||
gum=subprojects/frida-gum
|
||||
mkdir -p $gum/build/none-x86_64-softfloat
|
||||
(cd $gum/build/none-x86_64-softfloat \
|
||||
&& FRIDA_DEPS=$PWD/../../../../deps ../../configure \
|
||||
--host=none-x86_64-softfloat \
|
||||
--enable-gumjs \
|
||||
--with-devkits=gumjs \
|
||||
--with-devkit-symbol-scope=original \
|
||||
&& make)
|
||||
|
||||
make -C src/barebone/agent/linux \
|
||||
FRIDA_SDK=$HOME/sdk-none-x86_64-softfloat \
|
||||
GUMJS_DEVKIT_DIR=$PWD/$gum/build/none-x86_64-softfloat/bindings/gumjs/devkit
|
||||
|
||||
Everything else — `KDIR`, the Rust target, the binutils, the kernel's own name for the
|
||||
architecture — follows from the running kernel.
|
||||
|
||||
### For an arm64 device
|
||||
|
||||
Here there are published bundles to start from, and the kernel has to be assembled.
|
||||
|
||||
#### The devkit and the SDK
|
||||
|
||||
version=17.17.0
|
||||
base=https://github.com/frida/frida/releases/download/$version
|
||||
curl -LO $base/frida-gumjs-devkit-$version-none-arm64-softfloat.tar.xz
|
||||
mkdir -p ~/gumjs-devkit
|
||||
tar -C ~/gumjs-devkit -xf frida-gumjs-devkit-$version-none-arm64-softfloat.tar.xz
|
||||
|
||||
releng/deps.py sync sdk none-arm64-softfloat ~/sdk-none-arm64-softfloat
|
||||
|
||||
The SDK carries picolibc and the compiler-rt builtins the devkit expects to be linked
|
||||
against, and `releng/deps.toml` pins which one. Mixing a devkit with an SDK built for
|
||||
a different libc leaves undefined symbols at prelink time.
|
||||
|
||||
#### The kernel
|
||||
|
||||
Nothing here needs the device's own kernel to be built, but the config alone is not
|
||||
enough either. `uname -r` ends in the GKI commit and the build number:
|
||||
|
||||
adb shell uname -r
|
||||
6.1.145-android14-11-gc1de4747ac59-ab14219743
|
||||
|
||||
which is commit `c1de4747ac59` and build `14219743`. Three pieces are keyed to them:
|
||||
|
||||
commit=c1de4747ac59
|
||||
build=14219743
|
||||
ci=https://ci.android.com/builds/submitted/$build/kernel_aarch64/latest/raw
|
||||
|
||||
mkdir -p ~/kernel-prepared ~/kernel-source
|
||||
curl -sSL $ci/modules_prepare_outdir.tar.gz | tar -xz -C ~/kernel-prepared
|
||||
curl -sSL $ci/kernel_aarch64_Module.symvers -o ~/kernel-prepared/Module.symvers
|
||||
curl -sSL https://android.googlesource.com/kernel/common/+archive/$commit.tar.gz \
|
||||
| tar -xz -C ~/kernel-source
|
||||
|
||||
`modules_prepare_outdir.tar.gz` is the configured build tree with `scripts/` already
|
||||
compiled as x86-64 binaries, so the kbuild half has to run on an x86-64 Linux host —
|
||||
an arm64 one gets as far as `CC [M]` and then fails to run `fixdep`. `Module.symvers`
|
||||
carries the CRCs `CONFIG_MODVERSIONS` checks. The top-level `Makefile` in that prepared tree is a stub
|
||||
that includes the source's, hence both `KDIR` and `KOUT` below.
|
||||
|
||||
A different GKI build of the same branch does not substitute for these: vermagic is
|
||||
compared verbatim, and it names the exact build.
|
||||
|
||||
#### Build
|
||||
|
||||
make -C src/barebone/agent/linux \
|
||||
FRIDA_SDK=$HOME/sdk-none-arm64-softfloat \
|
||||
GUMJS_DEVKIT_DIR=$HOME/gumjs-devkit \
|
||||
AGENT_LD=aarch64-linux-gnu-ld \
|
||||
AGENT_AR=aarch64-linux-gnu-ar \
|
||||
AGENT_NM=aarch64-linux-gnu-nm \
|
||||
AGENT_OBJCOPY=aarch64-linux-gnu-objcopy \
|
||||
KDIR=$HOME/kernel-source \
|
||||
KOUT=$HOME/kernel-prepared \
|
||||
LLVM=1
|
||||
|
||||
`make` builds the Rust staticlib itself, prelinks it against the devkit and the SDK,
|
||||
then hands the result to kbuild. The `AGENT_*` overrides are only needed because the
|
||||
defaults name the `aarch64-none-elf-` toolchain the XNU flavour uses; drop them if
|
||||
that is what is installed.
|
||||
|
||||
The prelink half runs anywhere; the kbuild half needs the x86-64 Linux host above.
|
||||
Cross-building from macOS means running that step in a container holding the kernel
|
||||
tree.
|
||||
|
||||
adb push src/barebone/agent/linux/frida-agent.ko /data/local/tmp/
|
||||
|
||||
## Loading
|
||||
|
||||
insmod frida-agent.ko
|
||||
|
||||
The module's init path returns as soon as the worker thread is running — watch
|
||||
`dmesg` for `frida: listening on /dev/frida`. `rmmod frida-agent` unwinds in the
|
||||
reverse order and does not return until the worker is off the module's text.
|
||||
|
||||
## Talking to it
|
||||
|
||||
The agent exposes `/dev/frida` and waits: one client at a time, `read`/`write` of the
|
||||
same length-prefixed frames the other transports carry, with `poll` for readiness.
|
||||
It never dials out, so there is no network namespace to get right and no ordering
|
||||
requirement between loading the module and attaching a client.
|
||||
|
||||
On Android the node is created by devtmpfs without a policy-specific label, so an
|
||||
SELinux-enforcing system needs the client's domain granted access to it before it can
|
||||
open the device. A socket-based transport was the other option considered and is a
|
||||
worse fit: `AF_VSOCK` only reaches a peer when a hypervisor is on the other side, and
|
||||
having the kernel connect out to loopback needs something already listening before
|
||||
the module loads.
|
||||
|
||||
### Reaching it from a host
|
||||
|
||||
Serve the Barebone device from a frida-server running on the target, so the `open()`
|
||||
happens next to the device node and the host talks to an ordinary frida-server:
|
||||
|
||||
FRIDA_BAREBONE_CONFIG=/data/local/tmp/linux-kmod.json \
|
||||
frida-server --device barebone -l 127.0.0.1:27042
|
||||
|
||||
`etc/linux-kmod.json` is that config. From the host, with `adb forward tcp:27042
|
||||
tcp:27042` in place:
|
||||
|
||||
frida -H 127.0.0.1:27042 -p 0
|
||||
|
||||
## Injecting into userspace
|
||||
|
||||
Loaded on a live system the module doubles as a ptrace-free injector for ordinary
|
||||
processes — how the Linux local backend reaches its targets when the module is
|
||||
present, falling back to `ptrace` when it is not. There is no second device node;
|
||||
the channel is `prctl()` with a magic option and a token in the fifth argument,
|
||||
|
||||
prctl(0x46524944, op, arg1, arg2, 0x1d5f9e6b2c7a4038)
|
||||
|
||||
and any call whose option or token does not match falls through to the real
|
||||
prctl, so probing for the module looks like a stock kernel. `op` 32 pings and
|
||||
returns the magic.
|
||||
|
||||
Allocate, free, read, write and spawn — the primitives the injector drives
|
||||
against a target pid — require `CAP_SYS_ADMIN`. A spawned thread is a real
|
||||
`CLONE_THREAD` sibling that adopts the target's mm, files, credentials,
|
||||
namespaces, fs and SysV semaphore undo list, so it is a native member of the
|
||||
group rather than a kernel thread wearing its address space.
|
||||
|
||||
Cloaking is Gum's own registry mirrored into the kernel so it holds against
|
||||
`/proc` too: the module intercepts the `maps`, `smaps`, task and status readers
|
||||
and drops cloaked threads, ranges and fds while keeping the counts consistent. A
|
||||
process cloaks its own resources with the token alone — no capability — so an
|
||||
injected agent self-cloaks by forwarding Gum's updates over the same channel,
|
||||
and the loader adds the footprint the agent itself never sees. A cloaked thread
|
||||
still sees through the cloak when it reads `/proc`, so the agent is not blind to
|
||||
its own process.
|
||||
|
||||
## What the target kernel has to provide
|
||||
|
||||
- **x86_64 or arm64.** The register-level pieces — the page size, cache maintenance,
|
||||
where the syscall ABI leaves prctl()'s arguments, and how a spawned thread's
|
||||
registers are set up for its return to userspace — have a half for each. The XNU
|
||||
flavour remains arm64-only.
|
||||
- **`CONFIG_KPROBES` and `CONFIG_KALLSYMS_ALL`.** A good deal of what the shim
|
||||
needs is compiled in but not exported — `kallsyms_lookup_name`,
|
||||
`kallsyms_on_each_symbol` and `set_memory_*` all are, and GKI trims its export
|
||||
table to the KMI symbol list on top of that. The shim resolves them by name
|
||||
through a kprobe, which is the standard way back in. Without kallsyms the module
|
||||
still loads but cannot look up symbols or change page permissions.
|
||||
- **Unsigned modules.** `CONFIG_MODULE_SIG_FORCE` must be off. Android's
|
||||
`CONFIG_MODULE_SIG_PROTECT` is fine: it only demands signatures for modules on
|
||||
the GKI list.
|
||||
|
||||
## Kernel ABI constraints
|
||||
|
||||
A hardened kernel imposes an ABI on anything linked into it, and the FP constraint
|
||||
below is why this flavour is built against a soft-float SDK
|
||||
(`none-arm64-softfloat`, `none-x86_64-softfloat`) rather than an ordinary bare-metal
|
||||
one. The remaining constraints are per-architecture.
|
||||
|
||||
- **FP/SIMD.** JavaScript numbers are doubles, so a hardfloat build would use FP
|
||||
for as long as the runtime lives, and neither architecture lets a kernel thread
|
||||
hold it for free — x86 wants every stretch bracketed by `kernel_fpu_begin()`, and
|
||||
on arm64 kernels before 6.9 there is no correct way to hold it at all:
|
||||
`kernel_neon_begin()` runs with preemption disabled so we cannot sleep under it,
|
||||
and the scheduler does not save a kernel thread's FP registers — live values are
|
||||
lost on preemption and the FP state of whichever user task was interrupted is
|
||||
corrupted. Building everything soft-float removes the question rather than
|
||||
answering it: `-mabi=aapcs-soft -mgeneral-regs-only` on arm64, and on x86_64
|
||||
LLVM's `+soft-float`, which is also what Rust's own `x86_64-unknown-none` turns
|
||||
on, so the two halves agree on where a double travels. `-mno-sse` alone does not
|
||||
reach the ABI; and `long double` has to be narrowed to 64 bits with
|
||||
`-mlong-double-64`, since soft-float has no lowering for x87's 80-bit format and
|
||||
LLVM crashes rather than calling out to a builtin.
|
||||
- **Indirect branch tracking (x86_64).** With `CONFIG_X86_KERNEL_IBT` an indirect
|
||||
call landing on anything but an `endbr64` faults, so everything linked in is built
|
||||
`-fcf-protection=branch` and the Rust half `-Zcf-protection=branch`.
|
||||
- **Kernel code model and red zone (x86_64).** Modules are loaded into the top 2GB
|
||||
of the address space and their relocations are resolved as if by `-mcmodel=kernel`,
|
||||
and there is no red zone below the stack pointer for an interrupt to step on. Both
|
||||
come with the Rust target; the C half is told explicitly.
|
||||
- **objtool (x86_64).** It validates every object entering a module and builds its
|
||||
ORC unwind tables, and was not written to read a JavaScript runtime linked against
|
||||
a freestanding libc. `Kbuild` marks the prelinked blob `OBJECT_FILES_NON_STANDARD`.
|
||||
- **Shadow call stack (arm64).** With `CONFIG_SHADOW_CALL_STACK` the kernel reserves x18, so
|
||||
the SDK and Gum are built `-ffixed-x18` and the Rust half with `-Zfixed-x18`.
|
||||
Anything linked in that uses x18 as a scratch register overwrites the kernel's SCS
|
||||
pointer, and the thread then returns through whatever that address happens to hold.
|
||||
|
||||
The Rust flag only reaches crates Cargo compiles, so `core`, `alloc` and
|
||||
`compiler_builtins` — which the toolchain ships prebuilt, and which between them
|
||||
account for every x18 write that used to reach the module — are rebuilt from source
|
||||
via `-Z build-std`. That needs `rustup component add rust-src`.
|
||||
|
||||
## The soft-float libc
|
||||
|
||||
The libc is picolibc and the compiler runtime is compiler-rt, both from the
|
||||
soft-float SDK that `FRIDA_SDK` points at. `Makefile` trims a copy of its
|
||||
`libc.a` before the prelink: the agent implements `malloc`, the C library's locks and
|
||||
its process stubs over the kernel's own facilities, so picolibc's are duplicate
|
||||
definitions rather than fallbacks. The set is derived from what the agent defines
|
||||
rather than listed, since picolibc moves things between releases.
|
||||
|
||||
On arm64 those compiler-rt builtins are load-bearing in a way that is easy to miss.
|
||||
Rust's toolchain carries its own `compiler_builtins`, compiled for the ordinary AAPCS
|
||||
where a double travels in `d0`, and the prelink whole-archives the Rust staticlib — so
|
||||
without the filtering step in `Makefile` those definitions answer the C library's calls
|
||||
to `__muldf3` and friends, which pass the same double in `x0`. Nothing crashes. The
|
||||
arithmetic simply returns garbage, and the first thing to notice is GLib deciding a
|
||||
hash table need not grow. x86_64 needs none of that filtering: soft-float is part of
|
||||
the Rust target there, so the copy `-Z build-std` compiles already agrees with the C
|
||||
half.
|
||||
|
||||
## What a script may look like
|
||||
|
||||
The runtime compiles scripts on a kernel thread's stack, and QuickJS parses
|
||||
expressions by recursion, so the ceiling is nesting rather than length: roughly
|
||||
fourteen levels of parentheses or fifteen of object literals on arm64 Linux, whose
|
||||
kernel stack is 16 KiB — as is x86_64's. A script that exceeds it fails to compile rather than taking
|
||||
anything down, though the message is not always `stack overflow` — an over-deep
|
||||
`if`/`else` chain reports `expecting ';'`. Long scripts are fine; deep ones are not.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,7 @@
|
||||
use core::ffi::c_void;
|
||||
|
||||
use crate::bindings::{gpointer, gsize, GumPageProtection};
|
||||
use crate::{gum, libc, xnu};
|
||||
use crate::bindings::{gpointer, gsize, gum_try_mprotect, GumPageProtection};
|
||||
use crate::{kernel, libc};
|
||||
|
||||
const GUM_PAGE_READ: GumPageProtection = 1;
|
||||
const GUM_PAGE_EXECUTE: GumPageProtection = 4;
|
||||
@@ -10,7 +10,7 @@ const GUM_PAGE_EXECUTE: GumPageProtection = 4;
|
||||
// static trampoline `text` made executable, followed by a writable parameter
|
||||
// table `map_size` bytes later (the trampolines reach their parameters via that
|
||||
// fixed offset). The two halves are whole pages, so flipping the first to RX
|
||||
// over the hostlink leaves the parameter table writable.
|
||||
// leaves the parameter table writable.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ffi_tramp_embedder_map(
|
||||
text: *const c_void,
|
||||
@@ -19,7 +19,7 @@ pub unsafe extern "C" fn ffi_tramp_embedder_map(
|
||||
parm_table: *mut *mut c_void,
|
||||
) -> i32 {
|
||||
unsafe {
|
||||
let base = xnu::kalloc(map_size * 2);
|
||||
let base = kernel::alloc_code(map_size * 2);
|
||||
if base.is_null() {
|
||||
return 0;
|
||||
}
|
||||
@@ -28,9 +28,9 @@ pub unsafe extern "C" fn ffi_tramp_embedder_map(
|
||||
libc::__clear_cache(base, base.add(map_size));
|
||||
|
||||
let made_executable =
|
||||
gum::gum_try_mprotect(base as gpointer, map_size as gsize, GUM_PAGE_READ | GUM_PAGE_EXECUTE);
|
||||
gum_try_mprotect(base as gpointer, map_size as gsize, GUM_PAGE_READ | GUM_PAGE_EXECUTE);
|
||||
if made_executable == 0 {
|
||||
xnu::free(base, map_size * 2);
|
||||
kernel::free_code(base, map_size * 2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,5 @@ pub unsafe extern "C" fn ffi_tramp_embedder_unmap(
|
||||
_parm_table: *mut c_void,
|
||||
map_size: usize,
|
||||
) {
|
||||
unsafe {
|
||||
xnu::free(code_table as *mut u8, map_size * 2);
|
||||
}
|
||||
kernel::free_code(code_table as *mut u8, map_size * 2);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use core::ptr;
|
||||
|
||||
use crate::bindings::{g_wait_is_set, gint64, gpointer};
|
||||
use crate::xnu;
|
||||
use crate::kernel;
|
||||
|
||||
const G_WAIT_INFINITE: gint64 = -1;
|
||||
|
||||
@@ -9,38 +9,35 @@ pub static mut WAKEUP_TOKEN: u64 = 0;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_get_monotonic_time() -> gint64 {
|
||||
let abstime = xnu::mach_absolute_time();
|
||||
let time = xnu::absolutetime_to_nanoseconds(abstime);
|
||||
(time / 1000) as gint64
|
||||
kernel::monotonic_micros()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_wait_sleep(token: gpointer, timeout_us: gint64) {
|
||||
let wait_event = ptr::addr_of_mut!(WAKEUP_TOKEN) as *const u8;
|
||||
|
||||
let wait_result = if timeout_us == G_WAIT_INFINITE {
|
||||
xnu::assert_wait(wait_event, xnu::THREAD_INTERRUPTIBLE)
|
||||
let timeout = if timeout_us == G_WAIT_INFINITE {
|
||||
None
|
||||
} else {
|
||||
xnu::assert_wait_timeout(
|
||||
wait_event,
|
||||
xnu::THREAD_INTERRUPTIBLE,
|
||||
(timeout_us * 1000) as u32,
|
||||
1, // TODO
|
||||
)
|
||||
Some(timeout_us as u64)
|
||||
};
|
||||
if wait_result != xnu::THREAD_WAITING {
|
||||
panic!("assert_wait_timeout failed: {}", wait_result);
|
||||
}
|
||||
|
||||
if unsafe { g_wait_is_set(token) != 0 } {
|
||||
xnu::thread_wakeup(wait_event);
|
||||
return;
|
||||
}
|
||||
|
||||
xnu::thread_block(None);
|
||||
kernel::wait(wait_event, timeout, &mut || unsafe {
|
||||
g_wait_is_set(token) != 0
|
||||
});
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_wait_wake(_token: gpointer) {
|
||||
xnu::thread_wakeup(ptr::addr_of_mut!(WAKEUP_TOKEN) as *const u8);
|
||||
kernel::wake(ptr::addr_of_mut!(WAKEUP_TOKEN) as *const u8);
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_io_channel_unix_new(_fd: i32) -> gpointer {
|
||||
ptr::null_mut()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_spawn_async_with_pipes() -> i32 {
|
||||
0
|
||||
}
|
||||
@@ -8,34 +8,39 @@ use crate::bindings::GThreadFunc;
|
||||
use crate::bindings::{gpointer, gulong};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::collections::BTreeMap;
|
||||
use core::arch::asm;
|
||||
use core::ffi::c_void;
|
||||
use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||
use core::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize, Ordering};
|
||||
|
||||
// Our implementation structures that overlay the opaque GLib structures
|
||||
|
||||
#[repr(C, align(8))]
|
||||
#[cfg_attr(target_pointer_width = "32", repr(C, align(4)))]
|
||||
#[cfg_attr(target_pointer_width = "64", repr(C, align(8)))]
|
||||
struct MutexImpl {
|
||||
lock: AtomicU32,
|
||||
_padding: [u8; core::mem::size_of::<GMutex>() - core::mem::size_of::<AtomicU32>()],
|
||||
}
|
||||
|
||||
#[repr(C, align(8))]
|
||||
#[cfg_attr(target_pointer_width = "32", repr(C, align(4)))]
|
||||
#[cfg_attr(target_pointer_width = "64", repr(C, align(8)))]
|
||||
struct RecMutexImpl {
|
||||
owner: AtomicU64, // Thread ID of the owner (uses the 'p' field)
|
||||
owner: AtomicUsize, // Thread ID of the owner (uses the 'p' field)
|
||||
count: AtomicU32, // Recursion count (uses i[0])
|
||||
_unused: u32, // Unused (uses i[1])
|
||||
}
|
||||
|
||||
#[repr(C, align(8))]
|
||||
#[cfg_attr(target_pointer_width = "32", repr(C, align(4)))]
|
||||
#[cfg_attr(target_pointer_width = "64", repr(C, align(8)))]
|
||||
struct RWLockImpl {
|
||||
state: AtomicU32, // Lock state (readers count + writer bit)
|
||||
_padding: [u8; core::mem::size_of::<GRWLock>() - core::mem::size_of::<AtomicU32>()],
|
||||
}
|
||||
|
||||
#[repr(C, align(8))]
|
||||
// A generation counter rather than a flag: a flag stays set once signalled, so every
|
||||
// later wait returns at once and the caller's loop spins instead of blocking.
|
||||
#[cfg_attr(target_pointer_width = "32", repr(C, align(4)))]
|
||||
#[cfg_attr(target_pointer_width = "64", repr(C, align(8)))]
|
||||
struct CondImpl {
|
||||
signal: AtomicU32, // Signal state
|
||||
generation: AtomicU32,
|
||||
_padding: [u8; core::mem::size_of::<GCond>() - core::mem::size_of::<AtomicU32>()],
|
||||
}
|
||||
|
||||
@@ -144,10 +149,9 @@ pub extern "C" fn g_rec_mutex_clear(_rec_mutex: *mut GRecMutex) {
|
||||
pub extern "C" fn g_rec_mutex_lock(rec_mutex: *mut GRecMutex) {
|
||||
unsafe {
|
||||
let impl_ = rec_mutex_impl(rec_mutex);
|
||||
let current_thread = get_current_thread_id();
|
||||
|
||||
loop {
|
||||
if rec_mutex_try_acquire(impl_, current_thread) {
|
||||
if rec_mutex_try_acquire(impl_) {
|
||||
break;
|
||||
}
|
||||
rec_mutex_wait(impl_);
|
||||
@@ -159,9 +163,8 @@ pub extern "C" fn g_rec_mutex_lock(rec_mutex: *mut GRecMutex) {
|
||||
pub extern "C" fn g_rec_mutex_trylock(rec_mutex: *mut GRecMutex) -> u32 {
|
||||
unsafe {
|
||||
let impl_ = rec_mutex_impl(rec_mutex);
|
||||
let current_thread = get_current_thread_id();
|
||||
|
||||
if rec_mutex_try_acquire(impl_, current_thread) {
|
||||
if rec_mutex_try_acquire(impl_) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
@@ -183,7 +186,7 @@ pub extern "C" fn g_rec_mutex_unlock(rec_mutex: *mut GRecMutex) {
|
||||
.is_ok()
|
||||
{
|
||||
impl_.owner.store(0, Ordering::Relaxed);
|
||||
lock_notify_all();
|
||||
wake_waiters(&impl_.count);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -198,7 +201,10 @@ pub extern "C" fn g_rec_mutex_unlock(rec_mutex: *mut GRecMutex) {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl, current_thread: u64) -> bool {
|
||||
// Asking the kernel who we are costs more than the uncontended acquisition itself, and
|
||||
// every allocation takes this path, so the identity is only fetched once the lock turns
|
||||
// out to be held.
|
||||
unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl) -> bool {
|
||||
let current_count = impl_.count.load(Ordering::Relaxed);
|
||||
|
||||
if current_count == 0 {
|
||||
@@ -206,12 +212,12 @@ unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl, current_thread: u64) -
|
||||
.compare_exchange_weak(0, 1, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
impl_.owner.store(current_thread, Ordering::Relaxed);
|
||||
impl_.owner.store(crate::kernel::current_thread_id() as usize, Ordering::Relaxed);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
let current_owner = impl_.owner.load(Ordering::Relaxed);
|
||||
if current_owner == current_thread {
|
||||
if current_owner == crate::kernel::current_thread_id() as usize {
|
||||
if impl_.count
|
||||
.compare_exchange_weak(
|
||||
current_count,
|
||||
@@ -230,7 +236,7 @@ unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl, current_thread: u64) -
|
||||
|
||||
unsafe fn rec_mutex_wait(impl_: &mut RecMutexImpl) {
|
||||
while impl_.count.load(Ordering::Relaxed) != 0 {
|
||||
unsafe { lock_wait_primitive() };
|
||||
wait_on(&impl_.count, &mut || impl_.count.load(Ordering::Relaxed) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +288,7 @@ pub extern "C" fn g_rw_lock_writer_unlock(rw_lock: *mut GRWLock) {
|
||||
unsafe {
|
||||
let impl_ = rw_lock_impl(rw_lock);
|
||||
impl_.state.store(0, Ordering::Release);
|
||||
lock_notify_all();
|
||||
wake_waiters(&impl_.state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +335,7 @@ pub extern "C" fn g_rw_lock_reader_unlock(rw_lock: *mut GRWLock) {
|
||||
.is_ok()
|
||||
{
|
||||
if current - 1 == 0 {
|
||||
lock_notify_all();
|
||||
wake_waiters(&impl_.state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -355,13 +361,15 @@ unsafe fn rw_lock_reader_try_acquire(impl_: &mut RWLockImpl) -> bool {
|
||||
|
||||
unsafe fn rw_lock_wait_for_writers(impl_: &mut RWLockImpl) {
|
||||
while impl_.state.load(Ordering::Relaxed) & RW_WRITER_LOCK_BIT != 0 {
|
||||
unsafe { lock_wait_primitive() };
|
||||
wait_on(&impl_.state, &mut || {
|
||||
impl_.state.load(Ordering::Relaxed) & RW_WRITER_LOCK_BIT == 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn rw_lock_wait_for_all(impl_: &mut RWLockImpl) {
|
||||
while impl_.state.load(Ordering::Relaxed) != 0 {
|
||||
unsafe { lock_wait_primitive() };
|
||||
wait_on(&impl_.state, &mut || impl_.state.load(Ordering::Relaxed) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +377,7 @@ unsafe fn rw_lock_wait_for_all(impl_: &mut RWLockImpl) {
|
||||
pub extern "C" fn g_cond_init(cond: *mut GCond) {
|
||||
unsafe {
|
||||
let impl_ = cond_impl(cond);
|
||||
impl_.signal.store(0, Ordering::Relaxed);
|
||||
impl_.generation.store(0, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,11 +387,17 @@ pub extern "C" fn g_cond_clear(_cond: *mut GCond) {}
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_cond_wait(cond: *mut GCond, mutex: *mut GMutex) {
|
||||
unsafe {
|
||||
// Read the generation under the mutex, so a signal that lands after the
|
||||
// unlock below is still observed as a change.
|
||||
let seen = cond_impl(cond).generation.load(Ordering::Acquire);
|
||||
|
||||
g_mutex_unlock(mutex);
|
||||
|
||||
let impl_ = cond_impl(cond);
|
||||
while impl_.signal.load(Ordering::Acquire) == 0 {
|
||||
lock_wait_primitive();
|
||||
while impl_.generation.load(Ordering::Acquire) == seen {
|
||||
wait_on(&impl_.generation, &mut || {
|
||||
impl_.generation.load(Ordering::Acquire) != seen
|
||||
});
|
||||
}
|
||||
|
||||
g_mutex_lock(mutex);
|
||||
@@ -394,8 +408,8 @@ pub extern "C" fn g_cond_wait(cond: *mut GCond, mutex: *mut GMutex) {
|
||||
pub extern "C" fn g_cond_signal(cond: *mut GCond) {
|
||||
unsafe {
|
||||
let impl_ = cond_impl(cond);
|
||||
impl_.signal.store(1, Ordering::Release);
|
||||
lock_notify_all();
|
||||
impl_.generation.fetch_add(1, Ordering::Release);
|
||||
wake_waiters(&impl_.generation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,22 +417,37 @@ pub extern "C" fn g_cond_signal(cond: *mut GCond) {
|
||||
pub extern "C" fn g_cond_broadcast(cond: *mut GCond) {
|
||||
unsafe {
|
||||
let impl_ = cond_impl(cond);
|
||||
impl_.signal.store(1, Ordering::Release);
|
||||
lock_notify_all();
|
||||
impl_.generation.fetch_add(1, Ordering::Release);
|
||||
wake_waiters(&impl_.generation);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_cond_wait_until(cond: *mut GCond, mutex: *mut GMutex, _end_time: i64) -> u32 {
|
||||
// TODO: Implement proper timeout handling
|
||||
g_cond_wait(cond, mutex);
|
||||
1
|
||||
pub extern "C" fn g_cond_wait_until(cond: *mut GCond, mutex: *mut GMutex, end_time: i64) -> u32 {
|
||||
unsafe {
|
||||
let seen = cond_impl(cond).generation.load(Ordering::Acquire);
|
||||
|
||||
g_mutex_unlock(mutex);
|
||||
|
||||
let impl_ = cond_impl(cond);
|
||||
let mut signalled = impl_.generation.load(Ordering::Acquire) != seen;
|
||||
while !signalled && crate::kernel::monotonic_micros() < end_time {
|
||||
wait_on(&impl_.generation, &mut || {
|
||||
impl_.generation.load(Ordering::Acquire) != seen
|
||||
});
|
||||
signalled = impl_.generation.load(Ordering::Acquire) != seen;
|
||||
}
|
||||
|
||||
g_mutex_lock(mutex);
|
||||
|
||||
if signalled { 1 } else { 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_private_get(key: *mut GPrivate) -> *mut c_void {
|
||||
unsafe {
|
||||
let thread_id = get_current_thread_id();
|
||||
let thread_id = crate::kernel::current_thread_id();
|
||||
lock_acquire(&TLS_LOCK);
|
||||
|
||||
ensure_tls_storage();
|
||||
@@ -438,7 +467,7 @@ pub extern "C" fn g_private_get(key: *mut GPrivate) -> *mut c_void {
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_private_set(key: *mut GPrivate, value: *mut c_void) {
|
||||
unsafe {
|
||||
let thread_id = get_current_thread_id();
|
||||
let thread_id = crate::kernel::current_thread_id();
|
||||
lock_acquire(&TLS_LOCK);
|
||||
|
||||
ensure_tls_storage();
|
||||
@@ -468,7 +497,9 @@ pub extern "C" fn _g_system_thread_create(
|
||||
data: gpointer,
|
||||
) -> *mut GSystemThread {
|
||||
unsafe {
|
||||
let system_thread = crate::xnu::kalloc(core::mem::size_of::<SystemThreadImpl>()) as *mut SystemThreadImpl;
|
||||
crate::kernel::log("frida: glib is creating a thread\n\0");
|
||||
|
||||
let system_thread = crate::kernel::alloc(core::mem::size_of::<SystemThreadImpl>()) as *mut SystemThreadImpl;
|
||||
|
||||
g_mutex_init(&mut (*system_thread).mutex);
|
||||
g_cond_init(&mut (*system_thread).cond);
|
||||
@@ -478,12 +509,12 @@ pub extern "C" fn _g_system_thread_create(
|
||||
(*system_thread).finished.store(0, Ordering::Relaxed);
|
||||
(*system_thread).detached.store(0, Ordering::Relaxed);
|
||||
|
||||
let wrapper_data = crate::xnu::kalloc(core::mem::size_of::<ThreadWrapperData>()) as *mut ThreadWrapperData;
|
||||
let wrapper_data = crate::kernel::alloc(core::mem::size_of::<ThreadWrapperData>()) as *mut ThreadWrapperData;
|
||||
(*wrapper_data).func = func;
|
||||
(*wrapper_data).data = data;
|
||||
(*wrapper_data).system_thread = system_thread;
|
||||
|
||||
let _xnu_result = crate::xnu::kernel_thread_start(thread_wrapper, wrapper_data as *mut c_void);
|
||||
let _xnu_result = crate::kernel::spawn_thread(thread_wrapper, wrapper_data as *mut c_void);
|
||||
|
||||
system_thread as *mut GSystemThread
|
||||
}
|
||||
@@ -496,8 +527,9 @@ extern "C" fn thread_wrapper(parameter: *mut c_void, _wait_result: i32) {
|
||||
let data = (*wrapper_data).data;
|
||||
let system_thread = (*wrapper_data).system_thread;
|
||||
|
||||
let current_thread_id = get_current_thread_id();
|
||||
(*system_thread).thread_id.store(current_thread_id, Ordering::Relaxed);
|
||||
let current_thread_id = crate::kernel::current_thread_id();
|
||||
(*system_thread).thread_id.store(current_thread_id, Ordering::Release);
|
||||
wake_waiters(&(*system_thread).thread_id);
|
||||
|
||||
func.unwrap()(data);
|
||||
|
||||
@@ -537,12 +569,10 @@ pub extern "C" fn _g_system_thread_wait(thread: *mut GSystemThread) {
|
||||
unsafe {
|
||||
let system_thread = thread as *mut SystemThreadImpl;
|
||||
|
||||
loop {
|
||||
let thread_id = (*system_thread).thread_id.load(Ordering::Acquire);
|
||||
if thread_id != 0 {
|
||||
break;
|
||||
}
|
||||
core::hint::spin_loop();
|
||||
while (*system_thread).thread_id.load(Ordering::Acquire) == 0 {
|
||||
wait_on(&(*system_thread).thread_id, &mut || {
|
||||
(*system_thread).thread_id.load(Ordering::Acquire) != 0
|
||||
});
|
||||
}
|
||||
|
||||
g_mutex_lock(&mut (*system_thread).mutex);
|
||||
@@ -556,19 +586,16 @@ pub extern "C" fn _g_system_thread_wait(thread: *mut GSystemThread) {
|
||||
}
|
||||
|
||||
unsafe fn cleanup_system_thread(system_thread: *mut SystemThreadImpl) {
|
||||
crate::xnu::free(system_thread as *mut u8, core::mem::size_of::<SystemThreadImpl>());
|
||||
crate::kernel::free(system_thread as *mut u8, core::mem::size_of::<SystemThreadImpl>());
|
||||
}
|
||||
|
||||
unsafe fn cleanup_wrapper_data(wrapper_data: *mut ThreadWrapperData) {
|
||||
crate::xnu::free(wrapper_data as *mut u8, core::mem::size_of::<ThreadWrapperData>());
|
||||
crate::kernel::free(wrapper_data as *mut u8, core::mem::size_of::<ThreadWrapperData>());
|
||||
}
|
||||
|
||||
unsafe fn lock_acquire(lock: &AtomicU32) {
|
||||
loop {
|
||||
if unsafe { lock_try_acquire(lock) } {
|
||||
break;
|
||||
}
|
||||
unsafe { lock_wait(lock) };
|
||||
while !unsafe { lock_try_acquire(lock) } {
|
||||
wait_on(lock, &mut || lock.load(Ordering::Relaxed) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,35 +606,36 @@ unsafe fn lock_try_acquire(lock: &AtomicU32) -> bool {
|
||||
|
||||
unsafe fn lock_release(lock: &AtomicU32) {
|
||||
lock.store(0, Ordering::Release);
|
||||
unsafe { lock_notify_all() };
|
||||
wake_waiters(lock);
|
||||
}
|
||||
|
||||
unsafe fn lock_wait(lock: &AtomicU32) {
|
||||
while lock.load(Ordering::Relaxed) != 0 {
|
||||
unsafe { lock_wait_primitive() };
|
||||
}
|
||||
// Sleeps until `ready` holds or the slice expires, whichever comes first.
|
||||
//
|
||||
// These used to spin on wfe/sev. A kernel thread that spins never reaches the
|
||||
// scheduler, so a lost wakeup — or a genuine deadlock, which is how we found this —
|
||||
// starves the CPU it is on until the hardware watchdog takes the machine down. The
|
||||
// bounded sleep turns both into a stall that can be observed instead.
|
||||
//
|
||||
// The address of the word being waited on names the wait channel: XNU pairs
|
||||
// assert_wait()/thread_wakeup() on it directly, while the Linux shim has a single
|
||||
// channel and treats it as a hint. Every caller re-checks its own condition in a
|
||||
// loop, so a spurious wakeup costs nothing either way.
|
||||
fn wait_on<T>(word: &T, ready: &mut dyn FnMut() -> bool) {
|
||||
crate::kernel::wait(token_of(word), Some(WAIT_SLICE_US), ready);
|
||||
|
||||
// The wait above returns without sleeping whenever `ready` already holds, so on
|
||||
// its own it is not enough to keep a contended loop off the CPU: every caller
|
||||
// here loops, and a loop in a kernel thread that never reaches the scheduler
|
||||
// takes the machine down rather than merely running hot. Yield unconditionally.
|
||||
crate::kernel::yield_now();
|
||||
}
|
||||
|
||||
unsafe fn lock_wait_primitive() {
|
||||
unsafe {
|
||||
asm!("wfe", options(nomem, nostack));
|
||||
}
|
||||
fn wake_waiters<T>(word: &T) {
|
||||
crate::kernel::wake(token_of(word));
|
||||
}
|
||||
|
||||
unsafe fn lock_notify_all() {
|
||||
unsafe {
|
||||
asm!("sev", options(nomem, nostack));
|
||||
}
|
||||
fn token_of<T>(word: &T) -> *const u8 {
|
||||
word as *const T as *const u8
|
||||
}
|
||||
|
||||
// The thread pointer is exposed to JavaScript as a GumThreadId, so it must fit
|
||||
// in a double without losing precision; the low bits keep it unique per thread.
|
||||
const JS_SAFE_THREAD_ID_MASK: u64 = (1 << 48) - 1;
|
||||
|
||||
pub unsafe fn get_current_thread_id() -> u64 {
|
||||
let thread_ptr: u64;
|
||||
unsafe {
|
||||
asm!("mrs {}, tpidr_el1", out(reg) thread_ptr, options(nomem, nostack));
|
||||
}
|
||||
thread_ptr & JS_SAFE_THREAD_ID_MASK
|
||||
}
|
||||
const WAIT_SLICE_US: u64 = 1000;
|
||||
+85
-413
@@ -1,87 +1,42 @@
|
||||
// Gum's platform backend, minus the parts that depend on which kernel we are
|
||||
// running inside: memory protection, symbols and the module registry live in
|
||||
// gum_xnu.rs and gum_linux.rs respectively.
|
||||
|
||||
use crate::{
|
||||
bindings::{
|
||||
_GInterfaceInfo, _GTypeInfo, _GumPageProtection_GUM_PAGE_EXECUTE,
|
||||
_GumPageProtection_GUM_PAGE_READ, _GumPageProtection_GUM_PAGE_WRITE,
|
||||
_GumRwxSupport_GUM_RWX_NONE, GArray, GObject, GObjectClass, GPrivate, GType,
|
||||
GumDebugSymbolDetails, GumExportDetails, GumExportType_GUM_EXPORT_FUNCTION,
|
||||
GumFoundExportFunc, GumMemoryRange, GumModule, GumModuleInterface, GumModuleRegistry,
|
||||
GumPageProtection, GumRwxSupport, GumThreadId, GumTlsKey, g_array_append_vals, g_array_new,
|
||||
g_free, g_object_get_type, g_object_new, g_object_unref, g_once_init_enter,
|
||||
g_once_init_leave, g_strdup, g_type_add_interface_static, g_type_class_peek_parent,
|
||||
g_type_register_static, g_variant_get_boolean, g_variant_get_uint64, g_variant_new,
|
||||
g_variant_new_fixed_array, g_variant_type_free, g_variant_type_new, gboolean, gchar,
|
||||
gconstpointer, gpointer, gsize, guint,
|
||||
gum_barebone_register_module,
|
||||
gum_barebone_try_remap_writable_pages as _gum_barebone_try_remap_writable_pages,
|
||||
g_variant_unref, gum_module_get_type, gum_mprotect, gum_query_page_size,
|
||||
_GInterfaceInfo, _GTypeInfo, GObject, GObjectClass, GPrivate, GType, GumExportDetails,
|
||||
GumExportType_GUM_EXPORT_FUNCTION, GumFoundExportFunc, GumMemoryRange, GumModule,
|
||||
GumModuleInterface, GumThreadId, GumTlsKey, g_free, g_object_get_type, g_object_new,
|
||||
g_once_init_enter, g_once_init_leave, g_strdup, g_type_add_interface_static,
|
||||
g_type_class_peek_parent, g_type_register_static, gchar, gpointer, gsize, guint,
|
||||
},
|
||||
gthread, host_rpc, libc, FridaCommand,
|
||||
gthread, kernel, libc,
|
||||
};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::ffi::CString;
|
||||
use alloc::format;
|
||||
use alloc::vec::Vec;
|
||||
use core::ffi::CStr;
|
||||
use core::mem::size_of;
|
||||
use core::ptr;
|
||||
use core::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
// Code slabs the agent allocates are normal writable RAM, so gum can keep a real writable alias to
|
||||
// them across the slab's lifetime. Pre-existing kernel/kext text is CTRR-locked and can only be
|
||||
// modified through the host's physical-memory bridge, so we shadow it and commit on dispose. We
|
||||
// distinguish the two by tracking the executable ranges we hand out.
|
||||
static SLAB_LOCK: AtomicU32 = AtomicU32::new(0);
|
||||
static mut SLABS: Vec<(u64, u64)> = Vec::new();
|
||||
|
||||
const SHADOW_MAGIC: u64 = 0x4644_4f48_5341_4853;
|
||||
const SHADOW_HEADER: usize = 24;
|
||||
const SHADOW_MIN_ADDRESS: u64 = 0xffff_f000_0000_0000;
|
||||
|
||||
fn slab_lock() {
|
||||
while SLAB_LOCK
|
||||
.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_err()
|
||||
{
|
||||
core::hint::spin_loop();
|
||||
}
|
||||
}
|
||||
|
||||
fn slab_unlock() {
|
||||
SLAB_LOCK.store(0, Ordering::Release);
|
||||
}
|
||||
|
||||
fn register_slab(start: u64, size: usize) {
|
||||
slab_lock();
|
||||
unsafe {
|
||||
(*core::ptr::addr_of_mut!(SLABS)).push((start, start + size as u64));
|
||||
}
|
||||
slab_unlock();
|
||||
}
|
||||
|
||||
fn unregister_slab(start: u64) {
|
||||
slab_lock();
|
||||
unsafe {
|
||||
(*core::ptr::addr_of_mut!(SLABS)).retain(|&(begin, _)| begin != start);
|
||||
}
|
||||
slab_unlock();
|
||||
}
|
||||
|
||||
fn is_agent_slab(address: u64) -> bool {
|
||||
slab_lock();
|
||||
let found = unsafe {
|
||||
(*core::ptr::addr_of!(SLABS))
|
||||
.iter()
|
||||
.any(|&(begin, end)| address >= begin && address < end)
|
||||
};
|
||||
slab_unlock();
|
||||
found
|
||||
}
|
||||
#[cfg(feature = "linux")]
|
||||
use crate::gum_linux::enumerate_exports_in_range;
|
||||
#[cfg(feature = "win9x")]
|
||||
use crate::gum_win9x::enumerate_exports_in_range;
|
||||
#[cfg(feature = "xnu")]
|
||||
use crate::gum_xnu::enumerate_exports_in_range;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_process_get_current_thread_id() -> GumThreadId {
|
||||
unsafe { gthread::get_current_thread_id() }
|
||||
kernel::current_thread_id() as GumThreadId
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_barebone_query_page_size() -> guint {
|
||||
4096
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_barebone_query_page_size() -> guint {
|
||||
unsafe {
|
||||
@@ -99,200 +54,6 @@ pub extern "C" fn gum_barebone_query_page_size() -> guint {
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_query_rwx_support() -> GumRwxSupport {
|
||||
_GumRwxSupport_GUM_RWX_NONE
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_can_remap_writable() -> gboolean {
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_try_remap_writable_pages(
|
||||
first_page: gpointer,
|
||||
n_pages: guint,
|
||||
) -> gpointer {
|
||||
if is_agent_slab(first_page as u64) {
|
||||
return remap_agent_pages(first_page, n_pages);
|
||||
}
|
||||
shadow_kernel_pages(first_page, n_pages)
|
||||
}
|
||||
|
||||
fn remap_agent_pages(first_page: gpointer, n_pages: guint) -> gpointer {
|
||||
unsafe {
|
||||
let page_size = gum_query_page_size() as usize;
|
||||
let mut virtual_addrs = Vec::with_capacity(n_pages as usize);
|
||||
|
||||
let mut current_page = first_page as u64;
|
||||
for _ in 0..n_pages {
|
||||
virtual_addrs.push(current_page as gpointer);
|
||||
current_page += page_size as u64;
|
||||
}
|
||||
|
||||
_gum_barebone_try_remap_writable_pages(
|
||||
virtual_addrs.as_ptr() as *mut *const core::ffi::c_void,
|
||||
virtual_addrs.len() as guint,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn shadow_kernel_pages(first_page: gpointer, n_pages: guint) -> gpointer {
|
||||
unsafe {
|
||||
let total = n_pages as usize * gum_query_page_size() as usize;
|
||||
let buffer = crate::xnu::kalloc(SHADOW_HEADER + total);
|
||||
*(buffer as *mut u64) = SHADOW_MAGIC;
|
||||
*(buffer.add(8) as *mut u64) = first_page as u64;
|
||||
*(buffer.add(16) as *mut u32) = n_pages;
|
||||
|
||||
let body = buffer.add(SHADOW_HEADER);
|
||||
core::ptr::copy_nonoverlapping(first_page as *const u8, body, total);
|
||||
body as gpointer
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_dispose_writable_pages(writable: gpointer, _n_pages: guint) {
|
||||
if (writable as u64) < SHADOW_MIN_ADDRESS {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
let buffer = (writable as *mut u8).sub(SHADOW_HEADER);
|
||||
if *(buffer as *const u64) != SHADOW_MAGIC {
|
||||
return;
|
||||
}
|
||||
let first_page = *(buffer.add(8) as *const u64);
|
||||
let n_pages = *(buffer.add(16) as *const u32);
|
||||
let total = n_pages as usize * gum_query_page_size() as usize;
|
||||
|
||||
commit_kernel_patch(first_page, writable as *const u8, total);
|
||||
libc::__clear_cache(first_page as *const u8, (first_page + total as u64) as *const u8);
|
||||
|
||||
crate::xnu::free(buffer, SHADOW_HEADER + total);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn commit_kernel_patch(address: u64, data: *const u8, len: usize) {
|
||||
unsafe {
|
||||
let element_type = g_variant_type_new(c"y".as_ptr());
|
||||
let bytes = g_variant_new_fixed_array(element_type, data as gconstpointer, len as gsize, 1);
|
||||
g_variant_type_free(element_type);
|
||||
|
||||
let payload = g_variant_new(c"(t@ay)".as_ptr(), address, bytes);
|
||||
let reply = host_rpc(FridaCommand::PatchCode, payload);
|
||||
g_variant_unref(reply);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_barebone_try_remap_writable_pages(
|
||||
addrs: *const gpointer,
|
||||
n_addrs: guint,
|
||||
) -> gpointer {
|
||||
if !crate::transport_is_up() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
unsafe {
|
||||
let element_type = g_variant_type_new(c"t".as_ptr());
|
||||
let payload = g_variant_new_fixed_array(
|
||||
element_type,
|
||||
addrs as gconstpointer,
|
||||
n_addrs as gsize,
|
||||
size_of::<u64>() as gsize,
|
||||
);
|
||||
g_variant_type_free(element_type);
|
||||
|
||||
let reply = host_rpc(FridaCommand::RemapWritablePages, payload);
|
||||
let virtual_address = g_variant_get_uint64(reply);
|
||||
g_variant_unref(reply);
|
||||
|
||||
virtual_address as gpointer
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_try_mprotect(
|
||||
address: gpointer,
|
||||
size: gsize,
|
||||
prot: GumPageProtection,
|
||||
) -> gboolean {
|
||||
if !crate::transport_is_up() {
|
||||
return 1;
|
||||
}
|
||||
unsafe {
|
||||
let payload = g_variant_new(c"(ttu)".as_ptr(), address as u64, size as u64, prot as u32);
|
||||
|
||||
let reply = host_rpc(FridaCommand::MemoryProtect, payload);
|
||||
let success = g_variant_get_boolean(reply);
|
||||
g_variant_unref(reply);
|
||||
|
||||
if success != 0 {
|
||||
flush_tlb_range(address as u64, size as u64);
|
||||
}
|
||||
|
||||
success
|
||||
}
|
||||
}
|
||||
|
||||
// The host rewrites our page-table descriptors through the physical-memory
|
||||
// bridge, which leaves this CPU's TLB holding the stale translation. Stalker
|
||||
// flips a slab page RW then RX in place, so without this the freeze never takes
|
||||
// effect and executing the page faults with a permission abort.
|
||||
unsafe fn flush_tlb_range(address: u64, size: u64) {
|
||||
unsafe {
|
||||
let page_size = gum_query_page_size() as u64;
|
||||
let start = address & !(page_size - 1);
|
||||
let end = (address + size + page_size - 1) & !(page_size - 1);
|
||||
|
||||
core::arch::asm!("dsb ish", options(nostack, preserves_flags));
|
||||
let mut va = start;
|
||||
while va < end {
|
||||
core::arch::asm!("tlbi vaae1is, {operand}", operand = in(reg) va >> 12,
|
||||
options(nostack, preserves_flags));
|
||||
va += page_size;
|
||||
}
|
||||
core::arch::asm!("dsb ish", "isb", options(nostack, preserves_flags));
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_allocate(
|
||||
_address: gpointer,
|
||||
size: gsize,
|
||||
_alignment: gsize,
|
||||
prot: GumPageProtection,
|
||||
) -> gpointer {
|
||||
let ptr = crate::xnu::kalloc(size as usize);
|
||||
unsafe {
|
||||
core::ptr::write_bytes(ptr, 0, size as usize);
|
||||
if (prot & _GumPageProtection_GUM_PAGE_EXECUTE) != 0 {
|
||||
register_slab(ptr as u64, size as usize);
|
||||
gum_mprotect(ptr as gpointer, size, prot);
|
||||
}
|
||||
}
|
||||
ptr as gpointer
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_free(address: gpointer, size: gsize) -> gboolean {
|
||||
// Executable slabs were flipped to RX in the page tables; restore RW before returning them to
|
||||
// the allocator, otherwise the reclaimed pages stay non-writable and the next consumer faults.
|
||||
if is_agent_slab(address as u64) {
|
||||
unsafe {
|
||||
gum_mprotect(
|
||||
address,
|
||||
size,
|
||||
(_GumPageProtection_GUM_PAGE_READ | _GumPageProtection_GUM_PAGE_WRITE)
|
||||
as GumPageProtection,
|
||||
);
|
||||
}
|
||||
unregister_slab(address as u64);
|
||||
}
|
||||
crate::xnu::free(address as *mut u8, size as usize);
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_clear_cache(address: gpointer, size: gsize) {
|
||||
unsafe {
|
||||
@@ -324,40 +85,6 @@ pub extern "C" fn gum_tls_key_set_value(key: GumTlsKey, value: gpointer) {
|
||||
gthread::g_private_set(key as *mut GPrivate, value)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_barebone_on_registry_activating(registry: *mut GumModuleRegistry) {
|
||||
let kernel_base = crate::xnu::get_kernel_base();
|
||||
|
||||
unsafe {
|
||||
let module_infos = core::ptr::addr_of!(crate::MODULE_INFO);
|
||||
let module_infos = &*module_infos;
|
||||
|
||||
let mut i = 0;
|
||||
for module_info in module_infos.iter() {
|
||||
let module_base = kernel_base + module_info.offset as u64;
|
||||
|
||||
let module_path = if i == 0 {
|
||||
"/System/Library/Kernels/kernel"
|
||||
} else {
|
||||
&format!(
|
||||
"/System/Library/Extensions/{}.kext/{}",
|
||||
module_info.name, module_info.name
|
||||
)
|
||||
};
|
||||
let module_range = GumMemoryRange {
|
||||
base_address: module_base,
|
||||
size: module_info.size as u64,
|
||||
};
|
||||
|
||||
let module = gum_native_module_new(&module_path, &module_info.version, &module_range);
|
||||
gum_barebone_register_module(registry, module);
|
||||
g_object_unref(module as gpointer);
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct GumNativeModule {
|
||||
parent: GObject,
|
||||
@@ -405,7 +132,7 @@ fn gum_native_module_get_type() -> GType {
|
||||
interface_data: core::ptr::null_mut(),
|
||||
};
|
||||
|
||||
g_type_add_interface_static(new_type, gum_module_get_type(), &interface_info);
|
||||
g_type_add_interface_static(new_type, crate::bindings::gum_module_get_type(), &interface_info);
|
||||
|
||||
g_once_init_leave(
|
||||
core::ptr::addr_of_mut!(GUM_NATIVE_MODULE_TYPE) as *mut ::core::ffi::c_void,
|
||||
@@ -448,7 +175,11 @@ unsafe extern "C" fn gum_native_module_finalize(object: *mut GObject) {
|
||||
}
|
||||
}
|
||||
|
||||
fn gum_native_module_new(path: &str, version: &str, range: &GumMemoryRange) -> *mut GumModule {
|
||||
pub(crate) fn gum_native_module_new(
|
||||
path: &str,
|
||||
version: &str,
|
||||
range: &GumMemoryRange,
|
||||
) -> *mut GumModule {
|
||||
unsafe {
|
||||
let path_cstr = CString::new(path).unwrap();
|
||||
let version_cstr = CString::new(version).unwrap();
|
||||
@@ -499,134 +230,75 @@ unsafe extern "C" fn gum_native_module_enumerate_exports(
|
||||
) {
|
||||
unsafe {
|
||||
let module = self_ as *mut GumNativeModule;
|
||||
let module_range = &(*module).range;
|
||||
|
||||
let symbol_table = core::ptr::addr_of!(crate::SYMBOL_TABLE);
|
||||
let symbol_table = &*symbol_table;
|
||||
|
||||
const N_EXT: u8 = 0x01; // External symbol flag
|
||||
const N_TYPE: u8 = 0x0e; // Type mask
|
||||
const N_SECT: u8 = 0x0e; // Defined in section
|
||||
|
||||
let start_address = module_range.base_address;
|
||||
let end_address = module_range.base_address + module_range.size;
|
||||
|
||||
for symbol_ref in symbol_table.iter_symbols_in_range(start_address, end_address) {
|
||||
let is_external = (symbol_ref.symbol_type() & N_EXT) != 0;
|
||||
let is_defined = (symbol_ref.symbol_type() & N_TYPE) == N_SECT;
|
||||
if !is_external || !is_defined {
|
||||
continue;
|
||||
}
|
||||
|
||||
let export_type = GumExportType_GUM_EXPORT_FUNCTION;
|
||||
let range = (*module).range;
|
||||
|
||||
let mut on_export = |name: *const gchar, address: u64| {
|
||||
let export_details = GumExportDetails {
|
||||
type_: export_type,
|
||||
name: g_strdup(symbol_ref.name_ptr()),
|
||||
address: symbol_ref.address(),
|
||||
type_: GumExportType_GUM_EXPORT_FUNCTION,
|
||||
name: g_strdup(name),
|
||||
address,
|
||||
size: -1,
|
||||
};
|
||||
|
||||
let should_continue =
|
||||
func.unwrap()(&export_details as *const GumExportDetails, user_data);
|
||||
if should_continue == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
func.unwrap()(&export_details as *const GumExportDetails, user_data) != 0
|
||||
};
|
||||
|
||||
enumerate_exports_in_range(
|
||||
range.base_address,
|
||||
range.base_address + range.size as u64,
|
||||
&mut on_export,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_symbol_details_from_address(
|
||||
address: gpointer,
|
||||
details: *mut GumDebugSymbolDetails,
|
||||
) -> gboolean {
|
||||
/// What the backends implement for `gum_native_module_enumerate_exports`: call
|
||||
/// `callback` for every exported function in `[start, end)`, stopping early when
|
||||
/// it returns false.
|
||||
pub(crate) type FoundExportCallback<'a> = dyn FnMut(*const gchar, u64) -> bool + 'a;
|
||||
|
||||
// Code slabs the agent allocates are normal writable RAM, so Gum can keep a real writable alias to
|
||||
// them across the slab's lifetime. Pre-existing kernel text is write-protected — CTRR-locked on
|
||||
// Apple silicon, STRICT_KERNEL_RWX on Linux — and can only be modified through a separate path, so
|
||||
// we treat it differently. We distinguish the two by tracking the executable ranges we hand out.
|
||||
static SLAB_LOCK: AtomicU32 = AtomicU32::new(0);
|
||||
static mut SLABS: Vec<(u64, u64)> = Vec::new();
|
||||
|
||||
fn slab_lock() {
|
||||
while SLAB_LOCK
|
||||
.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_err()
|
||||
{
|
||||
kernel::yield_now();
|
||||
}
|
||||
}
|
||||
|
||||
fn slab_unlock() {
|
||||
SLAB_LOCK.store(0, Ordering::Release);
|
||||
}
|
||||
|
||||
pub(crate) fn register_slab(start: u64, size: usize) {
|
||||
slab_lock();
|
||||
unsafe {
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
if let Some(symbol) = table.find_symbol_by_address(address as u64) {
|
||||
let name_bytes = symbol.name().as_bytes();
|
||||
let copy_len = core::cmp::min(name_bytes.len(), 2048);
|
||||
core::ptr::copy_nonoverlapping(
|
||||
name_bytes.as_ptr(),
|
||||
(*details).symbol_name.as_mut_ptr() as *mut u8,
|
||||
copy_len,
|
||||
);
|
||||
(*details).symbol_name[copy_len] = 0;
|
||||
|
||||
(*details).address = symbol.address();
|
||||
|
||||
(*details).module_name[0] = 0;
|
||||
(*details).file_name[0] = 0;
|
||||
(*details).line_number = 0;
|
||||
(*details).column = 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
0
|
||||
(*core::ptr::addr_of_mut!(SLABS)).push((start, start + size as u64));
|
||||
}
|
||||
slab_unlock();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_symbol_name_from_address(address: gpointer) -> *mut gchar {
|
||||
pub(crate) fn unregister_slab(start: u64) {
|
||||
slab_lock();
|
||||
unsafe {
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
let name_ptr = table.find_symbol_name_ptr_by_address(address as u64);
|
||||
g_strdup(name_ptr as *const gchar)
|
||||
(*core::ptr::addr_of_mut!(SLABS)).retain(|&(begin, _)| begin != start);
|
||||
}
|
||||
slab_unlock();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_function(name: *const gchar) -> gpointer {
|
||||
unsafe {
|
||||
let target_name = CStr::from_ptr(name).to_string_lossy();
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
|
||||
if let Some(symbol) = table.find_symbol_by_name(&target_name) {
|
||||
return symbol.address() as gpointer;
|
||||
}
|
||||
|
||||
core::ptr::null_mut()
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_functions_named(name: *const gchar) -> *mut GArray {
|
||||
unsafe {
|
||||
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
|
||||
|
||||
let target_name = CStr::from_ptr(name).to_string_lossy();
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
|
||||
let symbols = table.find_symbols_by_name(&target_name);
|
||||
for symbol in symbols {
|
||||
let addr = symbol.address() as gpointer;
|
||||
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
|
||||
}
|
||||
|
||||
array
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_functions_matching(pattern: *const gchar) -> *mut GArray {
|
||||
unsafe {
|
||||
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
|
||||
|
||||
let glob_pattern = CStr::from_ptr(pattern).to_string_lossy();
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
|
||||
let symbols = table.find_symbols_matching_glob(&glob_pattern);
|
||||
for symbol in symbols {
|
||||
let addr = symbol.address() as gpointer;
|
||||
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
|
||||
}
|
||||
|
||||
array
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_load_symbols(_path: *const gchar) -> gboolean {
|
||||
0
|
||||
pub(crate) fn is_agent_slab(address: u64) -> bool {
|
||||
slab_lock();
|
||||
let found = unsafe {
|
||||
(*core::ptr::addr_of!(SLABS))
|
||||
.iter()
|
||||
.any(|&(begin, end)| address >= begin && address < end)
|
||||
};
|
||||
slab_unlock();
|
||||
found
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// Linux half of Gum's platform backend. Everything is done locally: we are the
|
||||
// kernel, so page permissions go through set_memory_*(), writable aliases of
|
||||
// write-protected text come from vmap(), and symbols come from kallsyms.
|
||||
|
||||
use crate::{
|
||||
bindings::{
|
||||
_GumPageProtection_GUM_PAGE_EXECUTE, _GumPageProtection_GUM_PAGE_READ,
|
||||
_GumPageProtection_GUM_PAGE_WRITE, _GumRwxSupport_GUM_RWX_NONE, GArray,
|
||||
GumDebugSymbolDetails, GumMemoryRange, GumModuleRegistry, GumPageProtection, GumRwxSupport,
|
||||
g_array_append_vals, g_array_new, g_object_unref, g_pattern_spec_free, g_pattern_spec_match_string,
|
||||
g_pattern_spec_new, g_strdup, gboolean, gchar, gconstpointer, gpointer, gsize, guint,
|
||||
gum_barebone_register_module, gum_mprotect,
|
||||
},
|
||||
gum::{self, FoundExportCallback},
|
||||
kernel,
|
||||
};
|
||||
use alloc::format;
|
||||
use core::ffi::CStr;
|
||||
use core::ptr;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_query_rwx_support() -> GumRwxSupport {
|
||||
_GumRwxSupport_GUM_RWX_NONE
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_can_remap_writable() -> gboolean {
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_try_remap_writable_pages(
|
||||
first_page: gpointer,
|
||||
n_pages: guint,
|
||||
) -> gpointer {
|
||||
kernel::remap_writable(first_page as u64, n_pages) as gpointer
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_dispose_writable_pages(writable: gpointer, _n_pages: guint) {
|
||||
kernel::unmap_writable(writable as *mut core::ffi::c_void);
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_try_mprotect(
|
||||
address: gpointer,
|
||||
size: gsize,
|
||||
prot: GumPageProtection,
|
||||
) -> gboolean {
|
||||
if kernel::protect(address as u64, size as usize, prot as u32) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_allocate(
|
||||
_address: gpointer,
|
||||
size: gsize,
|
||||
_alignment: gsize,
|
||||
prot: GumPageProtection,
|
||||
) -> gpointer {
|
||||
let ptr = kernel::alloc_code(size as usize);
|
||||
unsafe {
|
||||
core::ptr::write_bytes(ptr, 0, size as usize);
|
||||
if (prot & _GumPageProtection_GUM_PAGE_EXECUTE) != 0 {
|
||||
gum::register_slab(ptr as u64, size as usize);
|
||||
gum_mprotect(ptr as gpointer, size, prot);
|
||||
}
|
||||
}
|
||||
|
||||
ptr as gpointer
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_free(address: gpointer, size: gsize) -> gboolean {
|
||||
// vfree() wants the range writable again: the pages go back to the vmalloc
|
||||
// allocator, and leaving them RX would fault the next consumer.
|
||||
if gum::is_agent_slab(address as u64) {
|
||||
unsafe {
|
||||
gum_mprotect(
|
||||
address,
|
||||
size,
|
||||
(_GumPageProtection_GUM_PAGE_READ | _GumPageProtection_GUM_PAGE_WRITE)
|
||||
as GumPageProtection,
|
||||
);
|
||||
}
|
||||
gum::unregister_slab(address as u64);
|
||||
}
|
||||
kernel::free_code(address as *mut u8, size as usize);
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_barebone_on_registry_activating(registry: *mut GumModuleRegistry) {
|
||||
unsafe {
|
||||
let kernel_base = kernel::get_kernel_base();
|
||||
if kernel_base != 0 {
|
||||
register_module(registry, "/boot/vmlinux", "", kernel_base, kernel::get_kernel_size());
|
||||
}
|
||||
|
||||
for m in kernel::enumerate_modules() {
|
||||
let path = format!("/lib/modules/{}.ko", m.name);
|
||||
register_module(registry, &path, &m.version, m.base, m.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn register_module(
|
||||
registry: *mut GumModuleRegistry,
|
||||
path: &str,
|
||||
version: &str,
|
||||
base: u64,
|
||||
size: u64,
|
||||
) {
|
||||
unsafe {
|
||||
let range = GumMemoryRange {
|
||||
base_address: base,
|
||||
size: size as gsize,
|
||||
};
|
||||
let module = gum::gum_native_module_new(path, version, &range);
|
||||
gum_barebone_register_module(registry, module);
|
||||
g_object_unref(module as gpointer);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn enumerate_exports_in_range(
|
||||
start_address: u64,
|
||||
end_address: u64,
|
||||
callback: &mut FoundExportCallback<'_>,
|
||||
) {
|
||||
kernel::enumerate_symbols(&mut |name, address| {
|
||||
if address < start_address || address >= end_address {
|
||||
return true;
|
||||
}
|
||||
callback(name.as_ptr() as *const gchar, address)
|
||||
});
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_symbol_details_from_address(
|
||||
address: gpointer,
|
||||
details: *mut GumDebugSymbolDetails,
|
||||
) -> gboolean {
|
||||
let Some((name, symbol_address)) = kernel::symbol_from_address(address as u64) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let name_bytes = name.as_bytes();
|
||||
let copy_len = core::cmp::min(name_bytes.len(), (*details).symbol_name.len() - 1);
|
||||
core::ptr::copy_nonoverlapping(
|
||||
name_bytes.as_ptr(),
|
||||
(*details).symbol_name.as_mut_ptr() as *mut u8,
|
||||
copy_len,
|
||||
);
|
||||
(*details).symbol_name[copy_len] = 0;
|
||||
|
||||
(*details).address = symbol_address;
|
||||
|
||||
(*details).module_name[0] = 0;
|
||||
(*details).file_name[0] = 0;
|
||||
(*details).line_number = 0;
|
||||
(*details).column = 0;
|
||||
}
|
||||
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_symbol_name_from_address(address: gpointer) -> *mut gchar {
|
||||
let Some((name, _)) = kernel::symbol_from_address(address as u64) else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
|
||||
let mut name = name;
|
||||
name.push('\0');
|
||||
unsafe { g_strdup(name.as_ptr() as *const gchar) }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_function(name: *const gchar) -> gpointer {
|
||||
let address = kernel::find_function(unsafe { CStr::from_ptr(name) });
|
||||
address as gpointer
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_functions_named(name: *const gchar) -> *mut GArray {
|
||||
let wanted = unsafe { CStr::from_ptr(name) };
|
||||
|
||||
collect_addresses(&mut |candidate, _address| candidate == wanted)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_functions_matching(pattern: *const gchar) -> *mut GArray {
|
||||
unsafe {
|
||||
let spec = g_pattern_spec_new(pattern);
|
||||
let array = collect_addresses(&mut |candidate, _address| {
|
||||
g_pattern_spec_match_string(spec, candidate.as_ptr() as *const gchar) != 0
|
||||
});
|
||||
g_pattern_spec_free(spec);
|
||||
array
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_addresses(predicate: &mut dyn FnMut(&CStr, u64) -> bool) -> *mut GArray {
|
||||
unsafe {
|
||||
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
|
||||
|
||||
kernel::enumerate_symbols(&mut |name, address| {
|
||||
if predicate(name, address) {
|
||||
let addr = address as gpointer;
|
||||
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
|
||||
}
|
||||
true
|
||||
});
|
||||
|
||||
array
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_load_symbols(_path: *const gchar) -> gboolean {
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Win9x half of Gum's platform backend. The processors this kernel runs on have
|
||||
// no execute permission to withhold and no supervisor write protection worth
|
||||
// speaking of, so code is writable where it lies and none of the aliasing the
|
||||
// other backends need arises.
|
||||
|
||||
use crate::{
|
||||
bindings::{
|
||||
_GumPageProtection_GUM_PAGE_EXECUTE, _GumRwxSupport_GUM_RWX_FULL, GumMemoryRange,
|
||||
GumModuleRegistry, GumPageProtection, GumRwxSupport, g_object_unref, gboolean, gpointer,
|
||||
gsize, guint, gum_barebone_register_module, gum_mprotect,
|
||||
},
|
||||
gum::{self, FoundExportCallback},
|
||||
kernel,
|
||||
};
|
||||
use core::ptr;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_query_rwx_support() -> GumRwxSupport {
|
||||
_GumRwxSupport_GUM_RWX_FULL
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_can_remap_writable() -> gboolean {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_try_remap_writable_pages(
|
||||
first_page: gpointer,
|
||||
_n_pages: guint,
|
||||
) -> gpointer {
|
||||
first_page
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_dispose_writable_pages(_writable: gpointer, _n_pages: guint) {}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_try_mprotect(
|
||||
address: gpointer,
|
||||
size: gsize,
|
||||
prot: GumPageProtection,
|
||||
) -> gboolean {
|
||||
if kernel::protect(address as u64, size as usize, prot as u32) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_allocate(
|
||||
_address: gpointer,
|
||||
size: gsize,
|
||||
_alignment: gsize,
|
||||
prot: GumPageProtection,
|
||||
) -> gpointer {
|
||||
let ptr = kernel::alloc_code(size as usize);
|
||||
if ptr.is_null() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
unsafe {
|
||||
core::ptr::write_bytes(ptr, 0, size as usize);
|
||||
if (prot & _GumPageProtection_GUM_PAGE_EXECUTE) != 0 {
|
||||
gum::register_slab(ptr as u64, size as usize);
|
||||
gum_mprotect(ptr as gpointer, size, prot);
|
||||
}
|
||||
}
|
||||
|
||||
ptr as gpointer
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_free(address: gpointer, size: gsize) -> gboolean {
|
||||
if gum::is_agent_slab(address as u64) {
|
||||
gum::unregister_slab(address as u64);
|
||||
}
|
||||
kernel::free_code(address as *mut u8, size as usize);
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_barebone_on_registry_activating(registry: *mut GumModuleRegistry) {
|
||||
let kernel_base = kernel::get_kernel_base();
|
||||
if kernel_base == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let range = GumMemoryRange {
|
||||
base_address: kernel_base,
|
||||
size: 0,
|
||||
};
|
||||
let module = gum::gum_native_module_new("/VMM32.VXD", "", &range);
|
||||
gum_barebone_register_module(registry, module);
|
||||
g_object_unref(module as gpointer);
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing here has a symbol table to walk: the loader resolves VxD services
|
||||
// through a dispatch table rather than by name.
|
||||
pub(crate) unsafe fn enumerate_exports_in_range(
|
||||
_start: u64,
|
||||
_end: u64,
|
||||
_callback: &mut FoundExportCallback,
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
// XNU half of Gum's platform backend. Memory permissions and kernel-text writes
|
||||
// go out over the hostlink, because the guest CPU cannot perform them itself:
|
||||
// the host owns the page tables and the physical-memory bridge.
|
||||
|
||||
use crate::{
|
||||
FridaCommand,
|
||||
bindings::{
|
||||
_GumPageProtection_GUM_PAGE_EXECUTE, _GumPageProtection_GUM_PAGE_READ,
|
||||
_GumPageProtection_GUM_PAGE_WRITE, _GumRwxSupport_GUM_RWX_NONE, GArray,
|
||||
GumDebugSymbolDetails, GumMemoryRange, GumModuleRegistry, GumPageProtection, GumRwxSupport,
|
||||
g_array_append_vals, g_array_new, g_object_unref, g_strdup, g_variant_get_boolean,
|
||||
g_variant_get_uint64, g_variant_new, g_variant_new_fixed_array, g_variant_type_free,
|
||||
g_variant_type_new, g_variant_unref, gboolean, gchar, gconstpointer, gpointer, gsize, guint,
|
||||
gum_barebone_register_module,
|
||||
gum_barebone_try_remap_writable_pages as _gum_barebone_try_remap_writable_pages,
|
||||
gum_mprotect, gum_query_page_size,
|
||||
},
|
||||
gum::{self, FoundExportCallback},
|
||||
host_rpc, kernel, libc,
|
||||
};
|
||||
use alloc::format;
|
||||
use alloc::vec::Vec;
|
||||
use core::ffi::CStr;
|
||||
use core::mem::size_of;
|
||||
use core::ptr;
|
||||
|
||||
const SHADOW_MAGIC: u64 = 0x4644_4f48_5341_4853;
|
||||
const SHADOW_HEADER: usize = 24;
|
||||
const SHADOW_MIN_ADDRESS: u64 = 0xffff_f000_0000_0000;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_query_rwx_support() -> GumRwxSupport {
|
||||
_GumRwxSupport_GUM_RWX_NONE
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_can_remap_writable() -> gboolean {
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_try_remap_writable_pages(
|
||||
first_page: gpointer,
|
||||
n_pages: guint,
|
||||
) -> gpointer {
|
||||
if gum::is_agent_slab(first_page as u64) {
|
||||
return remap_agent_pages(first_page, n_pages);
|
||||
}
|
||||
shadow_kernel_pages(first_page, n_pages)
|
||||
}
|
||||
|
||||
fn remap_agent_pages(first_page: gpointer, n_pages: guint) -> gpointer {
|
||||
unsafe {
|
||||
let page_size = gum_query_page_size() as usize;
|
||||
let mut virtual_addrs = Vec::with_capacity(n_pages as usize);
|
||||
|
||||
let mut current_page = first_page as u64;
|
||||
for _ in 0..n_pages {
|
||||
virtual_addrs.push(current_page as gpointer);
|
||||
current_page += page_size as u64;
|
||||
}
|
||||
|
||||
_gum_barebone_try_remap_writable_pages(
|
||||
virtual_addrs.as_ptr() as *mut *const core::ffi::c_void,
|
||||
virtual_addrs.len() as guint,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn shadow_kernel_pages(first_page: gpointer, n_pages: guint) -> gpointer {
|
||||
unsafe {
|
||||
let total = n_pages as usize * gum_query_page_size() as usize;
|
||||
let buffer = kernel::alloc(SHADOW_HEADER + total);
|
||||
*(buffer as *mut u64) = SHADOW_MAGIC;
|
||||
*(buffer.add(8) as *mut u64) = first_page as u64;
|
||||
*(buffer.add(16) as *mut u32) = n_pages;
|
||||
|
||||
let body = buffer.add(SHADOW_HEADER);
|
||||
core::ptr::copy_nonoverlapping(first_page as *const u8, body, total);
|
||||
body as gpointer
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_dispose_writable_pages(writable: gpointer, _n_pages: guint) {
|
||||
if (writable as u64) < SHADOW_MIN_ADDRESS {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
let buffer = (writable as *mut u8).sub(SHADOW_HEADER);
|
||||
if *(buffer as *const u64) != SHADOW_MAGIC {
|
||||
return;
|
||||
}
|
||||
let first_page = *(buffer.add(8) as *const u64);
|
||||
let n_pages = *(buffer.add(16) as *const u32);
|
||||
let total = n_pages as usize * gum_query_page_size() as usize;
|
||||
|
||||
commit_kernel_patch(first_page, writable as *const u8, total);
|
||||
libc::__clear_cache(first_page as *const u8, (first_page + total as u64) as *const u8);
|
||||
|
||||
kernel::free(buffer, SHADOW_HEADER + total);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn commit_kernel_patch(address: u64, data: *const u8, len: usize) {
|
||||
unsafe {
|
||||
let element_type = g_variant_type_new(c"y".as_ptr());
|
||||
let bytes = g_variant_new_fixed_array(element_type, data as gconstpointer, len as gsize, 1);
|
||||
g_variant_type_free(element_type);
|
||||
|
||||
let payload = g_variant_new(c"(t@ay)".as_ptr(), address, bytes);
|
||||
let reply = host_rpc(FridaCommand::PatchCode, payload);
|
||||
g_variant_unref(reply);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_barebone_try_remap_writable_pages(
|
||||
addrs: *const gpointer,
|
||||
n_addrs: guint,
|
||||
) -> gpointer {
|
||||
if !crate::transport_is_up() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
unsafe {
|
||||
let element_type = g_variant_type_new(c"t".as_ptr());
|
||||
let payload = g_variant_new_fixed_array(
|
||||
element_type,
|
||||
addrs as gconstpointer,
|
||||
n_addrs as gsize,
|
||||
size_of::<u64>() as gsize,
|
||||
);
|
||||
g_variant_type_free(element_type);
|
||||
|
||||
let reply = host_rpc(FridaCommand::RemapWritablePages, payload);
|
||||
let virtual_address = g_variant_get_uint64(reply);
|
||||
g_variant_unref(reply);
|
||||
|
||||
virtual_address as gpointer
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_try_mprotect(
|
||||
address: gpointer,
|
||||
size: gsize,
|
||||
prot: GumPageProtection,
|
||||
) -> gboolean {
|
||||
if !crate::transport_is_up() {
|
||||
return 1;
|
||||
}
|
||||
unsafe {
|
||||
let payload = g_variant_new(c"(ttu)".as_ptr(), address as u64, size as u64, prot as u32);
|
||||
|
||||
let reply = host_rpc(FridaCommand::MemoryProtect, payload);
|
||||
let success = g_variant_get_boolean(reply);
|
||||
g_variant_unref(reply);
|
||||
|
||||
if success != 0 {
|
||||
flush_tlb_range(address as u64, size as u64);
|
||||
}
|
||||
|
||||
success
|
||||
}
|
||||
}
|
||||
|
||||
// The host rewrites our page-table descriptors through the physical-memory
|
||||
// bridge, which leaves this CPU's TLB holding the stale translation. Stalker
|
||||
// flips a slab page RW then RX in place, so without this the freeze never takes
|
||||
// effect and executing the page faults with a permission abort.
|
||||
unsafe fn flush_tlb_range(address: u64, size: u64) {
|
||||
unsafe {
|
||||
let page_size = gum_query_page_size() as u64;
|
||||
let start = address & !(page_size - 1);
|
||||
let end = (address + size + page_size - 1) & !(page_size - 1);
|
||||
|
||||
core::arch::asm!("dsb ish", options(nostack, preserves_flags));
|
||||
let mut va = start;
|
||||
while va < end {
|
||||
core::arch::asm!("tlbi vaae1is, {operand}", operand = in(reg) va >> 12,
|
||||
options(nostack, preserves_flags));
|
||||
va += page_size;
|
||||
}
|
||||
core::arch::asm!("dsb ish", "isb", options(nostack, preserves_flags));
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_allocate(
|
||||
_address: gpointer,
|
||||
size: gsize,
|
||||
_alignment: gsize,
|
||||
prot: GumPageProtection,
|
||||
) -> gpointer {
|
||||
let ptr = kernel::alloc_code(size as usize);
|
||||
unsafe {
|
||||
core::ptr::write_bytes(ptr, 0, size as usize);
|
||||
if (prot & _GumPageProtection_GUM_PAGE_EXECUTE) != 0 {
|
||||
gum::register_slab(ptr as u64, size as usize);
|
||||
gum_mprotect(ptr as gpointer, size, prot);
|
||||
}
|
||||
}
|
||||
ptr as gpointer
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_free(address: gpointer, size: gsize) -> gboolean {
|
||||
// Executable slabs were flipped to RX in the page tables; restore RW before returning them to
|
||||
// the allocator, otherwise the reclaimed pages stay non-writable and the next consumer faults.
|
||||
if gum::is_agent_slab(address as u64) {
|
||||
unsafe {
|
||||
gum_mprotect(
|
||||
address,
|
||||
size,
|
||||
(_GumPageProtection_GUM_PAGE_READ | _GumPageProtection_GUM_PAGE_WRITE)
|
||||
as GumPageProtection,
|
||||
);
|
||||
}
|
||||
gum::unregister_slab(address as u64);
|
||||
}
|
||||
kernel::free_code(address as *mut u8, size as usize);
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_barebone_on_registry_activating(registry: *mut GumModuleRegistry) {
|
||||
let kernel_base = kernel::get_kernel_base();
|
||||
|
||||
unsafe {
|
||||
let module_infos = core::ptr::addr_of!(crate::MODULE_INFO);
|
||||
let module_infos = &*module_infos;
|
||||
|
||||
let mut i = 0;
|
||||
for module_info in module_infos.iter() {
|
||||
let module_base = kernel_base + module_info.offset as u64;
|
||||
|
||||
let module_path = if i == 0 {
|
||||
"/System/Library/Kernels/kernel"
|
||||
} else {
|
||||
&format!(
|
||||
"/System/Library/Extensions/{}.kext/{}",
|
||||
module_info.name, module_info.name
|
||||
)
|
||||
};
|
||||
let module_range = GumMemoryRange {
|
||||
base_address: module_base,
|
||||
size: module_info.size as u64,
|
||||
};
|
||||
|
||||
let module = gum::gum_native_module_new(&module_path, &module_info.version, &module_range);
|
||||
gum_barebone_register_module(registry, module);
|
||||
g_object_unref(module as gpointer);
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn enumerate_exports_in_range(
|
||||
start_address: u64,
|
||||
end_address: u64,
|
||||
callback: &mut FoundExportCallback<'_>,
|
||||
) {
|
||||
unsafe {
|
||||
let symbol_table = core::ptr::addr_of!(crate::SYMBOL_TABLE);
|
||||
let symbol_table = &*symbol_table;
|
||||
|
||||
const N_EXT: u8 = 0x01; // External symbol flag
|
||||
const N_TYPE: u8 = 0x0e; // Type mask
|
||||
const N_SECT: u8 = 0x0e; // Defined in section
|
||||
|
||||
for symbol_ref in symbol_table.iter_symbols_in_range(start_address, end_address) {
|
||||
let is_external = (symbol_ref.symbol_type() & N_EXT) != 0;
|
||||
let is_defined = (symbol_ref.symbol_type() & N_TYPE) == N_SECT;
|
||||
if !is_external || !is_defined {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !callback(symbol_ref.name_ptr(), symbol_ref.address()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_symbol_details_from_address(
|
||||
address: gpointer,
|
||||
details: *mut GumDebugSymbolDetails,
|
||||
) -> gboolean {
|
||||
unsafe {
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
if let Some(symbol) = table.find_symbol_by_address(address as u64) {
|
||||
let name_bytes = symbol.name().as_bytes();
|
||||
let copy_len = core::cmp::min(name_bytes.len(), 2048);
|
||||
core::ptr::copy_nonoverlapping(
|
||||
name_bytes.as_ptr(),
|
||||
(*details).symbol_name.as_mut_ptr() as *mut u8,
|
||||
copy_len,
|
||||
);
|
||||
(*details).symbol_name[copy_len] = 0;
|
||||
|
||||
(*details).address = symbol.address();
|
||||
|
||||
(*details).module_name[0] = 0;
|
||||
(*details).file_name[0] = 0;
|
||||
(*details).line_number = 0;
|
||||
(*details).column = 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_symbol_name_from_address(address: gpointer) -> *mut gchar {
|
||||
unsafe {
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
let name_ptr = table.find_symbol_name_ptr_by_address(address as u64);
|
||||
g_strdup(name_ptr as *const gchar)
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_function(name: *const gchar) -> gpointer {
|
||||
unsafe {
|
||||
let target_name = CStr::from_ptr(name).to_string_lossy();
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
|
||||
if let Some(symbol) = table.find_symbol_by_name(&target_name) {
|
||||
return symbol.address() as gpointer;
|
||||
}
|
||||
|
||||
core::ptr::null_mut()
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_functions_named(name: *const gchar) -> *mut GArray {
|
||||
unsafe {
|
||||
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
|
||||
|
||||
let target_name = CStr::from_ptr(name).to_string_lossy();
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
|
||||
let symbols = table.find_symbols_by_name(&target_name);
|
||||
for symbol in symbols {
|
||||
let addr = symbol.address() as gpointer;
|
||||
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
|
||||
}
|
||||
|
||||
array
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_functions_matching(pattern: *const gchar) -> *mut GArray {
|
||||
unsafe {
|
||||
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
|
||||
|
||||
let glob_pattern = CStr::from_ptr(pattern).to_string_lossy();
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
|
||||
let symbols = table.find_symbols_matching_glob(&glob_pattern);
|
||||
for symbol in symbols {
|
||||
let addr = symbol.address() as gpointer;
|
||||
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
|
||||
}
|
||||
|
||||
array
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_load_symbols(_path: *const gchar) -> gboolean {
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Linux transport: /dev/frida, framed the same way as the XNU hostlinks (4-byte
|
||||
// little-endian length prefix) so the host-side (yqv) protocol is identical across
|
||||
// every transport.
|
||||
//
|
||||
// The character device itself lives in linux/frida-kmod.c and hands us a byte
|
||||
// stream; whoever opens the device decides how it reaches the host.
|
||||
|
||||
use core::cell::UnsafeCell;
|
||||
use core::ffi::{c_int, c_void};
|
||||
|
||||
use alloc::vec::Vec;
|
||||
|
||||
struct Inner {
|
||||
rx_lenbuf: [u8; 4],
|
||||
rx_lenhave: usize,
|
||||
rx_buf: Vec<u8>,
|
||||
rx_have: usize,
|
||||
rx_need: usize,
|
||||
}
|
||||
|
||||
pub struct Hostlink {
|
||||
state: UnsafeCell<Inner>,
|
||||
on_rx: Option<fn(&[u8])>,
|
||||
}
|
||||
|
||||
unsafe impl Send for Hostlink {}
|
||||
|
||||
impl Hostlink {
|
||||
pub fn init(on_rx: Option<fn(&[u8])>) -> Result<Self, ()> {
|
||||
if unsafe { frida_kmod_link_open() } != 0 {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok(Hostlink {
|
||||
state: UnsafeCell::new(Inner {
|
||||
rx_lenbuf: [0; 4],
|
||||
rx_lenhave: 0,
|
||||
rx_buf: Vec::new(),
|
||||
rx_have: 0,
|
||||
rx_need: 0,
|
||||
}),
|
||||
on_rx,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send(&self, payload: &[u8]) {
|
||||
let header = (payload.len() as u32).to_le_bytes();
|
||||
unsafe {
|
||||
frida_kmod_link_send(header.as_ptr() as *const c_void, header.len());
|
||||
frida_kmod_link_send(payload.as_ptr() as *const c_void, payload.len());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process(&self) {
|
||||
let s = unsafe { &mut *self.state.get() };
|
||||
loop {
|
||||
if s.rx_lenhave < 4 {
|
||||
let lo = s.rx_lenhave;
|
||||
let n = recv_nonblocking(&mut s.rx_lenbuf[lo..4]);
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
s.rx_lenhave += n;
|
||||
if s.rx_lenhave < 4 {
|
||||
continue;
|
||||
}
|
||||
s.rx_need = u32::from_le_bytes(s.rx_lenbuf) as usize;
|
||||
s.rx_have = 0;
|
||||
s.rx_buf.resize(s.rx_need, 0);
|
||||
}
|
||||
|
||||
while s.rx_have < s.rx_need {
|
||||
let lo = s.rx_have;
|
||||
let hi = s.rx_need;
|
||||
let n = recv_nonblocking(&mut s.rx_buf[lo..hi]);
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
s.rx_have += n;
|
||||
}
|
||||
|
||||
// Detach the frame and reset receive state before dispatching: the callback may
|
||||
// re-enter process() (a synchronous host RPC issued while handling a command), and
|
||||
// it must start from a clean state instead of re-dispatching this same frame.
|
||||
let frame = core::mem::take(&mut s.rx_buf);
|
||||
let need = s.rx_need;
|
||||
s.rx_lenhave = 0;
|
||||
s.rx_have = 0;
|
||||
s.rx_need = 0;
|
||||
|
||||
if let Some(cb) = self.on_rx {
|
||||
cb(&frame[..need]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Hostlink {
|
||||
fn drop(&mut self) {
|
||||
unsafe { frida_kmod_link_close() };
|
||||
}
|
||||
}
|
||||
|
||||
fn recv_nonblocking(dst: &mut [u8]) -> usize {
|
||||
let n = unsafe { frida_kmod_link_recv(dst.as_mut_ptr() as *mut c_void, dst.len()) };
|
||||
if n <= 0 { 0 } else { n as usize }
|
||||
}
|
||||
|
||||
unsafe extern "C" {
|
||||
fn frida_kmod_link_open() -> c_int;
|
||||
fn frida_kmod_link_close();
|
||||
fn frida_kmod_link_send(data: *const c_void, size: usize) -> c_int;
|
||||
fn frida_kmod_link_recv(data: *mut c_void, size: usize) -> isize;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use core::ptr::{read_volatile, write_volatile};
|
||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crate::gum::gum_barebone_query_page_size;
|
||||
use crate::xnu;
|
||||
use crate::kernel;
|
||||
|
||||
const MMIO_SIZE: u64 = 0x200;
|
||||
|
||||
@@ -51,6 +51,46 @@ const F_MULTIPORT: u64 = 1u64 << 1;
|
||||
|
||||
const INT_VRING: u32 = 1;
|
||||
|
||||
const PCI_CONFIG_ADDRESS: u16 = 0xcf8;
|
||||
const PCI_CONFIG_DATA: u16 = 0xcfc;
|
||||
|
||||
const PCI_VENDOR_VIRTIO: u16 = 0x1af4;
|
||||
const PCI_DEVICE_CONSOLE_LEGACY: u16 = 0x1003;
|
||||
const PCI_DEVICE_CONSOLE_MODERN: u16 = 0x1043;
|
||||
|
||||
const PCI_COMMAND: u8 = 0x04;
|
||||
const PCI_CAP_LIST_POINTER: u8 = 0x34;
|
||||
const PCI_INTERRUPT_LINE: u8 = 0x3c;
|
||||
const PCI_BASE_ADDRESS_0: u8 = 0x10;
|
||||
|
||||
const PCI_COMMAND_MEMORY: u32 = 1 << 1;
|
||||
const PCI_COMMAND_BUS_MASTER: u32 = 1 << 2;
|
||||
|
||||
const PCI_CAP_ID_VENDOR: u8 = 0x09;
|
||||
|
||||
const VIRTIO_CAP_CFG_TYPE: u8 = 3;
|
||||
const VIRTIO_CAP_BAR: u8 = 4;
|
||||
const VIRTIO_CAP_OFFSET: u8 = 8;
|
||||
const VIRTIO_CAP_LENGTH: u8 = 12;
|
||||
const VIRTIO_CAP_NOTIFY_MULTIPLIER: u8 = 16;
|
||||
|
||||
const CFG_TYPE_COMMON: u8 = 1;
|
||||
const CFG_TYPE_NOTIFY: u8 = 2;
|
||||
const CFG_TYPE_ISR: u8 = 3;
|
||||
|
||||
const COMMON_DEVFEAT_SEL: usize = 0x00;
|
||||
const COMMON_DEVFEAT: usize = 0x04;
|
||||
const COMMON_DRVFEAT_SEL: usize = 0x08;
|
||||
const COMMON_DRVFEAT: usize = 0x0c;
|
||||
const COMMON_STATUS: usize = 0x14;
|
||||
const COMMON_QSEL: usize = 0x16;
|
||||
const COMMON_QNUM: usize = 0x18;
|
||||
const COMMON_QREADY: usize = 0x1c;
|
||||
const COMMON_QNOTIFY_OFF: usize = 0x1e;
|
||||
const COMMON_QDESC: usize = 0x20;
|
||||
const COMMON_QAVAIL: usize = 0x28;
|
||||
const COMMON_QUSED: usize = 0x30;
|
||||
|
||||
const Q_RX0: u16 = 0;
|
||||
const Q_TX0: u16 = 1;
|
||||
const Q_CTRL_RX: u16 = 2;
|
||||
@@ -107,18 +147,19 @@ struct DmaPage {
|
||||
|
||||
fn dma_page_alloc() -> DmaPage {
|
||||
let len = PAGE_SIZE.load(Ordering::Relaxed);
|
||||
let va = xnu::kalloc(len);
|
||||
let pa = xnu::ml_vtophys(va as u64);
|
||||
let va = kernel::alloc_code(len);
|
||||
let pa = kernel::virt_to_phys(va as u64);
|
||||
DmaPage { va, pa }
|
||||
}
|
||||
|
||||
fn dma_page_free(p: DmaPage) {
|
||||
let len = PAGE_SIZE.load(Ordering::Relaxed);
|
||||
xnu::free(p.va, len);
|
||||
kernel::free_code(p.va, len);
|
||||
}
|
||||
|
||||
struct Vq {
|
||||
sel: u16,
|
||||
notify_off: u16,
|
||||
size: u16,
|
||||
desc_va: *mut u8,
|
||||
avail_va: *mut u8,
|
||||
@@ -129,12 +170,7 @@ struct Vq {
|
||||
free_cnt: u16,
|
||||
}
|
||||
impl Vq {
|
||||
fn new(mmio: *mut u8, sel: u16, size: u16) -> Self {
|
||||
w32(mmio, QSEL, sel as u32);
|
||||
let max = r32(mmio, QNUM_MAX) as u16;
|
||||
debug_assert!(max >= size && max != 0);
|
||||
w32(mmio, QNUM, size as u32);
|
||||
|
||||
fn new(regs: &Regs, sel: u16, size: u16) -> Self {
|
||||
let d = dma_page_alloc();
|
||||
let a = dma_page_alloc();
|
||||
let u = dma_page_alloc();
|
||||
@@ -146,10 +182,6 @@ impl Vq {
|
||||
core::ptr::write_bytes(u.va, 0, ps);
|
||||
}
|
||||
|
||||
w64(mmio, QDESC_LO, QDESC_HI, d.pa);
|
||||
w64(mmio, QAVAIL_LO, QAVAIL_HI, a.pa);
|
||||
w64(mmio, QUSED_LO, QUSED_HI, u.pa);
|
||||
|
||||
let dp = d.va as *mut Desc;
|
||||
for i in 0..size {
|
||||
unsafe {
|
||||
@@ -158,10 +190,11 @@ impl Vq {
|
||||
}
|
||||
}
|
||||
|
||||
w32(mmio, QREADY, 1);
|
||||
let notify_off = regs.queue_prepare(sel, size, d.pa, a.pa, u.pa);
|
||||
|
||||
Self {
|
||||
sel,
|
||||
notify_off,
|
||||
size,
|
||||
desc_va: d.va,
|
||||
avail_va: a.va,
|
||||
@@ -230,7 +263,7 @@ impl Vq {
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
mmio: *mut u8,
|
||||
regs: Regs,
|
||||
|
||||
ctrl_rx: Vq,
|
||||
ctrl_tx: Vq,
|
||||
@@ -271,27 +304,46 @@ unsafe impl Send for Hostlink {}
|
||||
|
||||
impl Hostlink {
|
||||
pub fn init(mmio_base: u64, irq_line: u32, on_rx: Option<fn(&[u8])>, wake_token: *const u8) -> Result<Self, ()> {
|
||||
let page_size = gum_barebone_query_page_size();
|
||||
PAGE_SIZE.store(page_size as usize, Ordering::Relaxed);
|
||||
|
||||
let mmio = xnu::ml_io_map(mmio_base, MMIO_SIZE) as *mut u8;
|
||||
let mmio = kernel::map_io(mmio_base, MMIO_SIZE) as *mut u8;
|
||||
if mmio.is_null() {
|
||||
return Err(());
|
||||
}
|
||||
let regs = Regs::Mmio(mmio);
|
||||
|
||||
w32(mmio, STATUS, 0);
|
||||
w32(mmio, STATUS, ST_ACK | ST_DRV);
|
||||
regs.reset();
|
||||
|
||||
let magic_ok = r32(mmio, MAGIC) == 0x7472_6976;
|
||||
let version_ok = r32(mmio, VERSION) == 2;
|
||||
let device_ok = r32(mmio, DEVICE) == DEV_ID_CONSOLE;
|
||||
if !(magic_ok && version_ok && device_ok) {
|
||||
w32(mmio, STATUS, ST_FAILED);
|
||||
regs.set_status(ST_FAILED);
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let dev_lo = feat_get(mmio, 0) as u64;
|
||||
let dev_hi = (feat_get(mmio, 1) as u64) << 32;
|
||||
Self::start(regs, irq_line, on_rx, wake_token)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86")]
|
||||
pub fn init_pci(on_rx: Option<fn(&[u8])>, wake_token: *const u8) -> Result<Self, ()> {
|
||||
let Some(device) = PciDevice::find_console() else {
|
||||
return Err(());
|
||||
};
|
||||
|
||||
let Some(regs) = device.map_virtio_regs() else {
|
||||
return Err(());
|
||||
};
|
||||
|
||||
regs.reset();
|
||||
|
||||
Self::start(regs, device.irq_line(), on_rx, wake_token)
|
||||
}
|
||||
|
||||
fn start(regs: Regs, irq_line: u32, on_rx: Option<fn(&[u8])>, wake_token: *const u8) -> Result<Self, ()> {
|
||||
let page_size = gum_barebone_query_page_size();
|
||||
PAGE_SIZE.store(page_size as usize, Ordering::Relaxed);
|
||||
|
||||
let dev_lo = regs.feat_get(0) as u64;
|
||||
let dev_hi = (regs.feat_get(1) as u64) << 32;
|
||||
let mut drv: u64 = 0;
|
||||
if (dev_hi & F_VERSION_1) != 0 {
|
||||
drv |= F_VERSION_1;
|
||||
@@ -299,21 +351,24 @@ impl Hostlink {
|
||||
if (dev_lo & F_MULTIPORT) != 0 {
|
||||
drv |= F_MULTIPORT;
|
||||
}
|
||||
feat_set(mmio, 0, (drv & 0xffff_ffff) as u32);
|
||||
feat_set(mmio, 1, (drv >> 32) as u32);
|
||||
w32(mmio, STATUS, r32(mmio, STATUS) | ST_FEAT_OK);
|
||||
let feats_ok = (r32(mmio, STATUS) & ST_FEAT_OK) != 0;
|
||||
regs.feat_set(0, (drv & 0xffff_ffff) as u32);
|
||||
regs.feat_set(1, (drv >> 32) as u32);
|
||||
regs.set_status(regs.status() | ST_FEAT_OK);
|
||||
let feats_ok = (regs.status() & ST_FEAT_OK) != 0;
|
||||
if !feats_ok {
|
||||
w32(mmio, STATUS, ST_FAILED);
|
||||
regs.set_status(ST_FAILED);
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let ctrl_rx = Vq::new(mmio, Q_CTRL_RX, QSZ);
|
||||
let ctrl_tx = Vq::new(mmio, Q_CTRL_TX, QSZ);
|
||||
let ctrl_rx = Vq::new(®s, Q_CTRL_RX, QSZ);
|
||||
let ctrl_tx = Vq::new(®s, Q_CTRL_TX, QSZ);
|
||||
|
||||
w32(mmio, STATUS, r32(mmio, STATUS) | ST_DRV_OK);
|
||||
regs.set_status(regs.status() | ST_DRV_OK);
|
||||
|
||||
xnu::install_interrupt_handler(
|
||||
unsafe {
|
||||
ISR_REGS = Some(regs);
|
||||
}
|
||||
kernel::install_interrupt_handler(
|
||||
irq_line,
|
||||
wake_token as *mut c_void,
|
||||
isr_wake,
|
||||
@@ -321,7 +376,7 @@ impl Hostlink {
|
||||
);
|
||||
|
||||
let inner = Inner {
|
||||
mmio,
|
||||
regs,
|
||||
ctrl_rx,
|
||||
ctrl_tx,
|
||||
port_id: None,
|
||||
@@ -359,7 +414,7 @@ impl Hostlink {
|
||||
let s = unsafe { &mut *self.state.get() };
|
||||
|
||||
let total = 4 + payload.len();
|
||||
let buf = xnu::kalloc(total);
|
||||
let buf = kernel::alloc(total);
|
||||
unsafe {
|
||||
let len = payload.len() as u32;
|
||||
*buf.add(0) = (len & 0xFF) as u8;
|
||||
@@ -370,7 +425,7 @@ impl Hostlink {
|
||||
}
|
||||
let frame: &'static [u8] = unsafe { core::slice::from_raw_parts(buf, total) };
|
||||
|
||||
let node = xnu::kalloc(size_of::<TxNode>()) as *mut TxNode;
|
||||
let node = kernel::alloc(size_of::<TxNode>()) as *mut TxNode;
|
||||
unsafe {
|
||||
(*node).next = core::ptr::null_mut();
|
||||
(*node).frame = frame;
|
||||
@@ -386,15 +441,13 @@ impl Hostlink {
|
||||
s.tx_tail = node;
|
||||
}
|
||||
|
||||
xnu::thread_wakeup(s.wake_token);
|
||||
kernel::wake(s.wake_token);
|
||||
}
|
||||
|
||||
pub fn process(&self) {
|
||||
let s = unsafe { &mut *self.state.get() };
|
||||
|
||||
if (r32(s.mmio, ISR) & INT_VRING) != 0 {
|
||||
w32(s.mmio, ISR_ACK, INT_VRING);
|
||||
}
|
||||
s.regs.isr_ack();
|
||||
|
||||
self.ctrl_complete();
|
||||
self.ctrl_prime_rx(QSZ as usize);
|
||||
@@ -425,8 +478,8 @@ impl Hostlink {
|
||||
s.ctrl_rx.push_avail(h);
|
||||
posted += 1;
|
||||
}
|
||||
let sel = s.ctrl_rx.sel;
|
||||
self.kick(sel);
|
||||
let (sel, notify_off) = (s.ctrl_rx.sel, s.ctrl_rx.notify_off);
|
||||
self.kick(sel, notify_off);
|
||||
}
|
||||
|
||||
fn ctrl_send(&self, msg: VConsCtrl) {
|
||||
@@ -445,8 +498,8 @@ impl Hostlink {
|
||||
}
|
||||
s.tx_pages[h as usize] = Some(pg);
|
||||
s.ctrl_tx.push_avail(h);
|
||||
let sel = s.ctrl_tx.sel;
|
||||
self.kick(sel);
|
||||
let (sel, notify_off) = (s.ctrl_tx.sel, s.ctrl_tx.notify_off);
|
||||
self.kick(sel, notify_off);
|
||||
}
|
||||
|
||||
fn ctrl_complete(&self) {
|
||||
@@ -488,8 +541,8 @@ impl Hostlink {
|
||||
s.ctrl_rx.push_avail(h);
|
||||
}
|
||||
}
|
||||
let sel = s.ctrl_rx.sel;
|
||||
self.kick(sel);
|
||||
let (sel, notify_off) = (s.ctrl_rx.sel, s.ctrl_rx.notify_off);
|
||||
self.kick(sel, notify_off);
|
||||
|
||||
while let Some(u) = s.ctrl_tx.pop_used() {
|
||||
let head = u.id as u16;
|
||||
@@ -511,8 +564,8 @@ impl Hostlink {
|
||||
let base = 4 + ((id as u16 - 1) * 2);
|
||||
(base, base + 1)
|
||||
};
|
||||
let rx = Vq::new(s.mmio, rx_i, QSZ);
|
||||
let tx = Vq::new(s.mmio, tx_i, QSZ);
|
||||
let rx = Vq::new(&s.regs, rx_i, QSZ);
|
||||
let tx = Vq::new(&s.regs, tx_i, QSZ);
|
||||
s.rx = Some(rx);
|
||||
s.tx = Some(tx);
|
||||
s.port_id = Some(id);
|
||||
@@ -537,8 +590,8 @@ impl Hostlink {
|
||||
s.data_rx_pages[h as usize] = Some(pg);
|
||||
rxq.push_avail(h);
|
||||
}
|
||||
let sel = rxq.sel;
|
||||
self.kick(sel);
|
||||
let (sel, notify_off) = (rxq.sel, rxq.notify_off);
|
||||
self.kick(sel, notify_off);
|
||||
}
|
||||
|
||||
fn data_rx_complete(&self) {
|
||||
@@ -569,8 +622,8 @@ impl Hostlink {
|
||||
rxq.push_avail(h);
|
||||
}
|
||||
}
|
||||
let sel = rxq.sel;
|
||||
self.kick(sel);
|
||||
let (sel, notify_off) = (rxq.sel, rxq.notify_off);
|
||||
self.kick(sel, notify_off);
|
||||
}
|
||||
|
||||
fn feed_rx_stream(&self, mut chunk: &[u8]) {
|
||||
@@ -592,7 +645,7 @@ impl Hostlink {
|
||||
| ((s.rx_lenbuf[3] as usize) << 24);
|
||||
|
||||
if s.rx_buf.is_none() && len > 0 {
|
||||
let buf = xnu::kalloc(len);
|
||||
let buf = kernel::alloc(len);
|
||||
let slice: &'static mut [u8] =
|
||||
unsafe { core::slice::from_raw_parts_mut(buf, len) };
|
||||
s.rx_buf = Some(slice);
|
||||
@@ -713,26 +766,278 @@ impl Hostlink {
|
||||
}
|
||||
|
||||
if let Some(h) = head {
|
||||
let sel = txq.sel;
|
||||
let (sel, notify_off) = (txq.sel, txq.notify_off);
|
||||
txq.push_avail(h);
|
||||
self.kick(sel);
|
||||
self.kick(sel, notify_off);
|
||||
}
|
||||
|
||||
xnu::free(frame.as_ptr() as *mut u8, frame.len());
|
||||
xnu::free(node as *mut u8, core::mem::size_of::<TxNode>());
|
||||
kernel::free(frame.as_ptr() as *mut u8, frame.len());
|
||||
kernel::free(node as *mut u8, core::mem::size_of::<TxNode>());
|
||||
}
|
||||
}
|
||||
|
||||
fn kick(&self, sel: u16) {
|
||||
fn kick(&self, sel: u16, notify_off: u16) {
|
||||
let s = unsafe { &*self.state.get() };
|
||||
wmb();
|
||||
w32(s.mmio, QSEL, sel as u32);
|
||||
w32(s.mmio, QNOTIFY, sel as u32);
|
||||
s.regs.notify(sel, notify_off);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn isr_wake(token: *mut c_void, _refcon: *mut c_void, _nub: *mut c_void, _src: i32) {
|
||||
xnu::thread_wakeup(token as *const u8);
|
||||
unsafe {
|
||||
if let Some(regs) = ISR_REGS {
|
||||
regs.isr_ack();
|
||||
}
|
||||
}
|
||||
kernel::wake(token as *const u8);
|
||||
}
|
||||
|
||||
static mut ISR_REGS: Option<Regs> = None;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Regs {
|
||||
Mmio(*mut u8),
|
||||
Pci(PciRegs),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct PciRegs {
|
||||
common: *mut u8,
|
||||
notify: *mut u8,
|
||||
notify_off_multiplier: u32,
|
||||
isr: *mut u8,
|
||||
}
|
||||
|
||||
impl Regs {
|
||||
fn reset(&self) {
|
||||
match self {
|
||||
Regs::Mmio(base) => {
|
||||
w32(*base, STATUS, 0);
|
||||
w32(*base, STATUS, ST_ACK | ST_DRV);
|
||||
}
|
||||
Regs::Pci(p) => {
|
||||
w8(p.common, COMMON_STATUS, 0);
|
||||
while r8(p.common, COMMON_STATUS) != 0 {}
|
||||
w8(p.common, COMMON_STATUS, (ST_ACK | ST_DRV) as u8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn feat_get(&self, sel: u32) -> u32 {
|
||||
match self {
|
||||
Regs::Mmio(base) => {
|
||||
w32(*base, DEVFEAT_SEL, sel);
|
||||
r32(*base, DEVFEAT)
|
||||
}
|
||||
Regs::Pci(p) => {
|
||||
w32(p.common, COMMON_DEVFEAT_SEL, sel);
|
||||
r32(p.common, COMMON_DEVFEAT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn feat_set(&self, sel: u32, v: u32) {
|
||||
match self {
|
||||
Regs::Mmio(base) => {
|
||||
w32(*base, DRVFEAT_SEL, sel);
|
||||
w32(*base, DRVFEAT, v);
|
||||
}
|
||||
Regs::Pci(p) => {
|
||||
w32(p.common, COMMON_DRVFEAT_SEL, sel);
|
||||
w32(p.common, COMMON_DRVFEAT, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn status(&self) -> u32 {
|
||||
match self {
|
||||
Regs::Mmio(base) => r32(*base, STATUS),
|
||||
Regs::Pci(p) => r8(p.common, COMMON_STATUS) as u32,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_status(&self, v: u32) {
|
||||
match self {
|
||||
Regs::Mmio(base) => w32(*base, STATUS, v),
|
||||
Regs::Pci(p) => w8(p.common, COMMON_STATUS, v as u8),
|
||||
}
|
||||
}
|
||||
|
||||
fn queue_prepare(&self, sel: u16, size: u16, desc: u64, avail: u64, used: u64) -> u16 {
|
||||
match self {
|
||||
Regs::Mmio(base) => {
|
||||
let base = *base;
|
||||
w32(base, QSEL, sel as u32);
|
||||
debug_assert!(r32(base, QNUM_MAX) as u16 >= size);
|
||||
w32(base, QNUM, size as u32);
|
||||
w64(base, QDESC_LO, QDESC_HI, desc);
|
||||
w64(base, QAVAIL_LO, QAVAIL_HI, avail);
|
||||
w64(base, QUSED_LO, QUSED_HI, used);
|
||||
w32(base, QREADY, 1);
|
||||
0
|
||||
}
|
||||
Regs::Pci(p) => {
|
||||
w16(p.common, COMMON_QSEL, sel);
|
||||
debug_assert!(r16(p.common, COMMON_QNUM) >= size);
|
||||
w16(p.common, COMMON_QNUM, size);
|
||||
w64(p.common, COMMON_QDESC, COMMON_QDESC + 4, desc);
|
||||
w64(p.common, COMMON_QAVAIL, COMMON_QAVAIL + 4, avail);
|
||||
w64(p.common, COMMON_QUSED, COMMON_QUSED + 4, used);
|
||||
let notify_off = r16(p.common, COMMON_QNOTIFY_OFF);
|
||||
w16(p.common, COMMON_QREADY, 1);
|
||||
notify_off
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn notify(&self, sel: u16, notify_off: u16) {
|
||||
match self {
|
||||
Regs::Mmio(base) => {
|
||||
w32(*base, QSEL, sel as u32);
|
||||
w32(*base, QNOTIFY, sel as u32);
|
||||
}
|
||||
Regs::Pci(p) => {
|
||||
let off = notify_off as usize * p.notify_off_multiplier as usize;
|
||||
w16(p.notify, off, sel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn isr_ack(&self) {
|
||||
match self {
|
||||
Regs::Mmio(base) => {
|
||||
if (r32(*base, ISR) & INT_VRING) != 0 {
|
||||
w32(*base, ISR_ACK, INT_VRING);
|
||||
}
|
||||
}
|
||||
Regs::Pci(p) => {
|
||||
r8(p.isr, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86")]
|
||||
#[derive(Copy, Clone)]
|
||||
struct PciDevice {
|
||||
bus: u8,
|
||||
devfn: u8,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86")]
|
||||
impl PciDevice {
|
||||
fn find_console() -> Option<Self> {
|
||||
for devfn in 0..=u8::MAX {
|
||||
let device = PciDevice { bus: 0, devfn };
|
||||
|
||||
let identity = device.read_config(0);
|
||||
let vendor = (identity & 0xffff) as u16;
|
||||
let model = (identity >> 16) as u16;
|
||||
if vendor != PCI_VENDOR_VIRTIO {
|
||||
continue;
|
||||
}
|
||||
if model != PCI_DEVICE_CONSOLE_LEGACY && model != PCI_DEVICE_CONSOLE_MODERN {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Some(device);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn map_virtio_regs(&self) -> Option<Regs> {
|
||||
self.enable_memory_and_bus_mastering();
|
||||
|
||||
let mut common = core::ptr::null_mut();
|
||||
let mut notify = core::ptr::null_mut();
|
||||
let mut notify_off_multiplier = 0;
|
||||
let mut isr = core::ptr::null_mut();
|
||||
|
||||
let mut cap = (self.read_config(PCI_CAP_LIST_POINTER) & 0xfc) as u8;
|
||||
while cap != 0 {
|
||||
let header = self.read_config(cap);
|
||||
if (header & 0xff) as u8 == PCI_CAP_ID_VENDOR {
|
||||
let cfg_type = self.read_config_byte(cap + VIRTIO_CAP_CFG_TYPE);
|
||||
let bar = self.read_config_byte(cap + VIRTIO_CAP_BAR);
|
||||
let offset = self.read_config(cap + VIRTIO_CAP_OFFSET);
|
||||
let length = self.read_config(cap + VIRTIO_CAP_LENGTH);
|
||||
|
||||
match cfg_type {
|
||||
CFG_TYPE_COMMON => {
|
||||
common = self.map_bar_region(bar, offset, length);
|
||||
}
|
||||
CFG_TYPE_NOTIFY => {
|
||||
notify = self.map_bar_region(bar, offset, length);
|
||||
notify_off_multiplier =
|
||||
self.read_config(cap + VIRTIO_CAP_NOTIFY_MULTIPLIER);
|
||||
}
|
||||
CFG_TYPE_ISR => {
|
||||
isr = self.map_bar_region(bar, offset, length);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
cap = ((header >> 8) & 0xfc) as u8;
|
||||
}
|
||||
|
||||
if common.is_null() || notify.is_null() || isr.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Regs::Pci(PciRegs {
|
||||
common,
|
||||
notify,
|
||||
notify_off_multiplier,
|
||||
isr,
|
||||
}))
|
||||
}
|
||||
|
||||
fn irq_line(&self) -> u32 {
|
||||
self.read_config_byte(PCI_INTERRUPT_LINE) as u32
|
||||
}
|
||||
|
||||
fn enable_memory_and_bus_mastering(&self) {
|
||||
let command = self.read_config(PCI_COMMAND);
|
||||
self.write_config(
|
||||
PCI_COMMAND,
|
||||
command | PCI_COMMAND_MEMORY | PCI_COMMAND_BUS_MASTER,
|
||||
);
|
||||
}
|
||||
|
||||
fn map_bar_region(&self, bar: u8, offset: u32, length: u32) -> *mut u8 {
|
||||
let slot = PCI_BASE_ADDRESS_0 + bar * 4;
|
||||
let lo = self.read_config(slot);
|
||||
|
||||
let is_sixty_four_bit = (lo & 0b110) == 0b100;
|
||||
let base = if is_sixty_four_bit {
|
||||
((self.read_config(slot + 4) as u64) << 32) | ((lo & !0xf) as u64)
|
||||
} else {
|
||||
(lo & !0xf) as u64
|
||||
};
|
||||
|
||||
kernel::map_io(base + offset as u64, length as u64) as *mut u8
|
||||
}
|
||||
|
||||
fn read_config_byte(&self, offset: u8) -> u8 {
|
||||
(self.read_config(offset) >> ((offset & 3) * 8)) as u8
|
||||
}
|
||||
|
||||
fn read_config(&self, offset: u8) -> u32 {
|
||||
outl(PCI_CONFIG_ADDRESS, self.config_address(offset));
|
||||
inl(PCI_CONFIG_DATA)
|
||||
}
|
||||
|
||||
fn write_config(&self, offset: u8, value: u32) {
|
||||
outl(PCI_CONFIG_ADDRESS, self.config_address(offset));
|
||||
outl(PCI_CONFIG_DATA, value);
|
||||
}
|
||||
|
||||
fn config_address(&self, offset: u8) -> u32 {
|
||||
0x8000_0000
|
||||
| ((self.bus as u32) << 16)
|
||||
| ((self.devfn as u32) << 8)
|
||||
| ((offset as u32) & 0xfc)
|
||||
}
|
||||
}
|
||||
|
||||
fn r32(mmio: *mut u8, off: usize) -> u32 {
|
||||
@@ -748,16 +1053,46 @@ fn w64(mmio: *mut u8, lo: usize, hi: usize, v: u64) {
|
||||
w32(mmio, hi, (v >> 32) as u32);
|
||||
}
|
||||
|
||||
fn feat_get(mmio: *mut u8, sel: u32) -> u32 {
|
||||
w32(mmio, DEVFEAT_SEL, sel);
|
||||
r32(mmio, DEVFEAT)
|
||||
fn r16(mmio: *mut u8, off: usize) -> u16 {
|
||||
unsafe { read_volatile(mmio.add(off) as *const u16) }
|
||||
}
|
||||
|
||||
fn feat_set(mmio: *mut u8, sel: u32, v: u32) {
|
||||
w32(mmio, DRVFEAT_SEL, sel);
|
||||
w32(mmio, DRVFEAT, v)
|
||||
fn w16(mmio: *mut u8, off: usize, val: u16) {
|
||||
unsafe { write_volatile(mmio.add(off) as *mut u16, val) }
|
||||
}
|
||||
|
||||
fn r8(mmio: *mut u8, off: usize) -> u8 {
|
||||
unsafe { read_volatile(mmio.add(off)) }
|
||||
}
|
||||
|
||||
fn w8(mmio: *mut u8, off: usize, val: u8) {
|
||||
unsafe { write_volatile(mmio.add(off), val) }
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86")]
|
||||
fn inl(port: u16) -> u32 {
|
||||
let value: u32;
|
||||
unsafe {
|
||||
core::arch::asm!("in eax, dx", out("eax") value, in("dx") port,
|
||||
options(nomem, nostack, preserves_flags));
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86")]
|
||||
fn outl(port: u16, value: u32) {
|
||||
unsafe {
|
||||
core::arch::asm!("out dx, eax", in("dx") port, in("eax") value,
|
||||
options(nomem, nostack, preserves_flags));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86")]
|
||||
fn wmb() {
|
||||
unsafe { core::arch::asm!("sfence", options(nostack, preserves_flags)) }
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
fn wmb() {
|
||||
unsafe { core::arch::asm!("dmb ishst", options(nostack, preserves_flags)) }
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use core::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::xnu;
|
||||
use crate::kernel;
|
||||
|
||||
const AF_VSOCK: c_int = 40;
|
||||
const SOCK_STREAM: c_int = 1;
|
||||
@@ -262,6 +262,6 @@ unsafe fn recv_nonblocking(so: SocketT, dst: &mut [u8]) -> usize {
|
||||
unsafe extern "C" fn upcall(_so: SocketT, cookie: *mut c_void, _waitf: c_int) {
|
||||
UPCALL_PENDING.fetch_add(1, Ordering::Release);
|
||||
if !cookie.is_null() {
|
||||
xnu::thread_wakeup(cookie as *const u8);
|
||||
kernel::wake(cookie as *const u8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Facade over the host kernel the agent is running inside of. Both backends
|
||||
// expose the same primitives, so the rest of the agent never names a specific
|
||||
// kernel.
|
||||
//
|
||||
// XNU is reached from the outside: a remote stub injects the agent and patches
|
||||
// the addresses it needs into .kernel_addrs. Linux is reached from the inside:
|
||||
// the agent is a kernel module, so its glue is an ordinary C translation unit
|
||||
// (linux/frida-kmod.c) compiled against the target kernel's headers by kbuild.
|
||||
|
||||
#[cfg(feature = "linux")]
|
||||
pub use crate::linux::*;
|
||||
#[cfg(feature = "win9x")]
|
||||
pub use crate::win9x::*;
|
||||
#[cfg(feature = "xnu")]
|
||||
pub use crate::xnu::*;
|
||||
|
||||
use core::ffi::c_void;
|
||||
|
||||
pub type ThreadEntry = unsafe extern "C" fn(parameter: *mut c_void, wait_result: i32);
|
||||
File diff suppressed because it is too large.
Load diff
+545
-84
@@ -1,35 +1,69 @@
|
||||
// The C library's lower edge. Nothing here does real work: the agent has no
|
||||
// filesystem and never exits, so these exist to satisfy references and to give
|
||||
// malloc some memory to hand out.
|
||||
//
|
||||
// The two flavours link against different C libraries, and they disagree on what
|
||||
// this layer is called. newlib asks for the underscore-prefixed syscall names;
|
||||
// picolibc, which the soft-float flavour needs because newlib has no soft-float
|
||||
// build for arm64, calls them without the prefix. The bodies are shared and the
|
||||
// exported names live in the two `exports` modules at the end.
|
||||
|
||||
use core::ptr;
|
||||
use core::sync::atomic::Ordering;
|
||||
|
||||
use crate::bindings::{GMutex, GRecMutex};
|
||||
use crate::gthread;
|
||||
|
||||
static mut MALLOC_LOCK: GRecMutex = unsafe { core::mem::zeroed() };
|
||||
// Serialises the C library. Which hooks the library reaches for differs: newlib asks
|
||||
// for __malloc_lock/__malloc_unlock, picolibc for the retargetable lock API below,
|
||||
// whose default implementation is an empty function — so on picolibc malloc runs
|
||||
// unlocked unless we supply these, and any second thread corrupts the heap.
|
||||
static mut LIBC_LOCK: GRecMutex = unsafe { core::mem::zeroed() };
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __malloc_lock(_reent: *mut core::ffi::c_void) {
|
||||
gthread::g_rec_mutex_lock(ptr::addr_of_mut!(MALLOC_LOCK));
|
||||
fn libc_lock() {
|
||||
gthread::g_rec_mutex_lock(ptr::addr_of_mut!(LIBC_LOCK));
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __malloc_unlock(_reent: *mut core::ffi::c_void) {
|
||||
gthread::g_rec_mutex_unlock(ptr::addr_of_mut!(MALLOC_LOCK));
|
||||
fn libc_unlock() {
|
||||
gthread::g_rec_mutex_unlock(ptr::addr_of_mut!(LIBC_LOCK));
|
||||
offer_preemption_point();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _fini() {
|
||||
fn offer_preemption_point() {
|
||||
let total = ALLOCATIONS_TOTAL.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if total % ALLOCATIONS_PER_YIELD == 0 {
|
||||
crate::kernel::yield_now();
|
||||
}
|
||||
}
|
||||
|
||||
static ALLOCATIONS_TOTAL: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
|
||||
const ALLOCATIONS_PER_YIELD: u32 = 1024;
|
||||
|
||||
// GLib's g_get_num_processors() probes this; report a single CPU so the
|
||||
// parallel scanners fall back to their inline path.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _exit(_status: i32) -> ! {
|
||||
loop {}
|
||||
pub extern "C" fn sysconf(_name: i32) -> isize {
|
||||
1
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _kill(_pid: i32, _sig: i32) -> i32 {
|
||||
-1
|
||||
pub extern "C" fn __clear_cache(_start: *const u8, _end: *const u8) {}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __clear_cache(_start: *const u8, _end: *const u8) {
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"ic iallu", // Invalidate all instruction caches
|
||||
"dsb sy", // Data synchronization barrier
|
||||
"isb", // Instruction synchronization barrier
|
||||
options(nomem, nostack),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
|
||||
fn sbrk_impl(incr: isize) -> *mut u8 {
|
||||
const HEAP_SIZE: usize = 32 * 1024 * 1024; // 32 MB
|
||||
|
||||
static mut HEAP_START: *mut u8 = ptr::null_mut();
|
||||
@@ -40,7 +74,7 @@ pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
|
||||
gthread::g_mutex_lock(ptr::addr_of_mut!(HEAP_MUTEX));
|
||||
|
||||
if HEAP_START.is_null() {
|
||||
HEAP_START = crate::xnu::kalloc(HEAP_SIZE);
|
||||
HEAP_START = crate::kernel::alloc(HEAP_SIZE);
|
||||
if HEAP_START.is_null() {
|
||||
gthread::g_mutex_unlock(ptr::addr_of_mut!(HEAP_MUTEX));
|
||||
panic!("Failed to allocate 32 MB heap");
|
||||
@@ -49,7 +83,8 @@ pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
|
||||
}
|
||||
|
||||
let prev_heap_current = HEAP_CURRENT;
|
||||
let new_heap_current = HEAP_CURRENT.add(incr as usize);
|
||||
// Signed: the allocator hands memory back by asking for a negative increment.
|
||||
let new_heap_current = HEAP_CURRENT.offset(incr);
|
||||
|
||||
if new_heap_current > HEAP_START.add(HEAP_SIZE) {
|
||||
gthread::g_mutex_unlock(ptr::addr_of_mut!(HEAP_MUTEX));
|
||||
@@ -65,20 +100,14 @@ pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _getpid() -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct Timeval {
|
||||
tv_sec: i64,
|
||||
tv_usec: i64,
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _gettimeofday(tp: *mut core::ffi::c_void, _tzp: *mut core::ffi::c_void) -> i32 {
|
||||
let (secs, microsecs) = crate::xnu::clock_get_calendar_microtime();
|
||||
fn gettimeofday_impl(tp: *mut core::ffi::c_void) -> i32 {
|
||||
let (secs, microsecs) = crate::kernel::wall_clock_micros();
|
||||
let timeval = Timeval {
|
||||
tv_sec: secs as i64,
|
||||
tv_usec: microsecs as i64,
|
||||
@@ -93,68 +122,500 @@ pub extern "C" fn _gettimeofday(tp: *mut core::ffi::c_void, _tzp: *mut core::ffi
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _isatty(_fd: i32) -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _open(_path: *const u8, _flags: i32, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _close(_fd: i32) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _read(_fd: i32, _buf: *mut u8, _count: usize) -> isize {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _write(_fd: i32, _buf: *const u8, count: usize) -> isize {
|
||||
fn write_impl(count: usize) -> isize {
|
||||
count as isize
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _lseek(_fd: i32, _offset: isize, _whence: i32) -> isize {
|
||||
0
|
||||
}
|
||||
#[cfg(feature = "xnu")]
|
||||
mod exports {
|
||||
use super::*;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _fstat(_fd: i32, _st: *mut core::ffi::c_void) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _link(_old: *const u8, _new: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _unlink(_path: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
// GLib's g_get_num_processors() probes this; report a single CPU so the
|
||||
// parallel scanners fall back to their inline path.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn sysconf(_name: i32) -> isize {
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __clear_cache(_start: *const u8, _end: *const u8) {
|
||||
// On AArch64, we need to flush the instruction cache
|
||||
// This is a simplified implementation for the kernel environment
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"ic iallu", // Invalidate all instruction caches
|
||||
"dsb sy", // Data synchronization barrier
|
||||
"isb", // Instruction synchronization barrier
|
||||
options(nomem, nostack),
|
||||
);
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __malloc_lock(_reent: *mut core::ffi::c_void) {
|
||||
libc_lock();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __malloc_unlock(_reent: *mut core::ffi::c_void) {
|
||||
libc_unlock();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _fini() {}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _exit(_status: i32) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _kill(_pid: i32, _sig: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
|
||||
sbrk_impl(incr)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _getpid() -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _gettimeofday(
|
||||
tp: *mut core::ffi::c_void,
|
||||
_tzp: *mut core::ffi::c_void,
|
||||
) -> i32 {
|
||||
gettimeofday_impl(tp)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _isatty(_fd: i32) -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _open(_path: *const u8, _flags: i32, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _close(_fd: i32) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _read(_fd: i32, _buf: *mut u8, _count: usize) -> isize {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _write(_fd: i32, _buf: *const u8, count: usize) -> isize {
|
||||
write_impl(count)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _lseek(_fd: i32, _offset: isize, _whence: i32) -> isize {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _fstat(_fd: i32, _st: *mut core::ffi::c_void) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _stat(_path: *const u8, _st: *mut core::ffi::c_void) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _link(_old: *const u8, _new: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _unlink(_path: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn creat(_path: *const u8, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn access(_path: *const u8, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn chmod(_path: *const u8, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn chdir(_path: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn rmdir(_path: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gethostname(_name: *mut u8, _len: usize) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
// newlib wires these up for exit(), which we never reach: the agent runs until
|
||||
// the kernel goes down or the module is unloaded.
|
||||
#[unsafe(no_mangle)]
|
||||
pub static __libc_fini: Option<extern "C" fn()> = None;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub static __fini_array_start: [extern "C" fn(); 0] = [];
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub static __fini_array_end: [extern "C" fn(); 0] = [];
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "linux", feature = "win9x"))]
|
||||
mod exports {
|
||||
use super::*;
|
||||
|
||||
// The C library's own allocator sits on a fixed sbrk heap and carries its own
|
||||
// locking; inside a kernel module the kernel's allocator is already there, already
|
||||
// safe to share between threads, and not capped at a size we picked up front.
|
||||
// Every C allocation goes here instead.
|
||||
//
|
||||
// The sixteen bytes before the returned pointer hold the block the kernel gave us
|
||||
// and its total size, so a pointer that was moved forward to satisfy an alignment
|
||||
// request frees exactly like one that was not.
|
||||
use core::ffi::c_void;
|
||||
|
||||
const HEAP_HEADER_SIZE: usize = 16;
|
||||
|
||||
const MALLOC_ALIGNMENT: usize = 2 * core::mem::size_of::<*const u8>();
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn malloc(size: usize) -> *mut c_void {
|
||||
let total = size + HEAP_HEADER_SIZE + MALLOC_ALIGNMENT;
|
||||
let block = crate::kernel::alloc(total);
|
||||
if block.is_null() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
offer_preemption_point();
|
||||
|
||||
unsafe {
|
||||
let lowest = block.add(HEAP_HEADER_SIZE) as usize;
|
||||
let aligned = (lowest + MALLOC_ALIGNMENT - 1) & !(MALLOC_ALIGNMENT - 1);
|
||||
attach_header(block, total, aligned as *mut u8)
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn posix_memalign(
|
||||
result: *mut *mut c_void,
|
||||
alignment: usize,
|
||||
size: usize,
|
||||
) -> i32 {
|
||||
let total = size + alignment + HEAP_HEADER_SIZE;
|
||||
let block = crate::kernel::alloc(total);
|
||||
if block.is_null() {
|
||||
return 12; // ENOMEM
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let lowest = block.add(HEAP_HEADER_SIZE) as usize;
|
||||
let aligned = (lowest + alignment - 1) & !(alignment - 1);
|
||||
*result = attach_header(block, total, aligned as *mut u8);
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn free(p: *mut c_void) {
|
||||
if p.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let (block, total) = header_of(p);
|
||||
crate::kernel::free(block, total);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn malloc_usable_size(p: *mut c_void) -> usize {
|
||||
unsafe { usable_size(p) }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn calloc(count: usize, size: usize) -> *mut c_void {
|
||||
let total = count * size;
|
||||
let p = malloc(total);
|
||||
if !p.is_null() {
|
||||
unsafe { ptr::write_bytes(p as *mut u8, 0, total) };
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn realloc(p: *mut c_void, size: usize) -> *mut c_void {
|
||||
if p.is_null() {
|
||||
return malloc(size);
|
||||
}
|
||||
|
||||
let previous_size = unsafe { usable_size(p) };
|
||||
let grown = malloc(size);
|
||||
if !grown.is_null() {
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(p as *const u8, grown as *mut u8, previous_size.min(size))
|
||||
};
|
||||
free(p);
|
||||
}
|
||||
grown
|
||||
}
|
||||
|
||||
unsafe fn attach_header(block: *mut u8, total: usize, p: *mut u8) -> *mut c_void {
|
||||
unsafe {
|
||||
let header = p.sub(HEAP_HEADER_SIZE) as *mut usize;
|
||||
*header = block as usize;
|
||||
*header.add(1) = total;
|
||||
}
|
||||
p as *mut c_void
|
||||
}
|
||||
|
||||
unsafe fn header_of(p: *mut c_void) -> (*mut u8, usize) {
|
||||
unsafe {
|
||||
let header = (p as *mut u8).sub(HEAP_HEADER_SIZE) as *const usize;
|
||||
(*header as *mut u8, *header.add(1))
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn usable_size(p: *mut c_void) -> usize {
|
||||
unsafe {
|
||||
let (block, total) = header_of(p);
|
||||
total - (p as usize - block as usize)
|
||||
}
|
||||
}
|
||||
|
||||
// One recursive mutex covers every lock the C library asks for. They are coarse
|
||||
// and few, and over-serialising them is safe where getting it wrong is not.
|
||||
type Lock = *mut core::ffi::c_void;
|
||||
|
||||
// The lock object itself, which we define so the C library's own no-op lock
|
||||
// implementation is never pulled out of its archive. Its contents are never
|
||||
// read: the address is just a token, and one mutex covers them all.
|
||||
// Nothing here references it, so without #[used] it is dropped before the linker
|
||||
// ever sees it, and the archive member arrives after all to supply it.
|
||||
#[allow(non_upper_case_globals)]
|
||||
#[unsafe(no_mangle)]
|
||||
#[used]
|
||||
pub static mut __lock___libc_recursive_mutex: u8 = 0;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __retarget_lock_init(_lock: *mut Lock) {}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __retarget_lock_init_recursive(_lock: *mut Lock) {}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __retarget_lock_close(_lock: Lock) {}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __retarget_lock_close_recursive(_lock: Lock) {}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __retarget_lock_acquire(_lock: Lock) {
|
||||
libc_lock();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __retarget_lock_acquire_recursive(_lock: Lock) {
|
||||
libc_lock();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __retarget_lock_release(_lock: Lock) {
|
||||
libc_unlock();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __retarget_lock_release_recursive(_lock: Lock) {
|
||||
libc_unlock();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn exit(_status: i32) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _exit(_status: i32) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn kill(_pid: i32, _sig: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn sbrk(incr: isize) -> *mut u8 {
|
||||
sbrk_impl(incr)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn getpid() -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gettimeofday(
|
||||
tp: *mut core::ffi::c_void,
|
||||
_tzp: *mut core::ffi::c_void,
|
||||
) -> i32 {
|
||||
gettimeofday_impl(tp)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn isatty(_fd: i32) -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn open(_path: *const u8, _flags: i32, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn close(_fd: i32) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn read(_fd: i32, _buf: *mut u8, _count: usize) -> isize {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn write(_fd: i32, _buf: *const u8, count: usize) -> isize {
|
||||
write_impl(count)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn lseek(_fd: i32, _offset: isize, _whence: i32) -> isize {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn fstat(_fd: i32, _st: *mut core::ffi::c_void) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn stat(_path: *const u8, _st: *mut core::ffi::c_void) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn unlink(_path: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn rename(_old: *const u8, _new: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn access(_path: *const u8, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn chmod(_path: *const u8, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn chdir(_path: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn rmdir(_path: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gethostname(_name: *mut u8, _len: usize) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Timespec {
|
||||
tv_sec: i64,
|
||||
tv_nsec: i64,
|
||||
}
|
||||
|
||||
const CLOCK_MONOTONIC: i32 = 1;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn clock_gettime(clock_id: i32, tp: *mut Timespec) -> i32 {
|
||||
let (secs, nsecs) = if clock_id == CLOCK_MONOTONIC {
|
||||
let micros = crate::kernel::monotonic_micros();
|
||||
(micros / 1_000_000, (micros % 1_000_000) * 1000)
|
||||
} else {
|
||||
let (secs, micros) = crate::kernel::wall_clock_micros();
|
||||
(secs as i64, (micros as i64) * 1000)
|
||||
};
|
||||
|
||||
unsafe {
|
||||
(*tp).tv_sec = secs;
|
||||
(*tp).tv_nsec = nsecs;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
// picolibc consults this for errno values it does not know itself.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _user_strerror(_errnum: i32) -> *const core::ffi::c_char {
|
||||
ptr::null()
|
||||
}
|
||||
|
||||
// No dynamic loader here. Gum calls dladdr() to describe an address and already
|
||||
// falls back to printing it raw when the lookup fails.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn dladdr(_address: *const core::ffi::c_void, _info: *mut core::ffi::c_void) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn dlopen(_file: *const u8, _mode: i32) -> *mut core::ffi::c_void {
|
||||
ptr::null_mut()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn dlsym(
|
||||
_handle: *mut core::ffi::c_void,
|
||||
_name: *const u8,
|
||||
) -> *mut core::ffi::c_void {
|
||||
ptr::null_mut()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn dlclose(_handle: *mut core::ffi::c_void) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn dlerror() -> *mut core::ffi::c_char {
|
||||
ptr::null_mut()
|
||||
}
|
||||
|
||||
// picolibc declares the standard streams and leaves defining them to the
|
||||
// application. The agent has no I/O — its logging goes to the kernel's own log —
|
||||
// so nothing should ever reach for one of these.
|
||||
#[repr(transparent)]
|
||||
pub struct Stream(*mut core::ffi::c_void);
|
||||
|
||||
unsafe impl Sync for Stream {}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub static stdin: Stream = Stream(ptr::null_mut());
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub static stdout: Stream = Stream(ptr::null_mut());
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub static stderr: Stream = Stream(ptr::null_mut());
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// Linux backend. Unlike XNU — where a remote stub injects the agent and patches
|
||||
// resolved kernel addresses into .kernel_addrs — here the agent is loaded from
|
||||
// the inside as a kernel module, so every primitive is an ordinary call into
|
||||
// linux/frida-kmod.c. That shim is compiled by kbuild against the target
|
||||
// kernel's headers, which is what keeps us out of the business of guessing
|
||||
// struct layouts, config-dependent macros and per-version symbol names.
|
||||
|
||||
use core::ffi::{CStr, c_char, c_int, c_void};
|
||||
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::kernel::ThreadEntry;
|
||||
|
||||
pub fn log(msg: &str) {
|
||||
unsafe { frida_kmod_log(msg.as_ptr() as *const c_char) };
|
||||
}
|
||||
|
||||
pub fn panic(msg: &str) -> ! {
|
||||
unsafe { frida_kmod_panic(msg.as_ptr() as *const c_char) }
|
||||
}
|
||||
|
||||
pub fn run_when_ready(action: fn()) {
|
||||
action();
|
||||
}
|
||||
|
||||
pub fn spawn_thread(entry: ThreadEntry, parameter: *mut c_void) -> isize {
|
||||
unsafe { frida_kmod_spawn_thread(entry, parameter) as isize }
|
||||
}
|
||||
|
||||
pub fn install_hooks() {
|
||||
unsafe { frida_kmod_install_hooks() };
|
||||
}
|
||||
|
||||
pub fn alloc(size: usize) -> *mut u8 {
|
||||
unsafe { frida_kmod_alloc(size) }
|
||||
}
|
||||
|
||||
pub fn free(ptr: *mut u8, size: usize) {
|
||||
unsafe { frida_kmod_free(ptr, size) };
|
||||
}
|
||||
|
||||
pub fn alloc_code(size: usize) -> *mut u8 {
|
||||
unsafe { frida_kmod_alloc_code(size) }
|
||||
}
|
||||
|
||||
pub fn free_code(ptr: *mut u8, size: usize) {
|
||||
unsafe { frida_kmod_free_code(ptr, size) };
|
||||
}
|
||||
|
||||
pub fn own_range() -> (u64, u64) {
|
||||
let mut base: u64 = 0;
|
||||
let mut size: u64 = 0;
|
||||
unsafe { frida_kmod_own_range(&mut base, &mut size) };
|
||||
(base, size)
|
||||
}
|
||||
|
||||
// The sequence number is taken before `check` runs, so a wakeup racing with the
|
||||
// condition we are about to observe still makes the commit return immediately.
|
||||
pub fn wait(_token: *const u8, timeout_us: Option<u64>, check: &mut dyn FnMut() -> bool) {
|
||||
let seq = unsafe { frida_kmod_wait_prepare() };
|
||||
|
||||
if check() {
|
||||
return;
|
||||
}
|
||||
|
||||
unsafe { frida_kmod_wait_commit(seq, timeout_us.map_or(-1, |us| us as i64)) };
|
||||
}
|
||||
|
||||
pub fn wake(_token: *const u8) {
|
||||
unsafe { frida_kmod_wake() };
|
||||
}
|
||||
|
||||
/// Gives the scheduler a chance to run something else. A kernel thread that loops
|
||||
/// without doing this is a hard lockup waiting to happen.
|
||||
pub fn yield_now() {
|
||||
unsafe { frida_kmod_yield() };
|
||||
}
|
||||
|
||||
pub fn monotonic_micros() -> i64 {
|
||||
unsafe { frida_kmod_monotonic_micros() }
|
||||
}
|
||||
|
||||
pub fn wall_clock_micros() -> (u32, u32) {
|
||||
let mut secs: u32 = 0;
|
||||
let mut micros: u32 = 0;
|
||||
unsafe { frida_kmod_wall_clock_micros(&mut secs, &mut micros) };
|
||||
(secs, micros)
|
||||
}
|
||||
|
||||
pub fn current_thread_id() -> u64 {
|
||||
unsafe { frida_kmod_current_thread_id() }
|
||||
}
|
||||
|
||||
pub fn protect(address: u64, size: usize, gum_prot: u32) -> bool {
|
||||
unsafe { frida_kmod_protect(address, size, gum_prot) != 0 }
|
||||
}
|
||||
|
||||
pub fn remap_writable(first_page: u64, n_pages: u32) -> *mut c_void {
|
||||
unsafe { frida_kmod_remap_writable(first_page, n_pages) }
|
||||
}
|
||||
|
||||
/// Tears down an alias handed out by [`remap_writable`] and flushes the icache
|
||||
/// over the range it aliased; the shim remembers which range that was.
|
||||
pub fn unmap_writable(mapping: *mut c_void) {
|
||||
unsafe { frida_kmod_unmap_writable(mapping) };
|
||||
}
|
||||
|
||||
pub fn get_kernel_base() -> u64 {
|
||||
unsafe { frida_kmod_kernel_base() }
|
||||
}
|
||||
|
||||
pub fn get_kernel_size() -> u64 {
|
||||
unsafe { frida_kmod_kernel_size() }
|
||||
}
|
||||
|
||||
pub fn enumerate_modules() -> Vec<LoadedModule> {
|
||||
unsafe extern "C" fn on_module(
|
||||
name: *const c_char,
|
||||
version: *const c_char,
|
||||
base: u64,
|
||||
size: u64,
|
||||
user_data: *mut c_void,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
let modules = &mut *(user_data as *mut Vec<LoadedModule>);
|
||||
modules.push(LoadedModule {
|
||||
name: String::from(CStr::from_ptr(name).to_str().unwrap_or("?")),
|
||||
version: String::from(CStr::from_ptr(version).to_str().unwrap_or("")),
|
||||
base,
|
||||
size,
|
||||
});
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
let mut modules: Vec<LoadedModule> = Vec::new();
|
||||
unsafe {
|
||||
frida_kmod_enumerate_modules(on_module, &mut modules as *mut _ as *mut c_void);
|
||||
}
|
||||
modules
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoadedModule {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub base: u64,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
pub fn find_symbol(name: &CStr) -> u64 {
|
||||
unsafe { frida_kmod_find_symbol(name.as_ptr()) }
|
||||
}
|
||||
|
||||
pub fn find_function(name: &CStr) -> u64 {
|
||||
unsafe { frida_kmod_find_function(name.as_ptr()) }
|
||||
}
|
||||
|
||||
/// Resolves `address` to `(name, symbol_address)`, or `None` when it falls
|
||||
/// outside every known symbol.
|
||||
pub fn symbol_from_address(address: u64) -> Option<(String, u64)> {
|
||||
const NAME_CAPACITY: usize = 256;
|
||||
|
||||
let mut buffer = [0u8; NAME_CAPACITY];
|
||||
let mut symbol_address: u64 = 0;
|
||||
let found = unsafe {
|
||||
frida_kmod_symbol_name_from_address(
|
||||
address,
|
||||
buffer.as_mut_ptr() as *mut c_char,
|
||||
NAME_CAPACITY,
|
||||
&mut symbol_address,
|
||||
)
|
||||
};
|
||||
if found == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = CStr::from_bytes_until_nul(&buffer).unwrap();
|
||||
Some((String::from(name.to_str().unwrap()), symbol_address))
|
||||
}
|
||||
|
||||
/// Visits every symbol the running kernel knows about. Returns false when
|
||||
/// kallsyms is unavailable, which is the case on kernels built without
|
||||
/// CONFIG_KALLSYMS or when the shim could not resolve its entry points.
|
||||
pub fn enumerate_symbols(callback: &mut dyn FnMut(&CStr, u64) -> bool) -> bool {
|
||||
unsafe extern "C" fn on_symbol(
|
||||
name: *const c_char,
|
||||
address: u64,
|
||||
user_data: *mut c_void,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
let callback = &mut *(user_data as *mut &mut dyn FnMut(&CStr, u64) -> bool);
|
||||
if callback(CStr::from_ptr(name), address) { 1 } else { 0 }
|
||||
}
|
||||
}
|
||||
|
||||
let mut boxed: &mut dyn FnMut(&CStr, u64) -> bool = callback;
|
||||
unsafe { frida_kmod_enumerate_symbols(on_symbol, &mut boxed as *mut _ as *mut c_void) != 0 }
|
||||
}
|
||||
|
||||
type FoundSymbolFunc =
|
||||
unsafe extern "C" fn(name: *const c_char, address: u64, user_data: *mut c_void) -> c_int;
|
||||
type FoundModuleFunc = unsafe extern "C" fn(
|
||||
name: *const c_char,
|
||||
version: *const c_char,
|
||||
base: u64,
|
||||
size: u64,
|
||||
user_data: *mut c_void,
|
||||
) -> c_int;
|
||||
|
||||
unsafe extern "C" {
|
||||
fn frida_kmod_log(message: *const c_char);
|
||||
fn frida_kmod_panic(message: *const c_char) -> !;
|
||||
fn frida_kmod_spawn_thread(entry: ThreadEntry, parameter: *mut c_void) -> c_int;
|
||||
fn frida_kmod_install_hooks();
|
||||
fn frida_kmod_alloc(size: usize) -> *mut u8;
|
||||
fn frida_kmod_free(ptr: *mut u8, size: usize);
|
||||
fn frida_kmod_alloc_code(size: usize) -> *mut u8;
|
||||
fn frida_kmod_free_code(ptr: *mut u8, size: usize);
|
||||
fn frida_kmod_own_range(base: *mut u64, size: *mut u64);
|
||||
fn frida_kmod_wait_prepare() -> u32;
|
||||
fn frida_kmod_wait_commit(seq: u32, timeout_us: i64);
|
||||
fn frida_kmod_wake();
|
||||
fn frida_kmod_yield();
|
||||
fn frida_kmod_monotonic_micros() -> i64;
|
||||
fn frida_kmod_wall_clock_micros(secs: *mut u32, micros: *mut u32);
|
||||
fn frida_kmod_current_thread_id() -> u64;
|
||||
fn frida_kmod_protect(address: u64, size: usize, gum_prot: u32) -> c_int;
|
||||
fn frida_kmod_remap_writable(first_page: u64, n_pages: u32) -> *mut c_void;
|
||||
fn frida_kmod_unmap_writable(mapping: *mut c_void);
|
||||
fn frida_kmod_kernel_base() -> u64;
|
||||
fn frida_kmod_kernel_size() -> u64;
|
||||
fn frida_kmod_enumerate_modules(func: FoundModuleFunc, user_data: *mut c_void);
|
||||
fn frida_kmod_find_symbol(name: *const c_char) -> u64;
|
||||
fn frida_kmod_find_function(name: *const c_char) -> u64;
|
||||
fn frida_kmod_symbol_name_from_address(
|
||||
address: u64,
|
||||
buffer: *mut c_char,
|
||||
size: usize,
|
||||
symbol_address: *mut u64,
|
||||
) -> c_int;
|
||||
fn frida_kmod_enumerate_symbols(func: FoundSymbolFunc, user_data: *mut c_void) -> c_int;
|
||||
}
|
||||
+10
-691
@@ -1,693 +1,12 @@
|
||||
#![no_main]
|
||||
// The XNU flavour ships as a freestanding ELF executable: the host injects the
|
||||
// image and calls its `_start`. All of the agent lives in the library crate;
|
||||
// this target exists only to produce that link. build.rs passes `--undefined
|
||||
// _start` so the entrypoint survives --gc-sections.
|
||||
//
|
||||
// The Linux flavour is built as a static library instead and linked into a
|
||||
// kernel module by kbuild — see linux/Kbuild.
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::alloc::{GlobalAlloc, Layout};
|
||||
use core::ffi::{CStr, c_void};
|
||||
use core::ptr;
|
||||
use core::ptr::null_mut;
|
||||
use core::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use bindings::{
|
||||
GBytes, GCancellable, GError, GVariant, GVariantIter, GumScript, g_error_free, g_free,
|
||||
g_main_context_default, g_main_context_iteration, g_memdup2, g_object_unref,
|
||||
g_variant_check_format_string, g_variant_get, g_variant_get_byte, g_variant_get_child_value,
|
||||
g_variant_get_data, g_variant_get_size, g_variant_get_string, g_variant_get_uint32,
|
||||
g_variant_get_uint64, g_variant_get_variant, g_variant_iter_init, g_variant_iter_next,
|
||||
g_variant_new, g_variant_new_from_data, g_variant_new_string, g_variant_new_tuple,
|
||||
g_variant_new_uint32, g_variant_type_free, g_variant_type_new, g_variant_unref, gchar,
|
||||
gpointer, gsize, gum_script_backend_create_sync, gum_script_backend_obtain_qjs,
|
||||
gum_script_get_stalker, gum_script_load_sync, gum_script_post, gum_script_set_message_handler,
|
||||
gum_script_unload_sync, gum_stalker_exclude, GumMemoryRange,
|
||||
};
|
||||
use symbols::SymbolTable;
|
||||
|
||||
mod ffi;
|
||||
mod glib;
|
||||
mod gthread;
|
||||
mod gum;
|
||||
mod hostlink_virtio;
|
||||
mod hostlink_vsock;
|
||||
|
||||
mod libc;
|
||||
mod pac;
|
||||
mod symbols;
|
||||
mod xnu;
|
||||
|
||||
mod bindings {
|
||||
#![allow(
|
||||
dead_code,
|
||||
improper_ctypes,
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
non_upper_case_globals,
|
||||
unused_imports
|
||||
)]
|
||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||
pub enum FridaCommand {
|
||||
CreateScript = 1,
|
||||
LoadScript = 2,
|
||||
DestroyScript = 3,
|
||||
PostScriptMessage = 4,
|
||||
RemapWritablePages = 5,
|
||||
MemoryProtect = 6,
|
||||
PatchCode = 7,
|
||||
|
||||
Reply = 128,
|
||||
ScriptMessage = 129,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for FridaCommand {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
FridaCommand::CreateScript => write!(f, "CreateScript"),
|
||||
FridaCommand::LoadScript => write!(f, "LoadScript"),
|
||||
FridaCommand::DestroyScript => write!(f, "DestroyScript"),
|
||||
FridaCommand::PostScriptMessage => write!(f, "PostScriptMessage"),
|
||||
FridaCommand::RemapWritablePages => write!(f, "RemapWritablePages"),
|
||||
FridaCommand::MemoryProtect => write!(f, "MemoryProtect"),
|
||||
FridaCommand::PatchCode => write!(f, "PatchCode"),
|
||||
FridaCommand::Reply => write!(f, "Reply"),
|
||||
FridaCommand::ScriptMessage => write!(f, "ScriptMessage"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct HandlerResponse {
|
||||
variant: *mut GVariant,
|
||||
}
|
||||
|
||||
impl HandlerResponse {
|
||||
fn success(variant: *mut GVariant) -> Self {
|
||||
Self { variant }
|
||||
}
|
||||
|
||||
fn success_empty() -> Self {
|
||||
let variant = unsafe { g_variant_new_tuple(ptr::null(), 0) };
|
||||
Self { variant }
|
||||
}
|
||||
|
||||
fn error(message: &str) -> Self {
|
||||
let mut c_message = String::from(message);
|
||||
c_message.push('\0');
|
||||
let error_variant = unsafe { g_variant_new_string(c_message.as_ptr()) };
|
||||
|
||||
Self {
|
||||
variant: error_variant,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static mut CONFIG_DATA: &'static [u8] = &[];
|
||||
pub static mut MODULE_INFO: Vec<ModuleInfo> = Vec::new();
|
||||
pub static mut SYMBOL_TABLE: SymbolTable = SymbolTable::empty();
|
||||
|
||||
pub enum Transport {
|
||||
Virtio(hostlink_virtio::Hostlink),
|
||||
Vsock(hostlink_vsock::Hostlink),
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
pub fn send(&self, payload: &[u8]) {
|
||||
match self {
|
||||
Transport::Virtio(h) => h.send(payload),
|
||||
Transport::Vsock(h) => h.send(payload),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process(&self) {
|
||||
match self {
|
||||
Transport::Virtio(h) => h.process(),
|
||||
Transport::Vsock(h) => h.process(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum TransportConfig {
|
||||
Virtio { mmio: u64, irq: u32 },
|
||||
Vsock { host_port: u32 },
|
||||
}
|
||||
|
||||
static mut TRANSPORT_DRIVER: *mut Transport = core::ptr::null_mut();
|
||||
|
||||
#[inline(always)]
|
||||
fn transport_set(driver: Transport) {
|
||||
unsafe {
|
||||
let boxed = Box::into_raw(Box::new(driver));
|
||||
TRANSPORT_DRIVER = boxed;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn transport_get_unchecked() -> &'static Transport {
|
||||
unsafe {
|
||||
debug_assert!(!TRANSPORT_DRIVER.is_null());
|
||||
&*TRANSPORT_DRIVER
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn transport_is_up() -> bool {
|
||||
unsafe { !TRANSPORT_DRIVER.is_null() }
|
||||
}
|
||||
|
||||
static mut SCRIPTS: BTreeMap<u32, *mut GumScript> = BTreeMap::new();
|
||||
static NEXT_SCRIPT_ID: AtomicU32 = AtomicU32::new(1);
|
||||
static mut OWN_RANGE: GumMemoryRange = GumMemoryRange {
|
||||
base_address: 0,
|
||||
size: 0,
|
||||
};
|
||||
|
||||
static NEXT_REQUEST_ID: AtomicU32 = AtomicU32::new(1);
|
||||
static mut PENDING_REPLIES: BTreeMap<u16, *mut GVariant> = BTreeMap::new();
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModuleInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub offset: u32,
|
||||
pub size: u32,
|
||||
pub start_func_offset: u32,
|
||||
pub stop_func_offset: u32,
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn _start(config_data: *const u8, config_size: usize) {
|
||||
unsafe {
|
||||
xnu::io_log("frida: _start entry\n\0");
|
||||
CONFIG_DATA = core::slice::from_raw_parts(config_data, config_size);
|
||||
xnu::io_log("frida: _start config stored\n\0");
|
||||
|
||||
let _r = xnu::kernel_thread_start(frida_agent_worker, 12345usize as *mut core::ffi::c_void);
|
||||
}
|
||||
}
|
||||
|
||||
// KNOWN ISSUE (vphone research kernel): the worker panics during gum_init_embedded with
|
||||
// "debug exceptions enabled in kernel mode". The worker itself is healthy — it runs with
|
||||
// PSTATE.D=1 (debug masked) throughout. The panic is the research kernel's synchronous-
|
||||
// exception handler reacting to ANY exception the agent takes in kernel mode while the VZ
|
||||
// debug stub is active. Two confirmed sources: (1) the host's breakpoint-based Callbacks
|
||||
// patch BRK at gum_try_mprotect/remap, and the BRK is a debug exception; (2) a stray jump
|
||||
// into the agent's data region during GObject init (instruction abort) — looks like a
|
||||
// misapplied relocation for a function pointer stored in .data/.rodata.
|
||||
unsafe extern "C" fn frida_agent_worker(_parameter: *mut core::ffi::c_void, _wait_result: i32) {
|
||||
unsafe {
|
||||
xnu::io_log("frida: worker entry\n\0");
|
||||
bindings::g_set_panic_handler(Some(frida_panic_handler), ptr::null_mut());
|
||||
bindings::gum_init_embedded();
|
||||
xnu::io_log("frida: gum_init_embedded done\n\0");
|
||||
bindings::g_log_set_default_handler(Some(frida_log_handler), ptr::null_mut());
|
||||
|
||||
let (transport_config, kernel_base, module_info, symbol_table, own_range) =
|
||||
parse_config(core::ptr::addr_of!(CONFIG_DATA).read());
|
||||
xnu::set_kernel_base(kernel_base);
|
||||
MODULE_INFO = module_info;
|
||||
SYMBOL_TABLE = symbol_table;
|
||||
OWN_RANGE = own_range;
|
||||
xnu::io_log("frida: config parsed, init transport\n\0");
|
||||
|
||||
let wake_token = ptr::addr_of_mut!(glib::WAKEUP_TOKEN) as *const u8;
|
||||
let transport = match transport_config {
|
||||
TransportConfig::Virtio { mmio, irq } => Transport::Virtio(
|
||||
hostlink_virtio::Hostlink::init(mmio, irq, Some(on_frame_from_host), wake_token)
|
||||
.unwrap(),
|
||||
),
|
||||
TransportConfig::Vsock { host_port } => Transport::Vsock(
|
||||
hostlink_vsock::Hostlink::init(host_port, Some(on_frame_from_host), wake_token)
|
||||
.unwrap(),
|
||||
),
|
||||
};
|
||||
transport_set(transport);
|
||||
xnu::io_log("frida: transport up, entering main loop\n\0");
|
||||
|
||||
let main_context = g_main_context_default();
|
||||
|
||||
loop {
|
||||
transport_get_unchecked().process();
|
||||
g_main_context_iteration(main_context, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_frame_from_host(frame: &[u8]) {
|
||||
if let Some(variant) = deserialize_message(&frame) {
|
||||
process_incoming_message(variant);
|
||||
unsafe {
|
||||
g_variant_unref(variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn parse_config(
|
||||
config: &[u8],
|
||||
) -> (TransportConfig, u64, Vec<ModuleInfo>, SymbolTable, GumMemoryRange) {
|
||||
unsafe {
|
||||
let type_string = c"((tt)yvta(ssuuuu)ay)".as_ptr() as *const gchar;
|
||||
let variant_type = g_variant_type_new(type_string);
|
||||
|
||||
let root_variant = g_variant_new_from_data(
|
||||
variant_type,
|
||||
config.as_ptr() as *const core::ffi::c_void,
|
||||
config.len() as gsize,
|
||||
1,
|
||||
None,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
|
||||
let transport_kind_variant = g_variant_get_child_value(root_variant, 1);
|
||||
let transport_kind = g_variant_get_byte(transport_kind_variant);
|
||||
|
||||
let transport_cfg_outer = g_variant_get_child_value(root_variant, 2);
|
||||
let transport_cfg_inner = g_variant_get_variant(transport_cfg_outer);
|
||||
let transport_config = match transport_kind {
|
||||
0 => {
|
||||
let mmio_variant = g_variant_get_child_value(transport_cfg_inner, 0);
|
||||
let mmio = g_variant_get_uint64(mmio_variant);
|
||||
let irq_variant = g_variant_get_child_value(transport_cfg_inner, 1);
|
||||
let irq = g_variant_get_uint32(irq_variant);
|
||||
g_variant_unref(irq_variant);
|
||||
g_variant_unref(mmio_variant);
|
||||
TransportConfig::Virtio { mmio, irq }
|
||||
}
|
||||
1 => {
|
||||
let host_port = g_variant_get_uint32(transport_cfg_inner);
|
||||
TransportConfig::Vsock { host_port }
|
||||
}
|
||||
_ => panic!("Unsupported transport kind: {}", transport_kind),
|
||||
};
|
||||
g_variant_unref(transport_cfg_inner);
|
||||
g_variant_unref(transport_cfg_outer);
|
||||
g_variant_unref(transport_kind_variant);
|
||||
|
||||
let kernel_base_variant = g_variant_get_child_value(root_variant, 3);
|
||||
let kernel_base = g_variant_get_uint64(kernel_base_variant);
|
||||
xnu::set_kernel_base(kernel_base);
|
||||
|
||||
let module_info_variant = g_variant_get_child_value(root_variant, 4);
|
||||
let mut iter: GVariantIter = core::mem::zeroed();
|
||||
g_variant_iter_init(&mut iter as *mut GVariantIter, module_info_variant);
|
||||
|
||||
let mut module_info: Vec<ModuleInfo> = Vec::new();
|
||||
let mut raw_name: *mut gchar = null_mut();
|
||||
let mut raw_version: *mut gchar = null_mut();
|
||||
let mut offset: u32 = 0;
|
||||
let mut size: u32 = 0;
|
||||
let mut start_func_offset: u32 = 0;
|
||||
let mut stop_func_offset: u32 = 0;
|
||||
while g_variant_iter_next(
|
||||
&mut iter as *mut GVariantIter,
|
||||
c"(&s&suuuu)".as_ptr(),
|
||||
&mut raw_name,
|
||||
&mut raw_version,
|
||||
&mut offset,
|
||||
&mut size,
|
||||
&mut start_func_offset,
|
||||
&mut stop_func_offset,
|
||||
) != 0
|
||||
{
|
||||
module_info.push(ModuleInfo {
|
||||
name: String::from(CStr::from_ptr(raw_name).to_str().unwrap()),
|
||||
version: String::from(CStr::from_ptr(raw_version).to_str().unwrap()),
|
||||
offset,
|
||||
size,
|
||||
start_func_offset,
|
||||
stop_func_offset,
|
||||
});
|
||||
}
|
||||
|
||||
let symbol_array_variant = g_variant_get_child_value(root_variant, 5);
|
||||
let symbol_data_ptr = g_variant_get_data(symbol_array_variant) as *const u8;
|
||||
let symbol_data_size = g_variant_get_size(symbol_array_variant) as usize;
|
||||
let symbol_table = SymbolTable::new(core::slice::from_raw_parts(
|
||||
symbol_data_ptr,
|
||||
symbol_data_size,
|
||||
));
|
||||
|
||||
let own_range_variant = g_variant_get_child_value(root_variant, 0);
|
||||
let mut own_base: u64 = 0;
|
||||
let mut own_size: u64 = 0;
|
||||
g_variant_get(
|
||||
own_range_variant,
|
||||
c"(tt)".as_ptr(),
|
||||
&mut own_base,
|
||||
&mut own_size,
|
||||
);
|
||||
let own_range = GumMemoryRange {
|
||||
base_address: own_base,
|
||||
size: own_size as gsize,
|
||||
};
|
||||
|
||||
g_variant_unref(own_range_variant);
|
||||
g_variant_unref(symbol_array_variant);
|
||||
g_variant_unref(module_info_variant);
|
||||
g_variant_unref(kernel_base_variant);
|
||||
g_variant_unref(root_variant);
|
||||
g_variant_type_free(variant_type);
|
||||
|
||||
(transport_config, kernel_base, module_info, symbol_table, own_range)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn serialize_message(variant: *mut GVariant) -> Option<Vec<u8>> {
|
||||
unsafe {
|
||||
let size = g_variant_get_size(variant) as usize;
|
||||
if size == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let data_ptr = g_variant_get_data(variant) as *const u8;
|
||||
let mut result = Vec::with_capacity(size);
|
||||
result.resize(size, 0);
|
||||
core::ptr::copy_nonoverlapping(data_ptr, result.as_mut_ptr(), size);
|
||||
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_message(data: &[u8]) -> Option<*mut GVariant> {
|
||||
unsafe {
|
||||
if data.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let variant_type = g_variant_type_new(c"(yqv)".as_ptr());
|
||||
let data_copy = g_memdup2(data.as_ptr() as *const c_void, data.len() as u64);
|
||||
let variant = g_variant_new_from_data(
|
||||
variant_type,
|
||||
data_copy,
|
||||
data.len() as u64,
|
||||
0,
|
||||
Some(g_free),
|
||||
data_copy,
|
||||
);
|
||||
g_variant_type_free(variant_type);
|
||||
|
||||
if variant.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(variant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_incoming_message(variant: *mut GVariant) {
|
||||
{
|
||||
let mut cmd_value: u8 = 0;
|
||||
let mut request_id: u16 = 0;
|
||||
let mut payload_variant: *mut GVariant = ptr::null_mut();
|
||||
|
||||
let cmd = unsafe {
|
||||
g_variant_get(
|
||||
variant,
|
||||
c"(yqv)".as_ptr(),
|
||||
&mut cmd_value,
|
||||
&mut request_id,
|
||||
&mut payload_variant,
|
||||
);
|
||||
|
||||
core::mem::transmute::<u8, FridaCommand>(cmd_value)
|
||||
};
|
||||
|
||||
if cmd == FridaCommand::Reply {
|
||||
unsafe {
|
||||
core::ptr::addr_of_mut!(PENDING_REPLIES)
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.insert(request_id, payload_variant);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let response = match cmd {
|
||||
FridaCommand::CreateScript => handle_create_script(payload_variant),
|
||||
FridaCommand::LoadScript => handle_load_script(payload_variant),
|
||||
FridaCommand::DestroyScript => handle_destroy_script(payload_variant),
|
||||
FridaCommand::PostScriptMessage => handle_post_script_message(payload_variant),
|
||||
_ => HandlerResponse::error("Unknown command"),
|
||||
};
|
||||
|
||||
send_command_reply(request_id, response);
|
||||
|
||||
unsafe { g_variant_unref(payload_variant) };
|
||||
}
|
||||
}
|
||||
|
||||
fn send_command_reply(request_id: u16, response: HandlerResponse) {
|
||||
unsafe {
|
||||
let message = g_variant_new(
|
||||
c"(yqv)".as_ptr(),
|
||||
FridaCommand::Reply as u8 as u32,
|
||||
request_id as u32,
|
||||
response.variant,
|
||||
);
|
||||
|
||||
if let Some(serialized) = serialize_message(message) {
|
||||
transport_get_unchecked().send(&serialized);
|
||||
}
|
||||
|
||||
g_variant_unref(message);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn host_rpc(command: FridaCommand, payload: *mut GVariant) -> *mut GVariant {
|
||||
unsafe {
|
||||
let request_id = NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed) as u16;
|
||||
let message = g_variant_new(
|
||||
c"(yqv)".as_ptr(),
|
||||
command as u8 as u32,
|
||||
request_id as u32,
|
||||
payload,
|
||||
);
|
||||
let transport = transport_get_unchecked();
|
||||
transport.send(&serialize_message(message).unwrap());
|
||||
g_variant_unref(message);
|
||||
|
||||
let wait_event = ptr::addr_of_mut!(glib::WAKEUP_TOKEN) as *const u8;
|
||||
loop {
|
||||
xnu::assert_wait(wait_event, xnu::THREAD_INTERRUPTIBLE);
|
||||
transport.process();
|
||||
if let Some(reply) = take_pending_reply(request_id) {
|
||||
xnu::thread_wakeup(wait_event);
|
||||
return reply;
|
||||
}
|
||||
xnu::thread_block(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn take_pending_reply(request_id: u16) -> Option<*mut GVariant> {
|
||||
unsafe {
|
||||
core::ptr::addr_of_mut!(PENDING_REPLIES)
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.remove(&request_id)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_create_script(payload_variant: *mut GVariant) -> HandlerResponse {
|
||||
unsafe {
|
||||
if g_variant_check_format_string(payload_variant, c"s".as_ptr(), 0) == 0 {
|
||||
return HandlerResponse::error("Invalid payload format: expected string");
|
||||
}
|
||||
let source = g_variant_get_string(payload_variant, core::ptr::null_mut());
|
||||
|
||||
let backend = gum_script_backend_obtain_qjs();
|
||||
let cancellable: *mut GCancellable = ptr::null_mut();
|
||||
let mut error: *mut GError = ptr::null_mut();
|
||||
|
||||
let script = gum_script_backend_create_sync(
|
||||
backend,
|
||||
c"agent.js".as_ptr(),
|
||||
source,
|
||||
ptr::null_mut(),
|
||||
cancellable,
|
||||
&mut error,
|
||||
);
|
||||
|
||||
if !error.is_null() {
|
||||
let error_msg = CStr::from_ptr((*error).message).to_str().unwrap();
|
||||
let error_string = String::from(error_msg);
|
||||
g_error_free(error);
|
||||
return HandlerResponse::error(&error_string);
|
||||
}
|
||||
|
||||
exclude_own_range_from_stalker(script);
|
||||
|
||||
let script_id = NEXT_SCRIPT_ID.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let script_id_ptr = Box::into_raw(Box::new(script_id));
|
||||
gum_script_set_message_handler(
|
||||
script,
|
||||
Some(frida_message_handler),
|
||||
script_id_ptr as *mut c_void,
|
||||
None,
|
||||
);
|
||||
|
||||
core::ptr::addr_of_mut!(SCRIPTS)
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.insert(script_id, script);
|
||||
|
||||
HandlerResponse::success(g_variant_new_uint32(script_id))
|
||||
}
|
||||
}
|
||||
|
||||
// Stalker must not instrument our own runtime: when it follows the current
|
||||
// thread into the hostlink transport it would deadlock on the locks that the
|
||||
// RPC it depends on is holding. The host injected the image, so it tells us our
|
||||
// range as part of the config.
|
||||
unsafe fn exclude_own_range_from_stalker(script: *mut GumScript) {
|
||||
unsafe {
|
||||
gum_stalker_exclude(gum_script_get_stalker(script), core::ptr::addr_of!(OWN_RANGE));
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn frida_message_handler(
|
||||
message: *const gchar,
|
||||
_data: *mut GBytes,
|
||||
user_data: gpointer,
|
||||
) {
|
||||
unsafe {
|
||||
let script_id = *(user_data as *const u32);
|
||||
|
||||
let message_variant = g_variant_new(
|
||||
c"(yqv)".as_ptr(),
|
||||
FridaCommand::ScriptMessage as u8 as u32,
|
||||
0u32,
|
||||
g_variant_new(c"(us)".as_ptr(), script_id, message),
|
||||
);
|
||||
|
||||
if let Some(serialized) = serialize_message(message_variant) {
|
||||
transport_get_unchecked().send(&serialized);
|
||||
}
|
||||
|
||||
g_variant_unref(message_variant);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_load_script(payload_variant: *mut GVariant) -> HandlerResponse {
|
||||
unsafe {
|
||||
if g_variant_check_format_string(payload_variant, c"u".as_ptr(), 0) == 0 {
|
||||
return HandlerResponse::error("Invalid payload format: expected uint32");
|
||||
}
|
||||
let script_id = g_variant_get_uint32(payload_variant);
|
||||
|
||||
let Some(script) = get_script_by_id(script_id) else {
|
||||
return HandlerResponse::error(&format!("Script with ID {} not found", script_id));
|
||||
};
|
||||
|
||||
gum_script_load_sync(script, ptr::null_mut());
|
||||
|
||||
HandlerResponse::success_empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_destroy_script(payload_variant: *mut GVariant) -> HandlerResponse {
|
||||
unsafe {
|
||||
if g_variant_check_format_string(payload_variant, c"u".as_ptr(), 0) == 0 {
|
||||
return HandlerResponse::error("Invalid payload format: expected uint32");
|
||||
}
|
||||
let script_id = g_variant_get_uint32(payload_variant);
|
||||
|
||||
let scripts = core::ptr::addr_of_mut!(SCRIPTS).as_mut().unwrap();
|
||||
let Some(script) = scripts.remove(&script_id) else {
|
||||
return HandlerResponse::error(&format!("Script with ID {} not found", script_id));
|
||||
};
|
||||
|
||||
gum_script_unload_sync(script, ptr::null_mut());
|
||||
g_object_unref(script as *mut c_void);
|
||||
|
||||
HandlerResponse::success_empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_post_script_message(payload_variant: *mut GVariant) -> HandlerResponse {
|
||||
unsafe {
|
||||
if g_variant_check_format_string(payload_variant, c"(us)".as_ptr(), 0) == 0 {
|
||||
return HandlerResponse::error("Invalid payload format: expected (uint32, string)");
|
||||
}
|
||||
let mut script_id: u32 = 0;
|
||||
let mut message: *const gchar = ptr::null();
|
||||
g_variant_get(
|
||||
payload_variant,
|
||||
c"(u&s)".as_ptr(),
|
||||
&mut script_id,
|
||||
&mut message,
|
||||
);
|
||||
|
||||
let Some(script) = get_script_by_id(script_id) else {
|
||||
return HandlerResponse::error(&format!("Script with ID {} not found", script_id));
|
||||
};
|
||||
|
||||
gum_script_post(script, message, ptr::null_mut());
|
||||
|
||||
HandlerResponse::success_empty()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_script_by_id(script_id: u32) -> Option<*mut GumScript> {
|
||||
unsafe {
|
||||
let scripts = core::ptr::addr_of_mut!(SCRIPTS).as_mut().unwrap();
|
||||
scripts.get(&script_id).copied()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn frida_panic_handler(message: *const u8, _user_data: *mut core::ffi::c_void) {
|
||||
let msg = unsafe { core::ffi::CStr::from_ptr(message).to_str().unwrap() };
|
||||
panic!("[Frida] {}", msg);
|
||||
}
|
||||
|
||||
unsafe extern "C" fn frida_log_handler(
|
||||
_log_domain: *const core::ffi::c_char,
|
||||
_log_level: i32,
|
||||
message: *const core::ffi::c_char,
|
||||
_user_data: *mut core::ffi::c_void,
|
||||
) {
|
||||
let msg = unsafe { core::ffi::CStr::from_ptr(message).to_str().unwrap() };
|
||||
kprintln!("[Frida] {}", msg);
|
||||
}
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(info: &core::panic::PanicInfo<'_>) -> ! {
|
||||
let mut s = format!("{}", info);
|
||||
s.push('\0');
|
||||
xnu::panic(s.as_str());
|
||||
loop {}
|
||||
}
|
||||
|
||||
pub struct XnuAllocator;
|
||||
|
||||
unsafe impl GlobalAlloc for XnuAllocator {
|
||||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||
xnu::kalloc(layout.size())
|
||||
}
|
||||
|
||||
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||
xnu::free(ptr, layout.size());
|
||||
}
|
||||
}
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: XnuAllocator = XnuAllocator;
|
||||
extern crate alloc;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! kprintln {
|
||||
($($arg:tt)*) => {{
|
||||
use core::fmt::Write;
|
||||
let mut buf = alloc::string::String::new();
|
||||
write!(&mut buf, $($arg)*).unwrap();
|
||||
buf.push('\n');
|
||||
buf.push('\0');
|
||||
crate::xnu::io_log(&buf)
|
||||
}};
|
||||
}
|
||||
use frida_barebone_agent as _;
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::bindings::{
|
||||
GPatternSpec, g_pattern_spec_free, g_pattern_spec_match_string, g_pattern_spec_new,
|
||||
};
|
||||
use crate::xnu::get_kernel_base;
|
||||
use crate::kernel::get_kernel_base;
|
||||
use alloc::ffi::CString;
|
||||
use core::cmp::Ordering;
|
||||
use core::ffi::{CStr, c_char};
|
||||
|
||||
@@ -0,0 +1,922 @@
|
||||
// The VMM side of the blob flavor. Reached the way XNU is: the host fills in a
|
||||
// slot per service before calling _start, so nothing here is a link-time import.
|
||||
//
|
||||
// The signatures follow the VxD service documentation; none of them has been
|
||||
// exercised against a running kernel yet.
|
||||
|
||||
use core::ffi::c_void;
|
||||
use core::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use crate::kernel::ThreadEntry;
|
||||
|
||||
const PAGE_SIZE: u32 = 4096;
|
||||
|
||||
const PG_SYS: u32 = 1;
|
||||
const PAGE_FLAGS_CODE: u32 = PAGE_FIXED | PAGE_LOCKED | PAGE_ZERO_INIT;
|
||||
const PAGE_FIXED: u32 = 0x8;
|
||||
const PAGE_LOCKED: u32 = 0x80;
|
||||
const PAGE_ZERO_INIT: u32 = 0x1;
|
||||
|
||||
const PAGE_PRESENT: u32 = 0x1;
|
||||
const PAGE_WRITEABLE: u32 = 0x2;
|
||||
const GUM_PAGE_WRITE: u32 = 0x2;
|
||||
|
||||
const BLOCK_SVC_INTS: u32 = 1 << 0;
|
||||
|
||||
const DEBUG_CONSOLE_PORT: u16 = 0xe9;
|
||||
|
||||
pub fn log(msg: &str) {
|
||||
for byte in msg.bytes() {
|
||||
if byte == 0 {
|
||||
break;
|
||||
}
|
||||
write_debug_byte(byte);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_hex(value: u32) {
|
||||
let mut shift = 32;
|
||||
while shift != 0 {
|
||||
shift -= 4;
|
||||
let digit = ((value >> shift) & 0xf) as u8;
|
||||
write_debug_byte(if digit < 10 { b'0' + digit } else { b'a' + digit - 10 });
|
||||
}
|
||||
write_debug_byte(b'\n');
|
||||
}
|
||||
|
||||
fn write_debug_byte(byte: u8) {
|
||||
unsafe {
|
||||
core::arch::asm!("out dx, al", in("dx") DEBUG_CONSOLE_PORT, in("al") byte,
|
||||
options(nomem, nostack, preserves_flags));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn panic(msg: &str) {
|
||||
log(msg);
|
||||
unsafe {
|
||||
fatal_error_handler(msg.as_ptr(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alloc(size: usize) -> *mut u8 {
|
||||
unsafe { __HeapAllocate(size as u32, 0) }
|
||||
}
|
||||
|
||||
pub fn free(ptr: *mut u8, _size: usize) {
|
||||
unsafe {
|
||||
__HeapFree(ptr, 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alloc_code(size: usize) -> *mut u8 {
|
||||
let pages = size.div_ceil(PAGE_SIZE as usize) as u32;
|
||||
unsafe { __PageAllocate(pages, PG_SYS, 0, 0, 0, 0, core::ptr::null_mut(), PAGE_FLAGS_CODE) }
|
||||
}
|
||||
|
||||
pub fn free_code(ptr: *mut u8, _size: usize) {
|
||||
unsafe {
|
||||
__PageFree(ptr, 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_thread(entry: ThreadEntry, parameter: *mut c_void) -> isize {
|
||||
let stack = alloc_code(THREAD_STACK_SIZE);
|
||||
if stack.is_null() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
THREAD_ENTRY = Some(entry);
|
||||
THREAD_PARAMETER = parameter;
|
||||
|
||||
vmm_create_thread(
|
||||
segment_ss(),
|
||||
stack as u32 + THREAD_STACK_SIZE as u32,
|
||||
segment_cs(),
|
||||
frida_win9x_thread_thunk as u32,
|
||||
segment_ds(),
|
||||
segment_es(),
|
||||
0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// VMM refuses to build a thread from the borrowed context the host enters us on, and only
|
||||
// says so by never returning, so hand the work to a point where VMM is between jobs.
|
||||
pub fn run_when_ready(action: fn()) {
|
||||
unsafe {
|
||||
READY_ACTION = Some(action);
|
||||
schedule_global_event(frida_win9x_event_thunk);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
extern "C" fn frida_win9x_on_event() {
|
||||
if let Some(action) = unsafe { READY_ACTION } {
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
static mut READY_ACTION: Option<fn()> = None;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
extern "C" fn frida_win9x_thread_start() {
|
||||
unsafe {
|
||||
if let Some(entry) = THREAD_ENTRY {
|
||||
entry(THREAD_PARAMETER, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static mut THREAD_ENTRY: Option<ThreadEntry> = None;
|
||||
static mut THREAD_PARAMETER: *mut c_void = core::ptr::null_mut();
|
||||
|
||||
const THREAD_STACK_SIZE: usize = 64 * 1024;
|
||||
|
||||
fn segment_cs() -> u32 {
|
||||
let selector: u32;
|
||||
unsafe { core::arch::asm!("mov {0:e}, cs", out(reg) selector, options(nomem, nostack, preserves_flags)) };
|
||||
selector
|
||||
}
|
||||
|
||||
fn segment_ss() -> u32 {
|
||||
let selector: u32;
|
||||
unsafe { core::arch::asm!("mov {0:e}, ss", out(reg) selector, options(nomem, nostack, preserves_flags)) };
|
||||
selector
|
||||
}
|
||||
|
||||
fn segment_ds() -> u32 {
|
||||
let selector: u32;
|
||||
unsafe { core::arch::asm!("mov {0:e}, ds", out(reg) selector, options(nomem, nostack, preserves_flags)) };
|
||||
selector
|
||||
}
|
||||
|
||||
fn segment_es() -> u32 {
|
||||
let selector: u32;
|
||||
unsafe { core::arch::asm!("mov {0:e}, es", out(reg) selector, options(nomem, nostack, preserves_flags)) };
|
||||
selector
|
||||
}
|
||||
|
||||
pub fn wait(token: *const u8, timeout_us: Option<u64>, check: &mut dyn FnMut() -> bool) {
|
||||
let semaphore = semaphore_for(token);
|
||||
if check() {
|
||||
return;
|
||||
}
|
||||
|
||||
match timeout_us {
|
||||
None => unsafe { wait_semaphore(semaphore, BLOCK_SVC_INTS) },
|
||||
Some(us) => block_until_signalled_or_timed_out(semaphore, us),
|
||||
}
|
||||
}
|
||||
|
||||
fn block_until_signalled_or_timed_out(semaphore: u32, timeout_us: u64) {
|
||||
let timeout_ms = (timeout_us / 1000).max(1) as u32;
|
||||
let timeout = unsafe { set_global_time_out(timeout_ms, semaphore) };
|
||||
unsafe {
|
||||
wait_semaphore(semaphore, BLOCK_SVC_INTS);
|
||||
}
|
||||
if timeout != 0 {
|
||||
unsafe {
|
||||
cancel_time_out(timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wake(token: *const u8) {
|
||||
unsafe {
|
||||
signal_semaphore(semaphore_for(token));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn yield_now() {
|
||||
unsafe {
|
||||
_Release_Time_Slice();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn monotonic_micros() -> i64 {
|
||||
unsafe { _Get_System_Time() as i64 * 1000 }
|
||||
}
|
||||
|
||||
pub fn wall_clock_micros() -> (u32, u32) {
|
||||
let micros = monotonic_micros() as u64;
|
||||
((micros / 1_000_000) as u32, (micros % 1_000_000) as u32)
|
||||
}
|
||||
|
||||
pub fn current_thread_id() -> u64 {
|
||||
unsafe { get_cur_thread_handle() as u64 }
|
||||
}
|
||||
|
||||
pub fn protect(address: u64, size: usize, gum_prot: u32) -> bool {
|
||||
let first_page = address / PAGE_SIZE as u64;
|
||||
let pages = size.div_ceil(PAGE_SIZE as usize) as u32;
|
||||
|
||||
let mut set = PAGE_PRESENT;
|
||||
if (gum_prot & GUM_PAGE_WRITE) != 0 {
|
||||
set |= PAGE_WRITEABLE;
|
||||
}
|
||||
let clear = PAGE_WRITEABLE & !set;
|
||||
|
||||
unsafe { __PageModifyPermissions(first_page as u32, pages, !clear, set) != 0xffff_ffff }
|
||||
}
|
||||
|
||||
pub fn map_io(phys_addr: u64, size: u64) -> *mut c_void {
|
||||
let pages = (size as usize).div_ceil(PAGE_SIZE as usize) as u32;
|
||||
unsafe { __MapPhysToLinear(phys_addr as u32, pages * PAGE_SIZE, 0) as *mut c_void }
|
||||
}
|
||||
|
||||
pub fn virt_to_phys(vaddr: u64) -> u64 {
|
||||
let mut entry: u32 = 0;
|
||||
let page = (vaddr / PAGE_SIZE as u64) as u32;
|
||||
unsafe {
|
||||
__CopyPageTable(page, 1, &mut entry, 0);
|
||||
}
|
||||
((entry & !(PAGE_SIZE - 1)) as u64) | (vaddr & (PAGE_SIZE - 1) as u64)
|
||||
}
|
||||
|
||||
pub fn install_interrupt_handler(
|
||||
irq: u32,
|
||||
target: *mut c_void,
|
||||
handler: InterruptHandler,
|
||||
refcon: *mut c_void,
|
||||
) -> i32 {
|
||||
unsafe {
|
||||
HW_INT_HANDLER = Some(handler);
|
||||
HW_INT_TARGET = target;
|
||||
HW_INT_REFCON = refcon;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
IRQ_DESCRIPTOR = VpicdIrqDescriptor {
|
||||
irq_number: irq as u16,
|
||||
options: VPICD_OPT_REF_DATA | VPICD_OPT_CAN_SHARE,
|
||||
hw_int_proc: frida_win9x_hw_int_thunk,
|
||||
virt_int_proc: 0,
|
||||
eoi_proc: 0,
|
||||
mask_change_proc: 0,
|
||||
iret_proc: 0,
|
||||
iret_time_out: VPICD_DEFAULT_IRET_TIME_OUT,
|
||||
hw_int_ref: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let handle = unsafe { vpicd_virtualize_irq(core::ptr::addr_of_mut!(IRQ_DESCRIPTOR)) };
|
||||
if handle != 0 {
|
||||
unsafe {
|
||||
IRQ_HANDLE = handle;
|
||||
vpicd_physically_unmask(handle);
|
||||
}
|
||||
}
|
||||
if handle == 0 { -1 } else { 0 }
|
||||
}
|
||||
|
||||
pub struct ProcessInfo {
|
||||
pub id: u32,
|
||||
pub path: *const u8,
|
||||
}
|
||||
|
||||
// Win32 threads carry the process they belong to in VWIN32's per-thread block, so the
|
||||
// process list is the deduplicated set of those, and the image path is where the command
|
||||
// line starts.
|
||||
pub fn enumerate_processes(found: &mut dyn FnMut(ProcessInfo)) {
|
||||
let slot = unsafe { (0xc00211ccu32 as *const u32).read() };
|
||||
let vm = unsafe { get_sys_vm_handle() };
|
||||
let first = unsafe { get_initial_thread_handle(vm) };
|
||||
|
||||
let mut seen: [u32; 64] = [0; 64];
|
||||
let mut count = 0usize;
|
||||
let mut thread = first;
|
||||
while thread != 0 && count < seen.len() {
|
||||
if unsafe { (thread as *const u32).add(0x2c / 4).read() } == WIN32_THREAD {
|
||||
let block = unsafe { (thread as *const u32).byte_add(slot as usize).read() };
|
||||
let pdb = unsafe { (block as *const u32).add(1).read() };
|
||||
if !seen[..count].contains(&pdb) {
|
||||
seen[count] = pdb;
|
||||
count += 1;
|
||||
found(ProcessInfo {
|
||||
id: pdb,
|
||||
path: image_path(pdb),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let next = unsafe { get_next_thread_handle(thread) };
|
||||
thread = if next == first { 0 } else { next };
|
||||
}
|
||||
}
|
||||
|
||||
fn image_path(pdb: u32) -> *const u8 {
|
||||
let env_db = unsafe { (pdb as *const u32).byte_add(0x40).read() };
|
||||
if env_db < ARENA_FLOOR {
|
||||
return core::ptr::null();
|
||||
}
|
||||
|
||||
let command_line = unsafe { (env_db as *const u32).byte_add(0x08).read() };
|
||||
if command_line < ARENA_FLOOR || unsafe { (command_line as *const u8).add(1).read() } != b':' {
|
||||
return core::ptr::null();
|
||||
}
|
||||
|
||||
command_line as *const u8
|
||||
}
|
||||
|
||||
const WIN32_THREAD: u32 = 0x2a;
|
||||
const ARENA_FLOOR: u32 = 0x10000;
|
||||
|
||||
pub fn install_fault_reporter() {
|
||||
unsafe {
|
||||
FAULT_CHAIN[INVALID_OPCODE as usize] = hook_vmm_fault(INVALID_OPCODE, frida_win9x_fault_thunk_ud);
|
||||
FAULT_CHAIN[GENERAL_PROTECTION as usize] =
|
||||
hook_vmm_fault(GENERAL_PROTECTION, frida_win9x_fault_thunk_gp);
|
||||
FAULT_CHAIN[PAGE_FAULT as usize] = hook_vmm_fault(PAGE_FAULT, frida_win9x_fault_thunk_pf);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
extern "C" fn frida_win9x_on_fault(fault: u32, frame: *mut u32) -> u32 {
|
||||
let mut cpu_context = unsafe {
|
||||
crate::bindings::_GumIA32CpuContext {
|
||||
eip: frame.add(fault_frame_eip_slot(fault)).read(),
|
||||
edi: frame.read(),
|
||||
esi: frame.add(1).read(),
|
||||
ebp: frame.add(2).read(),
|
||||
esp: frame.add(3).read(),
|
||||
ebx: frame.add(4).read(),
|
||||
edx: frame.add(5).read(),
|
||||
ecx: frame.add(6).read(),
|
||||
eax: frame.add(7).read(),
|
||||
xmm: core::ptr::null_mut(),
|
||||
}
|
||||
};
|
||||
|
||||
let handled = unsafe {
|
||||
crate::bindings::gum_barebone_handle_exception(
|
||||
exception_type_for(fault),
|
||||
cpu_context.eip as *mut c_void,
|
||||
faulting_address() as *mut c_void,
|
||||
&mut cpu_context,
|
||||
)
|
||||
};
|
||||
if handled == 0 {
|
||||
return unsafe { FAULT_CHAIN[fault as usize] };
|
||||
}
|
||||
|
||||
unsafe {
|
||||
frida_win9x_resume = [
|
||||
cpu_context.eip,
|
||||
cpu_context.edi,
|
||||
cpu_context.esi,
|
||||
cpu_context.ebp,
|
||||
cpu_context.esp,
|
||||
cpu_context.ebx,
|
||||
cpu_context.edx,
|
||||
cpu_context.ecx,
|
||||
cpu_context.eax,
|
||||
];
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
static mut frida_win9x_resume: [u32; 9] = [0; 9];
|
||||
|
||||
fn exception_type_for(fault: u32) -> crate::bindings::GumExceptionType {
|
||||
use crate::bindings::*;
|
||||
|
||||
match fault {
|
||||
DIVIDE_ERROR | OVERFLOW | BOUND_RANGE_EXCEEDED | X87_FLOATING_POINT
|
||||
| SIMD_FLOATING_POINT => _GumExceptionType_GUM_EXCEPTION_ARITHMETIC,
|
||||
DEBUG => _GumExceptionType_GUM_EXCEPTION_SINGLE_STEP,
|
||||
BREAKPOINT => _GumExceptionType_GUM_EXCEPTION_BREAKPOINT,
|
||||
INVALID_OPCODE => _GumExceptionType_GUM_EXCEPTION_ILLEGAL_INSTRUCTION,
|
||||
STACK_SEGMENT_FAULT => _GumExceptionType_GUM_EXCEPTION_STACK_OVERFLOW,
|
||||
GENERAL_PROTECTION | PAGE_FAULT => _GumExceptionType_GUM_EXCEPTION_ACCESS_VIOLATION,
|
||||
_ => _GumExceptionType_GUM_EXCEPTION_SYSTEM,
|
||||
}
|
||||
}
|
||||
|
||||
const DIVIDE_ERROR: u32 = 0;
|
||||
const DEBUG: u32 = 1;
|
||||
const BREAKPOINT: u32 = 3;
|
||||
const OVERFLOW: u32 = 4;
|
||||
const BOUND_RANGE_EXCEEDED: u32 = 5;
|
||||
const STACK_SEGMENT_FAULT: u32 = 12;
|
||||
const X87_FLOATING_POINT: u32 = 16;
|
||||
const SIMD_FLOATING_POINT: u32 = 19;
|
||||
|
||||
fn fault_frame_eip_slot(fault: u32) -> usize {
|
||||
const PUSHED_REGISTERS: usize = 8;
|
||||
const VECTOR: usize = 1;
|
||||
|
||||
let error_code = match fault {
|
||||
8 | 10 | 11 | 12 | 13 | 14 | 17 | 21 => 1,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
PUSHED_REGISTERS + VECTOR + error_code
|
||||
}
|
||||
|
||||
fn faulting_address() -> u32 {
|
||||
let address: u32;
|
||||
unsafe { core::arch::asm!("mov {0:e}, cr2", out(reg) address, options(nomem, nostack, preserves_flags)) };
|
||||
address
|
||||
}
|
||||
|
||||
static mut FAULT_CHAIN: [u32; 32] = [0; 32];
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
static mut frida_win9x_fault_chain: u32 = 0;
|
||||
|
||||
const INVALID_OPCODE: u32 = 6;
|
||||
const GENERAL_PROTECTION: u32 = 13;
|
||||
const PAGE_FAULT: u32 = 14;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
extern "C" fn frida_win9x_on_hw_int(_ref_data: *mut c_void) {
|
||||
unsafe {
|
||||
if let Some(handler) = HW_INT_HANDLER {
|
||||
handler(HW_INT_TARGET, HW_INT_REFCON, core::ptr::null_mut(), 0);
|
||||
}
|
||||
vpicd_phys_eoi(IRQ_HANDLE);
|
||||
}
|
||||
}
|
||||
|
||||
static mut IRQ_HANDLE: u32 = 0;
|
||||
|
||||
static mut IRQ_DESCRIPTOR: VpicdIrqDescriptor = VpicdIrqDescriptor {
|
||||
irq_number: 0,
|
||||
options: 0,
|
||||
hw_int_proc: frida_win9x_hw_int_thunk,
|
||||
virt_int_proc: 0,
|
||||
eoi_proc: 0,
|
||||
mask_change_proc: 0,
|
||||
iret_proc: 0,
|
||||
iret_time_out: 0,
|
||||
hw_int_ref: 0,
|
||||
};
|
||||
|
||||
static mut HW_INT_HANDLER: Option<InterruptHandler> = None;
|
||||
static mut HW_INT_TARGET: *mut c_void = core::ptr::null_mut();
|
||||
static mut HW_INT_REFCON: *mut c_void = core::ptr::null_mut();
|
||||
|
||||
#[repr(C)]
|
||||
struct VpicdIrqDescriptor {
|
||||
irq_number: u16,
|
||||
options: u16,
|
||||
hw_int_proc: unsafe extern "C" fn(),
|
||||
virt_int_proc: u32,
|
||||
eoi_proc: u32,
|
||||
mask_change_proc: u32,
|
||||
iret_proc: u32,
|
||||
iret_time_out: u32,
|
||||
hw_int_ref: u32,
|
||||
}
|
||||
|
||||
const VPICD_OPT_CAN_SHARE: u16 = 0x02;
|
||||
const VPICD_OPT_REF_DATA: u16 = 0x04;
|
||||
const VPICD_DEFAULT_IRET_TIME_OUT: u32 = 500;
|
||||
|
||||
pub type InterruptHandler =
|
||||
unsafe extern "C" fn(target: *mut c_void, refcon: *mut c_void, nub: *mut c_void, source: i32);
|
||||
|
||||
pub fn get_kernel_base() -> u64 {
|
||||
KERNEL_BASE.load(Ordering::Relaxed) as u64
|
||||
}
|
||||
|
||||
pub fn set_kernel_base(base: u64) {
|
||||
KERNEL_BASE.store(base as u32, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
static KERNEL_BASE: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
// One semaphore per waited-on address, created on first use. VMM has no
|
||||
// futex-alike, so the token has to be mapped onto something it does have.
|
||||
fn semaphore_for(token: *const u8) -> u32 {
|
||||
let slot = (token as usize / core::mem::align_of::<usize>()) % SEMAPHORES.len();
|
||||
let existing = SEMAPHORES[slot].load(Ordering::Acquire);
|
||||
if existing != 0 {
|
||||
return existing;
|
||||
}
|
||||
|
||||
let created = unsafe { create_semaphore(0) };
|
||||
match SEMAPHORES[slot].compare_exchange(0, created, Ordering::AcqRel, Ordering::Acquire) {
|
||||
Ok(_) => created,
|
||||
Err(raced) => raced,
|
||||
}
|
||||
}
|
||||
|
||||
const NUM_SEMAPHORES: usize = 64;
|
||||
static SEMAPHORES: [AtomicU32; NUM_SEMAPHORES] = [const { AtomicU32::new(0) }; NUM_SEMAPHORES];
|
||||
|
||||
unsafe extern "C" {
|
||||
fn wait_semaphore(semaphore: u32, flags: u32);
|
||||
fn signal_semaphore(semaphore: u32);
|
||||
fn create_semaphore(token_count: u32) -> u32;
|
||||
fn get_cur_thread_handle() -> u32;
|
||||
fn fatal_error_handler(message: *const u8, flags: u32);
|
||||
fn vpicd_virtualize_irq(descriptor: *mut VpicdIrqDescriptor) -> u32;
|
||||
fn vpicd_physically_unmask(handle: u32);
|
||||
fn vpicd_phys_eoi(handle: u32);
|
||||
fn set_global_time_out(milliseconds: u32, semaphore: u32) -> u32;
|
||||
fn cancel_time_out(timeout: u32);
|
||||
fn hook_vmm_fault(fault: u32, handler: unsafe extern "C" fn()) -> u32;
|
||||
fn frida_win9x_fault_thunk_ud();
|
||||
fn frida_win9x_fault_thunk_gp();
|
||||
fn frida_win9x_fault_thunk_pf();
|
||||
fn frida_win9x_time_out_thunk();
|
||||
fn get_cur_vm_handle() -> u32;
|
||||
fn get_sys_vm_handle() -> u32;
|
||||
fn schedule_global_event(callback: unsafe extern "C" fn()) -> u32;
|
||||
fn frida_win9x_event_thunk();
|
||||
fn get_next_vm_handle(vm: u32) -> u32;
|
||||
fn get_initial_thread_handle(vm: u32) -> u32;
|
||||
fn get_next_thread_handle(thread: u32) -> u32;
|
||||
fn frida_win9x_hw_int_thunk();
|
||||
fn frida_win9x_thread_thunk();
|
||||
fn vmm_create_thread(ss: u32, esp: u32, cs: u32, eip: u32, ds: u32, es: u32,
|
||||
ref_data: u32) -> isize;
|
||||
}
|
||||
|
||||
core::arch::global_asm!(
|
||||
r#"
|
||||
.intel_syntax noprefix
|
||||
|
||||
.global wait_semaphore
|
||||
wait_semaphore:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push esi
|
||||
push edi
|
||||
mov eax, [ebp + 8]
|
||||
mov ecx, [ebp + 12]
|
||||
call dword ptr [_Wait_Semaphore]
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global signal_semaphore
|
||||
signal_semaphore:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push esi
|
||||
push edi
|
||||
mov eax, [ebp + 8]
|
||||
call dword ptr [_Signal_Semaphore]
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global create_semaphore
|
||||
create_semaphore:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push esi
|
||||
push edi
|
||||
mov ecx, [ebp + 8]
|
||||
call dword ptr [_Create_Semaphore]
|
||||
cmc
|
||||
sbb ecx, ecx
|
||||
and eax, ecx
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global get_cur_thread_handle
|
||||
get_cur_thread_handle:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push esi
|
||||
push edi
|
||||
call dword ptr [_Get_Cur_Thread_Handle]
|
||||
mov eax, edi
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global fatal_error_handler
|
||||
fatal_error_handler:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push esi
|
||||
push edi
|
||||
mov esi, [ebp + 8]
|
||||
mov eax, [ebp + 12]
|
||||
call dword ptr [_Fatal_Error_Handler]
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global schedule_global_event
|
||||
schedule_global_event:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push esi
|
||||
push edi
|
||||
mov esi, [ebp + 8]
|
||||
call dword ptr [_Schedule_Global_Event]
|
||||
mov eax, ebx
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global frida_win9x_event_thunk
|
||||
frida_win9x_event_thunk:
|
||||
pushad
|
||||
call frida_win9x_on_event
|
||||
popad
|
||||
ret
|
||||
|
||||
.global get_sys_vm_handle
|
||||
get_sys_vm_handle:
|
||||
push ebx
|
||||
call dword ptr [_Get_Sys_VM_Handle]
|
||||
mov eax, ebx
|
||||
pop ebx
|
||||
ret
|
||||
|
||||
.global get_next_vm_handle
|
||||
get_next_vm_handle:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
mov ebx, [ebp + 8]
|
||||
call dword ptr [_Get_Next_VM_Handle]
|
||||
mov eax, ebx
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global get_initial_thread_handle
|
||||
get_initial_thread_handle:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push edi
|
||||
mov ebx, [ebp + 8]
|
||||
call dword ptr [_Get_Initial_Thread_Handle]
|
||||
mov eax, edi
|
||||
pop edi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global get_next_thread_handle
|
||||
get_next_thread_handle:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push edi
|
||||
mov edi, [ebp + 8]
|
||||
call dword ptr [_Get_Next_Thread_Handle]
|
||||
mov eax, edi
|
||||
pop edi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global get_cur_vm_handle
|
||||
get_cur_vm_handle:
|
||||
push ebx
|
||||
call dword ptr [_Get_Cur_VM_Handle]
|
||||
mov eax, ebx
|
||||
pop ebx
|
||||
ret
|
||||
|
||||
.global hook_vmm_fault
|
||||
hook_vmm_fault:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push esi
|
||||
push edi
|
||||
mov eax, [ebp + 8]
|
||||
mov esi, [ebp + 12]
|
||||
call dword ptr [_Hook_VMM_Fault]
|
||||
mov eax, esi
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global frida_win9x_fault_thunk_ud
|
||||
frida_win9x_fault_thunk_ud:
|
||||
push 6
|
||||
jmp frida_win9x_fault_common
|
||||
|
||||
.global frida_win9x_fault_thunk_gp
|
||||
frida_win9x_fault_thunk_gp:
|
||||
push 13
|
||||
jmp frida_win9x_fault_common
|
||||
|
||||
.global frida_win9x_fault_thunk_pf
|
||||
frida_win9x_fault_thunk_pf:
|
||||
push 14
|
||||
jmp frida_win9x_fault_common
|
||||
|
||||
frida_win9x_fault_common:
|
||||
pushad
|
||||
mov eax, [esp + 32]
|
||||
push esp
|
||||
push eax
|
||||
call frida_win9x_on_fault
|
||||
add esp, 8
|
||||
mov [frida_win9x_fault_chain], eax
|
||||
popad
|
||||
add esp, 4
|
||||
cmp dword ptr [frida_win9x_fault_chain], 0
|
||||
je frida_win9x_fault_resume
|
||||
jmp dword ptr [frida_win9x_fault_chain]
|
||||
frida_win9x_fault_resume:
|
||||
mov esp, [frida_win9x_resume + 16]
|
||||
mov edi, [frida_win9x_resume + 4]
|
||||
mov esi, [frida_win9x_resume + 8]
|
||||
mov ebp, [frida_win9x_resume + 12]
|
||||
mov ebx, [frida_win9x_resume + 20]
|
||||
mov edx, [frida_win9x_resume + 24]
|
||||
mov ecx, [frida_win9x_resume + 28]
|
||||
mov eax, [frida_win9x_resume + 32]
|
||||
push dword ptr [frida_win9x_resume]
|
||||
ret
|
||||
|
||||
.global set_global_time_out
|
||||
set_global_time_out:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push esi
|
||||
push edi
|
||||
mov eax, [ebp + 8]
|
||||
mov edx, [ebp + 12]
|
||||
lea esi, [frida_win9x_time_out_thunk]
|
||||
call dword ptr [_Set_Global_Time_Out]
|
||||
mov eax, esi
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global cancel_time_out
|
||||
cancel_time_out:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push esi
|
||||
push edi
|
||||
mov esi, [ebp + 8]
|
||||
call dword ptr [_Cancel_Time_Out]
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global frida_win9x_time_out_thunk
|
||||
frida_win9x_time_out_thunk:
|
||||
pushad
|
||||
mov eax, edx
|
||||
call dword ptr [_Signal_Semaphore]
|
||||
popad
|
||||
ret
|
||||
|
||||
.global vpicd_phys_eoi
|
||||
vpicd_phys_eoi:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push esi
|
||||
push edi
|
||||
mov eax, [ebp + 8]
|
||||
call dword ptr [_VPICD_Phys_EOI]
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global vpicd_physically_unmask
|
||||
vpicd_physically_unmask:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push esi
|
||||
push edi
|
||||
mov eax, [ebp + 8]
|
||||
call dword ptr [_VPICD_Physically_Unmask]
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global vpicd_virtualize_irq
|
||||
vpicd_virtualize_irq:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push esi
|
||||
push edi
|
||||
call dword ptr [_Get_Cur_VM_Handle]
|
||||
mov edi, [ebp + 8]
|
||||
call dword ptr [_VPICD_Virtualize_IRQ]
|
||||
cmc
|
||||
sbb ecx, ecx
|
||||
and eax, ecx
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global vmm_create_thread
|
||||
vmm_create_thread:
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push ebx
|
||||
push esi
|
||||
push edi
|
||||
push dword ptr [ebp + 32]
|
||||
push dword ptr [ebp + 28]
|
||||
push dword ptr [ebp + 24]
|
||||
push dword ptr [ebp + 20]
|
||||
push dword ptr [ebp + 16]
|
||||
push dword ptr [ebp + 12]
|
||||
push dword ptr [ebp + 8]
|
||||
call dword ptr [_VMMCreateThread]
|
||||
add esp, 28
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebx
|
||||
pop ebp
|
||||
ret
|
||||
|
||||
.global frida_win9x_thread_thunk
|
||||
frida_win9x_thread_thunk:
|
||||
call frida_win9x_thread_start
|
||||
1:
|
||||
jmp 1b
|
||||
|
||||
.global frida_win9x_hw_int_thunk
|
||||
frida_win9x_hw_int_thunk:
|
||||
pushad
|
||||
push edx
|
||||
call frida_win9x_on_hw_int
|
||||
add esp, 4
|
||||
popad
|
||||
clc
|
||||
ret
|
||||
"#
|
||||
);
|
||||
|
||||
unsafe extern "C" {
|
||||
static _Get_Cur_VM_Handle: unsafe extern "C" fn();
|
||||
static _Get_Sys_VM_Handle: unsafe extern "C" fn();
|
||||
static _Schedule_Global_Event: unsafe extern "C" fn();
|
||||
static _Create_Semaphore: unsafe extern "C" fn();
|
||||
static _Wait_Semaphore: unsafe extern "C" fn();
|
||||
static _Signal_Semaphore: unsafe extern "C" fn();
|
||||
static _Get_Next_VM_Handle: unsafe extern "C" fn();
|
||||
static _Release_Time_Slice: unsafe extern "C" fn();
|
||||
static _Set_Global_Time_Out: unsafe extern "C" fn();
|
||||
static _Cancel_Time_Out: unsafe extern "C" fn();
|
||||
static _Get_System_Time: unsafe extern "C" fn() -> u32;
|
||||
static __HeapAllocate: unsafe extern "C" fn(u32, u32) -> *mut u8;
|
||||
static __HeapFree: unsafe extern "C" fn(*mut u8, u32);
|
||||
static __PageAllocate:
|
||||
unsafe extern "C" fn(u32, u32, u32, u32, u32, u32, *mut c_void, u32) -> *mut u8;
|
||||
static __PageFree: unsafe extern "C" fn(*mut u8, u32);
|
||||
static __CopyPageTable: unsafe extern "C" fn(u32, u32, *mut u32, u32) -> u32;
|
||||
static __MapPhysToLinear: unsafe extern "C" fn(u32, u32, u32) -> u32;
|
||||
static _Hook_VMM_Fault: unsafe extern "C" fn();
|
||||
static _Fatal_Error_Handler: unsafe extern "C" fn();
|
||||
static _VMMCreateThread: unsafe extern "C" fn();
|
||||
static _Get_Cur_Thread_Handle: unsafe extern "C" fn();
|
||||
static _Get_Initial_Thread_Handle: unsafe extern "C" fn();
|
||||
static _Get_Next_Thread_Handle: unsafe extern "C" fn();
|
||||
static __Debug_Printf_Service: unsafe extern "C" fn(*const u8, ...);
|
||||
static __ContextSwitch: unsafe extern "C" fn(u32);
|
||||
static __PageModifyPermissions: unsafe extern "C" fn(u32, u32, u32, u32) -> u32;
|
||||
static __GetCurrentContext: unsafe extern "C" fn() -> u32;
|
||||
static _VPICD_Virtualize_IRQ: unsafe extern "C" fn();
|
||||
static _VPICD_Phys_EOI: unsafe extern "C" fn();
|
||||
static _VPICD_Physically_Unmask: unsafe extern "C" fn();
|
||||
}
|
||||
+125
-53
@@ -1,7 +1,87 @@
|
||||
use core::ffi::c_void;
|
||||
use core::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
static KERNEL_BASE: AtomicU64 = AtomicU64::new(0);
|
||||
use crate::kernel::ThreadEntry;
|
||||
|
||||
pub fn log(msg: &str) {
|
||||
unsafe {
|
||||
_IOLog(msg.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn panic(msg: &str) {
|
||||
unsafe { _panic(msg.as_ptr()) };
|
||||
}
|
||||
|
||||
pub fn run_when_ready(action: fn()) {
|
||||
action();
|
||||
}
|
||||
|
||||
pub fn spawn_thread(entry: ThreadEntry, parameter: *mut c_void) -> isize {
|
||||
kernel_thread_start(entry, parameter)
|
||||
}
|
||||
|
||||
pub fn alloc(size: usize) -> *mut u8 {
|
||||
kalloc(size)
|
||||
}
|
||||
|
||||
// Executable slabs come out of the same allocator; the host flips their page
|
||||
// permissions for us through its physical-memory bridge.
|
||||
pub fn alloc_code(size: usize) -> *mut u8 {
|
||||
kalloc(size)
|
||||
}
|
||||
|
||||
pub fn free_code(ptr: *mut u8, size: usize) {
|
||||
free(ptr, size);
|
||||
}
|
||||
|
||||
// Arms the wait, gives `check` a chance to observe the condition, and only then
|
||||
// commits to sleeping — the three-phase Mach protocol, which is race-free
|
||||
// because a wakeup landing after assert_wait() cancels the pending block.
|
||||
pub fn wait(token: *const u8, timeout_us: Option<u64>, check: &mut dyn FnMut() -> bool) {
|
||||
let wait_result = match timeout_us {
|
||||
None => assert_wait(token, THREAD_INTERRUPTIBLE),
|
||||
Some(us) => assert_wait_timeout(token, THREAD_INTERRUPTIBLE, (us * 1000) as u32, 1),
|
||||
};
|
||||
if wait_result != THREAD_WAITING {
|
||||
panic!("assert_wait failed: {}", wait_result);
|
||||
}
|
||||
|
||||
if check() {
|
||||
thread_wakeup(token);
|
||||
return;
|
||||
}
|
||||
|
||||
thread_block(None);
|
||||
}
|
||||
|
||||
pub fn wake(token: *const u8) {
|
||||
thread_wakeup(token);
|
||||
}
|
||||
|
||||
// XNU reschedules a kernel thread that stays runnable, so there is nothing this has
|
||||
// to do here. It exists because Linux does not forgive a loop that never yields.
|
||||
pub fn yield_now() {}
|
||||
|
||||
pub fn monotonic_micros() -> i64 {
|
||||
(absolutetime_to_nanoseconds(mach_absolute_time()) / 1000) as i64
|
||||
}
|
||||
|
||||
pub fn wall_clock_micros() -> (u32, u32) {
|
||||
clock_get_calendar_microtime()
|
||||
}
|
||||
|
||||
// The thread pointer is exposed to JavaScript as a GumThreadId, so it must fit
|
||||
// in a double without losing precision; the low bits keep it unique per thread.
|
||||
const JS_SAFE_THREAD_ID_MASK: u64 = (1 << 48) - 1;
|
||||
|
||||
pub fn current_thread_id() -> u64 {
|
||||
let thread_ptr: u64;
|
||||
unsafe {
|
||||
core::arch::asm!("mrs {}, tpidr_el1", out(reg) thread_ptr, options(nomem, nostack));
|
||||
}
|
||||
thread_ptr & JS_SAFE_THREAD_ID_MASK
|
||||
}
|
||||
|
||||
pub fn get_kernel_base() -> u64 {
|
||||
KERNEL_BASE.load(Ordering::Relaxed)
|
||||
@@ -11,54 +91,7 @@ pub fn set_kernel_base(base: u64) {
|
||||
KERNEL_BASE.store(base, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
type ContinuationFn = unsafe extern "C" fn(_parameter: *mut c_void, _wait_result: i32);
|
||||
|
||||
unsafe extern "C" {
|
||||
static _panic: unsafe extern "C" fn(*const u8);
|
||||
static _IOLog: unsafe extern "C" fn(*const u8, ...);
|
||||
// QEMU XNU exports plain kalloc/kfree; iOS XNU exports kalloc_data/kfree_data
|
||||
// (modern data-typed allocator KPIs). The host fills in whichever pair the
|
||||
// running kernel provides; the other slot stays null.
|
||||
static _kalloc: Option<unsafe extern "C" fn(usize) -> *mut u8>;
|
||||
static _kfree: Option<unsafe extern "C" fn(*mut u8, usize) -> *mut u8>;
|
||||
static _kalloc_data: Option<unsafe extern "C" fn(usize, u32) -> *mut u8>;
|
||||
static _kfree_data: Option<unsafe extern "C" fn(*mut u8, usize)>;
|
||||
static _kernel_thread_start:
|
||||
unsafe extern "C" fn(*const (), *mut c_void, *mut *mut c_void) -> isize;
|
||||
static _assert_wait: unsafe extern "C" fn(*const u8, u32) -> i32;
|
||||
static _assert_wait_timeout: unsafe extern "C" fn(*const u8, u32, u32, u32) -> i32;
|
||||
static _thread_block: unsafe extern "C" fn(Option<ContinuationFn>) -> i32;
|
||||
// QEMU XNU exports thread_wakeup; iOS XNU exports the BSD-style wakeup wrapper.
|
||||
static _thread_wakeup: Option<unsafe extern "C" fn(*const u8) -> i32>;
|
||||
static _wakeup: Option<unsafe extern "C" fn(*const u8)>;
|
||||
static _mach_absolute_time: unsafe extern "C" fn() -> u64;
|
||||
static _absolutetime_to_nanoseconds: unsafe extern "C" fn(u64, *mut u64);
|
||||
static _clock_get_calendar_microtime: unsafe extern "C" fn(*mut u32, *mut u32);
|
||||
static _ml_io_map: unsafe extern "C" fn(u64, u64) -> *mut c_void;
|
||||
static _ml_vtophys: unsafe extern "C" fn(u64) -> u64;
|
||||
static __ZN9IOService11getPlatformEv: unsafe extern "C" fn() -> *mut c_void;
|
||||
static __ZN9IOServiceC2Ev: unsafe extern "C" fn(*mut core::ffi::c_void);
|
||||
static __ZN8OSSymbol17withCStringNoCopyEPKc:
|
||||
unsafe extern "C" fn(*const core::ffi::c_char) -> *const OSSymbol;
|
||||
static __ZN6OSData9withBytesEPKvj:
|
||||
unsafe extern "C" fn(*const core::ffi::c_void, u32) -> *mut OSData;
|
||||
}
|
||||
|
||||
const IO_SERVICE_VTABLE_LENGTH: isize = 168;
|
||||
|
||||
const VT_LOOKUP_IC: isize = IO_SERVICE_VTABLE_LENGTH + 25; // IOPlatformExpert
|
||||
const VT_REGISTER_INT: isize = IO_SERVICE_VTABLE_LENGTH + 0; // IOInterruptController
|
||||
const VT_ENABLE_INT: isize = IO_SERVICE_VTABLE_LENGTH + 3; // IOInterruptController
|
||||
|
||||
pub fn panic(msg: &str) {
|
||||
unsafe { _panic(msg.as_ptr()) };
|
||||
}
|
||||
|
||||
pub fn io_log(msg: &str) {
|
||||
unsafe {
|
||||
_IOLog(msg.as_ptr());
|
||||
}
|
||||
}
|
||||
static KERNEL_BASE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
// Z_WAITOK (modern XNU): block until allocation succeeds.
|
||||
const Z_WAITOK: u32 = 0x0200;
|
||||
@@ -98,8 +131,8 @@ pub fn kernel_thread_start(continuation: ContinuationFn, thread_parameter: *mut
|
||||
};
|
||||
}
|
||||
|
||||
pub const THREAD_INTERRUPTIBLE: u32 = 1;
|
||||
pub const THREAD_WAITING: i32 = -1;
|
||||
const THREAD_INTERRUPTIBLE: u32 = 1;
|
||||
const THREAD_WAITING: i32 = -1;
|
||||
|
||||
pub fn assert_wait(event: *const u8, interruptible: u32) -> i32 {
|
||||
unsafe { _assert_wait(event, interruptible) }
|
||||
@@ -152,11 +185,11 @@ pub fn clock_get_calendar_microtime() -> (u32, u32) {
|
||||
(secs, microsecs)
|
||||
}
|
||||
|
||||
pub fn ml_io_map(phys_addr: u64, size: u64) -> *mut c_void {
|
||||
pub fn map_io(phys_addr: u64, size: u64) -> *mut c_void {
|
||||
unsafe { _ml_io_map(phys_addr, size) }
|
||||
}
|
||||
|
||||
pub fn ml_vtophys(vaddr: u64) -> u64 {
|
||||
pub fn virt_to_phys(vaddr: u64) -> u64 {
|
||||
unsafe { _ml_vtophys(vaddr) }
|
||||
}
|
||||
|
||||
@@ -268,3 +301,42 @@ where
|
||||
let entry_ptr = unsafe { crate::pac::ptrauth_strip_data(entry as *const u8) };
|
||||
unsafe { core::mem::transmute_copy::<*const u8, T>(&entry_ptr) }
|
||||
}
|
||||
|
||||
const IO_SERVICE_VTABLE_LENGTH: isize = 168;
|
||||
|
||||
const VT_LOOKUP_IC: isize = IO_SERVICE_VTABLE_LENGTH + 25; // IOPlatformExpert
|
||||
const VT_REGISTER_INT: isize = IO_SERVICE_VTABLE_LENGTH + 0; // IOInterruptController
|
||||
const VT_ENABLE_INT: isize = IO_SERVICE_VTABLE_LENGTH + 3; // IOInterruptController
|
||||
|
||||
type ContinuationFn = ThreadEntry;
|
||||
|
||||
unsafe extern "C" {
|
||||
static _panic: unsafe extern "C" fn(*const u8);
|
||||
static _IOLog: unsafe extern "C" fn(*const u8, ...);
|
||||
// QEMU XNU exports plain kalloc/kfree; iOS XNU exports kalloc_data/kfree_data
|
||||
// (modern data-typed allocator KPIs). The host fills in whichever pair the
|
||||
// running kernel provides; the other slot stays null.
|
||||
static _kalloc: Option<unsafe extern "C" fn(usize) -> *mut u8>;
|
||||
static _kfree: Option<unsafe extern "C" fn(*mut u8, usize) -> *mut u8>;
|
||||
static _kalloc_data: Option<unsafe extern "C" fn(usize, u32) -> *mut u8>;
|
||||
static _kfree_data: Option<unsafe extern "C" fn(*mut u8, usize)>;
|
||||
static _kernel_thread_start:
|
||||
unsafe extern "C" fn(*const (), *mut c_void, *mut *mut c_void) -> isize;
|
||||
static _assert_wait: unsafe extern "C" fn(*const u8, u32) -> i32;
|
||||
static _assert_wait_timeout: unsafe extern "C" fn(*const u8, u32, u32, u32) -> i32;
|
||||
static _thread_block: unsafe extern "C" fn(Option<ContinuationFn>) -> i32;
|
||||
// QEMU XNU exports thread_wakeup; iOS XNU exports the BSD-style wakeup wrapper.
|
||||
static _thread_wakeup: Option<unsafe extern "C" fn(*const u8) -> i32>;
|
||||
static _wakeup: Option<unsafe extern "C" fn(*const u8)>;
|
||||
static _mach_absolute_time: unsafe extern "C" fn() -> u64;
|
||||
static _absolutetime_to_nanoseconds: unsafe extern "C" fn(u64, *mut u64);
|
||||
static _clock_get_calendar_microtime: unsafe extern "C" fn(*mut u32, *mut u32);
|
||||
static _ml_io_map: unsafe extern "C" fn(u64, u64) -> *mut c_void;
|
||||
static _ml_vtophys: unsafe extern "C" fn(u64) -> u64;
|
||||
static __ZN9IOService11getPlatformEv: unsafe extern "C" fn() -> *mut c_void;
|
||||
static __ZN9IOServiceC2Ev: unsafe extern "C" fn(*mut core::ffi::c_void);
|
||||
static __ZN8OSSymbol17withCStringNoCopyEPKc:
|
||||
unsafe extern "C" fn(*const core::ffi::c_char) -> *const OSSymbol;
|
||||
static __ZN6OSData9withBytesEPKvj:
|
||||
unsafe extern "C" fn(*const core::ffi::c_void, u32) -> *mut OSData;
|
||||
}
|
||||
@@ -32,11 +32,6 @@ namespace Frida.Barebone {
|
||||
private uint64 code_template_descriptor;
|
||||
private bool code_template_known = false;
|
||||
|
||||
private enum AddressingMode {
|
||||
VIRTUAL,
|
||||
PHYSICAL
|
||||
}
|
||||
|
||||
private const uint NUM_ARGS_IN_REGS = 8;
|
||||
|
||||
private const uint64 INT2_MASK = 0x3ULL;
|
||||
@@ -90,11 +85,14 @@ namespace Frida.Barebone {
|
||||
MMUParameters p = yield MMUParameters.load (gdb, cancellable);
|
||||
|
||||
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
|
||||
GLib.Error? failure = null;
|
||||
try {
|
||||
yield collect_ranges_in_table (p.tt1, p.first_level, p.upper_bits, p, result, cancellable);
|
||||
} finally {
|
||||
set_addressing_mode.begin (gdb, VIRTUAL, null);
|
||||
} catch (GLib.Error e) {
|
||||
failure = e;
|
||||
}
|
||||
yield set_addressing_mode (gdb, VIRTUAL, cancellable);
|
||||
throw_if_failed (failure);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -154,53 +152,26 @@ namespace Frida.Barebone {
|
||||
return result;
|
||||
}
|
||||
|
||||
private Gee.List<RangeDetails> coalesce_ranges (Gee.List<RangeDetails> ranges) {
|
||||
var result = new Gee.ArrayList<RangeDetails> ();
|
||||
|
||||
RangeDetails? pending = null;
|
||||
foreach (RangeDetails r in ranges) {
|
||||
if (pending == null) {
|
||||
pending = r.clone ();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (r.base_va == pending.base_va + pending.size &&
|
||||
r.base_pa == pending.base_pa + pending.size &&
|
||||
r.protection == pending.protection &&
|
||||
r.type == pending.type) {
|
||||
pending.size += r.size;
|
||||
continue;
|
||||
}
|
||||
|
||||
result.add (pending);
|
||||
pending = r.clone ();
|
||||
}
|
||||
if (pending != null)
|
||||
result.add (pending);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Allocation allocate_pages (Gee.List<uint64?> physical_addresses, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
MMUParameters p = yield load_mmu_parameters (cancellable);
|
||||
|
||||
// The bridge reads and writes guest physical memory directly, so the stub's
|
||||
// physical-addressing mode (which requires a halted target) is unnecessary.
|
||||
bool needs_stub_addressing = physical_memory == null;
|
||||
if (needs_stub_addressing)
|
||||
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
|
||||
yield begin_physical_addressing (cancellable);
|
||||
Allocation? allocation = null;
|
||||
GLib.Error? failure = null;
|
||||
try {
|
||||
Allocation? allocation = yield maybe_insert_descriptor_in_table (physical_addresses, p.tt1, p.first_level,
|
||||
allocation = yield maybe_insert_descriptor_in_table (physical_addresses, p.tt1, p.first_level,
|
||||
p.upper_bits, p, cancellable);
|
||||
if (allocation == null)
|
||||
throw new Error.NOT_SUPPORTED ("Unable to insert page table mapping; please file a bug");
|
||||
|
||||
return allocation;
|
||||
} finally {
|
||||
if (needs_stub_addressing)
|
||||
set_addressing_mode.begin (gdb, VIRTUAL, null);
|
||||
} catch (GLib.Error e) {
|
||||
failure = e;
|
||||
}
|
||||
yield end_physical_addressing (cancellable);
|
||||
throw_if_failed (failure);
|
||||
|
||||
if (allocation == null)
|
||||
throw new Error.NOT_SUPPORTED ("Unable to insert page table mapping; please file a bug");
|
||||
|
||||
return allocation;
|
||||
}
|
||||
|
||||
private async Allocation? maybe_insert_descriptor_in_table (Gee.List<uint64?> physical_addresses,
|
||||
@@ -339,27 +310,36 @@ namespace Frida.Barebone {
|
||||
return;
|
||||
}
|
||||
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
|
||||
GLib.Error? failure = null;
|
||||
try {
|
||||
yield gdb.write_byte_array (pa, new Bytes (data), cancellable);
|
||||
} finally {
|
||||
set_addressing_mode.begin (gdb, VIRTUAL, null);
|
||||
} catch (GLib.Error e) {
|
||||
failure = e;
|
||||
}
|
||||
yield set_addressing_mode (gdb, VIRTUAL, cancellable);
|
||||
throw_if_failed (failure);
|
||||
}
|
||||
|
||||
public async uint8[] read_physical_via_stub (uint64 pa, size_t size, Cancellable? cancellable) throws Error, IOError {
|
||||
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
|
||||
uint8[]? data = null;
|
||||
GLib.Error? failure = null;
|
||||
try {
|
||||
return (yield gdb.read_byte_array (pa, size, cancellable)).get_data ();
|
||||
} finally {
|
||||
set_addressing_mode.begin (gdb, VIRTUAL, null);
|
||||
data = (yield gdb.read_byte_array (pa, size, cancellable)).get_data ();
|
||||
} catch (GLib.Error e) {
|
||||
failure = e;
|
||||
}
|
||||
yield set_addressing_mode (gdb, VIRTUAL, cancellable);
|
||||
throw_if_failed (failure);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public async uint64 read_level3_descriptor (uint64 va, Cancellable? cancellable) throws Error, IOError {
|
||||
MMUParameters p = yield load_mmu_parameters (cancellable);
|
||||
bool needs_stub_addressing = physical_memory == null;
|
||||
if (needs_stub_addressing)
|
||||
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
|
||||
yield begin_physical_addressing (cancellable);
|
||||
uint64 descriptor = 0;
|
||||
GLib.Error? failure = null;
|
||||
try {
|
||||
var cache = new TableWalkCache ();
|
||||
uint64 table_pa = yield find_level3_table (va, p, cache, cancellable);
|
||||
@@ -369,20 +349,22 @@ namespace Frida.Barebone {
|
||||
uint64 index = (va >> index_shift) & index_mask;
|
||||
uint64 slot_pa = table_pa + (index * Descriptor.SIZE);
|
||||
var buf = yield read_physical_buffer (slot_pa, Descriptor.SIZE, cancellable);
|
||||
return buf.read_uint64 (0);
|
||||
} finally {
|
||||
if (needs_stub_addressing)
|
||||
set_addressing_mode.begin (gdb, VIRTUAL, null);
|
||||
descriptor = buf.read_uint64 (0);
|
||||
} catch (GLib.Error e) {
|
||||
failure = e;
|
||||
}
|
||||
yield end_physical_addressing (cancellable);
|
||||
throw_if_failed (failure);
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public async void protect_pages (uint64 virtual_address, size_t size, Gum.PageProtection prot, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
MMUParameters p = yield load_mmu_parameters (cancellable);
|
||||
|
||||
bool needs_stub_addressing = physical_memory == null;
|
||||
if (needs_stub_addressing)
|
||||
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
|
||||
yield begin_physical_addressing (cancellable);
|
||||
GLib.Error? failure = null;
|
||||
try {
|
||||
uint64 page_mask = p.granule - 1;
|
||||
uint64 aligned_va = virtual_address & ~page_mask;
|
||||
@@ -390,10 +372,23 @@ namespace Frida.Barebone {
|
||||
uint num_pages = (uint) ((aligned_end - aligned_va) / p.granule);
|
||||
|
||||
yield perform_protect_pages (aligned_va, num_pages, prot, p, cancellable);
|
||||
} finally {
|
||||
if (needs_stub_addressing)
|
||||
set_addressing_mode.begin (gdb, VIRTUAL, null);
|
||||
} catch (GLib.Error e) {
|
||||
failure = e;
|
||||
}
|
||||
yield end_physical_addressing (cancellable);
|
||||
throw_if_failed (failure);
|
||||
}
|
||||
|
||||
// The bridge reads and writes guest physical memory directly, so the stub's
|
||||
// physical-addressing mode, which requires a halted target, is unnecessary.
|
||||
private async void begin_physical_addressing (Cancellable? cancellable) throws Error, IOError {
|
||||
if (physical_memory == null)
|
||||
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
|
||||
}
|
||||
|
||||
private async void end_physical_addressing (Cancellable? cancellable) throws Error, IOError {
|
||||
if (physical_memory == null)
|
||||
yield set_addressing_mode (gdb, VIRTUAL, cancellable);
|
||||
}
|
||||
|
||||
private async MMUParameters load_mmu_parameters (Cancellable? cancellable) throws Error, IOError {
|
||||
@@ -1051,18 +1046,6 @@ namespace Frida.Barebone {
|
||||
}
|
||||
}
|
||||
|
||||
private static async void set_addressing_mode (GDB.Client gdb, AddressingMode mode, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
Gee.Set<string> features = gdb.features;
|
||||
string enabled = (mode == PHYSICAL) ? "1" : "0";
|
||||
if ("qemu-phy-mem-mode" in features)
|
||||
yield gdb.execute_simple ("Qqemu.PhyMemMode:" + enabled, cancellable);
|
||||
else if ("vf-phy-mem-mode" in features)
|
||||
yield gdb.execute_simple ("Qvf.PhyMemMode:" + enabled, cancellable);
|
||||
else
|
||||
throw new Error.NOT_SUPPORTED ("Unsupported GDB remote stub; please file a bug");
|
||||
}
|
||||
|
||||
private class MMUParameters {
|
||||
public Granule granule;
|
||||
public uint first_level;
|
||||
@@ -1280,11 +1263,14 @@ namespace Frida.Barebone {
|
||||
Bytes d = old_descriptors;
|
||||
old_descriptors = null;
|
||||
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
|
||||
GLib.Error? failure = null;
|
||||
try {
|
||||
yield gdb.write_byte_array (first_slot, d, cancellable);
|
||||
} finally {
|
||||
set_addressing_mode.begin (gdb, VIRTUAL, null);
|
||||
} catch (GLib.Error e) {
|
||||
failure = e;
|
||||
}
|
||||
yield set_addressing_mode (gdb, VIRTUAL, cancellable);
|
||||
throw_if_failed (failure);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,21 @@ namespace Frida.Barebone {
|
||||
get { return "small"; }
|
||||
}
|
||||
|
||||
public PhysicalMemory? physical_memory {
|
||||
get {
|
||||
return page_tables.physical_memory;
|
||||
}
|
||||
set {
|
||||
page_tables.physical_memory = value;
|
||||
}
|
||||
}
|
||||
|
||||
private X86PageTables page_tables;
|
||||
|
||||
public IA32Machine (GDB.Client gdb) {
|
||||
Object (gdb: gdb);
|
||||
|
||||
page_tables = new X86PageTables (gdb);
|
||||
}
|
||||
|
||||
public async size_t query_page_size (Cancellable? cancellable) throws Error, IOError {
|
||||
@@ -28,17 +41,22 @@ namespace Frida.Barebone {
|
||||
|
||||
public async void enumerate_ranges (Gum.PageProtection prot, FoundRangeFunc func, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
throw_not_supported ();
|
||||
foreach (RangeDetails r in coalesce_ranges (yield page_tables.collect_ranges (cancellable))) {
|
||||
if ((r.protection & prot) != prot)
|
||||
continue;
|
||||
if (!func (r))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public async Allocation allocate_pages (Gee.List<uint64?> physical_addresses, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
throw_not_supported ();
|
||||
return yield page_tables.map (physical_addresses, cancellable);
|
||||
}
|
||||
|
||||
public async void protect_pages (uint64 virtual_address, size_t size, Gum.PageProtection prot,
|
||||
Cancellable? cancellable) throws Error, IOError {
|
||||
throw_not_supported ();
|
||||
yield page_tables.protect (virtual_address, size, prot, cancellable);
|
||||
}
|
||||
|
||||
public async Gee.List<uint64?> scan_ranges (Gee.List<Gum.MemoryRange?> ranges, MatchPattern pattern, uint max_matches,
|
||||
@@ -47,11 +65,70 @@ namespace Frida.Barebone {
|
||||
}
|
||||
|
||||
public void apply_relocation (Gum.ElfRelocationDetails r, uint64 base_va, Buffer relocated) throws Error {
|
||||
throw_not_supported ();
|
||||
Gum.ElfIA32Relocation type = (Gum.ElfIA32Relocation) r.type;
|
||||
switch (type) {
|
||||
case NONE:
|
||||
break;
|
||||
case @32:
|
||||
case RELATIVE:
|
||||
// The in-place value is already the link-time target, the image being linked
|
||||
// at zero and loaded as a unit.
|
||||
relocated.write_uint32 ((size_t) r.address,
|
||||
(uint32) (base_va + relocated.read_uint32 ((size_t) r.address)));
|
||||
break;
|
||||
case PC32:
|
||||
case PLT32:
|
||||
case GOTPC:
|
||||
// Both ends of the displacement move by the same amount.
|
||||
break;
|
||||
default:
|
||||
throw new Error.NOT_SUPPORTED ("Unsupported relocation type: %s",
|
||||
Marshal.enum_to_nick<Gum.ElfIA32Relocation> (type));
|
||||
}
|
||||
}
|
||||
|
||||
public async uint64 invoke (uint64 impl, uint64[] args, Cancellable? cancellable) throws Error, IOError {
|
||||
throw_not_supported ();
|
||||
bool was_running = gdb.state != STOPPED;
|
||||
if (was_running)
|
||||
yield gdb.stop (cancellable);
|
||||
|
||||
GDB.Thread thread = gdb.exception.thread;
|
||||
Gee.Map<string, Variant> saved_regs = yield thread.read_registers (cancellable);
|
||||
|
||||
var regs = new Gee.HashMap<string, Variant> ();
|
||||
regs.set_all (saved_regs);
|
||||
|
||||
uint64 landing_zone = saved_regs["eip"].get_uint64 ();
|
||||
|
||||
uint64 sp = saved_regs["esp"].get_uint64 () - ((1 + args.length) * 4);
|
||||
sp = (sp & ~15ULL) - 4;
|
||||
|
||||
var builder = gdb.make_buffer_builder ();
|
||||
builder.append_uint32 ((uint32) landing_zone);
|
||||
foreach (uint64 arg in args)
|
||||
builder.append_uint32 ((uint32) arg);
|
||||
yield gdb.write_byte_array (sp, builder.build (), cancellable);
|
||||
|
||||
regs["eip"] = impl;
|
||||
regs["esp"] = sp;
|
||||
yield thread.write_registers (regs, cancellable);
|
||||
|
||||
GDB.Breakpoint bp = yield gdb.add_breakpoint (SOFT, landing_zone, 1, cancellable);
|
||||
GDB.Exception ex = null;
|
||||
do {
|
||||
ex = yield gdb.continue_until_exception (cancellable);
|
||||
} while (ex.breakpoint != bp);
|
||||
yield bp.remove (cancellable);
|
||||
|
||||
GDB.Thread landed = ex.thread;
|
||||
uint64 retval = yield landed.read_register ("eax", cancellable);
|
||||
|
||||
yield landed.write_registers (saved_regs, cancellable);
|
||||
|
||||
if (was_running)
|
||||
yield gdb.continue (cancellable);
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
public async CallFrame load_call_frame (GDB.Thread thread, uint arity, Cancellable? cancellable) throws Error, IOError {
|
||||
@@ -162,7 +239,11 @@ namespace Frida.Barebone {
|
||||
|
||||
public async InlineHook create_inline_hook (uint64 target, uint64 handler, Allocator allocator, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
throw_not_supported ();
|
||||
return yield X86InlineHook.create (target, handler, IA32, allocator, gdb, cancellable);
|
||||
}
|
||||
|
||||
public async uint64 translate_address (uint64 va, Cancellable? cancellable) throws Error, IOError {
|
||||
return yield page_tables.translate (va, cancellable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,25 @@ namespace Frida.Barebone {
|
||||
get { return "small"; }
|
||||
}
|
||||
|
||||
public PhysicalMemory? physical_memory {
|
||||
get {
|
||||
return page_tables.physical_memory;
|
||||
}
|
||||
set {
|
||||
page_tables.physical_memory = value;
|
||||
}
|
||||
}
|
||||
|
||||
private X86PageTables page_tables;
|
||||
|
||||
private const uint NUM_ARGS_IN_REGS = 6;
|
||||
private const string[] ARG_REG_NAMES = { "rdi", "rsi", "rdx", "rcx", "r8", "r9" };
|
||||
private const size_t RED_ZONE_SIZE = 128;
|
||||
|
||||
public X64Machine (GDB.Client gdb) {
|
||||
Object (gdb: gdb);
|
||||
|
||||
page_tables = new X86PageTables (gdb);
|
||||
}
|
||||
|
||||
public async size_t query_page_size (Cancellable? cancellable) throws Error, IOError {
|
||||
@@ -30,17 +45,22 @@ namespace Frida.Barebone {
|
||||
|
||||
public async void enumerate_ranges (Gum.PageProtection prot, FoundRangeFunc func, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
throw_not_supported ();
|
||||
foreach (RangeDetails r in coalesce_ranges (yield page_tables.collect_ranges (cancellable))) {
|
||||
if ((r.protection & prot) != prot)
|
||||
continue;
|
||||
if (!func (r))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public async Allocation allocate_pages (Gee.List<uint64?> physical_addresses, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
throw_not_supported ();
|
||||
return yield page_tables.map (physical_addresses, cancellable);
|
||||
}
|
||||
|
||||
public async void protect_pages (uint64 virtual_address, size_t size, Gum.PageProtection prot,
|
||||
Cancellable? cancellable) throws Error, IOError {
|
||||
throw_not_supported ();
|
||||
yield page_tables.protect (virtual_address, size, prot, cancellable);
|
||||
}
|
||||
|
||||
public async Gee.List<uint64?> scan_ranges (Gee.List<Gum.MemoryRange?> ranges, MatchPattern pattern, uint max_matches,
|
||||
@@ -49,11 +69,74 @@ namespace Frida.Barebone {
|
||||
}
|
||||
|
||||
public void apply_relocation (Gum.ElfRelocationDetails r, uint64 base_va, Buffer relocated) throws Error {
|
||||
throw_not_supported ();
|
||||
Gum.ElfX64Relocation type = (Gum.ElfX64Relocation) r.type;
|
||||
switch (type) {
|
||||
case NONE:
|
||||
break;
|
||||
case @64:
|
||||
case RELATIVE:
|
||||
// The in-place value is already the link-time target, the image being linked
|
||||
// at zero and loaded as a unit.
|
||||
relocated.write_uint64 ((size_t) r.address,
|
||||
base_va + relocated.read_uint64 ((size_t) r.address));
|
||||
break;
|
||||
case PC32:
|
||||
case PLT32:
|
||||
case GOTPCREL:
|
||||
// Both ends of the displacement move by the same amount.
|
||||
break;
|
||||
default:
|
||||
throw new Error.NOT_SUPPORTED ("Unsupported relocation type: %s",
|
||||
Marshal.enum_to_nick<Gum.ElfX64Relocation> (type));
|
||||
}
|
||||
}
|
||||
|
||||
public async uint64 invoke (uint64 impl, uint64[] args, Cancellable? cancellable) throws Error, IOError {
|
||||
throw_not_supported ();
|
||||
if (args.length > NUM_ARGS_IN_REGS)
|
||||
throw new Error.NOT_SUPPORTED ("Unsupported number of arguments; please open a PR");
|
||||
|
||||
bool was_running = gdb.state != STOPPED;
|
||||
if (was_running)
|
||||
yield gdb.stop (cancellable);
|
||||
|
||||
GDB.Thread thread = gdb.exception.thread;
|
||||
Gee.Map<string, Variant> saved_regs = yield thread.read_registers (cancellable);
|
||||
|
||||
var regs = new Gee.HashMap<string, Variant> ();
|
||||
regs.set_all (saved_regs);
|
||||
|
||||
uint64 landing_zone = saved_regs["rip"].get_uint64 ();
|
||||
|
||||
uint64 sp = saved_regs["rsp"].get_uint64 () - RED_ZONE_SIZE - 8;
|
||||
sp = (sp & ~15ULL) - 8;
|
||||
|
||||
var builder = gdb.make_buffer_builder ();
|
||||
builder.append_uint64 (landing_zone);
|
||||
yield gdb.write_byte_array (sp, builder.build (), cancellable);
|
||||
|
||||
for (uint i = 0; i != args.length; i++)
|
||||
regs[ARG_REG_NAMES[i]] = args[i];
|
||||
|
||||
regs["rip"] = impl;
|
||||
regs["rsp"] = sp;
|
||||
yield thread.write_registers (regs, cancellable);
|
||||
|
||||
GDB.Breakpoint bp = yield gdb.add_breakpoint (SOFT, landing_zone, 1, cancellable);
|
||||
GDB.Exception ex = null;
|
||||
do {
|
||||
ex = yield gdb.continue_until_exception (cancellable);
|
||||
} while (ex.breakpoint != bp);
|
||||
yield bp.remove (cancellable);
|
||||
|
||||
GDB.Thread landed = ex.thread;
|
||||
uint64 retval = yield landed.read_register ("rax", cancellable);
|
||||
|
||||
yield landed.write_registers (saved_regs, cancellable);
|
||||
|
||||
if (was_running)
|
||||
yield gdb.continue (cancellable);
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
public async CallFrame load_call_frame (GDB.Thread thread, uint arity, Cancellable? cancellable) throws Error, IOError {
|
||||
@@ -63,10 +146,10 @@ namespace Frida.Barebone {
|
||||
var num_stack_args = int.max ((int) arity - (int) NUM_ARGS_IN_REGS, 0);
|
||||
var stack = yield gdb.read_buffer (original_rsp, (1 + num_stack_args) * 8, cancellable);
|
||||
|
||||
return new Arm64CallFrame (thread, regs, stack, original_rsp);
|
||||
return new X64CallFrame (thread, regs, stack, original_rsp);
|
||||
}
|
||||
|
||||
private class Arm64CallFrame : Object, CallFrame {
|
||||
private class X64CallFrame : Object, CallFrame {
|
||||
public uint64 return_address {
|
||||
get { return stack.read_uint64 (0); }
|
||||
}
|
||||
@@ -83,14 +166,13 @@ namespace Frida.Barebone {
|
||||
private uint64 original_rsp;
|
||||
private State stack_state = PRISTINE;
|
||||
|
||||
private const string[] ARG_REG_NAMES = { "rdi", "rsi", "rdx", "rcx", "r8", "r9" };
|
||||
|
||||
private enum State {
|
||||
PRISTINE,
|
||||
MODIFIED
|
||||
}
|
||||
|
||||
public Arm64CallFrame (GDB.Thread thread, Gee.Map<string, Variant> regs, Buffer stack, uint64 original_rsp) {
|
||||
public X64CallFrame (GDB.Thread thread, Gee.Map<string, Variant> regs, Buffer stack, uint64 original_rsp) {
|
||||
this.thread = thread;
|
||||
|
||||
this.regs = regs;
|
||||
@@ -191,7 +273,11 @@ namespace Frida.Barebone {
|
||||
|
||||
public async InlineHook create_inline_hook (uint64 target, uint64 handler, Allocator allocator, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
throw_not_supported ();
|
||||
return yield X86InlineHook.create (target, handler, AMD64, allocator, gdb, cancellable);
|
||||
}
|
||||
|
||||
public async uint64 translate_address (uint64 va, Cancellable? cancellable) throws Error, IOError {
|
||||
return yield page_tables.translate (va, cancellable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,9 +56,17 @@ namespace Frida {
|
||||
if (host_session != null)
|
||||
throw new Error.INVALID_OPERATION ("Already created");
|
||||
|
||||
Barebone.Config config;
|
||||
Barebone.Config? config = null;
|
||||
if (options != null) {
|
||||
Value? val = options.map["config"];
|
||||
if (val != null) {
|
||||
config = (Barebone.Config) val.get_object ();
|
||||
config.check ();
|
||||
}
|
||||
}
|
||||
|
||||
unowned string? config_path = Environment.get_variable ("FRIDA_BAREBONE_CONFIG");
|
||||
if (config_path != null) {
|
||||
if (config == null && config_path != null) {
|
||||
try {
|
||||
var config_data = yield FS.read_all_text (File.new_for_path (config_path), cancellable);
|
||||
var cfg = (Barebone.Config) Json.gobject_from_data (typeof (Barebone.Config), config_data);
|
||||
@@ -67,8 +75,18 @@ namespace Frida {
|
||||
} catch (GLib.Error e) {
|
||||
throw new Error.INVALID_ARGUMENT ("Unable to load %s: %s", config_path, e.message);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if (config == null)
|
||||
config = new Barebone.Config ();
|
||||
|
||||
Barebone.AgentConfig? resident_agent = config.agent;
|
||||
if (resident_agent != null && (resident_agent.transport is Barebone.DeviceTransportConfig
|
||||
|| resident_agent.transport is Barebone.SocketTransportConfig)) {
|
||||
host_session = yield attach_to_resident_agent (resident_agent.transport, cancellable);
|
||||
host_session.agent_session_detached.connect (on_agent_session_detached);
|
||||
|
||||
return host_session;
|
||||
}
|
||||
|
||||
SocketConnectable connectable;
|
||||
@@ -148,8 +166,14 @@ namespace Frida {
|
||||
}
|
||||
}
|
||||
|
||||
Gee.List<Barebone.SymbolInfo> kernel_symbols = new Gee.ArrayList<Barebone.SymbolInfo> ();
|
||||
if (config.kernel == WIN9X)
|
||||
kernel_symbols = yield Barebone.collect_win9x_symbols (machine, cancellable);
|
||||
|
||||
Barebone.Allocator allocator;
|
||||
Barebone.AllocatorConfig? ac = config.allocator;
|
||||
if (ac == null)
|
||||
ac = infer_allocator_config (config.kernel, kernel_symbols);
|
||||
if (ac == null) {
|
||||
allocator = new Barebone.NullAllocator (page_size);
|
||||
} else if (ac is Barebone.PhysicalAllocatorConfig) {
|
||||
@@ -171,8 +195,8 @@ namespace Frida {
|
||||
Barebone.AgentConnection? agent_connection = null;
|
||||
Barebone.AgentConfig? agent_config = config.agent;
|
||||
if (agent_config != null) {
|
||||
agent_connection = yield Barebone.AgentConnection.open (agent_config, config.image, relocation,
|
||||
kernel_base, machine, allocator, cancellable);
|
||||
agent_connection = yield Barebone.AgentConnection.open (agent_config, config.image, config.kernel,
|
||||
relocation, kernel_base, machine, allocator, kernel_symbols, cancellable);
|
||||
}
|
||||
|
||||
var interceptor = new Barebone.Interceptor (machine, allocator);
|
||||
@@ -185,6 +209,56 @@ namespace Frida {
|
||||
return host_session;
|
||||
}
|
||||
|
||||
private async BareboneHostSession attach_to_resident_agent (Barebone.TransportConfig transport,
|
||||
Cancellable? cancellable) throws Error, IOError {
|
||||
#if WINDOWS
|
||||
throw new Error.NOT_SUPPORTED ("Resident agents are not available on this OS");
|
||||
#else
|
||||
IOStream stream;
|
||||
if (transport is Barebone.SocketTransportConfig) {
|
||||
string path = ((Barebone.SocketTransportConfig) transport).path;
|
||||
var client = new SocketClient ();
|
||||
try {
|
||||
stream = yield client.connect_async (new UnixSocketAddress (path), cancellable);
|
||||
} catch (GLib.Error e) {
|
||||
throw new Error.TRANSPORT ("Unable to connect to %s: %s", path, e.message);
|
||||
}
|
||||
} else {
|
||||
string path = ((Barebone.DeviceTransportConfig) transport).path;
|
||||
int fd = Posix.open (path, Posix.O_RDWR);
|
||||
if (fd == -1)
|
||||
throw new Error.TRANSPORT ("Unable to open %s: %s", path, Posix.strerror (Posix.errno));
|
||||
stream = new SimpleIOStream (new UnixInputStream (fd, true), new UnixOutputStream (fd, false));
|
||||
}
|
||||
|
||||
var connection = yield Barebone.AgentConnection.open_resident (stream, cancellable);
|
||||
|
||||
return new BareboneHostSession (connection, null);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static Barebone.AllocatorConfig? infer_allocator_config (Barebone.KernelKind kind,
|
||||
Gee.List<Barebone.SymbolInfo> kernel_symbols) {
|
||||
if (kind != WIN9X)
|
||||
return null;
|
||||
|
||||
Barebone.SymbolInfo? alloc = null;
|
||||
Barebone.SymbolInfo? free = null;
|
||||
foreach (var s in kernel_symbols) {
|
||||
if (s.name == "_HeapAllocate")
|
||||
alloc = s;
|
||||
else if (s.name == "_HeapFree")
|
||||
free = s;
|
||||
}
|
||||
if (alloc == null || free == null)
|
||||
return null;
|
||||
|
||||
return new Barebone.TargetFunctionsAllocatorConfig () {
|
||||
alloc_function = new Barebone.NonNullMemoryAddress ("allocator.alloc_function", alloc.offset),
|
||||
free_function = new Barebone.NonNullMemoryAddress ("allocator.free_function", free.offset),
|
||||
};
|
||||
}
|
||||
|
||||
public async void destroy (HostSession session, Cancellable? cancellable) throws Error, IOError {
|
||||
if (session != host_session)
|
||||
throw new Error.INVALID_ARGUMENT ("Invalid host session");
|
||||
@@ -236,7 +310,8 @@ namespace Frida {
|
||||
construct;
|
||||
}
|
||||
|
||||
public Barebone.Services services {
|
||||
/** Absent for a resident agent: every service here is built on a debugger stub. */
|
||||
public Barebone.Services? services {
|
||||
get;
|
||||
construct;
|
||||
}
|
||||
@@ -244,7 +319,7 @@ namespace Frida {
|
||||
private Gee.Map<AgentSessionId?, BareboneAgentSession> agent_sessions =
|
||||
new Gee.HashMap<AgentSessionId?, BareboneAgentSession> (AgentSessionId.hash, AgentSessionId.equal);
|
||||
|
||||
public BareboneHostSession (Barebone.AgentConnection? connection, Barebone.Services services) {
|
||||
public BareboneHostSession (Barebone.AgentConnection? connection, Barebone.Services? services) {
|
||||
Object (connection: connection, services: services);
|
||||
}
|
||||
|
||||
@@ -279,7 +354,9 @@ namespace Frida {
|
||||
|
||||
public async HostProcessInfo[] enumerate_processes (HashTable<string, Variant> options,
|
||||
Cancellable? cancellable) throws Error, IOError {
|
||||
throw_not_supported ();
|
||||
if (connection == null)
|
||||
throw_not_supported ();
|
||||
return yield connection.enumerate_processes (cancellable);
|
||||
}
|
||||
|
||||
public async void enable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
|
||||
@@ -333,10 +410,13 @@ namespace Frida {
|
||||
MainContext dbus_context = yield get_dbus_context ();
|
||||
|
||||
BareboneAgentSession session;
|
||||
if (connection != null)
|
||||
if (connection != null) {
|
||||
session = new RemoteBareboneAgentSession (connection, session_id, opts.persist_timeout, dbus_context);
|
||||
else
|
||||
} else {
|
||||
if (services == null)
|
||||
throw new Error.NOT_SUPPORTED ("Barebone target has neither an agent nor a debugger stub");
|
||||
session = new LocalBareboneAgentSession (services, session_id, opts.persist_timeout, dbus_context);
|
||||
}
|
||||
agent_sessions[session_id] = session;
|
||||
session.closed.connect (on_agent_session_closed);
|
||||
|
||||
|
||||
@@ -76,6 +76,12 @@ namespace Frida.Barebone {
|
||||
set;
|
||||
}
|
||||
|
||||
public KernelKind kernel {
|
||||
get;
|
||||
set;
|
||||
default = AUTO;
|
||||
}
|
||||
|
||||
public void check () throws Error {
|
||||
if (allocator != null)
|
||||
allocator.check ();
|
||||
@@ -111,9 +117,36 @@ namespace Frida.Barebone {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (property_name == "kernel") {
|
||||
var v = Value (typeof (KernelKind));
|
||||
v.set_enum (parse_kernel_kind (property_node.get_string ()));
|
||||
value = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = Value (pspec.value_type);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static KernelKind parse_kernel_kind (string? name) {
|
||||
switch (name) {
|
||||
case "bare": return KernelKind.BARE;
|
||||
case "xnu": return KernelKind.XNU;
|
||||
case "win9x": return KernelKind.WIN9X;
|
||||
default: return KernelKind.AUTO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects which bring-up dance the target kernel needs before the agent can be
|
||||
* injected. AUTO infers XNU when an image is configured, and BARE otherwise.
|
||||
*/
|
||||
public enum KernelKind {
|
||||
AUTO,
|
||||
BARE,
|
||||
XNU,
|
||||
WIN9X
|
||||
}
|
||||
|
||||
public sealed class ConnectionConfig : Object, Json.Serializable {
|
||||
@@ -261,12 +294,13 @@ namespace Frida.Barebone {
|
||||
}
|
||||
|
||||
public void check () throws Error {
|
||||
if (path == null)
|
||||
throw new Error.NOT_SUPPORTED ("Config for 'agent.path' is missing");
|
||||
|
||||
if (transport == null)
|
||||
throw new Error.NOT_SUPPORTED ("Config for 'agent.transport' is missing");
|
||||
transport.check ();
|
||||
|
||||
if (path == null && !(transport is DeviceTransportConfig)
|
||||
&& !(transport is SocketTransportConfig))
|
||||
throw new Error.NOT_SUPPORTED ("Config for 'agent.path' is missing");
|
||||
}
|
||||
|
||||
public bool deserialize_property (string property_name, out Value value, ParamSpec pspec, Json.Node property_node) {
|
||||
@@ -281,6 +315,12 @@ namespace Frida.Barebone {
|
||||
case "vsock":
|
||||
t = typeof (VsockTransportConfig);
|
||||
break;
|
||||
case "device":
|
||||
t = typeof (DeviceTransportConfig);
|
||||
break;
|
||||
case "socket":
|
||||
t = typeof (SocketTransportConfig);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -316,6 +356,11 @@ namespace Frida.Barebone {
|
||||
set;
|
||||
}
|
||||
|
||||
public string? bus {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public override void check () throws Error {
|
||||
if (qmp == null)
|
||||
throw new Error.NOT_SUPPORTED ("Config for 'agent.transport.qmp' is missing");
|
||||
@@ -351,6 +396,39 @@ namespace Frida.Barebone {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An agent already resident in the target, reached through a device node it
|
||||
* exposes. Nothing is injected, so there is no image and no stub.
|
||||
*/
|
||||
public sealed class DeviceTransportConfig : TransportConfig {
|
||||
public string path {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public override void check () throws Error {
|
||||
if (path == null)
|
||||
throw new Error.NOT_SUPPORTED ("Config for 'agent.transport.path' is missing");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An agent already resident in the target, reached over a UNIX socket that the
|
||||
* hypervisor has bridged to a port the guest kernel holds open. Nothing is
|
||||
* injected, so there is no image and no stub.
|
||||
*/
|
||||
public sealed class SocketTransportConfig : TransportConfig {
|
||||
public string path {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public override void check () throws Error {
|
||||
if (path == null)
|
||||
throw new Error.NOT_SUPPORTED ("Config for 'agent.transport.path' is missing");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ImageConfig : Object, Json.Serializable {
|
||||
public string file {
|
||||
get;
|
||||
|
||||
@@ -9,6 +9,11 @@ name = "memory-scanner"
|
||||
path = "bin/memory_scanner.rs"
|
||||
test = false
|
||||
|
||||
[[bin]]
|
||||
name = "marker"
|
||||
path = "bin/marker.rs"
|
||||
test = false
|
||||
|
||||
[features]
|
||||
panic-info = []
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
build: memory-scanner-arm64.elf
|
||||
build: memory-scanner-arm64.elf marker-x86.elf
|
||||
|
||||
memory-scanner-arm64.elf:
|
||||
RUSTFLAGS="-C code-model=tiny -C link-arg=--emit-relocs -C link-arg=-n -C link-arg=--discard-all" \
|
||||
@@ -10,4 +10,13 @@ memory-scanner-arm64.elf:
|
||||
$@.tmp
|
||||
mv $@.tmp $@
|
||||
|
||||
marker-x86.elf:
|
||||
RUSTC_BOOTSTRAP=1 RUSTFLAGS="-C link-arg=--emit-relocs -C link-arg=-n -C link-arg=--discard-all" \
|
||||
cargo build -r --bin marker \
|
||||
--target ../agent/i686-unknown-none.json -Z json-target-spec \
|
||||
-Z build-std=core
|
||||
cp target/i686-unknown-none/release/marker $@.tmp
|
||||
rust-strip --strip-unneeded $@.tmp
|
||||
mv $@.tmp $@
|
||||
|
||||
.PHONY: build
|
||||
@@ -0,0 +1,17 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
static ANSWER: u32 = 0x1234abcd;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start(destination: *mut u32, _size: usize) {
|
||||
unsafe {
|
||||
*destination = &ANSWER as *const u32 as u32;
|
||||
*destination.add(1) = core::ptr::read_volatile(&ANSWER);
|
||||
}
|
||||
}
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(_info: &core::panic::PanicInfo<'_>) -> ! {
|
||||
loop {}
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,121 @@
|
||||
[CCode (gir_namespace = "FridaBarebone", gir_version = "1.0")]
|
||||
namespace Frida.Barebone {
|
||||
/**
|
||||
* What differs between the kernels we inject into: how the target is brought to a
|
||||
* quiescent state beforehand, and how it is let go afterwards.
|
||||
*/
|
||||
internal interface KernelFlavor : Object {
|
||||
public abstract async void prepare (Cancellable? cancellable) throws Error, IOError;
|
||||
public abstract async void settle (Cancellable? cancellable) throws Error, IOError;
|
||||
}
|
||||
|
||||
internal sealed class BareKernelFlavor : Object, KernelFlavor {
|
||||
private Machine machine;
|
||||
|
||||
public BareKernelFlavor (Machine machine) {
|
||||
this.machine = machine;
|
||||
}
|
||||
|
||||
public async void prepare (Cancellable? cancellable) throws Error, IOError {
|
||||
}
|
||||
|
||||
public async void settle (Cancellable? cancellable) throws Error, IOError {
|
||||
yield machine.gdb.continue (cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class XnuKernelFlavor : Object, KernelFlavor {
|
||||
private Machine machine;
|
||||
private uint64 kernel_base;
|
||||
private SymbolInfo thread_block;
|
||||
private SymbolInfo? panic;
|
||||
|
||||
public XnuKernelFlavor (Machine machine, uint64 kernel_base, Gee.Map<string, SymbolInfo> symbols)
|
||||
throws Error {
|
||||
this.machine = machine;
|
||||
this.kernel_base = kernel_base;
|
||||
|
||||
thread_block = symbols["thread_block"];
|
||||
if (thread_block == null)
|
||||
throw new Error.NOT_SUPPORTED ("Missing symbol for thread_block");
|
||||
|
||||
panic = symbols["panic"];
|
||||
}
|
||||
|
||||
public async void prepare (Cancellable? cancellable) throws Error, IOError {
|
||||
uint64 thread_block_address = kernel_base + thread_block.offset;
|
||||
|
||||
var arm64 = machine as Arm64Machine;
|
||||
if (panic != null && arm64 != null)
|
||||
arm64.call_landing_zone = kernel_base + panic.offset;
|
||||
|
||||
yield machine.enter_exception_level (1, 1000, cancellable);
|
||||
|
||||
yield run_until_thread_block (thread_block_address, cancellable);
|
||||
|
||||
if (arm64 != null)
|
||||
yield arm64.learn_permission_templates (thread_block_address, cancellable);
|
||||
}
|
||||
|
||||
public async void settle (Cancellable? cancellable) throws Error, IOError {
|
||||
GDB.Client gdb = machine.gdb;
|
||||
|
||||
// The vphone research kernel panics on any synchronous exception taken while a
|
||||
// debugger is attached, which the worker hits in the allocator during gum_init.
|
||||
var arm64 = machine as Arm64Machine;
|
||||
bool post_inject_access_uses_bridge = arm64 != null && arm64.physical_memory != null;
|
||||
if (post_inject_access_uses_bridge)
|
||||
yield gdb.detach (cancellable);
|
||||
else
|
||||
yield gdb.continue (cancellable);
|
||||
}
|
||||
|
||||
private async void run_until_thread_block (uint64 address, Cancellable? cancellable) throws Error, IOError {
|
||||
GDB.Client gdb = machine.gdb;
|
||||
var bp = yield gdb.add_breakpoint (SOFT, address, 4, cancellable);
|
||||
|
||||
GDB.Breakpoint? hit = null;
|
||||
do {
|
||||
var exception = yield gdb.continue_until_exception (cancellable);
|
||||
hit = exception.breakpoint;
|
||||
} while (hit != bp);
|
||||
|
||||
yield bp.remove (cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class Win9xKernelFlavor : Object, KernelFlavor {
|
||||
private Machine machine;
|
||||
private uint64 yield_point;
|
||||
|
||||
public Win9xKernelFlavor (Machine machine, Gee.Map<string, SymbolInfo> symbols) throws Error {
|
||||
this.machine = machine;
|
||||
|
||||
SymbolInfo? get_system_time = symbols["Get_System_Time"];
|
||||
if (get_system_time == null)
|
||||
throw new Error.NOT_SUPPORTED ("Missing symbol for Get_System_Time");
|
||||
yield_point = get_system_time.offset;
|
||||
}
|
||||
|
||||
public async void prepare (Cancellable? cancellable) throws Error, IOError {
|
||||
yield run_until_yield_point (cancellable);
|
||||
}
|
||||
|
||||
public async void settle (Cancellable? cancellable) throws Error, IOError {
|
||||
yield machine.gdb.continue (cancellable);
|
||||
}
|
||||
|
||||
private async void run_until_yield_point (Cancellable? cancellable) throws Error, IOError {
|
||||
GDB.Client gdb = machine.gdb;
|
||||
var bp = yield gdb.add_breakpoint (SOFT, yield_point, 1, cancellable);
|
||||
|
||||
GDB.Breakpoint? hit = null;
|
||||
do {
|
||||
var exception = yield gdb.continue_until_exception (cancellable);
|
||||
hit = exception.breakpoint;
|
||||
} while (hit != bp);
|
||||
|
||||
yield bp.remove (cancellable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,6 +192,62 @@ namespace Frida.Barebone {
|
||||
public abstract async void disable (Cancellable? cancellable) throws Error, IOError;
|
||||
}
|
||||
|
||||
internal enum AddressingMode {
|
||||
VIRTUAL,
|
||||
PHYSICAL
|
||||
}
|
||||
|
||||
// Restoring the mode must be awaited rather than fired and forgotten: a reply left in flight
|
||||
// arrives while the next command is pending, and that command consumes it instead — leaving
|
||||
// its caller with an empty response and the stub still in the old mode. Vala rejects a yield
|
||||
// inside a finally block, so callers hold on to any failure across the restore.
|
||||
internal static async void set_addressing_mode (GDB.Client gdb, AddressingMode mode, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
Gee.Set<string> features = gdb.features;
|
||||
string enabled = (mode == PHYSICAL) ? "1" : "0";
|
||||
if ("qemu-phy-mem-mode" in features)
|
||||
yield gdb.execute_simple ("Qqemu.PhyMemMode:" + enabled, cancellable);
|
||||
else if ("vf-phy-mem-mode" in features)
|
||||
yield gdb.execute_simple ("Qvf.PhyMemMode:" + enabled, cancellable);
|
||||
else
|
||||
throw new Error.NOT_SUPPORTED ("Unsupported GDB remote stub; please file a bug");
|
||||
}
|
||||
|
||||
internal static void throw_if_failed (GLib.Error? failure) throws Error, IOError {
|
||||
if (failure == null)
|
||||
return;
|
||||
if (failure is IOError)
|
||||
throw (IOError) failure;
|
||||
throw (Error) failure;
|
||||
}
|
||||
|
||||
internal static Gee.List<RangeDetails> coalesce_ranges (Gee.List<RangeDetails> ranges) {
|
||||
var result = new Gee.ArrayList<RangeDetails> ();
|
||||
|
||||
RangeDetails? pending = null;
|
||||
foreach (RangeDetails r in ranges) {
|
||||
if (pending == null) {
|
||||
pending = r.clone ();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (r.base_va == pending.base_va + pending.size &&
|
||||
r.base_pa == pending.base_pa + pending.size &&
|
||||
r.protection == pending.protection &&
|
||||
r.type == pending.type) {
|
||||
pending.size += r.size;
|
||||
continue;
|
||||
}
|
||||
|
||||
result.add (pending);
|
||||
pending = r.clone ();
|
||||
}
|
||||
if (pending != null)
|
||||
result.add (pending);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static uint64 round_address_up (uint64 address, size_t n) {
|
||||
return (address + n - 1) & ~((uint64) n - 1);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ backend_sources += files(
|
||||
'lzfse.vala',
|
||||
'qmp-client.vala',
|
||||
'kernel-relocation.vala',
|
||||
'kernel-flavor.vala',
|
||||
'win9x.vala',
|
||||
'win9x-service-names.vala',
|
||||
'x86-paging.vala',
|
||||
'x86-inline-hook.vala',
|
||||
'vz-stub.vala',
|
||||
'arch-x86' / 'machine.vala',
|
||||
'arch-x86_64' / 'machine.vala',
|
||||
|
||||
@@ -48,7 +48,13 @@ namespace Frida.Barebone {
|
||||
}
|
||||
|
||||
private async bool init_async (int io_priority, Cancellable? cancellable) throws Error, IOError {
|
||||
var connectable = parse_socket_address (address, port, "localhost", 4444);
|
||||
SocketConnectable connectable;
|
||||
if (address.has_prefix ("unix:")) {
|
||||
connectable = new UnixSocketAddress.with_type (address.substring (5), -1,
|
||||
UnixSocketAddressType.PATH);
|
||||
} else {
|
||||
connectable = parse_socket_address (address, port, "localhost", 4444);
|
||||
}
|
||||
|
||||
try {
|
||||
var client = new SocketClient ();
|
||||
@@ -101,13 +107,19 @@ namespace Frida.Barebone {
|
||||
}
|
||||
}
|
||||
|
||||
public async Hostlink open_hostlink (Cancellable? cancellable = null) throws Error, IOError {
|
||||
public async Hostlink open_hostlink (string? preferred_bus = null, Cancellable? cancellable = null)
|
||||
throws Error, IOError {
|
||||
#if WINDOWS
|
||||
throw new Error.NOT_SUPPORTED ("Missing open_hostlink() for Windows");
|
||||
#else
|
||||
uint64 mmio = (uint64) yield get_qom_property_int ("/machine", "hostlink-mmio", cancellable);
|
||||
uint irq = (uint) yield get_qom_property_int ("/machine", "hostlink-irq", cancellable);
|
||||
string bus = yield get_qom_property_string ("/machine", "hostlink-bus", cancellable);
|
||||
uint64 mmio = 0;
|
||||
uint irq = 0;
|
||||
string bus = preferred_bus;
|
||||
if (bus == null) {
|
||||
mmio = (uint64) yield get_qom_property_int ("/machine", "hostlink-mmio", cancellable);
|
||||
irq = (uint) yield get_qom_property_int ("/machine", "hostlink-irq", cancellable);
|
||||
bus = yield get_qom_property_string ("/machine", "hostlink-bus", cancellable);
|
||||
}
|
||||
|
||||
int fds[2];
|
||||
if (Posix.socketpair (Posix.AF_UNIX, Posix.SOCK_STREAM, 0, fds) != 0)
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
[CCode (gir_namespace = "FridaBarebone", gir_version = "1.0")]
|
||||
namespace Frida.Barebone {
|
||||
|
||||
private const string[] VMM_SERVICE_NAMES = {
|
||||
"Get_VMM_Version",
|
||||
"Get_Cur_VM_Handle",
|
||||
"Test_Cur_VM_Handle",
|
||||
"Get_Sys_VM_Handle",
|
||||
"Test_Sys_VM_Handle",
|
||||
"Validate_VM_Handle",
|
||||
"Get_VMM_Reenter_Count",
|
||||
"Begin_Reentrant_Execution",
|
||||
"End_Reentrant_Execution",
|
||||
"Install_V86_Break_Point",
|
||||
"Remove_V86_Break_Point",
|
||||
"Allocate_V86_Call_Back",
|
||||
"Allocate_PM_Call_Back",
|
||||
"Call_When_VM_Returns",
|
||||
"Schedule_Global_Event",
|
||||
"Schedule_VM_Event",
|
||||
"Call_Global_Event",
|
||||
"Call_VM_Event",
|
||||
"Cancel_Global_Event",
|
||||
"Cancel_VM_Event",
|
||||
"Call_Priority_VM_Event",
|
||||
"Cancel_Priority_VM_Event",
|
||||
"Get_NMI_Handler_Addr",
|
||||
"Set_NMI_Handler_Addr",
|
||||
"Hook_NMI_Event",
|
||||
"Call_When_VM_Ints_Enabled",
|
||||
"Enable_VM_Ints",
|
||||
"Disable_VM_Ints",
|
||||
"Map_Flat",
|
||||
"Map_Lin_To_VM_Addr",
|
||||
"Adjust_Exec_Priority",
|
||||
"Begin_Critical_Section",
|
||||
"End_Critical_Section",
|
||||
"End_Crit_And_Suspend",
|
||||
"Claim_Critical_Section",
|
||||
"Release_Critical_Section",
|
||||
"Call_When_Not_Critical",
|
||||
"Create_Semaphore",
|
||||
"Destroy_Semaphore",
|
||||
"Wait_Semaphore",
|
||||
"Signal_Semaphore",
|
||||
"Get_Crit_Section_Status",
|
||||
"Call_When_Task_Switched",
|
||||
"Suspend_VM",
|
||||
"Resume_VM",
|
||||
"No_Fail_Resume_VM",
|
||||
"Nuke_VM",
|
||||
"Crash_Cur_VM",
|
||||
"Get_Execution_Focus",
|
||||
"Set_Execution_Focus",
|
||||
"Get_Time_Slice_Priority",
|
||||
"Set_Time_Slice_Priority",
|
||||
"Get_Time_Slice_Granularity",
|
||||
"Set_Time_Slice_Granularity",
|
||||
"Get_Time_Slice_Info",
|
||||
"Adjust_Execution_Time",
|
||||
"Release_Time_Slice",
|
||||
"Wake_Up_VM",
|
||||
"Call_When_Idle",
|
||||
"Get_Next_VM_Handle",
|
||||
"Set_Global_Time_Out",
|
||||
"Set_VM_Time_Out",
|
||||
"Cancel_Time_Out",
|
||||
"Get_System_Time",
|
||||
"Get_VM_Exec_Time",
|
||||
"Hook_V86_Int_Chain",
|
||||
"Get_V86_Int_Vector",
|
||||
"Set_V86_Int_Vector",
|
||||
"Get_PM_Int_Vector",
|
||||
"Set_PM_Int_Vector",
|
||||
"Simulate_Int",
|
||||
"Simulate_Iret",
|
||||
"Simulate_Far_Call",
|
||||
"Simulate_Far_Jmp",
|
||||
"Simulate_Far_Ret",
|
||||
"Simulate_Far_Ret_N",
|
||||
"Build_Int_Stack_Frame",
|
||||
"Simulate_Push",
|
||||
"Simulate_Pop",
|
||||
"_HeapAllocate",
|
||||
"_HeapReAllocate",
|
||||
"_HeapFree",
|
||||
"_HeapGetSize",
|
||||
"_PageAllocate",
|
||||
"_PageReAllocate",
|
||||
"_PageFree",
|
||||
"_PageLock",
|
||||
"_PageUnLock",
|
||||
"_PageGetSizeAddr",
|
||||
"_PageGetAllocInfo",
|
||||
"_GetFreePageCount",
|
||||
"_GetSysPageCount",
|
||||
"_GetVMPgCount",
|
||||
"_MapIntoV86",
|
||||
"_PhysIntoV86",
|
||||
"_TestGlobalV86Mem",
|
||||
"_ModifyPageBits",
|
||||
"_CopyPageTable",
|
||||
"_LinMapIntoV86",
|
||||
"_LinPageLock",
|
||||
"_LinPageUnLock",
|
||||
"_SetResetV86Pageable",
|
||||
"_GetV86PageableArray",
|
||||
"_PageCheckLinRange",
|
||||
"_PageOutDirtyPages",
|
||||
"_PageDiscardPages",
|
||||
"_GetNulPageHandle",
|
||||
"_GetFirstV86Page",
|
||||
"_MapPhysToLinear",
|
||||
"_GetAppFlatDSAlias",
|
||||
"_SelectorMapFlat",
|
||||
"_GetDemandPageInfo",
|
||||
"_GetSetPageOutCount",
|
||||
"Hook_V86_Page",
|
||||
"_Assign_Device_V86_Pages",
|
||||
"_DeAssign_Device_V86_Pages",
|
||||
"_Get_Device_V86_Pages_Array",
|
||||
"MMGR_SetNULPageAddr",
|
||||
"_Allocate_GDT_Selector",
|
||||
"_Free_GDT_Selector",
|
||||
"_Allocate_LDT_Selector",
|
||||
"_Free_LDT_Selector",
|
||||
"_BuildDescriptorDWORDs",
|
||||
"_GetDescriptor",
|
||||
"_SetDescriptor",
|
||||
"_MMGR_Toggle_HMA",
|
||||
"Get_Fault_Hook_Addrs",
|
||||
"Hook_V86_Fault",
|
||||
"Hook_PM_Fault",
|
||||
"Hook_VMM_Fault",
|
||||
"Begin_Nest_V86_Exec",
|
||||
"Begin_Nest_Exec",
|
||||
"Exec_Int",
|
||||
"Resume_Exec",
|
||||
"End_Nest_Exec",
|
||||
"Allocate_PM_App_CB_Area",
|
||||
"Get_Cur_PM_App_CB",
|
||||
"Set_V86_Exec_Mode",
|
||||
"Set_PM_Exec_Mode",
|
||||
"Begin_Use_Locked_PM_Stack",
|
||||
"End_Use_Locked_PM_Stack",
|
||||
"Save_Client_State",
|
||||
"Restore_Client_State",
|
||||
"Exec_VxD_Int",
|
||||
"Hook_Device_Service",
|
||||
"Hook_Device_V86_API",
|
||||
"Hook_Device_PM_API",
|
||||
"System_Control",
|
||||
"Simulate_IO",
|
||||
"Install_Mult_IO_Handlers",
|
||||
"Install_IO_Handler",
|
||||
"Enable_Global_Trapping",
|
||||
"Enable_Local_Trapping",
|
||||
"Disable_Global_Trapping",
|
||||
"Disable_Local_Trapping",
|
||||
"List_Create",
|
||||
"List_Destroy",
|
||||
"List_Allocate",
|
||||
"List_Attach",
|
||||
"List_Attach_Tail",
|
||||
"List_Insert",
|
||||
"List_Remove",
|
||||
"List_Deallocate",
|
||||
"List_Get_First",
|
||||
"List_Get_Next",
|
||||
"List_Remove_First",
|
||||
"_AddInstanceItem",
|
||||
"_Allocate_Device_CB_Area",
|
||||
"_Allocate_Global_V86_Data_Area",
|
||||
"_Allocate_Temp_V86_Data_Area",
|
||||
"_Free_Temp_V86_Data_Area",
|
||||
"Get_Profile_Decimal_Int",
|
||||
"Convert_Decimal_String",
|
||||
"Get_Profile_Fixed_Point",
|
||||
"Convert_Fixed_Point_String",
|
||||
"Get_Profile_Hex_Int",
|
||||
"Convert_Hex_String",
|
||||
"Get_Profile_Boolean",
|
||||
"Convert_Boolean_String",
|
||||
"Get_Profile_String",
|
||||
"Get_Next_Profile_String",
|
||||
"Get_Environment_String",
|
||||
"Get_Exec_Path",
|
||||
"Get_Config_Directory",
|
||||
"OpenFile",
|
||||
"Get_PSP_Segment",
|
||||
"GetDOSVectors",
|
||||
"Get_Machine_Info",
|
||||
"GetSet_HMA_Info",
|
||||
"RESERVED_Set_System_Exit_Code",
|
||||
"Fatal_Error_Handler",
|
||||
"Fatal_Memory_Error",
|
||||
"Update_System_Clock",
|
||||
"Test_Debug_Installed",
|
||||
"Out_Debug_String",
|
||||
"Out_Debug_Chr",
|
||||
"In_Debug_Chr",
|
||||
"Debug_Convert_Hex_Binary",
|
||||
"Debug_Convert_Hex_Decimal",
|
||||
"Debug_Test_Valid_Handle",
|
||||
"Validate_Client_Ptr",
|
||||
"Test_Reenter",
|
||||
"Queue_Debug_String",
|
||||
"Log_Proc_Call",
|
||||
"Debug_Test_Cur_VM",
|
||||
"Get_PM_Int_Type",
|
||||
"Set_PM_Int_Type",
|
||||
"Get_Last_Updated_System_Time",
|
||||
"Get_Last_Updated_VM_Exec_Time",
|
||||
"Test_DBCS_Lead_Byte",
|
||||
"_AddFreePhysPage",
|
||||
"_PageResetHandlePAddr",
|
||||
"_SetLastV86Page",
|
||||
"_GetLastV86Page",
|
||||
"_MapFreePhysReg",
|
||||
"_UnmapFreePhysReg",
|
||||
"_XchgFreePhysReg",
|
||||
"_SetFreePhysRegCalBk",
|
||||
"Get_Next_Arena",
|
||||
"Get_Name_Of_Ugly_TSR",
|
||||
"Get_Debug_Options",
|
||||
"Set_Physical_HMA_Alias",
|
||||
"_GetGlblRng0V86IntBase",
|
||||
"_Add_Global_V86_Data_Area",
|
||||
"GetSetDetailedVMError",
|
||||
"Is_Debug_Chr",
|
||||
"Clear_Mono_Screen",
|
||||
"Out_Mono_Chr",
|
||||
"Out_Mono_String",
|
||||
"Set_Mono_Cur_Pos",
|
||||
"Get_Mono_Cur_Pos",
|
||||
"Get_Mono_Chr",
|
||||
"Locate_Byte_In_ROM",
|
||||
"Hook_Invalid_Page_Fault",
|
||||
"Unhook_Invalid_Page_Fault",
|
||||
"RESERVED_Set_Delete_On_Exit_File",
|
||||
"Close_VM",
|
||||
"Enable_Touch_1st_Meg",
|
||||
"Disable_Touch_1st_Meg",
|
||||
"Install_Exception_Handler",
|
||||
"Remove_Exception_Handler",
|
||||
"Get_Crit_Status_No_Block",
|
||||
"_GetLastUpdatedThreadExecTime",
|
||||
"_Trace_Out_Service",
|
||||
"_Debug_Out_Service",
|
||||
"_Debug_Flags_Service",
|
||||
"VMMAddImportModuleName",
|
||||
"VMM_Add_DDB",
|
||||
"VMM_Remove_DDB",
|
||||
"Test_VM_Ints_Enabled",
|
||||
"_BlockOnID",
|
||||
"Schedule_Thread_Event",
|
||||
"Cancel_Thread_Event",
|
||||
"Set_Thread_Time_Out",
|
||||
"Set_Async_Time_Out",
|
||||
"_AllocateThreadDataSlot",
|
||||
"_FreeThreadDataSlot",
|
||||
"_CreateMutex",
|
||||
"_DestroyMutex",
|
||||
"_GetMutexOwner",
|
||||
"Call_When_Thread_Switched",
|
||||
"VMMCreateThread",
|
||||
"_GetThreadExecTime",
|
||||
"VMMTerminateThread",
|
||||
"Get_Cur_Thread_Handle",
|
||||
"Test_Cur_Thread_Handle",
|
||||
"Get_Sys_Thread_Handle",
|
||||
"Test_Sys_Thread_Handle",
|
||||
"Validate_Thread_Handle",
|
||||
"Get_Initial_Thread_Handle",
|
||||
"Test_Initial_Thread_Handle",
|
||||
"Debug_Test_Valid_Thread_Handle",
|
||||
"Debug_Test_Cur_Thread",
|
||||
"VMM_GetSystemInitState",
|
||||
"Cancel_Call_When_Thread_Switched",
|
||||
"Get_Next_Thread_Handle",
|
||||
"Adjust_Thread_Exec_Priority",
|
||||
"_Deallocate_Device_CB_Area",
|
||||
"Remove_IO_Handler",
|
||||
"Remove_Mult_IO_Handlers",
|
||||
"Unhook_V86_Int_Chain",
|
||||
"Unhook_V86_Fault",
|
||||
"Unhook_PM_Fault",
|
||||
"Unhook_VMM_Fault",
|
||||
"Unhook_Device_Service",
|
||||
"_PageReserve",
|
||||
"_PageCommit",
|
||||
"_PageDecommit",
|
||||
"_PagerRegister",
|
||||
"_PagerQuery",
|
||||
"_PagerDeregister",
|
||||
"_ContextCreate",
|
||||
"_ContextDestroy",
|
||||
"_PageAttach",
|
||||
"_PageFlush",
|
||||
"_SignalID",
|
||||
"_PageCommitPhys",
|
||||
"_Register_Win32_Services",
|
||||
"Cancel_Call_When_Not_Critical",
|
||||
"Cancel_Call_When_Idle",
|
||||
"Cancel_Call_When_Task_Switched",
|
||||
"_Debug_Printf_Service",
|
||||
"_EnterMutex",
|
||||
"_LeaveMutex",
|
||||
"Simulate_VM_IO",
|
||||
"Signal_Semaphore_No_Switch",
|
||||
"_ContextSwitch",
|
||||
"_PageModifyPermissions",
|
||||
"_PageQuery",
|
||||
"_EnterMustComplete",
|
||||
"_LeaveMustComplete",
|
||||
"_ResumeExecMustComplete",
|
||||
"_GetThreadTerminationStatus",
|
||||
"_GetInstanceInfo",
|
||||
"_ExecIntMustComplete",
|
||||
"_ExecVxDIntMustComplete",
|
||||
"Begin_V86_Serialization",
|
||||
"Unhook_V86_Page",
|
||||
"VMM_GetVxDLocationList",
|
||||
"VMM_GetDDBList",
|
||||
"Unhook_NMI_Event",
|
||||
"Get_Instanced_V86_Int_Vector",
|
||||
"Get_Set_Real_DOS_PSP",
|
||||
"Call_Priority_Thread_Event",
|
||||
"Get_System_Time_Address",
|
||||
"Get_Crit_Status_Thread",
|
||||
"Get_DDB",
|
||||
"Directed_Sys_Control",
|
||||
"_RegOpenKey",
|
||||
"_RegCloseKey",
|
||||
"_RegCreateKey",
|
||||
"_RegDeleteKey",
|
||||
"_RegEnumKey",
|
||||
"_RegQueryValue",
|
||||
"_RegSetValue",
|
||||
"_RegDeleteValue",
|
||||
"_RegEnumValue",
|
||||
"_RegQueryValueEx",
|
||||
"_RegSetValueEx",
|
||||
"_CallRing3",
|
||||
"Exec_PM_Int",
|
||||
"_RegFlushKey",
|
||||
"_PageCommitContig",
|
||||
"_GetCurrentContext",
|
||||
"_LocalizeSprintf",
|
||||
"_LocalizeStackSprintf",
|
||||
"Call_Restricted_Event",
|
||||
"Cancel_Restricted_Event",
|
||||
"Register_PEF_Provider",
|
||||
"_GetPhysPageInfo",
|
||||
"_RegQueryInfoKey",
|
||||
"MemArb_Reserve_Pages",
|
||||
"Time_Slice_Sys_VM_Idle",
|
||||
"Time_Slice_Sleep",
|
||||
"Boost_With_Decay",
|
||||
"Set_Inversion_Pri",
|
||||
"Reset_Inversion_Pri",
|
||||
"Release_Inversion_Pri",
|
||||
"Get_Thread_Win32_Pri",
|
||||
"Set_Thread_Win32_Pri",
|
||||
"Set_Thread_Static_Boost",
|
||||
"Set_VM_Static_Boost",
|
||||
"Release_Inversion_Pri_ID",
|
||||
"Attach_Thread_To_Group",
|
||||
"Detach_Thread_From_Group",
|
||||
"Set_Group_Static_Boost",
|
||||
"_GetRegistryPath",
|
||||
"_GetRegistryKey",
|
||||
"Cleanup_Thread_State",
|
||||
"_RegRemapPreDefKey",
|
||||
"End_V86_Serialization",
|
||||
"_Assert_Range",
|
||||
"_Sprintf",
|
||||
"_PageChangePager",
|
||||
"_RegCreateDynKey",
|
||||
"_RegQueryMultipleValues",
|
||||
"Boost_Thread_With_VM",
|
||||
"Get_Boot_Flags",
|
||||
"Set_Boot_Flags",
|
||||
"_lstrcpyn",
|
||||
"_lstrlen",
|
||||
"_lmemcpy",
|
||||
"_GetVxDName",
|
||||
"Force_Mutexes_Free",
|
||||
"Restore_Forced_Mutexes",
|
||||
"_AddReclaimableItem",
|
||||
"_SetReclaimableItem",
|
||||
"_EnumReclaimableItem",
|
||||
"Time_Slice_Wake_Sys_VM",
|
||||
"VMM_Replace_Global_Environment",
|
||||
"Begin_Non_Serial_Nest_V86_Exec",
|
||||
"Get_Nest_Exec_Status",
|
||||
"Open_Boot_Log",
|
||||
"Write_Boot_Log",
|
||||
"Close_Boot_Log",
|
||||
"EnableDisable_Boot_Log",
|
||||
"_Call_On_My_Stack",
|
||||
"Get_Inst_V86_Int_Vec_Base",
|
||||
"_lstrcmpi",
|
||||
"_strupr",
|
||||
"Log_Fault_Call_Out",
|
||||
"_AtEventTime",
|
||||
"_PageOutPages",
|
||||
"_Call_On_My_Not_Flat_Stack",
|
||||
"_LinRegionLock",
|
||||
"_LinRegionUnLock",
|
||||
"_AttemptingSomethingDangerous",
|
||||
"_Vsprintf",
|
||||
"_Vsprintfw",
|
||||
"Load_FS_Service",
|
||||
"Assert_FS_Service",
|
||||
"ObsoleteRtlUnwind",
|
||||
"ObsoleteRtlRaiseException",
|
||||
"ObsoleteRtlRaiseStatus",
|
||||
"ObsoleteKeGetCurrentIrql",
|
||||
"ObsoleteKfRaiseIrql",
|
||||
"ObsoleteKfLowerIrql",
|
||||
"_Begin_Preemptable_Code",
|
||||
"_End_Preemptable_Code",
|
||||
"Set_Preemptable_Count",
|
||||
"ObsoleteKeInitializeDpc",
|
||||
"ObsoleteKeInsertQueueDpc",
|
||||
"ObsoleteKeRemoveQueueDpc",
|
||||
"HeapAllocateEx",
|
||||
"HeapReAllocateEx",
|
||||
"HeapGetSizeEx",
|
||||
"HeapFreeEx",
|
||||
"_Get_CPUID_Flags",
|
||||
"KeCheckDivideByZeroTrap",
|
||||
"_RegisterGARTHandler",
|
||||
"_GARTReserve",
|
||||
"_GARTCommit",
|
||||
"_GARTUnCommit",
|
||||
"_GARTFree",
|
||||
"_GARTMemAttributes",
|
||||
"KfRaiseIrqlToDpcLevel",
|
||||
"VMMCreateThreadEx",
|
||||
"_FlushCaches",
|
||||
"Set_Thread_Win32_Pri_NoYield",
|
||||
"_FlushMappedCacheBlock",
|
||||
"_ReleaseMappedCacheBlock",
|
||||
"Run_Preemptable_Events",
|
||||
"_MMPreSystemExit",
|
||||
"_MMPageFileShutDown",
|
||||
"_Set_Global_Time_Out_Ex",
|
||||
"Query_Thread_Priority",
|
||||
"_UnmapPhysToLinear",
|
||||
"_VmmRtInfo",
|
||||
"_MPGetProcessorCount",
|
||||
"_MPEnterSingleProcessor",
|
||||
"_MPLeaveSingleProcessor",
|
||||
};
|
||||
|
||||
private const string[] VWIN32_SERVICE_NAMES = {
|
||||
"VWIN32_Get_Version",
|
||||
"VWIN32_DIOCCompletionRoutine",
|
||||
"_VWIN32_QueueUserApc",
|
||||
"_VWIN32_Get_Thread_Context",
|
||||
"_VWIN32_Set_Thread_Context",
|
||||
"_VWIN32_CopyMem",
|
||||
"_VWIN32_Npx_Exception",
|
||||
"_VWIN32_Emulate_Npx",
|
||||
"_VWIN32_CheckDelayedNpxTrap",
|
||||
"VWIN32_EnterCrstR0",
|
||||
"VWIN32_LeaveCrstR0",
|
||||
"_VWIN32_FaultPopup",
|
||||
"VWIN32_GetContextHandle",
|
||||
"VWIN32_GetCurrentProcessHandle",
|
||||
"_VWIN32_SetWin32Event",
|
||||
"_VWIN32_PulseWin32Event",
|
||||
"_VWIN32_ResetWin32Event",
|
||||
"_VWIN32_WaitSingleObject",
|
||||
"_VWIN32_WaitMultipleObjects",
|
||||
"_VWIN32_CreateRing0Thread",
|
||||
"_VWIN32_CloseVxDHandle",
|
||||
"VWIN32_ActiveTimeBiasSet",
|
||||
"VWIN32_GetCurrentDirectory",
|
||||
"VWIN32_BlueScreenPopup",
|
||||
"VWIN32_TerminateApp",
|
||||
"_VWIN32_QueueKernelAPC",
|
||||
"VWIN32_SysErrorBox",
|
||||
"_VWIN32_IsClientWin32",
|
||||
"VWIN32_IFSRIPWhenLev2Taken",
|
||||
"_VWIN32_InitWin32Event",
|
||||
"_VWIN32_InitWin32Mutex",
|
||||
"_VWIN32_ReleaseWin32Mutex",
|
||||
"_VWIN32_BlockThreadEx",
|
||||
"VWIN32_GetProcessHandle",
|
||||
"_VWIN32_InitWin32Semaphore",
|
||||
"_VWIN32_SignalWin32Sem",
|
||||
"_VWIN32_QueueUserApcEx",
|
||||
"_VWIN32_OpenVxDHandle",
|
||||
"_VWIN32_CloseWin32Handle",
|
||||
"_VWIN32_AllocExternalHandle",
|
||||
"_VWIN32_UseExternalHandle",
|
||||
"_VWIN32_UnuseExternalHandle",
|
||||
"KeInitializeTimer",
|
||||
"KeSetTimer",
|
||||
"KeCancelTimer",
|
||||
"KeReadStateTimer",
|
||||
"_VWIN32_ReferenceObject",
|
||||
"_VWIN32_GetExternalHandle",
|
||||
"VWIN32_ConvertNtTimeout",
|
||||
"_VWIN32_SetWin32EventBoostPriority",
|
||||
"_VWIN32_GetRing3Flat32Selectors",
|
||||
"_VWIN32_GetCurThreadCondition",
|
||||
"VWIN32_Init_FP",
|
||||
"R0SetWaitableTimer",
|
||||
};
|
||||
|
||||
private const string[] VPICD_SERVICE_NAMES = {
|
||||
"VPICD_Get_Version",
|
||||
"VPICD_Virtualize_IRQ",
|
||||
"VPICD_Set_Int_Request",
|
||||
"VPICD_Clear_Int_Request",
|
||||
"VPICD_Phys_EOI",
|
||||
"VPICD_Get_Complete_Status",
|
||||
"VPICD_Get_Status",
|
||||
"VPICD_Test_Phys_Request",
|
||||
"VPICD_Physically_Mask",
|
||||
"VPICD_Physically_Unmask",
|
||||
"VPICD_Set_Auto_Masking",
|
||||
"VPICD_Get_IRQ_Complete_Status",
|
||||
"VPICD_Convert_Handle_To_IRQ",
|
||||
"VPICD_Convert_IRQ_To_Int",
|
||||
"VPICD_Convert_Int_To_IRQ",
|
||||
"VPICD_Call_When_Hw_Int",
|
||||
"VPICD_Force_Default_Owner",
|
||||
"VPICD_Force_Default_Behavior",
|
||||
"VPICD_Auto_Mask_At_Inst_Swap",
|
||||
"VPICD_Begin_Inst_Page_Swap",
|
||||
"VPICD_End_Inst_Page_Swap",
|
||||
"VPICD_Virtual_EOI",
|
||||
"VPICD_Get_Virtualization_Count",
|
||||
"VPICD_Post_Sys_Critical_Init",
|
||||
"VPICD_VM_SlavePIC_Mask_Change",
|
||||
"_VPICD_Clear_IR_Bits",
|
||||
"RESERVED_VPICD_Get_Level_Mask",
|
||||
"RESERVED_VPICD_Set_Level_Mask",
|
||||
"_VPICD_Set_Irql_Mask",
|
||||
"_VPICD_Set_Channel_Irql",
|
||||
"_VPICD_Prepare_For_Shutdown",
|
||||
"_VPICD_Register_Trigger_Handler",
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
[CCode (gir_namespace = "FridaBarebone", gir_version = "1.0")]
|
||||
namespace Frida.Barebone {
|
||||
public static async Gee.List<SymbolInfo> collect_win9x_symbols (Machine machine, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
var symbols = new Gee.ArrayList<SymbolInfo> ();
|
||||
|
||||
foreach (DeviceDescriptorBlock ddb in yield find_descriptor_blocks (machine, cancellable)) {
|
||||
unowned string[]? names = known_service_names (ddb.name);
|
||||
if (names == null)
|
||||
continue;
|
||||
|
||||
var addresses = yield read_service_table (machine, ddb, cancellable);
|
||||
for (int ordinal = 0; ordinal != addresses.size; ordinal++) {
|
||||
uint64 address = addresses[ordinal];
|
||||
if (!is_implemented (address))
|
||||
continue;
|
||||
|
||||
symbols.add (new SymbolInfo () {
|
||||
name = (ordinal < names.length)
|
||||
? names[ordinal]
|
||||
: "%s_service_%d".printf (ddb.name, ordinal),
|
||||
offset = (uint32) address,
|
||||
symbol_type = 0xf,
|
||||
section = 0x10,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
||||
private static async Gee.List<DeviceDescriptorBlock> find_descriptor_blocks (Machine machine, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
var arena = new Gee.ArrayList<RangeDetails> ();
|
||||
yield machine.enumerate_ranges (Gum.PageProtection.READ, r => {
|
||||
if (r.base_va >= SYSTEM_ARENA_BASE && r.base_va < SYSTEM_ARENA_LIMIT)
|
||||
arena.add (r.clone ());
|
||||
return true;
|
||||
}, cancellable);
|
||||
|
||||
var blocks = new Gee.ArrayList<DeviceDescriptorBlock> ();
|
||||
foreach (RangeDetails r in arena) {
|
||||
foreach (DeviceDescriptorBlock candidate in yield harvest_candidates (machine, r, cancellable)) {
|
||||
if (yield candidate.is_credible (machine, cancellable))
|
||||
blocks.add (candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static async Gee.List<DeviceDescriptorBlock> harvest_candidates (Machine machine, RangeDetails range,
|
||||
Cancellable? cancellable) throws Error, IOError {
|
||||
var candidates = new Gee.ArrayList<DeviceDescriptorBlock> ();
|
||||
|
||||
GDB.Client gdb = machine.gdb;
|
||||
for (uint64 offset = 0; offset < range.size; offset += SCAN_CHUNK_SIZE) {
|
||||
uint64 chunk_start = range.base_va + offset;
|
||||
uint64 remaining = range.size - offset;
|
||||
size_t readable_size = (size_t) uint64.min (SCAN_CHUNK_SIZE + STRADDLE_ALLOWANCE, remaining);
|
||||
size_t claimed_size = (size_t) uint64.min (SCAN_CHUNK_SIZE, remaining);
|
||||
|
||||
Bytes chunk;
|
||||
try {
|
||||
chunk = yield gdb.read_byte_array (chunk_start, readable_size, cancellable);
|
||||
} catch (Error e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Buffer buf = gdb.make_buffer (chunk);
|
||||
for (size_t pos = 0; pos < claimed_size && pos + DDB_SIZE <= readable_size; pos += 4) {
|
||||
DeviceDescriptorBlock? ddb = DeviceDescriptorBlock.parse (buf, pos, chunk_start + pos);
|
||||
if (ddb != null)
|
||||
candidates.add (ddb);
|
||||
}
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static async Gee.List<uint64?> read_service_table (Machine machine, DeviceDescriptorBlock ddb,
|
||||
Cancellable? cancellable) throws Error, IOError {
|
||||
Bytes raw = yield machine.gdb.read_byte_array (ddb.service_table, ddb.service_count * 4, cancellable);
|
||||
Buffer buf = machine.gdb.make_buffer (raw);
|
||||
|
||||
var addresses = new Gee.ArrayList<uint64?> ();
|
||||
for (uint i = 0; i != ddb.service_count; i++)
|
||||
addresses.add (buf.read_uint32 (i * 4));
|
||||
return addresses;
|
||||
}
|
||||
|
||||
private class DeviceDescriptorBlock {
|
||||
public string name;
|
||||
public uint64 address;
|
||||
public uint64 service_table;
|
||||
public uint32 service_count;
|
||||
|
||||
public static DeviceDescriptorBlock? parse (Buffer buf, size_t offset, uint64 address) {
|
||||
string? name = parse_name (buf, offset + NAME_OFFSET);
|
||||
if (name == null)
|
||||
return null;
|
||||
|
||||
uint32 count = buf.read_uint32 (offset + SERVICE_TABLE_SIZE_OFFSET);
|
||||
if (count == 0 || count > MAX_SERVICES)
|
||||
return null;
|
||||
|
||||
uint32 table = buf.read_uint32 (offset + SERVICE_TABLE_PTR_OFFSET);
|
||||
if (!is_arena_address (table))
|
||||
return null;
|
||||
|
||||
uint32 control_proc = buf.read_uint32 (offset + CONTROL_PROC_OFFSET);
|
||||
if (control_proc != 0 && !is_arena_address (control_proc))
|
||||
return null;
|
||||
|
||||
return new DeviceDescriptorBlock () {
|
||||
name = name,
|
||||
address = address,
|
||||
service_table = table,
|
||||
service_count = count,
|
||||
};
|
||||
}
|
||||
|
||||
public async bool is_credible (Machine machine, Cancellable? cancellable) throws Error, IOError {
|
||||
Bytes first_entry;
|
||||
try {
|
||||
first_entry = yield machine.gdb.read_byte_array (service_table, 4, cancellable);
|
||||
} catch (Error e) {
|
||||
return false;
|
||||
}
|
||||
return is_arena_address (machine.gdb.make_buffer (first_entry).read_uint32 (0));
|
||||
}
|
||||
|
||||
private static string? parse_name (Buffer buf, size_t offset) {
|
||||
var name = new StringBuilder ();
|
||||
bool padding_reached = false;
|
||||
for (size_t i = 0; i != NAME_SIZE; i++) {
|
||||
char c = (char) buf.read_uint8 (offset + i);
|
||||
if (c == ' ') {
|
||||
padding_reached = true;
|
||||
continue;
|
||||
}
|
||||
if (padding_reached)
|
||||
return null;
|
||||
if (!c.isupper () && !c.isdigit () && c != '_' && c != '$')
|
||||
return null;
|
||||
name.append_c (c);
|
||||
}
|
||||
if (name.len == 0)
|
||||
return null;
|
||||
return name.str;
|
||||
}
|
||||
|
||||
private const size_t CONTROL_PROC_OFFSET = 0x18;
|
||||
private const size_t NAME_OFFSET = 0x0c;
|
||||
private const size_t NAME_SIZE = 8;
|
||||
private const size_t SERVICE_TABLE_PTR_OFFSET = 0x30;
|
||||
private const size_t SERVICE_TABLE_SIZE_OFFSET = 0x34;
|
||||
}
|
||||
|
||||
private static unowned string[]? known_service_names (string vxd) {
|
||||
if (vxd == "VMM")
|
||||
return VMM_SERVICE_NAMES;
|
||||
if (vxd == "VPICD")
|
||||
return VPICD_SERVICE_NAMES;
|
||||
if (vxd == "VWIN32")
|
||||
return VWIN32_SERVICE_NAMES;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool is_arena_address (uint64 address) {
|
||||
return address >= SYSTEM_ARENA_BASE && address < SYSTEM_ARENA_LIMIT;
|
||||
}
|
||||
|
||||
private static bool is_implemented (uint64 service_address) {
|
||||
return service_address != 0;
|
||||
}
|
||||
|
||||
private const size_t DDB_SIZE = 0x38;
|
||||
private const size_t STRADDLE_ALLOWANCE = DDB_SIZE;
|
||||
|
||||
private const uint64 SYSTEM_ARENA_BASE = 0xc0000000;
|
||||
private const uint64 SYSTEM_ARENA_LIMIT = 0xc4000000;
|
||||
|
||||
private const uint32 MAX_SERVICES = 0x400;
|
||||
|
||||
private const size_t SCAN_CHUNK_SIZE = 256 * 1024;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
[CCode (gir_namespace = "FridaBarebone", gir_version = "1.0")]
|
||||
namespace Frida.Barebone {
|
||||
internal class X86InlineHook : Object, InlineHook {
|
||||
private uint64 target;
|
||||
private Bytes old_target_code;
|
||||
private Bytes new_target_code;
|
||||
private Allocation allocation;
|
||||
private GDB.Client gdb;
|
||||
private State state = DISABLED;
|
||||
|
||||
private enum State {
|
||||
DISABLED,
|
||||
ENABLED,
|
||||
DESTROYED
|
||||
}
|
||||
|
||||
private const size_t SCRATCH_SIZE = 512;
|
||||
private const size_t DIRECT_REDIRECT_SIZE = 5;
|
||||
private const size_t INDIRECT_REDIRECT_SIZE = 16;
|
||||
private const size_t MAX_INSTRUCTION_SIZE = 15;
|
||||
|
||||
public static async InlineHook create (uint64 target, uint64 handler, Gum.CpuType cpu, Allocator allocator,
|
||||
GDB.Client gdb, Cancellable? cancellable) throws Error, IOError {
|
||||
Allocation allocation = yield allocator.allocate (allocator.page_size, allocator.page_size, cancellable);
|
||||
uint64 code_va = allocation.virtual_address;
|
||||
|
||||
var scratch = new uint8[SCRATCH_SIZE];
|
||||
size_t pointer_size = (cpu == IA32) ? 4 : 8;
|
||||
|
||||
var cw = new Gum.X86Writer (scratch);
|
||||
cw.set_target_cpu (cpu);
|
||||
cw.pc = code_va;
|
||||
|
||||
emit_prolog (cw, target);
|
||||
cw.put_mov_reg_reg (Gum.X86Reg.XAX, Gum.X86Reg.XSP);
|
||||
cw.put_call_address_with_arguments (CAPI, handler, 1, Gum.ArgType.REGISTER, Gum.X86Reg.XAX);
|
||||
emit_epilog (cw, pointer_size);
|
||||
|
||||
size_t redirect_size = Gum.X86Writer.can_branch_directly_between (target, code_va)
|
||||
? DIRECT_REDIRECT_SIZE
|
||||
: INDIRECT_REDIRECT_SIZE;
|
||||
|
||||
Bytes displaced = yield gdb.read_byte_array (target, redirect_size + MAX_INSTRUCTION_SIZE, cancellable);
|
||||
|
||||
var rl = new Gum.X86Relocator (displaced.get_data (), cw);
|
||||
rl.input_pc = target;
|
||||
uint reloc_bytes = 0;
|
||||
do
|
||||
reloc_bytes = rl.read_one ();
|
||||
while (reloc_bytes < redirect_size);
|
||||
rl.write_all ();
|
||||
if (!rl.eoi)
|
||||
cw.put_jmp_address (target + reloc_bytes);
|
||||
cw.flush ();
|
||||
|
||||
var trampoline_code = new Bytes (scratch[:cw.offset ()]);
|
||||
yield gdb.write_byte_array (code_va, trampoline_code, cancellable);
|
||||
|
||||
cw.reset (scratch);
|
||||
cw.set_target_cpu (cpu);
|
||||
cw.pc = target;
|
||||
cw.put_jmp_address (code_va);
|
||||
cw.flush ();
|
||||
var new_target_code = new Bytes (scratch[:cw.offset ()]);
|
||||
|
||||
var old_target_code = new Bytes (displaced.get_data ()[:new_target_code.get_size ()]);
|
||||
|
||||
return new X86InlineHook (target, old_target_code, new_target_code, allocation, gdb);
|
||||
}
|
||||
|
||||
private static void emit_prolog (Gum.X86Writer cw, uint64 target) {
|
||||
cw.put_push_u32 (0);
|
||||
cw.put_pushax ();
|
||||
cw.put_mov_reg_address (Gum.X86Reg.XAX, target);
|
||||
cw.put_push_reg (Gum.X86Reg.XAX);
|
||||
}
|
||||
|
||||
private static void emit_epilog (Gum.X86Writer cw, size_t pointer_size) {
|
||||
cw.put_add_reg_imm (Gum.X86Reg.XSP, (ssize_t) pointer_size);
|
||||
cw.put_popax ();
|
||||
cw.put_add_reg_imm (Gum.X86Reg.XSP, (ssize_t) pointer_size);
|
||||
}
|
||||
|
||||
public X86InlineHook (uint64 target, Bytes old_target_code, Bytes new_target_code, Allocation allocation,
|
||||
GDB.Client gdb) {
|
||||
this.target = target;
|
||||
this.old_target_code = old_target_code;
|
||||
this.new_target_code = new_target_code;
|
||||
this.allocation = allocation;
|
||||
this.gdb = gdb;
|
||||
}
|
||||
|
||||
public async void destroy (Cancellable? cancellable) throws Error, IOError {
|
||||
if (state == DESTROYED)
|
||||
return;
|
||||
|
||||
bool was_running = gdb.state != STOPPED;
|
||||
if (was_running)
|
||||
yield gdb.stop (cancellable);
|
||||
|
||||
yield disable (cancellable);
|
||||
yield allocation.deallocate (cancellable);
|
||||
state = DESTROYED;
|
||||
|
||||
if (was_running)
|
||||
yield gdb.continue (cancellable);
|
||||
}
|
||||
|
||||
public async void enable (Cancellable? cancellable) throws Error, IOError {
|
||||
if (state == ENABLED)
|
||||
return;
|
||||
if (state != DISABLED)
|
||||
throw new Error.INVALID_OPERATION ("Invalid operation");
|
||||
yield gdb.write_byte_array (target, new_target_code, cancellable);
|
||||
state = ENABLED;
|
||||
}
|
||||
|
||||
public async void disable (Cancellable? cancellable) throws Error, IOError {
|
||||
if (state != ENABLED)
|
||||
return;
|
||||
yield gdb.write_byte_array (target, old_target_code, cancellable);
|
||||
state = DISABLED;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,716 @@
|
||||
[CCode (gir_namespace = "FridaBarebone", gir_version = "1.0")]
|
||||
namespace Frida.Barebone {
|
||||
internal class X86PageTables : Object {
|
||||
public GDB.Client gdb {
|
||||
get;
|
||||
construct;
|
||||
}
|
||||
|
||||
public PhysicalMemory? physical_memory {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
// Fixed for the kernel we are attached to.
|
||||
private MMUParameters? cached_parameters;
|
||||
|
||||
private const size_t PAGE_SIZE = 4096;
|
||||
|
||||
private const Gum.PageProtection MAPPING_PROTECTION =
|
||||
Gum.PageProtection.READ | Gum.PageProtection.WRITE | Gum.PageProtection.EXECUTE;
|
||||
|
||||
private const uint64 PRESENT_BIT = 1ULL << 0;
|
||||
private const uint64 WRITABLE_BIT = 1ULL << 1;
|
||||
private const uint64 LARGE_PAGE_BIT = 1ULL << 7;
|
||||
private const uint64 NX_BIT = 1ULL << 63;
|
||||
|
||||
private const uint64 LEGACY_ADDRESS_MASK = 0xfffff000ULL;
|
||||
private const uint64 EXTENDED_ADDRESS_MASK = 0x000ffffffffff000ULL;
|
||||
|
||||
private const uint64 LEGACY_LARGE_PAGE_ADDRESS_MASK = 0xffc00000ULL;
|
||||
private const uint64 PSE36_ADDRESS_MASK = 0x001fe000ULL;
|
||||
private const uint PSE36_ADDRESS_SHIFT = 19;
|
||||
|
||||
public X86PageTables (GDB.Client gdb) {
|
||||
Object (gdb: gdb);
|
||||
}
|
||||
|
||||
public async Gee.List<RangeDetails> collect_ranges (Cancellable? cancellable) throws Error, IOError {
|
||||
var result = new Gee.ArrayList<RangeDetails> ();
|
||||
|
||||
MMUParameters p = yield load_parameters (cancellable);
|
||||
if (!p.paging_enabled) {
|
||||
result.add (new RangeDetails (0, 0, 1ULL << 32, READ | WRITE | EXECUTE, MappingType.UNKNOWN));
|
||||
return result;
|
||||
}
|
||||
|
||||
yield begin_access (cancellable);
|
||||
GLib.Error? failure = null;
|
||||
try {
|
||||
yield collect_ranges_in_table (p.root_table, 0, 0, READ | WRITE | EXECUTE, p, result, cancellable);
|
||||
} catch (GLib.Error e) {
|
||||
failure = e;
|
||||
}
|
||||
yield end_access (cancellable);
|
||||
throw_if_failed (failure);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async void collect_ranges_in_table (uint64 table_pa, uint level, uint64 upper_bits,
|
||||
Gum.PageProtection inherited, MMUParameters p, Gee.List<RangeDetails> ranges,
|
||||
Cancellable? cancellable) throws Error, IOError {
|
||||
Level l = p.levels[level];
|
||||
Buffer entries = yield read_buffer (table_pa, l.num_entries * p.entry_size, cancellable);
|
||||
|
||||
for (uint i = 0; i != l.num_entries; i++) {
|
||||
uint64 entry = read_entry (entries, i * p.entry_size, p);
|
||||
if ((entry & PRESENT_BIT) == 0)
|
||||
continue;
|
||||
|
||||
uint64 va = upper_bits | ((uint64) i << l.shift);
|
||||
Gum.PageProtection prot = inherited & protection_from_entry (entry, level, p);
|
||||
|
||||
if (is_leaf_entry (entry, level, p)) {
|
||||
ranges.add (new RangeDetails (canonicalize (va, p), leaf_address (entry, level, p),
|
||||
1ULL << l.shift, prot, MappingType.UNKNOWN));
|
||||
continue;
|
||||
}
|
||||
|
||||
yield collect_ranges_in_table (table_address (entry, p), level + 1, va, prot, p, ranges,
|
||||
cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
public async uint64 translate (uint64 va, Cancellable? cancellable) throws Error, IOError {
|
||||
MMUParameters p = yield load_parameters (cancellable);
|
||||
if (!p.paging_enabled)
|
||||
return va;
|
||||
|
||||
yield begin_access (cancellable);
|
||||
uint64 pa = 0;
|
||||
GLib.Error? failure = null;
|
||||
try {
|
||||
LeafEntry leaf = yield find_leaf_entry (va, p, cancellable);
|
||||
uint64 page_mask = (1ULL << p.levels[leaf.level].shift) - 1;
|
||||
|
||||
pa = leaf_address (leaf.value, leaf.level, p) | (va & page_mask);
|
||||
} catch (GLib.Error e) {
|
||||
failure = e;
|
||||
}
|
||||
yield end_access (cancellable);
|
||||
throw_if_failed (failure);
|
||||
|
||||
return pa;
|
||||
}
|
||||
|
||||
public async void protect (uint64 va, size_t size, Gum.PageProtection prot, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
MMUParameters p = yield load_parameters (cancellable);
|
||||
if (!p.paging_enabled)
|
||||
throw new Error.INVALID_OPERATION ("Unable to change protection while paging is disabled");
|
||||
|
||||
uint64 start_va = page_start (va, PAGE_SIZE);
|
||||
uint64 end_va = round_address_up (va + size, PAGE_SIZE);
|
||||
|
||||
yield begin_access (cancellable);
|
||||
GLib.Error? failure = null;
|
||||
try {
|
||||
var relaxed_slots = new Gee.HashSet<uint64?> (Numeric.uint64_hash, Numeric.uint64_equal);
|
||||
Level leaf = p.levels[p.leaf_level];
|
||||
|
||||
uint64 page_va = start_va;
|
||||
while (page_va != end_va) {
|
||||
uint64 table_pa = yield find_leaf_table (page_va, p, cancellable);
|
||||
|
||||
uint index = (uint) ((page_va >> leaf.shift) & (leaf.num_entries - 1));
|
||||
uint64 remaining_here = (uint64) (leaf.num_entries - index) * PAGE_SIZE;
|
||||
uint64 chunk_end = uint64.min (end_va, page_va + remaining_here);
|
||||
uint num_pages = (uint) ((chunk_end - page_va) / PAGE_SIZE);
|
||||
|
||||
yield relax_parent_entries (page_va, prot, p, relaxed_slots, cancellable);
|
||||
|
||||
uint64 first_slot = table_pa + ((uint64) index * p.entry_size);
|
||||
Buffer current = yield read_buffer (first_slot, num_pages * p.entry_size,
|
||||
cancellable);
|
||||
|
||||
var builder = gdb.make_buffer_builder ();
|
||||
bool changed = false;
|
||||
for (uint i = 0; i != num_pages; i++) {
|
||||
uint64 entry = read_entry (current, i * p.entry_size, p);
|
||||
uint64 updated = apply_protection_bits (entry, prot, p);
|
||||
changed = changed || updated != entry;
|
||||
if (p.entry_size == 8)
|
||||
builder.append_uint64 (updated);
|
||||
else
|
||||
builder.append_uint32 ((uint32) updated);
|
||||
}
|
||||
if (changed)
|
||||
yield write_buffer (first_slot, builder.build (), cancellable);
|
||||
|
||||
page_va = chunk_end;
|
||||
}
|
||||
} catch (GLib.Error e) {
|
||||
failure = e;
|
||||
}
|
||||
yield end_access (cancellable);
|
||||
throw_if_failed (failure);
|
||||
}
|
||||
|
||||
private async uint64 find_leaf_table (uint64 va, MMUParameters p, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
uint64 table_pa = p.root_table;
|
||||
|
||||
for (uint level = 0; level != p.leaf_level; level++) {
|
||||
uint64 entry = yield read_entry_at (slot_address (va, table_pa, level, p), p, cancellable);
|
||||
if ((entry & PRESENT_BIT) == 0) {
|
||||
throw new Error.NOT_SUPPORTED ("Unable to resolve 0x%" + uint64.FORMAT_MODIFIER + "x",
|
||||
va);
|
||||
}
|
||||
if (is_leaf_entry (entry, level, p)) {
|
||||
throw new Error.NOT_SUPPORTED (
|
||||
"Unable to change protection of the large page mapping at 0x%" +
|
||||
uint64.FORMAT_MODIFIER + "x", va);
|
||||
}
|
||||
table_pa = table_address (entry, p);
|
||||
}
|
||||
|
||||
return table_pa;
|
||||
}
|
||||
|
||||
private async LeafEntry find_leaf_entry (uint64 va, MMUParameters p, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
uint64 table_pa = p.root_table;
|
||||
for (uint level = 0; ; level++) {
|
||||
uint64 slot_pa = slot_address (va, table_pa, level, p);
|
||||
uint64 entry = yield read_entry_at (slot_pa, p, cancellable);
|
||||
|
||||
if ((entry & PRESENT_BIT) == 0) {
|
||||
throw new Error.NOT_SUPPORTED ("Address 0x%" + uint64.FORMAT_MODIFIER + "x is not mapped",
|
||||
va);
|
||||
}
|
||||
|
||||
if (is_leaf_entry (entry, level, p))
|
||||
return { slot_pa, entry, level };
|
||||
|
||||
table_pa = table_address (entry, p);
|
||||
}
|
||||
}
|
||||
|
||||
private struct LeafEntry {
|
||||
public uint64 slot_pa;
|
||||
public uint64 value;
|
||||
public uint level;
|
||||
}
|
||||
|
||||
private async void relax_parent_entries (uint64 va, Gum.PageProtection prot, MMUParameters p,
|
||||
Gee.HashSet<uint64?> already_relaxed, Cancellable? cancellable) throws Error, IOError {
|
||||
uint64 table_pa = p.root_table;
|
||||
for (uint level = 0; level != p.leaf_level; level++) {
|
||||
uint64 slot_pa = slot_address (va, table_pa, level, p);
|
||||
uint64 entry = yield read_entry_at (slot_pa, p, cancellable);
|
||||
|
||||
if (!already_relaxed.contains (slot_pa) && p.levels[level].has_protection_bits) {
|
||||
uint64 new_entry = entry;
|
||||
if ((prot & Gum.PageProtection.WRITE) != 0)
|
||||
new_entry |= WRITABLE_BIT;
|
||||
if ((prot & Gum.PageProtection.EXECUTE) != 0 && p.nx_enabled)
|
||||
new_entry &= ~NX_BIT;
|
||||
|
||||
if (new_entry != entry) {
|
||||
yield write_entry (slot_pa, new_entry, p, cancellable);
|
||||
entry = new_entry;
|
||||
}
|
||||
already_relaxed.add (slot_pa);
|
||||
}
|
||||
|
||||
table_pa = table_address (entry, p);
|
||||
}
|
||||
}
|
||||
|
||||
public async Allocation map (Gee.List<uint64?> physical_addresses, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
MMUParameters p = yield load_parameters (cancellable);
|
||||
if (!p.paging_enabled)
|
||||
throw new Error.NOT_SUPPORTED ("Paging is disabled");
|
||||
|
||||
var run = new Run (physical_addresses.size);
|
||||
|
||||
yield begin_access (cancellable);
|
||||
Allocation? allocation = null;
|
||||
GLib.Error? failure = null;
|
||||
try {
|
||||
if (yield scan_for_run (run, p.root_table, 0, 0, p, cancellable))
|
||||
allocation = yield occupy (run, physical_addresses, p, cancellable);
|
||||
} catch (GLib.Error e) {
|
||||
failure = e;
|
||||
}
|
||||
yield end_access (cancellable);
|
||||
throw_if_failed (failure);
|
||||
|
||||
if (allocation == null)
|
||||
throw new Error.NOT_SUPPORTED ("Unable to find a free run of page table entries");
|
||||
|
||||
return allocation;
|
||||
}
|
||||
|
||||
// A run has to be contiguous in virtual address space but not in the tables
|
||||
// describing it, so anything longer than a table holds is gathered a segment
|
||||
// at a time and a gap anywhere above the leaves starts the search over.
|
||||
private async bool scan_for_run (Run run, uint64 table_pa, uint level, uint64 upper_bits,
|
||||
MMUParameters p, Cancellable? cancellable) throws Error, IOError {
|
||||
Level l = p.levels[level];
|
||||
bool at_leaf_level = level == p.leaf_level;
|
||||
|
||||
Buffer entries = yield read_buffer (table_pa, l.num_entries * p.entry_size, cancellable);
|
||||
|
||||
for (uint i = 0; i != l.num_entries; i++) {
|
||||
uint64 entry = read_entry (entries, i * p.entry_size, p);
|
||||
uint64 prefix = upper_bits | ((uint64) i << l.shift);
|
||||
|
||||
if (!at_leaf_level) {
|
||||
bool usable = (entry & PRESENT_BIT) != 0
|
||||
&& !is_leaf_entry (entry, level, p)
|
||||
&& (protection_from_entry (entry, level, p) & MAPPING_PROTECTION)
|
||||
== MAPPING_PROTECTION;
|
||||
if (!usable) {
|
||||
run.reset ();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (yield scan_for_run (run, table_address (entry, p), level + 1, prefix, p,
|
||||
cancellable))
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint64 va = canonicalize (prefix, p);
|
||||
if ((entry & PRESENT_BIT) != 0 || va == 0) {
|
||||
run.reset ();
|
||||
continue;
|
||||
}
|
||||
|
||||
run.take (va, table_pa + ((uint64) i * p.entry_size), p.entry_size);
|
||||
if (run.is_complete ())
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Allocation occupy (Run run, Gee.List<uint64?> physical_addresses, MMUParameters p,
|
||||
Cancellable? cancellable) throws Error, IOError {
|
||||
var displaced = new Gee.ArrayList<DisplacedEntries> ();
|
||||
|
||||
uint next_page = 0;
|
||||
foreach (RunSegment segment in run.segments) {
|
||||
var builder = gdb.make_buffer_builder ();
|
||||
for (uint i = 0; i != segment.num_entries; i++) {
|
||||
uint64 pa = physical_addresses[(int) (next_page + i)];
|
||||
uint64 entry = apply_protection_bits (pa | PRESENT_BIT, MAPPING_PROTECTION, p);
|
||||
if (p.entry_size == 8)
|
||||
builder.append_uint64 (entry);
|
||||
else
|
||||
builder.append_uint32 ((uint32) entry);
|
||||
}
|
||||
Bytes new_entries = builder.build ();
|
||||
|
||||
Bytes old_entries = (yield read_buffer (segment.slot_pa, new_entries.get_size (),
|
||||
cancellable)).bytes;
|
||||
yield write_buffer (segment.slot_pa, new_entries, cancellable);
|
||||
|
||||
displaced.add (new DisplacedEntries (segment.slot_pa, old_entries));
|
||||
next_page += segment.num_entries;
|
||||
}
|
||||
|
||||
return new EntryAllocation (run.va, run.count * PAGE_SIZE, displaced, this);
|
||||
}
|
||||
|
||||
private async void restore_entries (Gee.List<DisplacedEntries> displaced, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
yield begin_access (cancellable);
|
||||
GLib.Error? failure = null;
|
||||
try {
|
||||
foreach (DisplacedEntries d in displaced)
|
||||
yield write_buffer (d.slot_pa, d.entries, cancellable);
|
||||
} catch (GLib.Error e) {
|
||||
failure = e;
|
||||
}
|
||||
yield end_access (cancellable);
|
||||
throw_if_failed (failure);
|
||||
}
|
||||
|
||||
private class Run {
|
||||
public uint needed;
|
||||
public uint64 va;
|
||||
public uint count;
|
||||
public Gee.ArrayList<RunSegment> segments = new Gee.ArrayList<RunSegment> ();
|
||||
|
||||
private uint64 next_va;
|
||||
private size_t entry_size;
|
||||
|
||||
public Run (uint needed) {
|
||||
this.needed = needed;
|
||||
}
|
||||
|
||||
public void reset () {
|
||||
va = 0;
|
||||
count = 0;
|
||||
next_va = 0;
|
||||
segments.clear ();
|
||||
}
|
||||
|
||||
public void take (uint64 page_va, uint64 slot_pa, size_t entry_size) {
|
||||
if (count != 0 && page_va != next_va)
|
||||
reset ();
|
||||
|
||||
this.entry_size = entry_size;
|
||||
|
||||
if (count == 0) {
|
||||
va = page_va;
|
||||
segments.add (new RunSegment (slot_pa));
|
||||
} else {
|
||||
RunSegment last = segments[segments.size - 1];
|
||||
if (slot_pa == last.slot_pa + (last.num_entries * entry_size))
|
||||
last.num_entries++;
|
||||
else
|
||||
segments.add (new RunSegment (slot_pa));
|
||||
}
|
||||
|
||||
count++;
|
||||
next_va = page_va + PAGE_SIZE;
|
||||
}
|
||||
|
||||
public bool is_complete () {
|
||||
return count == needed;
|
||||
}
|
||||
}
|
||||
|
||||
private class RunSegment {
|
||||
public uint64 slot_pa;
|
||||
public uint num_entries = 1;
|
||||
|
||||
public RunSegment (uint64 slot_pa) {
|
||||
this.slot_pa = slot_pa;
|
||||
}
|
||||
}
|
||||
|
||||
private class DisplacedEntries {
|
||||
public uint64 slot_pa;
|
||||
public Bytes entries;
|
||||
|
||||
public DisplacedEntries (uint64 slot_pa, Bytes entries) {
|
||||
this.slot_pa = slot_pa;
|
||||
this.entries = entries;
|
||||
}
|
||||
}
|
||||
|
||||
private class EntryAllocation : Object, Allocation {
|
||||
public uint64 virtual_address {
|
||||
get { return va; }
|
||||
}
|
||||
|
||||
public size_t size {
|
||||
get { return allocated_size; }
|
||||
}
|
||||
|
||||
private uint64 va;
|
||||
private size_t allocated_size;
|
||||
private Gee.List<DisplacedEntries> displaced;
|
||||
private X86PageTables page_tables;
|
||||
|
||||
public EntryAllocation (uint64 va, size_t allocated_size, Gee.List<DisplacedEntries> displaced,
|
||||
X86PageTables page_tables) {
|
||||
this.va = va;
|
||||
this.allocated_size = allocated_size;
|
||||
this.displaced = displaced;
|
||||
this.page_tables = page_tables;
|
||||
}
|
||||
|
||||
public async void deallocate (Cancellable? cancellable) throws Error, IOError {
|
||||
yield page_tables.restore_entries (displaced, cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint64 slot_address (uint64 va, uint64 table_pa, uint level, MMUParameters p) {
|
||||
Level l = p.levels[level];
|
||||
uint index = (uint) ((va >> l.shift) & (l.num_entries - 1));
|
||||
return table_pa + ((uint64) index * p.entry_size);
|
||||
}
|
||||
|
||||
private static bool is_leaf_entry (uint64 entry, uint level, MMUParameters p) {
|
||||
if (level == p.leaf_level)
|
||||
return true;
|
||||
return p.levels[level].may_hold_large_page && (entry & LARGE_PAGE_BIT) != 0;
|
||||
}
|
||||
|
||||
private static uint64 table_address (uint64 entry, MMUParameters p) {
|
||||
return entry & ((p.entry_size == 8) ? EXTENDED_ADDRESS_MASK : LEGACY_ADDRESS_MASK);
|
||||
}
|
||||
|
||||
private static uint64 leaf_address (uint64 entry, uint level, MMUParameters p) {
|
||||
if (p.entry_size == 8)
|
||||
return entry & EXTENDED_ADDRESS_MASK & ~((1ULL << p.levels[level].shift) - 1);
|
||||
|
||||
if (level == p.leaf_level)
|
||||
return entry & LEGACY_ADDRESS_MASK;
|
||||
|
||||
uint64 low_bits = entry & LEGACY_LARGE_PAGE_ADDRESS_MASK;
|
||||
uint64 pse36_high_bits = (entry & PSE36_ADDRESS_MASK) << PSE36_ADDRESS_SHIFT;
|
||||
return low_bits | pse36_high_bits;
|
||||
}
|
||||
|
||||
private static uint64 canonicalize (uint64 va, MMUParameters p) {
|
||||
if (p.canonical_bits == 0)
|
||||
return va;
|
||||
uint64 sign = 1ULL << (p.canonical_bits - 1);
|
||||
return (va ^ sign) - sign;
|
||||
}
|
||||
|
||||
// What a single entry contributes is a ceiling, not the effective right: the bits are ANDed
|
||||
// across levels.
|
||||
private static Gum.PageProtection protection_from_entry (uint64 entry, uint level, MMUParameters p) {
|
||||
if (!p.levels[level].has_protection_bits)
|
||||
return READ | WRITE | EXECUTE;
|
||||
|
||||
Gum.PageProtection prot = READ;
|
||||
if ((entry & WRITABLE_BIT) != 0)
|
||||
prot |= WRITE;
|
||||
if (!p.nx_enabled || (entry & NX_BIT) == 0)
|
||||
prot |= EXECUTE;
|
||||
return prot;
|
||||
}
|
||||
|
||||
private static uint64 apply_protection_bits (uint64 entry, Gum.PageProtection prot, MMUParameters p) {
|
||||
uint64 result = entry;
|
||||
|
||||
if ((prot & Gum.PageProtection.WRITE) != 0)
|
||||
result |= WRITABLE_BIT;
|
||||
else
|
||||
result &= ~WRITABLE_BIT;
|
||||
|
||||
if (p.nx_enabled) {
|
||||
if ((prot & Gum.PageProtection.EXECUTE) != 0)
|
||||
result &= ~NX_BIT;
|
||||
else
|
||||
result |= NX_BIT;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async uint64 read_entry_at (uint64 slot_pa, MMUParameters p, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
Buffer buf = yield read_buffer (slot_pa, p.entry_size, cancellable);
|
||||
return read_entry (buf, 0, p);
|
||||
}
|
||||
|
||||
private async void write_entry (uint64 slot_pa, uint64 entry, MMUParameters p, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
Buffer buf = gdb.make_buffer (new Bytes (new uint8[p.entry_size]));
|
||||
if (p.entry_size == 8)
|
||||
buf.write_uint64 (0, entry);
|
||||
else
|
||||
buf.write_uint32 (0, (uint32) entry);
|
||||
|
||||
yield write_buffer (slot_pa, buf.bytes, cancellable);
|
||||
}
|
||||
|
||||
private static uint64 read_entry (Buffer entries, size_t offset, MMUParameters p) {
|
||||
return (p.entry_size == 8) ? entries.read_uint64 (offset) : entries.read_uint32 (offset);
|
||||
}
|
||||
|
||||
private async void begin_access (Cancellable? cancellable) throws Error, IOError {
|
||||
if (physical_memory == null)
|
||||
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
|
||||
}
|
||||
|
||||
private async void end_access (Cancellable? cancellable) throws Error, IOError {
|
||||
if (physical_memory == null)
|
||||
yield set_addressing_mode (gdb, VIRTUAL, cancellable);
|
||||
}
|
||||
|
||||
private async Buffer read_buffer (uint64 pa, size_t size, Cancellable? cancellable) throws Error, IOError {
|
||||
if (physical_memory != null && physical_memory.contains (pa))
|
||||
return gdb.make_buffer (new Bytes (physical_memory.read (pa, size)));
|
||||
return yield gdb.read_buffer (pa, size, cancellable);
|
||||
}
|
||||
|
||||
private async void write_buffer (uint64 pa, Bytes data, Cancellable? cancellable) throws Error, IOError {
|
||||
if (physical_memory != null && physical_memory.contains (pa)) {
|
||||
physical_memory.write (pa, data.get_data ());
|
||||
return;
|
||||
}
|
||||
yield gdb.write_byte_array (pa, data, cancellable);
|
||||
}
|
||||
|
||||
private async MMUParameters load_parameters (Cancellable? cancellable) throws Error, IOError {
|
||||
if (cached_parameters == null)
|
||||
cached_parameters = yield MMUParameters.load (gdb, cancellable);
|
||||
return cached_parameters;
|
||||
}
|
||||
|
||||
private class MMUParameters {
|
||||
public bool paging_enabled;
|
||||
public bool nx_enabled;
|
||||
public uint canonical_bits;
|
||||
public size_t entry_size;
|
||||
public uint64 root_table;
|
||||
public Level[] levels;
|
||||
|
||||
public uint leaf_level {
|
||||
get { return levels.length - 1; }
|
||||
}
|
||||
|
||||
private const uint64 CR0_PG = 1ULL << 31;
|
||||
private const uint64 CR3_PAE_ROOT_MASK = ~0x1fULL;
|
||||
private const uint64 CR4_PSE = 1ULL << 4;
|
||||
private const uint64 CR4_PAE = 1ULL << 5;
|
||||
private const uint64 CR4_LA57 = 1ULL << 12;
|
||||
private const uint64 EFER_LMA = 1ULL << 10;
|
||||
private const uint64 EFER_NXE = 1ULL << 11;
|
||||
|
||||
public static async MMUParameters load (GDB.Client gdb, Cancellable? cancellable) throws Error, IOError {
|
||||
ControlRegisters regs = yield ControlRegisters.read (gdb, cancellable);
|
||||
|
||||
var parameters = new MMUParameters ();
|
||||
|
||||
parameters.paging_enabled = (regs.cr0 & CR0_PG) != 0;
|
||||
|
||||
bool pae = (regs.cr4 & CR4_PAE) != 0;
|
||||
parameters.nx_enabled = pae && (regs.efer & EFER_NXE) != 0;
|
||||
|
||||
if ((regs.efer & EFER_LMA) != 0)
|
||||
parameters.adopt_long_mode ((regs.cr4 & CR4_LA57) != 0, regs.cr3);
|
||||
else if (pae)
|
||||
parameters.adopt_pae (regs.cr3);
|
||||
else
|
||||
parameters.adopt_legacy ((regs.cr4 & CR4_PSE) != 0, regs.cr3);
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private void adopt_long_mode (bool la57, uint64 cr3) {
|
||||
var levels = new Gee.ArrayList<Level> ();
|
||||
if (la57)
|
||||
levels.add (new Level (48, 512, false, true));
|
||||
levels.add (new Level (39, 512, false, true));
|
||||
levels.add (new Level (30, 512, true, true));
|
||||
levels.add (new Level (21, 512, true, true));
|
||||
levels.add (new Level (12, 512, false, true));
|
||||
|
||||
this.levels = levels.to_array ();
|
||||
canonical_bits = la57 ? 57 : 48;
|
||||
entry_size = 8;
|
||||
root_table = cr3 & EXTENDED_ADDRESS_MASK;
|
||||
}
|
||||
|
||||
// A page-directory-pointer entry has reserved bits where the others keep RW and NX.
|
||||
private void adopt_pae (uint64 cr3) {
|
||||
levels = {
|
||||
new Level (30, 4, false, false),
|
||||
new Level (21, 512, true, true),
|
||||
new Level (12, 512, false, true)
|
||||
};
|
||||
entry_size = 8;
|
||||
root_table = cr3 & CR3_PAE_ROOT_MASK;
|
||||
}
|
||||
|
||||
private void adopt_legacy (bool pse_enabled, uint64 cr3) {
|
||||
levels = {
|
||||
new Level (22, 1024, pse_enabled, true),
|
||||
new Level (12, 1024, false, true)
|
||||
};
|
||||
entry_size = 4;
|
||||
root_table = cr3 & LEGACY_ADDRESS_MASK;
|
||||
}
|
||||
}
|
||||
|
||||
private class Level {
|
||||
public uint shift;
|
||||
public uint num_entries;
|
||||
public bool may_hold_large_page;
|
||||
public bool has_protection_bits;
|
||||
|
||||
public Level (uint shift, uint num_entries, bool may_hold_large_page, bool has_protection_bits) {
|
||||
this.shift = shift;
|
||||
this.num_entries = num_entries;
|
||||
this.may_hold_large_page = may_hold_large_page;
|
||||
this.has_protection_bits = has_protection_bits;
|
||||
}
|
||||
}
|
||||
|
||||
private class ControlRegisters {
|
||||
public uint64 cr0;
|
||||
public uint64 cr3;
|
||||
public uint64 cr4;
|
||||
public uint64 efer;
|
||||
|
||||
public static async ControlRegisters read (GDB.Client gdb, Cancellable? cancellable) throws Error, IOError {
|
||||
var regs = new ControlRegisters ();
|
||||
|
||||
if (yield regs.try_read_from_registers (gdb, cancellable))
|
||||
return regs;
|
||||
|
||||
yield regs.read_from_monitor (gdb, cancellable);
|
||||
|
||||
return regs;
|
||||
}
|
||||
|
||||
private async bool try_read_from_registers (GDB.Client gdb, Cancellable? cancellable) throws Error, IOError {
|
||||
GDB.Exception? exception = gdb.exception;
|
||||
if (exception == null)
|
||||
throw new Error.INVALID_OPERATION ("Unable to query in current state");
|
||||
GDB.Thread thread = exception.thread;
|
||||
|
||||
try {
|
||||
cr0 = yield thread.read_register ("cr0", cancellable);
|
||||
cr3 = yield thread.read_register ("cr3", cancellable);
|
||||
cr4 = yield thread.read_register ("cr4", cancellable);
|
||||
} catch (Error e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
efer = yield thread.read_register ("efer", cancellable);
|
||||
} catch (Error e) {
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async void read_from_monitor (GDB.Client gdb, Cancellable? cancellable) throws Error, IOError {
|
||||
string dump = yield gdb.run_remote_command ("info registers", cancellable);
|
||||
|
||||
bool found_cr3 = false;
|
||||
foreach (string token in dump.split_set (" \t\r\n")) {
|
||||
string[] parts = token.split ("=");
|
||||
if (parts.length != 2)
|
||||
continue;
|
||||
uint64 val = uint64.parse (parts[1], 16);
|
||||
|
||||
switch (parts[0]) {
|
||||
case "CR0":
|
||||
cr0 = val;
|
||||
break;
|
||||
case "CR3":
|
||||
cr3 = val;
|
||||
found_cr3 = true;
|
||||
break;
|
||||
case "CR4":
|
||||
cr4 = val;
|
||||
break;
|
||||
case "EFER":
|
||||
efer = val;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_cr3)
|
||||
throw new Error.NOT_SUPPORTED ("Unable to determine the target's control registers");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,6 +381,45 @@ namespace Frida {
|
||||
return task.execute (cancellable);
|
||||
}
|
||||
|
||||
public async Device add_barebone_device (Barebone.Config config, Cancellable? cancellable = null)
|
||||
throws Error, IOError {
|
||||
#if HAVE_BAREBONE_BACKEND
|
||||
check_open ();
|
||||
|
||||
var barebone_device = yield get_device ((device) => {
|
||||
return device.provider is BareboneHostSessionProvider;
|
||||
}, 0, cancellable);
|
||||
|
||||
var raw_options = new HostSessionOptions ();
|
||||
raw_options.map["config"] = config;
|
||||
|
||||
var device = new Device (this, barebone_device.provider, barebone_device.provider.id,
|
||||
barebone_device.provider.name, raw_options);
|
||||
devices.add (device);
|
||||
added (device);
|
||||
changed ();
|
||||
|
||||
return device;
|
||||
#else
|
||||
throw new Error.NOT_SUPPORTED ("Barebone backend not available");
|
||||
#endif
|
||||
}
|
||||
|
||||
public Device add_barebone_device_sync (Barebone.Config config, Cancellable? cancellable = null)
|
||||
throws Error, IOError {
|
||||
var task = create<AddBareboneDeviceTask> ();
|
||||
task.config = config;
|
||||
return task.execute (cancellable);
|
||||
}
|
||||
|
||||
private class AddBareboneDeviceTask : ManagerTask<Device> {
|
||||
public Barebone.Config config;
|
||||
|
||||
protected override async Device perform_operation () throws Error, IOError {
|
||||
return yield parent.add_barebone_device (config, cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
private class AddRemoteDeviceTask : ManagerTask<Device> {
|
||||
public string address;
|
||||
public RemoteDeviceOptions? options;
|
||||
|
||||
+53
-11
@@ -99,6 +99,8 @@ namespace Frida.GDB {
|
||||
protected const char NOTIFICATION_TYPE_STOP_WITH_PROPERTIES = 'T';
|
||||
protected const char NOTIFICATION_TYPE_OUTPUT = 'O';
|
||||
|
||||
private bool binary_writes_supported = true;
|
||||
|
||||
private const char STOP_CHARACTER = 0x03;
|
||||
private const string ACK_NOTIFICATION = "+";
|
||||
private const string NACK_NOTIFICATION = "-";
|
||||
@@ -526,7 +528,8 @@ namespace Frida.GDB {
|
||||
|
||||
public async void write_byte_array (uint64 address, Bytes bytes, Cancellable? cancellable = null)
|
||||
throws Error, IOError {
|
||||
size_t max_bytes_per_packet = (max_packet_size - 1 - 16 - 1 - 8 - 1 - Packet.OVERHEAD) / 2;
|
||||
size_t header_size = 1 + 16 + 1 + 8 + 1 + Packet.OVERHEAD;
|
||||
size_t budget = max_packet_size - header_size;
|
||||
|
||||
var data = bytes.get_data ();
|
||||
size_t offset = 0;
|
||||
@@ -535,22 +538,47 @@ namespace Frida.GDB {
|
||||
var builder = make_packet_builder_sized (32 + (remaining * 2));
|
||||
|
||||
while (remaining != 0) {
|
||||
uint64 slice_address = address + offset;
|
||||
size_t slice_size = size_t.min (remaining, max_bytes_per_packet);
|
||||
size_t slice_size, cost;
|
||||
if (binary_writes_supported) {
|
||||
slice_size = 0;
|
||||
cost = 0;
|
||||
while (slice_size != remaining) {
|
||||
size_t byte_cost = needs_escaping (data[offset + slice_size]) ? 2 : 1;
|
||||
if (cost + byte_cost > budget)
|
||||
break;
|
||||
cost += byte_cost;
|
||||
slice_size++;
|
||||
}
|
||||
} else {
|
||||
slice_size = size_t.min (remaining, budget / 2);
|
||||
}
|
||||
|
||||
builder
|
||||
.append_c ('M')
|
||||
.append_address (slice_address)
|
||||
.append_c (binary_writes_supported ? 'X' : 'M')
|
||||
.append_address (address + offset)
|
||||
.append_c (',')
|
||||
.append_size (slice_size)
|
||||
.append_c (':');
|
||||
|
||||
for (size_t i = 0; i != slice_size; i++) {
|
||||
uint8 byte = data[offset + i];
|
||||
builder.append_hexbyte (byte);
|
||||
if (binary_writes_supported)
|
||||
builder.append_c ((char) byte);
|
||||
else
|
||||
builder.append_hexbyte (byte);
|
||||
}
|
||||
|
||||
yield execute (builder.build (), cancellable);
|
||||
if (binary_writes_supported) {
|
||||
try {
|
||||
yield execute (builder.build (), cancellable);
|
||||
} catch (Error e) {
|
||||
binary_writes_supported = false;
|
||||
builder.reset ();
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
yield execute (builder.build (), cancellable);
|
||||
}
|
||||
|
||||
builder.reset ();
|
||||
|
||||
@@ -559,6 +587,18 @@ namespace Frida.GDB {
|
||||
}
|
||||
}
|
||||
|
||||
private static bool needs_escaping (uint8 byte) {
|
||||
switch ((char) byte) {
|
||||
case PACKET_CHARACTER:
|
||||
case CHECKSUM_CHARACTER:
|
||||
case ESCAPE_CHARACTER:
|
||||
case REPEAT_CHARACTER:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async uint64 read_pointer (uint64 address, Cancellable? cancellable = null) throws Error, IOError {
|
||||
var buffer = yield read_buffer (address, pointer_size, cancellable);
|
||||
return buffer.read_pointer (0);
|
||||
@@ -1738,7 +1778,7 @@ namespace Frida.GDB {
|
||||
if (!client.bulk_registers) {
|
||||
foreach (var e in regs.entries) {
|
||||
var reg = client.get_register_by_name (e.key);
|
||||
if (reg.writable && reg.bitsize == 64 && e.value.is_of_type (VariantType.UINT64))
|
||||
if (reg.writable && e.value.is_of_type (VariantType.UINT64))
|
||||
yield write_register (e.key, e.value.get_uint64 (), cancellable);
|
||||
}
|
||||
return;
|
||||
@@ -1756,9 +1796,11 @@ namespace Frida.GDB {
|
||||
throw new Error.INVALID_ARGUMENT ("Missing %s", reg.name);
|
||||
|
||||
if (val.is_of_type (VariantType.UINT64)) {
|
||||
builder.append (Protocol.unparse_integer_value (val.get_uint64 (), sizeof (uint64), byte_order));
|
||||
builder.append (Protocol.unparse_integer_value (val.get_uint64 (), reg.bitsize / 8,
|
||||
byte_order));
|
||||
} else if (val.is_of_type (VariantType.UINT32)) {
|
||||
builder.append (Protocol.unparse_integer_value (val.get_uint64 (), sizeof (uint32), byte_order));
|
||||
builder.append (Protocol.unparse_integer_value (val.get_uint32 (), reg.bitsize / 8,
|
||||
byte_order));
|
||||
} else {
|
||||
builder.append (Protocol.unparse_hex_bytes (val.get_data_as_bytes ()));
|
||||
}
|
||||
@@ -1800,7 +1842,7 @@ namespace Frida.GDB {
|
||||
.append_c ('P')
|
||||
.append_register_id (reg.id)
|
||||
.append_c ('=')
|
||||
.append (Protocol.unparse_integer_value (val, client.pointer_size, client.byte_order));
|
||||
.append (Protocol.unparse_integer_value (val, reg.bitsize / 8, client.byte_order));
|
||||
if ("thread-suffix" in client.features) {
|
||||
command
|
||||
.append (";thread:")
|
||||
|
||||
@@ -324,6 +324,11 @@ namespace Frida {
|
||||
if (pss != null)
|
||||
yield pss.interrupt (cancellable);
|
||||
|
||||
if (KernelInjectSession.is_available ()) {
|
||||
var session = yield KernelInjectSession.open (pid, cancellable);
|
||||
return yield session.inject (spec, cancellable);
|
||||
}
|
||||
|
||||
try {
|
||||
var session = yield InjectSession.open (pid, cancellable);
|
||||
RemoteAgent agent = yield session.inject (spec, cancellable);
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
namespace Frida {
|
||||
// Bridges to the Frida kernel module over its magic-prctl control channel. The module
|
||||
// exposes ptrace-free memory and thread primitives against an arbitrary pid, which the
|
||||
// kernel-assisted injector drives instead of ptrace or /proc/<pid>/mem.
|
||||
internal sealed class KernelSession : Object {
|
||||
public uint pid {
|
||||
get;
|
||||
construct;
|
||||
}
|
||||
|
||||
private const long CONTROL_MAGIC = 0x46524944;
|
||||
private const uint64 CONTROL_TOKEN = 0x1d5f9e6b2c7a4038;
|
||||
|
||||
private enum Op {
|
||||
PING = 32,
|
||||
ALLOC = 64,
|
||||
FREE,
|
||||
WRITE,
|
||||
READ,
|
||||
SPAWN,
|
||||
CLOAK_THREAD,
|
||||
CLOAK_RANGE,
|
||||
}
|
||||
|
||||
private struct AllocArgs {
|
||||
uint64 size;
|
||||
uint32 pid;
|
||||
uint32 prot;
|
||||
}
|
||||
|
||||
private struct FreeArgs {
|
||||
uint64 address;
|
||||
uint64 size;
|
||||
uint32 pid;
|
||||
}
|
||||
|
||||
private struct XferArgs {
|
||||
uint64 address;
|
||||
uint64 buffer;
|
||||
uint64 size;
|
||||
uint32 pid;
|
||||
}
|
||||
|
||||
private struct SpawnArgs {
|
||||
uint64 entry;
|
||||
uint64 stack;
|
||||
uint64 arg;
|
||||
uint64 tls;
|
||||
uint32 pid;
|
||||
}
|
||||
|
||||
private struct CloakThreadArgs {
|
||||
uint32 tgid;
|
||||
uint32 tid;
|
||||
}
|
||||
|
||||
private struct CloakRangeArgs {
|
||||
uint64 start;
|
||||
uint64 size;
|
||||
uint32 tgid;
|
||||
}
|
||||
|
||||
private KernelSession (uint pid) {
|
||||
Object (pid: pid);
|
||||
}
|
||||
|
||||
public static bool is_available () {
|
||||
return control (Op.PING, 0, 0) == CONTROL_MAGIC;
|
||||
}
|
||||
|
||||
public static KernelSession open (uint pid) throws Error {
|
||||
if (!is_available ())
|
||||
throw new Error.NOT_SUPPORTED ("Frida kernel module is not loaded");
|
||||
return new KernelSession (pid);
|
||||
}
|
||||
|
||||
public uint64 alloc (uint64 size, int prot) throws Error {
|
||||
var args = AllocArgs () {
|
||||
size = size,
|
||||
pid = pid,
|
||||
prot = prot,
|
||||
};
|
||||
long result = control (Op.ALLOC, (ulong) (uintptr) (&args), 0);
|
||||
if (result <= 0)
|
||||
throw new Error.NOT_SUPPORTED ("Unable to allocate %s bytes in the target", size.to_string ());
|
||||
return (uint64) result;
|
||||
}
|
||||
|
||||
public void free (uint64 address, uint64 size) throws Error {
|
||||
var args = FreeArgs () {
|
||||
address = address,
|
||||
size = size,
|
||||
pid = pid,
|
||||
};
|
||||
control (Op.FREE, (ulong) (uintptr) (&args), 0);
|
||||
}
|
||||
|
||||
public void write_memory (uint64 address, uint8[] data) throws Error {
|
||||
var args = XferArgs () {
|
||||
address = address,
|
||||
buffer = (uint64) (uintptr) (uint8 *) data,
|
||||
size = data.length,
|
||||
pid = pid,
|
||||
};
|
||||
long result = control (Op.WRITE, (ulong) (uintptr) (&args), 0);
|
||||
if (result < 0 || (uint64) result != data.length)
|
||||
throw new Error.NOT_SUPPORTED ("Unable to write %d bytes at 0x%" + uint64.FORMAT_MODIFIER + "x",
|
||||
data.length, address);
|
||||
}
|
||||
|
||||
public uint8[] read_memory (uint64 address, size_t size) throws Error {
|
||||
var result = new uint8[size];
|
||||
var args = XferArgs () {
|
||||
address = address,
|
||||
buffer = (uint64) (uintptr) (uint8 *) result,
|
||||
size = size,
|
||||
pid = pid,
|
||||
};
|
||||
long n = control (Op.READ, (ulong) (uintptr) (&args), 0);
|
||||
if (n < 0 || (size_t) n != size)
|
||||
throw new Error.NOT_SUPPORTED ("Unable to read %s bytes at 0x%" + uint64.FORMAT_MODIFIER + "x",
|
||||
size.to_string (), address);
|
||||
return result;
|
||||
}
|
||||
|
||||
public uint64 read_pointer (uint64 address) throws Error {
|
||||
uint8[] raw = read_memory (address, sizeof (uint64));
|
||||
return *((uint64 *) raw);
|
||||
}
|
||||
|
||||
// entry/stack/arg/tls seed the new thread's PC/SP/x0 and TPIDR_EL0. The thread joins
|
||||
// the target's thread group; tls must point at a private block the caller prepared.
|
||||
public uint spawn_thread (uint64 entry, uint64 stack, uint64 arg, uint64 tls) throws Error {
|
||||
var args = SpawnArgs () {
|
||||
entry = entry,
|
||||
stack = stack,
|
||||
arg = arg,
|
||||
tls = tls,
|
||||
pid = pid,
|
||||
};
|
||||
long result = control (Op.SPAWN, (ulong) (uintptr) (&args), 0);
|
||||
if (result <= 0)
|
||||
throw new Error.NOT_SUPPORTED ("Unable to spawn a thread in the target");
|
||||
return (uint) result;
|
||||
}
|
||||
|
||||
// Hide the loader's own footprint (which Gum doesn't track and so wouldn't self-cloak):
|
||||
// the bootstrap thread plus the region/stack/tls we mapped for it.
|
||||
public void cloak_thread (uint tid) {
|
||||
var args = CloakThreadArgs () { tgid = pid, tid = tid };
|
||||
control (Op.CLOAK_THREAD, (ulong) (uintptr) (&args), 0);
|
||||
}
|
||||
|
||||
public void cloak_range (uint64 address, uint64 size) {
|
||||
var args = CloakRangeArgs () { start = address, size = size, tgid = pid };
|
||||
control (Op.CLOAK_RANGE, (ulong) (uintptr) (&args), 0);
|
||||
}
|
||||
|
||||
// prctl (FRIDA_CONTROL_MAGIC, op, arg2, arg3, FRIDA_CONTROL_TOKEN). Absent the token
|
||||
// the module falls through to the real prctl, so probing the magic looks like a stock
|
||||
// kernel. We bind syscall() with a 64-bit return since the libc prctl() wrapper — and
|
||||
// the vapi's syscall() — would truncate an allocated address.
|
||||
private static long control (Op op, ulong arg2, ulong arg3) {
|
||||
return syscall ((long) LinuxSyscall.PRCTL, CONTROL_MAGIC, (ulong) op, arg2, arg3, CONTROL_TOKEN);
|
||||
}
|
||||
|
||||
[CCode (cname = "syscall", cheader_filename = "unistd.h,sys/syscall.h")]
|
||||
private extern static long syscall (long number, ...);
|
||||
}
|
||||
|
||||
// Injects frida-agent.so using the kernel module's ptrace-free primitives: stage the loader +
|
||||
// context in a fresh region, then spawn a bootstrap thread straight into it. The loader hands
|
||||
// off to a real pthread it creates, so the only libc call on the kernel-spawned thread is
|
||||
// pthread_create — which we give a private, isolated bionic TLS so it never touches the
|
||||
// target's live per-thread state.
|
||||
internal sealed class KernelInjectSession : Object {
|
||||
public uint pid {
|
||||
get;
|
||||
construct;
|
||||
}
|
||||
|
||||
private KernelSession kernel;
|
||||
|
||||
private const int PROT_RW = 3;
|
||||
private const int PROT_RWX = 7;
|
||||
private const uint64 STACK_SIZE = 128 * 1024;
|
||||
private const uint64 TLS_SIZE = 8 * 1024;
|
||||
private const uint SETTLE_TIMEOUT_SECONDS = 10;
|
||||
|
||||
private struct RegionLayout {
|
||||
public size_t entry_offset;
|
||||
public size_t libc_offset;
|
||||
public size_t entrypoint_offset;
|
||||
public size_t data_offset;
|
||||
public size_t fallback_offset;
|
||||
public size_t context_offset;
|
||||
public size_t page_size;
|
||||
public size_t total;
|
||||
|
||||
public static RegionLayout compute (InjectSpec spec, string data, string fallback_address) {
|
||||
size_t loader_size = Frida.Data.HelperBackend.get_loader_bin_blob ().data.length;
|
||||
size_t entrypoint_size = make_cstring (spec.entrypoint).length;
|
||||
size_t data_size = make_cstring (data).length;
|
||||
size_t fallback_size = make_cstring (fallback_address).length;
|
||||
size_t page_size = Gum.query_page_size ();
|
||||
|
||||
var l = RegionLayout ();
|
||||
l.entry_offset = (size_t) align_up (loader_size, 16);
|
||||
size_t o = (size_t) align_up (l.entry_offset + 256, 16);
|
||||
l.libc_offset = o; o += sizeof (HelperLibcApi);
|
||||
l.entrypoint_offset = o; o += entrypoint_size;
|
||||
l.data_offset = o; o += data_size;
|
||||
l.fallback_offset = o; o += fallback_size;
|
||||
l.context_offset = (size_t) align_up (o, page_size);
|
||||
l.page_size = page_size;
|
||||
l.total = l.context_offset + page_size;
|
||||
return l;
|
||||
}
|
||||
}
|
||||
|
||||
private KernelInjectSession (uint pid, KernelSession kernel) {
|
||||
Object (pid: pid);
|
||||
this.kernel = kernel;
|
||||
}
|
||||
|
||||
public static bool is_available () {
|
||||
return KernelSession.is_available ();
|
||||
}
|
||||
|
||||
public static async KernelInjectSession open (uint pid, Cancellable? cancellable) throws Error, IOError {
|
||||
return new KernelInjectSession (pid, KernelSession.open (pid));
|
||||
}
|
||||
|
||||
public async RemoteAgent inject (InjectSpec spec, Cancellable? cancellable) throws Error, IOError {
|
||||
var maps = ProcMapsSnapshot.from_pid (pid);
|
||||
var libc = RemoteLibcApi.resolve (maps);
|
||||
|
||||
string data = spec.data;
|
||||
|
||||
string fallback_address = make_fallback_address ();
|
||||
var region = RegionLayout.compute (spec, data, fallback_address);
|
||||
|
||||
Future<RemoteAgent> future_agent = establish_connection (spec, fallback_address, cancellable);
|
||||
|
||||
uint64 region_base = kernel.alloc (region.total, PROT_RWX);
|
||||
uint64 stack_base = kernel.alloc (STACK_SIZE, PROT_RW);
|
||||
uint64 tls_base;
|
||||
uint64 tls = synthesize_tls (out tls_base);
|
||||
|
||||
write_region (region_base, region, spec, data, fallback_address, libc);
|
||||
|
||||
kernel.cloak_range (region_base, region.total);
|
||||
kernel.cloak_range (stack_base, STACK_SIZE);
|
||||
kernel.cloak_range (tls_base, TLS_SIZE);
|
||||
|
||||
uint64 entry = region_base + region.entry_offset;
|
||||
uint64 context = region_base + region.context_offset;
|
||||
uint64 stack_top = (stack_base + STACK_SIZE - 16) & ~((uint64) 15);
|
||||
|
||||
uint tid = kernel.spawn_thread (entry, stack_top, context, tls);
|
||||
kernel.cloak_thread (tid);
|
||||
|
||||
return yield await_agent (future_agent, cancellable);
|
||||
}
|
||||
|
||||
private void write_region (uint64 region_base, RegionLayout l, InjectSpec spec, string data,
|
||||
string fallback_address, RemoteLibcApi libc) throws Error {
|
||||
kernel.write_memory (region_base, Frida.Data.HelperBackend.get_loader_bin_blob ().data);
|
||||
kernel.write_memory (region_base + l.entry_offset, build_bootstrap_code (region_base, l));
|
||||
|
||||
var ctx = HelperLoaderContext ();
|
||||
ctx.ctrlfds[0] = -1;
|
||||
ctx.ctrlfds[1] = -1;
|
||||
ctx.agent_entrypoint = (string *) (region_base + l.entrypoint_offset);
|
||||
ctx.agent_data = (string *) (region_base + l.data_offset);
|
||||
ctx.fallback_address = (string *) (region_base + l.fallback_offset);
|
||||
ctx.libc = (HelperLibcApi *) (region_base + l.libc_offset);
|
||||
|
||||
kernel.write_memory (region_base + l.context_offset, (uint8[]) &ctx);
|
||||
kernel.write_memory (region_base + l.libc_offset, (uint8[]) &libc.table);
|
||||
kernel.write_memory (region_base + l.entrypoint_offset, make_cstring (spec.entrypoint));
|
||||
kernel.write_memory (region_base + l.data_offset, make_cstring (data));
|
||||
kernel.write_memory (region_base + l.fallback_offset, make_cstring (fallback_address));
|
||||
}
|
||||
|
||||
private uint8[] build_bootstrap_code (uint64 region_base, RegionLayout l) throws Error {
|
||||
#if ARM64
|
||||
var buffer = new uint8[64];
|
||||
var writer = new Gum.Arm64Writer ((void *) buffer);
|
||||
writer.pc = region_base + l.entry_offset;
|
||||
// x0 already holds the loader context (passed as the spawn argument). Call frida_load,
|
||||
// then terminate this bootstrap thread; the agent runs on the pthread the loader spawned.
|
||||
if (!writer.put_bl_imm ((Gum.Address) region_base))
|
||||
throw new Error.NOT_SUPPORTED ("Loader is out of branch range of the bootstrap");
|
||||
writer.put_instruction ((uint32) 0xd2800ba8); // movz x8, #93 (__NR_exit)
|
||||
writer.put_instruction ((uint32) 0xd4000001); // svc #0
|
||||
writer.flush ();
|
||||
return buffer[:writer.offset ()];
|
||||
#else
|
||||
throw new Error.NOT_SUPPORTED ("Kernel-assisted injection is only implemented for arm64");
|
||||
#endif
|
||||
}
|
||||
|
||||
// Give the bootstrap thread a private, isolated bionic TLS so pthread_create's stack-guard
|
||||
// check and errno access land in our own block, never the target's live per-thread state.
|
||||
// Slot indices per bionic tls_defines.h (arm64): BIONIC_TLS=-1, THREAD_ID=1, STACK_GUARD=5,
|
||||
// each at tp + index * 8. The guard check is self-consistent within our block, so any stable
|
||||
// value passes and we never read the target's real canary.
|
||||
private uint64 synthesize_tls (out uint64 block_base) throws Error {
|
||||
uint64 block = kernel.alloc (TLS_SIZE, PROT_RW);
|
||||
block_base = block;
|
||||
uint64 tp = block + (TLS_SIZE / 2);
|
||||
uint64 pthread_scratch = block + (TLS_SIZE / 4);
|
||||
uint64 bionic_scratch = block + (3 * TLS_SIZE / 4);
|
||||
|
||||
write_slot (tp, -1, bionic_scratch);
|
||||
write_slot (tp, 1, pthread_scratch);
|
||||
write_slot (tp, 5, 0x1122334455667788);
|
||||
|
||||
return tp;
|
||||
}
|
||||
|
||||
private void write_slot (uint64 tp, int index, uint64 value) throws Error {
|
||||
uint8[] raw = new uint8[8];
|
||||
*((uint64 *) raw) = value;
|
||||
kernel.write_memory ((uint64) ((int64) tp + (int64) index * 8), raw);
|
||||
}
|
||||
|
||||
private Future<RemoteAgent> establish_connection (InjectSpec spec, string fallback_address,
|
||||
Cancellable? cancellable) throws Error {
|
||||
var promise = new Promise<RemoteAgent> ();
|
||||
|
||||
var server_address = new UnixSocketAddress.with_type (fallback_address, -1, UnixSocketAddressType.ABSTRACT);
|
||||
Socket server_socket;
|
||||
try {
|
||||
var socket = new Socket (SocketFamily.UNIX, SocketType.STREAM, SocketProtocol.DEFAULT);
|
||||
socket.bind (server_address, true);
|
||||
socket.listen ();
|
||||
server_socket = socket;
|
||||
} catch (GLib.Error e) {
|
||||
throw new Error.TRANSPORT ("%s", e.message);
|
||||
}
|
||||
|
||||
accept_agent.begin (server_socket, spec, promise, cancellable);
|
||||
|
||||
return promise.future;
|
||||
}
|
||||
|
||||
private async void accept_agent (Socket server_socket, InjectSpec spec, Promise<RemoteAgent> promise,
|
||||
Cancellable? cancellable) {
|
||||
var listener = new SocketListener ();
|
||||
try {
|
||||
listener.add_socket (server_socket, null);
|
||||
|
||||
var connection = (UnixConnection) yield listener.accept_async (cancellable);
|
||||
var agent = yield RemoteAgent.start (FROM_SCRATCH, spec, pid, new BootstrapResult (), connection, null,
|
||||
cancellable);
|
||||
promise.resolve (agent);
|
||||
} catch (Error e) {
|
||||
promise.reject (e);
|
||||
} catch (IOError e) {
|
||||
promise.reject (e);
|
||||
} catch (GLib.Error e) {
|
||||
promise.reject (new Error.TRANSPORT ("%s", e.message));
|
||||
} finally {
|
||||
listener.close ();
|
||||
}
|
||||
}
|
||||
|
||||
private async RemoteAgent await_agent (Future<RemoteAgent> future_agent, Cancellable? cancellable)
|
||||
throws Error, IOError {
|
||||
var settle_cancellable = new Cancellable ();
|
||||
var main_context = MainContext.get_thread_default ();
|
||||
|
||||
var timeout_source = new TimeoutSource.seconds (SETTLE_TIMEOUT_SECONDS);
|
||||
timeout_source.set_callback (() => {
|
||||
settle_cancellable.cancel ();
|
||||
return Source.REMOVE;
|
||||
});
|
||||
timeout_source.attach (main_context);
|
||||
|
||||
var cancel_source = new CancellableSource (cancellable);
|
||||
cancel_source.set_callback (() => {
|
||||
settle_cancellable.cancel ();
|
||||
return Source.REMOVE;
|
||||
});
|
||||
cancel_source.attach (main_context);
|
||||
|
||||
RemoteAgent? agent = null;
|
||||
try {
|
||||
agent = yield future_agent.wait_async (settle_cancellable);
|
||||
} catch (IOError e) {
|
||||
cancellable.set_error_if_cancelled ();
|
||||
throw new Error.PROCESS_NOT_RESPONDING ("Timed out waiting for the injected agent to connect back");
|
||||
} finally {
|
||||
cancel_source.destroy ();
|
||||
timeout_source.destroy ();
|
||||
}
|
||||
|
||||
agent.ack ();
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
private static string make_fallback_address () {
|
||||
return "/frida-" + Uuid.string_random ();
|
||||
}
|
||||
|
||||
private static uint8[] make_cstring (string str) {
|
||||
unowned uint8[] bytes = str.data;
|
||||
var result = new uint8[bytes.length + 1];
|
||||
Memory.copy (result, bytes, bytes.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static uint64 align_up (uint64 value, size_t alignment) {
|
||||
return (value + (alignment - 1)) & ~((uint64) alignment - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -359,6 +359,7 @@ if have_local_backend
|
||||
'linux' / 'symbol-resolver.vala',
|
||||
'linux' / 'proc-maps.vala',
|
||||
'linux' / 'proc-mem-injector.vala',
|
||||
'linux' / 'kernel-injector.vala',
|
||||
]
|
||||
if host_abi == 'armhf'
|
||||
host_lowlevel_abi = 'arm'
|
||||
|
||||
@@ -8,10 +8,17 @@ frida_access_get_sddl_string_for_temp_directory (void)
|
||||
{
|
||||
#define DACL_START_INHERIT L"D:AI"
|
||||
#define DACL_ACE_APPCONTAINER_RWX_WITH_CHILD_INHERIT L"(A;OICI;GRGWGX;;;AC)"
|
||||
#define DACL_ACE_SYSTEM_RWX_WITH_CHILD_INHERIT L"(A;OICI;GA;;;SY)"
|
||||
#define DACL_ACE_ADMINS_RWX_WITH_CHILD_INHERIT L"(A;OICI;GA;;;BA)"
|
||||
#define DACL_ACE_LOCAL_SERVICE_RX_WITH_CHILD_INHERIT L"(A;OICI;GRGX;;;LS)"
|
||||
|
||||
if (frida_access_is_windows_8_or_greater ())
|
||||
{
|
||||
return DACL_START_INHERIT DACL_ACE_APPCONTAINER_RWX_WITH_CHILD_INHERIT;
|
||||
return DACL_START_INHERIT
|
||||
DACL_ACE_APPCONTAINER_RWX_WITH_CHILD_INHERIT
|
||||
DACL_ACE_SYSTEM_RWX_WITH_CHILD_INHERIT
|
||||
DACL_ACE_ADMINS_RWX_WITH_CHILD_INHERIT
|
||||
DACL_ACE_LOCAL_SERVICE_RX_WITH_CHILD_INHERIT;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[wrap-git]
|
||||
url = https://github.com/frida/frida-gum.git
|
||||
revision = 17.16.4
|
||||
revision = 04f4c1499cf04e9c1004cbb02b8ff874a8dfc376
|
||||
depth = 1
|
||||
|
||||
[provide]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[wrap-git]
|
||||
url = https://github.com/frida/glib.git
|
||||
revision = 92d65736b198c2cf131f691dd83db0b25f7b02d2
|
||||
revision = 9ba86eaeb758aed3a6411a2d508be8b257c36f5f
|
||||
depth = 1
|
||||
|
||||
[provide]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[wrap-git]
|
||||
url = https://github.com/frida/lwip.git
|
||||
revision = a9f759897f26347c1a41d8d2a6d3a89aba9ce57b
|
||||
revision = 0217c5d9e5193cea0a02151305e8bc3a9b5e579f
|
||||
depth = 1
|
||||
|
||||
[provide]
|
||||
|
||||
@@ -41,6 +41,10 @@ if have_local_backend
|
||||
]
|
||||
endif
|
||||
|
||||
if have_barebone_backend
|
||||
test_sources += 'test-barebone.vala'
|
||||
endif
|
||||
|
||||
if have_compiler_backend
|
||||
test_sources += 'test-compiler.vala'
|
||||
endif
|
||||
@@ -54,6 +58,7 @@ else
|
||||
endif
|
||||
|
||||
frida_tests = static_library('frida-tests', test_sources,
|
||||
c_args: ['-DFRIDA_TESTS_SRCDIR="' + meson.current_source_dir() + '"'],
|
||||
vala_args: [
|
||||
'--pkg=gmodule-2.0',
|
||||
test_vala_args,
|
||||
|
||||
@@ -82,6 +82,10 @@ namespace Frida.Test {
|
||||
#endif
|
||||
Frida.HostSessionTest.add_tests ();
|
||||
|
||||
#if HAVE_BAREBONE_BACKEND
|
||||
Frida.BareboneTest.add_tests ();
|
||||
#endif
|
||||
|
||||
#if HAVE_COMPILER_BACKEND && !QNX
|
||||
Frida.CompilerTest.add_tests ();
|
||||
#endif
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+416
-4
@@ -1,11 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import pexpect
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from typing import NamedTuple
|
||||
import urllib.request
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) >= 2 and sys.argv[1].split("-", maxsplit=1)[0] in ("check", "boot"):
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
for arch in GUESTS:
|
||||
subparsers.add_parser(f"check-{arch}",
|
||||
help=f"verify that the {arch} guest can be booted, fetching it if needed")
|
||||
|
||||
boot = subparsers.add_parser(f"boot-{arch}",
|
||||
help=f"boot a minimal {arch} guest with the GDB stub enabled")
|
||||
boot.add_argument("--gdb-port", type=int, required=True)
|
||||
boot.add_argument("--memory", type=int, default=256)
|
||||
|
||||
for command, help_text in [
|
||||
("check-win95", "verify that the Windows 95 guest can be booted"),
|
||||
("boot-win95", "boot a Windows 95 disk image with the GDB stub enabled"),
|
||||
]:
|
||||
win95 = subparsers.add_parser(command, help=help_text)
|
||||
win95.add_argument("--image", type=Path, required=True)
|
||||
win95.add_argument("--format", default="qcow2")
|
||||
if command.startswith("boot"):
|
||||
win95.add_argument("--gdb-port", type=int, required=True)
|
||||
win95.add_argument("--memory", type=int, default=128)
|
||||
win95.add_argument("--boot-seconds", type=int, default=120)
|
||||
win95.add_argument("--qmp", type=Path)
|
||||
win95.add_argument("--debugcon", type=Path)
|
||||
|
||||
subparsers.add_parser("check-kmod",
|
||||
help="verify that a kmod guest can be booted, fetching what it needs")
|
||||
|
||||
boot_kmod = subparsers.add_parser("boot-kmod",
|
||||
help="boot the running kernel in a guest and load a module into it")
|
||||
boot_kmod.add_argument("--module", type=Path, required=True)
|
||||
boot_kmod.add_argument("--kernel", type=Path)
|
||||
boot_kmod.add_argument("--memory", type=int, default=512)
|
||||
boot_kmod.add_argument("--cpus", type=int, default=4)
|
||||
boot_kmod.add_argument("--socket", type=Path)
|
||||
boot_kmod.add_argument("--ibt", action=argparse.BooleanOptionalAction, default=True)
|
||||
|
||||
args = parser.parse_args()
|
||||
action, target = args.command.split("-", maxsplit=1)
|
||||
if target == "kmod":
|
||||
if action == "check":
|
||||
check_kmod_guest()
|
||||
else:
|
||||
boot_kmod_guest(args)
|
||||
elif target == "win95":
|
||||
if action == "check":
|
||||
check_win95_guest(args)
|
||||
else:
|
||||
boot_win95_guest(args)
|
||||
elif action == "check":
|
||||
check_guest(target)
|
||||
else:
|
||||
boot_guest(target, args)
|
||||
return
|
||||
|
||||
arch = sys.argv[1]
|
||||
args = sys.argv[2:] if len(sys.argv) >= 3 else []
|
||||
run(arch, args)
|
||||
|
||||
|
||||
def run(arch: str, args: [str]):
|
||||
import pexpect
|
||||
|
||||
child = pexpect.spawn("arm_now", ["start", arch, "--sync"])
|
||||
|
||||
child.expect("buildroot login: ")
|
||||
@@ -16,7 +89,346 @@ def run(arch: str, args: [str]):
|
||||
child.interact()
|
||||
|
||||
|
||||
def check_guest(arch: str):
|
||||
"""
|
||||
Do everything booting needs except the boot itself, so that a caller can tell a missing
|
||||
prerequisite apart from a guest that failed to come up — and pay for the download here
|
||||
rather than inside its connect timeout.
|
||||
"""
|
||||
guest = GUESTS[arch]
|
||||
require_qemu(guest)
|
||||
fetch_kernel(guest)
|
||||
print("ok")
|
||||
|
||||
|
||||
def boot_guest(arch: str, args):
|
||||
"""
|
||||
Boot a stock 32-bit Linux kernel far enough to have its page tables up, and hand it to
|
||||
QEMU's GDB stub. There is no root filesystem, so the kernel panics once it goes looking
|
||||
for one; panic=0 then parks it forever with paging still enabled, which is exactly the
|
||||
quiescent state we want to inspect.
|
||||
|
||||
QEMU discards every GDB packet except Ctrl-C while the guest is running, so the caller
|
||||
cannot attach and then wait for the boot — it has to wait first. Watch the serial console
|
||||
on its behalf and announce readiness once the kernel has parked itself.
|
||||
"""
|
||||
guest = GUESTS[arch]
|
||||
qemu = require_qemu(guest)
|
||||
kernel = fetch_kernel(guest)
|
||||
|
||||
process = subprocess.Popen([
|
||||
qemu,
|
||||
"-m", str(args.memory),
|
||||
"-kernel", str(kernel),
|
||||
"-append", "console=ttyS0 panic=0",
|
||||
"-display", "none",
|
||||
"-serial", "stdio",
|
||||
"-monitor", "none",
|
||||
"-no-reboot",
|
||||
"-gdb", f"tcp::{args.gdb_port}",
|
||||
], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
||||
|
||||
try:
|
||||
for raw_line in process.stdout:
|
||||
line = raw_line.decode(errors="replace").rstrip()
|
||||
print(line, file=sys.stderr)
|
||||
if BOOT_COMPLETE_MARKER in line:
|
||||
print("ready", flush=True)
|
||||
process.wait()
|
||||
return
|
||||
|
||||
raise Unavailable("guest exited before it finished booting")
|
||||
finally:
|
||||
process.kill()
|
||||
|
||||
|
||||
def check_win95_guest(args):
|
||||
require_win95(args)
|
||||
print("ok")
|
||||
|
||||
|
||||
def boot_win95_guest(args):
|
||||
"""
|
||||
Boot a Windows 95 disk image and hand it to QEMU's GDB stub.
|
||||
|
||||
Windows 95 says nothing on the serial line, so there is no marker to wait for the way the
|
||||
Linux guests provide one. Give the boot a fixed budget instead and let the caller decide
|
||||
whether what it finds looks like a kernel with its page tables up.
|
||||
|
||||
TCG rather than KVM: the stub is well-behaved there, and it sidesteps the divide overflow
|
||||
that fast processors provoke in this kernel.
|
||||
"""
|
||||
qemu = require_win95(args)
|
||||
|
||||
control = []
|
||||
if args.qmp is not None:
|
||||
control = [
|
||||
"-qmp", f"unix:{args.qmp},server=on,wait=off",
|
||||
"-device", "virtio-serial-pci,id=" + VIRTIO_SERIAL_ID,
|
||||
]
|
||||
|
||||
diagnostics = []
|
||||
if args.debugcon is not None:
|
||||
diagnostics = [
|
||||
"-chardev", f"file,id={DEBUGCON_ID},path={args.debugcon}",
|
||||
"-device", f"isa-debugcon,iobase={DEBUGCON_PORT:#x},chardev={DEBUGCON_ID}",
|
||||
]
|
||||
|
||||
process = subprocess.Popen([
|
||||
qemu,
|
||||
"-machine", "pc,accel=tcg",
|
||||
"-cpu", GUEST_CPU,
|
||||
"-m", str(args.memory),
|
||||
"-drive", f"file={args.image},format={args.format},if=ide",
|
||||
"-vga", "cirrus",
|
||||
"-nic", "none",
|
||||
"-display", "none",
|
||||
"-monitor", "none",
|
||||
"-no-shutdown",
|
||||
"-chardev", f"socket,id={GDB_ID},host=127.0.0.1,port={args.gdb_port},server=on,wait=off",
|
||||
"-gdb", f"chardev:{GDB_ID}",
|
||||
] + control + diagnostics, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
try:
|
||||
for _ in range(args.boot_seconds):
|
||||
if process.poll() is not None:
|
||||
raise Unavailable("guest exited before it finished booting")
|
||||
time.sleep(1)
|
||||
|
||||
print("ready", flush=True)
|
||||
process.wait()
|
||||
finally:
|
||||
process.kill()
|
||||
|
||||
|
||||
def require_win95(args) -> str:
|
||||
qemu = shutil.which("qemu-system-i386")
|
||||
if qemu is None:
|
||||
raise Unavailable("qemu-system-i386 is not installed")
|
||||
if not args.image.exists():
|
||||
raise Unavailable(f"{args.image} does not exist")
|
||||
return qemu
|
||||
|
||||
|
||||
def check_kmod_guest():
|
||||
guest = GUESTS[KMOD_GUEST_ARCH]
|
||||
require_qemu(guest)
|
||||
require_program("cpio")
|
||||
fetch_busybox(guest)
|
||||
|
||||
kernel = host_kernel()
|
||||
if not os.access(kernel, os.R_OK):
|
||||
raise Unavailable(f"{kernel} is not readable")
|
||||
|
||||
print("ok")
|
||||
|
||||
|
||||
def boot_kmod_guest(args):
|
||||
guest = GUESTS[KMOD_GUEST_ARCH]
|
||||
qemu = require_qemu(guest)
|
||||
busybox = fetch_busybox(guest)
|
||||
kernel = args.kernel if args.kernel is not None else host_kernel()
|
||||
|
||||
cmdline = ["console=ttyS0", "panic=-1"]
|
||||
if not args.ibt:
|
||||
cmdline.append("ibt=off")
|
||||
|
||||
port = PORT_NAME if args.socket is not None else None
|
||||
|
||||
with tempfile.TemporaryDirectory() as staging_dir:
|
||||
initramfs = build_initramfs(Path(staging_dir), busybox, args.module, port)
|
||||
|
||||
channel = []
|
||||
if args.socket is not None:
|
||||
channel = [
|
||||
"-device", "virtio-serial",
|
||||
"-chardev", f"socket,id=frida,path={args.socket},server=on,wait=off",
|
||||
"-device", f"virtserialport,chardev=frida,name={port}",
|
||||
]
|
||||
|
||||
process = subprocess.Popen([
|
||||
qemu,
|
||||
"-m", str(args.memory),
|
||||
"-smp", str(args.cpus),
|
||||
"-enable-kvm",
|
||||
"-cpu", "host",
|
||||
"-kernel", str(kernel),
|
||||
"-initrd", str(initramfs),
|
||||
"-append", " ".join(cmdline),
|
||||
"-display", "none",
|
||||
"-serial", "stdio",
|
||||
"-monitor", "none",
|
||||
"-no-reboot",
|
||||
] + channel, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
||||
|
||||
listening = False
|
||||
try:
|
||||
for raw_line in process.stdout:
|
||||
line = raw_line.decode(errors="replace").rstrip()
|
||||
print(line, file=sys.stderr)
|
||||
if MODULE_LOAD_FAILED_MARKER in line:
|
||||
raise Unavailable("the module failed to load")
|
||||
if SLEPT_WHERE_FORBIDDEN_MARKER in line:
|
||||
raise Unavailable("the module slept where the caller forbids it")
|
||||
if not listening and AGENT_READY_MARKER in line:
|
||||
print("ready", flush=True)
|
||||
listening = True
|
||||
|
||||
if not listening:
|
||||
raise Unavailable("guest exited before the module was listening")
|
||||
finally:
|
||||
process.kill()
|
||||
|
||||
|
||||
def require_qemu(guest: "Guest") -> str:
|
||||
qemu = shutil.which(guest.qemu_binary)
|
||||
if qemu is None:
|
||||
raise Unavailable(f"{guest.qemu_binary} is not installed")
|
||||
return qemu
|
||||
|
||||
|
||||
def fetch_kernel(guest: "Guest") -> Path:
|
||||
return fetch_cached(guest, "vmlinuz-lts", guest.kernel_url)
|
||||
|
||||
|
||||
def require_program(name: str) -> str:
|
||||
program = shutil.which(name)
|
||||
if program is None:
|
||||
raise Unavailable(f"{name} is not installed")
|
||||
return program
|
||||
|
||||
|
||||
def fetch_busybox(guest: "Guest") -> Path:
|
||||
return fetch_cached(guest, "busybox", BUSYBOX_URL)
|
||||
|
||||
|
||||
def host_kernel() -> Path:
|
||||
return Path("/boot") / ("vmlinuz-" + os.uname().release)
|
||||
|
||||
|
||||
def build_initramfs(staging_dir: Path, busybox: Path, module: Path,
|
||||
port: str | None) -> Path:
|
||||
root = staging_dir / "root"
|
||||
(root / "bin").mkdir(parents=True)
|
||||
(root / "dev").mkdir()
|
||||
(root / "proc").mkdir()
|
||||
|
||||
shutil.copy(busybox, root / "bin" / "busybox")
|
||||
(root / "bin" / "busybox").chmod(0o755)
|
||||
for applet in ("sh", "insmod", "dmesg", "mount", "poweroff", "cat", "seq"):
|
||||
(root / "bin" / applet).symlink_to("busybox")
|
||||
|
||||
shutil.copy(module, root / module.name)
|
||||
|
||||
init = root / "init"
|
||||
init.write_text("\n".join([
|
||||
"#!/bin/sh",
|
||||
"mount -t proc proc /proc",
|
||||
"mount -t devtmpfs dev /dev",
|
||||
f"insmod /{module.name} || echo {MODULE_LOAD_FAILED_MARKER}",
|
||||
"for i in 1 2 3 4 5 6 7 8; do",
|
||||
" (for j in $(seq 200); do cat /proc/self/maps /proc/1/status > /dev/null; done) &",
|
||||
"done",
|
||||
"wait",
|
||||
*(BRIDGE if port is not None else []),
|
||||
"dmesg",
|
||||
"exec sh",
|
||||
"",
|
||||
]))
|
||||
init.chmod(0o755)
|
||||
|
||||
initramfs = staging_dir / "initramfs.cpio"
|
||||
names = subprocess.run(["find", "."], cwd=root, check=True, capture_output=True)
|
||||
with initramfs.open("wb") as f:
|
||||
subprocess.run(["cpio", "--create", "--format=newc", "--quiet"],
|
||||
cwd=root, input=names.stdout, stdout=f, check=True)
|
||||
|
||||
return initramfs
|
||||
|
||||
|
||||
def fetch_cached(guest: "Guest", name: str, url: str) -> Path:
|
||||
cached = cache_dir(guest) / name
|
||||
if cached.exists():
|
||||
return cached
|
||||
|
||||
cached.parent.mkdir(parents=True, exist_ok=True)
|
||||
staging = cached.with_suffix(".partial")
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=60) as response, staging.open("wb") as f:
|
||||
shutil.copyfileobj(response, f)
|
||||
except Exception as e:
|
||||
staging.unlink(missing_ok=True)
|
||||
raise Unavailable(f"unable to download {url}: {e}")
|
||||
staging.replace(cached)
|
||||
|
||||
return cached
|
||||
|
||||
|
||||
def cache_dir(guest: "Guest") -> Path:
|
||||
base = os.environ.get("XDG_CACHE_HOME")
|
||||
root = Path(base) if base is not None else Path.home() / ".cache"
|
||||
return root / "frida-tests" / f"qemu-{guest.arch}"
|
||||
|
||||
|
||||
class Unavailable(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Guest(NamedTuple):
|
||||
arch: str
|
||||
qemu_binary: str
|
||||
kernel_url: str
|
||||
|
||||
# Paging comes up long before this, so the panic that follows the missing root filesystem is a
|
||||
# safely late — and unmistakable — sign that the kernel has finished with its page tables.
|
||||
BOOT_COMPLETE_MARKER = "Kernel panic"
|
||||
|
||||
# Alpine's netboot kernels are the smallest stock ones that are trivially fetchable. They are
|
||||
# not versioned in-place, so treat whatever the mirror currently serves as the fixture; the
|
||||
# tests only care that it is a Linux kernel that enables paging.
|
||||
ALPINE_NETBOOT_URL = "https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/{0}/netboot/vmlinuz-lts"
|
||||
|
||||
GUESTS = {
|
||||
guest.arch: guest
|
||||
for guest in [
|
||||
Guest("x86", "qemu-system-i386", ALPINE_NETBOOT_URL.format("x86")),
|
||||
Guest("x86_64", "qemu-system-x86_64", ALPINE_NETBOOT_URL.format("x86_64")),
|
||||
]
|
||||
}
|
||||
|
||||
KMOD_GUEST_ARCH = "x86_64"
|
||||
|
||||
BUSYBOX_URL = "https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox"
|
||||
|
||||
VIRTIO_SERIAL_ID = "frida-vserial"
|
||||
|
||||
GDB_ID = "frida-gdb"
|
||||
|
||||
# Newest model that boots 95; older ones lack the CMOVs the agent emits.
|
||||
GUEST_CPU = "pentium3"
|
||||
|
||||
DEBUGCON_ID = "frida-debugcon"
|
||||
DEBUGCON_PORT = 0xe9
|
||||
|
||||
PORT_NAME = "frida"
|
||||
PORT_PATH = "/dev/vport0p1"
|
||||
|
||||
BRIDGE = [
|
||||
"exec 3<> /dev/frida",
|
||||
f"exec 4<> {PORT_PATH}",
|
||||
"while :; do cat <&3 >&4; done &",
|
||||
"while :; do cat <&4 >&3; sleep 1; done &",
|
||||
]
|
||||
|
||||
AGENT_READY_MARKER = "frida: listening on /dev/"
|
||||
MODULE_LOAD_FAILED_MARKER = "frida-kmod-load-failed"
|
||||
|
||||
SLEPT_WHERE_FORBIDDEN_MARKER = "Voluntary context switch within RCU"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arch = sys.argv[1]
|
||||
args = sys.argv[2:] if len(sys.argv) >= 3 else []
|
||||
run(arch, args)
|
||||
try:
|
||||
main()
|
||||
except Unavailable as e:
|
||||
print(f"unavailable: {e}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
Reference in new issue
Block a user