Compare commits

..
4 Commits
Author SHA1 Message Date
Ole André Vadla Ravnås f67d1e81df Dump alive tasks every 30s in server and agent
Route the dump through GLib.info() using the new
g_task_describe_alive_tasks(), so the agent's dump reaches
logcat where its stdio is not visible. Bump the GLib wrap to
the branch holding that function.
2026-07-29 18:12:10 +02:00
Ole André Vadla Ravnås a068c704d1 Trace agent session detach notification path
Log every hop that agent_session_detached has to survive, from
the agent emitting closed, through frida-server's provider and
control channel, the droidy relay, and finally Device's pending
detach request. Each silent id-keyed drop now reports whether
the lookup succeeded, so a hanging detach() points straight at
the hop that swallowed the notification.
2026-07-29 17:47:25 +02:00
Ole André Vadla Ravnås 34ac3a0d50 server: Add output redirection option
Add -o/--output for redirecting stdout and stderr to a file,
so output survives daemonizing. Skip the stdin debug console
when daemonized, as stdin is then /dev/null.
2026-07-29 15:10:37 +02:00
Ole André Vadla Ravnås 1ea81bcd59 server: Add stdin-based task debug console 2026-07-28 14:47:05 +02:00
39 changed files with 1542 additions and 4132 deletions

No files matched your search

-3
View File
@@ -3,9 +3,6 @@
/deps/
/src/android-helper/build/
/src/barebone/agent/target/
/src/barebone/agent/target-linux/
/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/
+18
View File
@@ -292,6 +292,8 @@ namespace Frida.Agent {
start.begin ((owned) padder);
start_alive_task_dumper ();
main_loop.run ();
main_context.pop_thread_default ();
@@ -362,6 +364,16 @@ namespace Frida.Agent {
padder = null;
}
// TEMPORARY DEBUG HACK: dump alive GTasks every 30 seconds.
private void start_alive_task_dumper () {
var source = new TimeoutSource.seconds (30);
source.set_callback (() => {
GLib.info ("%s", g_task_describe_alive_tasks ());
return Source.CONTINUE;
});
source.attach (main_context);
}
private void keep_running_eternalized () {
agent_gthread = new Thread<bool> ("frida-eternal-agent", () => {
var ignore_scope = new ThreadIgnoreScope (FRIDA_THREAD);
@@ -924,6 +936,8 @@ namespace Frida.Agent {
private void on_session_closed (BaseAgentSession base_session) {
LiveAgentSession session = (LiveAgentSession) base_session;
GLib.info ("detach-trace: Agent emitting provider closed id=%s", session.id.handle);
closed (session.id);
unregister_session (session);
@@ -1724,4 +1738,8 @@ namespace Frida.Agent {
return repeat;
});
}
// TEMPORARY DEBUG HACK: only present when GLib is built with G_ENABLE_DEBUG.
[CCode (cname = "g_task_describe_alive_tasks")]
private extern string g_task_describe_alive_tasks ();
}
+3
View File
@@ -361,6 +361,8 @@ namespace Frida {
private async void teardown_peer_connection_and_emit_closed () {
schedule_on_frida_thread (() => {
GLib.info ("detach-trace: AgentMessageTransmitter %p emitting closed, nice_agent=%p",
this, nice_agent);
if (nice_agent != null)
close_nice_resources_and_emit_closed.begin ();
else
@@ -598,6 +600,7 @@ namespace Frida {
private async void teardown_peer_connection_and_emit_closed () {
schedule_on_frida_thread (() => {
GLib.info ("detach-trace: AgentMessageTransmitter %p emitting closed", this);
closed ();
return Source.REMOVE;
});
+6
View File
@@ -57,6 +57,8 @@ namespace Frida {
}
public async void close (Cancellable? cancellable) throws IOError {
GLib.info ("detach-trace: BaseAgentSession.close() id=%s close_request=%p", id.handle, close_request);
while (close_request != null) {
try {
yield close_request.future.wait_async (cancellable);
@@ -85,6 +87,8 @@ namespace Frida {
yield transmitter.close (cancellable);
GLib.info ("detach-trace: BaseAgentSession.close() done id=%s", id.handle);
close_request.resolve (true);
}
@@ -276,6 +280,8 @@ namespace Frida {
transmitter.new_candidates.disconnect (on_transmitter_new_candidates);
transmitter.candidate_gathering_done.disconnect (on_transmitter_candidate_gathering_done);
GLib.info ("detach-trace: BaseAgentSession emitting closed id=%s", id.handle);
closed ();
}
+1 -1
Submodule releng updated: c9cc18138b...abb8238970.
+87 -4
View File
@@ -12,6 +12,7 @@ namespace Frida.Server {
private static string? directory = null;
#if !WINDOWS && !TVOS
private static bool daemonize = false;
private static string? output_path = null;
#endif
private static string? softener_flavor_str = null;
private static bool enable_preload = true;
@@ -42,6 +43,7 @@ namespace Frida.Server {
{ "directory", 'd', 0, OptionArg.STRING, ref directory, "Store binaries in DIRECTORY", "DIRECTORY" },
#if !WINDOWS && !TVOS
{ "daemonize", 'D', 0, OptionArg.NONE, ref daemonize, "Detach and become a daemon", null },
{ "output", 'o', 0, OptionArg.FILENAME, ref output_path, "Redirect output to FILE", "FILE" },
#endif
{ "policy-softener", 0, 0, OptionArg.STRING, ref softener_flavor_str, "Select policy softener", "system|internal" },
{ "disable-preload", 'P', OptionFlags.REVERSE, OptionArg.NONE, ref enable_preload, "Disable preload optimization", null },
@@ -78,6 +80,13 @@ namespace Frida.Server {
Environment.set_verbose_logging_enabled (verbose);
#if !WINDOWS && !TVOS
if (output_path != null && !daemonize) {
if (!redirect_output_to (output_path))
return 6;
}
#endif
EndpointParameters endpoint_params;
try {
endpoint_params = new EndpointParameters (listen_address, 0, parse_certificate (certpath), origin,
@@ -156,12 +165,15 @@ namespace Frida.Server {
Posix.setsid ();
var null_in = Posix.open ("/dev/null", Posix.O_RDONLY);
var null_out = Posix.open ("/dev/null", Posix.O_WRONLY);
Posix.dup2 (null_in, Posix.STDIN_FILENO);
Posix.dup2 (null_out, Posix.STDOUT_FILENO);
Posix.dup2 (null_out, Posix.STDERR_FILENO);
Posix.close (null_in);
Posix.close (null_out);
if (output_path == null || !redirect_output_to (output_path)) {
var null_out = Posix.open ("/dev/null", Posix.O_WRONLY);
Posix.dup2 (null_out, Posix.STDOUT_FILENO);
Posix.dup2 (null_out, Posix.STDERR_FILENO);
Posix.close (null_out);
}
}
var status = new uint8[1];
@@ -218,6 +230,22 @@ namespace Frida.Server {
return application.run ();
}
#if !WINDOWS && !TVOS
private static bool redirect_output_to (string path) {
int fd = Posix.open (path, Posix.O_WRONLY | Posix.O_CREAT | Posix.O_APPEND, 0644);
if (fd == -1) {
printerr ("Unable to open “%s”: %s\n", path, Posix.strerror (Posix.errno));
return false;
}
Posix.dup2 (fd, Posix.STDOUT_FILENO);
Posix.dup2 (fd, Posix.STDERR_FILENO);
Posix.close (fd);
return true;
}
#endif
namespace Environment {
public extern void init ();
public extern void set_verbose_logging_enabled (bool enabled);
@@ -274,6 +302,15 @@ namespace Frida.Server {
return false;
});
#if !WINDOWS && !TVOS
if (!daemonize)
process_commands.begin ();
#elif !WINDOWS
process_commands.begin ();
#endif
start_alive_task_dumper ();
exit_code = 0;
loop.run ();
@@ -281,6 +318,45 @@ namespace Frida.Server {
return exit_code;
}
#if !WINDOWS
// TEMPORARY DEBUG HACK: type “t” + Enter to dump alive GTasks.
private async void process_commands () {
var input = new DataInputStream (new UnixInputStream (Posix.STDIN_FILENO, false));
while (true) {
string? line;
try {
line = yield input.read_line_async (Priority.DEFAULT, io_cancellable);
} catch (GLib.Error e) {
return;
}
if (line == null)
return;
switch (line.strip ()) {
case "t":
g_task_print_alive_tasks ();
break;
case "":
break;
default:
printerr ("Unknown command; try “t”\n");
break;
}
}
}
#endif
// TEMPORARY DEBUG HACK: dump alive GTasks every 30 seconds.
private void start_alive_task_dumper () {
var source = new TimeoutSource.seconds (30);
source.set_callback (() => {
GLib.info ("%s", g_task_describe_alive_tasks ());
return Source.CONTINUE;
});
source.attach (MainContext.default ());
}
private async void start () {
try {
if (device_id != null && device_id != "local") {
@@ -355,4 +431,11 @@ namespace Frida.Server {
return new TlsCertificate.from_file (path);
}
// TEMPORARY DEBUG HACK: only present when GLib is built with G_ENABLE_DEBUG.
[CCode (cname = "g_task_print_alive_tasks")]
private extern void g_task_print_alive_tasks ();
[CCode (cname = "g_task_describe_alive_tasks")]
private extern string g_task_describe_alive_tasks ();
}
+9 -38
View File
@@ -5,13 +5,10 @@ namespace Frida.Barebone {
private Cancellable io_cancellable = new Cancellable ();
private IOStream hostlink;
private SocketConnection 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;
@@ -49,23 +46,6 @@ 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;
@@ -73,8 +53,8 @@ namespace Frida.Barebone {
var transport_tag = yield resolve_transport (cancellable);
var gdb = machine.gdb;
byte_order = gdb.byte_order;
pointer_size = gdb.pointer_size;
ByteOrder byte_order = gdb.byte_order;
uint pointer_size = gdb.pointer_size;
Layout layout;
uint64 preferred_base = 0;
@@ -287,7 +267,7 @@ namespace Frida.Barebone {
});
}
private void adopt_hostlink_streams (IOStream connection) {
private void adopt_hostlink_streams (SocketConnection connection) {
hostlink = connection;
input = (BufferedInputStream) Object.new (typeof (BufferedInputStream),
"base-stream", hostlink.get_input_stream (),
@@ -361,25 +341,16 @@ namespace Frida.Barebone {
});
timeout_source.attach (MainContext.get_thread_default ());
Variant response = null;
try {
response = yield promise.future.wait_async (cancellable);
return 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;
@@ -532,10 +503,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 (byte_order != ByteOrder.HOST)
if (machine.gdb.byte_order != ByteOrder.HOST)
message = message.byteswap ();
var message_bytes = message.get_data_as_bytes ();
return new BufferBuilder (byte_order, pointer_size)
return machine.gdb.make_buffer_builder ()
.append_uint32 ((uint32) message_bytes.get_size ())
.append_bytes (message_bytes)
.build ();
-16
View File
@@ -4,22 +4,6 @@ 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 = ["xnu"]
[features]
xnu = []
linux = []
[profile.release]
panic = "abort"
opt-level = "z"
+1 -20
View File
@@ -21,29 +21,10 @@
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 --features xnu \
&& make -C ~/src/frida-python \
&& killall -9 qemu-system-aarch64 && sleep 2 \
&& frida -D barebone -p 0
cargo build --release && make -C ~/src/frida-python && killall -9 qemu-system-aarch64 && sleep 2 && frida -D barebone -p 0
## Speeding up loop
+19 -33
View File
@@ -11,13 +11,9 @@ fn main() {
let cc_str = env::var("CC_aarch64_unknown_none")
.or_else(|_| env::var("CC"))
.unwrap_or_else(|_| "cc".to_string());
// 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 = Path::new(&cc_str);
let cc_include_paths = detect_gcc_include_paths(cc);
let cc_library_paths = detect_gcc_library_paths(cc);
let cc_clang_args: Vec<String> = cc_include_paths
.iter()
@@ -40,35 +36,25 @@ fn main() {
.write_to_file(out_dir.join("bindings.rs"))
.expect("Couldn't write bindings");
// 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_XNU").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());
}
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");
// 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-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());
}
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, args: &[&str]) -> Vec<PathBuf> {
pub fn detect_gcc_include_paths(gcc: &Path) -> Vec<PathBuf> {
let out = Command::new(gcc)
.args(args)
.args(["-xc", "-E", "-v", "-"])
.stdin(Stdio::null())
.output()
@@ -108,8 +94,8 @@ pub fn detect_gcc_include_paths(gcc: &Path, args: &[&str]) -> Vec<PathBuf> {
paths
}
pub fn detect_gcc_library_paths(gcc: &Path, args: &[&str]) -> Vec<PathBuf> {
let out = Command::new(gcc).args(args).arg("-print-search-dirs").output()
pub fn detect_gcc_library_paths(gcc: &Path) -> Vec<PathBuf> {
let out = Command::new(gcc).arg("-print-search-dirs").output()
.expect("Failed to execute GCC to detect library paths");
let stdout = String::from_utf8_lossy(&out.stdout);
-8
View File
@@ -1,8 +0,0 @@
{
"agent": {
"transport": {
"type": "device",
"path": "/dev/frida"
}
}
}
-17
View File
@@ -1,17 +0,0 @@
obj-m += frida-agent.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.
frida-agent-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)
ccflags-y += -Wno-declaration-after-statement
-108
View File
@@ -1,108 +0,0 @@
# 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
ARCH ?= arm64
RUST_TARGET ?= aarch64-unknown-none
PROFILE ?= release
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 aarch64 ELF will do; nothing here is compiled with them.
AGENT_LD ?= aarch64-none-elf-ld
AGENT_AR ?= aarch64-none-elf-ar
AGENT_NM ?= aarch64-none-elf-nm
AGENT_OBJCOPY ?= aarch64-none-elf-objcopy
# arm64 kernels reserve x18 for the shadow call stack, so nothing we link in may use
# it as a general-purpose register. The flag only reaches crates Cargo compiles, and
# the toolchain ships core, alloc and compiler_builtins prebuilt without it, so those
# three have to be rebuilt from source as well. That needs the rust-src component and
# an unstable flag, hence RUSTC_BOOTSTRAP.
AGENT_RUSTFLAGS ?= -Zfixed-x18
AGENT_BUILD_STD ?= -Z build-std=core,alloc,compiler_builtins
# 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-arm64-softfloat SDK prefix)
LIBM = $(FRIDA_SDK)/lib/libm.a
LIBBUILTINS = $(FRIDA_SDK)/lib/libclang_rt.builtins.a
PRELINKED := frida-agent-prelinked.o.bin
# Rust's toolchain ships compiler_builtins compiled for the ordinary AAPCS, where a
# double travels in d0: its __muldf3 opens with "fmov x14, d0". Everything else here
# 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.
AGENT_LIB_SOFTFLOAT := $(AGENT_TARGET_DIR)/libfrida-barebone-agent-softfloat.a
# 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
KEEP_GLOBAL := $(foreach s,$(ENTRY_POINTS),--keep-global-symbol=$(s))
all: modules
modules: $(PRELINKED)
$(MAKE) -C $(KDIR) $(if $(KOUT),O=$(KOUT)) M=$(CURDIR) ARCH=$(ARCH) modules
$(PRELINKED): $(AGENT_LIB_SOFTFLOAT) $(LIBC)
$(AGENT_LD) -r -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) $@
$(AGENT_AR) d $@ $$($(AGENT_NM) --print-armap $@ | sed -n 's/^__muldf3 in //p')
$(AGENT_AR) s $@
$(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)" cargo build --$(PROFILE) \
--target $(RUST_TARGET) --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
-154
View File
@@ -1,154 +0,0 @@
# 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
export GUMJS_DEVKIT_DIR=/path/to/frida-gum/build/bindings/gumjs/devkit
make -C linux KDIR=/path/to/kernel/build
`make` builds the Rust staticlib itself, then prelinks it and hands the result to
kbuild. `KDIR` must be the configured build tree of the kernel the module is for:
same source commit, same config. Under `CONFIG_MODVERSIONS` anything else is rejected
at load time, and the `module_layout` CRC covers `struct module` itself, so even
config options that merely add fields to it — `CONFIG_DEBUG_INFO_BTF_MODULES`, for
one — have to match.
The prelink half runs anywhere; the kbuild half needs a Linux host. Cross-building
from macOS means running that step in a container holding the kernel tree.
### Getting a KDIR for an Android device
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 — for
`6.1.145-android14-11-gc1de4747ac59-ab14219743` that is commit `c1de4747ac59` and
build `14219743` — and three pieces keyed to them are published:
- `modules_prepare_outdir.tar.gz` and `kernel_aarch64_Module.symvers` from
`ci.android.com`, under that build's `kernel_aarch64` target. The first is the
configured build tree with `scripts/` already compiled for a Linux x86-64 host; the
second carries the CRCs `CONFIG_MODVERSIONS` checks.
- `kernel/common` at that commit, which a shallow single-commit fetch gets in a few
hundred megabytes. Only its makefiles are wanted: the top-level `Makefile` in the
prepared tree is a stub that includes the source's, so point it at wherever the
fetch landed and build with `make -C <source> O=<outdir> M=<here>`.
A different GKI build of the same branch does not substitute for these: vermagic is
compared verbatim, and it names the exact build.
## 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
## What the target kernel has to provide
- **arm64.** The register-level pieces (`tcr_el1` for the page size, cache
maintenance) are aarch64-only, matching the XNU flavour.
- **`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.
- **`CONFIG_VSOCKETS`**, plus a vsock transport for the guest.
- **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 arm64 kernel imposes an ABI on anything linked into it, and both of the
constraints below are why this flavour is built against a soft-float SDK
(`none-arm64-softfloat`) rather than the prebuilt `none-arm64` one.
- **FP/SIMD.** JavaScript numbers are doubles, so a hardfloat build would use FP
for as long as the runtime lives. Kernels from 6.9 preserve kernel-mode FP/SIMD
across sleeping, but before 6.9 there is no correct way to hold it:
`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 `-mabi=aapcs-soft -mgeneral-regs-only` removes the
question rather than answering it.
- **Shadow call stack.** 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
`none-arm64-softfloat` 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.
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.
## 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, where
that stack is 16 KiB. 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.
-823
View File
@@ -1,823 +0,0 @@
/*
* Glue between the Rust agent and the Linux kernel it is loaded into.
*
* Everything the agent needs from the kernel lives here rather than in Rust,
* because all of it is version- and config-dependent: struct layouts, inline helpers
* that are really macros, the character-device and uaccess APIs, and which of
* kallsyms' entry points happen to be exported this release. Compiling against the
* target kernel's own headers is what keeps that knowledge correct.
*/
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/kallsyms.h>
#include <linux/kasan.h>
#include <linux/kfifo.h>
#include <linux/kprobes.h>
#include <linux/kthread.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/poll.h>
#include <linux/sched.h>
#include <linux/set_memory.h>
#include <linux/slab.h>
#include <linux/timekeeping.h>
#include <linux/uaccess.h>
#include <linux/version.h>
#include <linux/vmalloc.h>
#include <linux/wait.h>
#ifdef CONFIG_ARM64
# include <asm/cacheflush.h>
#endif
/* Mirrors GumPageProtection. */
#define FRIDA_PAGE_WRITE (1 << 1)
#define FRIDA_PAGE_EXECUTE (1 << 2)
typedef int (* FridaFoundSymbolFunc) (const char * name, u64 address, void * user_data);
typedef int (* FridaFoundModuleFunc) (const char * name, const char * version, u64 base, u64 size,
void * user_data);
typedef void (* FridaThreadEntry) (void * parameter, int wait_result);
struct frida_thread_ctx
{
FridaThreadEntry entry;
void * parameter;
};
struct frida_writable_alias
{
struct list_head node;
void * mapping;
unsigned long first_page;
unsigned int n_pages;
};
struct frida_symbol_visit_ctx
{
FridaFoundSymbolFunc func;
void * user_data;
};
int frida_agent_start (void);
void frida_agent_stop (void);
u64 frida_kmod_find_symbol (const char * name);
void frida_kmod_wake (void);
void frida_kmod_yield (void);
static void frida_resolve_kallsyms (void);
static void frida_resolve_kernel_range (void);
static void * frida_resolve_unexported (const char * name);
static int frida_thread_trampoline (void * data);
static int frida_dev_open (struct inode * inode, struct file * file);
static int frida_dev_release (struct inode * inode, struct file * file);
static ssize_t frida_dev_read (struct file * file, char __user * buffer, size_t size,
loff_t * offset);
static ssize_t frida_dev_write (struct file * file, const char __user * buffer, size_t size,
loff_t * offset);
static __poll_t frida_dev_poll (struct file * file, struct poll_table_struct * wait);
static struct page * frida_page_for_virtual (unsigned long address);
static bool frida_is_vmalloc_or_module_addr (const void * x);
#if LINUX_VERSION_CODE >= KERNEL_VERSION (6, 4, 0)
static int frida_on_each_symbol (void * data, const char * name, unsigned long address);
#else
static int frida_on_each_symbol (void * data, const char * name, struct module * mod,
unsigned long address);
#endif
static bool frida_should_wake (unsigned int seq);
static void frida_strip_module_suffix (char * rendered);
static DECLARE_WAIT_QUEUE_HEAD (frida_wq);
static atomic_t frida_wake_seq = ATOMIC_INIT (0);
static u64 frida_kernel_base;
static u64 frida_kernel_size;
static typeof (&kallsyms_lookup_name) frida_kallsyms_lookup_name_impl;
static typeof (&kallsyms_on_each_symbol) frida_kallsyms_on_each_symbol_impl;
static struct list_head * frida_modules;
static struct mutex * frida_module_mutex;
static typeof (&set_memory_ro) frida_set_memory_ro_impl;
static typeof (&set_memory_rw) frida_set_memory_rw_impl;
static typeof (&set_memory_x) frida_set_memory_x_impl;
static typeof (&set_memory_nx) frida_set_memory_nx_impl;
static LIST_HEAD (frida_writable_aliases);
static DEFINE_MUTEX (frida_writable_aliases_mutex);
#define FRIDA_LINK_CAPACITY (1024 * 1024)
#define FRIDA_WAIT_FOREVER_SLICE_US (1000 * 1000)
static DECLARE_KFIFO_PTR (frida_to_client, u8);
static DECLARE_KFIFO_PTR (frida_from_client, u8);
static DEFINE_MUTEX (frida_link_mutex);
static DECLARE_WAIT_QUEUE_HEAD (frida_readable_wq);
static atomic_t frida_client_count = ATOMIC_INIT (0);
static bool frida_link_registered;
static const struct file_operations frida_dev_fops =
{
.owner = THIS_MODULE,
.open = frida_dev_open,
.release = frida_dev_release,
.read = frida_dev_read,
.write = frida_dev_write,
.poll = frida_dev_poll,
};
static struct miscdevice frida_dev =
{
.minor = MISC_DYNAMIC_MINOR,
.name = "frida",
.fops = &frida_dev_fops,
.mode = 0600,
};
static int __init
frida_kmod_init (void)
{
frida_resolve_kallsyms ();
frida_resolve_kernel_range ();
return frida_agent_start ();
}
static void __exit
frida_kmod_exit (void)
{
frida_agent_stop ();
}
module_init (frida_kmod_init);
module_exit (frida_kmod_exit);
MODULE_LICENSE ("GPL");
MODULE_DESCRIPTION ("Frida instrumentation agent");
MODULE_VERSION ("1.0.0");
static void
frida_resolve_kallsyms (void)
{
frida_kallsyms_lookup_name_impl = frida_resolve_unexported ("kallsyms_lookup_name");
frida_kallsyms_on_each_symbol_impl = frida_resolve_unexported ("kallsyms_on_each_symbol");
if (frida_kallsyms_lookup_name_impl == NULL)
{
printk (KERN_WARNING "frida: kallsyms_lookup_name unavailable\n");
return;
}
frida_modules = (struct list_head *) frida_kallsyms_lookup_name_impl ("modules");
frida_module_mutex = (struct mutex *) frida_kallsyms_lookup_name_impl ("module_mutex");
/*
* GKI trims its export table to the KMI symbol list, which leaves set_memory_*()
* callable but unexported. Being absent from Module.symvers only stops the module
* loader from resolving them, so go through kallsyms like everything else here.
*/
frida_set_memory_ro_impl = (void *) frida_kallsyms_lookup_name_impl ("set_memory_ro");
frida_set_memory_rw_impl = (void *) frida_kallsyms_lookup_name_impl ("set_memory_rw");
frida_set_memory_x_impl = (void *) frida_kallsyms_lookup_name_impl ("set_memory_x");
frida_set_memory_nx_impl = (void *) frida_kallsyms_lookup_name_impl ("set_memory_nx");
}
static void
frida_resolve_kernel_range (void)
{
u64 start, end;
start = frida_kmod_find_symbol ("_stext");
end = frida_kmod_find_symbol ("_end");
if (start == 0 || end == 0)
return;
frida_kernel_base = start;
frida_kernel_size = end - start;
}
/*
* kallsyms_lookup_name() and kallsyms_on_each_symbol() stopped being exported in
* 5.7. A kprobe registered by symbol name resolves an address without needing an
* export, which is the standard way back in.
*/
static void *
frida_resolve_unexported (const char * name)
{
struct kprobe kp = { .symbol_name = name };
void * address;
if (register_kprobe (&kp) < 0)
return NULL;
address = kp.addr;
unregister_kprobe (&kp);
return address;
}
void
frida_kmod_log (const char * message)
{
printk (KERN_INFO "%s", message);
}
void
frida_kmod_panic (const char * message)
{
panic ("%s", message);
}
int
frida_kmod_spawn_thread (FridaThreadEntry entry,
void * parameter)
{
struct frida_thread_ctx * ctx;
struct task_struct * task;
ctx = kmalloc (sizeof (struct frida_thread_ctx), GFP_KERNEL);
ctx->entry = entry;
ctx->parameter = parameter;
task = kthread_run (frida_thread_trampoline, ctx, "frida-agent");
if (IS_ERR (task))
{
kfree (ctx);
return PTR_ERR (task);
}
return 0;
}
static int __nocfi
frida_thread_trampoline (void * data)
{
struct frida_thread_ctx ctx = *((struct frida_thread_ctx *) data);
kfree (data);
ctx.entry (ctx.parameter, 0);
return 0;
}
void *
frida_kmod_alloc (size_t size)
{
return kvmalloc (size, GFP_KERNEL);
}
void
frida_kmod_free (void * ptr,
size_t size)
{
kvfree (ptr);
}
/*
* Code slabs have to be page-granular and live somewhere set_memory_x() accepts,
* which rules out the slab allocator and its sub-page objects.
*/
void *
frida_kmod_alloc_code (size_t size)
{
return __vmalloc (PAGE_ALIGN (size), GFP_KERNEL | __GFP_ZERO);
}
void
frida_kmod_free_code (void * ptr,
size_t size)
{
vfree (ptr);
}
void
frida_kmod_own_range (u64 * base,
u64 * size)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION (6, 4, 0)
*base = (u64) (uintptr_t) THIS_MODULE->mem[MOD_TEXT].base;
*size = THIS_MODULE->mem[MOD_TEXT].size;
#else
*base = (u64) (uintptr_t) THIS_MODULE->core_layout.base;
*size = THIS_MODULE->core_layout.size;
#endif
}
/*
* The transport is a character device rather than a socket the agent dials out on:
* userspace decides when to attach, needs no network namespace to reach us, and the
* relay on the other side is whatever the operator points at /dev/frida. The 4-byte
* length prefix framing is done by the agent, so this is a plain byte stream.
*/
int
frida_kmod_link_open (void)
{
int res;
res = kfifo_alloc (&frida_to_client, FRIDA_LINK_CAPACITY, GFP_KERNEL);
if (res != 0)
return res;
res = kfifo_alloc (&frida_from_client, FRIDA_LINK_CAPACITY, GFP_KERNEL);
if (res != 0)
goto free_to_client;
res = misc_register (&frida_dev);
if (res != 0)
goto free_from_client;
frida_link_registered = true;
printk (KERN_INFO "frida: listening on /dev/%s\n", frida_dev.name);
return 0;
free_from_client:
kfifo_free (&frida_from_client);
free_to_client:
kfifo_free (&frida_to_client);
return res;
}
int
frida_kmod_link_send (const void * data,
size_t size)
{
unsigned int written;
/* Nothing is listening yet, or the client cannot keep up: the agent's replies are
* only meaningful to a client that is still there, so drop them rather than stall
* the runtime. */
if (atomic_read (&frida_client_count) == 0)
return -ENOTCONN;
mutex_lock (&frida_link_mutex);
written = kfifo_in (&frida_to_client, data, size);
mutex_unlock (&frida_link_mutex);
if (written != size)
return -ENOSPC;
wake_up_interruptible (&frida_readable_wq);
return 0;
}
long
frida_kmod_link_recv (void * data,
size_t size)
{
unsigned int n;
mutex_lock (&frida_link_mutex);
n = kfifo_out (&frida_from_client, data, size);
mutex_unlock (&frida_link_mutex);
return n;
}
void
frida_kmod_link_close (void)
{
if (!frida_link_registered)
return;
frida_link_registered = false;
misc_deregister (&frida_dev);
kfifo_free (&frida_from_client);
kfifo_free (&frida_to_client);
}
/* One client at a time: the protocol is a single conversation, and two readers
* would each get half of every frame. */
static int
frida_dev_open (struct inode * inode,
struct file * file)
{
if (atomic_cmpxchg (&frida_client_count, 0, 1) != 0)
return -EBUSY;
return nonseekable_open (inode, file);
}
static int
frida_dev_release (struct inode * inode,
struct file * file)
{
mutex_lock (&frida_link_mutex);
kfifo_reset (&frida_to_client);
kfifo_reset (&frida_from_client);
mutex_unlock (&frida_link_mutex);
atomic_set (&frida_client_count, 0);
return 0;
}
static ssize_t
frida_dev_read (struct file * file,
char __user * buffer,
size_t size,
loff_t * offset)
{
unsigned int copied;
int res;
while (kfifo_is_empty (&frida_to_client))
{
if ((file->f_flags & O_NONBLOCK) != 0)
return -EAGAIN;
if (wait_event_interruptible (frida_readable_wq, !kfifo_is_empty (&frida_to_client)) != 0)
return -ERESTARTSYS;
}
mutex_lock (&frida_link_mutex);
res = kfifo_to_user (&frida_to_client, buffer, size, &copied);
mutex_unlock (&frida_link_mutex);
return (res != 0) ? res : copied;
}
static ssize_t
frida_dev_write (struct file * file,
const char __user * buffer,
size_t size,
loff_t * offset)
{
unsigned int copied;
int res;
mutex_lock (&frida_link_mutex);
res = kfifo_from_user (&frida_from_client, buffer, size, &copied);
mutex_unlock (&frida_link_mutex);
if (res != 0)
return res;
frida_kmod_wake ();
return copied;
}
static __poll_t
frida_dev_poll (struct file * file,
struct poll_table_struct * wait)
{
__poll_t events = EPOLLOUT | EPOLLWRNORM;
poll_wait (file, &frida_readable_wq, wait);
if (!kfifo_is_empty (&frida_to_client))
events |= EPOLLIN | EPOLLRDNORM;
return events;
}
/*
* Two-phase wait: the caller reads the sequence number, checks its condition, and
* only then commits to sleeping. A wakeup that lands in between bumps the sequence,
* so the commit returns immediately instead of sleeping through it.
*/
unsigned int
frida_kmod_wait_prepare (void)
{
return (unsigned int) atomic_read (&frida_wake_seq);
}
void
frida_kmod_wait_commit (unsigned int seq,
s64 timeout_us)
{
s64 capped_timeout_us;
long timeout;
/*
* Every wait is bounded and uninterruptible. Interruptible would be wrong twice
* over: a kernel thread with a signal pending gets it back immediately, turning
* each sleep into a no-op and the caller's loop into a spin, and an unbounded
* uninterruptible sleep would leave the thread unkillable. A caller asking to wait
* forever gets a long slice instead — anything with something to say bumps the
* sequence, so this costs latency to nobody.
*/
capped_timeout_us = (timeout_us < 0)
? FRIDA_WAIT_FOREVER_SLICE_US
: min_t (s64, timeout_us, FRIDA_WAIT_FOREVER_SLICE_US);
timeout = max_t (long, (capped_timeout_us * HZ) / USEC_PER_SEC, 1);
wait_event_timeout (frida_wq, frida_should_wake (seq), timeout);
}
/*
* Queued input has to be level-triggered, not just the edge that delivered it. The
* agent drains the fifo before arming its wait, so a write landing in between would
* bump the sequence that the arming then reads as its baseline — and the agent would
* sleep with a frame already waiting.
*/
static bool
frida_should_wake (unsigned int seq)
{
if ((unsigned int) atomic_read (&frida_wake_seq) != seq)
return true;
return !kfifo_is_empty (&frida_from_client);
}
void
frida_kmod_wake (void)
{
atomic_inc (&frida_wake_seq);
wake_up_all (&frida_wq);
}
/*
* A kernel thread that loops without reaching the scheduler takes the machine down
* via the hardware watchdog rather than merely running hot, which makes any such loop
* undiagnosable. Every loop the agent runs calls this.
*/
void
frida_kmod_yield (void)
{
cond_resched ();
}
s64
frida_kmod_monotonic_micros (void)
{
return ktime_to_us (ktime_get ());
}
void
frida_kmod_wall_clock_micros (unsigned int * secs,
unsigned int * micros)
{
struct timespec64 ts;
ktime_get_real_ts64 (&ts);
*secs = (unsigned int) ts.tv_sec;
*micros = (unsigned int) (ts.tv_nsec / 1000);
}
/* Handed to JavaScript as a GumThreadId, so it has to fit in the 48 bits a
* double represents exactly. */
u64
frida_kmod_current_thread_id (void)
{
return ((u64) (uintptr_t) current) & ((1ULL << 48) - 1);
}
int
frida_kmod_protect (u64 address,
size_t size,
unsigned int gum_prot)
{
unsigned long start = address & PAGE_MASK;
unsigned long end = PAGE_ALIGN (address + size);
int n_pages = (end - start) / PAGE_SIZE;
if (frida_set_memory_ro_impl == NULL)
return 0;
if ((gum_prot & FRIDA_PAGE_WRITE) != 0)
{
if (frida_set_memory_nx_impl (start, n_pages) != 0)
return 0;
return frida_set_memory_rw_impl (start, n_pages) == 0;
}
if (frida_set_memory_ro_impl (start, n_pages) != 0)
return 0;
if ((gum_prot & FRIDA_PAGE_EXECUTE) != 0)
return frida_set_memory_x_impl (start, n_pages) == 0;
return 1;
}
/*
* A second mapping of the same physical pages, writable regardless of what the
* primary mapping allows. This is how the agent lands hooks in text that
* STRICT_KERNEL_RWX has made read-only.
*/
void *
frida_kmod_remap_writable (u64 first_page,
unsigned int n_pages)
{
struct page ** pages;
unsigned int i;
void * mapping;
struct frida_writable_alias * alias;
pages = kmalloc_array (n_pages, sizeof (struct page *), GFP_KERNEL);
for (i = 0; i != n_pages; i++)
pages[i] = frida_page_for_virtual (first_page + ((unsigned long) i * PAGE_SIZE));
mapping = vmap (pages, n_pages, VM_MAP, PAGE_KERNEL);
kfree (pages);
if (mapping == NULL)
return NULL;
alias = kmalloc (sizeof (struct frida_writable_alias), GFP_KERNEL);
alias->mapping = mapping;
alias->first_page = first_page;
alias->n_pages = n_pages;
mutex_lock (&frida_writable_aliases_mutex);
list_add (&alias->node, &frida_writable_aliases);
mutex_unlock (&frida_writable_aliases_mutex);
return mapping;
}
void
frida_kmod_unmap_writable (void * mapping)
{
struct frida_writable_alias * alias = NULL, * candidate;
unsigned long mapped_size;
mutex_lock (&frida_writable_aliases_mutex);
list_for_each_entry (candidate, &frida_writable_aliases, node)
{
if (candidate->mapping == mapping)
{
alias = candidate;
list_del (&alias->node);
break;
}
}
mutex_unlock (&frida_writable_aliases_mutex);
mapped_size = (unsigned long) alias->n_pages * PAGE_SIZE;
vunmap (mapping);
/* flush_icache_range() inlines a reference to __icache_flags, which GKI does not
* export; these two are what it would have called. */
caches_clean_inval_pou (alias->first_page, alias->first_page + mapped_size);
kick_all_cpus_sync ();
kfree (alias);
}
static struct page *
frida_page_for_virtual (unsigned long address)
{
if (frida_is_vmalloc_or_module_addr ((void *) address))
return vmalloc_to_page ((void *) address);
if (virt_addr_valid ((void *) address))
return virt_to_page ((void *) address);
/*
* Kernel text and rodata live in the image mapping, which is neither the linear
* map nor vmalloc space. __pa_symbol() is the sanctioned way out of there.
*/
return pfn_to_page (__phys_to_pfn (__pa_symbol (address)));
}
/* The kernel's own is_vmalloc_or_module_addr() is not exported, and on arm64 the
* module region sits below the vmalloc range rather than inside it, so testing for
* vmalloc alone would miss it. */
static bool
frida_is_vmalloc_or_module_addr (const void * x)
{
#if defined (CONFIG_MODULES) && defined (MODULES_VADDR)
unsigned long address = (unsigned long) kasan_reset_tag (x);
if (address >= MODULES_VADDR && address < MODULES_END)
return true;
#endif
return is_vmalloc_addr (x);
}
u64
frida_kmod_kernel_base (void)
{
return frida_kernel_base;
}
u64
frida_kmod_kernel_size (void)
{
return frida_kernel_size;
}
/*
* Neither the module list nor the mutex guarding it is exported, so both come
* from kallsyms. Without them we report nothing rather than walk unsynchronised.
*/
void __nocfi
frida_kmod_enumerate_modules (FridaFoundModuleFunc func,
void * user_data)
{
struct module * mod;
if (frida_modules == NULL || frida_module_mutex == NULL)
return;
mutex_lock (frida_module_mutex);
list_for_each_entry (mod, frida_modules, list)
{
u64 base, size;
if (mod->state != MODULE_STATE_LIVE)
continue;
#if LINUX_VERSION_CODE >= KERNEL_VERSION (6, 4, 0)
base = (u64) (uintptr_t) mod->mem[MOD_TEXT].base;
size = mod->mem[MOD_TEXT].size;
#else
base = (u64) (uintptr_t) mod->core_layout.base;
size = mod->core_layout.size;
#endif
if (!func (mod->name, "", base, size, user_data))
break;
}
mutex_unlock (frida_module_mutex);
}
u64
frida_kmod_find_symbol (const char * name)
{
if (frida_kallsyms_lookup_name_impl == NULL)
return 0;
return frida_kallsyms_lookup_name_impl (name);
}
/*
* sprint_symbol_no_offset() is the only exported reverse lookup. It renders
* addresses it cannot name as a hex string, which then fails to resolve back to
* an address, so no separate check for that case is needed.
*/
int
frida_kmod_symbol_name_from_address (u64 address,
char * buffer,
size_t size,
u64 * symbol_address)
{
char rendered[KSYM_SYMBOL_LEN];
u64 base;
sprint_symbol_no_offset (rendered, (unsigned long) address);
frida_strip_module_suffix (rendered);
base = frida_kmod_find_symbol (rendered);
if (base == 0)
return 0;
strscpy (buffer, rendered, size);
*symbol_address = base;
return 1;
}
static void
frida_strip_module_suffix (char * rendered)
{
char * suffix = strchr (rendered, ' ');
if (suffix != NULL)
*suffix = '\0';
}
int
frida_kmod_enumerate_symbols (FridaFoundSymbolFunc func,
void * user_data)
{
struct frida_symbol_visit_ctx ctx = { func, user_data };
if (frida_kallsyms_on_each_symbol_impl == NULL)
return 0;
frida_kallsyms_on_each_symbol_impl (frida_on_each_symbol, &ctx);
return 1;
}
/* kallsyms_on_each_symbol() stops when the callback returns non-zero. __nocfi
* because ctx->func points into the agent, which is not CFI-instrumented. */
static int __nocfi
frida_on_each_symbol (void * data,
const char * name,
#if LINUX_VERSION_CODE < KERNEL_VERSION (6, 4, 0)
struct module * mod,
#endif
unsigned long address)
{
struct frida_symbol_visit_ctx * ctx = data;
return ctx->func (name, address, ctx->user_data) ? 0 : 1;
}
+9 -7
View File
@@ -1,7 +1,7 @@
use core::ffi::c_void;
use crate::bindings::{gpointer, gsize, gum_try_mprotect, GumPageProtection};
use crate::{kernel, libc};
use crate::bindings::{gpointer, gsize, GumPageProtection};
use crate::{gum, libc, xnu};
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
// leaves the parameter table writable.
// over the hostlink 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 = kernel::alloc_code(map_size * 2);
let base = xnu::kalloc(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_try_mprotect(base as gpointer, map_size as gsize, GUM_PAGE_READ | GUM_PAGE_EXECUTE);
gum::gum_try_mprotect(base as gpointer, map_size as gsize, GUM_PAGE_READ | GUM_PAGE_EXECUTE);
if made_executable == 0 {
kernel::free_code(base, map_size * 2);
xnu::free(base, map_size * 2);
return 0;
}
@@ -46,5 +46,7 @@ pub unsafe extern "C" fn ffi_tramp_embedder_unmap(
_parm_table: *mut c_void,
map_size: usize,
) {
kernel::free_code(code_table as *mut u8, map_size * 2);
unsafe {
xnu::free(code_table as *mut u8, map_size * 2);
}
}
+22 -19
View File
@@ -1,7 +1,7 @@
use core::ptr;
use crate::bindings::{g_wait_is_set, gint64, gpointer};
use crate::kernel;
use crate::xnu;
const G_WAIT_INFINITE: gint64 = -1;
@@ -9,35 +9,38 @@ pub static mut WAKEUP_TOKEN: u64 = 0;
#[unsafe(no_mangle)]
pub extern "C" fn g_get_monotonic_time() -> gint64 {
kernel::monotonic_micros()
let abstime = xnu::mach_absolute_time();
let time = xnu::absolutetime_to_nanoseconds(abstime);
(time / 1000) as gint64
}
#[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 timeout = if timeout_us == G_WAIT_INFINITE {
None
let wait_result = if timeout_us == G_WAIT_INFINITE {
xnu::assert_wait(wait_event, xnu::THREAD_INTERRUPTIBLE)
} else {
Some(timeout_us as u64)
xnu::assert_wait_timeout(
wait_event,
xnu::THREAD_INTERRUPTIBLE,
(timeout_us * 1000) as u32,
1, // TODO
)
};
if wait_result != xnu::THREAD_WAITING {
panic!("assert_wait_timeout failed: {}", wait_result);
}
kernel::wait(wait_event, timeout, &mut || unsafe {
g_wait_is_set(token) != 0
});
if unsafe { g_wait_is_set(token) != 0 } {
xnu::thread_wakeup(wait_event);
return;
}
xnu::thread_block(None);
}
#[unsafe(no_mangle)]
pub extern "C" fn g_wait_wake(_token: gpointer) {
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
xnu::thread_wakeup(ptr::addr_of_mut!(WAKEUP_TOKEN) as *const u8);
}
+70 -94
View File
@@ -8,6 +8,7 @@ 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};
@@ -32,11 +33,9 @@ struct RWLockImpl {
_padding: [u8; core::mem::size_of::<GRWLock>() - core::mem::size_of::<AtomicU32>()],
}
// 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.
#[repr(C, align(8))]
struct CondImpl {
generation: AtomicU32,
signal: AtomicU32, // Signal state
_padding: [u8; core::mem::size_of::<GCond>() - core::mem::size_of::<AtomicU32>()],
}
@@ -145,9 +144,10 @@ 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_) {
if rec_mutex_try_acquire(impl_, current_thread) {
break;
}
rec_mutex_wait(impl_);
@@ -159,8 +159,9 @@ 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_) {
if rec_mutex_try_acquire(impl_, current_thread) {
1
} else {
0
@@ -182,7 +183,7 @@ pub extern "C" fn g_rec_mutex_unlock(rec_mutex: *mut GRecMutex) {
.is_ok()
{
impl_.owner.store(0, Ordering::Relaxed);
wake_waiters(&impl_.count);
lock_notify_all();
break;
}
} else {
@@ -197,10 +198,7 @@ pub extern "C" fn g_rec_mutex_unlock(rec_mutex: *mut GRecMutex) {
}
}
// 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 {
unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl, current_thread: u64) -> bool {
let current_count = impl_.count.load(Ordering::Relaxed);
if current_count == 0 {
@@ -208,12 +206,12 @@ unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl) -> bool {
.compare_exchange_weak(0, 1, Ordering::Acquire, Ordering::Relaxed)
.is_ok()
{
impl_.owner.store(crate::kernel::current_thread_id(), Ordering::Relaxed);
impl_.owner.store(current_thread, Ordering::Relaxed);
return true;
}
} else {
let current_owner = impl_.owner.load(Ordering::Relaxed);
if current_owner == crate::kernel::current_thread_id() {
if current_owner == current_thread {
if impl_.count
.compare_exchange_weak(
current_count,
@@ -232,7 +230,7 @@ unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl) -> bool {
unsafe fn rec_mutex_wait(impl_: &mut RecMutexImpl) {
while impl_.count.load(Ordering::Relaxed) != 0 {
wait_on(&impl_.count, &mut || impl_.count.load(Ordering::Relaxed) == 0);
unsafe { lock_wait_primitive() };
}
}
@@ -284,7 +282,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);
wake_waiters(&impl_.state);
lock_notify_all();
}
}
@@ -331,7 +329,7 @@ pub extern "C" fn g_rw_lock_reader_unlock(rw_lock: *mut GRWLock) {
.is_ok()
{
if current - 1 == 0 {
wake_waiters(&impl_.state);
lock_notify_all();
}
break;
}
@@ -357,15 +355,13 @@ 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 {
wait_on(&impl_.state, &mut || {
impl_.state.load(Ordering::Relaxed) & RW_WRITER_LOCK_BIT == 0
});
unsafe { lock_wait_primitive() };
}
}
unsafe fn rw_lock_wait_for_all(impl_: &mut RWLockImpl) {
while impl_.state.load(Ordering::Relaxed) != 0 {
wait_on(&impl_.state, &mut || impl_.state.load(Ordering::Relaxed) == 0);
unsafe { lock_wait_primitive() };
}
}
@@ -373,7 +369,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_.generation.store(0, Ordering::Relaxed);
impl_.signal.store(0, Ordering::Relaxed);
}
}
@@ -383,17 +379,11 @@ 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_.generation.load(Ordering::Acquire) == seen {
wait_on(&impl_.generation, &mut || {
impl_.generation.load(Ordering::Acquire) != seen
});
while impl_.signal.load(Ordering::Acquire) == 0 {
lock_wait_primitive();
}
g_mutex_lock(mutex);
@@ -404,8 +394,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_.generation.fetch_add(1, Ordering::Release);
wake_waiters(&impl_.generation);
impl_.signal.store(1, Ordering::Release);
lock_notify_all();
}
}
@@ -413,37 +403,22 @@ 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_.generation.fetch_add(1, Ordering::Release);
wake_waiters(&impl_.generation);
impl_.signal.store(1, Ordering::Release);
lock_notify_all();
}
}
#[unsafe(no_mangle)]
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 }
}
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
}
#[unsafe(no_mangle)]
pub extern "C" fn g_private_get(key: *mut GPrivate) -> *mut c_void {
unsafe {
let thread_id = crate::kernel::current_thread_id();
let thread_id = get_current_thread_id();
lock_acquire(&TLS_LOCK);
ensure_tls_storage();
@@ -463,7 +438,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 = crate::kernel::current_thread_id();
let thread_id = get_current_thread_id();
lock_acquire(&TLS_LOCK);
ensure_tls_storage();
@@ -493,9 +468,7 @@ pub extern "C" fn _g_system_thread_create(
data: gpointer,
) -> *mut GSystemThread {
unsafe {
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;
let system_thread = crate::xnu::kalloc(core::mem::size_of::<SystemThreadImpl>()) as *mut SystemThreadImpl;
g_mutex_init(&mut (*system_thread).mutex);
g_cond_init(&mut (*system_thread).cond);
@@ -505,12 +478,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::kernel::alloc(core::mem::size_of::<ThreadWrapperData>()) as *mut ThreadWrapperData;
let wrapper_data = crate::xnu::kalloc(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::kernel::spawn_thread(thread_wrapper, wrapper_data as *mut c_void);
let _xnu_result = crate::xnu::kernel_thread_start(thread_wrapper, wrapper_data as *mut c_void);
system_thread as *mut GSystemThread
}
@@ -523,9 +496,8 @@ 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 = crate::kernel::current_thread_id();
(*system_thread).thread_id.store(current_thread_id, Ordering::Release);
wake_waiters(&(*system_thread).thread_id);
let current_thread_id = get_current_thread_id();
(*system_thread).thread_id.store(current_thread_id, Ordering::Relaxed);
func.unwrap()(data);
@@ -565,10 +537,12 @@ pub extern "C" fn _g_system_thread_wait(thread: *mut GSystemThread) {
unsafe {
let system_thread = thread as *mut SystemThreadImpl;
while (*system_thread).thread_id.load(Ordering::Acquire) == 0 {
wait_on(&(*system_thread).thread_id, &mut || {
(*system_thread).thread_id.load(Ordering::Acquire) != 0
});
loop {
let thread_id = (*system_thread).thread_id.load(Ordering::Acquire);
if thread_id != 0 {
break;
}
core::hint::spin_loop();
}
g_mutex_lock(&mut (*system_thread).mutex);
@@ -582,16 +556,19 @@ pub extern "C" fn _g_system_thread_wait(thread: *mut GSystemThread) {
}
unsafe fn cleanup_system_thread(system_thread: *mut SystemThreadImpl) {
crate::kernel::free(system_thread as *mut u8, core::mem::size_of::<SystemThreadImpl>());
crate::xnu::free(system_thread as *mut u8, core::mem::size_of::<SystemThreadImpl>());
}
unsafe fn cleanup_wrapper_data(wrapper_data: *mut ThreadWrapperData) {
crate::kernel::free(wrapper_data as *mut u8, core::mem::size_of::<ThreadWrapperData>());
crate::xnu::free(wrapper_data as *mut u8, core::mem::size_of::<ThreadWrapperData>());
}
unsafe fn lock_acquire(lock: &AtomicU32) {
while !unsafe { lock_try_acquire(lock) } {
wait_on(lock, &mut || lock.load(Ordering::Relaxed) == 0);
loop {
if unsafe { lock_try_acquire(lock) } {
break;
}
unsafe { lock_wait(lock) };
}
}
@@ -602,36 +579,35 @@ unsafe fn lock_try_acquire(lock: &AtomicU32) -> bool {
unsafe fn lock_release(lock: &AtomicU32) {
lock.store(0, Ordering::Release);
wake_waiters(lock);
unsafe { lock_notify_all() };
}
// 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(lock: &AtomicU32) {
while lock.load(Ordering::Relaxed) != 0 {
unsafe { lock_wait_primitive() };
}
}
fn wake_waiters<T>(word: &T) {
crate::kernel::wake(token_of(word));
unsafe fn lock_wait_primitive() {
unsafe {
asm!("wfe", options(nomem, nostack));
}
}
fn token_of<T>(word: &T) -> *const u8 {
word as *const T as *const u8
unsafe fn lock_notify_all() {
unsafe {
asm!("sev", options(nomem, nostack));
}
}
const WAIT_SLICE_US: u64 = 1000;
// 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
}
+413 -76
View File
@@ -1,31 +1,85 @@
// 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, 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,
_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,
},
gthread, kernel, libc,
gthread, host_rpc, libc, FridaCommand,
};
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};
#[cfg(feature = "linux")]
use crate::gum_linux::enumerate_exports_in_range;
#[cfg(feature = "xnu")]
use crate::gum_xnu::enumerate_exports_in_range;
// 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
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_process_get_current_thread_id() -> GumThreadId {
kernel::current_thread_id()
unsafe { gthread::get_current_thread_id() }
}
#[unsafe(no_mangle)]
@@ -45,6 +99,200 @@ 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 {
@@ -76,6 +324,40 @@ 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,
@@ -123,7 +405,7 @@ fn gum_native_module_get_type() -> GType {
interface_data: core::ptr::null_mut(),
};
g_type_add_interface_static(new_type, crate::bindings::gum_module_get_type(), &interface_info);
g_type_add_interface_static(new_type, 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,
@@ -166,11 +448,7 @@ unsafe extern "C" fn gum_native_module_finalize(object: *mut GObject) {
}
}
pub(crate) fn gum_native_module_new(
path: &str,
version: &str,
range: &GumMemoryRange,
) -> *mut GumModule {
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();
@@ -221,75 +499,134 @@ unsafe extern "C" fn gum_native_module_enumerate_exports(
) {
unsafe {
let module = self_ as *mut GumNativeModule;
let range = (*module).range;
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 mut on_export = |name: *const gchar, address: u64| {
let export_details = GumExportDetails {
type_: GumExportType_GUM_EXPORT_FUNCTION,
name: g_strdup(name),
address,
type_: export_type,
name: g_strdup(symbol_ref.name_ptr()),
address: symbol_ref.address(),
size: -1,
};
func.unwrap()(&export_details as *const GumExportDetails, user_data) != 0
};
enumerate_exports_in_range(
range.base_address,
range.base_address + range.size,
&mut on_export,
);
let should_continue =
func.unwrap()(&export_details as *const GumExportDetails, user_data);
if should_continue == 0 {
break;
}
}
}
}
/// 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(no_mangle)]
pub extern "C" fn gum_symbol_details_from_address(
address: gpointer,
details: *mut GumDebugSymbolDetails,
) -> gboolean {
unsafe {
(*core::ptr::addr_of_mut!(SLABS)).push((start, start + size as u64));
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
}
slab_unlock();
}
pub(crate) fn unregister_slab(start: u64) {
slab_lock();
#[unsafe(no_mangle)]
pub extern "C" fn gum_symbol_name_from_address(address: gpointer) -> *mut gchar {
unsafe {
(*core::ptr::addr_of_mut!(SLABS)).retain(|&(begin, _)| begin != start);
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)
}
slab_unlock();
}
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
#[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
}
-226
View File
@@ -1,226 +0,0 @@
// 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_symbol(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
}
-379
View File
@@ -1,379 +0,0 @@
// 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
}
-114
View File
@@ -1,114 +0,0 @@
// 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;
}
+10 -10
View File
@@ -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::{kernel, xnu};
use crate::xnu;
const MMIO_SIZE: u64 = 0x200;
@@ -107,14 +107,14 @@ struct DmaPage {
fn dma_page_alloc() -> DmaPage {
let len = PAGE_SIZE.load(Ordering::Relaxed);
let va = kernel::alloc(len);
let va = xnu::kalloc(len);
let pa = xnu::ml_vtophys(va as u64);
DmaPage { va, pa }
}
fn dma_page_free(p: DmaPage) {
let len = PAGE_SIZE.load(Ordering::Relaxed);
kernel::free(p.va, len);
xnu::free(p.va, len);
}
struct Vq {
@@ -359,7 +359,7 @@ impl Hostlink {
let s = unsafe { &mut *self.state.get() };
let total = 4 + payload.len();
let buf = kernel::alloc(total);
let buf = xnu::kalloc(total);
unsafe {
let len = payload.len() as u32;
*buf.add(0) = (len & 0xFF) as u8;
@@ -370,7 +370,7 @@ impl Hostlink {
}
let frame: &'static [u8] = unsafe { core::slice::from_raw_parts(buf, total) };
let node = kernel::alloc(size_of::<TxNode>()) as *mut TxNode;
let node = xnu::kalloc(size_of::<TxNode>()) as *mut TxNode;
unsafe {
(*node).next = core::ptr::null_mut();
(*node).frame = frame;
@@ -386,7 +386,7 @@ impl Hostlink {
s.tx_tail = node;
}
kernel::wake(s.wake_token);
xnu::thread_wakeup(s.wake_token);
}
pub fn process(&self) {
@@ -592,7 +592,7 @@ impl Hostlink {
| ((s.rx_lenbuf[3] as usize) << 24);
if s.rx_buf.is_none() && len > 0 {
let buf = kernel::alloc(len);
let buf = xnu::kalloc(len);
let slice: &'static mut [u8] =
unsafe { core::slice::from_raw_parts_mut(buf, len) };
s.rx_buf = Some(slice);
@@ -718,8 +718,8 @@ impl Hostlink {
self.kick(sel);
}
kernel::free(frame.as_ptr() as *mut u8, frame.len());
kernel::free(node as *mut u8, core::mem::size_of::<TxNode>());
xnu::free(frame.as_ptr() as *mut u8, frame.len());
xnu::free(node as *mut u8, core::mem::size_of::<TxNode>());
}
}
@@ -732,7 +732,7 @@ impl Hostlink {
}
extern "C" fn isr_wake(token: *mut c_void, _refcon: *mut c_void, _nub: *mut c_void, _src: i32) {
kernel::wake(token as *const u8);
xnu::thread_wakeup(token as *const u8);
}
fn r32(mmio: *mut u8, off: usize) -> u32 {
+2 -2
View File
@@ -8,7 +8,7 @@ use core::sync::atomic::{AtomicU32, Ordering};
use alloc::vec::Vec;
use crate::kernel;
use crate::xnu;
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() {
kernel::wake(cookie as *const u8);
xnu::thread_wakeup(cookie as *const u8);
}
}
-17
View File
@@ -1,17 +0,0 @@
// 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 = "xnu")]
pub use crate::xnu::*;
use core::ffi::c_void;
pub type ThreadEntry = unsafe extern "C" fn(parameter: *mut c_void, wait_result: i32);
-997
View File
@@ -1,997 +0,0 @@
#![no_std]
#[cfg(not(any(feature = "xnu", feature = "linux")))]
compile_error!("pick a flavour: --features xnu or --features linux");
#[cfg(all(feature = "xnu", feature = "linux"))]
compile_error!("pick one flavour, not both");
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::sync::atomic::{AtomicU32, Ordering};
use bindings::{
GAsyncResult, GBytes, GError, GMainContext, GObject, GVariant, GumMemoryRange,
GumScript, GumScriptBackend, g_error_free, g_free, g_main_context_iteration,
g_main_context_push_thread_default, g_memdup2, g_object_unref, g_variant_check_format_string,
g_variant_get, g_variant_get_data, g_variant_get_size, g_variant_get_string,
g_variant_get_uint32, 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,
gum_script_backend_create_finish, gum_script_backend_get_scheduler,
gum_script_backend_obtain_qjs, gum_script_get_stalker, gum_script_load,
gum_script_load_finish, gum_script_post, gum_script_scheduler_disable_background_thread,
gum_script_scheduler_get_js_context, gum_script_set_message_handler, gum_script_unload,
gum_script_unload_finish, gum_stalker_exclude,
};
mod ffi;
mod glib;
mod gthread;
mod gum;
mod libc;
pub mod kernel;
#[cfg(feature = "linux")]
mod gum_linux;
#[cfg(feature = "linux")]
mod hostlink_chardev;
#[cfg(feature = "linux")]
mod linux;
#[cfg(feature = "xnu")]
mod gum_xnu;
#[cfg(feature = "xnu")]
mod hostlink_virtio;
#[cfg(feature = "xnu")]
mod hostlink_vsock;
#[cfg(feature = "xnu")]
mod pac;
#[cfg(feature = "xnu")]
mod symbols;
#[cfg(feature = "xnu")]
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: unsafe { g_variant_new(c"(bv)".as_ptr(), 1, variant) },
}
}
fn success_empty() -> Self {
Self::success(unsafe { g_variant_new_tuple(ptr::null(), 0) })
}
fn error(message: &str) -> Self {
let mut c_message = String::from(message);
c_message.push('\0');
Self {
variant: unsafe {
g_variant_new(
c"(bv)".as_ptr(),
0,
g_variant_new_string(c_message.as_ptr()),
)
},
}
}
}
#[cfg(feature = "xnu")]
mod entrypoint_xnu {
use super::*;
use crate::symbols::SymbolTable;
static mut CONFIG_DATA: &'static [u8] = &[];
pub static mut MODULE_INFO: Vec<ModuleInfo> = Vec::new();
pub static mut SYMBOL_TABLE: SymbolTable = SymbolTable::empty();
#[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 {
kernel::log("frida: _start entry\n\0");
CONFIG_DATA = core::slice::from_raw_parts(config_data, config_size);
kernel::log("frida: _start config stored\n\0");
let _r = kernel::spawn_thread(worker, 12345usize as *mut 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 worker(_parameter: *mut c_void, _wait_result: i32) {
unsafe {
kernel::log("frida: worker entry\n\0");
init_gum();
let (transport_config, kernel_base, module_info, symbol_table, own_range) =
parse_config(core::ptr::addr_of!(CONFIG_DATA).read());
kernel::set_kernel_base(kernel_base);
MODULE_INFO = module_info;
SYMBOL_TABLE = symbol_table;
OWN_RANGE = own_range;
kernel::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);
kernel::log("frida: transport up, entering main loop\n\0");
run_main_loop(adopt_js_context());
}
}
unsafe fn parse_config(
config: &[u8],
) -> (
TransportConfig,
u64,
Vec<ModuleInfo>,
SymbolTable,
GumMemoryRange,
) {
use crate::bindings::{
GVariantIter, g_variant_get_byte, g_variant_get_child_value, g_variant_get_uint64,
g_variant_get_variant, g_variant_iter_init, g_variant_iter_next,
};
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 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);
kernel::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 = ptr::null_mut();
let mut raw_version: *mut gchar = ptr::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,
)
}
}
}
#[cfg(feature = "linux")]
mod entrypoint_linux {
use super::*;
use core::ffi::c_int;
use core::sync::atomic::AtomicBool;
static STOP_REQUESTED: AtomicBool = AtomicBool::new(false);
static WORKER_EXITED: AtomicBool = AtomicBool::new(false);
/// Called from the module's init path. Everything interesting happens on our
/// own kernel thread: `insmod` runs with the module mutex held, and bringing
/// up Gum plus connecting to the host both block.
#[unsafe(no_mangle)]
pub extern "C" fn frida_agent_start() -> c_int {
kernel::log("frida: agent starting\n\0");
if kernel::spawn_thread(worker, ptr::null_mut()) != 0 {
kernel::log("frida: failed to spawn worker\n\0");
return -1;
}
0
}
/// Called from the module's exit path, which must not return until the worker
/// is off our text — the module's pages go away right after.
#[unsafe(no_mangle)]
pub extern "C" fn frida_agent_stop() {
kernel::log("frida: agent stopping\n\0");
STOP_REQUESTED.store(true, Ordering::Release);
while !WORKER_EXITED.load(Ordering::Acquire) {
kernel::wake(ptr::addr_of!(glib::WAKEUP_TOKEN) as *const u8);
kernel::wait(
ptr::addr_of!(glib::WAKEUP_TOKEN) as *const u8,
Some(10_000),
&mut || WORKER_EXITED.load(Ordering::Acquire),
);
}
kernel::log("frida: agent stopped\n\0");
}
pub fn stop_requested() -> bool {
STOP_REQUESTED.load(Ordering::Acquire)
}
unsafe extern "C" fn worker(_parameter: *mut c_void, _wait_result: i32) {
unsafe {
kernel::log("frida: worker entry\n\0");
init_gum();
let (own_base, own_size) = kernel::own_range();
OWN_RANGE = GumMemoryRange {
base_address: own_base,
size: own_size as gsize,
};
match hostlink_chardev::Hostlink::init(Some(on_frame_from_host)) {
Ok(hostlink) => transport_set(Transport::CharDevice(hostlink)),
Err(_) => {
kernel::log("frida: failed to connect to peer\n\0");
WORKER_EXITED.store(true, Ordering::Release);
return;
}
}
kernel::log("frida: transport up, entering main loop\n\0");
let main_context = adopt_js_context();
run_main_loop(main_context);
destroy_all_scripts(main_context);
transport_teardown();
WORKER_EXITED.store(true, Ordering::Release);
kernel::wake(ptr::addr_of!(glib::WAKEUP_TOKEN) as *const u8);
}
}
}
#[cfg(feature = "linux")]
pub use entrypoint_linux::{frida_agent_start, frida_agent_stop};
#[cfg(feature = "xnu")]
pub use entrypoint_xnu::{MODULE_INFO, ModuleInfo, SYMBOL_TABLE, _start};
pub enum Transport {
#[cfg(feature = "xnu")]
Virtio(hostlink_virtio::Hostlink),
#[cfg(feature = "xnu")]
Vsock(hostlink_vsock::Hostlink),
#[cfg(feature = "linux")]
CharDevice(hostlink_chardev::Hostlink),
}
impl Transport {
pub fn send(&self, payload: &[u8]) {
match self {
#[cfg(feature = "xnu")]
Transport::Virtio(h) => h.send(payload),
#[cfg(feature = "xnu")]
Transport::Vsock(h) => h.send(payload),
#[cfg(feature = "linux")]
Transport::CharDevice(h) => h.send(payload),
}
}
pub fn process(&self) {
match self {
#[cfg(feature = "xnu")]
Transport::Virtio(h) => h.process(),
#[cfg(feature = "xnu")]
Transport::Vsock(h) => h.process(),
#[cfg(feature = "linux")]
Transport::CharDevice(h) => h.process(),
}
}
}
#[cfg(feature = "xnu")]
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
}
}
// Only the XNU backend has to cope with Gum asking for memory work before the
// hostlink that performs it exists.
#[cfg(feature = "xnu")]
#[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();
unsafe fn init_gum() {
unsafe {
bindings::g_set_panic_handler(Some(frida_panic_handler), ptr::null_mut());
bindings::gum_init_embedded();
kernel::log("frida: gum_init_embedded done\n\0");
bindings::g_log_set_default_handler(Some(frida_log_handler), ptr::null_mut());
gum_script_scheduler_disable_background_thread(gum_script_backend_get_scheduler());
}
}
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 adopt_js_context() -> *mut GMainContext {
unsafe {
let context = gum_script_scheduler_get_js_context(gum_script_backend_get_scheduler());
// Acquires the context as well, which is what lets this thread run the jobs the
// asynchronous script calls queue on it. Being the thread default is what makes
// a script created here deliver its messages here too.
g_main_context_push_thread_default(context);
context
}
}
const IDLE_SLICE_US: u64 = 50_000;
fn run_main_loop(main_context: *mut GMainContext) {
unsafe {
loop {
transport_get_unchecked().process();
#[cfg(feature = "linux")]
if entrypoint_linux::stop_requested() {
return;
}
g_main_context_iteration(main_context, 0);
kernel::wait(
ptr::addr_of!(glib::WAKEUP_TOKEN) as *const u8,
Some(IDLE_SLICE_US),
&mut || false,
);
kernel::yield_now();
}
}
}
#[cfg(feature = "linux")]
fn destroy_all_scripts(main_context: *mut GMainContext) {
unsafe {
let scripts = core::mem::take(core::ptr::addr_of_mut!(SCRIPTS).as_mut().unwrap());
for (_, script) in scripts {
UNLOADS_IN_FLIGHT.fetch_add(1, Ordering::Relaxed);
gum_script_unload(script, ptr::null_mut(), Some(on_script_unloaded), ptr::null_mut());
}
while UNLOADS_IN_FLIGHT.load(Ordering::Relaxed) != 0 {
g_main_context_iteration(main_context, 0);
kernel::yield_now();
}
}
}
#[cfg(feature = "linux")]
unsafe extern "C" fn on_script_unloaded(
source_object: *mut GObject,
result: *mut GAsyncResult,
_user_data: gpointer,
) {
unsafe {
let script = source_object as *mut GumScript;
gum_script_unload_finish(script, result);
g_object_unref(script as *mut c_void);
UNLOADS_IN_FLIGHT.fetch_sub(1, Ordering::Relaxed);
}
}
#[cfg(feature = "linux")]
static UNLOADS_IN_FLIGHT: AtomicU32 = AtomicU32::new(0);
#[cfg(feature = "linux")]
fn transport_teardown() {
unsafe {
let driver = TRANSPORT_DRIVER;
TRANSPORT_DRIVER = ptr::null_mut();
drop(Box::from_raw(driver));
}
}
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, request_id),
FridaCommand::LoadScript => handle_load_script(payload_variant, request_id),
FridaCommand::DestroyScript => handle_destroy_script(payload_variant, request_id),
FridaCommand::PostScriptMessage => Some(handle_post_script_message(payload_variant)),
_ => Some(HandlerResponse::error("Unknown command")),
};
if let Some(response) = response {
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 {
let mut reply: Option<*mut GVariant> = None;
kernel::wait(wait_event, None, &mut || {
transport.process();
reply = take_pending_reply(request_id);
reply.is_some()
});
if let Some(reply) = reply {
return reply;
}
}
}
}
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, request_id: u16) -> Option<HandlerResponse> {
unsafe {
if g_variant_check_format_string(payload_variant, c"s".as_ptr(), 0) == 0 {
return Some(HandlerResponse::error("Invalid payload format: expected string"));
}
let source = g_variant_get_string(payload_variant, core::ptr::null_mut());
gum_script_backend_create(
gum_script_backend_obtain_qjs(),
c"agent.js".as_ptr(),
source,
ptr::null_mut(),
ptr::null_mut(),
Some(on_script_created),
Box::into_raw(Box::new(request_id)) as *mut c_void,
);
None
}
}
unsafe extern "C" fn on_script_created(
source_object: *mut GObject,
result: *mut GAsyncResult,
user_data: gpointer,
) {
unsafe {
let request_id = *Box::from_raw(user_data as *mut u16);
let mut error: *mut GError = ptr::null_mut();
let script = gum_script_backend_create_finish(
source_object as *mut GumScriptBackend,
result,
&mut error,
);
if !error.is_null() {
let message = String::from(CStr::from_ptr((*error).message).to_str().unwrap());
g_error_free(error);
send_command_reply(request_id, HandlerResponse::error(&message));
return;
}
exclude_own_range_from_stalker(script);
let script_id = NEXT_SCRIPT_ID.fetch_add(1, Ordering::Relaxed);
gum_script_set_message_handler(
script,
Some(frida_message_handler),
Box::into_raw(Box::new(script_id)) as *mut c_void,
None,
);
core::ptr::addr_of_mut!(SCRIPTS)
.as_mut()
.unwrap()
.insert(script_id, script);
send_command_reply(request_id, 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. On XNU the host injected the image, so it tells
// us our range as part of the config; on Linux we ask the module loader.
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, request_id: u16) -> Option<HandlerResponse> {
unsafe {
if g_variant_check_format_string(payload_variant, c"u".as_ptr(), 0) == 0 {
return Some(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 Some(HandlerResponse::error(&format!("Script with ID {} not found", script_id)));
};
gum_script_load(
script,
ptr::null_mut(),
Some(on_script_loaded),
Box::into_raw(Box::new(request_id)) as *mut c_void,
);
None
}
}
unsafe extern "C" fn on_script_loaded(
source_object: *mut GObject,
result: *mut GAsyncResult,
user_data: gpointer,
) {
unsafe {
let request_id = *Box::from_raw(user_data as *mut u16);
gum_script_load_finish(source_object as *mut GumScript, result);
send_command_reply(request_id, HandlerResponse::success_empty());
}
}
fn handle_destroy_script(payload_variant: *mut GVariant, request_id: u16) -> Option<HandlerResponse> {
unsafe {
if g_variant_check_format_string(payload_variant, c"u".as_ptr(), 0) == 0 {
return Some(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 Some(HandlerResponse::error(&format!("Script with ID {} not found", script_id)));
};
gum_script_unload(
script,
ptr::null_mut(),
Some(on_script_destroyed),
Box::into_raw(Box::new(request_id)) as *mut c_void,
);
None
}
}
unsafe extern "C" fn on_script_destroyed(
source_object: *mut GObject,
result: *mut GAsyncResult,
user_data: gpointer,
) {
unsafe {
let request_id = *Box::from_raw(user_data as *mut u16);
let script = source_object as *mut GumScript;
gum_script_unload_finish(script, result);
g_object_unref(script as *mut c_void);
send_command_reply(request_id, 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 c_void) {
let msg = unsafe { 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 c_void,
) {
let msg = unsafe { 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');
kernel::panic(s.as_str());
#[allow(unreachable_code)]
loop {}
}
pub struct KernelAllocator;
unsafe impl GlobalAlloc for KernelAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
kernel::alloc(layout.size())
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
kernel::free(ptr, layout.size());
}
}
#[global_allocator]
static GLOBAL: KernelAllocator = KernelAllocator;
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::kernel::log(&buf)
}};
}
+89 -539
View File
@@ -1,64 +1,35 @@
// 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;
// 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() };
static mut MALLOC_LOCK: GRecMutex = unsafe { core::mem::zeroed() };
fn libc_lock() {
gthread::g_rec_mutex_lock(ptr::addr_of_mut!(LIBC_LOCK));
}
fn libc_unlock() {
gthread::g_rec_mutex_unlock(ptr::addr_of_mut!(LIBC_LOCK));
offer_preemption_point();
}
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 sysconf(_name: i32) -> isize {
1
pub extern "C" fn __malloc_lock(_reent: *mut core::ffi::c_void) {
gthread::g_rec_mutex_lock(ptr::addr_of_mut!(MALLOC_LOCK));
}
#[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),
);
}
pub extern "C" fn __malloc_unlock(_reent: *mut core::ffi::c_void) {
gthread::g_rec_mutex_unlock(ptr::addr_of_mut!(MALLOC_LOCK));
}
fn sbrk_impl(incr: isize) -> *mut u8 {
#[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 {
const HEAP_SIZE: usize = 32 * 1024 * 1024; // 32 MB
static mut HEAP_START: *mut u8 = ptr::null_mut();
@@ -69,7 +40,7 @@ fn sbrk_impl(incr: isize) -> *mut u8 {
gthread::g_mutex_lock(ptr::addr_of_mut!(HEAP_MUTEX));
if HEAP_START.is_null() {
HEAP_START = crate::kernel::alloc(HEAP_SIZE);
HEAP_START = crate::xnu::kalloc(HEAP_SIZE);
if HEAP_START.is_null() {
gthread::g_mutex_unlock(ptr::addr_of_mut!(HEAP_MUTEX));
panic!("Failed to allocate 32 MB heap");
@@ -78,8 +49,7 @@ fn sbrk_impl(incr: isize) -> *mut u8 {
}
let prev_heap_current = HEAP_CURRENT;
// Signed: the allocator hands memory back by asking for a negative increment.
let new_heap_current = HEAP_CURRENT.offset(incr);
let new_heap_current = HEAP_CURRENT.add(incr as usize);
if new_heap_current > HEAP_START.add(HEAP_SIZE) {
gthread::g_mutex_unlock(ptr::addr_of_mut!(HEAP_MUTEX));
@@ -95,14 +65,20 @@ fn sbrk_impl(incr: isize) -> *mut u8 {
}
}
#[unsafe(no_mangle)]
pub extern "C" fn _getpid() -> i32 {
0
}
#[repr(C)]
struct Timeval {
tv_sec: i64,
tv_usec: i64,
}
fn gettimeofday_impl(tp: *mut core::ffi::c_void) -> i32 {
let (secs, microsecs) = crate::kernel::wall_clock_micros();
#[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();
let timeval = Timeval {
tv_sec: secs as i64,
tv_usec: microsecs as i64,
@@ -117,494 +93,68 @@ fn gettimeofday_impl(tp: *mut core::ffi::c_void) -> i32 {
0
}
fn write_impl(count: usize) -> isize {
#[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 {
count as isize
}
#[cfg(feature = "xnu")]
mod exports {
use super::*;
#[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] = [];
#[unsafe(no_mangle)]
pub extern "C" fn _lseek(_fd: i32, _offset: isize, _whence: i32) -> isize {
0
}
#[cfg(feature = "linux")]
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;
#[unsafe(no_mangle)]
pub extern "C" fn malloc(size: usize) -> *mut c_void {
let total = size + HEAP_HEADER_SIZE;
let block = crate::kernel::alloc(total);
if block.is_null() {
return ptr::null_mut();
}
offer_preemption_point();
unsafe { attach_header(block, total, block.add(HEAP_HEADER_SIZE)) }
}
#[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 _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 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());
}
-230
View File
@@ -1,230 +0,0 @@
// 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 spawn_thread(entry: ThreadEntry, parameter: *mut c_void) -> isize {
unsafe { frida_kmod_spawn_thread(entry, parameter) as isize }
}
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()) }
}
/// 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_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_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;
}
+691 -10
View File
@@ -1,12 +1,693 @@
// 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]
#![no_std]
use frida_barebone_agent as _;
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)
}};
}
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::bindings::{
GPatternSpec, g_pattern_spec_free, g_pattern_spec_match_string, g_pattern_spec_new,
};
use crate::kernel::get_kernel_base;
use crate::xnu::get_kernel_base;
use alloc::ffi::CString;
use core::cmp::Ordering;
use core::ffi::{CStr, c_char};
+51 -119
View File
@@ -1,83 +1,7 @@
use core::ffi::c_void;
use core::sync::atomic::{AtomicU64, Ordering};
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 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
}
static KERNEL_BASE: AtomicU64 = AtomicU64::new(0);
pub fn get_kernel_base() -> u64 {
KERNEL_BASE.load(Ordering::Relaxed)
@@ -87,7 +11,54 @@ pub fn set_kernel_base(base: u64) {
KERNEL_BASE.store(base, Ordering::Relaxed);
}
static KERNEL_BASE: AtomicU64 = AtomicU64::new(0);
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());
}
}
// Z_WAITOK (modern XNU): block until allocation succeeds.
const Z_WAITOK: u32 = 0x0200;
@@ -127,8 +98,8 @@ pub fn kernel_thread_start(continuation: ContinuationFn, thread_parameter: *mut
};
}
const THREAD_INTERRUPTIBLE: u32 = 1;
const THREAD_WAITING: i32 = -1;
pub const THREAD_INTERRUPTIBLE: u32 = 1;
pub const THREAD_WAITING: i32 = -1;
pub fn assert_wait(event: *const u8, interruptible: u32) -> i32 {
unsafe { _assert_wait(event, interruptible) }
@@ -297,42 +268,3 @@ 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;
}
+4 -35
View File
@@ -71,15 +71,6 @@ namespace Frida {
config = new Barebone.Config ();
}
Barebone.AgentConfig? resident_agent = config.agent;
if (resident_agent != null && resident_agent.transport is Barebone.DeviceTransportConfig) {
host_session = yield attach_to_resident_agent (
(Barebone.DeviceTransportConfig) resident_agent.transport, cancellable);
host_session.agent_session_detached.connect (on_agent_session_detached);
return host_session;
}
SocketConnectable connectable;
try {
Barebone.ConnectionConfig c = config.connection;
@@ -194,24 +185,6 @@ namespace Frida {
return host_session;
}
private async BareboneHostSession attach_to_resident_agent (Barebone.DeviceTransportConfig transport,
Cancellable? cancellable) throws Error, IOError {
#if WINDOWS
throw new Error.NOT_SUPPORTED ("Resident agents are not available on this OS");
#else
int fd = Posix.open (transport.path, Posix.O_RDWR);
if (fd == -1)
throw new Error.TRANSPORT ("Unable to open %s: %s", transport.path, Posix.strerror (Posix.errno));
// Both halves are the same descriptor, so exactly one of them may close it.
var 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
}
public async void destroy (HostSession session, Cancellable? cancellable) throws Error, IOError {
if (session != host_session)
throw new Error.INVALID_ARGUMENT ("Invalid host session");
@@ -263,8 +236,7 @@ namespace Frida {
construct;
}
/** Absent for a resident agent: every service here is built on a debugger stub. */
public Barebone.Services? services {
public Barebone.Services services {
get;
construct;
}
@@ -272,7 +244,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);
}
@@ -361,13 +333,10 @@ 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 {
if (services == null)
throw new Error.NOT_SUPPORTED ("Barebone target has neither an agent nor a debugger stub");
else
session = new LocalBareboneAgentSession (services, session_id, opts.persist_timeout, dbus_context);
}
agent_sessions[session_id] = session;
session.closed.connect (on_agent_session_closed);
+3 -22
View File
@@ -261,12 +261,12 @@ 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))
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,9 +281,6 @@ namespace Frida.Barebone {
case "vsock":
t = typeof (VsockTransportConfig);
break;
case "device":
t = typeof (DeviceTransportConfig);
break;
default:
break;
}
@@ -354,22 +351,6 @@ 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");
}
}
public sealed class ImageConfig : Object, Json.Serializable {
public string file {
get;
+7 -6
View File
@@ -762,12 +762,13 @@ namespace Frida {
private void on_agent_session_detached (AgentSessionId id, SessionDetachReason reason, CrashInfo crash) {
AgentSessionEntry entry;
if (agent_sessions.unset (id, out entry)) {
ControlChannel? controller = entry.controller;
if (controller != null) {
controller.agent_sessions.remove (id);
controller.agent_session_detached (id, reason, crash);
}
bool entry_found = agent_sessions.unset (id, out entry);
ControlChannel? controller = entry_found ? entry.controller : null;
GLib.info ("detach-trace: ControlService session detached id=%s reason=%d entry_found=%s controller=%p",
id.handle, reason, entry_found.to_string (), controller);
if (controller != null) {
controller.agent_sessions.remove (id);
controller.agent_session_detached (id, reason, crash);
}
}
+5 -1
View File
@@ -993,7 +993,11 @@ namespace Frida {
private void on_remote_agent_session_detached (AgentSessionId remote_id, SessionDetachReason reason, CrashInfo crash) {
AgentSessionId? local_id;
if (!remote_agent_sessions.unset (remote_id, out local_id))
bool mapping_found = remote_agent_sessions.unset (remote_id, out local_id);
GLib.info ("detach-trace: Droidy remote session detached remote_id=%s reason=%d mapping_found=%s local_id=%s",
remote_id.handle, reason, mapping_found.to_string (),
mapping_found ? local_id.handle : "-");
if (!mapping_found)
return;
bool agent_session_found = agent_sessions.unset (local_id);
+13
View File
@@ -1885,6 +1885,9 @@ namespace Frida {
break;
}
}
GLib.info ("detach-trace: Device._release_session id=%s may_block=%s",
(session_id != null) ? session_id.handle : "-", may_block.to_string ());
if (session_id == null)
return;
@@ -1900,11 +1903,16 @@ namespace Frida {
} catch (Error e) {
assert_not_reached ();
}
GLib.info ("detach-trace: Device._release_session resumed id=%s", session_id.handle);
}
}
private void on_agent_session_detached (AgentSessionId id, SessionDetachReason reason, CrashInfo crash) {
var session = agent_sessions[id];
GLib.info ("detach-trace: Device session detached id=%s reason=%d session=%p pending=%s",
id.handle, reason, session, pending_detach_requests.has_key (id).to_string ());
if (session != null)
session._on_detached (reason, crash);
@@ -3837,15 +3845,20 @@ namespace Frida {
}
private async void close_session_and_peer_connection (Cancellable? cancellable) throws IOError {
GLib.info ("detach-trace: Session closing remote id=%s", id.handle);
try {
yield active_session.close (cancellable);
} catch (GLib.Error e) {
GLib.info ("detach-trace: Session remote close failed id=%s error=%s", id.handle, e.message);
if (e is IOError.CANCELLED) {
discard_peer_connection ();
return;
}
}
GLib.info ("detach-trace: Session remote close returned id=%s", id.handle);
yield teardown_peer_connection (cancellable);
}
+6 -1
View File
@@ -871,6 +871,8 @@ namespace Frida {
private void on_agent_session_provider_closed (AgentSessionId id) {
var closed_after_opening = agent_sessions.unset (id);
GLib.info ("detach-trace: BaseDBusHostSession provider closed id=%s closed_after_opening=%s",
id.handle, closed_after_opening.to_string ());
if (!closed_after_opening)
return;
var reason = SessionDetachReason.APPLICATION_REQUESTED;
@@ -973,7 +975,10 @@ namespace Frida {
var crash_info = (crash != null) ? crash : CrashInfo.empty ();
foreach (var id in entry.sessions) {
if (agent_sessions.unset (id))
bool still_registered = agent_sessions.unset (id);
GLib.info ("detach-trace: BaseDBusHostSession teardown id=%s reason=%d still_registered=%s",
id.handle, reason, still_registered.to_string ());
if (still_registered)
agent_session_detached (id, reason, crash_info);
}
+1 -1
View File
@@ -1,6 +1,6 @@
[wrap-git]
url = https://github.com/frida/frida-gum.git
revision = 17.17.0
revision = 17.16.4
depth = 1
[provide]
+1 -1
View File
@@ -1,6 +1,6 @@
[wrap-git]
url = https://github.com/frida/glib.git
revision = 790ffa82e80d99fba8a3db494e46f907d560893c
revision = 90a8b77b3d4cdf0482eb398fb2000dcceaeb15f0
depth = 1
[provide]