mirror of
https://github.com/frida/frida-core.git
synced 2026-11-09 01:59:34 -10:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c812943c45 | ||
|
|
2e9521d006 | ||
|
|
65667a1973 | ||
|
|
2e50475515 | ||
|
|
fca672a9c1 | ||
|
|
a8d9cacb1d | ||
|
|
76a9ac10f2 | ||
|
|
4a9900d165 | ||
|
|
4c29710d97 | ||
|
|
883e51bb1e | ||
|
|
a8af49e5b3 | ||
|
|
872fb83da4 |
No files matched your search
@@ -3,6 +3,9 @@
|
||||
/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/
|
||||
|
||||
@@ -292,8 +292,6 @@ namespace Frida.Agent {
|
||||
|
||||
start.begin ((owned) padder);
|
||||
|
||||
start_alive_task_dumper ();
|
||||
|
||||
main_loop.run ();
|
||||
|
||||
main_context.pop_thread_default ();
|
||||
@@ -364,16 +362,6 @@ 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);
|
||||
@@ -936,8 +924,6 @@ 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);
|
||||
@@ -1738,8 +1724,4 @@ 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 ();
|
||||
}
|
||||
@@ -361,8 +361,6 @@ 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
|
||||
@@ -600,7 +598,6 @@ 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;
|
||||
});
|
||||
|
||||
@@ -57,8 +57,6 @@ 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);
|
||||
@@ -87,8 +85,6 @@ namespace Frida {
|
||||
|
||||
yield transmitter.close (cancellable);
|
||||
|
||||
GLib.info ("detach-trace: BaseAgentSession.close() done id=%s", id.handle);
|
||||
|
||||
close_request.resolve (true);
|
||||
}
|
||||
|
||||
@@ -280,8 +276,6 @@ 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: abb8238970...c9cc18138b.
+4
-87
@@ -12,7 +12,6 @@ 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;
|
||||
@@ -43,7 +42,6 @@ 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 },
|
||||
@@ -80,13 +78,6 @@ 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,
|
||||
@@ -165,15 +156,12 @@ 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);
|
||||
|
||||
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);
|
||||
}
|
||||
Posix.close (null_out);
|
||||
}
|
||||
|
||||
var status = new uint8[1];
|
||||
@@ -230,22 +218,6 @@ 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);
|
||||
@@ -302,15 +274,6 @@ 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 ();
|
||||
@@ -318,45 +281,6 @@ 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") {
|
||||
@@ -431,11 +355,4 @@ 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 ();
|
||||
}
|
||||
@@ -5,10 +5,13 @@ namespace Frida.Barebone {
|
||||
|
||||
private Cancellable io_cancellable = new Cancellable ();
|
||||
|
||||
private SocketConnection hostlink;
|
||||
private IOStream hostlink;
|
||||
private BufferedInputStream input;
|
||||
private OutputStream output;
|
||||
|
||||
private ByteOrder byte_order;
|
||||
private uint pointer_size;
|
||||
|
||||
private AgentConfig agent_config;
|
||||
private VsockTransportConfig? vsock_transport;
|
||||
private ImageConfig? image_config;
|
||||
@@ -46,6 +49,23 @@ 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;
|
||||
|
||||
@@ -53,8 +73,8 @@ namespace Frida.Barebone {
|
||||
var transport_tag = yield resolve_transport (cancellable);
|
||||
|
||||
var gdb = machine.gdb;
|
||||
ByteOrder byte_order = gdb.byte_order;
|
||||
uint pointer_size = gdb.pointer_size;
|
||||
byte_order = gdb.byte_order;
|
||||
pointer_size = gdb.pointer_size;
|
||||
|
||||
Layout layout;
|
||||
uint64 preferred_base = 0;
|
||||
@@ -267,7 +287,7 @@ namespace Frida.Barebone {
|
||||
});
|
||||
}
|
||||
|
||||
private void adopt_hostlink_streams (SocketConnection connection) {
|
||||
private void adopt_hostlink_streams (IOStream connection) {
|
||||
hostlink = connection;
|
||||
input = (BufferedInputStream) Object.new (typeof (BufferedInputStream),
|
||||
"base-stream", hostlink.get_input_stream (),
|
||||
@@ -341,16 +361,25 @@ namespace Frida.Barebone {
|
||||
});
|
||||
timeout_source.attach (MainContext.get_thread_default ());
|
||||
|
||||
Variant response = null;
|
||||
try {
|
||||
return yield promise.future.wait_async (cancellable);
|
||||
response = yield promise.future.wait_async (cancellable);
|
||||
} finally {
|
||||
timeout_source.destroy ();
|
||||
}
|
||||
|
||||
if (!response.check_format_string ("(bv)", false))
|
||||
throw new Error.PROTOCOL ("Malformed reply from agent");
|
||||
bool succeeded;
|
||||
Variant detail;
|
||||
response.get ("(bv)", out succeeded, out detail);
|
||||
if (!succeeded)
|
||||
throw new Error.NOT_SUPPORTED ("%s", detail.get_string ());
|
||||
|
||||
return detail;
|
||||
}
|
||||
|
||||
private async void process_incoming_messages () {
|
||||
var byte_order = machine.gdb.byte_order;
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
size_t header_size = 4;
|
||||
@@ -503,10 +532,10 @@ namespace Frida.Barebone {
|
||||
|
||||
private Bytes frame_message (Command command, uint16 request_id, Variant payload) {
|
||||
var message = new Variant ("(yqv)", (uint8) command, request_id, payload);
|
||||
if (machine.gdb.byte_order != ByteOrder.HOST)
|
||||
if (byte_order != ByteOrder.HOST)
|
||||
message = message.byteswap ();
|
||||
var message_bytes = message.get_data_as_bytes ();
|
||||
return machine.gdb.make_buffer_builder ()
|
||||
return new BufferBuilder (byte_order, pointer_size)
|
||||
.append_uint32 ((uint32) message_bytes.get_size ())
|
||||
.append_bytes (message_bytes)
|
||||
.build ();
|
||||
|
||||
@@ -4,6 +4,22 @@ 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"
|
||||
|
||||
@@ -21,10 +21,29 @@
|
||||
export AR_aarch64_unknown_none=aarch64-none-elf-ar
|
||||
export RANLIB_aarch64_unknown_none=aarch64-none-elf-ranlib
|
||||
|
||||
cargo build --release --features xnu
|
||||
|
||||
## Targets
|
||||
|
||||
The agent supports two kernels. One of them has to be picked; neither is a
|
||||
default, since neither follows from the host you build on:
|
||||
|
||||
- `xnu` — a freestanding ELF executable that the Barebone backend
|
||||
injects into a running kernel from the outside, over a remote stub.
|
||||
- `linux` — a static library that becomes a kernel module, loaded from the
|
||||
inside. See `linux/README.md`.
|
||||
|
||||
Everything above the `kernel` module is shared; each backend supplies the same
|
||||
set of primitives (logging, allocation, threads, waiting, time, symbols) plus its
|
||||
half of Gum's platform backend (`gum_xnu.rs` / `gum_linux.rs`).
|
||||
|
||||
## Development loop
|
||||
|
||||
export FRIDA_BAREBONE_CONFIG=$PWD/etc/xnu.json
|
||||
cargo build --release && make -C ~/src/frida-python && killall -9 qemu-system-aarch64 && sleep 2 && frida -D barebone -p 0
|
||||
cargo build --release --features xnu \
|
||||
&& make -C ~/src/frida-python \
|
||||
&& killall -9 qemu-system-aarch64 && sleep 2 \
|
||||
&& frida -D barebone -p 0
|
||||
|
||||
## Speeding up loop
|
||||
|
||||
|
||||
+33
-19
@@ -11,9 +11,13 @@ fn main() {
|
||||
let cc_str = env::var("CC_aarch64_unknown_none")
|
||||
.or_else(|_| env::var("CC"))
|
||||
.unwrap_or_else(|_| "cc".to_string());
|
||||
let cc = Path::new(&cc_str);
|
||||
let cc_include_paths = detect_gcc_include_paths(cc);
|
||||
let cc_library_paths = detect_gcc_library_paths(cc);
|
||||
// Same convention as cc-rs: the variable may carry flags, and a bare-metal
|
||||
// compiler needs them to find its libc at all.
|
||||
let mut cc_argv = cc_str.split_whitespace();
|
||||
let cc = Path::new(cc_argv.next().unwrap());
|
||||
let cc_args: Vec<&str> = cc_argv.collect();
|
||||
let cc_include_paths = detect_gcc_include_paths(cc, &cc_args);
|
||||
let cc_library_paths = detect_gcc_library_paths(cc, &cc_args);
|
||||
|
||||
let cc_clang_args: Vec<String> = cc_include_paths
|
||||
.iter()
|
||||
@@ -36,25 +40,35 @@ fn main() {
|
||||
.write_to_file(out_dir.join("bindings.rs"))
|
||||
.expect("Couldn't write bindings");
|
||||
|
||||
println!(
|
||||
"cargo:rustc-link-search=native={}",
|
||||
devkit_dir.to_str().unwrap()
|
||||
);
|
||||
println!("cargo:rustc-link-lib=static=frida-gumjs");
|
||||
for path in cc_library_paths {
|
||||
println!("cargo:rustc-link-search=native={}", path.to_string_lossy());
|
||||
// The Linux flavour is a staticlib: kbuild owns the final link, so it — not
|
||||
// us — decides where the devkit and libc archives come from.
|
||||
if env::var("CARGO_FEATURE_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-lib=static=c");
|
||||
println!("cargo:rustc-link-lib=static=m");
|
||||
println!("cargo:rustc-link-arg=--export-dynamic");
|
||||
println!("cargo:rustc-link-arg=--emit-relocs");
|
||||
println!("cargo:rustc-link-arg=--script=agent.lds");
|
||||
println!("cargo:rustc-link-arg=--gc-sections");
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
|
||||
pub fn detect_gcc_include_paths(gcc: &Path) -> Vec<PathBuf> {
|
||||
pub fn detect_gcc_include_paths(gcc: &Path, args: &[&str]) -> Vec<PathBuf> {
|
||||
let out = Command::new(gcc)
|
||||
.args(args)
|
||||
.args(["-xc", "-E", "-v", "-"])
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
@@ -94,8 +108,8 @@ pub fn detect_gcc_include_paths(gcc: &Path) -> Vec<PathBuf> {
|
||||
paths
|
||||
}
|
||||
|
||||
pub fn detect_gcc_library_paths(gcc: &Path) -> Vec<PathBuf> {
|
||||
let out = Command::new(gcc).arg("-print-search-dirs").output()
|
||||
pub fn detect_gcc_library_paths(gcc: &Path, args: &[&str]) -> Vec<PathBuf> {
|
||||
let out = Command::new(gcc).args(args).arg("-print-search-dirs").output()
|
||||
.expect("Failed to execute GCC to detect library paths");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"agent": {
|
||||
"transport": {
|
||||
"type": "device",
|
||||
"path": "/dev/frida"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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
|
||||
@@ -0,0 +1,108 @@
|
||||
# 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
|
||||
@@ -0,0 +1,154 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,823 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use core::ffi::c_void;
|
||||
|
||||
use crate::bindings::{gpointer, gsize, GumPageProtection};
|
||||
use crate::{gum, libc, xnu};
|
||||
use crate::bindings::{gpointer, gsize, gum_try_mprotect, GumPageProtection};
|
||||
use crate::{kernel, libc};
|
||||
|
||||
const GUM_PAGE_READ: GumPageProtection = 1;
|
||||
const GUM_PAGE_EXECUTE: GumPageProtection = 4;
|
||||
@@ -10,7 +10,7 @@ const GUM_PAGE_EXECUTE: GumPageProtection = 4;
|
||||
// static trampoline `text` made executable, followed by a writable parameter
|
||||
// table `map_size` bytes later (the trampolines reach their parameters via that
|
||||
// fixed offset). The two halves are whole pages, so flipping the first to RX
|
||||
// over the hostlink leaves the parameter table writable.
|
||||
// leaves the parameter table writable.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ffi_tramp_embedder_map(
|
||||
text: *const c_void,
|
||||
@@ -19,7 +19,7 @@ pub unsafe extern "C" fn ffi_tramp_embedder_map(
|
||||
parm_table: *mut *mut c_void,
|
||||
) -> i32 {
|
||||
unsafe {
|
||||
let base = xnu::kalloc(map_size * 2);
|
||||
let base = kernel::alloc_code(map_size * 2);
|
||||
if base.is_null() {
|
||||
return 0;
|
||||
}
|
||||
@@ -28,9 +28,9 @@ pub unsafe extern "C" fn ffi_tramp_embedder_map(
|
||||
libc::__clear_cache(base, base.add(map_size));
|
||||
|
||||
let made_executable =
|
||||
gum::gum_try_mprotect(base as gpointer, map_size as gsize, GUM_PAGE_READ | GUM_PAGE_EXECUTE);
|
||||
gum_try_mprotect(base as gpointer, map_size as gsize, GUM_PAGE_READ | GUM_PAGE_EXECUTE);
|
||||
if made_executable == 0 {
|
||||
xnu::free(base, map_size * 2);
|
||||
kernel::free_code(base, map_size * 2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,5 @@ pub unsafe extern "C" fn ffi_tramp_embedder_unmap(
|
||||
_parm_table: *mut c_void,
|
||||
map_size: usize,
|
||||
) {
|
||||
unsafe {
|
||||
xnu::free(code_table as *mut u8, map_size * 2);
|
||||
}
|
||||
kernel::free_code(code_table as *mut u8, map_size * 2);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use core::ptr;
|
||||
|
||||
use crate::bindings::{g_wait_is_set, gint64, gpointer};
|
||||
use crate::xnu;
|
||||
use crate::kernel;
|
||||
|
||||
const G_WAIT_INFINITE: gint64 = -1;
|
||||
|
||||
@@ -9,38 +9,35 @@ pub static mut WAKEUP_TOKEN: u64 = 0;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_get_monotonic_time() -> gint64 {
|
||||
let abstime = xnu::mach_absolute_time();
|
||||
let time = xnu::absolutetime_to_nanoseconds(abstime);
|
||||
(time / 1000) as gint64
|
||||
kernel::monotonic_micros()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_wait_sleep(token: gpointer, timeout_us: gint64) {
|
||||
let wait_event = ptr::addr_of_mut!(WAKEUP_TOKEN) as *const u8;
|
||||
|
||||
let wait_result = if timeout_us == G_WAIT_INFINITE {
|
||||
xnu::assert_wait(wait_event, xnu::THREAD_INTERRUPTIBLE)
|
||||
let timeout = if timeout_us == G_WAIT_INFINITE {
|
||||
None
|
||||
} else {
|
||||
xnu::assert_wait_timeout(
|
||||
wait_event,
|
||||
xnu::THREAD_INTERRUPTIBLE,
|
||||
(timeout_us * 1000) as u32,
|
||||
1, // TODO
|
||||
)
|
||||
Some(timeout_us as u64)
|
||||
};
|
||||
if wait_result != xnu::THREAD_WAITING {
|
||||
panic!("assert_wait_timeout failed: {}", wait_result);
|
||||
}
|
||||
|
||||
if unsafe { g_wait_is_set(token) != 0 } {
|
||||
xnu::thread_wakeup(wait_event);
|
||||
return;
|
||||
}
|
||||
|
||||
xnu::thread_block(None);
|
||||
kernel::wait(wait_event, timeout, &mut || unsafe {
|
||||
g_wait_is_set(token) != 0
|
||||
});
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_wait_wake(_token: gpointer) {
|
||||
xnu::thread_wakeup(ptr::addr_of_mut!(WAKEUP_TOKEN) as *const u8);
|
||||
kernel::wake(ptr::addr_of_mut!(WAKEUP_TOKEN) as *const u8);
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_io_channel_unix_new(_fd: i32) -> gpointer {
|
||||
ptr::null_mut()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_spawn_async_with_pipes() -> i32 {
|
||||
0
|
||||
}
|
||||
@@ -8,7 +8,6 @@ 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};
|
||||
|
||||
@@ -33,9 +32,11 @@ 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 {
|
||||
signal: AtomicU32, // Signal state
|
||||
generation: AtomicU32,
|
||||
_padding: [u8; core::mem::size_of::<GCond>() - core::mem::size_of::<AtomicU32>()],
|
||||
}
|
||||
|
||||
@@ -144,10 +145,9 @@ pub extern "C" fn g_rec_mutex_clear(_rec_mutex: *mut GRecMutex) {
|
||||
pub extern "C" fn g_rec_mutex_lock(rec_mutex: *mut GRecMutex) {
|
||||
unsafe {
|
||||
let impl_ = rec_mutex_impl(rec_mutex);
|
||||
let current_thread = get_current_thread_id();
|
||||
|
||||
loop {
|
||||
if rec_mutex_try_acquire(impl_, current_thread) {
|
||||
if rec_mutex_try_acquire(impl_) {
|
||||
break;
|
||||
}
|
||||
rec_mutex_wait(impl_);
|
||||
@@ -159,9 +159,8 @@ pub extern "C" fn g_rec_mutex_lock(rec_mutex: *mut GRecMutex) {
|
||||
pub extern "C" fn g_rec_mutex_trylock(rec_mutex: *mut GRecMutex) -> u32 {
|
||||
unsafe {
|
||||
let impl_ = rec_mutex_impl(rec_mutex);
|
||||
let current_thread = get_current_thread_id();
|
||||
|
||||
if rec_mutex_try_acquire(impl_, current_thread) {
|
||||
if rec_mutex_try_acquire(impl_) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
@@ -183,7 +182,7 @@ pub extern "C" fn g_rec_mutex_unlock(rec_mutex: *mut GRecMutex) {
|
||||
.is_ok()
|
||||
{
|
||||
impl_.owner.store(0, Ordering::Relaxed);
|
||||
lock_notify_all();
|
||||
wake_waiters(&impl_.count);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -198,7 +197,10 @@ pub extern "C" fn g_rec_mutex_unlock(rec_mutex: *mut GRecMutex) {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl, current_thread: u64) -> bool {
|
||||
// Asking the kernel who we are costs more than the uncontended acquisition itself, and
|
||||
// every allocation takes this path, so the identity is only fetched once the lock turns
|
||||
// out to be held.
|
||||
unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl) -> bool {
|
||||
let current_count = impl_.count.load(Ordering::Relaxed);
|
||||
|
||||
if current_count == 0 {
|
||||
@@ -206,12 +208,12 @@ unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl, current_thread: u64) -
|
||||
.compare_exchange_weak(0, 1, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
impl_.owner.store(current_thread, Ordering::Relaxed);
|
||||
impl_.owner.store(crate::kernel::current_thread_id(), Ordering::Relaxed);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
let current_owner = impl_.owner.load(Ordering::Relaxed);
|
||||
if current_owner == current_thread {
|
||||
if current_owner == crate::kernel::current_thread_id() {
|
||||
if impl_.count
|
||||
.compare_exchange_weak(
|
||||
current_count,
|
||||
@@ -230,7 +232,7 @@ unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl, current_thread: u64) -
|
||||
|
||||
unsafe fn rec_mutex_wait(impl_: &mut RecMutexImpl) {
|
||||
while impl_.count.load(Ordering::Relaxed) != 0 {
|
||||
unsafe { lock_wait_primitive() };
|
||||
wait_on(&impl_.count, &mut || impl_.count.load(Ordering::Relaxed) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +284,7 @@ pub extern "C" fn g_rw_lock_writer_unlock(rw_lock: *mut GRWLock) {
|
||||
unsafe {
|
||||
let impl_ = rw_lock_impl(rw_lock);
|
||||
impl_.state.store(0, Ordering::Release);
|
||||
lock_notify_all();
|
||||
wake_waiters(&impl_.state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +331,7 @@ pub extern "C" fn g_rw_lock_reader_unlock(rw_lock: *mut GRWLock) {
|
||||
.is_ok()
|
||||
{
|
||||
if current - 1 == 0 {
|
||||
lock_notify_all();
|
||||
wake_waiters(&impl_.state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -355,13 +357,15 @@ unsafe fn rw_lock_reader_try_acquire(impl_: &mut RWLockImpl) -> bool {
|
||||
|
||||
unsafe fn rw_lock_wait_for_writers(impl_: &mut RWLockImpl) {
|
||||
while impl_.state.load(Ordering::Relaxed) & RW_WRITER_LOCK_BIT != 0 {
|
||||
unsafe { lock_wait_primitive() };
|
||||
wait_on(&impl_.state, &mut || {
|
||||
impl_.state.load(Ordering::Relaxed) & RW_WRITER_LOCK_BIT == 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn rw_lock_wait_for_all(impl_: &mut RWLockImpl) {
|
||||
while impl_.state.load(Ordering::Relaxed) != 0 {
|
||||
unsafe { lock_wait_primitive() };
|
||||
wait_on(&impl_.state, &mut || impl_.state.load(Ordering::Relaxed) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +373,7 @@ unsafe fn rw_lock_wait_for_all(impl_: &mut RWLockImpl) {
|
||||
pub extern "C" fn g_cond_init(cond: *mut GCond) {
|
||||
unsafe {
|
||||
let impl_ = cond_impl(cond);
|
||||
impl_.signal.store(0, Ordering::Relaxed);
|
||||
impl_.generation.store(0, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,11 +383,17 @@ pub extern "C" fn g_cond_clear(_cond: *mut GCond) {}
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_cond_wait(cond: *mut GCond, mutex: *mut GMutex) {
|
||||
unsafe {
|
||||
// Read the generation under the mutex, so a signal that lands after the
|
||||
// unlock below is still observed as a change.
|
||||
let seen = cond_impl(cond).generation.load(Ordering::Acquire);
|
||||
|
||||
g_mutex_unlock(mutex);
|
||||
|
||||
let impl_ = cond_impl(cond);
|
||||
while impl_.signal.load(Ordering::Acquire) == 0 {
|
||||
lock_wait_primitive();
|
||||
while impl_.generation.load(Ordering::Acquire) == seen {
|
||||
wait_on(&impl_.generation, &mut || {
|
||||
impl_.generation.load(Ordering::Acquire) != seen
|
||||
});
|
||||
}
|
||||
|
||||
g_mutex_lock(mutex);
|
||||
@@ -394,8 +404,8 @@ pub extern "C" fn g_cond_wait(cond: *mut GCond, mutex: *mut GMutex) {
|
||||
pub extern "C" fn g_cond_signal(cond: *mut GCond) {
|
||||
unsafe {
|
||||
let impl_ = cond_impl(cond);
|
||||
impl_.signal.store(1, Ordering::Release);
|
||||
lock_notify_all();
|
||||
impl_.generation.fetch_add(1, Ordering::Release);
|
||||
wake_waiters(&impl_.generation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,22 +413,37 @@ pub extern "C" fn g_cond_signal(cond: *mut GCond) {
|
||||
pub extern "C" fn g_cond_broadcast(cond: *mut GCond) {
|
||||
unsafe {
|
||||
let impl_ = cond_impl(cond);
|
||||
impl_.signal.store(1, Ordering::Release);
|
||||
lock_notify_all();
|
||||
impl_.generation.fetch_add(1, Ordering::Release);
|
||||
wake_waiters(&impl_.generation);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_cond_wait_until(cond: *mut GCond, mutex: *mut GMutex, _end_time: i64) -> u32 {
|
||||
// TODO: Implement proper timeout handling
|
||||
g_cond_wait(cond, mutex);
|
||||
1
|
||||
pub extern "C" fn g_cond_wait_until(cond: *mut GCond, mutex: *mut GMutex, end_time: i64) -> u32 {
|
||||
unsafe {
|
||||
let seen = cond_impl(cond).generation.load(Ordering::Acquire);
|
||||
|
||||
g_mutex_unlock(mutex);
|
||||
|
||||
let impl_ = cond_impl(cond);
|
||||
let mut signalled = impl_.generation.load(Ordering::Acquire) != seen;
|
||||
while !signalled && crate::kernel::monotonic_micros() < end_time {
|
||||
wait_on(&impl_.generation, &mut || {
|
||||
impl_.generation.load(Ordering::Acquire) != seen
|
||||
});
|
||||
signalled = impl_.generation.load(Ordering::Acquire) != seen;
|
||||
}
|
||||
|
||||
g_mutex_lock(mutex);
|
||||
|
||||
if signalled { 1 } else { 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_private_get(key: *mut GPrivate) -> *mut c_void {
|
||||
unsafe {
|
||||
let thread_id = get_current_thread_id();
|
||||
let thread_id = crate::kernel::current_thread_id();
|
||||
lock_acquire(&TLS_LOCK);
|
||||
|
||||
ensure_tls_storage();
|
||||
@@ -438,7 +463,7 @@ pub extern "C" fn g_private_get(key: *mut GPrivate) -> *mut c_void {
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn g_private_set(key: *mut GPrivate, value: *mut c_void) {
|
||||
unsafe {
|
||||
let thread_id = get_current_thread_id();
|
||||
let thread_id = crate::kernel::current_thread_id();
|
||||
lock_acquire(&TLS_LOCK);
|
||||
|
||||
ensure_tls_storage();
|
||||
@@ -468,7 +493,9 @@ pub extern "C" fn _g_system_thread_create(
|
||||
data: gpointer,
|
||||
) -> *mut GSystemThread {
|
||||
unsafe {
|
||||
let system_thread = crate::xnu::kalloc(core::mem::size_of::<SystemThreadImpl>()) as *mut SystemThreadImpl;
|
||||
crate::kernel::log("frida: glib is creating a thread\n\0");
|
||||
|
||||
let system_thread = crate::kernel::alloc(core::mem::size_of::<SystemThreadImpl>()) as *mut SystemThreadImpl;
|
||||
|
||||
g_mutex_init(&mut (*system_thread).mutex);
|
||||
g_cond_init(&mut (*system_thread).cond);
|
||||
@@ -478,12 +505,12 @@ pub extern "C" fn _g_system_thread_create(
|
||||
(*system_thread).finished.store(0, Ordering::Relaxed);
|
||||
(*system_thread).detached.store(0, Ordering::Relaxed);
|
||||
|
||||
let wrapper_data = crate::xnu::kalloc(core::mem::size_of::<ThreadWrapperData>()) as *mut ThreadWrapperData;
|
||||
let wrapper_data = crate::kernel::alloc(core::mem::size_of::<ThreadWrapperData>()) as *mut ThreadWrapperData;
|
||||
(*wrapper_data).func = func;
|
||||
(*wrapper_data).data = data;
|
||||
(*wrapper_data).system_thread = system_thread;
|
||||
|
||||
let _xnu_result = crate::xnu::kernel_thread_start(thread_wrapper, wrapper_data as *mut c_void);
|
||||
let _xnu_result = crate::kernel::spawn_thread(thread_wrapper, wrapper_data as *mut c_void);
|
||||
|
||||
system_thread as *mut GSystemThread
|
||||
}
|
||||
@@ -496,8 +523,9 @@ extern "C" fn thread_wrapper(parameter: *mut c_void, _wait_result: i32) {
|
||||
let data = (*wrapper_data).data;
|
||||
let system_thread = (*wrapper_data).system_thread;
|
||||
|
||||
let current_thread_id = get_current_thread_id();
|
||||
(*system_thread).thread_id.store(current_thread_id, Ordering::Relaxed);
|
||||
let current_thread_id = crate::kernel::current_thread_id();
|
||||
(*system_thread).thread_id.store(current_thread_id, Ordering::Release);
|
||||
wake_waiters(&(*system_thread).thread_id);
|
||||
|
||||
func.unwrap()(data);
|
||||
|
||||
@@ -537,12 +565,10 @@ pub extern "C" fn _g_system_thread_wait(thread: *mut GSystemThread) {
|
||||
unsafe {
|
||||
let system_thread = thread as *mut SystemThreadImpl;
|
||||
|
||||
loop {
|
||||
let thread_id = (*system_thread).thread_id.load(Ordering::Acquire);
|
||||
if thread_id != 0 {
|
||||
break;
|
||||
}
|
||||
core::hint::spin_loop();
|
||||
while (*system_thread).thread_id.load(Ordering::Acquire) == 0 {
|
||||
wait_on(&(*system_thread).thread_id, &mut || {
|
||||
(*system_thread).thread_id.load(Ordering::Acquire) != 0
|
||||
});
|
||||
}
|
||||
|
||||
g_mutex_lock(&mut (*system_thread).mutex);
|
||||
@@ -556,19 +582,16 @@ pub extern "C" fn _g_system_thread_wait(thread: *mut GSystemThread) {
|
||||
}
|
||||
|
||||
unsafe fn cleanup_system_thread(system_thread: *mut SystemThreadImpl) {
|
||||
crate::xnu::free(system_thread as *mut u8, core::mem::size_of::<SystemThreadImpl>());
|
||||
crate::kernel::free(system_thread as *mut u8, core::mem::size_of::<SystemThreadImpl>());
|
||||
}
|
||||
|
||||
unsafe fn cleanup_wrapper_data(wrapper_data: *mut ThreadWrapperData) {
|
||||
crate::xnu::free(wrapper_data as *mut u8, core::mem::size_of::<ThreadWrapperData>());
|
||||
crate::kernel::free(wrapper_data as *mut u8, core::mem::size_of::<ThreadWrapperData>());
|
||||
}
|
||||
|
||||
unsafe fn lock_acquire(lock: &AtomicU32) {
|
||||
loop {
|
||||
if unsafe { lock_try_acquire(lock) } {
|
||||
break;
|
||||
}
|
||||
unsafe { lock_wait(lock) };
|
||||
while !unsafe { lock_try_acquire(lock) } {
|
||||
wait_on(lock, &mut || lock.load(Ordering::Relaxed) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,35 +602,36 @@ unsafe fn lock_try_acquire(lock: &AtomicU32) -> bool {
|
||||
|
||||
unsafe fn lock_release(lock: &AtomicU32) {
|
||||
lock.store(0, Ordering::Release);
|
||||
unsafe { lock_notify_all() };
|
||||
wake_waiters(lock);
|
||||
}
|
||||
|
||||
unsafe fn lock_wait(lock: &AtomicU32) {
|
||||
while lock.load(Ordering::Relaxed) != 0 {
|
||||
unsafe { lock_wait_primitive() };
|
||||
}
|
||||
// Sleeps until `ready` holds or the slice expires, whichever comes first.
|
||||
//
|
||||
// These used to spin on wfe/sev. A kernel thread that spins never reaches the
|
||||
// scheduler, so a lost wakeup — or a genuine deadlock, which is how we found this —
|
||||
// starves the CPU it is on until the hardware watchdog takes the machine down. The
|
||||
// bounded sleep turns both into a stall that can be observed instead.
|
||||
//
|
||||
// The address of the word being waited on names the wait channel: XNU pairs
|
||||
// assert_wait()/thread_wakeup() on it directly, while the Linux shim has a single
|
||||
// channel and treats it as a hint. Every caller re-checks its own condition in a
|
||||
// loop, so a spurious wakeup costs nothing either way.
|
||||
fn wait_on<T>(word: &T, ready: &mut dyn FnMut() -> bool) {
|
||||
crate::kernel::wait(token_of(word), Some(WAIT_SLICE_US), ready);
|
||||
|
||||
// The wait above returns without sleeping whenever `ready` already holds, so on
|
||||
// its own it is not enough to keep a contended loop off the CPU: every caller
|
||||
// here loops, and a loop in a kernel thread that never reaches the scheduler
|
||||
// takes the machine down rather than merely running hot. Yield unconditionally.
|
||||
crate::kernel::yield_now();
|
||||
}
|
||||
|
||||
unsafe fn lock_wait_primitive() {
|
||||
unsafe {
|
||||
asm!("wfe", options(nomem, nostack));
|
||||
}
|
||||
fn wake_waiters<T>(word: &T) {
|
||||
crate::kernel::wake(token_of(word));
|
||||
}
|
||||
|
||||
unsafe fn lock_notify_all() {
|
||||
unsafe {
|
||||
asm!("sev", options(nomem, nostack));
|
||||
}
|
||||
fn token_of<T>(word: &T) -> *const u8 {
|
||||
word as *const T as *const u8
|
||||
}
|
||||
|
||||
// The thread pointer is exposed to JavaScript as a GumThreadId, so it must fit
|
||||
// in a double without losing precision; the low bits keep it unique per thread.
|
||||
const JS_SAFE_THREAD_ID_MASK: u64 = (1 << 48) - 1;
|
||||
|
||||
pub unsafe fn get_current_thread_id() -> u64 {
|
||||
let thread_ptr: u64;
|
||||
unsafe {
|
||||
asm!("mrs {}, tpidr_el1", out(reg) thread_ptr, options(nomem, nostack));
|
||||
}
|
||||
thread_ptr & JS_SAFE_THREAD_ID_MASK
|
||||
}
|
||||
const WAIT_SLICE_US: u64 = 1000;
|
||||
+76
-413
@@ -1,85 +1,31 @@
|
||||
// Gum's platform backend, minus the parts that depend on which kernel we are
|
||||
// running inside: memory protection, symbols and the module registry live in
|
||||
// gum_xnu.rs and gum_linux.rs respectively.
|
||||
|
||||
use crate::{
|
||||
bindings::{
|
||||
_GInterfaceInfo, _GTypeInfo, _GumPageProtection_GUM_PAGE_EXECUTE,
|
||||
_GumPageProtection_GUM_PAGE_READ, _GumPageProtection_GUM_PAGE_WRITE,
|
||||
_GumRwxSupport_GUM_RWX_NONE, GArray, GObject, GObjectClass, GPrivate, GType,
|
||||
GumDebugSymbolDetails, GumExportDetails, GumExportType_GUM_EXPORT_FUNCTION,
|
||||
GumFoundExportFunc, GumMemoryRange, GumModule, GumModuleInterface, GumModuleRegistry,
|
||||
GumPageProtection, GumRwxSupport, GumThreadId, GumTlsKey, g_array_append_vals, g_array_new,
|
||||
g_free, g_object_get_type, g_object_new, g_object_unref, g_once_init_enter,
|
||||
g_once_init_leave, g_strdup, g_type_add_interface_static, g_type_class_peek_parent,
|
||||
g_type_register_static, g_variant_get_boolean, g_variant_get_uint64, g_variant_new,
|
||||
g_variant_new_fixed_array, g_variant_type_free, g_variant_type_new, gboolean, gchar,
|
||||
gconstpointer, gpointer, gsize, guint,
|
||||
gum_barebone_register_module,
|
||||
gum_barebone_try_remap_writable_pages as _gum_barebone_try_remap_writable_pages,
|
||||
g_variant_unref, gum_module_get_type, gum_mprotect, gum_query_page_size,
|
||||
_GInterfaceInfo, _GTypeInfo, GObject, GObjectClass, GPrivate, GType, GumExportDetails,
|
||||
GumExportType_GUM_EXPORT_FUNCTION, GumFoundExportFunc, GumMemoryRange, GumModule,
|
||||
GumModuleInterface, GumThreadId, GumTlsKey, g_free, g_object_get_type, g_object_new,
|
||||
g_once_init_enter, g_once_init_leave, g_strdup, g_type_add_interface_static,
|
||||
g_type_class_peek_parent, g_type_register_static, gchar, gpointer, gsize, guint,
|
||||
},
|
||||
gthread, host_rpc, libc, FridaCommand,
|
||||
gthread, kernel, libc,
|
||||
};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::ffi::CString;
|
||||
use alloc::format;
|
||||
use alloc::vec::Vec;
|
||||
use core::ffi::CStr;
|
||||
use core::mem::size_of;
|
||||
use core::ptr;
|
||||
use core::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
// Code slabs the agent allocates are normal writable RAM, so gum can keep a real writable alias to
|
||||
// them across the slab's lifetime. Pre-existing kernel/kext text is CTRR-locked and can only be
|
||||
// modified through the host's physical-memory bridge, so we shadow it and commit on dispose. We
|
||||
// distinguish the two by tracking the executable ranges we hand out.
|
||||
static SLAB_LOCK: AtomicU32 = AtomicU32::new(0);
|
||||
static mut SLABS: Vec<(u64, u64)> = Vec::new();
|
||||
|
||||
const SHADOW_MAGIC: u64 = 0x4644_4f48_5341_4853;
|
||||
const SHADOW_HEADER: usize = 24;
|
||||
const SHADOW_MIN_ADDRESS: u64 = 0xffff_f000_0000_0000;
|
||||
|
||||
fn slab_lock() {
|
||||
while SLAB_LOCK
|
||||
.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_err()
|
||||
{
|
||||
core::hint::spin_loop();
|
||||
}
|
||||
}
|
||||
|
||||
fn slab_unlock() {
|
||||
SLAB_LOCK.store(0, Ordering::Release);
|
||||
}
|
||||
|
||||
fn register_slab(start: u64, size: usize) {
|
||||
slab_lock();
|
||||
unsafe {
|
||||
(*core::ptr::addr_of_mut!(SLABS)).push((start, start + size as u64));
|
||||
}
|
||||
slab_unlock();
|
||||
}
|
||||
|
||||
fn unregister_slab(start: u64) {
|
||||
slab_lock();
|
||||
unsafe {
|
||||
(*core::ptr::addr_of_mut!(SLABS)).retain(|&(begin, _)| begin != start);
|
||||
}
|
||||
slab_unlock();
|
||||
}
|
||||
|
||||
fn is_agent_slab(address: u64) -> bool {
|
||||
slab_lock();
|
||||
let found = unsafe {
|
||||
(*core::ptr::addr_of!(SLABS))
|
||||
.iter()
|
||||
.any(|&(begin, end)| address >= begin && address < end)
|
||||
};
|
||||
slab_unlock();
|
||||
found
|
||||
}
|
||||
#[cfg(feature = "linux")]
|
||||
use crate::gum_linux::enumerate_exports_in_range;
|
||||
#[cfg(feature = "xnu")]
|
||||
use crate::gum_xnu::enumerate_exports_in_range;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_process_get_current_thread_id() -> GumThreadId {
|
||||
unsafe { gthread::get_current_thread_id() }
|
||||
kernel::current_thread_id()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
@@ -99,200 +45,6 @@ pub extern "C" fn gum_barebone_query_page_size() -> guint {
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_query_rwx_support() -> GumRwxSupport {
|
||||
_GumRwxSupport_GUM_RWX_NONE
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_can_remap_writable() -> gboolean {
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_try_remap_writable_pages(
|
||||
first_page: gpointer,
|
||||
n_pages: guint,
|
||||
) -> gpointer {
|
||||
if is_agent_slab(first_page as u64) {
|
||||
return remap_agent_pages(first_page, n_pages);
|
||||
}
|
||||
shadow_kernel_pages(first_page, n_pages)
|
||||
}
|
||||
|
||||
fn remap_agent_pages(first_page: gpointer, n_pages: guint) -> gpointer {
|
||||
unsafe {
|
||||
let page_size = gum_query_page_size() as usize;
|
||||
let mut virtual_addrs = Vec::with_capacity(n_pages as usize);
|
||||
|
||||
let mut current_page = first_page as u64;
|
||||
for _ in 0..n_pages {
|
||||
virtual_addrs.push(current_page as gpointer);
|
||||
current_page += page_size as u64;
|
||||
}
|
||||
|
||||
_gum_barebone_try_remap_writable_pages(
|
||||
virtual_addrs.as_ptr() as *mut *const core::ffi::c_void,
|
||||
virtual_addrs.len() as guint,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn shadow_kernel_pages(first_page: gpointer, n_pages: guint) -> gpointer {
|
||||
unsafe {
|
||||
let total = n_pages as usize * gum_query_page_size() as usize;
|
||||
let buffer = crate::xnu::kalloc(SHADOW_HEADER + total);
|
||||
*(buffer as *mut u64) = SHADOW_MAGIC;
|
||||
*(buffer.add(8) as *mut u64) = first_page as u64;
|
||||
*(buffer.add(16) as *mut u32) = n_pages;
|
||||
|
||||
let body = buffer.add(SHADOW_HEADER);
|
||||
core::ptr::copy_nonoverlapping(first_page as *const u8, body, total);
|
||||
body as gpointer
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_dispose_writable_pages(writable: gpointer, _n_pages: guint) {
|
||||
if (writable as u64) < SHADOW_MIN_ADDRESS {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
let buffer = (writable as *mut u8).sub(SHADOW_HEADER);
|
||||
if *(buffer as *const u64) != SHADOW_MAGIC {
|
||||
return;
|
||||
}
|
||||
let first_page = *(buffer.add(8) as *const u64);
|
||||
let n_pages = *(buffer.add(16) as *const u32);
|
||||
let total = n_pages as usize * gum_query_page_size() as usize;
|
||||
|
||||
commit_kernel_patch(first_page, writable as *const u8, total);
|
||||
libc::__clear_cache(first_page as *const u8, (first_page + total as u64) as *const u8);
|
||||
|
||||
crate::xnu::free(buffer, SHADOW_HEADER + total);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn commit_kernel_patch(address: u64, data: *const u8, len: usize) {
|
||||
unsafe {
|
||||
let element_type = g_variant_type_new(c"y".as_ptr());
|
||||
let bytes = g_variant_new_fixed_array(element_type, data as gconstpointer, len as gsize, 1);
|
||||
g_variant_type_free(element_type);
|
||||
|
||||
let payload = g_variant_new(c"(t@ay)".as_ptr(), address, bytes);
|
||||
let reply = host_rpc(FridaCommand::PatchCode, payload);
|
||||
g_variant_unref(reply);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_barebone_try_remap_writable_pages(
|
||||
addrs: *const gpointer,
|
||||
n_addrs: guint,
|
||||
) -> gpointer {
|
||||
if !crate::transport_is_up() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
unsafe {
|
||||
let element_type = g_variant_type_new(c"t".as_ptr());
|
||||
let payload = g_variant_new_fixed_array(
|
||||
element_type,
|
||||
addrs as gconstpointer,
|
||||
n_addrs as gsize,
|
||||
size_of::<u64>() as gsize,
|
||||
);
|
||||
g_variant_type_free(element_type);
|
||||
|
||||
let reply = host_rpc(FridaCommand::RemapWritablePages, payload);
|
||||
let virtual_address = g_variant_get_uint64(reply);
|
||||
g_variant_unref(reply);
|
||||
|
||||
virtual_address as gpointer
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_try_mprotect(
|
||||
address: gpointer,
|
||||
size: gsize,
|
||||
prot: GumPageProtection,
|
||||
) -> gboolean {
|
||||
if !crate::transport_is_up() {
|
||||
return 1;
|
||||
}
|
||||
unsafe {
|
||||
let payload = g_variant_new(c"(ttu)".as_ptr(), address as u64, size as u64, prot as u32);
|
||||
|
||||
let reply = host_rpc(FridaCommand::MemoryProtect, payload);
|
||||
let success = g_variant_get_boolean(reply);
|
||||
g_variant_unref(reply);
|
||||
|
||||
if success != 0 {
|
||||
flush_tlb_range(address as u64, size as u64);
|
||||
}
|
||||
|
||||
success
|
||||
}
|
||||
}
|
||||
|
||||
// The host rewrites our page-table descriptors through the physical-memory
|
||||
// bridge, which leaves this CPU's TLB holding the stale translation. Stalker
|
||||
// flips a slab page RW then RX in place, so without this the freeze never takes
|
||||
// effect and executing the page faults with a permission abort.
|
||||
unsafe fn flush_tlb_range(address: u64, size: u64) {
|
||||
unsafe {
|
||||
let page_size = gum_query_page_size() as u64;
|
||||
let start = address & !(page_size - 1);
|
||||
let end = (address + size + page_size - 1) & !(page_size - 1);
|
||||
|
||||
core::arch::asm!("dsb ish", options(nostack, preserves_flags));
|
||||
let mut va = start;
|
||||
while va < end {
|
||||
core::arch::asm!("tlbi vaae1is, {operand}", operand = in(reg) va >> 12,
|
||||
options(nostack, preserves_flags));
|
||||
va += page_size;
|
||||
}
|
||||
core::arch::asm!("dsb ish", "isb", options(nostack, preserves_flags));
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_allocate(
|
||||
_address: gpointer,
|
||||
size: gsize,
|
||||
_alignment: gsize,
|
||||
prot: GumPageProtection,
|
||||
) -> gpointer {
|
||||
let ptr = crate::xnu::kalloc(size as usize);
|
||||
unsafe {
|
||||
core::ptr::write_bytes(ptr, 0, size as usize);
|
||||
if (prot & _GumPageProtection_GUM_PAGE_EXECUTE) != 0 {
|
||||
register_slab(ptr as u64, size as usize);
|
||||
gum_mprotect(ptr as gpointer, size, prot);
|
||||
}
|
||||
}
|
||||
ptr as gpointer
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_free(address: gpointer, size: gsize) -> gboolean {
|
||||
// Executable slabs were flipped to RX in the page tables; restore RW before returning them to
|
||||
// the allocator, otherwise the reclaimed pages stay non-writable and the next consumer faults.
|
||||
if is_agent_slab(address as u64) {
|
||||
unsafe {
|
||||
gum_mprotect(
|
||||
address,
|
||||
size,
|
||||
(_GumPageProtection_GUM_PAGE_READ | _GumPageProtection_GUM_PAGE_WRITE)
|
||||
as GumPageProtection,
|
||||
);
|
||||
}
|
||||
unregister_slab(address as u64);
|
||||
}
|
||||
crate::xnu::free(address as *mut u8, size as usize);
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_clear_cache(address: gpointer, size: gsize) {
|
||||
unsafe {
|
||||
@@ -324,40 +76,6 @@ pub extern "C" fn gum_tls_key_set_value(key: GumTlsKey, value: gpointer) {
|
||||
gthread::g_private_set(key as *mut GPrivate, value)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_barebone_on_registry_activating(registry: *mut GumModuleRegistry) {
|
||||
let kernel_base = crate::xnu::get_kernel_base();
|
||||
|
||||
unsafe {
|
||||
let module_infos = core::ptr::addr_of!(crate::MODULE_INFO);
|
||||
let module_infos = &*module_infos;
|
||||
|
||||
let mut i = 0;
|
||||
for module_info in module_infos.iter() {
|
||||
let module_base = kernel_base + module_info.offset as u64;
|
||||
|
||||
let module_path = if i == 0 {
|
||||
"/System/Library/Kernels/kernel"
|
||||
} else {
|
||||
&format!(
|
||||
"/System/Library/Extensions/{}.kext/{}",
|
||||
module_info.name, module_info.name
|
||||
)
|
||||
};
|
||||
let module_range = GumMemoryRange {
|
||||
base_address: module_base,
|
||||
size: module_info.size as u64,
|
||||
};
|
||||
|
||||
let module = gum_native_module_new(&module_path, &module_info.version, &module_range);
|
||||
gum_barebone_register_module(registry, module);
|
||||
g_object_unref(module as gpointer);
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct GumNativeModule {
|
||||
parent: GObject,
|
||||
@@ -405,7 +123,7 @@ fn gum_native_module_get_type() -> GType {
|
||||
interface_data: core::ptr::null_mut(),
|
||||
};
|
||||
|
||||
g_type_add_interface_static(new_type, gum_module_get_type(), &interface_info);
|
||||
g_type_add_interface_static(new_type, crate::bindings::gum_module_get_type(), &interface_info);
|
||||
|
||||
g_once_init_leave(
|
||||
core::ptr::addr_of_mut!(GUM_NATIVE_MODULE_TYPE) as *mut ::core::ffi::c_void,
|
||||
@@ -448,7 +166,11 @@ unsafe extern "C" fn gum_native_module_finalize(object: *mut GObject) {
|
||||
}
|
||||
}
|
||||
|
||||
fn gum_native_module_new(path: &str, version: &str, range: &GumMemoryRange) -> *mut GumModule {
|
||||
pub(crate) fn gum_native_module_new(
|
||||
path: &str,
|
||||
version: &str,
|
||||
range: &GumMemoryRange,
|
||||
) -> *mut GumModule {
|
||||
unsafe {
|
||||
let path_cstr = CString::new(path).unwrap();
|
||||
let version_cstr = CString::new(version).unwrap();
|
||||
@@ -499,134 +221,75 @@ unsafe extern "C" fn gum_native_module_enumerate_exports(
|
||||
) {
|
||||
unsafe {
|
||||
let module = self_ as *mut GumNativeModule;
|
||||
let module_range = &(*module).range;
|
||||
|
||||
let symbol_table = core::ptr::addr_of!(crate::SYMBOL_TABLE);
|
||||
let symbol_table = &*symbol_table;
|
||||
|
||||
const N_EXT: u8 = 0x01; // External symbol flag
|
||||
const N_TYPE: u8 = 0x0e; // Type mask
|
||||
const N_SECT: u8 = 0x0e; // Defined in section
|
||||
|
||||
let start_address = module_range.base_address;
|
||||
let end_address = module_range.base_address + module_range.size;
|
||||
|
||||
for symbol_ref in symbol_table.iter_symbols_in_range(start_address, end_address) {
|
||||
let is_external = (symbol_ref.symbol_type() & N_EXT) != 0;
|
||||
let is_defined = (symbol_ref.symbol_type() & N_TYPE) == N_SECT;
|
||||
if !is_external || !is_defined {
|
||||
continue;
|
||||
}
|
||||
|
||||
let export_type = GumExportType_GUM_EXPORT_FUNCTION;
|
||||
let range = (*module).range;
|
||||
|
||||
let mut on_export = |name: *const gchar, address: u64| {
|
||||
let export_details = GumExportDetails {
|
||||
type_: export_type,
|
||||
name: g_strdup(symbol_ref.name_ptr()),
|
||||
address: symbol_ref.address(),
|
||||
type_: GumExportType_GUM_EXPORT_FUNCTION,
|
||||
name: g_strdup(name),
|
||||
address,
|
||||
size: -1,
|
||||
};
|
||||
|
||||
let should_continue =
|
||||
func.unwrap()(&export_details as *const GumExportDetails, user_data);
|
||||
if should_continue == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
func.unwrap()(&export_details as *const GumExportDetails, user_data) != 0
|
||||
};
|
||||
|
||||
enumerate_exports_in_range(
|
||||
range.base_address,
|
||||
range.base_address + range.size,
|
||||
&mut on_export,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_symbol_details_from_address(
|
||||
address: gpointer,
|
||||
details: *mut GumDebugSymbolDetails,
|
||||
) -> gboolean {
|
||||
/// What the backends implement for `gum_native_module_enumerate_exports`: call
|
||||
/// `callback` for every exported function in `[start, end)`, stopping early when
|
||||
/// it returns false.
|
||||
pub(crate) type FoundExportCallback<'a> = dyn FnMut(*const gchar, u64) -> bool + 'a;
|
||||
|
||||
// Code slabs the agent allocates are normal writable RAM, so Gum can keep a real writable alias to
|
||||
// them across the slab's lifetime. Pre-existing kernel text is write-protected — CTRR-locked on
|
||||
// Apple silicon, STRICT_KERNEL_RWX on Linux — and can only be modified through a separate path, so
|
||||
// we treat it differently. We distinguish the two by tracking the executable ranges we hand out.
|
||||
static SLAB_LOCK: AtomicU32 = AtomicU32::new(0);
|
||||
static mut SLABS: Vec<(u64, u64)> = Vec::new();
|
||||
|
||||
fn slab_lock() {
|
||||
while SLAB_LOCK
|
||||
.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_err()
|
||||
{
|
||||
kernel::yield_now();
|
||||
}
|
||||
}
|
||||
|
||||
fn slab_unlock() {
|
||||
SLAB_LOCK.store(0, Ordering::Release);
|
||||
}
|
||||
|
||||
pub(crate) fn register_slab(start: u64, size: usize) {
|
||||
slab_lock();
|
||||
unsafe {
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
if let Some(symbol) = table.find_symbol_by_address(address as u64) {
|
||||
let name_bytes = symbol.name().as_bytes();
|
||||
let copy_len = core::cmp::min(name_bytes.len(), 2048);
|
||||
core::ptr::copy_nonoverlapping(
|
||||
name_bytes.as_ptr(),
|
||||
(*details).symbol_name.as_mut_ptr() as *mut u8,
|
||||
copy_len,
|
||||
);
|
||||
(*details).symbol_name[copy_len] = 0;
|
||||
|
||||
(*details).address = symbol.address();
|
||||
|
||||
(*details).module_name[0] = 0;
|
||||
(*details).file_name[0] = 0;
|
||||
(*details).line_number = 0;
|
||||
(*details).column = 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
0
|
||||
(*core::ptr::addr_of_mut!(SLABS)).push((start, start + size as u64));
|
||||
}
|
||||
slab_unlock();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_symbol_name_from_address(address: gpointer) -> *mut gchar {
|
||||
pub(crate) fn unregister_slab(start: u64) {
|
||||
slab_lock();
|
||||
unsafe {
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
let name_ptr = table.find_symbol_name_ptr_by_address(address as u64);
|
||||
g_strdup(name_ptr as *const gchar)
|
||||
(*core::ptr::addr_of_mut!(SLABS)).retain(|&(begin, _)| begin != start);
|
||||
}
|
||||
slab_unlock();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_function(name: *const gchar) -> gpointer {
|
||||
unsafe {
|
||||
let target_name = CStr::from_ptr(name).to_string_lossy();
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
|
||||
if let Some(symbol) = table.find_symbol_by_name(&target_name) {
|
||||
return symbol.address() as gpointer;
|
||||
}
|
||||
|
||||
core::ptr::null_mut()
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_functions_named(name: *const gchar) -> *mut GArray {
|
||||
unsafe {
|
||||
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
|
||||
|
||||
let target_name = CStr::from_ptr(name).to_string_lossy();
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
|
||||
let symbols = table.find_symbols_by_name(&target_name);
|
||||
for symbol in symbols {
|
||||
let addr = symbol.address() as gpointer;
|
||||
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
|
||||
}
|
||||
|
||||
array
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_functions_matching(pattern: *const gchar) -> *mut GArray {
|
||||
unsafe {
|
||||
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
|
||||
|
||||
let glob_pattern = CStr::from_ptr(pattern).to_string_lossy();
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
|
||||
let symbols = table.find_symbols_matching_glob(&glob_pattern);
|
||||
for symbol in symbols {
|
||||
let addr = symbol.address() as gpointer;
|
||||
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
|
||||
}
|
||||
|
||||
array
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_load_symbols(_path: *const gchar) -> gboolean {
|
||||
0
|
||||
pub(crate) fn is_agent_slab(address: u64) -> bool {
|
||||
slab_lock();
|
||||
let found = unsafe {
|
||||
(*core::ptr::addr_of!(SLABS))
|
||||
.iter()
|
||||
.any(|&(begin, end)| address >= begin && address < end)
|
||||
};
|
||||
slab_unlock();
|
||||
found
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// Linux half of Gum's platform backend. Everything is done locally: we are the
|
||||
// kernel, so page permissions go through set_memory_*(), writable aliases of
|
||||
// write-protected text come from vmap(), and symbols come from kallsyms.
|
||||
|
||||
use crate::{
|
||||
bindings::{
|
||||
_GumPageProtection_GUM_PAGE_EXECUTE, _GumPageProtection_GUM_PAGE_READ,
|
||||
_GumPageProtection_GUM_PAGE_WRITE, _GumRwxSupport_GUM_RWX_NONE, GArray,
|
||||
GumDebugSymbolDetails, GumMemoryRange, GumModuleRegistry, GumPageProtection, GumRwxSupport,
|
||||
g_array_append_vals, g_array_new, g_object_unref, g_pattern_spec_free, g_pattern_spec_match_string,
|
||||
g_pattern_spec_new, g_strdup, gboolean, gchar, gconstpointer, gpointer, gsize, guint,
|
||||
gum_barebone_register_module, gum_mprotect,
|
||||
},
|
||||
gum::{self, FoundExportCallback},
|
||||
kernel,
|
||||
};
|
||||
use alloc::format;
|
||||
use core::ffi::CStr;
|
||||
use core::ptr;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_query_rwx_support() -> GumRwxSupport {
|
||||
_GumRwxSupport_GUM_RWX_NONE
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_can_remap_writable() -> gboolean {
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_try_remap_writable_pages(
|
||||
first_page: gpointer,
|
||||
n_pages: guint,
|
||||
) -> gpointer {
|
||||
kernel::remap_writable(first_page as u64, n_pages) as gpointer
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_dispose_writable_pages(writable: gpointer, _n_pages: guint) {
|
||||
kernel::unmap_writable(writable as *mut core::ffi::c_void);
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_try_mprotect(
|
||||
address: gpointer,
|
||||
size: gsize,
|
||||
prot: GumPageProtection,
|
||||
) -> gboolean {
|
||||
if kernel::protect(address as u64, size as usize, prot as u32) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_allocate(
|
||||
_address: gpointer,
|
||||
size: gsize,
|
||||
_alignment: gsize,
|
||||
prot: GumPageProtection,
|
||||
) -> gpointer {
|
||||
let ptr = kernel::alloc_code(size as usize);
|
||||
unsafe {
|
||||
core::ptr::write_bytes(ptr, 0, size as usize);
|
||||
if (prot & _GumPageProtection_GUM_PAGE_EXECUTE) != 0 {
|
||||
gum::register_slab(ptr as u64, size as usize);
|
||||
gum_mprotect(ptr as gpointer, size, prot);
|
||||
}
|
||||
}
|
||||
|
||||
ptr as gpointer
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_free(address: gpointer, size: gsize) -> gboolean {
|
||||
// vfree() wants the range writable again: the pages go back to the vmalloc
|
||||
// allocator, and leaving them RX would fault the next consumer.
|
||||
if gum::is_agent_slab(address as u64) {
|
||||
unsafe {
|
||||
gum_mprotect(
|
||||
address,
|
||||
size,
|
||||
(_GumPageProtection_GUM_PAGE_READ | _GumPageProtection_GUM_PAGE_WRITE)
|
||||
as GumPageProtection,
|
||||
);
|
||||
}
|
||||
gum::unregister_slab(address as u64);
|
||||
}
|
||||
kernel::free_code(address as *mut u8, size as usize);
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_barebone_on_registry_activating(registry: *mut GumModuleRegistry) {
|
||||
unsafe {
|
||||
let kernel_base = kernel::get_kernel_base();
|
||||
if kernel_base != 0 {
|
||||
register_module(registry, "/boot/vmlinux", "", kernel_base, kernel::get_kernel_size());
|
||||
}
|
||||
|
||||
for m in kernel::enumerate_modules() {
|
||||
let path = format!("/lib/modules/{}.ko", m.name);
|
||||
register_module(registry, &path, &m.version, m.base, m.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn register_module(
|
||||
registry: *mut GumModuleRegistry,
|
||||
path: &str,
|
||||
version: &str,
|
||||
base: u64,
|
||||
size: u64,
|
||||
) {
|
||||
unsafe {
|
||||
let range = GumMemoryRange {
|
||||
base_address: base,
|
||||
size: size as gsize,
|
||||
};
|
||||
let module = gum::gum_native_module_new(path, version, &range);
|
||||
gum_barebone_register_module(registry, module);
|
||||
g_object_unref(module as gpointer);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn enumerate_exports_in_range(
|
||||
start_address: u64,
|
||||
end_address: u64,
|
||||
callback: &mut FoundExportCallback<'_>,
|
||||
) {
|
||||
kernel::enumerate_symbols(&mut |name, address| {
|
||||
if address < start_address || address >= end_address {
|
||||
return true;
|
||||
}
|
||||
callback(name.as_ptr() as *const gchar, address)
|
||||
});
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_symbol_details_from_address(
|
||||
address: gpointer,
|
||||
details: *mut GumDebugSymbolDetails,
|
||||
) -> gboolean {
|
||||
let Some((name, symbol_address)) = kernel::symbol_from_address(address as u64) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let name_bytes = name.as_bytes();
|
||||
let copy_len = core::cmp::min(name_bytes.len(), (*details).symbol_name.len() - 1);
|
||||
core::ptr::copy_nonoverlapping(
|
||||
name_bytes.as_ptr(),
|
||||
(*details).symbol_name.as_mut_ptr() as *mut u8,
|
||||
copy_len,
|
||||
);
|
||||
(*details).symbol_name[copy_len] = 0;
|
||||
|
||||
(*details).address = symbol_address;
|
||||
|
||||
(*details).module_name[0] = 0;
|
||||
(*details).file_name[0] = 0;
|
||||
(*details).line_number = 0;
|
||||
(*details).column = 0;
|
||||
}
|
||||
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_symbol_name_from_address(address: gpointer) -> *mut gchar {
|
||||
let Some((name, _)) = kernel::symbol_from_address(address as u64) else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
|
||||
let mut name = name;
|
||||
name.push('\0');
|
||||
unsafe { g_strdup(name.as_ptr() as *const gchar) }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_function(name: *const gchar) -> gpointer {
|
||||
let address = kernel::find_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
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
// XNU half of Gum's platform backend. Memory permissions and kernel-text writes
|
||||
// go out over the hostlink, because the guest CPU cannot perform them itself:
|
||||
// the host owns the page tables and the physical-memory bridge.
|
||||
|
||||
use crate::{
|
||||
FridaCommand,
|
||||
bindings::{
|
||||
_GumPageProtection_GUM_PAGE_EXECUTE, _GumPageProtection_GUM_PAGE_READ,
|
||||
_GumPageProtection_GUM_PAGE_WRITE, _GumRwxSupport_GUM_RWX_NONE, GArray,
|
||||
GumDebugSymbolDetails, GumMemoryRange, GumModuleRegistry, GumPageProtection, GumRwxSupport,
|
||||
g_array_append_vals, g_array_new, g_object_unref, g_strdup, g_variant_get_boolean,
|
||||
g_variant_get_uint64, g_variant_new, g_variant_new_fixed_array, g_variant_type_free,
|
||||
g_variant_type_new, g_variant_unref, gboolean, gchar, gconstpointer, gpointer, gsize, guint,
|
||||
gum_barebone_register_module,
|
||||
gum_barebone_try_remap_writable_pages as _gum_barebone_try_remap_writable_pages,
|
||||
gum_mprotect, gum_query_page_size,
|
||||
},
|
||||
gum::{self, FoundExportCallback},
|
||||
host_rpc, kernel, libc,
|
||||
};
|
||||
use alloc::format;
|
||||
use alloc::vec::Vec;
|
||||
use core::ffi::CStr;
|
||||
use core::mem::size_of;
|
||||
use core::ptr;
|
||||
|
||||
const SHADOW_MAGIC: u64 = 0x4644_4f48_5341_4853;
|
||||
const SHADOW_HEADER: usize = 24;
|
||||
const SHADOW_MIN_ADDRESS: u64 = 0xffff_f000_0000_0000;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_query_rwx_support() -> GumRwxSupport {
|
||||
_GumRwxSupport_GUM_RWX_NONE
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_can_remap_writable() -> gboolean {
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_try_remap_writable_pages(
|
||||
first_page: gpointer,
|
||||
n_pages: guint,
|
||||
) -> gpointer {
|
||||
if gum::is_agent_slab(first_page as u64) {
|
||||
return remap_agent_pages(first_page, n_pages);
|
||||
}
|
||||
shadow_kernel_pages(first_page, n_pages)
|
||||
}
|
||||
|
||||
fn remap_agent_pages(first_page: gpointer, n_pages: guint) -> gpointer {
|
||||
unsafe {
|
||||
let page_size = gum_query_page_size() as usize;
|
||||
let mut virtual_addrs = Vec::with_capacity(n_pages as usize);
|
||||
|
||||
let mut current_page = first_page as u64;
|
||||
for _ in 0..n_pages {
|
||||
virtual_addrs.push(current_page as gpointer);
|
||||
current_page += page_size as u64;
|
||||
}
|
||||
|
||||
_gum_barebone_try_remap_writable_pages(
|
||||
virtual_addrs.as_ptr() as *mut *const core::ffi::c_void,
|
||||
virtual_addrs.len() as guint,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn shadow_kernel_pages(first_page: gpointer, n_pages: guint) -> gpointer {
|
||||
unsafe {
|
||||
let total = n_pages as usize * gum_query_page_size() as usize;
|
||||
let buffer = kernel::alloc(SHADOW_HEADER + total);
|
||||
*(buffer as *mut u64) = SHADOW_MAGIC;
|
||||
*(buffer.add(8) as *mut u64) = first_page as u64;
|
||||
*(buffer.add(16) as *mut u32) = n_pages;
|
||||
|
||||
let body = buffer.add(SHADOW_HEADER);
|
||||
core::ptr::copy_nonoverlapping(first_page as *const u8, body, total);
|
||||
body as gpointer
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_dispose_writable_pages(writable: gpointer, _n_pages: guint) {
|
||||
if (writable as u64) < SHADOW_MIN_ADDRESS {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
let buffer = (writable as *mut u8).sub(SHADOW_HEADER);
|
||||
if *(buffer as *const u64) != SHADOW_MAGIC {
|
||||
return;
|
||||
}
|
||||
let first_page = *(buffer.add(8) as *const u64);
|
||||
let n_pages = *(buffer.add(16) as *const u32);
|
||||
let total = n_pages as usize * gum_query_page_size() as usize;
|
||||
|
||||
commit_kernel_patch(first_page, writable as *const u8, total);
|
||||
libc::__clear_cache(first_page as *const u8, (first_page + total as u64) as *const u8);
|
||||
|
||||
kernel::free(buffer, SHADOW_HEADER + total);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn commit_kernel_patch(address: u64, data: *const u8, len: usize) {
|
||||
unsafe {
|
||||
let element_type = g_variant_type_new(c"y".as_ptr());
|
||||
let bytes = g_variant_new_fixed_array(element_type, data as gconstpointer, len as gsize, 1);
|
||||
g_variant_type_free(element_type);
|
||||
|
||||
let payload = g_variant_new(c"(t@ay)".as_ptr(), address, bytes);
|
||||
let reply = host_rpc(FridaCommand::PatchCode, payload);
|
||||
g_variant_unref(reply);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_barebone_try_remap_writable_pages(
|
||||
addrs: *const gpointer,
|
||||
n_addrs: guint,
|
||||
) -> gpointer {
|
||||
if !crate::transport_is_up() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
unsafe {
|
||||
let element_type = g_variant_type_new(c"t".as_ptr());
|
||||
let payload = g_variant_new_fixed_array(
|
||||
element_type,
|
||||
addrs as gconstpointer,
|
||||
n_addrs as gsize,
|
||||
size_of::<u64>() as gsize,
|
||||
);
|
||||
g_variant_type_free(element_type);
|
||||
|
||||
let reply = host_rpc(FridaCommand::RemapWritablePages, payload);
|
||||
let virtual_address = g_variant_get_uint64(reply);
|
||||
g_variant_unref(reply);
|
||||
|
||||
virtual_address as gpointer
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_try_mprotect(
|
||||
address: gpointer,
|
||||
size: gsize,
|
||||
prot: GumPageProtection,
|
||||
) -> gboolean {
|
||||
if !crate::transport_is_up() {
|
||||
return 1;
|
||||
}
|
||||
unsafe {
|
||||
let payload = g_variant_new(c"(ttu)".as_ptr(), address as u64, size as u64, prot as u32);
|
||||
|
||||
let reply = host_rpc(FridaCommand::MemoryProtect, payload);
|
||||
let success = g_variant_get_boolean(reply);
|
||||
g_variant_unref(reply);
|
||||
|
||||
if success != 0 {
|
||||
flush_tlb_range(address as u64, size as u64);
|
||||
}
|
||||
|
||||
success
|
||||
}
|
||||
}
|
||||
|
||||
// The host rewrites our page-table descriptors through the physical-memory
|
||||
// bridge, which leaves this CPU's TLB holding the stale translation. Stalker
|
||||
// flips a slab page RW then RX in place, so without this the freeze never takes
|
||||
// effect and executing the page faults with a permission abort.
|
||||
unsafe fn flush_tlb_range(address: u64, size: u64) {
|
||||
unsafe {
|
||||
let page_size = gum_query_page_size() as u64;
|
||||
let start = address & !(page_size - 1);
|
||||
let end = (address + size + page_size - 1) & !(page_size - 1);
|
||||
|
||||
core::arch::asm!("dsb ish", options(nostack, preserves_flags));
|
||||
let mut va = start;
|
||||
while va < end {
|
||||
core::arch::asm!("tlbi vaae1is, {operand}", operand = in(reg) va >> 12,
|
||||
options(nostack, preserves_flags));
|
||||
va += page_size;
|
||||
}
|
||||
core::arch::asm!("dsb ish", "isb", options(nostack, preserves_flags));
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_allocate(
|
||||
_address: gpointer,
|
||||
size: gsize,
|
||||
_alignment: gsize,
|
||||
prot: GumPageProtection,
|
||||
) -> gpointer {
|
||||
let ptr = kernel::alloc_code(size as usize);
|
||||
unsafe {
|
||||
core::ptr::write_bytes(ptr, 0, size as usize);
|
||||
if (prot & _GumPageProtection_GUM_PAGE_EXECUTE) != 0 {
|
||||
gum::register_slab(ptr as u64, size as usize);
|
||||
gum_mprotect(ptr as gpointer, size, prot);
|
||||
}
|
||||
}
|
||||
ptr as gpointer
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_memory_free(address: gpointer, size: gsize) -> gboolean {
|
||||
// Executable slabs were flipped to RX in the page tables; restore RW before returning them to
|
||||
// the allocator, otherwise the reclaimed pages stay non-writable and the next consumer faults.
|
||||
if gum::is_agent_slab(address as u64) {
|
||||
unsafe {
|
||||
gum_mprotect(
|
||||
address,
|
||||
size,
|
||||
(_GumPageProtection_GUM_PAGE_READ | _GumPageProtection_GUM_PAGE_WRITE)
|
||||
as GumPageProtection,
|
||||
);
|
||||
}
|
||||
gum::unregister_slab(address as u64);
|
||||
}
|
||||
kernel::free_code(address as *mut u8, size as usize);
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_barebone_on_registry_activating(registry: *mut GumModuleRegistry) {
|
||||
let kernel_base = kernel::get_kernel_base();
|
||||
|
||||
unsafe {
|
||||
let module_infos = core::ptr::addr_of!(crate::MODULE_INFO);
|
||||
let module_infos = &*module_infos;
|
||||
|
||||
let mut i = 0;
|
||||
for module_info in module_infos.iter() {
|
||||
let module_base = kernel_base + module_info.offset as u64;
|
||||
|
||||
let module_path = if i == 0 {
|
||||
"/System/Library/Kernels/kernel"
|
||||
} else {
|
||||
&format!(
|
||||
"/System/Library/Extensions/{}.kext/{}",
|
||||
module_info.name, module_info.name
|
||||
)
|
||||
};
|
||||
let module_range = GumMemoryRange {
|
||||
base_address: module_base,
|
||||
size: module_info.size as u64,
|
||||
};
|
||||
|
||||
let module = gum::gum_native_module_new(&module_path, &module_info.version, &module_range);
|
||||
gum_barebone_register_module(registry, module);
|
||||
g_object_unref(module as gpointer);
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn enumerate_exports_in_range(
|
||||
start_address: u64,
|
||||
end_address: u64,
|
||||
callback: &mut FoundExportCallback<'_>,
|
||||
) {
|
||||
unsafe {
|
||||
let symbol_table = core::ptr::addr_of!(crate::SYMBOL_TABLE);
|
||||
let symbol_table = &*symbol_table;
|
||||
|
||||
const N_EXT: u8 = 0x01; // External symbol flag
|
||||
const N_TYPE: u8 = 0x0e; // Type mask
|
||||
const N_SECT: u8 = 0x0e; // Defined in section
|
||||
|
||||
for symbol_ref in symbol_table.iter_symbols_in_range(start_address, end_address) {
|
||||
let is_external = (symbol_ref.symbol_type() & N_EXT) != 0;
|
||||
let is_defined = (symbol_ref.symbol_type() & N_TYPE) == N_SECT;
|
||||
if !is_external || !is_defined {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !callback(symbol_ref.name_ptr(), symbol_ref.address()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_symbol_details_from_address(
|
||||
address: gpointer,
|
||||
details: *mut GumDebugSymbolDetails,
|
||||
) -> gboolean {
|
||||
unsafe {
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
if let Some(symbol) = table.find_symbol_by_address(address as u64) {
|
||||
let name_bytes = symbol.name().as_bytes();
|
||||
let copy_len = core::cmp::min(name_bytes.len(), 2048);
|
||||
core::ptr::copy_nonoverlapping(
|
||||
name_bytes.as_ptr(),
|
||||
(*details).symbol_name.as_mut_ptr() as *mut u8,
|
||||
copy_len,
|
||||
);
|
||||
(*details).symbol_name[copy_len] = 0;
|
||||
|
||||
(*details).address = symbol.address();
|
||||
|
||||
(*details).module_name[0] = 0;
|
||||
(*details).file_name[0] = 0;
|
||||
(*details).line_number = 0;
|
||||
(*details).column = 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_symbol_name_from_address(address: gpointer) -> *mut gchar {
|
||||
unsafe {
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
let name_ptr = table.find_symbol_name_ptr_by_address(address as u64);
|
||||
g_strdup(name_ptr as *const gchar)
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_function(name: *const gchar) -> gpointer {
|
||||
unsafe {
|
||||
let target_name = CStr::from_ptr(name).to_string_lossy();
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
|
||||
if let Some(symbol) = table.find_symbol_by_name(&target_name) {
|
||||
return symbol.address() as gpointer;
|
||||
}
|
||||
|
||||
core::ptr::null_mut()
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_functions_named(name: *const gchar) -> *mut GArray {
|
||||
unsafe {
|
||||
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
|
||||
|
||||
let target_name = CStr::from_ptr(name).to_string_lossy();
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
|
||||
let symbols = table.find_symbols_by_name(&target_name);
|
||||
for symbol in symbols {
|
||||
let addr = symbol.address() as gpointer;
|
||||
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
|
||||
}
|
||||
|
||||
array
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_find_functions_matching(pattern: *const gchar) -> *mut GArray {
|
||||
unsafe {
|
||||
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
|
||||
|
||||
let glob_pattern = CStr::from_ptr(pattern).to_string_lossy();
|
||||
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
|
||||
|
||||
let symbols = table.find_symbols_matching_glob(&glob_pattern);
|
||||
for symbol in symbols {
|
||||
let addr = symbol.address() as gpointer;
|
||||
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
|
||||
}
|
||||
|
||||
array
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gum_load_symbols(_path: *const gchar) -> gboolean {
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Linux transport: /dev/frida, framed the same way as the XNU hostlinks (4-byte
|
||||
// little-endian length prefix) so the host-side (yqv) protocol is identical across
|
||||
// every transport.
|
||||
//
|
||||
// The character device itself lives in linux/frida-kmod.c and hands us a byte
|
||||
// stream; whoever opens the device decides how it reaches the host.
|
||||
|
||||
use core::cell::UnsafeCell;
|
||||
use core::ffi::{c_int, c_void};
|
||||
|
||||
use alloc::vec::Vec;
|
||||
|
||||
struct Inner {
|
||||
rx_lenbuf: [u8; 4],
|
||||
rx_lenhave: usize,
|
||||
rx_buf: Vec<u8>,
|
||||
rx_have: usize,
|
||||
rx_need: usize,
|
||||
}
|
||||
|
||||
pub struct Hostlink {
|
||||
state: UnsafeCell<Inner>,
|
||||
on_rx: Option<fn(&[u8])>,
|
||||
}
|
||||
|
||||
unsafe impl Send for Hostlink {}
|
||||
|
||||
impl Hostlink {
|
||||
pub fn init(on_rx: Option<fn(&[u8])>) -> Result<Self, ()> {
|
||||
if unsafe { frida_kmod_link_open() } != 0 {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok(Hostlink {
|
||||
state: UnsafeCell::new(Inner {
|
||||
rx_lenbuf: [0; 4],
|
||||
rx_lenhave: 0,
|
||||
rx_buf: Vec::new(),
|
||||
rx_have: 0,
|
||||
rx_need: 0,
|
||||
}),
|
||||
on_rx,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send(&self, payload: &[u8]) {
|
||||
let header = (payload.len() as u32).to_le_bytes();
|
||||
unsafe {
|
||||
frida_kmod_link_send(header.as_ptr() as *const c_void, header.len());
|
||||
frida_kmod_link_send(payload.as_ptr() as *const c_void, payload.len());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process(&self) {
|
||||
let s = unsafe { &mut *self.state.get() };
|
||||
loop {
|
||||
if s.rx_lenhave < 4 {
|
||||
let lo = s.rx_lenhave;
|
||||
let n = recv_nonblocking(&mut s.rx_lenbuf[lo..4]);
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
s.rx_lenhave += n;
|
||||
if s.rx_lenhave < 4 {
|
||||
continue;
|
||||
}
|
||||
s.rx_need = u32::from_le_bytes(s.rx_lenbuf) as usize;
|
||||
s.rx_have = 0;
|
||||
s.rx_buf.resize(s.rx_need, 0);
|
||||
}
|
||||
|
||||
while s.rx_have < s.rx_need {
|
||||
let lo = s.rx_have;
|
||||
let hi = s.rx_need;
|
||||
let n = recv_nonblocking(&mut s.rx_buf[lo..hi]);
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
s.rx_have += n;
|
||||
}
|
||||
|
||||
// Detach the frame and reset receive state before dispatching: the callback may
|
||||
// re-enter process() (a synchronous host RPC issued while handling a command), and
|
||||
// it must start from a clean state instead of re-dispatching this same frame.
|
||||
let frame = core::mem::take(&mut s.rx_buf);
|
||||
let need = s.rx_need;
|
||||
s.rx_lenhave = 0;
|
||||
s.rx_have = 0;
|
||||
s.rx_need = 0;
|
||||
|
||||
if let Some(cb) = self.on_rx {
|
||||
cb(&frame[..need]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Hostlink {
|
||||
fn drop(&mut self) {
|
||||
unsafe { frida_kmod_link_close() };
|
||||
}
|
||||
}
|
||||
|
||||
fn recv_nonblocking(dst: &mut [u8]) -> usize {
|
||||
let n = unsafe { frida_kmod_link_recv(dst.as_mut_ptr() as *mut c_void, dst.len()) };
|
||||
if n <= 0 { 0 } else { n as usize }
|
||||
}
|
||||
|
||||
unsafe extern "C" {
|
||||
fn frida_kmod_link_open() -> c_int;
|
||||
fn frida_kmod_link_close();
|
||||
fn frida_kmod_link_send(data: *const c_void, size: usize) -> c_int;
|
||||
fn frida_kmod_link_recv(data: *mut c_void, size: usize) -> isize;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use core::ptr::{read_volatile, write_volatile};
|
||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crate::gum::gum_barebone_query_page_size;
|
||||
use crate::xnu;
|
||||
use crate::{kernel, 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 = xnu::kalloc(len);
|
||||
let va = kernel::alloc(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);
|
||||
xnu::free(p.va, len);
|
||||
kernel::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 = xnu::kalloc(total);
|
||||
let buf = kernel::alloc(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 = xnu::kalloc(size_of::<TxNode>()) as *mut TxNode;
|
||||
let node = kernel::alloc(size_of::<TxNode>()) as *mut TxNode;
|
||||
unsafe {
|
||||
(*node).next = core::ptr::null_mut();
|
||||
(*node).frame = frame;
|
||||
@@ -386,7 +386,7 @@ impl Hostlink {
|
||||
s.tx_tail = node;
|
||||
}
|
||||
|
||||
xnu::thread_wakeup(s.wake_token);
|
||||
kernel::wake(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 = xnu::kalloc(len);
|
||||
let buf = kernel::alloc(len);
|
||||
let slice: &'static mut [u8] =
|
||||
unsafe { core::slice::from_raw_parts_mut(buf, len) };
|
||||
s.rx_buf = Some(slice);
|
||||
@@ -718,8 +718,8 @@ impl Hostlink {
|
||||
self.kick(sel);
|
||||
}
|
||||
|
||||
xnu::free(frame.as_ptr() as *mut u8, frame.len());
|
||||
xnu::free(node as *mut u8, core::mem::size_of::<TxNode>());
|
||||
kernel::free(frame.as_ptr() as *mut u8, frame.len());
|
||||
kernel::free(node as *mut u8, core::mem::size_of::<TxNode>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
xnu::thread_wakeup(token as *const u8);
|
||||
kernel::wake(token as *const u8);
|
||||
}
|
||||
|
||||
fn r32(mmio: *mut u8, off: usize) -> u32 {
|
||||
|
||||
@@ -8,7 +8,7 @@ use core::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::xnu;
|
||||
use crate::kernel;
|
||||
|
||||
const AF_VSOCK: c_int = 40;
|
||||
const SOCK_STREAM: c_int = 1;
|
||||
@@ -262,6 +262,6 @@ unsafe fn recv_nonblocking(so: SocketT, dst: &mut [u8]) -> usize {
|
||||
unsafe extern "C" fn upcall(_so: SocketT, cookie: *mut c_void, _waitf: c_int) {
|
||||
UPCALL_PENDING.fetch_add(1, Ordering::Release);
|
||||
if !cookie.is_null() {
|
||||
xnu::thread_wakeup(cookie as *const u8);
|
||||
kernel::wake(cookie as *const u8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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);
|
||||
@@ -0,0 +1,997 @@
|
||||
#![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)
|
||||
}};
|
||||
}
|
||||
+540
-90
@@ -1,35 +1,64 @@
|
||||
// The C library's lower edge. Nothing here does real work: the agent has no
|
||||
// filesystem and never exits, so these exist to satisfy references and to give
|
||||
// malloc some memory to hand out.
|
||||
//
|
||||
// The two flavours link against different C libraries, and they disagree on what
|
||||
// this layer is called. newlib asks for the underscore-prefixed syscall names;
|
||||
// picolibc, which the soft-float flavour needs because newlib has no soft-float
|
||||
// build for arm64, calls them without the prefix. The bodies are shared and the
|
||||
// exported names live in the two `exports` modules at the end.
|
||||
|
||||
use core::ptr;
|
||||
use core::sync::atomic::Ordering;
|
||||
|
||||
use crate::bindings::{GMutex, GRecMutex};
|
||||
use crate::gthread;
|
||||
|
||||
static mut MALLOC_LOCK: GRecMutex = unsafe { core::mem::zeroed() };
|
||||
// Serialises the C library. Which hooks the library reaches for differs: newlib asks
|
||||
// for __malloc_lock/__malloc_unlock, picolibc for the retargetable lock API below,
|
||||
// whose default implementation is an empty function — so on picolibc malloc runs
|
||||
// unlocked unless we supply these, and any second thread corrupts the heap.
|
||||
static mut LIBC_LOCK: GRecMutex = unsafe { core::mem::zeroed() };
|
||||
|
||||
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 __malloc_lock(_reent: *mut core::ffi::c_void) {
|
||||
gthread::g_rec_mutex_lock(ptr::addr_of_mut!(MALLOC_LOCK));
|
||||
pub extern "C" fn sysconf(_name: i32) -> isize {
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __malloc_unlock(_reent: *mut core::ffi::c_void) {
|
||||
gthread::g_rec_mutex_unlock(ptr::addr_of_mut!(MALLOC_LOCK));
|
||||
pub extern "C" fn __clear_cache(_start: *const u8, _end: *const u8) {
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"ic iallu", // Invalidate all instruction caches
|
||||
"dsb sy", // Data synchronization barrier
|
||||
"isb", // Instruction synchronization barrier
|
||||
options(nomem, nostack),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _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 {
|
||||
fn sbrk_impl(incr: isize) -> *mut u8 {
|
||||
const HEAP_SIZE: usize = 32 * 1024 * 1024; // 32 MB
|
||||
|
||||
static mut HEAP_START: *mut u8 = ptr::null_mut();
|
||||
@@ -40,7 +69,7 @@ pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
|
||||
gthread::g_mutex_lock(ptr::addr_of_mut!(HEAP_MUTEX));
|
||||
|
||||
if HEAP_START.is_null() {
|
||||
HEAP_START = crate::xnu::kalloc(HEAP_SIZE);
|
||||
HEAP_START = crate::kernel::alloc(HEAP_SIZE);
|
||||
if HEAP_START.is_null() {
|
||||
gthread::g_mutex_unlock(ptr::addr_of_mut!(HEAP_MUTEX));
|
||||
panic!("Failed to allocate 32 MB heap");
|
||||
@@ -49,7 +78,8 @@ pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
|
||||
}
|
||||
|
||||
let prev_heap_current = HEAP_CURRENT;
|
||||
let new_heap_current = HEAP_CURRENT.add(incr as usize);
|
||||
// Signed: the allocator hands memory back by asking for a negative increment.
|
||||
let new_heap_current = HEAP_CURRENT.offset(incr);
|
||||
|
||||
if new_heap_current > HEAP_START.add(HEAP_SIZE) {
|
||||
gthread::g_mutex_unlock(ptr::addr_of_mut!(HEAP_MUTEX));
|
||||
@@ -65,20 +95,14 @@ pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _getpid() -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct Timeval {
|
||||
tv_sec: i64,
|
||||
tv_usec: i64,
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _gettimeofday(tp: *mut core::ffi::c_void, _tzp: *mut core::ffi::c_void) -> i32 {
|
||||
let (secs, microsecs) = crate::xnu::clock_get_calendar_microtime();
|
||||
fn gettimeofday_impl(tp: *mut core::ffi::c_void) -> i32 {
|
||||
let (secs, microsecs) = crate::kernel::wall_clock_micros();
|
||||
let timeval = Timeval {
|
||||
tv_sec: secs as i64,
|
||||
tv_usec: microsecs as i64,
|
||||
@@ -93,68 +117,494 @@ pub extern "C" fn _gettimeofday(tp: *mut core::ffi::c_void, _tzp: *mut core::ffi
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _isatty(_fd: i32) -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _open(_path: *const u8, _flags: i32, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _close(_fd: i32) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _read(_fd: i32, _buf: *mut u8, _count: usize) -> isize {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _write(_fd: i32, _buf: *const u8, count: usize) -> isize {
|
||||
fn write_impl(count: usize) -> isize {
|
||||
count as isize
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _lseek(_fd: i32, _offset: isize, _whence: i32) -> isize {
|
||||
0
|
||||
}
|
||||
#[cfg(feature = "xnu")]
|
||||
mod exports {
|
||||
use super::*;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _fstat(_fd: i32, _st: *mut core::ffi::c_void) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _link(_old: *const u8, _new: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _unlink(_path: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
// GLib's g_get_num_processors() probes this; report a single CPU so the
|
||||
// parallel scanners fall back to their inline path.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn sysconf(_name: i32) -> isize {
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __clear_cache(_start: *const u8, _end: *const u8) {
|
||||
// On AArch64, we need to flush the instruction cache
|
||||
// This is a simplified implementation for the kernel environment
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"ic iallu", // Invalidate all instruction caches
|
||||
"dsb sy", // Data synchronization barrier
|
||||
"isb", // Instruction synchronization barrier
|
||||
options(nomem, nostack),
|
||||
);
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __malloc_lock(_reent: *mut core::ffi::c_void) {
|
||||
libc_lock();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn __malloc_unlock(_reent: *mut core::ffi::c_void) {
|
||||
libc_unlock();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _fini() {}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _exit(_status: i32) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _kill(_pid: i32, _sig: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
|
||||
sbrk_impl(incr)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _getpid() -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _gettimeofday(
|
||||
tp: *mut core::ffi::c_void,
|
||||
_tzp: *mut core::ffi::c_void,
|
||||
) -> i32 {
|
||||
gettimeofday_impl(tp)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _isatty(_fd: i32) -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _open(_path: *const u8, _flags: i32, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _close(_fd: i32) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _read(_fd: i32, _buf: *mut u8, _count: usize) -> isize {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _write(_fd: i32, _buf: *const u8, count: usize) -> isize {
|
||||
write_impl(count)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _lseek(_fd: i32, _offset: isize, _whence: i32) -> isize {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _fstat(_fd: i32, _st: *mut core::ffi::c_void) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _stat(_path: *const u8, _st: *mut core::ffi::c_void) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _link(_old: *const u8, _new: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _unlink(_path: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn creat(_path: *const u8, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn access(_path: *const u8, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn chmod(_path: *const u8, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn chdir(_path: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn rmdir(_path: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gethostname(_name: *mut u8, _len: usize) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
// newlib wires these up for exit(), which we never reach: the agent runs until
|
||||
// the kernel goes down or the module is unloaded.
|
||||
#[unsafe(no_mangle)]
|
||||
pub static __libc_fini: Option<extern "C" fn()> = None;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub static __fini_array_start: [extern "C" fn(); 0] = [];
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub static __fini_array_end: [extern "C" fn(); 0] = [];
|
||||
}
|
||||
|
||||
#[cfg(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 open(_path: *const u8, _flags: i32, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn close(_fd: i32) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn read(_fd: i32, _buf: *mut u8, _count: usize) -> isize {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn write(_fd: i32, _buf: *const u8, count: usize) -> isize {
|
||||
write_impl(count)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn lseek(_fd: i32, _offset: isize, _whence: i32) -> isize {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn fstat(_fd: i32, _st: *mut core::ffi::c_void) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn stat(_path: *const u8, _st: *mut core::ffi::c_void) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn unlink(_path: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn rename(_old: *const u8, _new: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn access(_path: *const u8, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn chmod(_path: *const u8, _mode: i32) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn chdir(_path: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn rmdir(_path: *const u8) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gethostname(_name: *mut u8, _len: usize) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Timespec {
|
||||
tv_sec: i64,
|
||||
tv_nsec: i64,
|
||||
}
|
||||
|
||||
const CLOCK_MONOTONIC: i32 = 1;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn clock_gettime(clock_id: i32, tp: *mut Timespec) -> i32 {
|
||||
let (secs, nsecs) = if clock_id == CLOCK_MONOTONIC {
|
||||
let micros = crate::kernel::monotonic_micros();
|
||||
(micros / 1_000_000, (micros % 1_000_000) * 1000)
|
||||
} else {
|
||||
let (secs, micros) = crate::kernel::wall_clock_micros();
|
||||
(secs as i64, (micros as i64) * 1000)
|
||||
};
|
||||
|
||||
unsafe {
|
||||
(*tp).tv_sec = secs;
|
||||
(*tp).tv_nsec = nsecs;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
// picolibc consults this for errno values it does not know itself.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn _user_strerror(_errnum: i32) -> *const core::ffi::c_char {
|
||||
ptr::null()
|
||||
}
|
||||
|
||||
// No dynamic loader here. Gum calls dladdr() to describe an address and already
|
||||
// falls back to printing it raw when the lookup fails.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn dladdr(_address: *const core::ffi::c_void, _info: *mut core::ffi::c_void) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn dlopen(_file: *const u8, _mode: i32) -> *mut core::ffi::c_void {
|
||||
ptr::null_mut()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn dlsym(
|
||||
_handle: *mut core::ffi::c_void,
|
||||
_name: *const u8,
|
||||
) -> *mut core::ffi::c_void {
|
||||
ptr::null_mut()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn dlclose(_handle: *mut core::ffi::c_void) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn dlerror() -> *mut core::ffi::c_char {
|
||||
ptr::null_mut()
|
||||
}
|
||||
|
||||
// picolibc declares the standard streams and leaves defining them to the
|
||||
// application. The agent has no I/O — its logging goes to the kernel's own log —
|
||||
// so nothing should ever reach for one of these.
|
||||
#[repr(transparent)]
|
||||
pub struct Stream(*mut core::ffi::c_void);
|
||||
|
||||
unsafe impl Sync for Stream {}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub static stdin: Stream = Stream(ptr::null_mut());
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub static stdout: Stream = Stream(ptr::null_mut());
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub static stderr: Stream = Stream(ptr::null_mut());
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
// 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;
|
||||
}
|
||||
+10
-691
@@ -1,693 +1,12 @@
|
||||
#![no_main]
|
||||
// The XNU flavour ships as a freestanding ELF executable: the host injects the
|
||||
// image and calls its `_start`. All of the agent lives in the library crate;
|
||||
// this target exists only to produce that link. build.rs passes `--undefined
|
||||
// _start` so the entrypoint survives --gc-sections.
|
||||
//
|
||||
// The Linux flavour is built as a static library instead and linked into a
|
||||
// kernel module by kbuild — see linux/Kbuild.
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::alloc::{GlobalAlloc, Layout};
|
||||
use core::ffi::{CStr, c_void};
|
||||
use core::ptr;
|
||||
use core::ptr::null_mut;
|
||||
use core::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use bindings::{
|
||||
GBytes, GCancellable, GError, GVariant, GVariantIter, GumScript, g_error_free, g_free,
|
||||
g_main_context_default, g_main_context_iteration, g_memdup2, g_object_unref,
|
||||
g_variant_check_format_string, g_variant_get, g_variant_get_byte, g_variant_get_child_value,
|
||||
g_variant_get_data, g_variant_get_size, g_variant_get_string, g_variant_get_uint32,
|
||||
g_variant_get_uint64, g_variant_get_variant, g_variant_iter_init, g_variant_iter_next,
|
||||
g_variant_new, g_variant_new_from_data, g_variant_new_string, g_variant_new_tuple,
|
||||
g_variant_new_uint32, g_variant_type_free, g_variant_type_new, g_variant_unref, gchar,
|
||||
gpointer, gsize, gum_script_backend_create_sync, gum_script_backend_obtain_qjs,
|
||||
gum_script_get_stalker, gum_script_load_sync, gum_script_post, gum_script_set_message_handler,
|
||||
gum_script_unload_sync, gum_stalker_exclude, GumMemoryRange,
|
||||
};
|
||||
use symbols::SymbolTable;
|
||||
|
||||
mod ffi;
|
||||
mod glib;
|
||||
mod gthread;
|
||||
mod gum;
|
||||
mod hostlink_virtio;
|
||||
mod hostlink_vsock;
|
||||
|
||||
mod libc;
|
||||
mod pac;
|
||||
mod symbols;
|
||||
mod xnu;
|
||||
|
||||
mod bindings {
|
||||
#![allow(
|
||||
dead_code,
|
||||
improper_ctypes,
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
non_upper_case_globals,
|
||||
unused_imports
|
||||
)]
|
||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||
pub enum FridaCommand {
|
||||
CreateScript = 1,
|
||||
LoadScript = 2,
|
||||
DestroyScript = 3,
|
||||
PostScriptMessage = 4,
|
||||
RemapWritablePages = 5,
|
||||
MemoryProtect = 6,
|
||||
PatchCode = 7,
|
||||
|
||||
Reply = 128,
|
||||
ScriptMessage = 129,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for FridaCommand {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
FridaCommand::CreateScript => write!(f, "CreateScript"),
|
||||
FridaCommand::LoadScript => write!(f, "LoadScript"),
|
||||
FridaCommand::DestroyScript => write!(f, "DestroyScript"),
|
||||
FridaCommand::PostScriptMessage => write!(f, "PostScriptMessage"),
|
||||
FridaCommand::RemapWritablePages => write!(f, "RemapWritablePages"),
|
||||
FridaCommand::MemoryProtect => write!(f, "MemoryProtect"),
|
||||
FridaCommand::PatchCode => write!(f, "PatchCode"),
|
||||
FridaCommand::Reply => write!(f, "Reply"),
|
||||
FridaCommand::ScriptMessage => write!(f, "ScriptMessage"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct HandlerResponse {
|
||||
variant: *mut GVariant,
|
||||
}
|
||||
|
||||
impl HandlerResponse {
|
||||
fn success(variant: *mut GVariant) -> Self {
|
||||
Self { variant }
|
||||
}
|
||||
|
||||
fn success_empty() -> Self {
|
||||
let variant = unsafe { g_variant_new_tuple(ptr::null(), 0) };
|
||||
Self { variant }
|
||||
}
|
||||
|
||||
fn error(message: &str) -> Self {
|
||||
let mut c_message = String::from(message);
|
||||
c_message.push('\0');
|
||||
let error_variant = unsafe { g_variant_new_string(c_message.as_ptr()) };
|
||||
|
||||
Self {
|
||||
variant: error_variant,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static mut CONFIG_DATA: &'static [u8] = &[];
|
||||
pub static mut MODULE_INFO: Vec<ModuleInfo> = Vec::new();
|
||||
pub static mut SYMBOL_TABLE: SymbolTable = SymbolTable::empty();
|
||||
|
||||
pub enum Transport {
|
||||
Virtio(hostlink_virtio::Hostlink),
|
||||
Vsock(hostlink_vsock::Hostlink),
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
pub fn send(&self, payload: &[u8]) {
|
||||
match self {
|
||||
Transport::Virtio(h) => h.send(payload),
|
||||
Transport::Vsock(h) => h.send(payload),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process(&self) {
|
||||
match self {
|
||||
Transport::Virtio(h) => h.process(),
|
||||
Transport::Vsock(h) => h.process(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum TransportConfig {
|
||||
Virtio { mmio: u64, irq: u32 },
|
||||
Vsock { host_port: u32 },
|
||||
}
|
||||
|
||||
static mut TRANSPORT_DRIVER: *mut Transport = core::ptr::null_mut();
|
||||
|
||||
#[inline(always)]
|
||||
fn transport_set(driver: Transport) {
|
||||
unsafe {
|
||||
let boxed = Box::into_raw(Box::new(driver));
|
||||
TRANSPORT_DRIVER = boxed;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn transport_get_unchecked() -> &'static Transport {
|
||||
unsafe {
|
||||
debug_assert!(!TRANSPORT_DRIVER.is_null());
|
||||
&*TRANSPORT_DRIVER
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn transport_is_up() -> bool {
|
||||
unsafe { !TRANSPORT_DRIVER.is_null() }
|
||||
}
|
||||
|
||||
static mut SCRIPTS: BTreeMap<u32, *mut GumScript> = BTreeMap::new();
|
||||
static NEXT_SCRIPT_ID: AtomicU32 = AtomicU32::new(1);
|
||||
static mut OWN_RANGE: GumMemoryRange = GumMemoryRange {
|
||||
base_address: 0,
|
||||
size: 0,
|
||||
};
|
||||
|
||||
static NEXT_REQUEST_ID: AtomicU32 = AtomicU32::new(1);
|
||||
static mut PENDING_REPLIES: BTreeMap<u16, *mut GVariant> = BTreeMap::new();
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModuleInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub offset: u32,
|
||||
pub size: u32,
|
||||
pub start_func_offset: u32,
|
||||
pub stop_func_offset: u32,
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn _start(config_data: *const u8, config_size: usize) {
|
||||
unsafe {
|
||||
xnu::io_log("frida: _start entry\n\0");
|
||||
CONFIG_DATA = core::slice::from_raw_parts(config_data, config_size);
|
||||
xnu::io_log("frida: _start config stored\n\0");
|
||||
|
||||
let _r = xnu::kernel_thread_start(frida_agent_worker, 12345usize as *mut core::ffi::c_void);
|
||||
}
|
||||
}
|
||||
|
||||
// KNOWN ISSUE (vphone research kernel): the worker panics during gum_init_embedded with
|
||||
// "debug exceptions enabled in kernel mode". The worker itself is healthy — it runs with
|
||||
// PSTATE.D=1 (debug masked) throughout. The panic is the research kernel's synchronous-
|
||||
// exception handler reacting to ANY exception the agent takes in kernel mode while the VZ
|
||||
// debug stub is active. Two confirmed sources: (1) the host's breakpoint-based Callbacks
|
||||
// patch BRK at gum_try_mprotect/remap, and the BRK is a debug exception; (2) a stray jump
|
||||
// into the agent's data region during GObject init (instruction abort) — looks like a
|
||||
// misapplied relocation for a function pointer stored in .data/.rodata.
|
||||
unsafe extern "C" fn frida_agent_worker(_parameter: *mut core::ffi::c_void, _wait_result: i32) {
|
||||
unsafe {
|
||||
xnu::io_log("frida: worker entry\n\0");
|
||||
bindings::g_set_panic_handler(Some(frida_panic_handler), ptr::null_mut());
|
||||
bindings::gum_init_embedded();
|
||||
xnu::io_log("frida: gum_init_embedded done\n\0");
|
||||
bindings::g_log_set_default_handler(Some(frida_log_handler), ptr::null_mut());
|
||||
|
||||
let (transport_config, kernel_base, module_info, symbol_table, own_range) =
|
||||
parse_config(core::ptr::addr_of!(CONFIG_DATA).read());
|
||||
xnu::set_kernel_base(kernel_base);
|
||||
MODULE_INFO = module_info;
|
||||
SYMBOL_TABLE = symbol_table;
|
||||
OWN_RANGE = own_range;
|
||||
xnu::io_log("frida: config parsed, init transport\n\0");
|
||||
|
||||
let wake_token = ptr::addr_of_mut!(glib::WAKEUP_TOKEN) as *const u8;
|
||||
let transport = match transport_config {
|
||||
TransportConfig::Virtio { mmio, irq } => Transport::Virtio(
|
||||
hostlink_virtio::Hostlink::init(mmio, irq, Some(on_frame_from_host), wake_token)
|
||||
.unwrap(),
|
||||
),
|
||||
TransportConfig::Vsock { host_port } => Transport::Vsock(
|
||||
hostlink_vsock::Hostlink::init(host_port, Some(on_frame_from_host), wake_token)
|
||||
.unwrap(),
|
||||
),
|
||||
};
|
||||
transport_set(transport);
|
||||
xnu::io_log("frida: transport up, entering main loop\n\0");
|
||||
|
||||
let main_context = g_main_context_default();
|
||||
|
||||
loop {
|
||||
transport_get_unchecked().process();
|
||||
g_main_context_iteration(main_context, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_frame_from_host(frame: &[u8]) {
|
||||
if let Some(variant) = deserialize_message(&frame) {
|
||||
process_incoming_message(variant);
|
||||
unsafe {
|
||||
g_variant_unref(variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn parse_config(
|
||||
config: &[u8],
|
||||
) -> (TransportConfig, u64, Vec<ModuleInfo>, SymbolTable, GumMemoryRange) {
|
||||
unsafe {
|
||||
let type_string = c"((tt)yvta(ssuuuu)ay)".as_ptr() as *const gchar;
|
||||
let variant_type = g_variant_type_new(type_string);
|
||||
|
||||
let root_variant = g_variant_new_from_data(
|
||||
variant_type,
|
||||
config.as_ptr() as *const core::ffi::c_void,
|
||||
config.len() as gsize,
|
||||
1,
|
||||
None,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
|
||||
let transport_kind_variant = g_variant_get_child_value(root_variant, 1);
|
||||
let transport_kind = g_variant_get_byte(transport_kind_variant);
|
||||
|
||||
let transport_cfg_outer = g_variant_get_child_value(root_variant, 2);
|
||||
let transport_cfg_inner = g_variant_get_variant(transport_cfg_outer);
|
||||
let transport_config = match transport_kind {
|
||||
0 => {
|
||||
let mmio_variant = g_variant_get_child_value(transport_cfg_inner, 0);
|
||||
let mmio = g_variant_get_uint64(mmio_variant);
|
||||
let irq_variant = g_variant_get_child_value(transport_cfg_inner, 1);
|
||||
let irq = g_variant_get_uint32(irq_variant);
|
||||
g_variant_unref(irq_variant);
|
||||
g_variant_unref(mmio_variant);
|
||||
TransportConfig::Virtio { mmio, irq }
|
||||
}
|
||||
1 => {
|
||||
let host_port = g_variant_get_uint32(transport_cfg_inner);
|
||||
TransportConfig::Vsock { host_port }
|
||||
}
|
||||
_ => panic!("Unsupported transport kind: {}", transport_kind),
|
||||
};
|
||||
g_variant_unref(transport_cfg_inner);
|
||||
g_variant_unref(transport_cfg_outer);
|
||||
g_variant_unref(transport_kind_variant);
|
||||
|
||||
let kernel_base_variant = g_variant_get_child_value(root_variant, 3);
|
||||
let kernel_base = g_variant_get_uint64(kernel_base_variant);
|
||||
xnu::set_kernel_base(kernel_base);
|
||||
|
||||
let module_info_variant = g_variant_get_child_value(root_variant, 4);
|
||||
let mut iter: GVariantIter = core::mem::zeroed();
|
||||
g_variant_iter_init(&mut iter as *mut GVariantIter, module_info_variant);
|
||||
|
||||
let mut module_info: Vec<ModuleInfo> = Vec::new();
|
||||
let mut raw_name: *mut gchar = null_mut();
|
||||
let mut raw_version: *mut gchar = null_mut();
|
||||
let mut offset: u32 = 0;
|
||||
let mut size: u32 = 0;
|
||||
let mut start_func_offset: u32 = 0;
|
||||
let mut stop_func_offset: u32 = 0;
|
||||
while g_variant_iter_next(
|
||||
&mut iter as *mut GVariantIter,
|
||||
c"(&s&suuuu)".as_ptr(),
|
||||
&mut raw_name,
|
||||
&mut raw_version,
|
||||
&mut offset,
|
||||
&mut size,
|
||||
&mut start_func_offset,
|
||||
&mut stop_func_offset,
|
||||
) != 0
|
||||
{
|
||||
module_info.push(ModuleInfo {
|
||||
name: String::from(CStr::from_ptr(raw_name).to_str().unwrap()),
|
||||
version: String::from(CStr::from_ptr(raw_version).to_str().unwrap()),
|
||||
offset,
|
||||
size,
|
||||
start_func_offset,
|
||||
stop_func_offset,
|
||||
});
|
||||
}
|
||||
|
||||
let symbol_array_variant = g_variant_get_child_value(root_variant, 5);
|
||||
let symbol_data_ptr = g_variant_get_data(symbol_array_variant) as *const u8;
|
||||
let symbol_data_size = g_variant_get_size(symbol_array_variant) as usize;
|
||||
let symbol_table = SymbolTable::new(core::slice::from_raw_parts(
|
||||
symbol_data_ptr,
|
||||
symbol_data_size,
|
||||
));
|
||||
|
||||
let own_range_variant = g_variant_get_child_value(root_variant, 0);
|
||||
let mut own_base: u64 = 0;
|
||||
let mut own_size: u64 = 0;
|
||||
g_variant_get(
|
||||
own_range_variant,
|
||||
c"(tt)".as_ptr(),
|
||||
&mut own_base,
|
||||
&mut own_size,
|
||||
);
|
||||
let own_range = GumMemoryRange {
|
||||
base_address: own_base,
|
||||
size: own_size as gsize,
|
||||
};
|
||||
|
||||
g_variant_unref(own_range_variant);
|
||||
g_variant_unref(symbol_array_variant);
|
||||
g_variant_unref(module_info_variant);
|
||||
g_variant_unref(kernel_base_variant);
|
||||
g_variant_unref(root_variant);
|
||||
g_variant_type_free(variant_type);
|
||||
|
||||
(transport_config, kernel_base, module_info, symbol_table, own_range)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn serialize_message(variant: *mut GVariant) -> Option<Vec<u8>> {
|
||||
unsafe {
|
||||
let size = g_variant_get_size(variant) as usize;
|
||||
if size == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let data_ptr = g_variant_get_data(variant) as *const u8;
|
||||
let mut result = Vec::with_capacity(size);
|
||||
result.resize(size, 0);
|
||||
core::ptr::copy_nonoverlapping(data_ptr, result.as_mut_ptr(), size);
|
||||
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_message(data: &[u8]) -> Option<*mut GVariant> {
|
||||
unsafe {
|
||||
if data.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let variant_type = g_variant_type_new(c"(yqv)".as_ptr());
|
||||
let data_copy = g_memdup2(data.as_ptr() as *const c_void, data.len() as u64);
|
||||
let variant = g_variant_new_from_data(
|
||||
variant_type,
|
||||
data_copy,
|
||||
data.len() as u64,
|
||||
0,
|
||||
Some(g_free),
|
||||
data_copy,
|
||||
);
|
||||
g_variant_type_free(variant_type);
|
||||
|
||||
if variant.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(variant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_incoming_message(variant: *mut GVariant) {
|
||||
{
|
||||
let mut cmd_value: u8 = 0;
|
||||
let mut request_id: u16 = 0;
|
||||
let mut payload_variant: *mut GVariant = ptr::null_mut();
|
||||
|
||||
let cmd = unsafe {
|
||||
g_variant_get(
|
||||
variant,
|
||||
c"(yqv)".as_ptr(),
|
||||
&mut cmd_value,
|
||||
&mut request_id,
|
||||
&mut payload_variant,
|
||||
);
|
||||
|
||||
core::mem::transmute::<u8, FridaCommand>(cmd_value)
|
||||
};
|
||||
|
||||
if cmd == FridaCommand::Reply {
|
||||
unsafe {
|
||||
core::ptr::addr_of_mut!(PENDING_REPLIES)
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.insert(request_id, payload_variant);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let response = match cmd {
|
||||
FridaCommand::CreateScript => handle_create_script(payload_variant),
|
||||
FridaCommand::LoadScript => handle_load_script(payload_variant),
|
||||
FridaCommand::DestroyScript => handle_destroy_script(payload_variant),
|
||||
FridaCommand::PostScriptMessage => handle_post_script_message(payload_variant),
|
||||
_ => HandlerResponse::error("Unknown command"),
|
||||
};
|
||||
|
||||
send_command_reply(request_id, response);
|
||||
|
||||
unsafe { g_variant_unref(payload_variant) };
|
||||
}
|
||||
}
|
||||
|
||||
fn send_command_reply(request_id: u16, response: HandlerResponse) {
|
||||
unsafe {
|
||||
let message = g_variant_new(
|
||||
c"(yqv)".as_ptr(),
|
||||
FridaCommand::Reply as u8 as u32,
|
||||
request_id as u32,
|
||||
response.variant,
|
||||
);
|
||||
|
||||
if let Some(serialized) = serialize_message(message) {
|
||||
transport_get_unchecked().send(&serialized);
|
||||
}
|
||||
|
||||
g_variant_unref(message);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn host_rpc(command: FridaCommand, payload: *mut GVariant) -> *mut GVariant {
|
||||
unsafe {
|
||||
let request_id = NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed) as u16;
|
||||
let message = g_variant_new(
|
||||
c"(yqv)".as_ptr(),
|
||||
command as u8 as u32,
|
||||
request_id as u32,
|
||||
payload,
|
||||
);
|
||||
let transport = transport_get_unchecked();
|
||||
transport.send(&serialize_message(message).unwrap());
|
||||
g_variant_unref(message);
|
||||
|
||||
let wait_event = ptr::addr_of_mut!(glib::WAKEUP_TOKEN) as *const u8;
|
||||
loop {
|
||||
xnu::assert_wait(wait_event, xnu::THREAD_INTERRUPTIBLE);
|
||||
transport.process();
|
||||
if let Some(reply) = take_pending_reply(request_id) {
|
||||
xnu::thread_wakeup(wait_event);
|
||||
return reply;
|
||||
}
|
||||
xnu::thread_block(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn take_pending_reply(request_id: u16) -> Option<*mut GVariant> {
|
||||
unsafe {
|
||||
core::ptr::addr_of_mut!(PENDING_REPLIES)
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.remove(&request_id)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_create_script(payload_variant: *mut GVariant) -> HandlerResponse {
|
||||
unsafe {
|
||||
if g_variant_check_format_string(payload_variant, c"s".as_ptr(), 0) == 0 {
|
||||
return HandlerResponse::error("Invalid payload format: expected string");
|
||||
}
|
||||
let source = g_variant_get_string(payload_variant, core::ptr::null_mut());
|
||||
|
||||
let backend = gum_script_backend_obtain_qjs();
|
||||
let cancellable: *mut GCancellable = ptr::null_mut();
|
||||
let mut error: *mut GError = ptr::null_mut();
|
||||
|
||||
let script = gum_script_backend_create_sync(
|
||||
backend,
|
||||
c"agent.js".as_ptr(),
|
||||
source,
|
||||
ptr::null_mut(),
|
||||
cancellable,
|
||||
&mut error,
|
||||
);
|
||||
|
||||
if !error.is_null() {
|
||||
let error_msg = CStr::from_ptr((*error).message).to_str().unwrap();
|
||||
let error_string = String::from(error_msg);
|
||||
g_error_free(error);
|
||||
return HandlerResponse::error(&error_string);
|
||||
}
|
||||
|
||||
exclude_own_range_from_stalker(script);
|
||||
|
||||
let script_id = NEXT_SCRIPT_ID.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let script_id_ptr = Box::into_raw(Box::new(script_id));
|
||||
gum_script_set_message_handler(
|
||||
script,
|
||||
Some(frida_message_handler),
|
||||
script_id_ptr as *mut c_void,
|
||||
None,
|
||||
);
|
||||
|
||||
core::ptr::addr_of_mut!(SCRIPTS)
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.insert(script_id, script);
|
||||
|
||||
HandlerResponse::success(g_variant_new_uint32(script_id))
|
||||
}
|
||||
}
|
||||
|
||||
// Stalker must not instrument our own runtime: when it follows the current
|
||||
// thread into the hostlink transport it would deadlock on the locks that the
|
||||
// RPC it depends on is holding. The host injected the image, so it tells us our
|
||||
// range as part of the config.
|
||||
unsafe fn exclude_own_range_from_stalker(script: *mut GumScript) {
|
||||
unsafe {
|
||||
gum_stalker_exclude(gum_script_get_stalker(script), core::ptr::addr_of!(OWN_RANGE));
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn frida_message_handler(
|
||||
message: *const gchar,
|
||||
_data: *mut GBytes,
|
||||
user_data: gpointer,
|
||||
) {
|
||||
unsafe {
|
||||
let script_id = *(user_data as *const u32);
|
||||
|
||||
let message_variant = g_variant_new(
|
||||
c"(yqv)".as_ptr(),
|
||||
FridaCommand::ScriptMessage as u8 as u32,
|
||||
0u32,
|
||||
g_variant_new(c"(us)".as_ptr(), script_id, message),
|
||||
);
|
||||
|
||||
if let Some(serialized) = serialize_message(message_variant) {
|
||||
transport_get_unchecked().send(&serialized);
|
||||
}
|
||||
|
||||
g_variant_unref(message_variant);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_load_script(payload_variant: *mut GVariant) -> HandlerResponse {
|
||||
unsafe {
|
||||
if g_variant_check_format_string(payload_variant, c"u".as_ptr(), 0) == 0 {
|
||||
return HandlerResponse::error("Invalid payload format: expected uint32");
|
||||
}
|
||||
let script_id = g_variant_get_uint32(payload_variant);
|
||||
|
||||
let Some(script) = get_script_by_id(script_id) else {
|
||||
return HandlerResponse::error(&format!("Script with ID {} not found", script_id));
|
||||
};
|
||||
|
||||
gum_script_load_sync(script, ptr::null_mut());
|
||||
|
||||
HandlerResponse::success_empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_destroy_script(payload_variant: *mut GVariant) -> HandlerResponse {
|
||||
unsafe {
|
||||
if g_variant_check_format_string(payload_variant, c"u".as_ptr(), 0) == 0 {
|
||||
return HandlerResponse::error("Invalid payload format: expected uint32");
|
||||
}
|
||||
let script_id = g_variant_get_uint32(payload_variant);
|
||||
|
||||
let scripts = core::ptr::addr_of_mut!(SCRIPTS).as_mut().unwrap();
|
||||
let Some(script) = scripts.remove(&script_id) else {
|
||||
return HandlerResponse::error(&format!("Script with ID {} not found", script_id));
|
||||
};
|
||||
|
||||
gum_script_unload_sync(script, ptr::null_mut());
|
||||
g_object_unref(script as *mut c_void);
|
||||
|
||||
HandlerResponse::success_empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_post_script_message(payload_variant: *mut GVariant) -> HandlerResponse {
|
||||
unsafe {
|
||||
if g_variant_check_format_string(payload_variant, c"(us)".as_ptr(), 0) == 0 {
|
||||
return HandlerResponse::error("Invalid payload format: expected (uint32, string)");
|
||||
}
|
||||
let mut script_id: u32 = 0;
|
||||
let mut message: *const gchar = ptr::null();
|
||||
g_variant_get(
|
||||
payload_variant,
|
||||
c"(u&s)".as_ptr(),
|
||||
&mut script_id,
|
||||
&mut message,
|
||||
);
|
||||
|
||||
let Some(script) = get_script_by_id(script_id) else {
|
||||
return HandlerResponse::error(&format!("Script with ID {} not found", script_id));
|
||||
};
|
||||
|
||||
gum_script_post(script, message, ptr::null_mut());
|
||||
|
||||
HandlerResponse::success_empty()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_script_by_id(script_id: u32) -> Option<*mut GumScript> {
|
||||
unsafe {
|
||||
let scripts = core::ptr::addr_of_mut!(SCRIPTS).as_mut().unwrap();
|
||||
scripts.get(&script_id).copied()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn frida_panic_handler(message: *const u8, _user_data: *mut core::ffi::c_void) {
|
||||
let msg = unsafe { core::ffi::CStr::from_ptr(message).to_str().unwrap() };
|
||||
panic!("[Frida] {}", msg);
|
||||
}
|
||||
|
||||
unsafe extern "C" fn frida_log_handler(
|
||||
_log_domain: *const core::ffi::c_char,
|
||||
_log_level: i32,
|
||||
message: *const core::ffi::c_char,
|
||||
_user_data: *mut core::ffi::c_void,
|
||||
) {
|
||||
let msg = unsafe { core::ffi::CStr::from_ptr(message).to_str().unwrap() };
|
||||
kprintln!("[Frida] {}", msg);
|
||||
}
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(info: &core::panic::PanicInfo<'_>) -> ! {
|
||||
let mut s = format!("{}", info);
|
||||
s.push('\0');
|
||||
xnu::panic(s.as_str());
|
||||
loop {}
|
||||
}
|
||||
|
||||
pub struct XnuAllocator;
|
||||
|
||||
unsafe impl GlobalAlloc for XnuAllocator {
|
||||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||
xnu::kalloc(layout.size())
|
||||
}
|
||||
|
||||
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||
xnu::free(ptr, layout.size());
|
||||
}
|
||||
}
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: XnuAllocator = XnuAllocator;
|
||||
extern crate alloc;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! kprintln {
|
||||
($($arg:tt)*) => {{
|
||||
use core::fmt::Write;
|
||||
let mut buf = alloc::string::String::new();
|
||||
write!(&mut buf, $($arg)*).unwrap();
|
||||
buf.push('\n');
|
||||
buf.push('\0');
|
||||
crate::xnu::io_log(&buf)
|
||||
}};
|
||||
}
|
||||
use frida_barebone_agent as _;
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::bindings::{
|
||||
GPatternSpec, g_pattern_spec_free, g_pattern_spec_match_string, g_pattern_spec_new,
|
||||
};
|
||||
use crate::xnu::get_kernel_base;
|
||||
use crate::kernel::get_kernel_base;
|
||||
use alloc::ffi::CString;
|
||||
use core::cmp::Ordering;
|
||||
use core::ffi::{CStr, c_char};
|
||||
|
||||
+119
-51
@@ -1,7 +1,83 @@
|
||||
use core::ffi::c_void;
|
||||
use core::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
static KERNEL_BASE: AtomicU64 = AtomicU64::new(0);
|
||||
use crate::kernel::ThreadEntry;
|
||||
|
||||
pub fn log(msg: &str) {
|
||||
unsafe {
|
||||
_IOLog(msg.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn panic(msg: &str) {
|
||||
unsafe { _panic(msg.as_ptr()) };
|
||||
}
|
||||
|
||||
pub fn spawn_thread(entry: ThreadEntry, parameter: *mut c_void) -> isize {
|
||||
kernel_thread_start(entry, parameter)
|
||||
}
|
||||
|
||||
pub fn alloc(size: usize) -> *mut u8 {
|
||||
kalloc(size)
|
||||
}
|
||||
|
||||
// Executable slabs come out of the same allocator; the host flips their page
|
||||
// permissions for us through its physical-memory bridge.
|
||||
pub fn alloc_code(size: usize) -> *mut u8 {
|
||||
kalloc(size)
|
||||
}
|
||||
|
||||
pub fn free_code(ptr: *mut u8, size: usize) {
|
||||
free(ptr, size);
|
||||
}
|
||||
|
||||
// Arms the wait, gives `check` a chance to observe the condition, and only then
|
||||
// commits to sleeping — the three-phase Mach protocol, which is race-free
|
||||
// because a wakeup landing after assert_wait() cancels the pending block.
|
||||
pub fn wait(token: *const u8, timeout_us: Option<u64>, check: &mut dyn FnMut() -> bool) {
|
||||
let wait_result = match timeout_us {
|
||||
None => assert_wait(token, THREAD_INTERRUPTIBLE),
|
||||
Some(us) => assert_wait_timeout(token, THREAD_INTERRUPTIBLE, (us * 1000) as u32, 1),
|
||||
};
|
||||
if wait_result != THREAD_WAITING {
|
||||
panic!("assert_wait failed: {}", wait_result);
|
||||
}
|
||||
|
||||
if check() {
|
||||
thread_wakeup(token);
|
||||
return;
|
||||
}
|
||||
|
||||
thread_block(None);
|
||||
}
|
||||
|
||||
pub fn wake(token: *const u8) {
|
||||
thread_wakeup(token);
|
||||
}
|
||||
|
||||
// XNU reschedules a kernel thread that stays runnable, so there is nothing this has
|
||||
// to do here. It exists because Linux does not forgive a loop that never yields.
|
||||
pub fn yield_now() {}
|
||||
|
||||
pub fn monotonic_micros() -> i64 {
|
||||
(absolutetime_to_nanoseconds(mach_absolute_time()) / 1000) as i64
|
||||
}
|
||||
|
||||
pub fn wall_clock_micros() -> (u32, u32) {
|
||||
clock_get_calendar_microtime()
|
||||
}
|
||||
|
||||
// The thread pointer is exposed to JavaScript as a GumThreadId, so it must fit
|
||||
// in a double without losing precision; the low bits keep it unique per thread.
|
||||
const JS_SAFE_THREAD_ID_MASK: u64 = (1 << 48) - 1;
|
||||
|
||||
pub fn current_thread_id() -> u64 {
|
||||
let thread_ptr: u64;
|
||||
unsafe {
|
||||
core::arch::asm!("mrs {}, tpidr_el1", out(reg) thread_ptr, options(nomem, nostack));
|
||||
}
|
||||
thread_ptr & JS_SAFE_THREAD_ID_MASK
|
||||
}
|
||||
|
||||
pub fn get_kernel_base() -> u64 {
|
||||
KERNEL_BASE.load(Ordering::Relaxed)
|
||||
@@ -11,54 +87,7 @@ pub fn set_kernel_base(base: u64) {
|
||||
KERNEL_BASE.store(base, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
type ContinuationFn = unsafe extern "C" fn(_parameter: *mut c_void, _wait_result: i32);
|
||||
|
||||
unsafe extern "C" {
|
||||
static _panic: unsafe extern "C" fn(*const u8);
|
||||
static _IOLog: unsafe extern "C" fn(*const u8, ...);
|
||||
// QEMU XNU exports plain kalloc/kfree; iOS XNU exports kalloc_data/kfree_data
|
||||
// (modern data-typed allocator KPIs). The host fills in whichever pair the
|
||||
// running kernel provides; the other slot stays null.
|
||||
static _kalloc: Option<unsafe extern "C" fn(usize) -> *mut u8>;
|
||||
static _kfree: Option<unsafe extern "C" fn(*mut u8, usize) -> *mut u8>;
|
||||
static _kalloc_data: Option<unsafe extern "C" fn(usize, u32) -> *mut u8>;
|
||||
static _kfree_data: Option<unsafe extern "C" fn(*mut u8, usize)>;
|
||||
static _kernel_thread_start:
|
||||
unsafe extern "C" fn(*const (), *mut c_void, *mut *mut c_void) -> isize;
|
||||
static _assert_wait: unsafe extern "C" fn(*const u8, u32) -> i32;
|
||||
static _assert_wait_timeout: unsafe extern "C" fn(*const u8, u32, u32, u32) -> i32;
|
||||
static _thread_block: unsafe extern "C" fn(Option<ContinuationFn>) -> i32;
|
||||
// QEMU XNU exports thread_wakeup; iOS XNU exports the BSD-style wakeup wrapper.
|
||||
static _thread_wakeup: Option<unsafe extern "C" fn(*const u8) -> i32>;
|
||||
static _wakeup: Option<unsafe extern "C" fn(*const u8)>;
|
||||
static _mach_absolute_time: unsafe extern "C" fn() -> u64;
|
||||
static _absolutetime_to_nanoseconds: unsafe extern "C" fn(u64, *mut u64);
|
||||
static _clock_get_calendar_microtime: unsafe extern "C" fn(*mut u32, *mut u32);
|
||||
static _ml_io_map: unsafe extern "C" fn(u64, u64) -> *mut c_void;
|
||||
static _ml_vtophys: unsafe extern "C" fn(u64) -> u64;
|
||||
static __ZN9IOService11getPlatformEv: unsafe extern "C" fn() -> *mut c_void;
|
||||
static __ZN9IOServiceC2Ev: unsafe extern "C" fn(*mut core::ffi::c_void);
|
||||
static __ZN8OSSymbol17withCStringNoCopyEPKc:
|
||||
unsafe extern "C" fn(*const core::ffi::c_char) -> *const OSSymbol;
|
||||
static __ZN6OSData9withBytesEPKvj:
|
||||
unsafe extern "C" fn(*const core::ffi::c_void, u32) -> *mut OSData;
|
||||
}
|
||||
|
||||
const IO_SERVICE_VTABLE_LENGTH: isize = 168;
|
||||
|
||||
const VT_LOOKUP_IC: isize = IO_SERVICE_VTABLE_LENGTH + 25; // IOPlatformExpert
|
||||
const VT_REGISTER_INT: isize = IO_SERVICE_VTABLE_LENGTH + 0; // IOInterruptController
|
||||
const VT_ENABLE_INT: isize = IO_SERVICE_VTABLE_LENGTH + 3; // IOInterruptController
|
||||
|
||||
pub fn panic(msg: &str) {
|
||||
unsafe { _panic(msg.as_ptr()) };
|
||||
}
|
||||
|
||||
pub fn io_log(msg: &str) {
|
||||
unsafe {
|
||||
_IOLog(msg.as_ptr());
|
||||
}
|
||||
}
|
||||
static KERNEL_BASE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
// Z_WAITOK (modern XNU): block until allocation succeeds.
|
||||
const Z_WAITOK: u32 = 0x0200;
|
||||
@@ -98,8 +127,8 @@ pub fn kernel_thread_start(continuation: ContinuationFn, thread_parameter: *mut
|
||||
};
|
||||
}
|
||||
|
||||
pub const THREAD_INTERRUPTIBLE: u32 = 1;
|
||||
pub const THREAD_WAITING: i32 = -1;
|
||||
const THREAD_INTERRUPTIBLE: u32 = 1;
|
||||
const THREAD_WAITING: i32 = -1;
|
||||
|
||||
pub fn assert_wait(event: *const u8, interruptible: u32) -> i32 {
|
||||
unsafe { _assert_wait(event, interruptible) }
|
||||
@@ -268,3 +297,42 @@ where
|
||||
let entry_ptr = unsafe { crate::pac::ptrauth_strip_data(entry as *const u8) };
|
||||
unsafe { core::mem::transmute_copy::<*const u8, T>(&entry_ptr) }
|
||||
}
|
||||
|
||||
const IO_SERVICE_VTABLE_LENGTH: isize = 168;
|
||||
|
||||
const VT_LOOKUP_IC: isize = IO_SERVICE_VTABLE_LENGTH + 25; // IOPlatformExpert
|
||||
const VT_REGISTER_INT: isize = IO_SERVICE_VTABLE_LENGTH + 0; // IOInterruptController
|
||||
const VT_ENABLE_INT: isize = IO_SERVICE_VTABLE_LENGTH + 3; // IOInterruptController
|
||||
|
||||
type ContinuationFn = ThreadEntry;
|
||||
|
||||
unsafe extern "C" {
|
||||
static _panic: unsafe extern "C" fn(*const u8);
|
||||
static _IOLog: unsafe extern "C" fn(*const u8, ...);
|
||||
// QEMU XNU exports plain kalloc/kfree; iOS XNU exports kalloc_data/kfree_data
|
||||
// (modern data-typed allocator KPIs). The host fills in whichever pair the
|
||||
// running kernel provides; the other slot stays null.
|
||||
static _kalloc: Option<unsafe extern "C" fn(usize) -> *mut u8>;
|
||||
static _kfree: Option<unsafe extern "C" fn(*mut u8, usize) -> *mut u8>;
|
||||
static _kalloc_data: Option<unsafe extern "C" fn(usize, u32) -> *mut u8>;
|
||||
static _kfree_data: Option<unsafe extern "C" fn(*mut u8, usize)>;
|
||||
static _kernel_thread_start:
|
||||
unsafe extern "C" fn(*const (), *mut c_void, *mut *mut c_void) -> isize;
|
||||
static _assert_wait: unsafe extern "C" fn(*const u8, u32) -> i32;
|
||||
static _assert_wait_timeout: unsafe extern "C" fn(*const u8, u32, u32, u32) -> i32;
|
||||
static _thread_block: unsafe extern "C" fn(Option<ContinuationFn>) -> i32;
|
||||
// QEMU XNU exports thread_wakeup; iOS XNU exports the BSD-style wakeup wrapper.
|
||||
static _thread_wakeup: Option<unsafe extern "C" fn(*const u8) -> i32>;
|
||||
static _wakeup: Option<unsafe extern "C" fn(*const u8)>;
|
||||
static _mach_absolute_time: unsafe extern "C" fn() -> u64;
|
||||
static _absolutetime_to_nanoseconds: unsafe extern "C" fn(u64, *mut u64);
|
||||
static _clock_get_calendar_microtime: unsafe extern "C" fn(*mut u32, *mut u32);
|
||||
static _ml_io_map: unsafe extern "C" fn(u64, u64) -> *mut c_void;
|
||||
static _ml_vtophys: unsafe extern "C" fn(u64) -> u64;
|
||||
static __ZN9IOService11getPlatformEv: unsafe extern "C" fn() -> *mut c_void;
|
||||
static __ZN9IOServiceC2Ev: unsafe extern "C" fn(*mut core::ffi::c_void);
|
||||
static __ZN8OSSymbol17withCStringNoCopyEPKc:
|
||||
unsafe extern "C" fn(*const core::ffi::c_char) -> *const OSSymbol;
|
||||
static __ZN6OSData9withBytesEPKvj:
|
||||
unsafe extern "C" fn(*const core::ffi::c_void, u32) -> *mut OSData;
|
||||
}
|
||||
@@ -71,6 +71,15 @@ 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;
|
||||
@@ -185,6 +194,24 @@ 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");
|
||||
@@ -236,7 +263,8 @@ namespace Frida {
|
||||
construct;
|
||||
}
|
||||
|
||||
public Barebone.Services services {
|
||||
/** Absent for a resident agent: every service here is built on a debugger stub. */
|
||||
public Barebone.Services? services {
|
||||
get;
|
||||
construct;
|
||||
}
|
||||
@@ -244,7 +272,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);
|
||||
}
|
||||
|
||||
@@ -333,10 +361,13 @@ namespace Frida {
|
||||
MainContext dbus_context = yield get_dbus_context ();
|
||||
|
||||
BareboneAgentSession session;
|
||||
if (connection != null)
|
||||
if (connection != null) {
|
||||
session = new RemoteBareboneAgentSession (connection, session_id, opts.persist_timeout, dbus_context);
|
||||
else
|
||||
} else {
|
||||
if (services == null)
|
||||
throw new Error.NOT_SUPPORTED ("Barebone target has neither an agent nor a debugger stub");
|
||||
session = new LocalBareboneAgentSession (services, session_id, opts.persist_timeout, dbus_context);
|
||||
}
|
||||
agent_sessions[session_id] = session;
|
||||
session.closed.connect (on_agent_session_closed);
|
||||
|
||||
|
||||
@@ -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,6 +281,9 @@ namespace Frida.Barebone {
|
||||
case "vsock":
|
||||
t = typeof (VsockTransportConfig);
|
||||
break;
|
||||
case "device":
|
||||
t = typeof (DeviceTransportConfig);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -351,6 +354,22 @@ 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;
|
||||
|
||||
@@ -762,13 +762,12 @@ namespace Frida {
|
||||
|
||||
private void on_agent_session_detached (AgentSessionId id, SessionDetachReason reason, CrashInfo crash) {
|
||||
AgentSessionEntry entry;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -993,11 +993,7 @@ namespace Frida {
|
||||
|
||||
private void on_remote_agent_session_detached (AgentSessionId remote_id, SessionDetachReason reason, CrashInfo crash) {
|
||||
AgentSessionId? 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)
|
||||
if (!remote_agent_sessions.unset (remote_id, out local_id))
|
||||
return;
|
||||
|
||||
bool agent_session_found = agent_sessions.unset (local_id);
|
||||
|
||||
@@ -1885,9 +1885,6 @@ 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;
|
||||
|
||||
@@ -1903,16 +1900,11 @@ 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);
|
||||
|
||||
@@ -3845,20 +3837,15 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -871,8 +871,6 @@ 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;
|
||||
@@ -975,10 +973,7 @@ namespace Frida {
|
||||
|
||||
var crash_info = (crash != null) ? crash : CrashInfo.empty ();
|
||||
foreach (var id in entry.sessions) {
|
||||
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)
|
||||
if (agent_sessions.unset (id))
|
||||
agent_session_detached (id, reason, crash_info);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[wrap-git]
|
||||
url = https://github.com/frida/frida-gum.git
|
||||
revision = 17.16.4
|
||||
revision = 17.17.0
|
||||
depth = 1
|
||||
|
||||
[provide]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[wrap-git]
|
||||
url = https://github.com/frida/glib.git
|
||||
revision = 90a8b77b3d4cdf0482eb398fb2000dcceaeb15f0
|
||||
revision = 790ffa82e80d99fba8a3db494e46f907d560893c
|
||||
depth = 1
|
||||
|
||||
[provide]
|
||||
|
||||
Reference in new issue
Block a user