Compare commits

..
175 Commits
Author SHA1 Message Date
Ole André Vadla Ravnås f67d1e81df Dump alive tasks every 30s in server and agent
Route the dump through GLib.info() using the new
g_task_describe_alive_tasks(), so the agent's dump reaches
logcat where its stdio is not visible. Bump the GLib wrap to
the branch holding that function.
2026-07-29 18:12:10 +02:00
Ole André Vadla Ravnås a068c704d1 Trace agent session detach notification path
Log every hop that agent_session_detached has to survive, from
the agent emitting closed, through frida-server's provider and
control channel, the droidy relay, and finally Device's pending
detach request. Each silent id-keyed drop now reports whether
the lookup succeeded, so a hanging detach() points straight at
the hop that swallowed the notification.
2026-07-29 17:47:25 +02:00
Ole André Vadla Ravnås 34ac3a0d50 server: Add output redirection option
Add -o/--output for redirecting stdout and stderr to a file,
so output survives daemonizing. Skip the stdin debug console
when daemonized, as stdin is then /dev/null.
2026-07-29 15:10:37 +02:00
Ole André Vadla Ravnås 1ea81bcd59 server: Add stdin-based task debug console 2026-07-28 14:47:05 +02:00
Ole André Vadla Ravnås 0e1a53ba7a subprojects: Prepare for release 2026-07-22 00:30:06 +02:00
Ole André Vadla Ravnås bae82ee212 subprojects: Bump outdated 2026-07-22 00:28:46 +02:00
Ole André Vadla Ravnås 5d714719bc subprojects: Prepare for release 2026-07-20 17:55:53 +02:00
Ole André Vadla Ravnås 1d68703132 subprojects: Bump outdated 2026-07-20 17:46:45 +02:00
Ole André Vadla Ravnås afba4699c1 subprojects: Prepare for release 2026-07-19 22:25:17 +02:00
Ole André Vadla Ravnås 8a3afe7c37 subprojects: Bump outdated 2026-07-19 22:24:20 +02:00
Ole André Vadla Ravnås 5ed252adad linux: Stop helper inheriting embedder stdio
For stdio=inherit, pass the host's streams to the helper with each
spawn; with that in place, redirect the helper's own 0/1/2 to
/dev/null at launch so it no longer holds the embedder's pipes.
2026-07-19 22:20:24 +02:00
Ole André Vadla Ravnås 195c6b0618 darwin: Stop helper inheriting embedder stdio
For stdio=inherit, pass the host's streams to the helper with each
spawn; with that in place, redirect the helper's own 0/1/2 to
/dev/null at launch so it no longer holds the embedder's pipes.
2026-07-19 22:20:24 +02:00
Ole André Vadla Ravnås 5934b352b3 subprojects: Prepare for release 2026-07-18 13:30:24 +02:00
Ole André Vadla Ravnås 62f0b50694 subprojects: Bump outdated 2026-07-18 13:29:03 +02:00
Ole André Vadla Ravnås 405fb10d1e submodules: Bump releng 2026-07-18 13:29:02 +02:00
Ole André Vadla Ravnås 07eb059d31 subprojects: Prepare for release 2026-07-17 23:28:11 +02:00
Ole André Vadla Ravnås aca4c8fd14 subprojects: Bump outdated 2026-07-17 23:07:26 +02:00
Ole André Vadla Ravnås 3e6668ef3b subprojects: Bump outdated 2026-07-17 20:48:01 +02:00
Ole André Vadla Ravnås b62cda4965 submodules: Bump releng 2026-07-17 20:48:00 +02:00
Ole André Vadla Ravnås e7d1a48dca barebone: Bump @types/frida-gum to 19.10.0 2026-07-17 18:26:09 +02:00
Ole André Vadla Ravnås 06f685320b compiler: Bump @types/frida-gum to 19.10.0 2026-07-17 18:26:09 +02:00
Ole André Vadla Ravnås b98ae95894 windows: Use gum_memory_allocate for worker code
Follow frida-gum dropping gum_alloc_n_pages()/gum_free_pages().
2026-07-17 15:06:50 +02:00
Ole André Vadla Ravnås a366b1cbf2 compat: Stop forwarding build-machine options
The compat build generates its own machine files, so forwarding the
parent's per-machine build options injected a foreign toolchain. In a
native compat build that produced a compiler-less build machine and
broke every native: true target.
2026-07-17 00:24:49 +02:00
Ole André Vadla Ravnås 206d98a2c5 subprojects: Bump outdated 2026-07-16 17:50:38 +02:00
Ole André Vadla Ravnås e1799ba509 submodules: Bump releng 2026-07-16 17:50:37 +02:00
Ole André Vadla Ravnås 53f3713e86 build: Add glib.wrap to resolve GLib up front
The GLib flavor decides whether payloads are built in-tree, which
frida-gum needs to know before it is configured. Without a wrap of its
own, frida-core could only obtain GLib from the frida-gum subproject, so
the flavor wasn't yet knowable and static cross builds failed to find
it. Shipping glib.wrap lets frida-core resolve GLib itself — the fork
bundles its build deps — so the flavor is known up front for every
build.
2026-07-16 16:43:07 +02:00
Ole André Vadla Ravnås 7b86da31a6 auth: Flush pending reply before closing channel
A rejected token kicked the authentication channel on an idle
source without flushing, so the INVALID_ARGUMENT reply raced
the teardown and clients could see a closed connection instead
of the error.
2026-07-16 15:36:20 +02:00
Ole André Vadla Ravnås 2309a88935 build: Base in-tree payloads on GLib flavor
Whether payloads are built in-tree depends on the GLib flavor, not the
library type: a shared build against Frida's static GLib still wants
them in-tree. Only a shared upstream GLib needs the out-of-tree agent.
2026-07-16 13:52:57 +02:00
Ole André Vadla Ravnås 91011dcdc3 build: Add native languages for cross builds
A cross build still builds the resource compiler for the build
machine, which needs its own C/C++/Vala compilers configured.
2026-07-16 13:52:34 +02:00
Ole André Vadla Ravnås 2f51f2e72c windows: Name helper services from the manager
A spawned helper derived its rendezvous pipe name from its own module
path, which only agrees with the manager when they are colocated (the
embedded layout). In the installed layout each helper lives in its own
arch directory, so the names diverged and they never met. Have the
manager mint one unique name and pass it to every service it spawns —
on the command line for standalone helpers, in the registered binary
path for SCM services — restoring both properties the shared temp dir
used to give: agreement and per-session uniqueness.
2026-07-16 13:52:14 +02:00
Ole André Vadla Ravnås 1355b05293 windows: Find arch helpers in installed layout
The sibling helper path was derived by swapping the filename suffix,
which only fits the embedded layout. Swap the arch token instead, so
it also finds …\<arch>\frida-helper.exe.
2026-07-16 13:24:43 +02:00
Ole André Vadla Ravnås ca736a3268 windows: Expand <arch> in installed helper path
In installed-assets builds the helper path kept the literal <arch>
placeholder, so spawning the cross-arch helper failed with 0x2. Expand
it to the native arch, like the Linux backend already does.
2026-07-16 13:24:21 +02:00
Ole André Vadla Ravnås c71b256bd8 spawn-gating: Fix unhandled-error warning
The droidy and fruity forwards let GLib.Error through raw so the caller can
spot UNKNOWN_METHOD, but declared only Error/IOError. Match the interface's
throws GLib.Error.
2026-07-15 23:58:51 +02:00
Ole André Vadla Ravnås dfc33cd512 spawn-gating: Make it fail-safe and scoped
17.8's eBPF spawn gater stops every execve and leans on the client to
resume each one, so a client that stops resuming wedges process creation
system-wide.

Add a shared SpawnGatingWatchdog that cancels gating and resumes
everything once a caught process sits unresumed too long, wired into all
four suspending gaters (Linux eBPF and RoboLauncher, iOS/tvOS
FruitController, macOS DTrace). Re-send SIGCONT until the task is seen
running, to beat the bpf_send_signal(SIGSTOP) vs SIGCONT race. Report it
via a new spawn_gating_disabled (reason) HostSession signal bubbled to
Device.

Make the system-wide gater opt-in: enable_spawn_gating takes a
SpawnGatingScope (DEFAULT, APPLICATIONS, PROCESSES), arming eBPF only for
PROCESSES. The scope rides a new enable_spawn_gating_with_options; the
plain call stays for wire compatibility and only DEFAULT falls back to
it, so an explicit scope fails loudly against an older Frida.

Also populate ControlService's pending_spawn so the approver model and
resume-on-disable work again.
2026-07-15 23:32:17 +02:00
Håvard Sørbø ca4c90eab1 fruity: Protect segments before applying fixups
The upload injector maps the agent module r-x, streams in its
segments, applies threaded and chained fixups, and only afterwards
restores each segment's protection.

The fixup passes write the rebased and authenticated pointers
directly into the module (e.g. into __DATA_CONST). Because the data
segments are still mapped r-x at that point, on arm64e the first
authenticated-rebase store faults with KERN_PROTECTION_FAILURE and
the injection is aborted with "Invocation of 0x... crashed".

Apply each segment's protection before running the fixup passes, so
the data segments are writable when the pointers land. __TEXT is not
fixed up and stays r-x, so no rwx mapping is created, and the WRITE
command keeps populating the module through its own temporary
writable mapping regardless of ordering.
2026-07-15 19:26:27 +02:00
Ole André Vadla Ravnås 4a1bf993a8 barebone: Bump @types/frida-gum to 19.9.0 2026-07-15 00:20:34 +02:00
Ole André Vadla Ravnås 2ff51da2bc compiler: Bump @types/frida-gum to 19.9.0 2026-07-15 00:20:34 +02:00
Ole André Vadla Ravnås 4372e5f84f subprojects: Prepare for release 2026-07-13 12:55:47 +02:00
Ole André Vadla Ravnås c7661416e5 subprojects: Bump outdated 2026-07-13 12:54:36 +02:00
dezige131 e2bca40e4e android: Tolerate transient USAP patch misses (#1246) 2026-07-13 12:51:34 +02:00
Ole André Vadla Ravnås f31061e846 subprojects: Prepare for release 2026-07-06 19:05:09 +02:00
Ole André Vadla Ravnås f8f9c4386a subprojects: Bump outdated 2026-07-06 19:04:15 +02:00
Ole André Vadla Ravnås a0bd34ebf6 barebone: Bump @types/frida-gum to 19.8.0 2026-07-06 19:03:23 +02:00
Ole André Vadla Ravnås abaebf8a41 compiler: Bump @types/frida-gum to 19.8.0 2026-07-06 19:03:23 +02:00
Ole André Vadla Ravnås e0ec2bee62 subprojects: Prepare for release 2026-06-22 23:31:20 +02:00
Ole André Vadla Ravnås d64a1a934f subprojects: Bump outdated 2026-06-22 23:30:29 +02:00
Ole André Vadla Ravnås 12c052f409 subprojects: Prepare for release 2026-06-22 11:46:17 +02:00
Ole André Vadla Ravnås 6d4443ff26 subprojects: Bump outdated 2026-06-22 11:45:30 +02:00
wave-sky 1b540b96c2 linux: Fix payload base pick on Android 10
Spawning processes on Android 10 fails with a remote connection
error due to Android's XOM mechanism, which restricts
libstagefright.so to execute-only access. This makes the read
permission check in do_prepare_zymbiote_injection fail.

Fix this by:
- Drop the read permission check for target libraries.
- Expand the list of eligible media shared libraries.
- Force read-execute permissions to ensure payload execution.
2026-06-22 11:41:53 +02:00
Ole André Vadla Ravnås 9492205c6a subprojects: Prepare for release 2026-06-20 15:56:50 +02:00
Ole André Vadla Ravnås ff1ecbefd4 subprojects: Bump outdated 2026-06-20 15:55:12 +02:00
Ole André Vadla Ravnås 774db58809 darwin: Fix unused variable warning on non-arm64e 2026-06-19 12:39:22 +02:00
Ole André Vadla Ravnås c1eff69ceb subprojects: Prepare for release 2026-06-19 12:38:27 +02:00
Ole André Vadla Ravnås 99000093ae subprojects: Bump outdated 2026-06-19 11:55:46 +02:00
Ole André Vadla Ravnås 03ba50b50f barebone: Bump @types/frida-gum to 19.7.0 2026-06-18 20:06:01 +02:00
Ole André Vadla Ravnås b3c5a68471 compiler: Bump @types/frida-gum to 19.7.0 2026-06-18 20:05:47 +02:00
Ole André Vadla Ravnås 75802359f5 subprojects: Prepare for release 2026-06-17 12:42:26 +02:00
Ole André Vadla Ravnås a27cc42c15 subprojects: Bump outdated 2026-06-17 12:39:19 +02:00
Ole André Vadla Ravnås c55252ee98 subprojects: Bump outdated 2026-06-17 12:33:00 +02:00
Ole André Vadla Ravnås 12948e78b7 subprojects: Prepare for release 2026-06-16 17:30:17 +02:00
Ole André Vadla Ravnås 8e2e44126f Expose process argv at metadata scope
Make the argv parameter from enumerate_processes() available at
metadata scope, mirroring path, instead of restricting it to full
scope.
2026-06-16 17:30:17 +02:00
Ole André Vadla Ravnås 9ed8e08c26 subprojects: Bump outdated 2026-06-16 16:38:18 +02:00
Ole André Vadla Ravnås eafaf83cdc script: Add interrupt() and terminate()
Expose the new Gum.Script interrupt() and terminate() capabilities
across the agent session API:

  - Script.interrupt() aborts any JavaScript currently executing,
    leaving the script loaded and able to run again.
  - Script.terminate() interrupts execution and unloads the script,
    so one stuck in a long-running or infinite operation can still be
    torn down.

Plumbed through AgentSession, the payload script engine, and the
public Script class. Barebone sessions report these as not supported.
2026-06-16 16:26:56 +02:00
Ole André Vadla Ravnås ac26f8fd84 subprojects: Prepare for release 2026-06-15 19:09:24 +02:00
Ole André Vadla Ravnås 26c180f69c subprojects: Bump outdated 2026-06-15 19:08:33 +02:00
Ole André Vadla Ravnås a461e5d5ab api: Emit filename/line on synthesized docs 2026-06-15 19:00:40 +02:00
Ole André Vadla Ravnås dac42a81d4 Add argv to enumerated process parameters
Populate an "argv" parameter, under full scope, with the target's
command-line arguments. Backed by the PEB on Windows, KERN_PROCARGS2
on macOS, /proc/<pid>/cmdline on Linux, and KERN_PROC_ARGS on
FreeBSD. QNX has no documented source, so it is left out.

Cover it with a local test that spawns a labrat with a known argument
and verifies the round-trip.
2026-06-15 19:00:02 +02:00
Ole André Vadla Ravnås 5c3e027adf subprojects: Prepare for release 2026-06-12 00:42:54 +02:00
Ole André Vadla Ravnås 0bab38e06b compiler: Fix the build on Go 1.24.3
The symbol-replacer module declared go 1.24.4, but CI installs the
toolchain based on the compiler module's go.mod, which says 1.24.3,
and GOTOOLCHAIN=local prevents auto-upgrade. The module has no
dependencies, so lower the requirement to match.
2026-06-12 00:42:54 +02:00
Ole André Vadla Ravnås 5a037879f2 core: Document compiler, packages, and services 2026-06-12 00:12:10 +02:00
Ole André Vadla Ravnås 4ae320f578 core: Document portal, peer, and web types 2026-06-12 00:12:10 +02:00
Ole André Vadla Ravnås bc8f67b45e core: Document base enums and option types 2026-06-12 00:12:10 +02:00
Ole André Vadla Ravnås f6774caab7 core: Document the public API 2026-06-12 00:12:10 +02:00
Ole André Vadla Ravnås 3d33d451a7 api: Inject Vala doc comments into the GIR 2026-06-12 00:12:10 +02:00
Ole André Vadla Ravnås 0d4d5c62f7 ci: Bump actions to latest 2026-06-12 00:12:10 +02:00
Ole André Vadla Ravnås 4d6decfe0a subprojects: Bump outdated 2026-06-11 22:56:26 +02:00
Ole André Vadla Ravnås 5c3da2ba20 submodules: Bump releng 2026-06-11 22:56:26 +02:00
Ole André Vadla Ravnås 522c843b72 subprojects: Bump outdated 2026-06-11 20:25:59 +02:00
Ole André Vadla Ravnås fb6f798400 submodules: Bump releng 2026-06-11 20:25:58 +02:00
Ole André Vadla Ravnås 006563006d gdb: Fix register access on LLDB stubs
Per-register access keyed the ;thread: suffix off the vCont feature,
but the suffix is the QThreadSuffixSupported extension, unrelated to
vCont. The iOS debugserver negotiates the suffix without advertising
vCont, so reads went out bare and came back E03, surfacing as
"Failed to attach: invalid integer value: E03".

Track a thread-suffix capability, set it where the stub negotiates
QThreadSuffixSupported, and gate the suffix on it. Plain gdbserver
now also stops receiving an LLDB-only suffix it never understood.

Add a smoke test driving the client against a debugserver-flavoured
stub that only honours suffixed reads.
2026-06-11 20:24:59 +02:00
Ole André Vadla Ravnås a007e94625 subprojects: Prepare for release 2026-06-10 00:47:55 +02:00
Ole André Vadla Ravnås b814d4a73b subprojects: Bump outdated 2026-06-10 00:46:46 +02:00
Ole André Vadla Ravnås 2d195613a3 ci: Publish GObject introspection data 2026-06-10 00:44:56 +02:00
Ole André Vadla Ravnås 880f7ac7a3 linux: Walk the stack to find a hookable trigger
Once a target is multi-threaded, a thread blocked in a cancellable
syscall parks in the tiny __syscall_cancel_arch wrapper, too small to
host the stub, so sampling its PC alone found nothing to re-hook and
injection timed out from the second time onwards. Also read a few stack
words via the syscall line's stack pointer and let hottest_libc_function
walk outward to the first frame in a large-enough libc routine, trusting
only words a call actually returns to so stray data is never re-hooked.
2026-06-10 00:22:27 +02:00
Ole André Vadla Ravnås f51327fa29 linux: Load agent via /proc/<pid>/fd
An attached debugger reacts to the dynamic linker's library-load event
by reading the freshly mapped object off the path dlopen () was given.
With /proc/self/fd/<n> that read resolves against the debugger's own fd
table rather than ours, so it fails; GDB then leaves the loading thread
parked at its dynamic-linker breakpoint, the agent never finishes coming
up, and injection times out waiting for it to connect back. Spelling out
our PID makes the path resolve identically from inside and outside the
process, so the debugger reads the real object and lets the thread run.
2026-06-09 23:48:10 +02:00
Ole André Vadla Ravnås f0cc3b44ff linux: Restore trigger symmetrically with install
restore_trigger overwrote the patched body while the entry still routed
callers into it, so a multi-byte prologue (e.g. endbr64) could be
re-entered mid-restore and execute a half-written instruction. Re-block
the entry and drain before putting the body back, then restore the first
instruction last, mirroring install_bootstrap.

Also give the agent-connect timeout its own wording, distinct from the
identical stub-trigger timeout, so the two stages can be told apart.
2026-06-09 23:48:10 +02:00
Ole André Vadla Ravnås 21dc508b4a linux: Capture libbpf log on error
Install an accumulating libbpf print callback and fold the captured
output into the errors we throw, so failures carry their diagnostics
instead of spilling onto stderr. The kernel verifier log arrives over
the same callback, making the separate log buffer redundant.
2026-06-09 23:48:10 +02:00
Ole André Vadla Ravnås 33e5879226 linux: Size triggers by the actual stub
The bootstrap is stamped over the trigger function in place, so a
candidate is only usable if it is at least as large as the stub. Build
that stub for the first-injection path and measure it, rather than
gating on a conservative fixed minimum that needlessly rejected small
but frequently-run routines such as glibc's shared syscall-cancellation
trampoline.
2026-06-09 23:48:10 +02:00
Ole André Vadla Ravnås a0a883839c linux: Fall back to /proc when eBPF is denied
When the activity sampler cannot start, typically because perf/eBPF
requires privileges we lack, poll /proc/<pid>/task/<tid>/syscall to find
which libc routines the target is calling and re-hook the hottest one.
2026-06-09 23:48:10 +02:00
Ole André Vadla Ravnås 837b6acaee barebone: Bump @types/frida-gum to 19.6.0 2026-06-09 23:38:46 +02:00
Ole André Vadla Ravnås 63ad485f8a compiler: Bump @types/frida-gum to 19.6.0 2026-06-09 23:38:46 +02:00
Ole André Vadla Ravnås 43fcdf5f0e barebone: Bump @types/frida-gum to 19.5.0 2026-06-05 18:17:04 +02:00
Ole André Vadla Ravnås 2176a7b85c compiler: Bump @types/frida-gum to 19.5.0 2026-06-05 18:17:04 +02:00
Ole André Vadla Ravnås 2c2bd9d159 barebone: Set size on synthesized export details
GumExportDetails gained a size field; the agent builds the struct
literally while enumerating Mach-O exports, which carry no size, so
report -1 to match the unknown convention.
2026-06-05 18:00:11 +02:00
Ole André Vadla Ravnås 3c0f86f393 subprojects: Prepare for release 2026-06-05 17:08:02 +02:00
Ole André Vadla Ravnås 1f0e01f927 activity-sampler: Suppress unused field warnings
SampleEvent must match the layout emitted by the BPF program, so touch
the fields we don't consume yet to keep valac quiet.
2026-06-05 17:06:11 +02:00
Ole André Vadla Ravnås 23455a0e72 subprojects: Bump outdated 2026-06-05 16:59:53 +02:00
Ole André Vadla Ravnås 73ea47f899 activity-sampler: Tweak style 2026-06-05 16:59:25 +02:00
Ole André Vadla Ravnås c0be13b982 linux: Discover a ptrace-free trigger by sampling
An idle ART/Dalvik process gets a SIGUSR1 nudge, but a busy native
target that never calls malloc had no way through. Sample what it
runs and resolve the hottest libc function from each sample's leaf
-- by symbol, then export, then a prologue/ret scan as a last
resort -- then restore malloc and re-hook there.

The leaf is the only reliable frame: the hot mem*/str* routines are
frame-less assembly, so unwinding past them misattributes the call
to whatever the frame pointer happened to hold. Resolution leans on
the symbol table because those routines are ifunc-resolved to
non-exported implementations the export table never names.
2026-06-05 16:59:25 +02:00
Ole André Vadla Ravnås 21f2cba74c linux: Make ActivitySampler collect call stacks
Sample once per millisecond of on-CPU time rather than every
nanosecond, and accumulate each thread's user call stack instead of
printing it. This turns the dormant debug sampler into something a
caller can read to learn what a target is actually running.

Groundwork for picking a ptrace-free trigger when malloc is quiet.
2026-06-05 16:59:25 +02:00
Ole André Vadla Ravnås 3ff34d2c31 linux: Preserve all arg regs in ptrace-free stub
The stub saved only the first argument across re-invoking the
hooked function -- fine for malloc, but not for an arbitrary
trigger. Save and restore the full integer-arg set (rdi-r9 on
x86_64, x0-x8 + lr on arm64) on both the loser path and the
winner's return into the function. x86 needs nothing: its args
ride the stack, which the stub never touches.

Groundwork for hooking something other than malloc when malloc
stays quiet.
2026-06-05 16:59:25 +02:00
Ole André Vadla Ravnås c2ff19e63b compiler: Rename symbols in the archive index too
The symbol index maps names to members and is what the linker uses to
pull a definition. ranlib does not rebuild it for the COFF archive
clang produces on windows-arm64, so the renamed objects no longer
matched their index entries and the link failed.

Rename the names in the index as well. It holds only names and offsets,
so the rewrite stays clear of the runtime data inside the objects.
2026-06-05 12:18:29 +02:00
Ole André Vadla Ravnås 2dad3485c5 barebone: Guard hostlink UNIX socket on Windows 2026-06-05 01:13:21 +02:00
Ole André Vadla Ravnås 196991bbe6 subprojects: Bump outdated 2026-06-05 00:55:55 +02:00
Ole André Vadla Ravnås 3dd3162117 submodules: Bump releng 2026-06-05 00:55:55 +02:00
Ole André Vadla Ravnås fff63df874 linux: Recycle ptrace-free loader region
The region cannot free itself: the worker runs the loader from it,
and afterwards no code of ours is left to call munmap -- ptrace,
which the ptrace path uses for this, is what's unavailable here.

So recycle it instead of leaking one per injection: rediscover the
region by signature -- an anonymous executable mapping still holding
our loader bytes -- and aim the bootstrap at it. Stateless, so it
survives the short-lived helper, and the signature rules out a
reused PID.

Keep nameless anonymous mappings in the maps parser so the scan
finds them.
2026-06-04 22:41:19 +02:00
Ole André Vadla Ravnås 50b9be7a80 linux: Add ptrace-free injection fallback
When a foreign tracer occupies the target's ptrace slot, fall back
to /proc/$pid/mem: patch an in-malloc bootstrap that mmap()s an R-X
region, stage the loader there, and let the elected thread run it.
Losers and re-entrants spin at malloc+0 until it is restored.

Implemented for x86, x86_64 and arm64. On a managed runtime (ART or
Dalvik) an idle process is nudged with SIGUSR1 to force an allocation.
2026-06-04 20:22:40 +02:00
Ole André Vadla Ravnås 5f7295c596 tests: Force sandbox to allow a given PID
Hook sb_evaluate_internal, the chokepoint every per-operation
sandbox check funnels through, and force its result to 0 (allow)
when the subject process matches a target PID. The proc is read
from the evaluator context at +0x10 and resolved via proc_pid.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås bfd7fd6f88 tests: Hook a kernel function in the VZ guest
Attach to sb_evaluate_internal and confirm the handler runs on a
foreign kernel thread without a panic.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås 17827a1493 barebone: Query page size from cached MMU params
Reuse the cached MMU parameters instead of reloading them over
the stub, which hangs once the stub has been detached.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås 8758398a53 barebone: Patch kernel text via physical bridge
Kernel text is locked read-only by CTRR, so guest-side writes
fault. Add a PATCH_CODE command that stages writes in a shadow
buffer and commits them through the host's physical-memory
bridge, which writes the backing store directly.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås 13a5b23288 barebone: Relocate kext code via kext summaries
An SPTM fileset kernelcache scatters each fileset entry's
segments to independent runtime bases, so kext code living past
the kernel's own __TEXT_EXEC slice cannot be translated through
the kernel's base. The runtime collection header is no help: its
LC_FILESET_ENTRY vmaddrs are clobbered by the loader and the kext
mach-headers are repacked away from their static positions.

Walk gLoadedKextSummaries instead, the array the kernel maintains
for debuggers. It pairs each loaded kext's bundle id with the
runtime address of its mach-header, from which we read that kext's
independently rebased segments and pair them with the static
segment table. The kernel itself is still located by translating
its header through the collection segments.

The summaries pointer is supplied via image.symbols; without it
only the kernel is relocated.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås 1c8e27f9c4 barebone: List kexts from fileset kernelcaches
Fileset kernelcaches (iOS 16+) carry each kext as an LC_FILESET_ENTRY
load command rather than the __PRELINK_INFO __kmod_info/__kmod_start
tables that compute_module_list knew about, so the module list collapsed
to a single fabricated mach_kernel entry. Parse the fileset entries
instead, expressing each kext relative to com.apple.kernel so the agent
rebases them with its single kernel base (and the per-segment
KernelRelocation scatters them to their real runtime addresses).

Verified against the vphone research kernel: Process.enumerateModules()
now returns all 121 kexts with their bundle ids and runtime bases
instead of just the kernel.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås e6a334e354 barebone: Detach the debug stub after injection
The vphone research kernel routes every synchronous exception taken
while a debugger is attached straight to a panic. The agent's worker
intermittently faults inside the kernel allocator during gum_init, so
with the stub still attached that fault became a kernel panic that
required rebooting the VM (frequent on a fully-settled kernel, ~25-50%
of injects in the worst case).

Detach the stub once the agent is in place so its exceptions are handled
normally. Post-injection all host reads/writes go through the
physical-memory bridge and the MMU parameters are already cached, so the
stub is no longer needed; gate the detach on the bridge being present
and otherwise keep continuing, since targets without it still drive the
W^X RPC through the stub.

Verified: longwait injects went from ~25-50% panic to ~1/26, and
Interceptor/Stalker still work (their W^X path uses the bridge).
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås 71a299bf25 barebone: Tolerate thread migration during invoke
invoke() and execute() hijack the core that hit thread_block, set the
callee's return address to a landing zone, breakpoint it, and resume.
The stub identifies threads by core, so the loop waited for the same
core to hit the landing zone. But a callee that reschedules (notably the
agent's _start calling kernel_thread_start to spawn the worker) migrates
the hijacked thread to another core; the return then stops on a
different core, the loop rejected it and waited forever, and registers
were restored onto the original core's now-unrelated thread, wedging the
kernel into a cross-core spinlock deadlock (or, on a missed landing,
panicking a bystander thread).

The landing zone is never reached by anything else, so any thread
stopping there is our call returning. Match on the breakpoint alone and
finish on whichever core it landed on. Fixes the intermittent inject
hang/panic that previously required rebooting the VM.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås dd79d5bbb0 barebone: Make Stalker work in the kernel
Bring up Stalker end-to-end against the in-kernel agent:

- Flush this CPU's TLB after the host rewrites our descriptors. The host
  edits page tables through the physical-memory bridge, so the guest TLB
  keeps the stale translation; Stalker flips a code slab RW then RX in
  place, and without the flush executing the frozen page faults with a
  permission abort.
- Exclude the agent's own range from Stalker. Following the current thread
  into the hostlink transport would deadlock on the locks the RPC it
  depends on is holding. The host injected the image, so it now passes our
  range first in the config and the agent excludes it on each script's
  Stalker, mirroring lib/payload.
- Mask the thread pointer down to a JS-safe width in get_current_thread_id
  so Process.getCurrentThreadId() round-trips through a double, letting
  Stalker.follow recognise the current thread.

Verified: Stalker.follow with a transform over a NativeFunction (traps:
'all') compiles and runs the instrumented blocks and returns correctly
(r: 42 blocks: 30).
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås d8e7075002 barebone: Support W^X remap so Interceptor works
Code-generating gum (Interceptor, Memory.patchCode) allocates pages RX
and writes through a writable alias via gum_memory_try_remap_writable_pages.
Make that path work in-kernel:

- The agent no longer resolves ml_vtophys (absent on this kernel); it sends
  the virtual pages and the host translates them to physical via its own
  page-table walk over the bridge.
- translate_address/read_level3_descriptor reuse the cached MMU registers
  and skip the stub's physical-addressing mode when the bridge is present,
  so they run without halting the agent.
- allocate_pages reads the displaced descriptors through the bridge instead
  of the stub (which would hang on a running target).

Verified: Interceptor.attach on an in-kernel function fires and the original
returns correctly (result: 42 hits: 1).
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås 33d4228871 barebone: Make in-kernel closures executable
NativeCallback is backed by a libffi closure whose trampoline must be
executable. With the static-trampoline backend (FFI_TRAMP_EMBEDDER),
libffi asks the embedder to map the trampoline table; ffi.rs copies the
template, then flips it RX over the hostlink RPC. Supporting changes:

- protect_pages/allocate_pages cache the kernel's MMU registers and skip
  the stub's physical-addressing mode when the bridge is present, so the
  page-table work runs without halting the agent.
- The RPC handlers no longer stop the VM (the bridge writes guest memory
  directly); halting froze the waiting worker.
- Hostlink.process() detaches the frame and resets receive state before
  dispatching, so a synchronous RPC issued while handling a command does
  not re-enter and re-dispatch the same frame.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås 5158b7bdb8 barebone: Deliver W^X callbacks over vsock-RPC
The mprotect/remap callbacks were patched as BRK breakpoints at the
agent's gum_try_mprotect/gum_barebone_try_remap_writable_pages, but a
BRK is a debug exception that panics this research kernel. Move them to
the hostlink: the agent sends a REMAP_WRITABLE_PAGES/MEMORY_PROTECT
request and blocks pumping the transport until the reply, while the
host does the page-table work via the bridge and replies. This lets
code-generating scripts (NativeCallback, Interceptor, ...) run.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås ae20324226 barebone: Bring up the vsock hostlink end-to-end
Disable the BRK-based mprotect/remap callbacks (debug exceptions panic
this research kernel; these will move to vsock-RPC) so the agent runs
through to its main loop, and restore the establish_hostlink flow. With
the async-connect and PAC fixes, host<->agent RPC now round-trips: a
script's send() arrives from kernel space over the hostlink.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås 9d938642d8 barebone: Handle async vsock connect handshake
sock_connect on the in-kernel KPI returns EWOULDBLOCK/EINPROGRESS and
completes in the background once the host accepts; re-calling connect
to poll corrupts the connecting state and yields a hard error. Treat
the in-progress codes (and EISCONN) as success and let the main loop
drive I/O once the link is up.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås bb3177d185 barebone: PAC-sign the vsock sock_upcall callback
XNU invokes the sock_upcall via an authenticated branch (blraa,
IA key) using the sock_upcall type discriminator, so the pointer
handed to sock_socket must be signed to match. Sign it like the
interrupt handler and thread continuation; otherwise the kernel
takes a PAC failure on the first upcall.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås a776a40e30 barebone: Bias ABS64 relocs by existing slot value
The agent is a fully-linked --emit-relocs image, so each ABS64 slot
already holds the final base-0 value. Recomputing from
symbol.address + addend is unsafe: some relocs reference the .text
section symbol, whose st_value is a bogus non-zero address rather
than the section base, corrupting the result (e.g. a class_init
pointer landing in .rodata and faulting on execute). Bias the
existing slot by base_va instead.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås 6910f51acb barebone: Add VZ physical-memory bridge
Map the guest's RAM straight from the Virtualization daemon
(task_for_pid + mach_vm) and route page-table I/O and the agent
upload through it, bypassing the slow kernel GDB stub. Locate
Hv::Vm with Gum.Memory.find_pointers over the daemon heap.

Add a sysconf stub to the barebone agent (gum's find_pointers
pulls it in) and document, in-code, the research-kernel debug-
exception panic that still blocks gum_init in the agent worker.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås d396365e48 barebone: Fill in vphone kernel symbol map
Resolve the agent's required XNU symbols against the vresearch101
research kernelcache (panic, IOLog, thread_block, assert_wait[_timeout],
thread_wakeup, kalloc_data/kfree_data, the sock_* KPIs,
kernel_thread_start, and the timebase/calendar helpers) and record them
in the example config as offsets from the kernelcache base, leaving the
per-segment relocation to translate them at injection time.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås b9c0b4dfdc barebone: Resolve kalloc_data/kfree_data allocator
Modern XNU exposes kalloc_data_external(size, flags) and
kfree_data_external(ptr, size) instead of kalloc/kfree. Pass an
allocator-configurable flags argument (invoke leaves x1 undefined) and
record the resolved functions in the vphone example config.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås 9917f78b59 barebone: Resolve per-segment kernel slides
On a scattered SPTM kernel collection each segment is relocated
independently, so a single KASLR slide cannot map static addresses to
runtime ones. Read the live com.apple.kernel header's rebased
per-segment vmaddrs and pair them with the static table to translate
any address. Compute the relocation once in the host session, apply it
to the allocator functions and to every resolved symbol, and enter
no-ack mode so the header scan has the throughput it needs.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås 0e770aa846 gdb: Fall back to per-register access without g/G
The VZ kernel stub is lldb-flavoured: it implements neither the bulk
g/G register packets nor qSupported's no-ack advertisement, and stays
halted across reconnects. Add read/write_registers fallbacks that walk
the 64-bit registers via p/P (skipping ones not readable in the current
context), a start_no_ack_mode helper, and make halt() also issue ? so a
stub that is already stopped still replies.
2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås 63d74d7909 barebone: Compute KASLR slide from kernelcache 2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås dd9de99215 tests: Add VZ kernel stub smoke test 2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås 118921b2e5 barebone: Add VZ kernel GDB stub client 2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås e88951d74b gdb: Add connection-preparation hook 2026-06-04 13:21:37 +02:00
Ole André Vadla Ravnås 71c91ece93 gitignore: Ignore clangd index cache 2026-06-04 13:21:37 +02:00
Håvard Sørbø 0cb45826bf gdb: Switch thread before register access 2026-06-04 13:21:37 +02:00
Håvard Sørbø 9e3ac7f1fb gdb: Detect vCont support with fallback 2026-06-04 13:21:37 +02:00
Håvard Sørbø af05c830c8 gdb: Allow qRcmd to return an empty reply 2026-06-04 13:21:37 +02:00
Håvard Sørbø bf3730d28a gdb: Load target properties based on qXfer support 2026-06-04 13:09:47 +02:00
Håvard Sørbø 4d7a1c3fc1 gdb: Always send proper checksum 2026-06-04 13:09:47 +02:00
Ole André Vadla Ravnås 40ec04e48b barebone: Add vsock transport alongside virtio
Give the agent config a transport kind byte; let the host pick
virtio or vsock per target. Add a hostlink_vsock module that
talks AF_VSOCK via XNU's in-kernel socket KPI. Make xnu.rs fall
back across kalloc / kalloc_data and thread_wakeup / wakeup so
one binary covers QEMU's bare XNU and iOS XNU.

Dispatch on transport type in agent-connection.vala and stop
erroring on unresolved .kernel_addrs slots — Option<fn> in the
agent absorbs per-kernel symbol drift.
2026-06-04 13:09:47 +02:00
Ole André Vadla Ravnås 87822f0668 compiler: Rename symbols only in string tables
The symbol-replacer rewrote every matching byte sequence across the
whole archive, mangling machine code, type metadata and GC pointer
maps. That corrupted the embedded Go runtime, causing rare crashes
during garbage collection when compiling agents.

Confine the rewrite to the symbol string tables, locating them in the
ELF, Mach-O and COFF members. Exported symbols are unchanged.
2026-06-04 12:20:11 +02:00
Ole André Vadla Ravnås 64086ff8b5 subprojects: Prepare for release 2026-06-02 02:10:37 +02:00
Ole André Vadla Ravnås d5e018ff2a subprojects: Bump outdated 2026-06-02 02:08:48 +02:00
Ole André Vadla Ravnås e6a05587bb subprojects: Bump outdated 2026-06-02 01:41:03 +02:00
cylentsec 6db41996b8 darwin: Support ElleKit/palera1n in launchd agent
Add findEllekitShouldEnableTweaks() and instrumentEllekitInjector()
to applyJailbreakQuirks(). On palera1n + ElleKit the injector gate
function should_enable_tweaks is exported from libinjector.dylib or
TweakLoader.dylib. Intercept it to return 0 for Frida-managed PIDs,
preventing double-injection for spawned processes.

Also add missing return after instrumentInserter() so ElleKit
detection only runs when no Substitute inserter is present.
2026-06-02 01:33:54 +02:00
cylentsec 8491a90d11 darwin: Fix ElleKit spawn compatibility
Restore selective exception mask in thread_swap_exception_ports:
EXC_MASK_BREAKPOINT | EXC_MASK_CRASH (plus EXC_BAD_ACCESS on
ptrauth/arm64e). This prevents Frida from intercepting non-
breakpoint exceptions raised by ElleKit's TweakLoader/libinjector
during DYLD_INSERT_LIBRARIES initialization on palera1n.

Replace the hard goto-unexpected_exception bailout for non-
EXC_BREAKPOINT exceptions with a forward_to_system path that
sends KERN_FAILURE, letting the kernel pass the exception to the
next handler in the task→host chain. Only EXC_CRASH is still
routed to unexpected_exception for early failure detection.

On ptrauth/arm64e, EXC_BAD_ACCESS at the ret_gadget address is
still allowed through so the PAC fixup already in place can take
effect — previously that fixup was dead code because the hard
bailout ran after it but before the state was committed.

Fixes #1239.
2026-06-02 01:33:54 +02:00
AeonLucid b5d14fda20 android: Fix helper initialization for Nothing OS 2026-06-02 00:43:53 +02:00
Ole André Vadla Ravnås e84c209a00 barebone: Bump @types/frida-gum to 19.4.0 2026-06-01 23:47:03 +02:00
Ole André Vadla Ravnås 1242a236bc compiler: Bump @types/frida-gum to 19.4.0 2026-06-01 23:47:03 +02:00
Ole André Vadla Ravnås b555792d15 subprojects: Prepare for release 2026-05-31 21:03:52 +02:00
Ole André Vadla Ravnås 9d19884fa8 subprojects: Bump outdated 2026-05-31 21:02:53 +02:00
Ole André Vadla Ravnås eba6766e5c submodules: Bump releng 2026-05-31 21:02:53 +02:00
Ole André Vadla Ravnås 3c72076909 barebone: Bump @types/frida-gum to 19.3.0 2026-05-31 15:29:21 +02:00
Ole André Vadla Ravnås 4b4ea3d710 compiler: Bump @types/frida-gum to 19.3.0 2026-05-31 15:29:21 +02:00
Ole André Vadla Ravnås 2e1e35537b subprojects: Bump outdated 2026-05-31 13:13:18 +02:00
Ole André Vadla Ravnås 38121a1815 Expose agent feature toggles and linker offsets
SessionOptions now carries the exceptor mode (full, handler-only, off),
the unwind-broker, exit-monitor, and thread-suspend-monitor toggles, and
a list of linker notifier offsets. These ride the agent parameters and
are applied before the relevant Gum singletons are first obtained.
2026-05-30 23:28:58 +02:00
Ole André Vadla Ravnås 25659c78b6 Adapt to new Interceptor attach/replace API
Move replacement_data and listener_function_data into the
GumReplaceOptions/GumAttachOptions structs, and drop the
removed GUM_ATTACH_FLAGS_NONE argument.
2026-05-30 23:21:36 +02:00
Ole André Vadla Ravnås a58e11d22a payload: Migrate unwind guts to generic Gum API
The dyld interception, libunwind vtable hooking, capstone-based
discovery and PAC resigning now live in Gum behind UnwindBroker and
UnwindSectionsProvider. UnwindSitter is reduced to registering a
provider for the invader's range.

Rework the exception-catcher tests to drive the ObjC runtime
directly, and add an arm64e labrat.
2026-05-30 17:37:34 +02:00
Ole André Vadla Ravnås 4a39801bed base: Fix installed-layout asset resolution
The EXISTS check stat'd a path still containing the literal
<arch> token, so it always failed and the flat fallback won
even when the frida-1.0/<arch>/ tree was present. Probe the
install root instead and return the templated path for
PathTemplate.expand() to resolve.
2026-05-24 09:02:31 +02:00
Ole André Vadla Ravnås 5e8ddd6863 subprojects: Prepare for release 2026-05-23 14:22:26 +02:00
Ole André Vadla Ravnås e1023ccc1d subprojects: Bump outdated 2026-05-23 14:21:15 +02:00
Ole André Vadla Ravnås b3e88c1c40 agent: Enable Exceptor on hardened Darwin tasks
The opt-out was put in place when Gum.Exceptor used Mach exception
ports — those didn't work for /System/, /usr/libexec/ and similar
hardened binaries. The POSIX-signal-based backend now works on
arm64e thanks to recent Gum work, so drop the opt-out.
2026-05-23 14:14:25 +02:00
Ole André Vadla Ravnås ebe45d7d52 subprojects: Bump outdated 2026-05-21 23:19:12 +02:00
Ole André Vadla Ravnås 219ff0ec59 darwin: Launder embedded agent through anon pages
The bytes we previously handed to mach_vm_remap pointed straight
into frida-server's own __DATA segment, which the kernel knows
as a file-backed code-signed mapping. The codesign state may have
been propagated through the cross-task remap on older kernels
too, but newer kernels add hardening that catches a mismatch when
the destination is asked to treat a subset of it (the embedded
dylib slice) as its own mapping.

Copy the agent into a page-aligned anonymous mapping at
host-session construction time so the source pages carry no
codesign state. Mirrors what the installed-asset path does
implicitly by memcpying the file-backed mapping into anon pages
before handing them to the mapper.
2026-05-21 23:15:24 +02:00
Ole André Vadla Ravnås 739e3f626e subprojects: Prepare for release 2026-05-15 14:23:26 +02:00
Ole André Vadla Ravnås 85fd4742c3 subprojects: Bump outdated 2026-05-15 14:21:33 +02:00
pakbaz-dev a41028792c android: Handle android:process in spawn matching
When an activity declares android:process, the zygote-forked
process is named "com.example.app:foo" rather than the bare
package name. The zymbiote's hello carried that suffixed name,
so it never matched the pending spawn_request keyed on the
package name, and the spawn timed out.

Strip the colon suffix before matching, and rename the
zymbiote-side field from package_name to process_name to
reflect what it actually carries.
2026-05-15 14:18:22 +02:00
Ole André Vadla Ravnås 81d8403d34 package-manager: Fix scoped name parsing
Splitting "@scope/name" on '@' left the name empty and
treated "scope/name" as the version range, then fetched
the registry root and failed with a misleading "missing
dist-tags" error. Skip the leading '@' when locating the
name/version separator.
2026-05-15 14:08:46 +02:00
Ole André Vadla Ravnås 8e9e376c7e subprojects: Prepare for release 2026-05-14 20:21:09 +02:00
Ole André Vadla Ravnås 4465abf747 subprojects: Bump outdated 2026-05-14 20:19:50 +02:00
Ole André Vadla Ravnås 3f51017a5d subprojects: Prepare for release 2026-05-11 13:22:42 +02:00
Ole André Vadla Ravnås b88584ec7e subprojects: Bump outdated 2026-05-11 13:21:56 +02:00
Ole André Vadla Ravnås 86c760f2d7 compiler: Bump @types/frida-gum to 19.1.0 2026-05-11 13:21:22 +02:00
Ole André Vadla Ravnås 6c3c58cc6e subprojects: Prepare for release 2026-05-08 10:39:58 +02:00
Ole André Vadla Ravnås b3f2bf1c24 subprojects: Bump outdated 2026-05-08 10:37:29 +02:00
Ole André Vadla Ravnås c80a32c59f compiler: Free backend strings on the cgo side
The backend (Go/cgo) used C.CString to hand bundle, error, and
diagnostic strings to the Vala callback, leaving Vala to g_free
them. On Windows the cgo runtime links against mingw's CRT while
the Vala consumer links against MSVC's ucrt, so the heaps don't
match and the free aborts the process with STATUS_HEAP_CORRUPTION.

Free the C strings on the Go side once the callback returns and
declare the delegates with unowned strings so the consumer dups
into the GLib heap when it needs to retain a copy.
2026-05-05 16:24:10 +02:00
123 changed files with 7962 additions and 1206 deletions

No files matched your search

+51 -25
View File
@@ -31,7 +31,7 @@ jobs:
runs-on: ${{ fromJSON(matrix.runner) }}
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install MinGW toolchain for cgo support
@@ -42,7 +42,7 @@ jobs:
msystem: ${{ matrix.sys }}
install: ${{ matrix.pkg }}
- name: Install Go toolchain
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: 'src/compiler/go.mod'
cache-dependency-path: 'src/compiler/go.mod'
@@ -95,14 +95,40 @@ jobs:
make
- name: Upload devkit
if: ${{ !startsWith(matrix.id, 'linux-') }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: core-devkit-${{ matrix.id }}
path: build/src/devkit/
- name: Upload GObject introspection data
if: ${{ matrix.id == 'linux-x86_64' }}
uses: actions/upload-artifact@v7
with:
name: core-gir
path: build/src/api/Frida-1.0.gir
- name: Test
if: ${{ matrix.id != 'windows-arm64' && !startsWith(matrix.id, 'macos-') }}
run: make test
publish-gir:
if: ${{ !cancelled() && github.ref_type == 'tag' && needs.native.result == 'success' }}
needs:
- native
runs-on: ubuntu-latest
steps:
- name: Download GObject introspection data
uses: actions/download-artifact@v8
with:
name: core-gir
path: gir
- name: Attach to GitHub release
uses: softprops/action-gh-release@v3
with:
files: |
gir/Frida-1.0.gir
prerelease: ${{ contains(github.ref_name, 'snapshot') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
cross:
strategy:
matrix:
@@ -117,7 +143,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install toolchain
@@ -125,7 +151,7 @@ jobs:
sudo apt-get update
sudo apt-get install ${{ matrix.pkg }}
- name: Install Go toolchain
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: 'src/compiler/go.mod'
cache-dependency-path: 'src/compiler/go.mod'
@@ -138,7 +164,7 @@ jobs:
make
- name: Upload devkit
if: ${{ !startsWith(matrix.id, 'linux-') }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: core-devkit-${{ matrix.id }}
path: build/src/devkit/
@@ -152,11 +178,11 @@ jobs:
container: ghcr.io/frida/x-tools-linux-${{ matrix.arch }}:latest
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install Go toolchain
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: 'src/compiler/go.mod'
cache-dependency-path: 'src/compiler/go.mod'
@@ -168,7 +194,7 @@ jobs:
${{ env.FRIDA_CORE_OPTIONS }}
make
- name: Upload devkit
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: core-devkit-linux-${{ matrix.arch }}
path: build/src/devkit/
@@ -185,7 +211,7 @@ jobs:
container: ghcr.io/frida/x-tools-linux-${{ matrix.arch }}:latest
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Build
@@ -316,11 +342,11 @@ jobs:
runs-on: ${{ startsWith(matrix.id, 'ios-') && 'macos-latest' || 'ubuntu-latest' }}
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install Go toolchain
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: 'src/compiler/go.mod'
cache-dependency-path: 'src/compiler/go.mod'
@@ -347,7 +373,7 @@ jobs:
${{ env.FRIDA_CORE_OPTIONS }}
make
- name: Upload devkit
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: core-devkit-${{ matrix.id }}
path: build/src/devkit/
@@ -406,7 +432,7 @@ jobs:
container: ghcr.io/frida/qnx-tools:latest
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Build
@@ -415,7 +441,7 @@ jobs:
./configure --host=arm-unknown-nto-qnx6.5.0eabi ${{ env.FRIDA_CORE_OPTIONS }}
make
- name: Upload devkit
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: core-devkit-qnx-armeabi
path: build/src/devkit/
@@ -441,7 +467,7 @@ jobs:
container: ghcr.io/frida/core-linux-helpers-${{ matrix.arch }}:latest
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Build
@@ -475,7 +501,7 @@ jobs:
container: ghcr.io/frida/core-linux-helpers-${{ matrix.arch }}:latest
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Build
@@ -504,24 +530,24 @@ jobs:
- mobile
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Download macOS/x86_64 devkit
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: core-devkit-macos-x86_64
path: devkits/macos-x86_64
- name: Download macOS/arm64 devkit
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: core-devkit-macos-arm64
path: devkits/macos-arm64
- name: Download iOS/arm64 devkit
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: core-devkit-ios-arm64
path: devkits/ios-arm64
- name: Download iOS/arm64-simulator devkit
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: core-devkit-ios-arm64-simulator
path: devkits/ios-arm64-simulator
@@ -584,7 +610,7 @@ jobs:
-headers xcframework_headers/ios-arm64-simulator \
-output FridaCore.xcframework
- name: Upload FridaCore.xcframework
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: FridaCore.xcframework
path: FridaCore.xcframework
@@ -596,7 +622,7 @@ jobs:
- xcframework
steps:
- name: Download FridaCore.xcframework artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: FridaCore.xcframework
path: dist
@@ -619,7 +645,7 @@ jobs:
cd dist
zip -r FridaCore.xcframework.zip FridaCore.xcframework
- name: Create GitHub release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
files: |
dist/FridaCore.xcframework.zip
+1
View File
@@ -1,3 +1,4 @@
/.cache/
/build/
/deps/
/src/android-helper/build/
+5 -1
View File
@@ -624,7 +624,11 @@ def option_should_be_forwarded(k: "OptionKey",
return is_for_us or is_for_child
if coredata.CoreData.is_per_machine_option(k):
return k.machine is coredata.MachineChoice.BUILD
# The compat build generates its own machine files, so forwarding the
# parent's build-machine options would inject a foreign toolchain. In a
# native compat build that conjures a compiler-less build machine and
# breaks any native: true target.
return False
if k.is_builtin():
if k.name in {"buildtype", "genvslite"}:
+49 -11
View File
@@ -117,6 +117,8 @@ namespace Frida.Agent {
void * opaque_injector_state) {
Environment._init ();
apply_linker_notifier_offsets (agent_parameters);
{
Gum.MemoryRange? mapped_range = null;
@@ -186,6 +188,25 @@ namespace Frida.Agent {
Environment._deinit ();
}
private static void apply_linker_notifier_offsets (string agent_parameters) {
foreach (unowned string token in agent_parameters.split ("|")) {
if (!token.has_prefix ("linker-notifier-offsets:"))
continue;
uint[] offsets = {};
foreach (unowned string entry in token[24:].split (",")) {
uint offset;
if (uint.try_parse (entry, out offset))
offsets += offset;
}
if (offsets.length != 0)
Gum.ModuleRegistry.set_rtld_notifier_offsets (offsets);
return;
}
}
public static void resume_after_transition (ref Frida.UnloadPolicy unload_policy, void * opaque_injector_state) {
{
#if LINUX || FREEBSD
@@ -271,6 +292,8 @@ namespace Frida.Agent {
start.begin ((owned) padder);
start_alive_task_dumper ();
main_loop.run ();
main_context.pop_thread_default ();
@@ -282,30 +305,29 @@ namespace Frida.Agent {
private async void start (owned FileDescriptorTablePadder padder) {
string[] tokens = agent_parameters.split ("|");
unowned string transport_uri = tokens[0];
bool enable_exceptor = true;
#if DARWIN
enable_exceptor = !Gum.Darwin.query_hardened ();
#endif
var exceptor_mode = Gum.ExceptorMode.FULL;
bool enable_unwind_broker = true;
bool enable_exit_monitor = true;
bool enable_thread_suspend_monitor = true;
bool enable_unwind_sitter = true;
foreach (unowned string option in tokens[1:]) {
if (option == "eternal")
ensure_eternalized ();
else if (option == "sticky")
stop_thread_on_unload = false;
else if (option == "exceptor:off")
enable_exceptor = false;
exceptor_mode = OFF;
else if (option == "exceptor:handler-only")
exceptor_mode = HANDLER_ONLY;
else if (option == "unwind-broker:off")
enable_unwind_broker = false;
else if (option == "exit-monitor:off")
enable_exit_monitor = false;
else if (option == "thread-suspend-monitor:off")
enable_thread_suspend_monitor = false;
else if (option == "unwind-sitter:off")
enable_unwind_sitter = false;
}
if (!enable_exceptor)
Gum.Exceptor.disable ();
if (exceptor_mode != FULL)
Gum.Exceptor.set_mode (exceptor_mode);
{
var interceptor = Gum.Interceptor.obtain ();
@@ -317,7 +339,7 @@ namespace Frida.Agent {
if (enable_thread_suspend_monitor)
thread_suspend_monitor = new ThreadSuspendMonitor (this);
if (enable_unwind_sitter)
if (enable_unwind_broker)
unwind_sitter = new UnwindSitter (this);
this.interceptor = interceptor;
@@ -342,6 +364,16 @@ namespace Frida.Agent {
padder = null;
}
// TEMPORARY DEBUG HACK: dump alive GTasks every 30 seconds.
private void start_alive_task_dumper () {
var source = new TimeoutSource.seconds (30);
source.set_callback (() => {
GLib.info ("%s", g_task_describe_alive_tasks ());
return Source.CONTINUE;
});
source.attach (main_context);
}
private void keep_running_eternalized () {
agent_gthread = new Thread<bool> ("frida-eternal-agent", () => {
var ignore_scope = new ThreadIgnoreScope (FRIDA_THREAD);
@@ -904,6 +936,8 @@ namespace Frida.Agent {
private void on_session_closed (BaseAgentSession base_session) {
LiveAgentSession session = (LiveAgentSession) base_session;
GLib.info ("detach-trace: Agent emitting provider closed id=%s", session.id.handle);
closed (session.id);
unregister_session (session);
@@ -1704,4 +1738,8 @@ namespace Frida.Agent {
return repeat;
});
}
// TEMPORARY DEBUG HACK: only present when GLib is built with G_ENABLE_DEBUG.
[CCode (cname = "g_task_describe_alive_tasks")]
private extern string g_task_describe_alive_tasks ();
}
+4 -4
View File
@@ -36,13 +36,13 @@ namespace Frida {
}
public string derive_asset_path (string arch, string filename) {
string root = Path.build_filename (libdir, Config.FRIDA_LIBDIR_NAME);
if (FileUtils.test (root, IS_DIR))
#if DARWIN
string installed = Path.build_filename (libdir, Config.FRIDA_LIBDIR_NAME, filename);
return Path.build_filename (root, filename);
#else
string installed = Path.build_filename (libdir, Config.FRIDA_LIBDIR_NAME, arch, filename);
return Path.build_filename (root, arch, filename);
#endif
if (FileUtils.test (installed, EXISTS))
return installed;
return Path.build_filename (libdir, filename);
}
+32 -10
View File
@@ -139,37 +139,35 @@ namespace Frida {
}
internal Libbpf.Object handle;
private char[] kernel_log_buf;
public static BpfObject open (string name, uint8[] blob) throws Error {
var log_buf = new char[128 * 1024];
var opts = Libbpf.Object.OpenOpts ();
opts.sz = sizeof (Libbpf.Object.OpenOpts);
opts.object_name = name;
opts.kernel_log_buf = log_buf;
var log = new LibbpfLogScope ();
var handle = Libbpf.Object.open_mem (blob, opts);
if (handle == null)
throw_libbpf_error (errno);
throw_libbpf_error (errno, log.take ());
return new BpfObject ((owned) handle, (owned) log_buf);
return new BpfObject ((owned) handle);
}
private BpfObject (owned Libbpf.Object handle, owned char[] kernel_log_buf) {
private BpfObject (owned Libbpf.Object handle) {
this.handle = (owned) handle;
this.kernel_log_buf = (owned) kernel_log_buf;
_programs = new Programs (this);
_maps = new Maps (this);
}
public void prepare () throws Error {
check_libbpf_result (handle.prepare ());
var log = new LibbpfLogScope ();
check_libbpf_result (handle.prepare (), log.take ());
}
public void load () throws Error {
check_libbpf_result (handle.load (), (string) kernel_log_buf);
var log = new LibbpfLogScope ();
check_libbpf_result (handle.load (), log.take ());
}
public sealed class Programs {
@@ -397,6 +395,30 @@ namespace Frida {
}
}
private sealed class LibbpfLogScope {
private static StringBuilder buffer;
public LibbpfLogScope () {
if (buffer == null)
buffer = new StringBuilder ();
buffer.truncate ();
Libbpf.set_print (on_message);
}
~LibbpfLogScope () {
Libbpf.set_print (null);
}
public string take () {
return buffer.str.chomp ();
}
private static int on_message (Libbpf.PrintLevel level, string format, va_list args) {
buffer.append_vprintf (format, args);
return 0;
}
}
private int check_libbpf_result (int result, string details = "") throws Error {
if (result < 0)
throw_libbpf_error (result, details);
+36
View File
@@ -1,5 +1,11 @@
namespace Frida {
/**
* Issues RPC calls over a {@link RpcPeer}, matching responses to requests.
*/
public sealed class RpcClient : Object {
/**
* The peer used to send and receive RPC messages.
*/
public weak RpcPeer peer {
get;
construct;
@@ -7,10 +13,23 @@ namespace Frida {
private Gee.HashMap<string, PendingResponse> pending_responses = new Gee.HashMap<string, PendingResponse> ();
/**
* Creates an RPC client that talks over @peer.
*
* @param peer the peer to send and receive messages through
*/
public RpcClient (RpcPeer peer) {
Object (peer: peer);
}
/**
* Calls a remote method and waits for its result.
*
* @param method the method name to invoke
* @param args the JSON arguments
* @param data binary data to accompany the call, if any
* @return the method's JSON result
*/
public async Json.Node call (string method, Json.Node[] args, Bytes? data, Cancellable? cancellable) throws Error, IOError {
string request_id = Uuid.string_random ();
@@ -69,6 +88,13 @@ namespace Frida {
return pending.result;
}
/**
* Feeds an incoming message to the client, matching it against pending
* calls if it is an RPC response.
*
* @param json the incoming message as a JSON string
* @return true if the message was an RPC message and was handled
*/
public bool try_handle_message (string json) {
if (json.index_of ("\"frida:rpc\"") == -1)
return false;
@@ -157,7 +183,17 @@ namespace Frida {
}
}
/**
* A transport over which an {@link RpcClient} sends and receives RPC
* messages.
*/
public interface RpcPeer : Object {
/**
* Posts an RPC message to the peer.
*
* @param json the message as a JSON string
* @param data binary data to accompany the message, if any
*/
public abstract async void post_rpc_message (string json, Bytes? data, Cancellable? cancellable) throws Error, IOError;
}
}
+606 -1
View File
@@ -12,6 +12,8 @@ namespace Frida {
Cancellable? cancellable) throws GLib.Error;
public abstract async void enable_spawn_gating (Cancellable? cancellable) throws GLib.Error;
public abstract async void enable_spawn_gating_with_options (HashTable<string, Variant> options,
Cancellable? cancellable) throws GLib.Error;
public abstract async void disable_spawn_gating (Cancellable? cancellable) throws GLib.Error;
public abstract async HostSpawnInfo[] enumerate_pending_spawn (Cancellable? cancellable) throws GLib.Error;
public abstract async HostChildInfo[] enumerate_pending_children (Cancellable? cancellable) throws GLib.Error;
@@ -31,6 +33,7 @@ namespace Frida {
public abstract async ServiceSessionId open_service (string address, Cancellable? cancellable) throws GLib.Error;
public signal void spawn_gating_disabled (SpawnGatingDisabledReason reason);
public signal void spawn_added (HostSpawnInfo info);
public signal void spawn_removed (HostSpawnInfo info);
public signal void child_added (HostChildInfo info);
@@ -78,6 +81,8 @@ namespace Frida {
Cancellable? cancellable) throws GLib.Error;
public abstract async void destroy_script (AgentScriptId script_id, Cancellable? cancellable) throws GLib.Error;
public abstract async void load_script (AgentScriptId script_id, Cancellable? cancellable) throws GLib.Error;
public abstract async void interrupt_script (AgentScriptId script_id, Cancellable? cancellable) throws GLib.Error;
public abstract async void terminate_script (AgentScriptId script_id, Cancellable? cancellable) throws GLib.Error;
public abstract async void eternalize_script (AgentScriptId script_id, Cancellable? cancellable) throws GLib.Error;
public abstract async void enable_debugger (AgentScriptId script_id, Cancellable? cancellable) throws GLib.Error;
@@ -356,6 +361,8 @@ namespace Frida {
private async void teardown_peer_connection_and_emit_closed () {
schedule_on_frida_thread (() => {
GLib.info ("detach-trace: AgentMessageTransmitter %p emitting closed, nice_agent=%p",
this, nice_agent);
if (nice_agent != null)
close_nice_resources_and_emit_closed.begin ();
else
@@ -593,6 +600,7 @@ namespace Frida {
private async void teardown_peer_connection_and_emit_closed () {
schedule_on_frida_thread (() => {
GLib.info ("detach-trace: AgentMessageTransmitter %p emitting closed", this);
closed ();
return Source.REMOVE;
});
@@ -755,10 +763,25 @@ namespace Frida {
public abstract async void break_and_detach (Cancellable? cancellable) throws GLib.Error;
}
/**
* What the Gadget should do when a configured breakpoint is hit.
*/
public enum GadgetBreakpointAction {
/**
* Invoke the function and return immediately.
*/
INVOKE_RETURN,
/**
* Resume execution.
*/
RESUME,
/**
* Detach the Gadget.
*/
DETACH,
/**
* Apply the page plan.
*/
PAGE_PLAN,
}
@@ -802,16 +825,37 @@ namespace Frida {
}
[DBus (name = "re.frida.AuthenticationService17")]
/**
* Authenticates clients connecting to a {@link PortalService} or
* {@link ControlService}.
*/
public interface AuthenticationService : Object {
/**
* Authenticates a client by its token.
*
* @param token the token presented by the client
* @return a JSON string describing the authenticated session
*/
public abstract async string authenticate (string token, Cancellable? cancellable) throws GLib.Error;
}
/**
* An {@link AuthenticationService} that accepts a single fixed token.
*/
public sealed class StaticAuthenticationService : Object, AuthenticationService {
/**
* The SHA-256 hash of the accepted token.
*/
public string token_hash {
get;
construct;
}
/**
* Creates a service that accepts the given token.
*
* @param token the token clients must present
*/
public StaticAuthenticationService (string token) {
Object (token_hash: Checksum.compute_for_string (SHA256, token));
}
@@ -865,6 +909,11 @@ namespace Frida {
throw_not_authorized ();
}
public async void enable_spawn_gating_with_options (HashTable<string, Variant> options,
Cancellable? cancellable) throws Error, IOError {
throw_not_authorized ();
}
public async void disable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
throw_not_authorized ();
}
@@ -944,8 +993,57 @@ namespace Frida {
throw new Error.PERMISSION_DENIED ("Not authorized, authentication required");
}
// Fires `expired` when a gated process goes unresumed too long, so the backend can cancel
// gating instead of letting a stalled client wedge process creation.
public sealed class SpawnGatingWatchdog : Object {
public signal void expired ();
public uint timeout_msec {
get;
construct;
}
private Gee.Map<uint, Source> deadlines = new Gee.HashMap<uint, Source> ();
// Default deadline: under the OS's own spawn-start timeouts (Android's is 10s).
public SpawnGatingWatchdog (uint timeout_msec = 8000) {
Object (timeout_msec: timeout_msec);
}
public void arm (uint pid) {
var deadline = new TimeoutSource (timeout_msec);
deadline.set_callback (() => {
expired ();
return Source.REMOVE;
});
deadline.attach (MainContext.get_thread_default ());
deadlines[pid] = deadline;
}
public void cancel (uint pid) {
Source? deadline;
if (deadlines.unset (pid, out deadline))
deadline.destroy ();
}
public void clear () {
foreach (var deadline in deadlines.values)
deadline.destroy ();
deadlines.clear ();
}
}
/**
* The realm an agent should run in.
*/
public enum Realm {
/**
* The process's native realm.
*/
NATIVE,
/**
* An emulated realm, such as code running under binary translation.
*/
EMULATED;
public static Realm from_nick (string nick) throws Error {
@@ -957,6 +1055,32 @@ namespace Frida {
}
}
/**
* How the agent should take over exception handling in the target.
*/
public enum Exceptor {
/**
* Install handlers and keep the target from overriding them.
*/
FULL,
/**
* Install handlers but let the target replace them.
*/
HANDLER_ONLY,
/**
* Do not handle exceptions.
*/
OFF;
public static Exceptor from_nick (string nick) throws Error {
return Marshal.enum_from_nick<Exceptor> (nick);
}
public string to_nick () {
return Marshal.enum_to_nick<Exceptor> (this);
}
}
public enum SpawnStartState {
RUNNING,
SUSPENDED;
@@ -1031,11 +1155,52 @@ namespace Frida {
}
#endif
public enum SessionDetachReason {
/**
* Why spawn gating was disabled on a {@link Device}.
*/
public enum SpawnGatingDisabledReason {
/**
* A client asked to disable spawn gating.
*/
APPLICATION_REQUESTED = 1,
/**
* A caught process went unresumed long enough that the gater cancelled gating to
* avoid stalling process creation.
*/
WATCHDOG_TIMEOUT;
public static SpawnGatingDisabledReason from_nick (string nick) throws Error {
return Marshal.enum_from_nick<SpawnGatingDisabledReason> (nick);
}
public string to_nick () {
return Marshal.enum_to_nick<SpawnGatingDisabledReason> (this);
}
}
/**
* Why a {@link Session} was detached from its target.
*/
public enum SessionDetachReason {
/**
* The host asked to detach.
*/
APPLICATION_REQUESTED = 1,
/**
* The target process was replaced, for example by an exec.
*/
PROCESS_REPLACED,
/**
* The target process exited.
*/
PROCESS_TERMINATED,
/**
* The connection to the target was lost.
*/
CONNECTION_TERMINATED,
/**
* The device became unreachable.
*/
DEVICE_LOST;
public static SessionDetachReason from_nick (string nick) throws Error {
@@ -1048,19 +1213,61 @@ namespace Frida {
}
[DBus (name = "re.frida.Error")]
/**
* The errors that Frida operations can fail with.
*/
public errordomain Error {
/**
* The frida-server is not running.
*/
SERVER_NOT_RUNNING,
/**
* The specified executable was not found.
*/
EXECUTABLE_NOT_FOUND,
/**
* The specified executable is not supported.
*/
EXECUTABLE_NOT_SUPPORTED,
/**
* No process matched the request.
*/
PROCESS_NOT_FOUND,
/**
* The process is not responding.
*/
PROCESS_NOT_RESPONDING,
/**
* An argument was invalid.
*/
INVALID_ARGUMENT,
/**
* The operation is not valid in the current state.
*/
INVALID_OPERATION,
/**
* Permission was denied.
*/
PERMISSION_DENIED,
/**
* The requested address is already in use.
*/
ADDRESS_IN_USE,
/**
* The operation timed out.
*/
TIMED_OUT,
/**
* The operation is not supported.
*/
NOT_SUPPORTED,
/**
* A protocol error occurred.
*/
PROTOCOL,
/**
* A transport-level error occurred.
*/
TRANSPORT
}
@@ -1127,7 +1334,13 @@ namespace Frida {
}
}
/**
* Options for {@link Device.get_frontmost_application}.
*/
public sealed class FrontmostQueryOptions : Object {
/**
* How much detail to include about the application.
*/
public Scope scope {
get;
set;
@@ -1157,7 +1370,14 @@ namespace Frida {
}
}
/**
* Options for {@link Device.enumerate_applications}, including an optional
* set of identifiers to restrict the results to.
*/
public sealed class ApplicationQueryOptions : Object {
/**
* How much detail to include about each application.
*/
public Scope scope {
get;
set;
@@ -1166,14 +1386,30 @@ namespace Frida {
private Gee.List<string> identifiers = new Gee.ArrayList<string> ();
/**
* Restricts the query to the given application identifier. May be called
* multiple times to select several.
*
* @param identifier the application identifier to include
*/
public void select_identifier (string identifier) {
identifiers.add (identifier);
}
/**
* Checks whether any identifiers have been selected.
*
* @return true if the query is restricted to specific identifiers
*/
public bool has_selected_identifiers () {
return !identifiers.is_empty;
}
/**
* Invokes @func for each selected identifier.
*
* @param func function called with each identifier
*/
public void enumerate_selected_identifiers (Func<string> func) {
foreach (var identifier in identifiers)
func (identifier);
@@ -1215,7 +1451,14 @@ namespace Frida {
}
}
/**
* Options for {@link Device.enumerate_processes}, including an optional set
* of PIDs to restrict the results to.
*/
public sealed class ProcessQueryOptions : Object {
/**
* How much detail to include about each process.
*/
public Scope scope {
get;
set;
@@ -1224,14 +1467,30 @@ namespace Frida {
private Gee.List<uint> pids = new Gee.ArrayList<uint> ();
/**
* Restricts the query to the given PID. May be called multiple times to
* select several.
*
* @param pid the process ID to include
*/
public void select_pid (uint pid) {
pids.add (pid);
}
/**
* Checks whether any PIDs have been selected.
*
* @return true if the query is restricted to specific PIDs
*/
public bool has_selected_pids () {
return !pids.is_empty;
}
/**
* Invokes @func for each selected PID.
*
* @param func function called with each PID
*/
public void enumerate_selected_pids (Func<uint> func) {
foreach (var pid in pids)
func (pid);
@@ -1273,9 +1532,21 @@ namespace Frida {
}
}
/**
* How much detail to include in query results.
*/
public enum Scope {
/**
* Only the essential fields.
*/
MINIMAL,
/**
* Essential fields plus metadata.
*/
METADATA,
/**
* Everything available.
*/
FULL;
public static Scope from_nick (string nick) throws Error {
@@ -1287,6 +1558,67 @@ namespace Frida {
}
}
/**
* Options for {@link Device.enable_spawn_gating}.
*/
public sealed class SpawnGatingOptions : Object {
/**
* Which processes to gate. Anything other than DEFAULT requires a Frida new enough to
* support scoping, and fails rather than silently gating something broader than asked.
*/
public SpawnGatingScope scope {
get;
set;
default = DEFAULT;
}
public HashTable<string, Variant> _serialize () {
var dict = make_parameters_dict ();
if (scope != DEFAULT)
dict["scope"] = new Variant.string (scope.to_nick ());
return dict;
}
public static SpawnGatingOptions _deserialize (HashTable<string, Variant> dict) throws Error {
var options = new SpawnGatingOptions ();
Variant? scope = dict["scope"];
if (scope != null) {
if (!scope.is_of_type (VariantType.STRING))
throw new Error.INVALID_ARGUMENT ("The 'scope' option must be a string");
options.scope = SpawnGatingScope.from_nick (scope.get_string ());
}
return options;
}
}
public enum SpawnGatingScope {
/**
* Leave the choice to the implementation. Sent as a plain enable_spawn_gating call, so
* it still works against a Frida too old to know about scopes.
*/
DEFAULT,
/**
* Application launches only.
*/
APPLICATIONS,
/**
* Every process created on the system. More potent but far more intrusive.
*/
PROCESSES;
public static SpawnGatingScope from_nick (string nick) throws Error {
return Marshal.enum_from_nick<SpawnGatingScope> (nick);
}
public string to_nick () {
return Marshal.enum_to_nick<SpawnGatingScope> (this);
}
}
public struct HostSpawnOptions {
public bool has_argv;
public string[] argv;
@@ -1357,24 +1689,103 @@ namespace Frida {
}
}
/**
* Options controlling how {@link Device.attach} sets up a {@link Session}.
*/
public sealed class SessionOptions : Object {
/**
* The realm to attach in.
*/
public Realm realm {
get;
set;
default = NATIVE;
}
/**
* How long the session may survive a disconnection before being torn
* down, in seconds; 0 disables persistence.
*/
public uint persist_timeout {
get;
set;
default = 0;
}
/**
* Path to a custom agent to use in the emulated realm.
*/
public string? emulated_agent_path {
get;
set;
}
/**
* How the agent should handle exceptions in the target.
*/
public Exceptor exceptor {
get;
set;
default = FULL;
}
/**
* Whether to install the unwind broker so exceptions can propagate
* through instrumented code.
*/
public bool unwind_broker {
get;
set;
default = true;
}
/**
* Whether to monitor the target for exit so the session is cleaned up.
*/
public bool exit_monitor {
get;
set;
default = true;
}
/**
* Whether to monitor thread suspension in the target.
*/
public bool thread_suspend_monitor {
get;
set;
default = true;
}
private Gee.List<uint> linker_notifier_offsets = new Gee.ArrayList<uint> ();
/**
* Clears the configured linker notifier offsets.
*/
public void clear_linker_notifier_offsets () {
linker_notifier_offsets.clear ();
}
/**
* Adds a linker notifier offset, used to detect module load and unload
* events in the target.
*
* @param offset the offset into the linker to instrument
*/
public void add_linker_notifier_offset (uint offset) {
linker_notifier_offsets.add (offset);
}
/**
* Invokes @func for each configured linker notifier offset.
*
* @param func function called with each offset
*/
public void enumerate_linker_notifier_offsets (Func<uint> func) {
foreach (var offset in linker_notifier_offsets)
func (offset);
}
public HashTable<string, Variant> _serialize () {
var dict = make_parameters_dict ();
@@ -1387,6 +1798,25 @@ namespace Frida {
if (emulated_agent_path != null)
dict["emulated-agent-path"] = new Variant.string (emulated_agent_path);
if (exceptor != FULL)
dict["exceptor"] = new Variant.string (exceptor.to_nick ());
if (!unwind_broker)
dict["unwind-broker"] = new Variant.string ("off");
if (!exit_monitor)
dict["exit-monitor"] = new Variant.string ("off");
if (!thread_suspend_monitor)
dict["thread-suspend-monitor"] = new Variant.string ("off");
if (!linker_notifier_offsets.is_empty) {
var builder = new VariantBuilder (new VariantType ("au"));
foreach (uint offset in linker_notifier_offsets)
builder.add ("u", offset);
dict["linker-notifier-offsets"] = builder.end ();
}
return dict;
}
@@ -1414,12 +1844,61 @@ namespace Frida {
options.emulated_agent_path = path.get_string ();
}
Variant? exceptor = dict["exceptor"];
if (exceptor != null) {
if (!exceptor.is_of_type (VariantType.STRING))
throw new Error.INVALID_ARGUMENT ("The 'exceptor' option must be a string");
options.exceptor = Exceptor.from_nick (exceptor.get_string ());
}
Variant? unwind_broker = dict["unwind-broker"];
if (unwind_broker != null) {
if (!unwind_broker.is_of_type (VariantType.STRING) || unwind_broker.get_string () != "off")
throw new Error.INVALID_ARGUMENT ("The 'unwind-broker' option is invalid");
options.unwind_broker = false;
}
Variant? exit_monitor = dict["exit-monitor"];
if (exit_monitor != null) {
if (!exit_monitor.is_of_type (VariantType.STRING) || exit_monitor.get_string () != "off")
throw new Error.INVALID_ARGUMENT ("The 'exit-monitor' option is invalid");
options.exit_monitor = false;
}
Variant? thread_suspend_monitor = dict["thread-suspend-monitor"];
if (thread_suspend_monitor != null) {
if (!thread_suspend_monitor.is_of_type (VariantType.STRING) ||
thread_suspend_monitor.get_string () != "off")
throw new Error.INVALID_ARGUMENT ("The 'thread-suspend-monitor' option is invalid");
options.thread_suspend_monitor = false;
}
Variant? offsets = dict["linker-notifier-offsets"];
if (offsets != null) {
if (!offsets.is_of_type (new VariantType ("au")))
throw new Error.INVALID_ARGUMENT (
"The 'linker-notifier-offsets' option must be an array of uint32");
uint n = (uint) offsets.n_children ();
for (uint i = 0; i != n; i++)
options.add_linker_notifier_offset (offsets.get_child_value (i).get_uint32 ());
}
return options;
}
}
/**
* How to set up the standard I/O streams of a spawned process.
*/
public enum Stdio {
/**
* Inherit the host's streams.
*/
INHERIT,
/**
* Pipe the streams through Frida, surfaced via {@link Device.output}
* and {@link Device.input}.
*/
PIPE;
public static Stdio from_nick (string nick) throws Error {
@@ -1483,9 +1962,21 @@ namespace Frida {
}
}
/**
* How a {@link Child} came to be.
*/
public enum ChildOrigin {
/**
* Created by fork().
*/
FORK,
/**
* The result of an exec() replacing the image.
*/
EXEC,
/**
* Spawned as a new process.
*/
SPAWN;
public static ChildOrigin from_nick (string nick) throws Error {
@@ -1602,23 +2093,39 @@ namespace Frida {
}
}
/**
* Options for creating a {@link Script} with {@link Session.create_script}.
*/
public sealed class ScriptOptions : Object {
/**
* A name for the script, used in logging and debugging.
*/
public string? name {
get;
set;
}
/**
* A heap snapshot to start the script from, as produced by
* {@link Session.snapshot_script}.
*/
public Bytes? snapshot {
get;
set;
}
/**
* How the snapshot is delivered to the runtime.
*/
public SnapshotTransport snapshot_transport {
get;
set;
default = INLINE;
}
/**
* Which JavaScript runtime to run the script in.
*/
public ScriptRuntime runtime {
get;
set;
@@ -1687,17 +2194,35 @@ namespace Frida {
}
}
/**
* How a script's heap snapshot is delivered to the runtime.
*/
public enum SnapshotTransport {
/**
* Embed the snapshot inline with the script.
*/
INLINE,
/**
* Pass the snapshot through shared memory.
*/
SHARED_MEMORY
}
/**
* Options for building a heap snapshot with {@link Session.snapshot_script}.
*/
public sealed class SnapshotOptions : Object {
/**
* Script to run to warm up the runtime before capturing the snapshot.
*/
public string? warmup_script {
get;
set;
}
/**
* Which JavaScript runtime to capture the snapshot for.
*/
public ScriptRuntime runtime {
get;
set;
@@ -1737,9 +2262,21 @@ namespace Frida {
}
}
/**
* Which JavaScript runtime a script should use.
*/
public enum ScriptRuntime {
/**
* Let Frida choose.
*/
DEFAULT,
/**
* The QuickJS runtime.
*/
QJS,
/**
* The V8 runtime.
*/
V8;
public static ScriptRuntime from_nick (string nick) throws Error {
@@ -1767,17 +2304,29 @@ namespace Frida {
}
}
/**
* Options for {@link Session.join_portal}.
*/
public sealed class PortalOptions : Object {
/**
* TLS certificate to present to the portal.
*/
public TlsCertificate? certificate {
get;
set;
}
/**
* Authentication token to present to the portal.
*/
public string? token {
get;
set;
}
/**
* Access-control tags scoping what this membership may reach.
*/
public string[]? acl {
get;
set;
@@ -1830,7 +2379,14 @@ namespace Frida {
}
}
/**
* Options for {@link Session.setup_peer_connection}, configuring how the
* peer-to-peer connection is negotiated.
*/
public sealed class PeerOptions : Object {
/**
* The STUN server to use for discovering the public address, if any.
*/
public string? stun_server {
get;
set;
@@ -1838,14 +2394,27 @@ namespace Frida {
private Gee.List<Relay> relays = new Gee.ArrayList<Relay> ();
/**
* Removes all configured relays.
*/
public void clear_relays () {
relays.clear ();
}
/**
* Adds a relay to use when a direct connection cannot be established.
*
* @param relay the relay to add
*/
public void add_relay (Relay relay) {
relays.add (relay);
}
/**
* Invokes @func for each configured relay.
*
* @param func function called with each relay
*/
public void enumerate_relays (Func<Relay> func) {
foreach (var relay in relays)
func (relay);
@@ -1891,27 +2460,51 @@ namespace Frida {
}
}
/**
* A TURN relay used to establish a peer-to-peer connection when a direct
* one is not possible.
*/
public sealed class Relay : Object {
/**
* The relay's address, as host and port.
*/
public string address {
get;
construct;
}
/**
* The username to authenticate with.
*/
public string username {
get;
construct;
}
/**
* The password to authenticate with.
*/
public string password {
get;
construct;
}
/**
* The kind of relay and transport to use.
*/
public RelayKind kind {
get;
construct;
}
/**
* Creates a relay.
*
* @param address the relay's address, as host and port
* @param username the username to authenticate with
* @param password the password to authenticate with
* @param kind the kind of relay and transport
*/
public Relay (string address, string username, string password, RelayKind kind) {
Object (
address: address,
@@ -1938,9 +2531,21 @@ namespace Frida {
}
}
/**
* The kind of a {@link Relay} used for peer-to-peer connections.
*/
public enum RelayKind {
/**
* A TURN relay reached over UDP.
*/
TURN_UDP,
/**
* A TURN relay reached over TCP.
*/
TURN_TCP,
/**
* A TURN relay reached over TLS.
*/
TURN_TLS
}
+92
View File
@@ -97,42 +97,77 @@ namespace Frida {
}
}
/**
* Configures the network endpoint a {@link PortalService} or
* {@link ControlService} listens on.
*/
public sealed class EndpointParameters : Object {
/**
* The address to bind to, or null for the default.
*/
public string? address {
get;
construct;
}
/**
* The port to listen on, or 0 for the default.
*/
public uint16 port {
get;
construct;
}
/**
* TLS certificate to serve, enabling TLS when set.
*/
public TlsCertificate? certificate {
get;
construct;
}
/**
* Origin to require from connecting clients, if any.
*/
public string? origin {
get;
construct;
}
/**
* Service used to authenticate connecting clients, if any.
*/
public AuthenticationService? auth_service {
get;
construct;
}
/**
* Directory whose files are served as static web assets, if any.
*/
public File? asset_root {
get;
set;
}
/**
* Handler invoked for web requests, if any.
*/
public WebRequestHandler? request_handler {
get;
set;
}
/**
* Creates endpoint parameters.
*
* @param address the address to bind to, or null for the default
* @param port the port to listen on, or 0 for the default
* @param certificate TLS certificate to serve, or null for plaintext
* @param origin origin to require from clients, or null
* @param auth_service service used to authenticate clients, or null
* @param asset_root directory of static assets to serve, or null
*/
public EndpointParameters (string? address = null, uint16 port = 0, TlsCertificate? certificate = null,
string? origin = null, AuthenticationService? auth_service = null, File? asset_root = null) {
Object (
@@ -146,26 +181,51 @@ namespace Frida {
}
}
/**
* Handles web requests made to an endpoint's served assets.
*/
public interface WebRequestHandler : Object {
/**
* Handles a single web request.
*
* @param request the incoming request
* @return the response to send, or null to fall through to the default
* handling
*/
public abstract async WebResponse? handle_request (WebRequest request, Cancellable? cancellable) throws Error, IOError;
}
/**
* An incoming web request passed to a {@link WebRequestHandler}.
*/
public sealed class WebRequest : Object {
/**
* The HTTP method, such as `GET` or `POST`.
*/
public string method {
get;
construct;
}
/**
* The request path.
*/
public string path {
get;
construct;
}
/**
* The raw query string, if any.
*/
public string? query_string {
get;
construct;
}
/**
* The request body, if any.
*/
public Bytes? body {
get;
construct;
@@ -178,6 +238,11 @@ namespace Frida {
raw_headers = headers;
}
/**
* Invokes @func for each request header.
*
* @param func function called with each header's name and value
*/
public void foreach_header (WebRequestHeaderFunc func) {
raw_headers.foreach ((name, val) => {
func (name, val);
@@ -185,14 +250,29 @@ namespace Frida {
}
}
/**
* Callback invoked for each header while iterating a {@link WebRequest}.
*
* @param name the header name
* @param val the header value
*/
public delegate void WebRequestHeaderFunc (string name, string val);
/**
* A response returned from a {@link WebRequestHandler}.
*/
public sealed class WebResponse : Object {
/**
* The HTTP status code.
*/
public uint status {
get;
construct;
}
/**
* The response body.
*/
public Bytes body {
get;
construct;
@@ -200,10 +280,22 @@ namespace Frida {
private Soup.MessageHeaders raw_headers = new Soup.MessageHeaders (RESPONSE);
/**
* Creates a response.
*
* @param status the HTTP status code
* @param body the response body
*/
public WebResponse (uint status, Bytes body) {
Object (status: status, body: body);
}
/**
* Adds a header to the response.
*
* @param name the header name
* @param val the header value
*/
public void add_header (string name, string val) {
raw_headers.append (name, val);
}
+6
View File
@@ -16,6 +16,9 @@ namespace Frida {
public string? name;
}
/**
* The XNU Mach trap numbers, as used on Apple platforms.
*/
public enum XnuMachTrap {
MACH_VM_ALLOCATE = -10,
MACH_VM_PURGABLE_CONTROL = -11,
@@ -81,6 +84,9 @@ namespace Frida {
IOKIT_USER_CLIENT = -100,
}
/**
* The XNU BSD syscall numbers, as used on Apple platforms.
*/
public enum XnuBsdSyscall {
EXIT = 1,
FORK = 2,
+2 -4
View File
@@ -144,10 +144,8 @@ frida_gadget_environment_ensure_debugger_breakpoints_only (void)
interceptor = gum_interceptor_obtain ();
gum_interceptor_attach (interceptor, exceptions_set, frida_dont_steal_my_breakpoints,
NULL, GUM_ATTACH_FLAGS_NONE);
gum_interceptor_attach (interceptor, exceptions_swap, frida_dont_steal_my_breakpoints,
NULL, GUM_ATTACH_FLAGS_NONE);
gum_interceptor_attach (interceptor, exceptions_set, frida_dont_steal_my_breakpoints, NULL);
gum_interceptor_attach (interceptor, exceptions_swap, frida_dont_steal_my_breakpoints, NULL);
g_object_unref (interceptor);
}
+5
View File
@@ -1810,6 +1810,11 @@ namespace Frida.Gadget {
throw new Error.NOT_SUPPORTED ("Not possible when embedded");
}
public async void enable_spawn_gating_with_options (HashTable<string, Variant> options,
Cancellable? cancellable) throws Error, IOError {
throw new Error.NOT_SUPPORTED ("Not possible when embedded");
}
public async void disable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
throw new Error.NOT_SUPPORTED ("Not possible when embedded");
}
+18
View File
@@ -57,6 +57,8 @@ namespace Frida {
}
public async void close (Cancellable? cancellable) throws IOError {
GLib.info ("detach-trace: BaseAgentSession.close() id=%s close_request=%p", id.handle, close_request);
while (close_request != null) {
try {
yield close_request.future.wait_async (cancellable);
@@ -85,6 +87,8 @@ namespace Frida {
yield transmitter.close (cancellable);
GLib.info ("detach-trace: BaseAgentSession.close() done id=%s", id.handle);
close_request.resolve (true);
}
@@ -179,6 +183,18 @@ namespace Frida {
yield script_engine.load_script (script_id);
}
public async void interrupt_script (AgentScriptId script_id, Cancellable? cancellable) throws Error, IOError {
check_open ();
script_engine.interrupt_script (script_id);
}
public async void terminate_script (AgentScriptId script_id, Cancellable? cancellable) throws Error, IOError {
check_open ();
yield script_engine.terminate_script (script_id);
}
public async void eternalize_script (AgentScriptId script_id, Cancellable? cancellable) throws Error, IOError {
check_open ();
@@ -264,6 +280,8 @@ namespace Frida {
transmitter.new_candidates.disconnect (on_transmitter_new_candidates);
transmitter.candidate_gathering_done.disconnect (on_transmitter_candidate_gathering_done);
GLib.info ("detach-trace: BaseAgentSession emitting closed id=%s", id.handle);
closed ();
}
+11 -4
View File
@@ -65,7 +65,9 @@ namespace Frida {
if (runtime != null) {
var set_argv0 = (void *) runtime.find_export_by_name ("_Z27android_os_Process_setArgV0P7_JNIEnvP8_jobjectP8_jstring");
if (set_argv0 != null) {
interceptor.attach (set_argv0, listener, (void *) HookId.SET_ARGV0);
var set_argv0_options = Gum.AttachOptions ();
set_argv0_options.listener_function_data = (void *) HookId.SET_ARGV0;
interceptor.attach (set_argv0, listener, set_argv0_options);
child_recovery_behavior = DEFERRED_UNTIL_SET_ARGV0;
}
}
@@ -73,8 +75,11 @@ namespace Frida {
var selinux = Gum.Process.find_module_by_name ("libselinux.so");
if (selinux != null) {
var setcontext = (void *) selinux.find_export_by_name ("selinux_android_setcontext");
if (setcontext != null)
interceptor.attach (setcontext, listener, (void *) HookId.SET_CTX);
if (setcontext != null) {
var setcontext_options = Gum.AttachOptions ();
setcontext_options.listener_function_data = (void *) HookId.SET_CTX;
interceptor.attach (setcontext, listener, setcontext_options);
}
}
}
} catch (FileError e) {
@@ -82,7 +87,9 @@ namespace Frida {
}
#endif
interceptor.attach (fork_impl, listener, (void *) HookId.FORK);
var fork_options = Gum.AttachOptions ();
fork_options.listener_function_data = (void *) HookId.FORK;
interceptor.attach (fork_impl, listener, fork_options);
interceptor.replace (vfork_impl, fork_impl);
}
+18
View File
@@ -166,6 +166,14 @@ namespace Frida {
yield get_instance (script_id).load ();
}
public void interrupt_script (AgentScriptId script_id) throws Error {
get_instance (script_id).interrupt ();
}
public async void terminate_script (AgentScriptId script_id) throws Error {
yield get_instance (script_id).terminate ();
}
public Gum.Script eternalize_script (AgentScriptId script_id) throws Error {
var instance = get_instance (script_id);
@@ -354,6 +362,16 @@ namespace Frida {
load_request.resolve (true);
}
public void interrupt () {
script.interrupt ();
}
public async void terminate () {
script.interrupt ();
yield close ();
}
private Promise<bool> unload () {
var request = new Promise<bool> ();
+3 -1
View File
@@ -59,7 +59,9 @@ namespace Frida {
interceptor.attach ((void *) posix_spawn, this);
interceptor.replace (execve, (void *) replacement_execve, this);
var replace_options = Gum.ReplaceOptions ();
replace_options.replacement_data = this;
interceptor.replace (execve, (void *) replacement_execve, null, replace_options);
#else
Gum.Address execve = 0;
#if ANDROID
+5 -3
View File
@@ -31,9 +31,11 @@ namespace Frida {
thread_suspend = (ThreadSuspendFunc) kernel.find_export_by_name ("thread_suspend");
thread_resume = (ThreadResumeFunc) kernel.find_export_by_name ("thread_resume");
interceptor.replace ((void *) task_threads, (void *) replacement_task_threads, this);
interceptor.replace ((void *) thread_suspend, (void *) replacement_thread_suspend, this);
interceptor.replace ((void *) thread_resume, (void *) replacement_thread_resume, this);
var replace_options = Gum.ReplaceOptions ();
replace_options.replacement_data = this;
interceptor.replace ((void *) task_threads, (void *) replacement_task_threads, null, replace_options);
interceptor.replace ((void *) thread_suspend, (void *) replacement_thread_suspend, null, replace_options);
interceptor.replace ((void *) thread_resume, (void *) replacement_thread_resume, null, replace_options);
}
public override void dispose () {
+2 -405
View File
@@ -2,29 +2,14 @@
#ifdef HAVE_DARWIN
#include <capstone.h>
#include <gum/gumdarwin.h>
#include <gum/gummemory.h>
#include <ptrauth.h>
#define FRIDA_MH_MAGIC_64 0xfeedfacf
#define FRIDA_LIBUNWIND_PATH "/usr/lib/system/libunwind.dylib"
#define FRIDA_UNWIND_CURSOR_VTABLE_OFFSET_SET_INFO 0x68
#define FRIDA_UNWIND_CURSOR_VTABLE_OFFSET_GET_REG 0x18
#define FRIDA_FP_TO_SP(fp) (fp + 0x10)
#ifdef HAVE_ARM64
# define FRIDA_UNWIND_CURSOR_unwindInfoMissing 0x268
# define FRIDA_UNWAARCH64_X29 29
# define FRIDA_STRIP_MASK 0x0000007fffffffffULL
#else
# define FRIDA_UNWIND_CURSOR_unwindInfoMissing 0x100
# define FRIDA_UNWX86_64_RBP 6
#endif
typedef struct _FridaFillInfoContext FridaFillInfoContext;
typedef struct _FridaDyldUnwindSections FridaDyldUnwindSections;
typedef struct _FridaCreateArgs FridaCreateArgs;
typedef struct _FridaUnwindHookState FridaUnwindHookState;
struct _FridaFillInfoContext
{
@@ -47,37 +32,9 @@ struct _FridaCreateArgs
GumAddress range_end;
};
struct _FridaUnwindHookState
{
gpointer vtable;
gssize shift;
gpointer * set_info_slot;
gpointer set_info_original;
void (* set_info) (gpointer cursor, gint is_return_address);
gpointer (* get_reg) (gpointer cursor, gint reg);
GumInterceptor * interceptor;
};
#if __has_feature (ptrauth_calls)
# define FRIDA_RESIGN_PTR(x) GSIZE_TO_POINTER (gum_sign_code_address (gum_strip_code_address (GUM_ADDRESS (x))))
#else
# define FRIDA_RESIGN_PTR(x) (x)
#endif
static FridaDyldUnwindSections * frida_get_cached_sections (GumAddress range_start, GumAddress range_end);
static FridaDyldUnwindSections * frida_create_cached_sections (FridaCreateArgs * args);
static gboolean frida_fill_info (const GumDarwinSectionDetails * details, FridaFillInfoContext * ctx);
static void frida_unwind_cursor_set_info_replacement (gpointer cursor, gint is_return_address);
static gpointer frida_find_vtable (void);
static gboolean frida_compute_vtable_shift (gpointer vtable, gssize * shift);
#ifdef HAVE_ARM64
static gboolean frida_find_bss_range (const GumSectionDetails * details, GumMemoryRange * range);
#else
static gboolean frida_is_empty_function (GumAddress address);
static gboolean frida_has_first_match (GumAddress address, gsize size, gboolean * matches);
#endif
static FridaUnwindHookState * state = NULL;
void
_frida_unwind_sitter_fill_unwind_sections (GumAddress invader_start, GumAddress invader_end, void * info)
@@ -92,63 +49,6 @@ _frida_unwind_sitter_fill_unwind_sections (GumAddress invader_start, GumAddress
memcpy (unwind_sections, cached, sizeof (FridaDyldUnwindSections));
}
void
_frida_unwind_sitter_hook_libunwind (void)
{
#if GLIB_SIZEOF_VOID_P == 8
gpointer * set_info_slot;
gpointer get_reg_impl;
if (state != NULL)
return;
state = g_slice_new0 (FridaUnwindHookState);
if (state == NULL)
return;
state->vtable = frida_find_vtable ();
if (state->vtable == NULL)
goto unsupported_version;
if (!frida_compute_vtable_shift (state->vtable, &state->shift))
goto unsupported_version;
set_info_slot = (gpointer *) (GUM_ADDRESS (state->vtable) + FRIDA_UNWIND_CURSOR_VTABLE_OFFSET_SET_INFO + state->shift);
get_reg_impl = *(gpointer *) (GUM_ADDRESS (state->vtable) + FRIDA_UNWIND_CURSOR_VTABLE_OFFSET_GET_REG + state->shift);
state->set_info_slot = set_info_slot;
state->set_info_original = *set_info_slot;
state->set_info = FRIDA_RESIGN_PTR (state->set_info_original);
state->get_reg = FRIDA_RESIGN_PTR (get_reg_impl);
state->interceptor = gum_interceptor_obtain ();
if (gum_interceptor_replace (state->interceptor, state->set_info_original, frida_unwind_cursor_set_info_replacement, NULL, NULL)
!= GUM_REPLACE_OK)
goto unsupported_version;
return;
unsupported_version:
g_slice_free (FridaUnwindHookState, state);
state = NULL;
#endif
}
void
_frida_unwind_sitter_unhook_libunwind (void)
{
if (state == NULL)
return;
gum_interceptor_revert (state->interceptor, state->set_info_original);
g_object_unref (state->interceptor);
g_slice_free (FridaUnwindHookState, state);
state = NULL;
}
static FridaDyldUnwindSections *
frida_get_cached_sections (GumAddress range_start, GumAddress range_end)
{
@@ -187,7 +87,8 @@ frida_create_cached_sections (FridaCreateArgs * args)
cached_sections = g_slice_new0 (FridaDyldUnwindSections);
cached_sections->mh = header;
module = gum_darwin_module_new_from_memory ("Frida", mach_task_self (), GPOINTER_TO_SIZE (header), GUM_DARWIN_MODULE_FLAGS_NONE, NULL);
module = gum_darwin_module_new_from_memory ("Frida", mach_task_self (), GPOINTER_TO_SIZE (header),
GUM_DARWIN_MODULE_FLAGS_NONE, NULL);
if (module == NULL)
return cached_sections;
@@ -222,308 +123,4 @@ frida_fill_info (const GumDarwinSectionDetails * details, FridaFillInfoContext *
return ctx->missing_info > 0;
}
static void
frida_unwind_cursor_set_info_replacement (gpointer self, gint is_return_address)
{
gboolean missing_info;
GumAddress fp, stored_pc;
gpointer * stored_pc_slot;
#if defined (HAVE_ARM64) && !__has_feature (ptrauth_calls)
gboolean was_signed = FALSE;
#endif
if (state == NULL)
return;
state->set_info (self, is_return_address);
#ifdef HAVE_ARM64
fp = GUM_ADDRESS (state->get_reg (self, FRIDA_UNWAARCH64_X29));
#else
fp = GUM_ADDRESS (state->get_reg (self, FRIDA_UNWX86_64_RBP));
#endif
if (fp == 0 || fp == -1)
return;
missing_info = *((guint8 *) self + FRIDA_UNWIND_CURSOR_unwindInfoMissing);
stored_pc_slot = GSIZE_TO_POINTER (fp + GLIB_SIZEOF_VOID_P);
stored_pc = GUM_ADDRESS (*stored_pc_slot);
#if __has_feature (ptrauth_calls)
stored_pc = gum_strip_code_address (stored_pc);
#elif defined (HAVE_ARM64)
was_signed = (stored_pc & ~FRIDA_STRIP_MASK) != 0ULL;
if (was_signed)
stored_pc &= FRIDA_STRIP_MASK;
#endif
if (!missing_info)
{
GumAddress translated;
translated = GUM_ADDRESS (gum_invocation_stack_translate (gum_interceptor_get_current_stack (), GSIZE_TO_POINTER (stored_pc)));
if (translated != stored_pc)
{
#if __has_feature (ptrauth_calls)
*stored_pc_slot = ptrauth_sign_unauthenticated (
ptrauth_strip (GSIZE_TO_POINTER (translated), ptrauth_key_asia), ptrauth_key_asib, FRIDA_FP_TO_SP (fp));
#elif defined (HAVE_ARM64)
if (was_signed)
{
GumAddress resigned;
asm volatile (
"mov x17, %1\n\t"
"mov x16, %2\n\t"
".byte 0x5f, 0x21, 0x03, 0xd5\n\t" /* pacib1716 */
"mov %0, x17\n\t"
: "=r" (resigned)
: "r" (translated & FRIDA_STRIP_MASK),
"r" (FRIDA_FP_TO_SP (fp))
: "x16", "x17"
);
*stored_pc_slot = GSIZE_TO_POINTER (resigned);
}
else
{
*stored_pc_slot = GSIZE_TO_POINTER (translated);
}
#else
*stored_pc_slot = GSIZE_TO_POINTER (translated);
#endif
}
}
}
static gpointer
frida_find_vtable (void)
{
GumAddress result = 0;
GumModule * libunwind;
GumAddress export;
uint64_t address;
G_GNUC_UNUSED cs_err err;
csh capstone;
cs_insn * insn = NULL;
const uint8_t * code;
size_t size;
const size_t max_size = 2048;
libunwind = gum_process_find_module_by_name (FRIDA_LIBUNWIND_PATH);
if (libunwind == NULL)
goto beach;
export = gum_module_find_export_by_name (libunwind, "unw_init_local");
if (export == 0)
export = gum_module_find_export_by_name (libunwind, "_Unwind_RaiseException");
if (export == 0)
goto beach;
export = gum_strip_code_address (export);
address = export;
#ifdef HAVE_ARM64
cs_arch_register_arm64 ();
err = cs_open (CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN, &capstone);
#else
cs_arch_register_x86 ();
err = cs_open (CS_ARCH_X86, CS_MODE_64, &capstone);
#endif
g_assert (err == CS_ERR_OK);
err = cs_option (capstone, CS_OPT_DETAIL, CS_OPT_ON);
g_assert (err == CS_ERR_OK);
insn = cs_malloc (capstone);
code = GSIZE_TO_POINTER (export);
size = max_size;
#ifdef HAVE_ARM64
{
GumAddress last_adrp;
guint last_adrp_reg;
GumMemoryRange bss_range;
bss_range.base_address = 0;
gum_module_enumerate_sections (libunwind, (GumFoundSectionFunc) frida_find_bss_range, &bss_range);
while (cs_disasm_iter (capstone, &code, &size, &address, insn))
{
if (insn->id == ARM64_INS_RET || insn->id == ARM64_INS_RETAA || insn->id == ARM64_INS_RETAB)
break;
if (insn->id == ARM64_INS_ADRP)
{
if (result != 0)
break;
last_adrp = (GumAddress) insn->detail->arm64.operands[1].imm;
last_adrp_reg = insn->detail->arm64.operands[0].reg;
}
else if (insn->id == ARM64_INS_ADD && insn->detail->arm64.operands[0].reg == last_adrp_reg)
{
GumAddress candidate;
gboolean is_bss;
candidate = last_adrp + (GumAddress) insn->detail->arm64.operands[2].imm;
is_bss = bss_range.base_address != 0 &&
bss_range.base_address <= candidate &&
candidate < bss_range.base_address + bss_range.size;
if (!is_bss)
{
if (result == 0)
{
result = candidate;
last_adrp = candidate;
}
else
{
result = candidate;
break;
}
}
}
else if (result != 0)
{
break;
}
}
}
#else
while (cs_disasm_iter (capstone, &code, &size, &address, insn))
{
if (insn->id == X86_INS_RET)
break;
if (insn->id == X86_INS_LEA)
{
const cs_x86_op * op = &insn->detail->x86.operands[1];
if (op->type == X86_OP_MEM && op->mem.base == X86_REG_RIP)
{
result = address + op->mem.disp * op->mem.scale;
break;
}
}
}
#endif
if (insn != NULL)
cs_free (insn, 1);
cs_close (&capstone);
beach:
g_clear_object (&libunwind);
return GSIZE_TO_POINTER (result);
}
#ifdef HAVE_ARM64
static gboolean
frida_find_bss_range (const GumSectionDetails * details, GumMemoryRange * range)
{
if (strcmp (details->name, "__bss") == 0)
{
range->base_address = details->address;
range->size = details->size;
return FALSE;
}
return TRUE;
}
static gboolean
frida_compute_vtable_shift (gpointer vtable, gssize * shift)
{
gboolean result = FALSE;
G_GNUC_UNUSED cs_err err;
csh capstone;
cs_insn * insn = NULL;
const uint8_t * code;
uint64_t address;
size_t size = 4;
cs_arch_register_arm64 ();
err = cs_open (CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN, &capstone);
g_assert (err == CS_ERR_OK);
insn = cs_malloc (capstone);
code = gum_strip_code_pointer (*(gpointer *) vtable);
address = GPOINTER_TO_SIZE (code);
if (cs_disasm_iter (capstone, &code, &size, &address, insn))
{
if (insn->id == ARM64_INS_RET || insn->id == ARM64_INS_RETAA || insn->id == ARM64_INS_RETAB)
*shift = 0;
else
*shift = -2 * GLIB_SIZEOF_VOID_P;
result = TRUE;
}
if (insn != NULL)
cs_free (insn, 1);
cs_close (&capstone);
return result;
}
#else
static gboolean
frida_compute_vtable_shift (gpointer vtable, gssize * shift)
{
GumAddress cursor = GPOINTER_TO_SIZE (vtable);
GumAddress error = cursor + 16 * GLIB_SIZEOF_VOID_P;
while (cursor < error && *(gpointer *) GSIZE_TO_POINTER (cursor) == NULL)
cursor += GLIB_SIZEOF_VOID_P;
if (cursor == error)
return FALSE;
if (frida_is_empty_function (GUM_ADDRESS (*(gpointer *) GSIZE_TO_POINTER (cursor))) &&
frida_is_empty_function (GUM_ADDRESS (*(gpointer *) GSIZE_TO_POINTER (cursor + GLIB_SIZEOF_VOID_P))))
{
*shift = cursor - GPOINTER_TO_SIZE (vtable);
}
else
{
*shift = cursor - GPOINTER_TO_SIZE (vtable) - 2 * GLIB_SIZEOF_VOID_P;
}
return TRUE;
}
static gboolean
frida_is_empty_function (GumAddress address)
{
gboolean matches = FALSE;
GumMemoryRange range;
GumMatchPattern * pattern;
range.base_address = address;
range.size = 6;
/*
* 55 push rbp
* 4889e5 mov rbp, rsp
* 5d pop rbp
* c3 ret
*/
pattern = gum_match_pattern_new_from_string ("55 48 89 e5 5d c3");
gum_memory_scan (&range, pattern, (GumMemoryScanMatchFunc) frida_has_first_match, &matches);
gum_match_pattern_unref (pattern);
return matches;
}
static gboolean
frida_has_first_match (GumAddress address, gsize size, gboolean * matches)
{
*matches = TRUE;
return FALSE;
}
#endif
#endif
+13 -37
View File
@@ -1,65 +1,41 @@
namespace Frida {
#if DARWIN
public sealed class UnwindSitter : Object {
public sealed class UnwindSitter : Object, Gum.UnwindSectionsProvider {
public weak ProcessInvader invader {
get;
construct;
}
private DyldFindUnwindSectionsFunc dyld_find_unwind_sections;
public unowned Gum.MemoryRange? range {
get { return _range; }
}
private const string LIBDYLD = "/usr/lib/system/libdyld.dylib";
[CCode (has_target = false)]
private delegate int DyldFindUnwindSectionsFunc (void * addr, void * info);
private Gum.MemoryRange? _range;
private Gum.UnwindBroker broker;
public UnwindSitter (ProcessInvader invader) {
Object (invader: invader);
}
construct {
var interceptor = Gum.Interceptor.obtain ();
_range = invader.get_memory_range ();
dyld_find_unwind_sections = (DyldFindUnwindSectionsFunc)
Gum.Process.find_module_by_name (LIBDYLD).find_export_by_name ("_dyld_find_unwind_sections");
interceptor.replace ((void *) dyld_find_unwind_sections, (void *) replacement_dyld_find_unwind_sections, this);
_hook_libunwind ();
broker = Gum.UnwindBroker.obtain ();
broker.add_sections_provider (this);
}
public override void dispose () {
var interceptor = Gum.Interceptor.obtain ();
_unhook_libunwind ();
interceptor.revert ((void *) dyld_find_unwind_sections);
broker.remove_sections_provider (this);
base.dispose ();
}
private static int replacement_dyld_find_unwind_sections (void * addr, void * info) {
unowned Gum.InvocationContext context = Gum.Interceptor.get_current_invocation ();
unowned UnwindSitter sitter = (UnwindSitter) context.get_replacement_data ();
Gum.MemoryRange range = sitter.invader.get_memory_range ();
var range_end = range.base_address + range.size;
var address = Gum.Address.from_pointer (addr);
#if ARM64
address &= 0x7ffffffffULL;
#endif
var is_ours = address >= range.base_address && address < range_end;
if (!is_ours)
return sitter.dyld_find_unwind_sections (addr, info);
_fill_unwind_sections (range.base_address, range_end, info);
return 1;
public bool fill (Gum.Address address, void * info) {
_fill_unwind_sections (_range.base_address, _range.base_address + _range.size, info);
return true;
}
public extern static void _fill_unwind_sections (Gum.Address invader_start, Gum.Address invader_end, void * info);
public extern static void _hook_libunwind ();
public extern static void _unhook_libunwind ();
}
#else
public sealed class UnwindSitter : Object {
+25 -20
View File
@@ -130,6 +130,10 @@ if host_os_family == 'darwin'
add_languages('objc', 'objcpp', native: false)
endif
if meson.is_cross_build()
add_languages('c', 'cpp', 'vala', native: true)
endif
python = import('python').find_installation()
if cc.get_argument_syntax() == 'msvc'
@@ -329,18 +333,6 @@ if get_option('b_sanitize') == 'address'
cdata.set('HAVE_ASAN', 1)
endif
have_in_tree_payloads = get_option('default_library') != 'shared'
gum_dep = dependency('frida-gum-1.0', default_options: [
'default_library=static',
f'frida_version=@frida_version@',
'gumjs=' + (have_in_tree_payloads ? 'enabled' : 'disabled'),
'inspector=enabled',
])
if have_in_tree_payloads
gumjs_dep = dependency('frida-gumjs-1.0', default_options: ['default_library=static'])
endif
gumjs_inspector_dep = dependency('frida-gumjs-inspector-1.0', default_options: ['default_library=static'])
glib_version_req = '>=2.74'
glib_options = [
'default_library=static',
@@ -359,14 +351,6 @@ if optimize_for_prod
]
endif
gee_options = [
'default_library=static',
'disable-introspection=true',
]
if optimize_for_prod
gee_options += 'disable-internal-asserts=true'
endif
glib_dep = dependency('glib-2.0', version: glib_version_req, default_options: glib_options)
if glib_dep.type_name() == 'internal'
have_shared_glib = get_option('default_library') != 'static'
@@ -385,6 +369,27 @@ if have_frida_glib
cdata.set('HAVE_FRIDA_GLIB', 1)
vala_flags += '--define=HAVE_FRIDA_GLIB'
endif
have_in_tree_payloads = not have_shared_glib
gum_dep = dependency('frida-gum-1.0', default_options: [
'default_library=static',
f'frida_version=@frida_version@',
'gumjs=' + (have_in_tree_payloads ? 'enabled' : 'disabled'),
'inspector=enabled',
])
if have_in_tree_payloads
gumjs_dep = dependency('frida-gumjs-1.0', default_options: ['default_library=static'])
endif
gumjs_inspector_dep = dependency('frida-gumjs-inspector-1.0', default_options: ['default_library=static'])
gee_options = [
'default_library=static',
'disable-introspection=true',
]
if optimize_for_prod
gee_options += 'disable-internal-asserts=true'
endif
gobject_dep = dependency('gobject-2.0')
gmodule_dep = dependency('gmodule-2.0')
gio_dep = dependency('gio-2.0')
+1 -1
Submodule releng updated: a76acfc2b5...abb8238970.
+87 -4
View File
@@ -12,6 +12,7 @@ namespace Frida.Server {
private static string? directory = null;
#if !WINDOWS && !TVOS
private static bool daemonize = false;
private static string? output_path = null;
#endif
private static string? softener_flavor_str = null;
private static bool enable_preload = true;
@@ -42,6 +43,7 @@ namespace Frida.Server {
{ "directory", 'd', 0, OptionArg.STRING, ref directory, "Store binaries in DIRECTORY", "DIRECTORY" },
#if !WINDOWS && !TVOS
{ "daemonize", 'D', 0, OptionArg.NONE, ref daemonize, "Detach and become a daemon", null },
{ "output", 'o', 0, OptionArg.FILENAME, ref output_path, "Redirect output to FILE", "FILE" },
#endif
{ "policy-softener", 0, 0, OptionArg.STRING, ref softener_flavor_str, "Select policy softener", "system|internal" },
{ "disable-preload", 'P', OptionFlags.REVERSE, OptionArg.NONE, ref enable_preload, "Disable preload optimization", null },
@@ -78,6 +80,13 @@ namespace Frida.Server {
Environment.set_verbose_logging_enabled (verbose);
#if !WINDOWS && !TVOS
if (output_path != null && !daemonize) {
if (!redirect_output_to (output_path))
return 6;
}
#endif
EndpointParameters endpoint_params;
try {
endpoint_params = new EndpointParameters (listen_address, 0, parse_certificate (certpath), origin,
@@ -156,12 +165,15 @@ namespace Frida.Server {
Posix.setsid ();
var null_in = Posix.open ("/dev/null", Posix.O_RDONLY);
var null_out = Posix.open ("/dev/null", Posix.O_WRONLY);
Posix.dup2 (null_in, Posix.STDIN_FILENO);
Posix.dup2 (null_out, Posix.STDOUT_FILENO);
Posix.dup2 (null_out, Posix.STDERR_FILENO);
Posix.close (null_in);
Posix.close (null_out);
if (output_path == null || !redirect_output_to (output_path)) {
var null_out = Posix.open ("/dev/null", Posix.O_WRONLY);
Posix.dup2 (null_out, Posix.STDOUT_FILENO);
Posix.dup2 (null_out, Posix.STDERR_FILENO);
Posix.close (null_out);
}
}
var status = new uint8[1];
@@ -218,6 +230,22 @@ namespace Frida.Server {
return application.run ();
}
#if !WINDOWS && !TVOS
private static bool redirect_output_to (string path) {
int fd = Posix.open (path, Posix.O_WRONLY | Posix.O_CREAT | Posix.O_APPEND, 0644);
if (fd == -1) {
printerr ("Unable to open “%s”: %s\n", path, Posix.strerror (Posix.errno));
return false;
}
Posix.dup2 (fd, Posix.STDOUT_FILENO);
Posix.dup2 (fd, Posix.STDERR_FILENO);
Posix.close (fd);
return true;
}
#endif
namespace Environment {
public extern void init ();
public extern void set_verbose_logging_enabled (bool enabled);
@@ -274,6 +302,15 @@ namespace Frida.Server {
return false;
});
#if !WINDOWS && !TVOS
if (!daemonize)
process_commands.begin ();
#elif !WINDOWS
process_commands.begin ();
#endif
start_alive_task_dumper ();
exit_code = 0;
loop.run ();
@@ -281,6 +318,45 @@ namespace Frida.Server {
return exit_code;
}
#if !WINDOWS
// TEMPORARY DEBUG HACK: type “t” + Enter to dump alive GTasks.
private async void process_commands () {
var input = new DataInputStream (new UnixInputStream (Posix.STDIN_FILENO, false));
while (true) {
string? line;
try {
line = yield input.read_line_async (Priority.DEFAULT, io_cancellable);
} catch (GLib.Error e) {
return;
}
if (line == null)
return;
switch (line.strip ()) {
case "t":
g_task_print_alive_tasks ();
break;
case "":
break;
default:
printerr ("Unknown command; try “t”\n");
break;
}
}
}
#endif
// TEMPORARY DEBUG HACK: dump alive GTasks every 30 seconds.
private void start_alive_task_dumper () {
var source = new TimeoutSource.seconds (30);
source.set_callback (() => {
GLib.info ("%s", g_task_describe_alive_tasks ());
return Source.CONTINUE;
});
source.attach (MainContext.default ());
}
private async void start () {
try {
if (device_id != null && device_id != "local") {
@@ -355,4 +431,11 @@ namespace Frida.Server {
return new TlsCertificate.from_file (path);
}
// TEMPORARY DEBUG HACK: only present when GLib is built with G_ENABLE_DEBUG.
[CCode (cname = "g_task_print_alive_tasks")]
private extern void g_task_print_alive_tasks ();
[CCode (cname = "g_task_describe_alive_tasks")]
private extern string g_task_describe_alive_tasks ();
}
Binary file not shown.
@@ -100,6 +100,8 @@ public class HelperBackend {
public HelperBackend() {
Looper.prepareMainLooper();
preInitVendorExtensions();
Context context;
try {
Class<?> ActivityThread = Class.forName("android.app.ActivityThread");
@@ -193,6 +195,23 @@ public class HelperBackend {
}
}
private static void preInitVendorExtensions() {
// Nothing OS: AppOpsManager.<clinit> chains into NtAppOpsManager.<clinit>, which
// calls NtExtFactory.getOrCreate(...). The factory dispatches through a static
// INtExtFactory singleton populated by NtExtFactory.init() — normally invoked
// from RuntimeInit.commonInit() during process bring-up, which our helper
// bypasses. Invoke it ourselves so the static initializer chain doesn't NPE.
invokeStaticVoidIfPresent("com.nothing.NtExtFactory", "init");
}
private static void invokeStaticVoidIfPresent(String className, String methodName) {
try {
Class.forName(className).getMethod(methodName).invoke(null);
} catch (ClassNotFoundException e) {
} catch (Throwable ignored) {
}
}
private boolean tryInitActivityStartApi() {
try {
Class<?> ActivityTaskManager = Class.forName("android.app.ActivityTaskManager");
+327 -2
View File
@@ -89,7 +89,7 @@ def main():
emit_header(api, output_dir)
if enable_gir:
emit_gir(api, core_gir, base_gir, output_dir)
emit_gir(api, core_gir, base_gir, output_dir, build_doc_index(src_dir))
if enable_vapi:
emit_vapi(api, output_dir)
@@ -195,7 +195,7 @@ def emit_header(api, output_dir):
output_header_file.write("\n\n#endif\n")
def emit_gir(api: ApiSpec, core_gir: str, base_gir: str, output_dir: Path) -> str:
def emit_gir(api: ApiSpec, core_gir: str, base_gir: str, output_dir: Path, docs: DocIndex) -> str:
ET.register_namespace("", CORE_NAMESPACE)
ET.register_namespace("c", C_NAMESPACE)
ET.register_namespace("glib", GLIB_NAMESPACE)
@@ -264,6 +264,8 @@ def emit_gir(api: ApiSpec, core_gir: str, base_gir: str, output_dir: Path) -> st
if callback.get("name") in needed_names:
merged_namespace.append(callback)
inject_documentation(merged_namespace, docs)
ET.indent(merged_root, space=" ")
result = ET.tostring(merged_root,
encoding="unicode",
@@ -275,6 +277,328 @@ def emit_gir(api: ApiSpec, core_gir: str, base_gir: str, output_dir: Path) -> st
def filter_elements(elements: List[ET.Element], spec_set: Set[str]):
return [elem for elem in elements if elem.get("name") in spec_set]
@dataclass
class DocComment:
body: str
params: dict
returns: str
filename: str = "<generated>"
line: int = 0
@dataclass
class DocIndex:
types: dict # type_name -> DocComment
members: dict # (type_name, member_name) -> DocComment
enum_members: dict # (enum_name, member_name) -> DocComment
DOC_TYPE_PATTERN = re.compile(r"public\s+(?:sealed\s+|abstract\s+)?(class|interface|enum|errordomain)\s+(\w+)")
DOC_SIGNAL_PATTERN = re.compile(r"public\s+signal\s+\S.*?\b(\w+)\s*\(")
DOC_PROPERTY_PATTERN = re.compile(r"public\s+(?:unowned\s+)?\S.*?\b(\w+)\s*\{")
DOC_METHOD_PATTERN = re.compile(r"public\s+\S.*?\b(\w+)\s*\(")
DOC_ENUM_MEMBER_PATTERN = re.compile(r"([A-Z][A-Z0-9_]*)\b")
def build_doc_index(src_dir: Path) -> DocIndex:
repo_root = src_dir.parent
sources = []
for name in TOPLEVEL_NAMES:
path = src_dir / name
if path.exists():
sources.append(path)
base_dir = src_dir.parent / "lib" / "base"
if base_dir.is_dir():
sources.extend(sorted(base_dir.rglob("*.vala")))
index = DocIndex(types={}, members={}, enum_members={})
for path in sources:
try:
filename = path.relative_to(repo_root).as_posix()
except ValueError:
filename = path.name
_collect_docs_from_source(path.read_text(encoding="utf-8"), index, filename)
return index
def _collect_docs_from_source(source: str, index: DocIndex, filename: str):
lines = source.split("\n")
pending = None
current_type = None
current_is_enum = False
i = 0
n = len(lines)
while i < n:
line = lines[i]
stripped = line.strip()
if stripped.startswith("/**"):
start_line = i + 1
block = [line]
while "*/" not in lines[i]:
i += 1
block.append(lines[i])
pending = _parse_doc_block(block)
pending.filename = filename
pending.line = start_line
i += 1
continue
if stripped == "" or stripped.startswith("//") or stripped.startswith("["):
i += 1
continue
indent = len(line) - len(line.lstrip("\t"))
type_match = DOC_TYPE_PATTERN.match(stripped)
if indent == 1 and type_match is not None:
current_type = type_match.group(2)
current_is_enum = type_match.group(1) in ("enum", "errordomain")
if pending is not None:
index.types[current_type] = pending
pending = None
i += 1
continue
if pending is not None and current_type is not None and indent >= 2:
if current_is_enum:
member_match = DOC_ENUM_MEMBER_PATTERN.match(stripped)
if member_match is not None:
index.enum_members[(current_type, member_match.group(1))] = pending
else:
name = _parse_member_name(stripped, current_type)
if name is not None:
index.members[(current_type, name)] = pending
pending = None
i += 1
continue
pending = None
i += 1
def _parse_member_name(stripped: str, type_name: str):
if not stripped.startswith("public"):
return None
signal_match = DOC_SIGNAL_PATTERN.match(stripped)
if signal_match is not None:
return signal_match.group(1)
brace = stripped.find("{")
if brace != -1 and "(" not in stripped[:brace]:
property_match = DOC_PROPERTY_PATTERN.match(stripped)
if property_match is not None:
return property_match.group(1)
ctor_match = re.match(r"public\s+" + re.escape(type_name) + r"(?:\.(\w+))?\s*\(", stripped)
if ctor_match is not None:
return "new" if ctor_match.group(1) is None else ctor_match.group(1)
method_match = DOC_METHOD_PATTERN.match(stripped)
if method_match is not None:
return method_match.group(1)
return None
def _parse_doc_block(block: List[str]) -> DocComment:
text_lines = []
for raw in block:
s = raw.strip()
if s.startswith("/**"):
s = s[3:]
if s.endswith("*/"):
s = s[:-2]
s = s.strip()
if s.startswith("*"):
s = s[1:]
if s.startswith(" "):
s = s[1:]
text_lines.append(s.rstrip())
# Valadoc block tags: "@param <name> <description>" and "@return <description>".
body_lines = []
params = {}
returns_lines = []
target = body_lines
for line in text_lines:
param_match = re.match(r"@param\s+(\w+)\s*(.*)", line)
return_match = re.match(r"@returns?\s*(.*)", line)
if param_match is not None:
params[param_match.group(1)] = [param_match.group(2)]
target = params[param_match.group(1)]
continue
if return_match is not None:
returns_lines = [return_match.group(1)]
target = returns_lines
continue
target.append(line)
def finish(lines):
return _valadoc_to_markdown("\n".join(lines).strip())
return DocComment(
body=finish(body_lines),
params={k: finish(v) for k, v in params.items()},
returns=finish(returns_lines),
)
def _valadoc_to_markdown(text: str) -> str:
if not text:
return text
# Code blocks: {{{ ... }}} -> fenced ``` blocks.
def code_block(match):
code = match.group(1).strip("\n")
return "\n```\n" + code + "\n```\n"
text = re.sub(r"\{\{\{(.*?)\}\}\}", code_block, text, flags=re.DOTALL)
# Note: {@link ...} cross-references are resolved later, at injection time,
# where the full symbol table is available (see inject_documentation).
# Inline monospace: ``x`` -> `x` (valadoc) is already markdown-compatible.
# Bold: ''x'' -> **x**.
text = re.sub(r"''(.+?)''", r"**\1**", text)
# Italic: //x// -> *x*, but leave protocol-relative URLs (://) alone.
text = re.sub(r"(?<![:/])//(?!/)(.+?)//", r"*\1*", text)
return text
def inject_documentation(merged_namespace: ET.Element, docs: DocIndex):
glib_signal_tag = f"{{{GLIB_NAMESPACE}}}signal"
callable_tags = {
f"{{{CORE_NAMESPACE}}}method",
f"{{{CORE_NAMESPACE}}}constructor",
f"{{{CORE_NAMESPACE}}}function",
f"{{{CORE_NAMESPACE}}}virtual-method",
glib_signal_tag,
}
property_tag = f"{{{CORE_NAMESPACE}}}property"
member_tag = f"{{{CORE_NAMESPACE}}}member"
doc_tag = f"{{{CORE_NAMESPACE}}}doc"
# Build a symbol table so {@link ...} can be turned into real gi-docgen
# cross-references. Keys use Vala-style names (underscores); values carry the
# gi-docgen link kind and the GIR name (dashes for properties/signals).
type_kinds = {} # type_name -> gi-docgen kind ("class"/"iface"/"enum"/"error")
member_kinds = {} # (type_name, vala_member) -> (kind, gir_name)
for type_elem in list(merged_namespace):
type_name = type_elem.get("name")
if type_name is None:
continue
tag = type_elem.tag.split("}")[-1]
if tag == "class":
type_kinds[type_name] = "class"
elif tag == "interface":
type_kinds[type_name] = "iface"
elif tag == "enumeration":
type_kinds[type_name] = "error" \
if type_elem.get(f"{{{GLIB_NAMESPACE}}}error-domain") is not None \
else "enum"
for child in list(type_elem):
cname = child.get("name")
if cname is None:
continue
if child.tag == f"{{{CORE_NAMESPACE}}}method":
member_kinds[(type_name, cname)] = ("method", cname)
elif child.tag == f"{{{CORE_NAMESPACE}}}constructor":
member_kinds[(type_name, cname)] = ("ctor", cname)
elif child.tag == property_tag:
member_kinds[(type_name, cname.replace("-", "_"))] = ("property", cname)
elif child.tag == glib_signal_tag:
member_kinds[(type_name, cname.replace("-", "_"))] = ("signal", cname)
def resolve_links(text: str) -> str:
def repl(match):
target = re.sub(r"^Frida\.", "", match.group(1).strip())
parts = target.split(".")
if len(parts) == 1:
kind = type_kinds.get(parts[0])
if kind is not None:
return f"[{kind}@Frida.{parts[0]}]"
elif len(parts) == 2:
info = member_kinds.get((parts[0], parts[1]))
if info is not None:
kind, gir_name = info
if kind == "method":
return f"[method@Frida.{parts[0]}.{gir_name}]"
if kind == "ctor":
return f"[ctor@Frida.{parts[0]}.{gir_name}]"
if kind == "property":
return f"[property@Frida.{parts[0]}:{gir_name}]"
if kind == "signal":
return f"[signal@Frida.{parts[0]}::{gir_name}]"
return "`" + target + "`"
return re.sub(r"\{@link\s+([^}]+)\}", repl, text)
def set_doc(elem: ET.Element, text: str, filename: str = "<generated>",
line: int = 0):
if not text:
return
if elem.find(doc_tag) is not None:
return
doc = ET.Element(doc_tag)
doc.set("xml:space", "preserve")
# gi-docgen accesses filename/line unconditionally, so always emit them,
# mirroring what g-ir-scanner produces for hand-written C.
doc.set("filename", filename)
doc.set("line", str(line))
doc.text = resolve_links(text)
elem.insert(0, doc)
def member_doc(type_name: str, name: str):
comment = docs.members.get((type_name, name))
if comment is None:
for suffix in ("_finish", "_sync"):
if name.endswith(suffix):
comment = docs.members.get((type_name, name[: -len(suffix)]))
if comment is not None:
break
return comment
def apply_callable(elem: ET.Element, comment: DocComment):
set_doc(elem, comment.body, comment.filename, comment.line)
if comment.returns:
rv = elem.find(f"{{{CORE_NAMESPACE}}}return-value")
if rv is not None:
set_doc(rv, comment.returns, comment.filename, comment.line)
params_elem = elem.find(f"{{{CORE_NAMESPACE}}}parameters")
if params_elem is not None and comment.params:
for param in params_elem.findall(f"{{{CORE_NAMESPACE}}}parameter"):
text = comment.params.get(param.get("name"))
if text:
set_doc(param, text, comment.filename, comment.line)
for type_elem in list(merged_namespace):
type_name = type_elem.get("name")
if type_name is None:
continue
tag = type_elem.tag.split("}")[-1]
type_comment = docs.types.get(type_name)
if type_comment is not None and tag in ("class", "interface", "enumeration"):
set_doc(type_elem, type_comment.body, type_comment.filename,
type_comment.line)
for child in list(type_elem):
child_name = child.get("name")
if child_name is None:
continue
if child.tag in callable_tags:
lookup = child_name.replace("-", "_") if child.tag == glib_signal_tag \
else child_name
comment = member_doc(type_name, lookup)
if comment is not None:
apply_callable(child, comment)
elif child.tag == f"{{{CORE_NAMESPACE}}}constructor" \
and child_name == "new":
# Vala's implicit default constructor; give it a sensible
# description rather than leaving it bare.
set_doc(child, f"Creates a new [class@Frida.{type_name}].")
elif child.tag == property_tag:
vala_name = child_name.replace("-", "_")
comment = docs.members.get((type_name, vala_name))
if comment is not None:
set_doc(child, comment.body, comment.filename, comment.line)
# The C API also exposes the property through getter/setter
# methods; give them the same description.
accessors = {"get_" + vala_name, "set_" + vala_name}
for sibling in type_elem:
if sibling.tag == f"{{{CORE_NAMESPACE}}}method" \
and sibling.get("name") in accessors:
set_doc(sibling, comment.body, comment.filename,
comment.line)
elif child.tag == member_tag:
comment = docs.enum_members.get((type_name, child_name.upper()))
if comment is not None:
set_doc(child, comment.body, comment.filename, comment.line)
def emit_vapi(api, output_dir):
with OutputFile(output_dir / f"frida-core-{api.version}.vapi") as output_vapi_file:
output_vapi_file.write("[CCode (cheader_filename = \"frida-core.h\", cprefix = \"Frida\", lower_case_cprefix = \"frida_\")]")
@@ -337,6 +661,7 @@ def parse_api(frida_version, frida_version_components, api_version, toplevel_cod
enum_types = []
base_public_types = {
"SpawnGatingOptions": "ProcessMatchOptions",
"FrontmostQueryOptions": "SpawnOptions",
"ApplicationQueryOptions": "FrontmostQueryOptions",
"ProcessQueryOptions": "ApplicationQueryOptions",
+253 -151
View File
@@ -10,25 +10,29 @@ namespace Frida.Barebone {
private OutputStream output;
private AgentConfig agent_config;
private VsockTransportConfig? vsock_transport;
private ImageConfig? image_config;
private KernelRelocation? relocation;
private uint64 kernel_base;
private Machine machine;
private Allocator allocator;
private Allocation elf_allocation;
private Allocation config_allocation;
private Callback mprotect_callback;
private Callback remap_writable_pages_callback;
private Gee.Map<uint16, Promise<Variant>> pending_requests = new Gee.HashMap<uint16, Promise<Variant>> ();
private uint16 next_request_id = 1;
private const int COMMAND_TIMEOUT_MS = 25000;
public static async AgentConnection open (AgentConfig agent_config, ImageConfig? image_config, Machine machine,
Allocator allocator, Cancellable? cancellable) throws Error, IOError {
public static async AgentConnection open (AgentConfig agent_config, ImageConfig? image_config,
KernelRelocation? relocation, uint64 kernel_base, Machine machine, Allocator allocator,
Cancellable? cancellable) throws Error, IOError {
var connection = new AgentConnection () {
agent_config = agent_config,
image_config = image_config,
relocation = relocation,
kernel_base = kernel_base,
machine = machine,
allocator = allocator,
};
@@ -42,32 +46,31 @@ namespace Frida.Barebone {
return connection;
}
private async bool init_async (int io_priority, Cancellable? cancellable) throws Error, IOError {
HostlinkTransportConfig? transport_config = agent_config.transport as HostlinkTransportConfig;
if (transport_config == null)
throw new Error.NOT_SUPPORTED ("Unsupported transport config: only hostlink is supported for now");
private const uint8 TRANSPORT_KIND_VIRTIO = 0;
private const uint8 TRANSPORT_KIND_VSOCK = 1;
var qmp = yield QmpClient.open (transport_config.qmp, 0, cancellable);
var link = yield qmp.open_hostlink (cancellable);
hostlink = link.connection;
input = (BufferedInputStream) Object.new (typeof (BufferedInputStream),
"base-stream", hostlink.get_input_stream (),
"close-base-stream", false,
"buffer-size", 128 * 1024);
output = hostlink.get_output_stream ();
private async bool init_async (int io_priority, Cancellable? cancellable) throws Error, IOError {
var transport_tag = yield resolve_transport (cancellable);
var gdb = machine.gdb;
ByteOrder byte_order = gdb.byte_order;
uint pointer_size = gdb.pointer_size;
uint64 kernel_base;
Layout layout;
uint64 preferred_base = 0;
if (image_config != null) {
kernel_base = image_config.base.address;
layout = yield Layout.load_from_symbol_source (File.new_for_path (image_config.file), kernel_base,
byte_order, pointer_size, cancellable);
var payload = yield Img4.parse_file (File.new_for_path (image_config.file), cancellable);
var kernelcache = Layout.parse_kernelcache (payload.data);
preferred_base = kernelcache.preferred_address;
if (relocation != null) {
layout = Layout.load_from_module (kernelcache, payload.data, preferred_base, byte_order,
pointer_size);
rebase_layout (layout, preferred_base, relocation);
} else {
layout = Layout.load_from_module (kernelcache, payload.data, kernel_base, byte_order,
pointer_size);
}
} else {
kernel_base = 0;
layout = new Layout.empty ();
}
if (kernel_base == 0)
@@ -83,9 +86,12 @@ namespace Frida.Barebone {
foreach (var e in image_config.symbols.entries) {
unowned string name = e.key;
if (!symbols.has_key (name)) {
uint32 offset = (relocation != null)
? relocation.runtime_offset (preferred_base + e.value)
: (uint32) e.value;
var s = new SymbolInfo () {
name = name,
offset = (uint32) e.value,
offset = offset,
symbol_type = 0xf,
section = 0x10, // FIXME
};
@@ -99,9 +105,59 @@ namespace Frida.Barebone {
if (thread_block == null)
throw new Error.NOT_SUPPORTED ("Missing symbol for thread_block");
var config_builder = new VariantBuilder (new VariantType ("(tuta(ssuuuu)ay)"));
config_builder.add ("t", link.mmio);
config_builder.add ("u", link.irq);
SymbolInfo? panic = symbols["panic"];
if (panic != null && machine is Arm64Machine)
((Arm64Machine) machine).call_landing_zone = kernel_base + panic.offset;
Bytes symbol_data = hash_builder.build (byte_order);
Gum.ElfModule elf;
try {
elf = new Gum.ElfModule.from_file (agent_config.path);
} catch (Gum.Error e) {
throw new Error.INVALID_ARGUMENT ("%s", e.message);
}
var raw_elf = gdb.make_buffer (new Bytes (elf.get_file_data ()));
// Slots whose symbol is absent on this kernel are left zero; the agent's
// xnu.rs uses Option<fn> and falls back across per-kernel name variants.
elf.enumerate_symbols (s => {
unowned Gum.ElfSectionDetails? sect = s.section;
if (sect != null && sect.name == ".kernel_addrs") {
string name = s.name[1:];
SymbolInfo? info = symbols[name];
if (info != null) {
size_t file_offset = (size_t) (sect.offset + (s.address - sect.address));
raw_elf.write_pointer (file_offset, kernel_base + info.offset);
}
}
return true;
});
yield machine.enter_exception_level (1, 1000, cancellable);
yield run_until_thread_block (kernel_base + thread_block.offset, cancellable);
size_t page_size = yield machine.query_page_size (cancellable);
yield ((Arm64Machine) machine).learn_permission_templates (kernel_base + thread_block.offset, cancellable);
elf_allocation = yield inject_elf (elf, raw_elf.bytes, page_size, machine, allocator, cancellable);
uint64 start_address = 0;
uint64 base_va = elf_allocation.virtual_address;
elf.enumerate_symbols (e => {
if (e.name == "_start")
start_address = base_va + e.address;
return true;
});
if (start_address == 0)
throw new Error.INVALID_ARGUMENT ("Invalid agent: no _start symbol found");
var config_builder = new VariantBuilder (new VariantType ("((tt)yvta(ssuuuu)ay)"));
config_builder.add ("(tt)", base_va, (uint64) elf_allocation.size);
config_builder.add_value (transport_tag.get_child_value (0));
config_builder.add_value (transport_tag.get_child_value (1));
config_builder.add ("t", kernel_base);
config_builder.open (new VariantType ("a(ssuuuu)"));
@@ -117,78 +173,9 @@ namespace Frida.Barebone {
}
config_builder.close ();
Bytes symbol_data = hash_builder.build (byte_order);
config_builder.add_value (Variant.new_from_data (new VariantType ("ay"), symbol_data.get_data (), true,
symbol_data));
Gum.ElfModule elf;
try {
elf = new Gum.ElfModule.from_file (agent_config.path);
} catch (Gum.Error e) {
throw new Error.INVALID_ARGUMENT ("%s", e.message);
}
var raw_elf = gdb.make_buffer (new Bytes (elf.get_file_data ()));
var missing_symbols = new Gee.ArrayList<string> ();
elf.enumerate_symbols (s => {
unowned Gum.ElfSectionDetails? sect = s.section;
if (sect != null && sect.name == ".kernel_addrs") {
string name = s.name[1:];
SymbolInfo? info = symbols[name];
if (info != null) {
size_t file_offset = (size_t) (sect.offset + (s.address - sect.address));
raw_elf.write_pointer (file_offset, kernel_base + info.offset);
} else {
missing_symbols.add (name);
}
}
return true;
});
if (!missing_symbols.is_empty) {
throw new Error.INVALID_ARGUMENT ("Missing symbols for: %s",
string.joinv (", ", missing_symbols.to_array ()));
}
yield machine.enter_exception_level (1, 1000, cancellable);
var bp = yield gdb.add_breakpoint (SOFT, kernel_base + thread_block.offset, 4, cancellable);
GDB.Breakpoint? hit_breakpoint = null;
do {
var exception = yield gdb.continue_until_exception (cancellable);
hit_breakpoint = exception.breakpoint;
} while (hit_breakpoint != bp);
yield bp.remove (cancellable);
size_t page_size = yield machine.query_page_size (cancellable);
elf_allocation = yield inject_elf (elf, raw_elf.bytes, page_size, machine, allocator, cancellable);
uint64 start_address = 0;
uint64 remap_writable_pages_address = 0;
uint64 mprotect_address = 0;
uint64 base_va = elf_allocation.virtual_address;
elf.enumerate_symbols (e => {
if (e.name == "_start")
start_address = base_va + e.address;
else if (e.name == "gum_barebone_try_remap_writable_pages")
remap_writable_pages_address = base_va + e.address;
else if (e.name == "gum_try_mprotect")
mprotect_address = base_va + e.address;
else
return true;
return start_address == 0 || mprotect_address == 0 || remap_writable_pages_address == 0;
});
if (start_address == 0)
throw new Error.INVALID_ARGUMENT ("Invalid agent: no _start symbol found");
if (remap_writable_pages_address == 0)
throw new Error.INVALID_ARGUMENT ("Invalid agent: no gum_barebone_try_remap_writable_pages symbol found");
if (mprotect_address == 0)
throw new Error.INVALID_ARGUMENT ("Invalid agent: no gum_try_mprotect symbol found");
remap_writable_pages_callback = yield new Callback (remap_writable_pages_address,
new RemapWritablePagesHandler (machine), machine, cancellable);
mprotect_callback = yield new Callback (mprotect_address, new MemoryProtectHandler (machine), machine, cancellable);
var config_blob = config_builder.end ().get_data_as_bytes ();
config_allocation = yield allocator.allocate (config_blob.get_size (), 8, cancellable);
@@ -200,13 +187,105 @@ namespace Frida.Barebone {
},
cancellable);
yield gdb.continue (cancellable);
// The vphone research kernel panics on any synchronous exception taken while a
// debugger is attached, which the worker hits in the allocator during gum_init.
var arm64 = machine as Arm64Machine;
bool post_inject_access_uses_bridge = arm64 != null && arm64.physical_memory != null;
if (post_inject_access_uses_bridge)
yield gdb.detach (cancellable);
else
yield gdb.continue (cancellable);
yield establish_hostlink (cancellable);
process_incoming_messages.begin ();
return true;
}
private async void run_until_thread_block (uint64 address, Cancellable? cancellable) throws Error, IOError {
var gdb = machine.gdb;
var bp = yield gdb.add_breakpoint (SOFT, address, 4, cancellable);
GDB.Breakpoint? hit = null;
do {
var exception = yield gdb.continue_until_exception (cancellable);
hit = exception.breakpoint;
} while (hit != bp);
yield bp.remove (cancellable);
}
private async Variant resolve_transport (Cancellable? cancellable) throws Error, IOError {
if (agent_config.transport is HostlinkTransportConfig)
return yield connect_virtio_transport ((HostlinkTransportConfig) agent_config.transport, cancellable);
if (agent_config.transport is VsockTransportConfig) {
var config = (VsockTransportConfig) agent_config.transport;
vsock_transport = config;
return new Variant.tuple ({
new Variant.byte (TRANSPORT_KIND_VSOCK),
new Variant.variant (new Variant.uint32 (config.port))
});
}
throw new Error.NOT_SUPPORTED ("Unsupported transport config");
}
private async void establish_hostlink (Cancellable? cancellable) throws Error, IOError {
if (vsock_transport == null)
return;
#if WINDOWS
throw new Error.NOT_SUPPORTED ("Hostlink transport is not available on this OS");
#else
var address = new UnixSocketAddress (vsock_transport.socket_path);
var client = new SocketClient ();
while (true) {
try {
adopt_hostlink_streams (yield client.connect_async (address, cancellable));
return;
} catch (GLib.Error e) {
if (e is IOError.CANCELLED)
throw (IOError) e;
var source = new TimeoutSource (50);
source.set_callback (establish_hostlink.callback);
source.attach (MainContext.get_thread_default ());
yield;
}
}
#endif
}
private async Variant connect_virtio_transport (HostlinkTransportConfig config, Cancellable? cancellable)
throws Error, IOError {
var qmp = yield QmpClient.open (config.qmp, 0, cancellable);
var link = yield qmp.open_hostlink (cancellable);
adopt_hostlink_streams (link.connection);
Variant[] virtio_cfg = { new Variant.uint64 (link.mmio), new Variant.uint32 (link.irq) };
return new Variant.tuple ({
new Variant.byte (TRANSPORT_KIND_VIRTIO),
new Variant.variant (new Variant.tuple (virtio_cfg))
});
}
private void adopt_hostlink_streams (SocketConnection connection) {
hostlink = connection;
input = (BufferedInputStream) Object.new (typeof (BufferedInputStream),
"base-stream", hostlink.get_input_stream (),
"close-base-stream", false,
"buffer-size", 128 * 1024);
output = hostlink.get_output_stream ();
}
private void rebase_layout (Layout layout, uint64 preferred_base, KernelRelocation reloc) throws Error {
foreach (var s in layout.symbols)
s.offset = reloc.runtime_offset (preferred_base + s.offset);
foreach (var m in layout.modules) {
m.offset = reloc.runtime_offset (preferred_base + m.offset);
m.start_func_offset = reloc.runtime_offset (preferred_base + m.start_func_offset);
m.stop_func_offset = reloc.runtime_offset (preferred_base + m.stop_func_offset);
}
}
public async void close (Cancellable? cancellable) throws IOError {
io_cancellable.cancel ();
}
@@ -241,16 +320,7 @@ namespace Frida.Barebone {
private async Variant execute_command (Command command, Variant payload, Cancellable? cancellable) throws Error, IOError {
uint16 request_id = next_request_id++;
var command_message = new Variant ("(yqv)", (uint8) command, request_id, payload);
if (machine.gdb.byte_order != ByteOrder.HOST)
command_message = command_message.byteswap ();
var command_bytes = command_message.get_data_as_bytes ();
var builder = machine.gdb.make_buffer_builder ();
Bytes frame = builder
.append_uint32 ((uint32) command_bytes.get_size ())
.append_bytes (command_bytes)
.build ();
Bytes frame = frame_message (command, request_id, payload);
var promise = new Promise<Variant> ();
pending_requests[request_id] = promise;
@@ -324,6 +394,30 @@ namespace Frida.Barebone {
payload.get ("(u&s)", out script_handle, out json);
script_message (AgentScriptId (script_handle), json, null);
} else if (command_code == Command.REMAP_WRITABLE_PAGES) {
Variant result;
try {
result = yield remap_writable_pages (payload, io_cancellable);
} catch (Error e) {
result = new Variant.uint64 (0);
}
yield send_reply (request_id, result);
} else if (command_code == Command.MEMORY_PROTECT) {
Variant result;
try {
result = yield protect_memory (payload, io_cancellable);
} catch (Error e) {
result = new Variant.boolean (false);
}
yield send_reply (request_id, result);
} else if (command_code == Command.PATCH_CODE) {
Variant result;
try {
result = yield patch_code (payload, io_cancellable);
} catch (Error e) {
result = new Variant.boolean (false);
}
yield send_reply (request_id, result);
} else if (command_code == Command.REPLY) {
Promise<Variant>? promise;
if (pending_requests.unset (request_id, out promise))
@@ -355,62 +449,67 @@ namespace Frida.Barebone {
}
}
private class RemapWritablePagesHandler : Object, CallbackHandler {
public signal void output (string message);
public uint arity {
get { return 2; }
private async Variant remap_writable_pages (Variant payload, Cancellable? cancellable) throws Error, IOError {
var arm64 = (Arm64Machine) machine;
var physical_addresses = new Gee.ArrayList<uint64?> ();
for (size_t i = 0; i != payload.n_children (); i++) {
uint64 va = payload.get_child_value (i).get_uint64 ();
physical_addresses.add (yield arm64.translate_address (va, cancellable));
}
private Machine machine;
Allocation allocation = yield machine.allocate_pages (physical_addresses, cancellable);
public RemapWritablePagesHandler (Machine machine) {
this.machine = machine;
}
public async uint64 handle_invocation (uint64[] args, CallFrame frame, Cancellable? cancellable)
throws Error, IOError {
var pages = args[0];
var num_pages = (uint) args[1];
var physical_addresses = new Gee.ArrayList<uint64?> ();
var gdb = machine.gdb;
var reader = new BufferReader (yield gdb.read_buffer (pages, num_pages * gdb.pointer_size, cancellable));
for (uint i = 0; i != num_pages; i++)
physical_addresses.add (reader.read_pointer ());
Allocation allocation = yield machine.allocate_pages (physical_addresses, cancellable);
// TODO: Handle cleanup.
return allocation.virtual_address;
}
return new Variant.uint64 (allocation.virtual_address);
}
private class MemoryProtectHandler : Object, CallbackHandler {
public signal void output (string message);
private async Variant protect_memory (Variant payload, Cancellable? cancellable) throws Error, IOError {
uint64 address;
uint64 size;
uint32 prot;
payload.get ("(ttu)", out address, out size, out prot);
public uint arity {
get { return 3; }
yield machine.protect_pages (address, (size_t) size, (Gum.PageProtection) prot, cancellable);
return new Variant.boolean (true);
}
// CTRR/KTRR locks kernel text read-only against the guest CPU, so the agent cannot patch it
// even through a writable alias. The physical-memory bridge writes the backing store directly,
// which the lock does not cover, letting us land hooks in kernel and kext text.
private async Variant patch_code (Variant payload, Cancellable? cancellable) throws Error, IOError {
var arm64 = (Arm64Machine) machine;
uint64 va;
Variant bytes_value;
payload.get ("(t@ay)", out va, out bytes_value);
var data = (uint8[]) bytes_value.get_data_as_bytes ().get_data ();
size_t page_size = yield machine.query_page_size (cancellable);
size_t offset = 0;
while (offset < data.length) {
uint64 page_va = va + offset;
uint64 pa = yield arm64.translate_address (page_va, cancellable);
size_t chunk = size_t.min (page_size - (size_t) (page_va & (page_size - 1)), data.length - offset);
yield arm64.write_physical (pa, data[offset : offset + chunk], cancellable);
offset += chunk;
}
private Machine machine;
return new Variant.boolean (true);
}
public MemoryProtectHandler (Machine machine) {
this.machine = machine;
}
private async void send_reply (uint16 request_id, Variant payload) throws GLib.Error {
Bytes frame = frame_message (Command.REPLY, request_id, payload);
yield output.write_all_async (frame.get_data (), Priority.DEFAULT, io_cancellable, null);
}
public async uint64 handle_invocation (uint64[] args, CallFrame frame, Cancellable? cancellable)
throws Error, IOError {
var address = args[0];
var size = (size_t) args[1];
var prot = (Gum.PageProtection) args[2];
try {
yield machine.protect_pages (address, size, prot, cancellable);
return 1;
} catch (GLib.Error e) {
return 0;
}
}
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)
message = message.byteswap ();
var message_bytes = message.get_data_as_bytes ();
return machine.gdb.make_buffer_builder ()
.append_uint32 ((uint32) message_bytes.get_size ())
.append_bytes (message_bytes)
.build ();
}
private enum Command {
@@ -418,6 +517,9 @@ namespace Frida.Barebone {
LOAD_SCRIPT = 2,
DESTROY_SCRIPT = 3,
POST_SCRIPT_MESSAGE = 4,
REMAP_WRITABLE_PAGES = 5,
MEMORY_PROTECT = 6,
PATCH_CODE = 7,
REPLY = 128,
SCRIPT_MESSAGE = 129
}
+1
View File
@@ -32,6 +32,7 @@
-- \
-Dfrida-core:compat=disabled \
-Dfrida-core:local_backend=disabled \
-Dfrida-core:simmy_backend=disabled \
-Dfrida-core:fruity_backend=disabled \
-Dfrida-core:droidy_backend=disabled \
-Dfrida-core:socket_backend=disabled \
+8
View File
@@ -26,6 +26,14 @@ SECTIONS {
__ZN9IOServiceC2Ev = .; . += 8;
__ZN8OSSymbol17withCStringNoCopyEPKc = .; . += 8;
__ZN6OSData9withBytesEPKvj = .; . += 8;
_kalloc_data = .; . += 8;
_kfree_data = .; . += 8;
_wakeup = .; . += 8;
_sock_socket = .; . += 8;
_sock_connect = .; . += 8;
_sock_send = .; . += 8;
_sock_receive = .; . += 8;
_sock_close = .; . += 8;
}
.data.rel.ro : {
*(.data.rel.ro*)
+50
View File
@@ -0,0 +1,50 @@
{
"connection": {
"host": "127.0.0.1",
"port": 52494,
"flavor": "vz",
"pid": 49248
},
"allocator": {
"mode": "target-functions",
"alloc_function": "0xfffffe0007ab6cdc",
"free_function": "0xfffffe0007ab74a8",
"alloc_flags": 512
},
"agent": {
"path": "/Users/oleavr/src/frida-core/src/barebone/agent/target/aarch64-unknown-none/release/frida-barebone-agent",
"transport": {
"type": "vsock",
"socket_path": "/tmp/claude-501/vphone-frida-21528.sock",
"port": 1339
}
},
"image": {
"file": "/Users/oleavr/src/vphone-cli/vm/iPhone17,3_26.1_23B85_Restore/kernelcache.research.vresearch101",
"symbols": {
"panic": "0x12a2814",
"IOLog": "0xac44cc",
"thread_block": "0xad3dac",
"assert_wait": "0xacf1f8",
"assert_wait_timeout": "0xacf3a0",
"thread_wakeup": "0xad24e0",
"kalloc_data": "0xab2cdc",
"kfree_data": "0xab34a8",
"sock_socket": "0x1006c88",
"sock_connect": "0x1005714",
"sock_send": "0x1006708",
"sock_receive": "0x100669c",
"sock_close": "0x1006de8",
"mach_absolute_time": "0xc0548c",
"absolutetime_to_nanoseconds": "0xc05a34",
"clock_get_calendar_microtime": "0xa996c0",
"kernel_thread_start": "0xafb660",
"sb_evaluate_internal": "0x214dc00",
"hook_cred_label_update_execve": "0x2156004",
"hook_vnode_check_create": "0x213cf48",
"hook_policy_syscall": "0x21443bc",
"gLoadedKextSummaries": "0x24ec160",
"proc_pid": "0xf65e5c"
}
}
}
+52
View File
@@ -0,0 +1,52 @@
use core::ffi::c_void;
use crate::bindings::{gpointer, gsize, GumPageProtection};
use crate::{gum, libc, xnu};
const GUM_PAGE_READ: GumPageProtection = 1;
const GUM_PAGE_EXECUTE: GumPageProtection = 4;
// libffi (FFI_TRAMP_EMBEDDER) asks us to back a trampoline table: a copy of its
// 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.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_tramp_embedder_map(
text: *const c_void,
map_size: usize,
code_table: *mut *mut c_void,
parm_table: *mut *mut c_void,
) -> i32 {
unsafe {
let base = xnu::kalloc(map_size * 2);
if base.is_null() {
return 0;
}
core::ptr::copy_nonoverlapping(text as *const u8, base, map_size);
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);
if made_executable == 0 {
xnu::free(base, map_size * 2);
return 0;
}
*code_table = base as *mut c_void;
*parm_table = base.add(map_size) as *mut c_void;
1
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_tramp_embedder_unmap(
code_table: *mut c_void,
_parm_table: *mut c_void,
map_size: usize,
) {
unsafe {
xnu::free(code_table as *mut u8, map_size * 2);
}
}
+5 -1
View File
@@ -600,10 +600,14 @@ unsafe fn lock_notify_all() {
}
}
// 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
thread_ptr & JS_SAFE_THREAD_ID_MASK
}
+191 -16
View File
@@ -1,24 +1,81 @@
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, gboolean, gchar, gconstpointer, gpointer, gsize, guint,
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,
gum_module_get_type, gum_mprotect, gum_query_page_size,
g_variant_unref, gum_module_get_type, gum_mprotect, gum_query_page_size,
},
gthread, libc, xnu,
gthread, host_rpc, libc, FridaCommand,
};
use alloc::boxed::Box;
use alloc::ffi::CString;
use alloc::format;
use alloc::vec::Vec;
use core::ffi::CStr;
use core::mem::size_of;
use core::ptr;
use core::{ffi::CStr, ptr::read_volatile};
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
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_process_get_current_thread_id() -> GumThreadId {
@@ -57,43 +114,146 @@ 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 physical_addrs = alloc::vec::Vec::with_capacity(n_pages 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 {
let phys_addr = xnu::ml_vtophys(current_page);
physical_addrs.push(phys_addr as gpointer);
virtual_addrs.push(current_page as gpointer);
current_page += page_size as u64;
}
_gum_barebone_try_remap_writable_pages(
physical_addrs.as_ptr() as *mut *const core::ffi::c_void,
physical_addrs.len() as guint,
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)]
#[inline(never)]
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 {
read_volatile(&n_addrs);
read_volatile(addrs)
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,
address: gpointer,
size: gsize,
prot: GumPageProtection,
) -> gboolean {
1
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)]
@@ -107,6 +267,7 @@ pub extern "C" fn gum_memory_allocate(
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);
}
}
@@ -115,6 +276,19 @@ pub extern "C" fn gum_memory_allocate(
#[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
}
@@ -350,6 +524,7 @@ unsafe extern "C" fn gum_native_module_enumerate_exports(
type_: export_type,
name: g_strdup(symbol_ref.name_ptr()),
address: symbol_ref.address(),
size: -1,
};
let should_continue =
+267
View File
@@ -0,0 +1,267 @@
// Same 4-byte LE length-prefix framing as hostlink_virtio so the host-side
// (yqv) protocol is identical across transports.
use core::cell::UnsafeCell;
use core::ffi::{c_int, c_void};
use core::ptr;
use core::sync::atomic::{AtomicU32, Ordering};
use alloc::vec::Vec;
use crate::xnu;
const AF_VSOCK: c_int = 40;
const SOCK_STREAM: c_int = 1;
const VMADDR_CID_HOST: u32 = 2;
const MSG_DONTWAIT: c_int = 0x80;
// Darwin errno values for a non-blocking connect handshake that completes in the background.
const EWOULDBLOCK: c_int = 35;
const EINPROGRESS: c_int = 36;
const EISCONN: c_int = 56;
#[repr(C)]
struct SockaddrVm {
svm_len: u8,
svm_family: u8,
svm_reserved1: u16,
svm_port: u32,
svm_cid: u32,
svm_zero: [u8; 4],
}
#[repr(C)]
struct Iovec {
iov_base: *mut c_void,
iov_len: usize,
}
#[repr(C)]
struct Msghdr {
msg_name: *mut c_void,
msg_namelen: u32,
msg_iov: *mut Iovec,
msg_iovlen: c_int,
msg_control: *mut c_void,
msg_controllen: u32,
msg_flags: c_int,
}
type SocketT = *mut c_void;
type SockUpcall = unsafe extern "C" fn(so: SocketT, cookie: *mut c_void, waitf: c_int);
unsafe extern "C" {
static _sock_socket: unsafe extern "C" fn(
domain: c_int,
type_: c_int,
proto: c_int,
callback: Option<SockUpcall>,
cookie: *mut c_void,
new_so: *mut SocketT,
) -> c_int;
static _sock_connect:
unsafe extern "C" fn(so: SocketT, to: *const c_void, flags: c_int) -> c_int;
static _sock_send: unsafe extern "C" fn(
so: SocketT,
msg: *const Msghdr,
flags: c_int,
sentlen: *mut usize,
) -> c_int;
static _sock_receive: unsafe extern "C" fn(
so: SocketT,
msg: *mut Msghdr,
flags: c_int,
recvlen: *mut usize,
) -> c_int;
static _sock_close: unsafe extern "C" fn(so: SocketT);
}
static UPCALL_PENDING: AtomicU32 = AtomicU32::new(0);
struct Inner {
so: SocketT,
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(
host_port: u32,
on_rx: Option<fn(&[u8])>,
wake_token: *const u8,
) -> Result<Self, ()> {
unsafe {
let mut so: SocketT = ptr::null_mut();
// The kernel invokes the sock_upcall via an authenticated branch (blraa, IA key)
// with the sock_upcall type discriminator, so the pointer must be signed to match.
let signed_upcall: SockUpcall =
core::mem::transmute(crate::pac::ptrauth_sign(upcall as *const u8, 0x12f7));
let rc = _sock_socket(
AF_VSOCK,
SOCK_STREAM,
0,
Some(signed_upcall),
wake_token as *mut c_void,
&mut so,
);
if rc != 0 || so.is_null() {
return Err(());
}
let addr = SockaddrVm {
svm_len: core::mem::size_of::<SockaddrVm>() as u8,
svm_family: AF_VSOCK as u8,
svm_reserved1: 0,
svm_port: host_port,
svm_cid: VMADDR_CID_HOST,
svm_zero: [0; 4],
};
// The KPI connect is asynchronous: it returns EWOULDBLOCK/EINPROGRESS and the
// handshake completes in the background once the host accepts. Re-calling connect to
// poll is invalid (it corrupts the connecting state), so accept the in-progress codes
// as success and let the main loop drive I/O once the link is up.
let rc = _sock_connect(so, &addr as *const _ as *const c_void, 0);
if rc != 0 && !matches!(rc, EWOULDBLOCK | EINPROGRESS | EISCONN) {
_sock_close(so);
return Err(());
}
Ok(Hostlink {
state: UnsafeCell::new(Inner {
so,
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 s = unsafe { &*self.state.get() };
unsafe {
let len = payload.len() as u32;
let mut hdr = [
(len & 0xff) as u8,
((len >> 8) & 0xff) as u8,
((len >> 16) & 0xff) as u8,
((len >> 24) & 0xff) as u8,
];
let mut iov = [
Iovec {
iov_base: hdr.as_mut_ptr() as *mut c_void,
iov_len: 4,
},
Iovec {
iov_base: payload.as_ptr() as *mut c_void,
iov_len: payload.len(),
},
];
let msg = Msghdr {
msg_name: ptr::null_mut(),
msg_namelen: 0,
msg_iov: iov.as_mut_ptr(),
msg_iovlen: 2,
msg_control: ptr::null_mut(),
msg_controllen: 0,
msg_flags: 0,
};
let mut sent: usize = 0;
let _ = _sock_send(s.so, &msg, 0, &mut sent);
}
}
pub fn process(&self) {
let s = unsafe { &mut *self.state.get() };
loop {
if s.rx_lenhave < 4 {
let lo = s.rx_lenhave;
let n = unsafe { recv_nonblocking(s.so, &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 = unsafe { recv_nonblocking(s.so, &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 {
let s = &*self.state.get();
_sock_close(s.so);
}
}
}
unsafe fn recv_nonblocking(so: SocketT, dst: &mut [u8]) -> usize {
unsafe {
let mut iov = Iovec {
iov_base: dst.as_mut_ptr() as *mut c_void,
iov_len: dst.len(),
};
let mut msg = Msghdr {
msg_name: ptr::null_mut(),
msg_namelen: 0,
msg_iov: &mut iov,
msg_iovlen: 1,
msg_control: ptr::null_mut(),
msg_controllen: 0,
msg_flags: 0,
};
let mut got: usize = 0;
let _ = _sock_receive(so, &mut msg, MSG_DONTWAIT, &mut got);
got
}
}
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);
}
}
+7
View File
@@ -138,6 +138,13 @@ 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
+189 -40
View File
@@ -15,21 +15,23 @@ 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_child_value, g_variant_get_data,
g_variant_get_size, g_variant_get_string, g_variant_get_uint32, g_variant_get_uint64,
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_load_sync, gum_script_post,
gum_script_set_message_handler, gum_script_unload_sync,
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 hostlink_virtio::Hostlink;
use symbols::SymbolTable;
mod ffi;
mod glib;
mod gthread;
mod gum;
mod hostlink_virtio;
mod hostlink_vsock;
mod libc;
mod pac;
@@ -55,6 +57,9 @@ pub enum FridaCommand {
LoadScript = 2,
DestroyScript = 3,
PostScriptMessage = 4,
RemapWritablePages = 5,
MemoryProtect = 6,
PatchCode = 7,
Reply = 128,
ScriptMessage = 129,
@@ -67,6 +72,9 @@ impl core::fmt::Display for FridaCommand {
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"),
}
@@ -103,10 +111,36 @@ static mut CONFIG_DATA: &'static [u8] = &[];
pub static mut MODULE_INFO: Vec<ModuleInfo> = Vec::new();
pub static mut SYMBOL_TABLE: SymbolTable = SymbolTable::empty();
static mut TRANSPORT_DRIVER: *mut Hostlink = core::ptr::null_mut();
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: Hostlink) {
fn transport_set(driver: Transport) {
unsafe {
let boxed = Box::into_raw(Box::new(driver));
TRANSPORT_DRIVER = boxed;
@@ -114,15 +148,27 @@ fn transport_set(driver: Hostlink) {
}
#[inline(always)]
fn transport_get_unchecked() -> &'static Hostlink {
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 {
@@ -137,33 +183,51 @@ pub struct ModuleInfo {
#[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");
xnu::kernel_thread_start(frida_agent_worker, 12345usize as *mut core::ffi::c_void);
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 (mmio, irq, kernel_base, module_info, symbol_table) =
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");
transport_set(
Hostlink::init(
mmio,
irq,
Some(on_frame_from_host),
ptr::addr_of_mut!(glib::WAKEUP_TOKEN) as *const u8,
)
.unwrap(),
);
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();
@@ -183,9 +247,11 @@ fn on_frame_from_host(frame: &[u8]) {
}
}
unsafe fn parse_config(config: &[u8]) -> (u64, u32, u64, Vec<ModuleInfo>, SymbolTable) {
unsafe fn parse_config(
config: &[u8],
) -> (TransportConfig, u64, Vec<ModuleInfo>, SymbolTable, GumMemoryRange) {
unsafe {
let type_string = c"(tuta(ssuuuu)ay)".as_ptr() as *const gchar;
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(
@@ -197,17 +263,36 @@ unsafe fn parse_config(config: &[u8]) -> (u64, u32, u64, Vec<ModuleInfo>, Symbol
ptr::null_mut(),
);
let hostlink_mmio_variant = g_variant_get_child_value(root_variant, 0);
let hostlink_mmio = g_variant_get_uint64(hostlink_mmio_variant);
let transport_kind_variant = g_variant_get_child_value(root_variant, 1);
let transport_kind = g_variant_get_byte(transport_kind_variant);
let hostlink_irq_variant = g_variant_get_child_value(root_variant, 1);
let hostlink_irq = g_variant_get_uint32(hostlink_irq_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, 2);
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, 3);
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);
@@ -239,7 +324,7 @@ unsafe fn parse_config(config: &[u8]) -> (u64, u32, u64, Vec<ModuleInfo>, Symbol
});
}
let symbol_array_variant = g_variant_get_child_value(root_variant, 4);
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(
@@ -247,21 +332,28 @@ unsafe fn parse_config(config: &[u8]) -> (u64, u32, u64, Vec<ModuleInfo>, Symbol
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(hostlink_irq_variant);
g_variant_unref(hostlink_mmio_variant);
g_variant_unref(root_variant);
g_variant_type_free(variant_type);
(
hostlink_mmio,
hostlink_irq,
kernel_base,
module_info,
symbol_table,
)
(transport_config, kernel_base, module_info, symbol_table, own_range)
}
}
@@ -325,6 +417,16 @@ fn process_incoming_message(variant: *mut GVariant) {
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),
@@ -356,6 +458,41 @@ fn send_command_reply(request_id: u16, response: HandlerResponse) {
}
}
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 {
@@ -383,6 +520,8 @@ fn handle_create_script(payload_variant: *mut GVariant) -> HandlerResponse {
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));
@@ -402,6 +541,16 @@ fn handle_create_script(payload_variant: *mut GVariant) -> HandlerResponse {
}
}
// 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,
+37 -6
View File
@@ -16,14 +16,21 @@ type ContinuationFn = unsafe extern "C" fn(_parameter: *mut c_void, _wait_result
unsafe extern "C" {
static _panic: unsafe extern "C" fn(*const u8);
static _IOLog: unsafe extern "C" fn(*const u8, ...);
static _kalloc: unsafe extern "C" fn(usize) -> *mut u8;
static _kfree: unsafe extern "C" fn(*mut u8, usize) -> *mut 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;
static _thread_wakeup: unsafe extern "C" fn(*const u8) -> 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);
@@ -53,13 +60,28 @@ pub fn io_log(msg: &str) {
}
}
// Z_WAITOK (modern XNU): block until allocation succeeds.
const Z_WAITOK: u32 = 0x0200;
pub fn kalloc(size: usize) -> *mut u8 {
unsafe { _kalloc(size) }
unsafe {
if let Some(f) = _kalloc_data {
f(size, Z_WAITOK)
} else if let Some(f) = _kalloc {
f(size)
} else {
core::ptr::null_mut()
}
}
}
pub fn free(ptr: *mut u8, size: usize) {
unsafe {
_kfree(ptr, size);
if let Some(f) = _kfree_data {
f(ptr, size);
} else if let Some(f) = _kfree {
f(ptr, size);
}
}
}
@@ -97,7 +119,16 @@ pub fn thread_block(continuation: Option<ContinuationFn>) -> i32 {
}
pub fn thread_wakeup(event: *const u8) -> i32 {
unsafe { _thread_wakeup(event) }
unsafe {
if let Some(f) = _thread_wakeup {
f(event)
} else if let Some(f) = _wakeup {
f(event);
0
} else {
0
}
}
}
pub fn mach_absolute_time() -> u64 {
+2 -1
View File
@@ -118,7 +118,8 @@ namespace Frida.Barebone {
}
public async Allocation allocate (size_t size, size_t alignment, Cancellable? cancellable) throws Error, IOError {
uint64 address = yield machine.invoke (config.alloc_function.address, { size }, cancellable);
uint64 address = yield machine.invoke (config.alloc_function.address, { size, config.alloc_flags },
cancellable);
// TODO: Handle alignment.
+180 -28
View File
@@ -14,6 +14,24 @@ namespace Frida.Barebone {
get { return "tiny"; }
}
/**
* Return-detection landing zone for invoke(). Defaults to the hijacked thread's own pc, but
* that address is typically a hot scheduler routine (thread_block) which every other thread
* also trips, drowning out our thread's return. Point this at a function that is never called
* during normal operation (e.g. panic) so only our returning thread stops here.
*/
public uint64 call_landing_zone = 0;
public PhysicalMemory? physical_memory;
// The kernel's MMU registers (TTBR1, TCR) are fixed, so cache them from the first read
// (taken while the target is stopped) to serve later page-table work over the bridge
// without halting the running agent.
private MMUParameters? cached_mmu_parameters;
private uint64 code_template_descriptor;
private bool code_template_known = false;
private enum AddressingMode {
VIRTUAL,
PHYSICAL
@@ -37,7 +55,7 @@ namespace Frida.Barebone {
}
public async size_t query_page_size (Cancellable? cancellable) throws Error, IOError {
MMUParameters p = yield MMUParameters.load (gdb, cancellable);
MMUParameters p = yield load_mmu_parameters (cancellable);
return p.granule;
}
@@ -84,7 +102,7 @@ namespace Frida.Barebone {
private async void collect_ranges_in_table (uint64 table_address, uint level, uint64 upper_bits, MMUParameters p,
Gee.List<RangeDetails> ranges, Cancellable? cancellable) throws Error, IOError {
uint max_entries = compute_max_entries (level, p);
Buffer entries = yield gdb.read_buffer (table_address, max_entries * Descriptor.SIZE, cancellable);
Buffer entries = yield read_physical_buffer (table_address, max_entries * Descriptor.SIZE, cancellable);
uint shift = address_shift_at_level (level, p.granule);
for (uint i = 0; i != max_entries; i++) {
uint64 raw_descriptor = entries.read_uint64 (i * Descriptor.SIZE);
@@ -165,9 +183,13 @@ namespace Frida.Barebone {
public async Allocation allocate_pages (Gee.List<uint64?> physical_addresses, Cancellable? cancellable)
throws Error, IOError {
MMUParameters p = yield MMUParameters.load (gdb, cancellable);
MMUParameters p = yield load_mmu_parameters (cancellable);
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
// The bridge reads and writes guest physical memory directly, so the stub's
// physical-addressing mode (which requires a halted target) is unnecessary.
bool needs_stub_addressing = physical_memory == null;
if (needs_stub_addressing)
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
try {
Allocation? allocation = yield maybe_insert_descriptor_in_table (physical_addresses, p.tt1, p.first_level,
p.upper_bits, p, cancellable);
@@ -176,7 +198,8 @@ namespace Frida.Barebone {
return allocation;
} finally {
set_addressing_mode.begin (gdb, VIRTUAL, null);
if (needs_stub_addressing)
set_addressing_mode.begin (gdb, VIRTUAL, null);
}
}
@@ -196,7 +219,7 @@ namespace Frida.Barebone {
uint64 chunk_base_address = table_address + (chunk_offset * Descriptor.SIZE);
uint chunk_size = uint.min (chunk_max_size, max_entries - chunk_offset);
Buffer descriptors = yield gdb.read_buffer (chunk_base_address, chunk_size * Descriptor.SIZE, cancellable);
Buffer descriptors = yield read_physical_buffer (chunk_base_address, chunk_size * Descriptor.SIZE, cancellable);
for (uint i = 0; i != chunk_size && num_available_slots != num_pages; i++) {
uint buffer_offset = i * Descriptor.SIZE;
uint64 raw_descriptor = descriptors.read_uint64 (buffer_offset);
@@ -238,7 +261,7 @@ namespace Frida.Barebone {
return null;
Gum.PageProtection page_prot = Gum.PageProtection.READ | Gum.PageProtection.WRITE;
if (!p.sprr_enabled)
if (!p.sprr_enabled && !code_template_known)
page_prot |= Gum.PageProtection.EXECUTE;
var builder = gdb.make_buffer_builder ();
@@ -248,18 +271,118 @@ namespace Frida.Barebone {
builder.append_uint64 (new_descriptor);
}
Bytes new_descriptors = builder.build ();
Bytes old_descriptors = yield gdb.read_byte_array (first_available_slot, new_descriptors.get_size (), cancellable);
yield gdb.write_byte_array (first_available_slot, new_descriptors, cancellable);
Bytes old_descriptors = (yield read_physical_buffer (first_available_slot, new_descriptors.get_size (),
cancellable)).bytes;
yield write_physical_buffer (first_available_slot, new_descriptors, cancellable);
size_t size = num_pages * p.granule;
return new DescriptorAllocation (first_available_va, size, first_available_slot, old_descriptors, gdb);
}
// The VZ kernel GDB stub does not expose the SPRR registers, so we cannot read the
// permission-remapping table that turns a descriptor's AP/XN bits into real access rights.
// Granting execute is the case that genuinely needs the right SPRR index, so we sample a
// known kernel-code page and replay its exact attribute encoding onto agent text; the
// read-only/read-write encodings the AP bits already give us continue to work as-is.
public async void learn_permission_templates (uint64 code_va, Cancellable? cancellable) throws Error, IOError {
code_template_descriptor = yield read_level3_descriptor (code_va, cancellable);
code_template_known = true;
}
public async uint64 translate_address (uint64 va, Cancellable? cancellable) throws Error, IOError {
MMUParameters p = yield load_mmu_parameters (cancellable);
uint64 descriptor = yield read_level3_descriptor (va, cancellable);
uint64 page_mask = (1ULL << inpage_bits_for_granule (p.granule)) - 1;
return (descriptor & INT48_MASK & ~page_mask) | (va & page_mask);
}
public override async void write_virtual (uint64 va, uint8[] data, Cancellable? cancellable) throws Error, IOError {
if (physical_memory == null) {
yield gdb.write_byte_array (va, new Bytes (data), cancellable);
return;
}
MMUParameters p = yield MMUParameters.load (gdb, cancellable);
size_t offset = 0;
while (offset < data.length) {
uint64 cur_va = va + offset;
uint64 pa = yield translate_address (cur_va, cancellable);
size_t chunk = size_t.min ((size_t) (p.granule - (cur_va & (p.granule - 1))), data.length - offset);
physical_memory.write (pa, data[offset : offset + chunk]);
offset += chunk;
}
}
private async Buffer read_physical_buffer (uint64 pa, size_t size, Cancellable? cancellable) throws Error, IOError {
if (physical_memory != null && physical_memory.contains (pa))
return gdb.make_buffer (new Bytes (physical_memory.read (pa, size)));
return yield gdb.read_buffer (pa, size, cancellable);
}
private async void write_physical_buffer (uint64 pa, Bytes data, Cancellable? cancellable) throws Error, IOError {
if (physical_memory != null && physical_memory.contains (pa)) {
physical_memory.write (pa, data.get_data ());
return;
}
yield gdb.write_byte_array (pa, data, cancellable);
}
public async uint8[] read_physical (uint64 pa, size_t size, Cancellable? cancellable) throws Error, IOError {
if (physical_memory != null && physical_memory.contains (pa))
return physical_memory.read (pa, size);
return yield read_physical_via_stub (pa, size, cancellable);
}
public async void write_physical (uint64 pa, uint8[] data, Cancellable? cancellable) throws Error, IOError {
if (physical_memory != null && physical_memory.contains (pa)) {
physical_memory.write (pa, data);
return;
}
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
try {
yield gdb.write_byte_array (pa, new Bytes (data), cancellable);
} finally {
set_addressing_mode.begin (gdb, VIRTUAL, null);
}
}
public async uint8[] read_physical_via_stub (uint64 pa, size_t size, Cancellable? cancellable) throws Error, IOError {
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
try {
return (yield gdb.read_byte_array (pa, size, cancellable)).get_data ();
} finally {
set_addressing_mode.begin (gdb, VIRTUAL, null);
}
}
public async uint64 read_level3_descriptor (uint64 va, Cancellable? cancellable) throws Error, IOError {
MMUParameters p = yield load_mmu_parameters (cancellable);
bool needs_stub_addressing = physical_memory == null;
if (needs_stub_addressing)
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
try {
var cache = new TableWalkCache ();
uint64 table_pa = yield find_level3_table (va, p, cache, cancellable);
uint index_bits = num_address_bits_at_level (3, p);
uint index_shift = inpage_bits_for_granule (p.granule);
uint index_mask = (1U << index_bits) - 1;
uint64 index = (va >> index_shift) & index_mask;
uint64 slot_pa = table_pa + (index * Descriptor.SIZE);
var buf = yield read_physical_buffer (slot_pa, Descriptor.SIZE, cancellable);
return buf.read_uint64 (0);
} finally {
if (needs_stub_addressing)
set_addressing_mode.begin (gdb, VIRTUAL, null);
}
}
public async void protect_pages (uint64 virtual_address, size_t size, Gum.PageProtection prot, Cancellable? cancellable)
throws Error, IOError {
MMUParameters p = yield MMUParameters.load (gdb, cancellable);
MMUParameters p = yield load_mmu_parameters (cancellable);
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
bool needs_stub_addressing = physical_memory == null;
if (needs_stub_addressing)
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
try {
uint64 page_mask = p.granule - 1;
uint64 aligned_va = virtual_address & ~page_mask;
@@ -268,10 +391,17 @@ namespace Frida.Barebone {
yield perform_protect_pages (aligned_va, num_pages, prot, p, cancellable);
} finally {
set_addressing_mode.begin (gdb, VIRTUAL, null);
if (needs_stub_addressing)
set_addressing_mode.begin (gdb, VIRTUAL, null);
}
}
private async MMUParameters load_mmu_parameters (Cancellable? cancellable) throws Error, IOError {
if (cached_mmu_parameters == null)
cached_mmu_parameters = yield MMUParameters.load (gdb, cancellable);
return cached_mmu_parameters;
}
private async void perform_protect_pages (uint64 start_va, uint num_pages, Gum.PageProtection prot, MMUParameters p,
Cancellable? cancellable) throws Error, IOError {
var table_cache = new TableWalkCache ();
@@ -313,7 +443,7 @@ namespace Frida.Barebone {
uint64 first_index = (current_va >> index_shift) & index_mask;
uint64 first_slot_pa = table_pa + (first_index * Descriptor.SIZE);
Buffer descriptors = yield gdb.read_buffer (first_slot_pa, batch_size * Descriptor.SIZE, cancellable);
Buffer descriptors = yield read_physical_buffer (first_slot_pa, batch_size * Descriptor.SIZE, cancellable);
bool any_changed = false;
for (uint i = 0; i != batch_size; i++) {
@@ -327,7 +457,7 @@ namespace Frida.Barebone {
}
if (any_changed)
yield gdb.write_byte_array (first_slot_pa, descriptors.bytes, cancellable);
yield write_physical_buffer (first_slot_pa, descriptors.bytes, cancellable);
}
private async uint64 find_level3_table (uint64 va, MMUParameters p, TableWalkCache cache, Cancellable? cancellable)
@@ -352,7 +482,7 @@ namespace Frida.Barebone {
uint index = (uint) ((va >> shift) & (entries - 1));
uint64 slot_pa = table_pa + ((uint64) index * Descriptor.SIZE);
Buffer d_buf = yield gdb.read_buffer (slot_pa, Descriptor.SIZE, cancellable);
Buffer d_buf = yield read_physical_buffer (slot_pa, Descriptor.SIZE, cancellable);
uint64 raw = d_buf.read_uint64 (0);
Descriptor desc = Descriptor.parse (raw, level, p.granule);
@@ -375,7 +505,7 @@ namespace Frida.Barebone {
uint index = (uint) ((va >> shift) & (entries - 1));
uint64 slot_pa = table_pa + (index * Descriptor.SIZE);
Buffer d_buf = yield gdb.read_buffer (slot_pa, Descriptor.SIZE, cancellable);
Buffer d_buf = yield read_physical_buffer (slot_pa, Descriptor.SIZE, cancellable);
uint64 raw_desc = d_buf.read_uint64 (0);
Descriptor desc = Descriptor.parse (raw_desc, level, p.granule);
if (desc.kind != TABLE)
@@ -384,7 +514,7 @@ namespace Frida.Barebone {
if ((raw_desc & TABLE_PXN_BIT) != 0) {
uint64 new_desc = raw_desc & ~TABLE_PXN_BIT;
d_buf.write_uint64 (0, new_desc);
yield gdb.write_byte_array (slot_pa, d_buf.bytes, cancellable);
yield write_physical_buffer (slot_pa, d_buf.bytes, cancellable);
}
table_pa = desc.target_address;
@@ -557,7 +687,7 @@ namespace Frida.Barebone {
Gum.ElfArm64Relocation type = (Gum.ElfArm64Relocation) r.type;
switch (type) {
case ABS64:
relocated.write_uint64 ((size_t) r.address, base_va + r.symbol.address + r.addend);
relocated.write_uint64 ((size_t) r.address, base_va + relocated.read_uint64 ((size_t) r.address));
break;
case PREL32:
var diff = (int64) (base_va + r.symbol.address + r.addend) - (int64) (base_va + r.address);
@@ -585,8 +715,11 @@ namespace Frida.Barebone {
regs["pc"] = impl;
uint64 landing_zone = saved_regs["pc"].get_uint64 ();
uint64 landing_zone = (call_landing_zone != 0) ? call_landing_zone : saved_regs["pc"].get_uint64 ();
// The VZ stub exposes "lr" and "x30" as distinct registers; the live link register is
// "lr", so the return address must be written there for the callee's RET to land here.
regs["x30"] = landing_zone;
regs["lr"] = landing_zone;
uint64 sp = saved_regs["sp"].get_uint64 ();
sp -= RED_ZONE_SIZE;
@@ -597,17 +730,21 @@ namespace Frida.Barebone {
yield thread.write_registers (regs, cancellable);
// The callee may reschedule (e.g. kernel_thread_start) onto another core, and the
// stub identifies threads by core; matching the original core's id would strand the
// call and clobber that core's now-unrelated thread. Nothing else reaches the landing
// zone, so any thread stopping there is ours.
GDB.Breakpoint bp = yield gdb.add_breakpoint (SOFT, landing_zone, 4, cancellable);
GDB.Exception ex = null;
do {
ex = yield gdb.continue_until_exception (cancellable);
} while (ex.breakpoint != bp || ex.thread.id != thread.id);
// TODO: Improve GDB.Client to guarantee a single GDB.Thread instance per ID.
} while (ex.breakpoint != bp);
yield bp.remove (cancellable);
uint64 retval = yield thread.read_register ("x0", cancellable);
GDB.Thread landed = ex.thread;
uint64 retval = yield landed.read_register ("x0", cancellable);
yield thread.write_registers (saved_regs, cancellable);
yield landed.write_registers (saved_regs, cancellable);
if (was_running)
yield gdb.continue (cancellable);
@@ -630,15 +767,16 @@ namespace Frida.Barebone {
yield thread.write_registers (regs, cancellable);
// As in invoke(): the executed code may migrate our thread to another core,
// so match on the end breakpoint alone and finish on the core it landed on.
GDB.Breakpoint bp = yield gdb.add_breakpoint (SOFT, end, 4, cancellable);
GDB.Exception ex = null;
do {
ex = yield gdb.continue_until_exception (cancellable);
} while (ex.breakpoint != bp || ex.thread.id != thread.id);
// TODO: Improve GDB.Client to guarantee a single GDB.Thread instance per ID.
} while (ex.breakpoint != bp);
yield bp.remove (cancellable);
yield thread.write_registers (saved_regs, cancellable);
yield ex.thread.write_registers (saved_regs, cancellable);
if (was_running)
yield gdb.continue (cancellable);
@@ -916,8 +1054,11 @@ namespace Frida.Barebone {
private static async void set_addressing_mode (GDB.Client gdb, AddressingMode mode, Cancellable? cancellable)
throws Error, IOError {
Gee.Set<string> features = gdb.features;
string enabled = (mode == PHYSICAL) ? "1" : "0";
if ("qemu-phy-mem-mode" in features)
yield gdb.execute_simple ("Qqemu.PhyMemMode:" + ((mode == PHYSICAL) ? "1" : "0"), cancellable);
yield gdb.execute_simple ("Qqemu.PhyMemMode:" + enabled, cancellable);
else if ("vf-phy-mem-mode" in features)
yield gdb.execute_simple ("Qvf.PhyMemMode:" + enabled, cancellable);
else
throw new Error.NOT_SUPPORTED ("Unsupported GDB remote stub; please file a bug");
}
@@ -1276,7 +1417,11 @@ namespace Frida.Barebone {
return prot;
}
private static uint64 apply_protection_bits (uint64 descriptor, Gum.PageProtection prot, MMUParameters p) throws Error {
private uint64 apply_protection_bits (uint64 descriptor, Gum.PageProtection prot, MMUParameters p) throws Error {
bool want_exec = (prot & Gum.PageProtection.EXECUTE) != 0;
if (want_exec && code_template_known)
return apply_code_template (descriptor, p);
uint64 result = descriptor;
if (p.sprr_enabled) {
@@ -1289,6 +1434,13 @@ namespace Frida.Barebone {
return result;
}
// Keep the page's own output address but adopt every attribute bit (the AP/XN SPRR index,
// memory type, shareability) from a sampled kernel-code page so the MMU grants EL1 execute.
private uint64 apply_code_template (uint64 descriptor, MMUParameters p) {
uint64 output_address_mask = INT48_MASK & ~((1ULL << inpage_bits_for_granule (p.granule)) - 1);
return (descriptor & output_address_mask) | (code_template_descriptor & ~output_address_mask);
}
private static uint64 apply_non_sprr_protection_bits (uint64 descriptor, Gum.PageProtection prot) {
uint64 result = descriptor;
+53 -5
View File
@@ -91,7 +91,11 @@ namespace Frida {
throw new Error.TRANSPORT ("The specified GDB remote stub cannot be reached: %s", e.message);
}
var gdb = yield GDB.Client.open (stream, cancellable);
GDB.Client gdb;
if (config.connection.flavor == Barebone.StubFlavor.VZ)
gdb = yield Barebone.VzStubClient.open (stream, cancellable);
else
gdb = yield GDB.Client.open (stream, cancellable);
Barebone.Machine machine;
switch (gdb.arch) {
@@ -112,6 +116,14 @@ namespace Frida {
break;
}
// Read the daemon's guest-RAM map only once the stub has halted the VM: reading the
// high-VA backing while the vCPUs run blocks mach_vm_read. The map is dynamic (the daemon
// reclaims idle RAM), so VzPhysicalMemory re-reads it on demand as the agent maps pages.
#if DARWIN
if (config.connection.flavor == VZ && machine is Barebone.Arm64Machine)
((Barebone.Arm64Machine) machine).physical_memory = Barebone.VzPhysicalMemory.open (config.connection.pid);
#endif
size_t page_size;
try {
page_size = yield machine.query_page_size (cancellable);
@@ -119,6 +131,23 @@ namespace Frida {
page_size = 0;
}
// Resolve the kernel's runtime layout before building the allocator: on a scattered
// SPTM kernel collection the config-supplied addresses are static and must be translated.
Barebone.KernelRelocation? relocation = null;
uint64 kernel_base = 0;
Barebone.ImageConfig? image = config.image;
if (image != null) {
if (image.base != null) {
kernel_base = image.base.address;
} else {
var payload = yield Barebone.Img4.parse_file (File.new_for_path (image.file), cancellable);
uint64? summaries = image.symbols["gLoadedKextSummaries"];
relocation = yield Barebone.KernelRelocation.compute (machine, payload.data,
summaries ?? 0, cancellable);
kernel_base = relocation.reference_base;
}
}
Barebone.Allocator allocator;
Barebone.AllocatorConfig? ac = config.allocator;
if (ac == null) {
@@ -127,8 +156,14 @@ namespace Frida {
allocator = new Barebone.PhysicalAllocator (machine, page_size,
(Barebone.PhysicalAllocatorConfig) ac);
} else if (ac is Barebone.TargetFunctionsAllocatorConfig) {
allocator = new Barebone.TargetFunctionsAllocator (machine, page_size,
(Barebone.TargetFunctionsAllocatorConfig) ac);
var tfa = (Barebone.TargetFunctionsAllocatorConfig) ac;
if (relocation != null) {
tfa.alloc_function = new Barebone.NonNullMemoryAddress ("allocator.alloc_function",
relocation.translate (tfa.alloc_function.address));
tfa.free_function = new Barebone.NonNullMemoryAddress ("allocator.free_function",
relocation.translate (tfa.free_function.address));
}
allocator = new Barebone.TargetFunctionsAllocator (machine, page_size, tfa);
} else {
assert_not_reached ();
}
@@ -136,8 +171,8 @@ namespace Frida {
Barebone.AgentConnection? agent_connection = null;
Barebone.AgentConfig? agent_config = config.agent;
if (agent_config != null) {
agent_connection = yield Barebone.AgentConnection.open (agent_config, config.image, machine, allocator,
cancellable);
agent_connection = yield Barebone.AgentConnection.open (agent_config, config.image, relocation,
kernel_base, machine, allocator, cancellable);
}
var interceptor = new Barebone.Interceptor (machine, allocator);
@@ -251,6 +286,11 @@ namespace Frida {
throw_not_supported ();
}
public async void enable_spawn_gating_with_options (HashTable<string, Variant> options,
Cancellable? cancellable) throws Error, IOError {
throw_not_supported ();
}
public async void disable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
throw_not_supported ();
}
@@ -608,6 +648,14 @@ namespace Frida {
public abstract async void load_script (AgentScriptId script_id, Cancellable? cancellable) throws Error, IOError;
public async void interrupt_script (AgentScriptId script_id, Cancellable? cancellable) throws Error, IOError {
throw_not_supported ();
}
public async void terminate_script (AgentScriptId script_id, Cancellable? cancellable) throws Error, IOError {
throw_not_supported ();
}
public async void eternalize_script (AgentScriptId script_id, Cancellable? cancellable) throws Error, IOError {
throw_not_supported ();
}
+82 -4
View File
@@ -116,7 +116,7 @@ namespace Frida.Barebone {
}
}
public sealed class ConnectionConfig : Object {
public sealed class ConnectionConfig : Object, Json.Serializable {
public string host {
get;
set;
@@ -128,6 +128,47 @@ namespace Frida.Barebone {
set;
default = 3333;
}
public uint pid {
get;
set;
default = 0;
}
public StubFlavor flavor {
get;
set;
default = GDB_REMOTE;
}
public bool deserialize_property (string property_name, out Value value, ParamSpec pspec, Json.Node property_node) {
if (property_name == "flavor") {
var v = Value (typeof (StubFlavor));
v.set_enum (parse_stub_flavor (property_node.get_string ()));
value = v;
return true;
}
value = Value (pspec.value_type);
return false;
}
private static StubFlavor parse_stub_flavor (string? name) {
switch (name) {
case "vz": return StubFlavor.VZ;
default: return StubFlavor.GDB_REMOTE;
}
}
}
/**
* Selects which GDB-remote dialect to speak. GDB_REMOTE drives the generic client
* (QEMU, Corellium, debugserver); VZ drives the Apple Virtualization.framework kernel
* stub, whose lldb-flavoured quirks the generic client cannot handle.
*/
public enum StubFlavor {
GDB_REMOTE,
VZ
}
public abstract class AllocatorConfig : Object {
@@ -174,6 +215,14 @@ namespace Frida.Barebone {
set;
}
/** Extra second argument passed to alloc_function. Modern XNU data allocators are
* kalloc_data_external(size, flags); QEMU's kalloc(size) ignores it. Defaults to zero. */
public uint64 alloc_flags {
get;
set;
default = 0;
}
public override void check () throws Error {
if (alloc_function == null)
throw new Error.NOT_SUPPORTED ("Config for 'allocator.alloc_function' is missing");
@@ -229,6 +278,9 @@ namespace Frida.Barebone {
case "hostlink":
t = typeof (HostlinkTransportConfig);
break;
case "vsock":
t = typeof (VsockTransportConfig);
break;
default:
break;
}
@@ -272,6 +324,33 @@ namespace Frida.Barebone {
}
}
/**
* Vsock transport. The agent inside the guest kernel connects out to the host
* over AF_VSOCK on `port`. The host can't speak vsock directly from frida-core
* (the hypervisor proxies it), so a UNIX-socket bridge is provided by the
* embedder (e.g. vphone-cli); frida-core just reads/writes that socket.
*/
public sealed class VsockTransportConfig : TransportConfig {
/** Path to a UNIX socket the embedder has bridged to the guest's hostlink endpoint. */
public string socket_path {
get;
set;
}
/** Vsock port the guest agent will connect to (advertised to the agent via its config). */
public uint port {
get;
set;
}
public override void check () throws Error {
if (socket_path == null)
throw new Error.NOT_SUPPORTED ("Config for 'agent.transport.socket_path' is missing");
if (port == 0)
throw new Error.NOT_SUPPORTED ("Config for 'agent.transport.port' is missing");
}
}
public sealed class ImageConfig : Object, Json.Serializable {
public string file {
get;
@@ -293,9 +372,8 @@ namespace Frida.Barebone {
if (file == null)
throw new Error.NOT_SUPPORTED ("Config for 'image.file' is missing");
if (@base == null)
throw new Error.NOT_SUPPORTED ("Config for 'image.base' is missing");
@base.check ();
if (@base != null)
@base.check ();
if (symbols == null)
throw new Error.NOT_SUPPORTED ("Config for 'image.symbols' is invalid");
+1 -1
View File
@@ -26,7 +26,7 @@ namespace Frida.Barebone {
uint64 base_va = allocation.virtual_address;
Bytes relocated_image = machine.relocate (elf, raw_elf, base_va);
yield machine.gdb.write_byte_array (base_va, relocated_image, cancellable);
yield machine.write_virtual (base_va, relocated_image.get_data (), cancellable);
yield machine.protect_pages (base_va + text_base, text_size, READ | EXECUTE, cancellable);
} catch (GLib.Error e) {
+226
View File
@@ -0,0 +1,226 @@
[CCode (gir_namespace = "FridaBarebone", gir_version = "1.0")]
namespace Frida.Barebone {
/**
* Translates static kernelcache addresses to their runtime locations.
*
* The on-device kernel collection slides each fileset entry independently (the SPTM loader
* scatters com.apple.kernel and every kext to unrelated runtime addresses), so a single KASLR
* slide cannot map a static address to its runtime location. Each entry's in-memory Mach-O
* header carries the rebased per-segment vmaddrs; we pair them with the static segment table
* (same header, same order) to translate any static address, kernel or kext.
*/
public sealed class KernelRelocation : Object {
public uint64 reference_base {
get;
private set;
}
private Gee.List<Entry> entries = new Gee.ArrayList<Entry> ();
private const uint64 COLLECTION_WINDOW = 0x8000000;
private const uint64 SUMMARY_HEADER_SIZE = 0x10;
private const uint64 SUMMARY_SIZE = 0x88;
private const size_t SUMMARY_LOAD_ADDRESS = 0x50;
private const uint32 MH_MAGIC_64 = 0xfeedfacfU;
private const uint32 MH_FILESET = 0xc;
private const uint32 CPU_TYPE_ARM64 = 0x0100000cU;
private const uint32 LC_SEGMENT_64 = 0x19;
private const uint32 LC_FILESET_ENTRY = 0x80000035U;
public static async KernelRelocation compute (Machine machine, Bytes kernelcache_blob,
uint64 loaded_kext_summaries, Cancellable? cancellable) throws Error, IOError {
var gdb = machine.gdb;
Buffer image = gdb.make_buffer (kernelcache_blob);
Gee.List<FilesetEntry> static_entries = parse_fileset_entries (image);
Gee.List<SegmentInfo> static_segments = parse_segments (image, 0);
uint64 preferred_base = static_segments[0].vmaddr;
uint64 collection_header = yield find_collection_header (machine, cancellable);
Buffer collection = yield read_mach_header (gdb, collection_header, cancellable);
var header_locator = new KernelRelocation ();
pair_segments (header_locator, static_segments, parse_segments (collection, 0));
var reloc = new KernelRelocation ();
FilesetEntry kernel = find_entry (static_entries, "com.apple.kernel");
yield add_fileset_entry (reloc, gdb, image, kernel, header_locator.translate (kernel.vmaddr), cancellable);
reloc.reference_base = reloc.translate (kernel.vmaddr);
if (loaded_kext_summaries != 0) {
var runtime_headers = yield read_loaded_kext_headers (gdb, reloc,
preferred_base + loaded_kext_summaries, cancellable);
foreach (var entry in static_entries) {
uint64? runtime_header = runtime_headers[entry.name];
if (runtime_header != null)
yield add_fileset_entry (reloc, gdb, image, entry, runtime_header, cancellable);
}
}
return reloc;
}
private static void pair_segments (KernelRelocation reloc, Gee.List<SegmentInfo> static_segments,
Gee.List<SegmentInfo> runtime_segments) {
for (int i = 0; i != static_segments.size; i++) {
reloc.entries.add (new Entry () {
static_base = static_segments[i].vmaddr,
size = static_segments[i].vmsize,
runtime_base = runtime_segments[i].vmaddr
});
}
}
private static FilesetEntry find_entry (Gee.List<FilesetEntry> entries, string name) throws Error {
foreach (var entry in entries) {
if (entry.name == name)
return entry;
}
throw new Error.NOT_SUPPORTED ("Kernelcache is missing the %s fileset entry", name);
}
private static async void add_fileset_entry (KernelRelocation reloc, GDB.Client gdb, Buffer image,
FilesetEntry entry, uint64 runtime_header, Cancellable? cancellable) throws Error, IOError {
Buffer runtime_image = yield read_mach_header (gdb, runtime_header, cancellable);
pair_segments (reloc, parse_segments (image, (size_t) entry.fileoff), parse_segments (runtime_image, 0));
}
/**
* The runtime kext metadata embedded in the collection header is unusable: the loader clobbers the
* fileset-entry vmaddrs and repacks the kext mach-headers away from their static positions. The
* gLoadedKextSummaries array, on the other hand, is purpose-built for debuggers — it pairs each
* loaded kext's bundle id with the runtime address of its mach-header, giving us a reliable
* name -> runtime-header map from which to translate that kext's independently scattered segments.
*/
private static async Gee.Map<string, uint64?> read_loaded_kext_headers (GDB.Client gdb,
KernelRelocation reloc, uint64 loaded_kext_summaries, Cancellable? cancellable) throws Error, IOError {
uint64 list = (yield gdb.read_buffer (reloc.translate (loaded_kext_summaries), 8, cancellable))
.read_uint64 (0);
uint32 count = (yield gdb.read_buffer (list, 16, cancellable)).read_uint32 (8);
var result = new Gee.HashMap<string, uint64?> ();
for (uint32 i = 0; i != count; i++) {
Buffer summary = yield gdb.read_buffer (list + SUMMARY_HEADER_SIZE + (uint64) i * SUMMARY_SIZE,
(size_t) SUMMARY_SIZE, cancellable);
result[read_lc_string (summary, 0)] = summary.read_uint64 (SUMMARY_LOAD_ADDRESS);
}
return result;
}
public uint64 translate (uint64 static_address) throws Error {
foreach (var e in entries) {
if (static_address >= e.static_base && static_address < e.static_base + e.size)
return e.runtime_base + (static_address - e.static_base);
}
throw new Error.INVALID_ARGUMENT ("Address 0x%" + uint64.FORMAT_MODIFIER + "x is outside the kernelcache",
static_address);
}
public uint32 runtime_offset (uint64 static_address) throws Error {
return (uint32) (translate (static_address) - reference_base);
}
private static async Buffer read_mach_header (GDB.Client gdb, uint64 address, Cancellable? cancellable)
throws Error, IOError {
Buffer head = yield gdb.read_buffer (address, 32, cancellable);
return yield gdb.read_buffer (address, 32 + head.read_uint32 (20), cancellable);
}
/**
* Reads of unmapped memory wedge the VZ stub indefinitely, so the collection header cannot be
* located by blind-scanning down from a kernel pointer: scattered segments leave unmapped gaps
* in between. We instead walk the kernel's own page tables for the readable ranges and probe
* only the first 16 bytes of each — every byte touched is guaranteed mapped.
*/
private static async uint64 find_collection_header (Machine machine, Cancellable? cancellable)
throws Error, IOError {
var gdb = machine.gdb;
uint64 vbar = yield gdb.exception.thread.read_register ("vbar_el1", cancellable);
var range_bases = new Gee.ArrayList<uint64?> ();
var range_sizes = new Gee.ArrayList<uint64?> ();
yield machine.enumerate_ranges (Gum.PageProtection.READ, details => {
if (details.base_va < vbar - COLLECTION_WINDOW || details.base_va > vbar + COLLECTION_WINDOW)
return true;
range_bases.add (details.base_va);
range_sizes.add (details.size);
return true;
}, cancellable);
for (int i = 0; i != range_bases.size; i++) {
uint64 range_base = range_bases[i];
if (range_sizes[i] < 16)
continue;
Buffer head = yield gdb.read_buffer (range_base, 16, cancellable);
if (head.read_uint32 (0) == MH_MAGIC_64 && head.read_uint32 (4) == CPU_TYPE_ARM64
&& head.read_uint32 (12) == MH_FILESET)
return range_base;
}
throw new Error.NOT_SUPPORTED ("Unable to locate the runtime kernel collection header");
}
private static Gee.List<FilesetEntry> parse_fileset_entries (Buffer image) throws Error {
var result = new Gee.ArrayList<FilesetEntry> ();
uint32 ncmds = image.read_uint32 (16);
size_t off = 32;
for (uint32 i = 0; i != ncmds; i++) {
uint32 cmd = image.read_uint32 (off);
uint32 cmdsize = image.read_uint32 (off + 4);
if (cmd == LC_FILESET_ENTRY) {
result.add (new FilesetEntry () {
name = read_lc_string (image, off + image.read_uint32 (off + 24)),
vmaddr = image.read_uint64 (off + 8),
fileoff = image.read_uint64 (off + 16)
});
}
off += cmdsize;
}
return result;
}
private static Gee.List<SegmentInfo> parse_segments (Buffer image, size_t header_offset) throws Error {
var result = new Gee.ArrayList<SegmentInfo> ();
uint32 ncmds = image.read_uint32 (header_offset + 16);
size_t off = header_offset + 32;
for (uint32 i = 0; i != ncmds; i++) {
uint32 cmd = image.read_uint32 (off);
uint32 cmdsize = image.read_uint32 (off + 4);
if (cmd == LC_SEGMENT_64) {
result.add (new SegmentInfo () {
vmaddr = image.read_uint64 (off + 24),
vmsize = image.read_uint64 (off + 32)
});
}
off += cmdsize;
}
return result;
}
private static string read_lc_string (Buffer image, size_t offset) throws Error {
var builder = new StringBuilder ();
for (size_t i = 0; ; i++) {
uint8 c = image.read_uint8 (offset + i);
if (c == 0)
break;
builder.append_c ((char) c);
}
return builder.str;
}
private class FilesetEntry {
public string name;
public uint64 vmaddr;
public uint64 fileoff;
}
private class SegmentInfo {
public uint64 vmaddr;
public uint64 vmsize;
}
private class Entry {
public uint64 static_base;
public uint64 size;
public uint64 runtime_base;
}
}
}
+89 -13
View File
@@ -12,6 +12,10 @@ namespace Frida.Barebone {
}
private const uint64 CHAINED_PTR_TARGET_MASK = (1 << 30) - 1;
private const uint32 MH_MAGIC_64 = 0xfeedfacfU;
private const uint32 MH_FILESET = 0xc;
private const uint32 LC_SEGMENT_64 = 0x19;
private const uint32 LC_FILESET_ENTRY = 0x80000035U;
private Layout (Gee.List<ModuleInfo> modules, Gee.List<SymbolInfo> symbols) {
Object (modules: modules, symbols: symbols);
@@ -21,19 +25,16 @@ namespace Frida.Barebone {
Object (modules: new Gee.ArrayList<ModuleInfo> (), symbols: new Gee.ArrayList<SymbolInfo> ());
}
public static async Layout load_from_symbol_source (File symbol_source, uint64 kernel_base, ByteOrder byte_order,
uint pointer_size, Cancellable? cancellable) throws Error, IOError {
var payload = yield Img4.parse_file (symbol_source, cancellable);
Bytes kerncache = payload.data;
Gum.DarwinModule mod;
public static Gum.DarwinModule parse_kernelcache (Bytes kerncache) throws Error {
try {
mod = new Gum.DarwinModule.from_blob (kerncache, ARM64, Gum.PtrauthSupport.SUPPORTED);
return new Gum.DarwinModule.from_blob (kerncache, ARM64, Gum.PtrauthSupport.SUPPORTED);
} catch (Gum.Error e) {
throw new Error.NOT_SUPPORTED ("%s", e.message);
}
}
public static Layout load_from_module (Gum.DarwinModule mod, Bytes kerncache, uint64 kernel_base, ByteOrder byte_order,
uint pointer_size) throws Error {
var blob = new Blob (kerncache, byte_order, pointer_size);
var modules = compute_module_list (mod, blob, kernel_base);
@@ -43,6 +44,10 @@ namespace Frida.Barebone {
}
private static Gee.List<ModuleInfo> compute_module_list (Gum.DarwinModule mod, Blob blob, uint64 kernel_base) throws Error {
var fileset = compute_fileset_module_list (blob, mod.source_version);
if (fileset != null)
return fileset;
Buffer? kmod_info = null;
Buffer? kmod_start = null;
Error? pending_error = null;
@@ -62,10 +67,16 @@ namespace Frida.Barebone {
});
if (pending_error != null)
throw pending_error;
if (kmod_info == null)
throw new Error.NOT_SUPPORTED ("Unable to find __kmod_info");
if (kmod_start == null)
throw new Error.NOT_SUPPORTED ("Unable to find __kmod_start");
if (kmod_info == null || kmod_start == null) {
var only_kernel = new Gee.ArrayList<ModuleInfo> ();
only_kernel.add (new ModuleInfo () {
name = "mach_kernel",
version = mod.source_version ?? "",
offset = 0,
size = (uint32) blob.size,
});
return only_kernel;
}
var kexts = new Gee.ArrayList<ModuleInfo> ();
var info_pointers = new BufferReader (kmod_info);
@@ -118,6 +129,66 @@ namespace Frida.Barebone {
return result;
}
private static Gee.List<ModuleInfo>? compute_fileset_module_list (Blob blob, string? kernel_version) throws Error {
var header = blob.slice (0, 32, "Mach-O header");
if (header.read_uint32 (0) != MH_MAGIC_64 || header.read_uint32 (12) != MH_FILESET)
return null;
var image = blob.slice (0, 32 + header.read_uint32 (20), "Mach-O load commands");
var entries = new Gee.ArrayList<FilesetEntry> ();
uint64 image_end = 0;
uint32 ncmds = image.read_uint32 (16);
size_t off = 32;
for (uint32 i = 0; i != ncmds; i++) {
uint32 cmd = image.read_uint32 (off);
uint32 cmdsize = image.read_uint32 (off + 4);
if (cmd == LC_FILESET_ENTRY) {
entries.add (new FilesetEntry () {
name = read_lc_string (image, off + image.read_uint32 (off + 24)),
vmaddr = image.read_uint64 (off + 8)
});
} else if (cmd == LC_SEGMENT_64) {
image_end = uint64.max (image_end,
image.read_uint64 (off + 24) + image.read_uint64 (off + 32));
}
off += cmdsize;
}
entries.sort ((a, b) => {
if (a.vmaddr < b.vmaddr)
return -1;
return (a.vmaddr > b.vmaddr) ? 1 : 0;
});
uint64 kernel_vmaddr = entries[0].vmaddr;
var modules = new Gee.ArrayList<ModuleInfo> ();
int n = entries.size;
for (int i = 0; i != n; i++) {
var entry = entries[i];
uint64 end = (i + 1 != n) ? entries[i + 1].vmaddr : image_end;
bool is_kernel = i == 0;
modules.add (new ModuleInfo () {
name = is_kernel ? "mach_kernel" : entry.name,
version = is_kernel ? (kernel_version ?? "") : "",
offset = (uint32) (entry.vmaddr - kernel_vmaddr),
size = (uint32) (end - entry.vmaddr)
});
}
return modules;
}
private static string read_lc_string (Buffer image, size_t offset) throws Error {
var builder = new StringBuilder ();
for (size_t i = 0; ; i++) {
uint8 c = image.read_uint8 (offset + i);
if (c == 0)
break;
builder.append_c ((char) c);
}
return builder.str;
}
private static size_t chained_pointer_to_vm_offset (uint64 val, uint64 kernel_base) {
var target = (size_t) (val & CHAINED_PTR_TARGET_MASK);
@@ -153,6 +224,11 @@ namespace Frida.Barebone {
public uint32 stop_func_offset;
}
private class FilesetEntry {
public string name;
public uint64 vmaddr;
}
public class SymbolInfo {
public string name;
public uint32 offset;
@@ -163,7 +239,7 @@ namespace Frida.Barebone {
private class Blob {
private Bytes bytes;
private size_t size;
public size_t size;
public ByteOrder byte_order;
public uint pointer_size;
+10
View File
@@ -1,5 +1,11 @@
[CCode (gir_namespace = "FridaBarebone", gir_version = "1.0")]
namespace Frida.Barebone {
public interface PhysicalMemory : Object {
public abstract uint8[] read (uint64 pa, size_t size) throws Error;
public abstract void write (uint64 pa, uint8[] data) throws Error;
public abstract bool contains (uint64 pa);
}
public interface Machine : Object {
public abstract GDB.Client gdb {
get;
@@ -16,6 +22,10 @@ namespace Frida.Barebone {
public abstract async size_t query_page_size (Cancellable? cancellable) throws Error, IOError;
public virtual async void write_virtual (uint64 va, uint8[] data, Cancellable? cancellable) throws Error, IOError {
yield gdb.write_byte_array (va, new Bytes (data), cancellable);
}
public abstract async uint query_exception_level (Cancellable? cancellable) throws Error, IOError;
public async void enter_exception_level (uint level, uint timeout, Cancellable? cancellable) throws Error, IOError {
+6
View File
@@ -16,12 +16,18 @@ backend_sources += files(
'img4.vala',
'lzfse.vala',
'qmp-client.vala',
'kernel-relocation.vala',
'vz-stub.vala',
'arch-x86' / 'machine.vala',
'arch-x86_64' / 'machine.vala',
'arch-arm' / 'machine.vala',
'arch-arm64' / 'machine.vala',
'arch-unknown' / 'machine.vala',
)
if host_os_family == 'darwin'
backend_sources += files('vz-physical-memory.vala')
backend_vala_args += '--pkg=frida-gum-darwin-1.0'
endif
barebone_script_runtime = custom_target('frida-barebone-script-runtime',
input: [
'package.json',
+4 -4
View File
@@ -8,7 +8,7 @@
"name": "frida-barebone-script-runtime",
"version": "1.0.0",
"devDependencies": {
"@types/frida-gum": "^19.0.1",
"@types/frida-gum": "^19.10.0",
"esbuild": "^0.25.5",
"typescript": "^5.8.3"
}
@@ -439,9 +439,9 @@
}
},
"node_modules/@types/frida-gum": {
"version": "19.0.1",
"resolved": "https://registry.npmjs.org/@types/frida-gum/-/frida-gum-19.0.1.tgz",
"integrity": "sha512-mp8BpWWkQFJH86ZN5uXZuDiA7KY/+jDknrw0a06x0/+F5ucPwctHrCuV6iT+LLCp/GyICc36ssedF33B1WoLsw==",
"version": "19.10.0",
"resolved": "https://registry.npmjs.org/@types/frida-gum/-/frida-gum-19.10.0.tgz",
"integrity": "sha512-GBKAzwmamtNPBXEcIve1yJ6BB9ZgyW9al3+f06S+lDAvennOx9GfFQ9RDY9kP5nJrkiiRHLzgbDFjQ/+g+iw1Q==",
"dev": true,
"license": "MIT"
},
+1 -1
View File
@@ -9,7 +9,7 @@
"watch": "tsc -w"
},
"devDependencies": {
"@types/frida-gum": "^19.0.1",
"@types/frida-gum": "^19.10.0",
"esbuild": "^0.25.5",
"typescript": "^5.8.3"
}
+223
View File
@@ -0,0 +1,223 @@
[CCode (gir_namespace = "FridaBarebone", gir_version = "1.0")]
namespace Frida.Barebone {
/**
* Direct host access to the guest's physical RAM, bypassing the kernel GDB stub.
*
* Apple's Virtualization daemon maps the whole guest RAM into its own address space and
* records the layout in the Hv::Vm singleton's vector<HvCore::SharedRamEntry>. We acquire
* the daemon's task port, find Hv::Vm by its vtable, walk that vector to build a guest-PA
* -> host-VA map, and then read and write guest physical memory with a single mach_vm call
* instead of thousands of RSP packets. The stub is left to do what only it can: drive the
* vCPUs.
*/
public sealed class VzPhysicalMemory : Object, PhysicalMemory {
private Gum.DarwinPort task;
private uint64 instance_address;
private Gee.ArrayList<SharedRamEntry?> entries = new Gee.ArrayList<SharedRamEntry?> ();
private const uint64 VPTR_MASK = 0x00007ffffffffff8;
private const uint64 PTR_MASK = 0x00ffffffffffffff;
private const uint64 GUEST_RAM_CHUNK_SIZE = 64 * 1024 * 1024;
private const uint64 MAX_HEAP_ADDRESS = 0x400000000;
private const uint64 SCAN_WINDOW = 16 * 1024 * 1024;
private const uint64 SHARED_RAM_VECTOR_OFFSET = 0x30;
private const uint64 SHARED_RAM_ENTRY_SIZE = 32;
private const uint64 MAX_SHARED_RAM_ENTRIES = 1024;
private const uint64 MIN_HOST_VA = 0x10000000;
private const uint64 MAPPING_ALIGNMENT = 0x4000;
private const uint MAX_DISCOVERY_ATTEMPTS = 10;
public static VzPhysicalMemory open (uint pid) throws Error {
var memory = new VzPhysicalMemory ();
if (Gum.Darwin.task_for_pid (Gum.Darwin.mach_task_self (), (int) pid, out memory.task) != Gum.Darwin.Status.SUCCESS) {
throw new Error.PERMISSION_DENIED (
"Unable to acquire the task port for the Virtualization daemon (pid %u); " +
"frida-core needs root or the com.apple.system-task-ports entitlement", pid);
}
for (uint attempt = 0; true; attempt++) {
try {
memory.discover ();
return memory;
} catch (Error e) {
if (attempt == MAX_DISCOVERY_ATTEMPTS - 1)
throw e;
}
}
}
private void discover () throws Error {
uint64 vptr = (resolve_hv_vm_vtable () + 16) & VPTR_MASK;
foreach (uint64 instance in find_vptr_candidates (vptr)) {
if (try_load_shared_ram (instance))
return;
}
throw new Error.NOT_SUPPORTED ("Unable to locate a populated Hv::Vm guest-RAM map in the daemon");
}
private bool try_load_shared_ram (uint64 instance) {
var loaded = read_shared_ram (instance);
if (loaded == null)
return false;
instance_address = instance;
entries = loaded;
return true;
}
// Validate the vector by shape, not size: the daemon reclaims guest RAM while the VM is idle
// so a real Hv::Vm can momentarily expose only the low-memory entry. Page-aligned sizes and
// plausible host VAs are what separate the real map from a random vtable-shaped false match.
// Non-throwing on purpose: a candidate vptr may point at anything, so every failed daemon read
// just means "not this one" — surfacing it as an exception only invites error-propagation faults.
private Gee.ArrayList<SharedRamEntry?>? read_shared_ram (uint64 instance) {
uint64 begin, end;
if (!try_read_u64 (instance + SHARED_RAM_VECTOR_OFFSET, out begin))
return null;
if (!try_read_u64 (instance + SHARED_RAM_VECTOR_OFFSET + 8, out end))
return null;
begin &= PTR_MASK;
end &= PTR_MASK;
if (end <= begin || (end - begin) % SHARED_RAM_ENTRY_SIZE != 0)
return null;
if ((end - begin) / SHARED_RAM_ENTRY_SIZE > MAX_SHARED_RAM_ENTRIES)
return null;
var result = new Gee.ArrayList<SharedRamEntry?> ();
for (uint64 cursor = begin; cursor < end; cursor += SHARED_RAM_ENTRY_SIZE) {
uint64 host_va, guest_pa, size;
if (!try_read_u64 (cursor, out host_va))
return null;
if (!try_read_u64 (cursor + 8, out guest_pa))
return null;
if (!try_read_u64 (cursor + 16, out size))
return null;
var entry = SharedRamEntry () { host_va = host_va & PTR_MASK, guest_pa = guest_pa, size = size };
if (entry.size == 0 || entry.size % MAPPING_ALIGNMENT != 0)
return null;
// Device/MMIO regions appear with no host backing; keep only the host-backed RAM ranges.
if (entry.host_va == 0)
continue;
if (entry.host_va < MIN_HOST_VA)
return null;
result.add (entry);
}
return result;
}
private void refresh () {
var loaded = read_shared_ram (instance_address);
if (loaded != null)
entries = loaded;
}
private static uint64 resolve_hv_vm_vtable () throws Error {
Gum.Module module;
try {
module = Gum.Module.load ("/System/Library/Frameworks/Hypervisor.framework/Hypervisor");
} catch (GLib.Error e) {
throw new Error.NOT_SUPPORTED ("Unable to load Hypervisor.framework: %s", e.message);
}
Gum.Address vtable = module.find_symbol_by_name ("_ZTVN2Hv2VmE");
if (vtable == 0)
throw new Error.NOT_SUPPORTED ("Unable to resolve the Hv::Vm vtable symbol");
return vtable;
}
private Gee.List<uint64?> find_vptr_candidates (uint64 vptr) {
var candidates = new Gee.ArrayList<uint64?> ();
Gum.Darwin.enumerate_ranges (task, READ, details => {
// Hv::Vm lives in the daemon's regular malloc heap; the high guest-RAM-mapped regions
// up at tens of GB block mach_vm_read while the VM is running, so stay below them.
if (details.range.base_address < MAX_HEAP_ADDRESS && details.range.size < GUEST_RAM_CHUNK_SIZE)
scan_span_for_vptr (details.range.base_address, details.range.base_address + details.range.size, vptr, candidates);
return true;
});
return candidates;
}
// find_pointers scans this process's own memory, so mirror each daemon window into a local
// buffer and let gum's tiled scanner sweep it for the masked vptr, then map hits back to the
// daemon address.
private void scan_span_for_vptr (uint64 start, uint64 end, uint64 vptr, Gee.List<uint64?> candidates) {
size_t[] values = { (size_t) vptr };
for (uint64 pos = start; pos < end; pos += SCAN_WINDOW) {
size_t length = (size_t) uint64.min (SCAN_WINDOW, end - pos);
uint8[]? window = Gum.Darwin.read (task, pos, length);
if (window == null)
continue;
Gum.Address window_base = (Gum.Address) (uintptr) (void *) window;
Gum.MemoryRange[] ranges = { { window_base, window.length } };
var matches = Gum.Memory.find_pointers (ranges, values, (size_t) VPTR_MASK);
for (uint i = 0; i != matches.length; i++)
candidates.add (pos + (matches.index (i).address - window_base));
}
}
public uint8[] read (uint64 pa, size_t size) throws Error {
SharedRamEntry entry = entry_for (pa, size);
uint8[]? data = Gum.Darwin.read (task, entry.host_va + (pa - entry.guest_pa), size);
if (data == null)
throw new Error.INVALID_ARGUMENT ("Unable to read guest physical memory at PA 0x%" + uint64.FORMAT_MODIFIER + "x", pa);
return data;
}
public void write (uint64 pa, uint8[] data) throws Error {
SharedRamEntry entry = entry_for (pa, data.length);
if (!Gum.Darwin.write (task, entry.host_va + (pa - entry.guest_pa), data))
throw new Error.INVALID_ARGUMENT ("Unable to write guest physical memory at PA 0x%" + uint64.FORMAT_MODIFIER + "x", pa);
}
public bool contains (uint64 pa) {
if (!is_backed (pa))
refresh ();
return is_backed (pa);
}
private bool is_backed (uint64 pa) {
foreach (var entry in entries) {
if (pa >= entry.guest_pa && pa < entry.guest_pa + entry.size)
return true;
}
return false;
}
private SharedRamEntry entry_for (uint64 pa, size_t length) throws Error {
SharedRamEntry? entry = covering_entry (pa, length);
if (entry == null) {
refresh ();
entry = covering_entry (pa, length);
}
if (entry == null)
throw new Error.INVALID_ARGUMENT ("Guest PA 0x%" + uint64.FORMAT_MODIFIER + "x is not backed by guest RAM", pa);
return entry;
}
private SharedRamEntry? covering_entry (uint64 pa, size_t length) {
foreach (var entry in entries) {
if (pa >= entry.guest_pa && pa + length <= entry.guest_pa + entry.size)
return entry;
}
return null;
}
private bool try_read_u64 (uint64 address, out uint64 value) {
value = 0;
uint8[]? buffer = Gum.Darwin.read (task, address, 8);
if (buffer == null || buffer.length < 8)
return false;
uint64 v = 0;
for (int i = 7; i >= 0; i--)
v = (v << 8) | buffer[i];
value = v;
return true;
}
private struct SharedRamEntry {
public uint64 host_va;
public uint64 guest_pa;
public uint64 size;
}
}
}
+132
View File
@@ -0,0 +1,132 @@
[CCode (gir_namespace = "FridaBarebone", gir_version = "1.0")]
namespace Frida.Barebone {
/**
* GDB-remote client for the kernel debug stub exposed by Apple's
* Virtualization.framework (the stub behind com.apple.Virtualization.VirtualMachine).
*
* It speaks an lldb debugserver-flavoured RSP dialect with a few quirks that the
* generic client cannot cope with: it stays silent until a CPU is halted, implements
* neither `qSupported` nor `qXfer:features:read` (registers are discovered through
* `qRegisterInfo`), reads memory with the binary `x` packet, and toggles
* physical-vs-virtual addressing through the `vf.` vendor namespace instead of `qemu.`.
*/
public sealed class VzStubClient : GDB.Client {
private const size_t MAX_BYTES_PER_READ = 0x800;
private const uint HALT_TIMEOUT_MSEC = 1000;
private VzStubClient (IOStream stream) {
Object (stream: stream);
}
public static new async VzStubClient open (IOStream stream, Cancellable? cancellable = null)
throws Error, IOError {
var client = new VzStubClient (stream);
try {
yield client.init_async (Priority.DEFAULT, cancellable);
} catch (GLib.Error e) {
throw_api_error (e);
}
return client;
}
protected override async void prepare_connection (Cancellable? cancellable) throws Error, IOError {
yield halt (cancellable, HALT_TIMEOUT_MSEC);
yield start_no_ack_mode (cancellable);
}
protected override async void detect_vendor_features (Cancellable? cancellable) throws Error, IOError {
var vcont = yield query_simple ("vCont?", cancellable);
unowned string payload = vcont.payload;
if (payload.length > 0 && payload[0] != 'E')
supported_features.add ("vcont");
string phy_mem_mode = yield query_property ("vf.PhyMemMode", cancellable);
if (phy_mem_mode.length == 1)
supported_features.add ("vf-phy-mem-mode");
}
protected override async void enable_extensions (Cancellable? cancellable) throws Error, IOError {
yield execute_simple ("QThreadSuffixSupported", cancellable);
supported_features.add ("thread-suffix");
yield execute_simple ("QListThreadsInStopReply", cancellable);
yield discover_target (cancellable);
}
private async void discover_target (Cancellable? cancellable) throws Error, IOError {
string info = yield query_property ("HostInfo", cancellable);
var host = GDB.Client.PropertyDictionary.parse (info);
arch = arch_from_cpu_type (GDB.Protocol.parse_uint (host.get_string ("cputype"), 10));
pointer_size = GDB.Protocol.parse_uint (host.get_string ("ptrsize"), 10);
byte_order = (host.get_string ("endian") == "little") ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN;
set_max_packet_size (0x4000);
// Force per-register access: the bulk G packet writes every register by position,
// which would clobber the system registers (PAC keys, ...) that must be preserved.
bulk_registers = false;
var regs = new Gee.ArrayList<GDB.Client.Register> ();
for (uint n = 0; ; n++) {
var response = yield query_simple ("qRegisterInfo%x".printf (n), cancellable);
unowned string payload = response.payload;
if (payload.length == 0 || payload[0] == 'E')
break;
var descriptor = GDB.Client.PropertyDictionary.parse (payload);
string name = descriptor.get_string ("name");
string? altname = descriptor.has ("alt-name") ? descriptor.get_string ("alt-name") : null;
uint bitsize = GDB.Protocol.parse_uint (descriptor.get_string ("bitsize"), 10);
// Writing system registers back would clobber live state such as the PAC keys
// (apiakey*, apdakey*, ...), breaking kernel-wide pointer authentication, so mark
// only the general-purpose set as writable.
bool writable = descriptor.has ("set") && descriptor.get_string ("set") == "General Purpose Registers";
regs.add (new GDB.Client.Register (name, altname, n, bitsize, writable));
}
install_registers (regs);
}
private static GDB.TargetArch arch_from_cpu_type (uint cpu_type) {
switch (cpu_type) {
case 0x00000007: return IA32;
case 0x01000007: return X64;
case 0x0000000c: return ARM;
case 0x0100000c: return ARM64;
default: return UNKNOWN;
}
}
public override async Bytes read_byte_array (uint64 address, size_t size, Cancellable? cancellable = null)
throws Error, IOError {
var result = new uint8[size];
size_t offset = 0;
while (offset != size) {
size_t chunk_size = size_t.min (size - offset, MAX_BYTES_PER_READ);
var request = make_packet_builder_sized (32)
.append_c ('x')
.append_address (address + offset)
.append_c (',')
.append_size (chunk_size)
.build ();
var response = yield query (request, cancellable);
Bytes chunk = response.payload_bytes;
if (chunk.get_size () != chunk_size) {
throw new Error.INVALID_ARGUMENT (
"Unable to read from 0x%" + uint64.FORMAT_MODIFIER + "x: invalid address", address);
}
Memory.copy ((uint8 *) result + offset, chunk.get_data (), chunk_size);
offset += chunk_size;
}
return new Bytes.take ((owned) result);
}
}
}
+1 -1
View File
@@ -96,7 +96,7 @@ def build_backend(
symbol_dest.mkdir(parents=True, exist_ok=True)
src_dir = base_dir / "symbol-replacer"
for name in ("main.go", "trie.go", "go.mod"):
for name in ("main.go", "trie.go", "stringtables.go", "go.mod"):
shutil.copy(src_dir / name, symbol_dest)
env_copy = config["env"].copy()
+152 -9
View File
@@ -1,8 +1,28 @@
namespace Frida {
/**
* Compiles a TypeScript or JavaScript project into a single script bundle
* that can be loaded with {@link Session.create_script}.
*/
public sealed class Compiler : Object {
/**
* Emitted when a build is starting.
*/
public signal void starting ();
/**
* Emitted when a build has finished.
*/
public signal void finished ();
/**
* Emitted with the freshly built bundle, primarily when watching.
*
* @param bundle the compiled script bundle
*/
public signal void output (string bundle);
/**
* Emitted with compiler diagnostics, such as type errors and warnings.
*
* @param diagnostics the diagnostics, as a variant
*/
public signal void diagnostics (Variant diagnostics);
private size_t watch_session_handle = 0;
@@ -11,6 +31,11 @@ namespace Frida {
private MainContext main_context;
// TODO: Remove the DeviceManager parameter.
/**
* Creates a compiler.
*
* @param manager unused; kept for compatibility
*/
public Compiler (DeviceManager? manager = null) {
Object ();
}
@@ -27,6 +52,13 @@ namespace Frida {
cancel_watch ();
}
/**
* Builds the project rooted at @entrypoint once.
*
* @param entrypoint path to the project's entrypoint module
* @param options build options, or null for the defaults
* @return the compiled script bundle
*/
public async string build (string entrypoint, BuildOptions? options = null, Cancellable? cancellable = null)
throws Error, IOError {
CompilerBackend.check_available ();
@@ -78,6 +110,13 @@ namespace Frida {
}
}
/**
* Builds the project and keeps rebuilding it as its sources change,
* emitting {@link Compiler.output} with each fresh bundle.
*
* @param entrypoint path to the project's entrypoint module
* @param options watch options, or null for the defaults
*/
public async void watch (string entrypoint, WatchOptions? options = null, Cancellable? cancellable = null)
throws Error, IOError {
CompilerBackend.check_available ();
@@ -146,15 +185,16 @@ namespace Frida {
});
}
private void on_output (owned string bundle) {
private void on_output (string bundle) {
string bundle_copy = bundle;
schedule_on_frida_thread (() => {
output (bundle);
output (bundle_copy);
return Source.REMOVE;
});
}
private void on_diagnostic (owned string category, int code, owned string? path, int line, int character,
owned string text) {
private void on_diagnostic (string category, int code, string? path, int line, int character,
string text) {
var diag = new Diagnostic () {
category = category,
code = code,
@@ -342,13 +382,13 @@ namespace Frida {
#endif
}
private delegate void BuildCompleteFunc (owned string? bundle, owned string? error_message);
private delegate void WatchReadyFunc (size_t session_handle, owned string? error_message);
private delegate void BuildCompleteFunc (string? bundle, string? error_message);
private delegate void WatchReadyFunc (size_t session_handle, string? error_message);
private delegate void StartingFunc ();
private delegate void FinishedFunc ();
private delegate void OutputFunc (owned string bundle);
private delegate void DiagnosticFunc (owned string category, int code, owned string? path, int line, int character,
owned string text);
private delegate void OutputFunc (string bundle);
private delegate void DiagnosticFunc (string category, int code, string? path, int line, int character,
string text);
#if HAVE_COMPILER_BACKEND && (COMPILER_BACKEND_EMBEDDED_LIBRARY || COMPILER_BACKEND_INSTALLED_LIBRARY)
private T resolve_symbol<T> (Module m, string name) {
@@ -979,42 +1019,66 @@ namespace Frida {
return Environment.get_current_dir ();
}
/**
* Options shared by {@link Compiler.build} and {@link Compiler.watch}.
*/
public class CompilerOptions : Object {
/**
* The project root directory, or null to infer it from the entrypoint.
*/
public string? project_root {
get;
set;
}
/**
* How the resulting bundle's bytes are encoded.
*/
public OutputFormat output_format {
get;
set;
default = UNESCAPED;
}
/**
* The module format of the bundle.
*/
public BundleFormat bundle_format {
get;
set;
default = ESM;
}
/**
* Whether to type-check the project.
*/
public TypeCheckMode type_check {
get;
set;
default = FULL;
}
/**
* Whether to include source maps in the bundle.
*/
public SourceMaps source_maps {
get;
set;
default = INCLUDED;
}
/**
* Which compressor to run the output through, if any.
*/
public JsCompression compression {
get;
set;
default = NONE;
}
/**
* Which platform the bundle targets.
*/
public JsPlatform platform {
get;
set;
@@ -1023,29 +1087,60 @@ namespace Frida {
internal Gee.List<string> externals = new Gee.ArrayList<string> ();
/**
* Removes all configured externals.
*/
public void clear_externals () {
externals.clear ();
}
/**
* Marks a module as external, leaving it out of the bundle.
*
* @param external the module specifier to treat as external
*/
public void add_external (string external) {
externals.add (external);
}
/**
* Invokes @func for each configured external.
*
* @param func function called with each external
*/
public void enumerate_externals (Func<string> func) {
foreach (var external in externals)
func (external);
}
}
/**
* Options for {@link Compiler.build}.
*/
public sealed class BuildOptions : CompilerOptions {
}
/**
* Options for {@link Compiler.watch}.
*/
public sealed class WatchOptions : CompilerOptions {
}
/**
* How a compiled bundle's bytes are encoded.
*/
public enum OutputFormat {
/**
* Raw, unescaped JavaScript.
*/
UNESCAPED,
/**
* A hex-encoded byte string.
*/
HEX_BYTES,
/**
* A C string literal.
*/
C_STRING;
public static OutputFormat from_nick (string nick) throws Error {
@@ -1057,8 +1152,17 @@ namespace Frida {
}
}
/**
* The module format of a compiled bundle.
*/
public enum BundleFormat {
/**
* An ECMAScript module.
*/
ESM,
/**
* An immediately-invoked function expression.
*/
IIFE;
public static BundleFormat from_nick (string nick) throws Error {
@@ -1070,8 +1174,17 @@ namespace Frida {
}
}
/**
* Whether the compiler type-checks the project.
*/
public enum TypeCheckMode {
/**
* Perform full type checking.
*/
FULL,
/**
* Skip type checking.
*/
NONE;
public static TypeCheckMode from_nick (string nick) throws Error {
@@ -1083,8 +1196,17 @@ namespace Frida {
}
}
/**
* Whether source maps are included in a compiled bundle.
*/
public enum SourceMaps {
/**
* Include source maps.
*/
INCLUDED,
/**
* Omit source maps.
*/
OMITTED;
public static SourceMaps from_nick (string nick) throws Error {
@@ -1096,8 +1218,17 @@ namespace Frida {
}
}
/**
* Which compressor the compiled JavaScript is run through.
*/
public enum JsCompression {
/**
* No compression.
*/
NONE,
/**
* Compress with Terser.
*/
TERSER;
public static JsCompression from_nick (string nick) throws Error {
@@ -1109,9 +1240,21 @@ namespace Frida {
}
}
/**
* The platform a compiled bundle targets.
*/
public enum JsPlatform {
/**
* Frida's Gum runtime inside a target process.
*/
GUM,
/**
* A web browser.
*/
BROWSER,
/**
* A platform-neutral environment.
*/
NEUTRAL;
public static JsPlatform from_nick (string nick) throws Error {
+15 -3
View File
@@ -4,6 +4,7 @@ package main
/*
#include <stdint.h>
#include <stdlib.h>
typedef enum {
FRIDA_OUTPUT_UNESCAPED,
@@ -126,6 +127,8 @@ func _frida_compiler_backend_build(cProjectRoot, cEntrypoint *C.char, outputForm
cErrorMessage = C.CString(err.Error())
}
C.invoke_build_complete_func(onComplete.Func, cBundle, cErrorMessage, onComplete.Data)
C.free(unsafe.Pointer(cBundle))
C.free(unsafe.Pointer(cErrorMessage))
}()
}
@@ -172,7 +175,9 @@ func _frida_compiler_backend_watch(cProjectRoot, cEntrypoint *C.char, outputForm
C.invoke_finished_func(onFinished.Func, onFinished.Data)
},
OnOutput: func(bundle string) {
C.invoke_output_func(onOutput.Func, C.CString(bundle), onOutput.Data)
cBundle := C.CString(bundle)
C.invoke_output_func(onOutput.Func, cBundle, onOutput.Data)
C.free(unsafe.Pointer(cBundle))
},
OnDiagnostic: makeCBuildDiagnosticCallback(onDiagnostic),
}
@@ -187,6 +192,7 @@ func _frida_compiler_backend_watch(cProjectRoot, cEntrypoint *C.char, outputForm
cErrorMessage = C.CString(err.Error())
}
C.invoke_watch_ready_func(onReady.Func, cSession, cErrorMessage, onReady.Data)
C.free(unsafe.Pointer(cErrorMessage))
}()
}
@@ -220,9 +226,15 @@ func makeCBuildDiagnosticCallback(onDiagnostic *CDelegate[C.FridaDiagnosticFunc]
if d.path != "" {
cPath = C.CString(d.path)
}
cCategory := C.CString(d.category)
cText := C.CString(d.text)
C.invoke_diagnostic_func(onDiagnostic.Func, C.CString(d.category), C.int(d.code), cPath, C.int(d.line),
C.int(d.character), C.CString(d.text), onDiagnostic.Data)
C.invoke_diagnostic_func(onDiagnostic.Func, cCategory, C.int(d.code), cPath, C.int(d.line),
C.int(d.character), cText, onDiagnostic.Data)
C.free(unsafe.Pointer(cCategory))
C.free(unsafe.Pointer(cPath))
C.free(unsafe.Pointer(cText))
}
}
+4 -4
View File
@@ -33,7 +33,7 @@
"@frida/url": "^1.0.2",
"@frida/util": "^1.0.3",
"@frida/vm": "^2.0.0",
"@types/frida-gum": "19.0.2",
"@types/frida-gum": "^19.10.0",
"@types/node": "18.19.103",
"frida-fs": "7.0.0"
}
@@ -248,9 +248,9 @@
"license": "MIT"
},
"node_modules/@types/frida-gum": {
"version": "19.0.2",
"resolved": "https://registry.npmjs.org/@types/frida-gum/-/frida-gum-19.0.2.tgz",
"integrity": "sha512-oh/f1r8dIMDWBoziXP3geIJ8ga/siU32MuifBztGsDglOKbHrhS0E3z0Dab0MIe3UFzC4Bvz/GRHfin2LDE8BQ==",
"version": "19.10.0",
"resolved": "https://registry.npmjs.org/@types/frida-gum/-/frida-gum-19.10.0.tgz",
"integrity": "sha512-GBKAzwmamtNPBXEcIve1yJ6BB9ZgyW9al3+f06S+lDAvennOx9GfFQ9RDY9kP5nJrkiiRHLzgbDFjQ/+g+iw1Q==",
"license": "MIT"
},
"node_modules/@types/node": {
+1 -1
View File
@@ -29,7 +29,7 @@
"@frida/url": "^1.0.2",
"@frida/util": "^1.0.3",
"@frida/vm": "^2.0.0",
"@types/frida-gum": "19.0.2",
"@types/frida-gum": "^19.10.0",
"@types/node": "18.19.103",
"frida-fs": "7.0.0"
}
+1 -1
View File
@@ -1,3 +1,3 @@
module github.com/frida/frida-symbol-replacer
go 1.24.4
go 1.24.3
+19 -11
View File
@@ -71,17 +71,7 @@ func main() {
}
data, _ := io.ReadAll(f)
modifiedData := make([]byte, len(data))
for i := 0; i < len(data); {
if l, replacement, ok := t.search(data, i); ok {
copy(modifiedData[i:], replacement)
i += l
} else {
modifiedData[i] = data[i]
i++
}
}
modifiedData := renameSymbolsWithinStringTables(data, t)
f.Truncate(0)
f.Seek(0, 0)
@@ -95,6 +85,24 @@ func main() {
}
}
func renameSymbolsWithinStringTables(data []byte, t *trie) []byte {
renamed := make([]byte, len(data))
copy(renamed, data)
for _, r := range symbolStringTableRanges(data) {
for i := r.lo; i < r.hi; {
if length, replacement, ok := t.search(data, i); ok && i+length <= r.hi {
copy(renamed[i:], replacement)
i += length
} else {
i++
}
}
}
return renamed
}
func flipAlpha(s []byte) []byte {
dt := make([]byte, len(s))
copy(dt, s)
@@ -0,0 +1,154 @@
package main
import (
"bytes"
"debug/elf"
"debug/pe"
"encoding/binary"
"strconv"
"strings"
)
const coffSymbolSize = 18
// debug/macho cannot read the symbol table of the object the Go linker emits
// on Darwin, so the Mach-O load commands are parsed directly.
var machoMagic64 = []byte{0xcf, 0xfa, 0xed, 0xfe}
const (
machoHeaderSize = 32
machoSymtabCommand = 0x2
)
type span struct {
lo, hi int
}
type member struct {
name string
span
}
func symbolStringTableRanges(data []byte) []span {
var ranges []span
for _, m := range archiveMembers(data) {
// The archive symbol index maps names to members and is what the linker
// consults; ranlib does not rebuild it for every format, so its names
// are renamed wholesale. It holds only names and offsets, never the
// runtime data that must be left intact inside objects.
if isArchiveSymbolIndex(m.name) {
ranges = append(ranges, m.span)
continue
}
object := data[m.lo:m.hi]
for _, table := range objectStringTableRanges(object) {
ranges = append(ranges, span{m.lo + table.lo, m.lo + table.hi})
}
}
return ranges
}
func archiveMembers(data []byte) []member {
var members []member
pos := len("!<arch>\n")
for pos+60 <= len(data) {
header := data[pos : pos+60]
size, _ := strconv.Atoi(string(bytes.TrimSpace(header[48:58])))
body := pos + 60
end := body + size
lo := body
name := string(bytes.TrimRight(header[0:16], " "))
if longNameLength, ok := bsdLongNameLength(name); ok {
lo += longNameLength
name = string(data[body : body+longNameLength])
}
members = append(members, member{name, span{lo, end}})
pos = end + end%2
}
return members
}
func bsdLongNameLength(name string) (int, bool) {
if !strings.HasPrefix(name, "#1/") {
return 0, false
}
length, _ := strconv.Atoi(name[3:])
return length, true
}
func isArchiveSymbolIndex(name string) bool {
return name == "/" || name == "//" || strings.HasPrefix(name, "__.SYMDEF")
}
func objectStringTableRanges(object []byte) []span {
if f, err := elf.NewFile(bytes.NewReader(object)); err == nil {
return elfStringTableRanges(f)
}
if bytes.HasPrefix(object, machoMagic64) {
return machoStringTableRanges(object)
}
if f, err := pe.NewFile(bytes.NewReader(object)); err == nil {
return coffStringTableRanges(f)
}
return nil
}
func elfStringTableRanges(f *elf.File) []span {
var ranges []span
for _, s := range f.Sections {
if s.Type == elf.SHT_STRTAB {
ranges = append(ranges, span{int(s.Offset), int(s.Offset + s.Size)})
}
}
return ranges
}
func machoStringTableRanges(object []byte) []span {
commandCount := binary.LittleEndian.Uint32(object[16:])
offset := machoHeaderSize
for i := uint32(0); i < commandCount; i++ {
command := binary.LittleEndian.Uint32(object[offset:])
commandSize := binary.LittleEndian.Uint32(object[offset+4:])
if command == machoSymtabCommand {
lo := int(binary.LittleEndian.Uint32(object[offset+16:]))
size := int(binary.LittleEndian.Uint32(object[offset+20:]))
return []span{{lo, lo + size}}
}
offset += int(commandSize)
}
return nil
}
// COFF keeps names longer than eight bytes in the string table and stores
// shorter ones inline in the symbol record, where an eight-byte name has no
// terminator. Both must be renamed, and the inline ranges are exactly the
// record's name field so a match cannot reach into the record's other fields.
func coffStringTableRanges(f *pe.File) []span {
symbolTable := int(f.FileHeader.PointerToSymbolTable)
symbolCount := int(f.FileHeader.NumberOfSymbols)
stringTable := symbolTable + symbolCount*coffSymbolSize
ranges := []span{{stringTable + 4, stringTable + 4 + len(f.StringTable)}}
for i := 0; i < symbolCount; {
symbol := f.COFFSymbols[i]
if storedInline(symbol.Name) {
name := symbolTable + i*coffSymbolSize
ranges = append(ranges, span{name, name + 8})
}
i += 1 + int(symbol.NumberOfAuxSymbols)
}
return ranges
}
func storedInline(name [8]uint8) bool {
return name[0] != 0 || name[1] != 0 || name[2] != 0 || name[3] != 0
}
+98 -18
View File
@@ -1,10 +1,20 @@
namespace Frida {
/**
* Hosts a Frida control endpoint that frida-server-style clients can connect
* to, exposing the local system over the network.
*/
public sealed class ControlService : Object {
/**
* The endpoint the service listens on.
*/
public EndpointParameters endpoint_params {
get;
construct;
}
/**
* The options the service was configured with.
*/
public ControlServiceOptions options {
get;
construct;
@@ -39,6 +49,12 @@ namespace Frida {
STOPPING
}
/**
* Creates a control service listening on the given endpoint.
*
* @param endpoint_params the endpoint to listen on
* @param options service options, or null for the defaults
*/
public ControlService (EndpointParameters endpoint_params, ControlServiceOptions? options = null) throws Error {
#if HAVE_LOCAL_BACKEND
ControlServiceOptions opts = (options != null) ? options : new ControlServiceOptions ();
@@ -73,6 +89,14 @@ namespace Frida {
#endif
}
/**
* Creates a control service exposing a specific device rather than the
* local system.
*
* @param device the device to expose
* @param endpoint_params the endpoint to listen on
* @param options service options, or null for the defaults
*/
public async ControlService.with_device (Device device, EndpointParameters endpoint_params,
ControlServiceOptions? options = null, Cancellable? cancellable = null) throws Error, IOError {
ControlServiceOptions opts = (options != null) ? options : new ControlServiceOptions ();
@@ -98,6 +122,7 @@ namespace Frida {
private void assign_session (HostSession session, HostSessionProvider provider) {
host_session = session;
host_session.spawn_gating_disabled.connect (on_spawn_gating_disabled);
host_session.spawn_added.connect (notify_spawn_added);
host_session.child_added.connect (notify_child_added);
host_session.child_removed.connect (notify_child_removed);
@@ -111,6 +136,9 @@ namespace Frida {
this.provider = provider;
}
/**
* Starts the service, binding the endpoint and accepting clients.
*/
public async void start (Cancellable? cancellable = null) throws Error, IOError {
if (state != STOPPED)
throw new Error.INVALID_OPERATION ("Invalid operation");
@@ -146,6 +174,9 @@ namespace Frida {
}
}
/**
* Stops the service, disconnecting clients and releasing the endpoint.
*/
public async void stop (Cancellable? cancellable = null) throws Error, IOError {
if (state != STARTED)
throw new Error.INVALID_OPERATION ("Invalid operation");
@@ -359,18 +390,30 @@ namespace Frida {
public void kick_authentication_channel (AuthenticationChannel channel) {
var source = new IdleSource ();
source.set_callback (() => {
channel.connection.close.begin (io_cancellable);
perform_kick.begin (channel);
return false;
});
source.attach (MainContext.get_thread_default ());
}
private async void perform_kick (AuthenticationChannel channel) {
try {
yield channel.connection.flush (io_cancellable);
} catch (GLib.Error e) {
}
try {
yield channel.connection.close (io_cancellable);
} catch (GLib.Error e) {
}
}
public async void teardown_control_channel (ControlChannel channel) {
yield parent.teardown_control_channel (channel);
}
public async void enable_spawn_gating (ControlChannel requester) throws GLib.Error {
yield parent.enable_spawn_gating (requester);
public async void enable_spawn_gating (HashTable<string, Variant> options, ControlChannel requester)
throws GLib.Error {
yield parent.enable_spawn_gating (options, requester);
}
public async void disable_spawn_gating (ControlChannel requester) throws GLib.Error {
@@ -508,20 +551,27 @@ namespace Frida {
return channels.iterator ();
}
private async void enable_spawn_gating (ControlChannel requester) throws GLib.Error {
private async void enable_spawn_gating (HashTable<string, Variant> options, ControlChannel requester)
throws GLib.Error {
bool is_first = spawn_gaters.is_empty;
spawn_gaters.add (requester);
foreach (var spawn in pending_spawn.values)
spawn.pending_approvers.add (requester);
if (is_first)
yield host_session.enable_spawn_gating (io_cancellable);
yield host_session.enable_spawn_gating_with_options (options, io_cancellable);
}
private async void disable_spawn_gating (ControlChannel requester) throws GLib.Error {
if (spawn_gaters.remove (requester)) {
foreach (uint pid in pending_spawn.keys.to_array ())
host_session.resume.begin (pid, io_cancellable);
foreach (uint pid in pending_spawn.keys.to_array ()) {
PendingSpawn spawn = pending_spawn[pid];
var approvers = spawn.pending_approvers;
if (approvers.remove (requester) && approvers.is_empty) {
forget_pending_spawn (pid, spawn);
host_session.resume.begin (pid, io_cancellable);
}
}
}
if (spawn_gaters.is_empty)
@@ -546,11 +596,8 @@ namespace Frida {
var approvers = spawn.pending_approvers;
approvers.remove (requester);
if (approvers.is_empty) {
pending_spawn.unset (pid);
forget_pending_spawn (pid, spawn);
yield host_session.resume (pid, io_cancellable);
notify_spawn_removed (spawn.info);
}
}
@@ -657,11 +704,29 @@ namespace Frida {
return id;
}
// Backend disabled gating and already resumed everything; drop our mirror, tell the gaters.
private void on_spawn_gating_disabled (SpawnGatingDisabledReason reason) {
foreach (var spawn in pending_spawn.values.to_array ())
notify_spawn_removed (spawn.info);
pending_spawn.clear ();
foreach (ControlChannel channel in spawn_gaters)
channel.spawn_gating_disabled (reason);
spawn_gaters.clear ();
}
private void notify_spawn_added (HostSpawnInfo info) {
pending_spawn[info.pid] = new PendingSpawn (info.pid, info.identifier, spawn_gaters.iterator ());
foreach (ControlChannel channel in spawn_gaters)
channel.spawn_added (info);
}
private void forget_pending_spawn (uint pid, PendingSpawn spawn) {
pending_spawn.unset (pid);
notify_spawn_removed (spawn.info);
}
private void notify_spawn_removed (HostSpawnInfo info) {
foreach (ControlChannel channel in spawn_gaters)
channel.spawn_removed (info);
@@ -697,12 +762,13 @@ namespace Frida {
private void on_agent_session_detached (AgentSessionId id, SessionDetachReason reason, CrashInfo crash) {
AgentSessionEntry entry;
if (agent_sessions.unset (id, out entry)) {
ControlChannel? controller = entry.controller;
if (controller != null) {
controller.agent_sessions.remove (id);
controller.agent_session_detached (id, reason, crash);
}
bool entry_found = agent_sessions.unset (id, out entry);
ControlChannel? controller = entry_found ? entry.controller : null;
GLib.info ("detach-trace: ControlService session detached id=%s reason=%d entry_found=%s controller=%p",
id.handle, reason, entry_found.to_string (), controller);
if (controller != null) {
controller.agent_sessions.remove (id);
controller.agent_session_detached (id, reason, crash);
}
}
@@ -896,7 +962,12 @@ namespace Frida {
}
public async void enable_spawn_gating (Cancellable? cancellable) throws GLib.Error {
yield parent.enable_spawn_gating (this);
yield parent.enable_spawn_gating (make_parameters_dict (), this);
}
public async void enable_spawn_gating_with_options (HashTable<string, Variant> options,
Cancellable? cancellable) throws GLib.Error {
yield parent.enable_spawn_gating (options, this);
}
public async void disable_spawn_gating (Cancellable? cancellable) throws GLib.Error {
@@ -1124,13 +1195,22 @@ namespace Frida {
}
}
/**
* Options for a {@link ControlService}.
*/
public sealed class ControlServiceOptions : Object {
/**
* Whether to preload the agent for faster first attach.
*/
public bool enable_preload {
get;
set;
default = true;
}
/**
* Whether to capture and report crashes in instrumented processes.
*/
public bool report_crashes {
get;
set;
+36
View File
@@ -177,6 +177,13 @@ function applyJailbreakQuirks() {
if (inserterResume !== null) {
pidsToIgnore = new Set();
instrumentInserter(inserterResume);
return;
}
const ellekitGate = findEllekitShouldEnableTweaks();
if (ellekitGate !== null) {
pidsToIgnore = new Set();
instrumentEllekitInjector(ellekitGate);
}
}
@@ -237,6 +244,18 @@ function instrumentInserter(at) {
}, 'int', ['uint', 'uint', 'uint', 'uint']));
}
function instrumentEllekitInjector(shouldEnableTweaks) {
const retType = 'int';
const argTypes = ['uint'];
const original = new NativeFunction(shouldEnableTweaks, retType, argTypes);
Interceptor.replace(shouldEnableTweaks, new NativeCallback(pid => {
if (pidsToIgnore.delete(pid))
return 0;
return original(pid);
}, retType, argTypes));
}
function findJbdCallImpl() {
const impl = Module.findGlobalExportByName('jbd_call');
if (impl !== null)
@@ -330,3 +349,20 @@ function findInserterResume() {
return null;
}
function findEllekitShouldEnableTweaks() {
const impl = Module.findGlobalExportByName('should_enable_tweaks');
if (impl !== null)
return impl;
for (const name of ['libinjector.dylib', 'TweakLoader.dylib']) {
const mod = Process.findModuleByName(name);
if (mod !== null) {
const exp = mod.findExportByName('should_enable_tweaks');
if (exp !== null)
return exp;
}
}
return null;
}
+68 -4
View File
@@ -65,6 +65,7 @@ namespace Frida {
construct {
helper.output.connect (on_output);
helper.gating_cancelled.connect (on_spawn_gating_cancelled);
helper.spawn_added.connect (on_spawn_added);
helper.spawn_removed.connect (on_spawn_removed);
@@ -75,11 +76,12 @@ namespace Frida {
#if HAVE_EMBEDDED_ASSETS
var blob = Frida.Data.Agent.get_frida_agent_dylib_blob ();
agent = new AgentResource (blob.name, new Bytes.static (blob.data), tempdir);
agent = new AgentResource (blob.name, copy_to_aligned_pages (blob.data), tempdir);
#endif
#if IOS || TVOS
fruit_controller = new FruitController (this, io_cancellable);
fruit_controller.gating_cancelled.connect (on_spawn_gating_cancelled);
fruit_controller.spawn_added.connect (on_spawn_added);
fruit_controller.spawn_removed.connect (on_spawn_removed);
fruit_controller.process_crashed.connect (on_process_crashed);
@@ -91,6 +93,7 @@ namespace Frida {
#if IOS || TVOS
yield fruit_controller.close (cancellable);
fruit_controller.gating_cancelled.disconnect (on_spawn_gating_cancelled);
fruit_controller.spawn_added.disconnect (on_spawn_added);
fruit_controller.spawn_removed.disconnect (on_spawn_removed);
fruit_controller.process_crashed.disconnect (on_process_crashed);
@@ -113,6 +116,7 @@ namespace Frida {
yield helper.close (cancellable);
helper.output.disconnect (on_output);
helper.gating_cancelled.disconnect (on_spawn_gating_cancelled);
helper.spawn_added.disconnect (on_spawn_added);
helper.spawn_removed.disconnect (on_spawn_removed);
@@ -182,11 +186,17 @@ namespace Frida {
return yield process_enumerator.enumerate_processes (ProcessQueryOptions._deserialize (options));
}
public override async void enable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
public override async void enable_spawn_gating_with_options (HashTable<string, Variant> options,
Cancellable? cancellable) throws Error, IOError {
var scope = SpawnGatingOptions._deserialize (options).scope;
#if IOS || TVOS
if (scope == PROCESSES)
throw new Error.NOT_SUPPORTED (
"Process-level gating is unavailable on this OS; use the 'applications' scope");
yield fruit_controller.enable_spawn_gating (cancellable);
#else
yield helper.enable_spawn_gating (cancellable);
yield helper.enable_spawn_gating (scope, cancellable);
#endif
}
@@ -196,6 +206,8 @@ namespace Frida {
#else
yield helper.disable_spawn_gating (cancellable);
#endif
spawn_gating_disabled (APPLICATION_REQUESTED);
}
public override async HostSpawnInfo[] enumerate_pending_spawn (Cancellable? cancellable) throws Error, IOError {
@@ -218,7 +230,16 @@ namespace Frida {
return yield fruit_controller.spawn (path, options, cancellable);
#endif
return yield helper.spawn (path, options, cancellable);
UnixInputStream? stdin_stream = null;
UnixOutputStream? stdout_stream = null;
UnixOutputStream? stderr_stream = null;
if (options.stdio == INHERIT) {
stdin_stream = new UnixInputStream (0, false);
stdout_stream = new UnixOutputStream (1, false);
stderr_stream = new UnixOutputStream (2, false);
}
return yield helper.spawn (path, options, stdin_stream, stdout_stream, stderr_stream, cancellable);
}
protected override async void await_exec_transition (uint pid, Cancellable? cancellable) throws Error, IOError {
@@ -278,6 +299,10 @@ namespace Frida {
output (pid, fd, data);
}
private void on_spawn_gating_cancelled () {
spawn_gating_disabled (WATCHDOG_TIMEOUT);
}
private void on_spawn_added (HostSpawnInfo info) {
spawn_added (info);
}
@@ -323,10 +348,37 @@ namespace Frida {
}
public extern static string _path_for_application_identifier (string identifier) throws Error;
#if HAVE_EMBEDDED_ASSETS
private static Bytes copy_to_aligned_pages (uint8[] data) {
size_t page_size = Gum.query_page_size ();
size_t size = data.length;
uint8 * pages = (uint8 *) Gum.memory_allocate (null, size, page_size, READ | WRITE);
Memory.copy (pages, data, size);
unowned uint8[] view = (uint8[]) pages;
view.length = (int) size;
return Bytes.new_with_owner<AlignedPages> (view, new AlignedPages (pages, size));
}
private class AlignedPages {
private uint8 * pages;
private size_t size;
public AlignedPages (uint8 * pages, size_t size) {
this.pages = pages;
this.size = size;
}
~AlignedPages () {
Gum.memory_free (pages, size);
}
}
#endif
}
#if IOS || TVOS
private sealed class FruitController : Object, MappedAgentContainer {
public signal void gating_cancelled ();
public signal void spawn_added (HostSpawnInfo info);
public signal void spawn_removed (HostSpawnInfo info);
public signal void process_crashed (CrashInfo crash);
@@ -351,6 +403,7 @@ namespace Frida {
private bool spawn_gating_enabled = false;
private Gee.HashMap<string, Promise<uint>> spawn_requests = new Gee.HashMap<string, Promise<uint>> ();
private Gee.HashMap<uint, HostSpawnInfo?> pending_spawn = new Gee.HashMap<uint, HostSpawnInfo?> ();
private SpawnGatingWatchdog watchdog = new SpawnGatingWatchdog ();
private CrashReporterState crash_reporter_state;
private Gee.HashMap<uint, ReportCrashAgent> crash_agents = new Gee.HashMap<uint, ReportCrashAgent> ();
@@ -389,6 +442,8 @@ namespace Frida {
helper.process_resumed.connect (on_process_resumed);
helper.process_killed.connect (on_process_killed);
watchdog.expired.connect (on_watchdog_expired);
}
~FruitController () {
@@ -422,6 +477,8 @@ namespace Frida {
public async void disable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
spawn_gating_enabled = false;
watchdog.clear ();
yield launchd_agent.disable_spawn_gating (cancellable);
var pending = pending_spawn.values.to_array ();
@@ -486,6 +543,7 @@ namespace Frida {
HostSpawnInfo? info;
if (!pending_spawn.unset (pid, out info))
return false;
watchdog.cancel (pid);
spawn_removed (info);
yield helper.resume (pid, cancellable);
@@ -618,6 +676,11 @@ namespace Frida {
launchd_agent.claim_process.begin (pid, io_cancellable);
}
private void on_watchdog_expired () {
disable_spawn_gating.begin (io_cancellable);
gating_cancelled ();
}
private async void handle_spawn (HostSpawnInfo info) {
try {
var pid = info.pid;
@@ -636,6 +699,7 @@ namespace Frida {
}
pending_spawn[pid] = info;
watchdog.arm (pid);
spawn_added (info);
} catch (GLib.Error e) {
}
+52 -7
View File
@@ -7,6 +7,8 @@
#include <dlfcn.h>
#include <errno.h>
#import <Foundation/Foundation.h>
#include <gio/gunixinputstream.h>
#include <gio/gunixoutputstream.h>
#include <glib-unix.h>
#include <spawn.h>
#include <stdio.h>
@@ -793,7 +795,8 @@ _frida_darwin_helper_backend_schedule_on_dispatch_queue (FridaDarwinHelperBacken
guint
_frida_darwin_helper_backend_spawn (FridaDarwinHelperBackend * self, const gchar * path,
FridaHostSpawnOptions * options, FridaStdioPipes ** pipes, GError ** error)
FridaHostSpawnOptions * options, GUnixInputStream * stdin_stream, GUnixOutputStream * stdout_stream,
GUnixOutputStream * stderr_stream, FridaStdioPipes ** pipes, GError ** error)
{
pid_t pid;
FridaSpawnInstance * instance;
@@ -830,11 +833,17 @@ _frida_darwin_helper_backend_spawn (FridaDarwinHelperBackend * self, const gchar
if (stdio_error != NULL)
goto propagate_stdio_error;
if (in_fd != NULL)
if (stdin_stream != NULL)
posix_spawn_file_actions_adddup2 (&file_actions, g_unix_input_stream_get_fd (stdin_stream), 0);
else if (in_fd != NULL)
posix_spawn_file_actions_adddup2 (&file_actions, in_fd->handle, 0);
if (out_fd != NULL)
if (stdout_stream != NULL)
posix_spawn_file_actions_adddup2 (&file_actions, g_unix_output_stream_get_fd (stdout_stream), 1);
else if (out_fd != NULL)
posix_spawn_file_actions_adddup2 (&file_actions, out_fd->handle, 1);
if (err_fd != NULL)
if (stderr_stream != NULL)
posix_spawn_file_actions_adddup2 (&file_actions, g_unix_output_stream_get_fd (stderr_stream), 2);
else if (err_fd != NULL)
posix_spawn_file_actions_adddup2 (&file_actions, err_fd->handle, 2);
aslr_value = g_hash_table_lookup (options->aux, "aslr");
@@ -2127,7 +2136,11 @@ _frida_darwin_helper_backend_prepare_spawn_instance_for_injection (FridaDarwinHe
previous_ports = &instance->previous_ports;
kr = thread_swap_exception_ports (child_thread,
EXC_MASK_ALL,
#if __has_feature (ptrauth_calls)
EXC_MASK_BAD_ACCESS | EXC_MASK_BREAKPOINT | EXC_MASK_CRASH,
#else
EXC_MASK_BREAKPOINT | EXC_MASK_CRASH,
#endif
instance->server_port,
EXCEPTION_DEFAULT,
state_flavor,
@@ -2869,6 +2882,9 @@ frida_spawn_instance_on_server_recv (void * context)
thread_state_flavor_t state_flavor = GUM_DARWIN_THREAD_STATE_FLAVOR;
mach_msg_type_number_t state_count = GUM_DARWIN_THREAD_STATE_COUNT;
GumDarwinUnifiedThreadState state;
#if __has_feature (ptrauth_calls)
gboolean pc_may_need_fixup_due_to_ptrauth_failure;
#endif
guint i, current_bp_index;
FridaBreakpoint * breakpoint = NULL;
gboolean carry_on, pc_changed;
@@ -2897,7 +2913,6 @@ frida_spawn_instance_on_server_recv (void * context)
#if __has_feature (ptrauth_calls)
{
const GumAddress ret_gadget = frida_spawn_instance_get_ret_gadget_address (self);
gboolean pc_may_need_fixup_due_to_ptrauth_failure;
pc_may_need_fixup_due_to_ptrauth_failure = gum_strip_code_address (GUM_ADDRESS (state.ts_64.__opaque_pc)) == ret_gadget;
if (pc_may_need_fixup_due_to_ptrauth_failure)
@@ -2920,7 +2935,16 @@ frida_spawn_instance_on_server_recv (void * context)
#endif
if (request->exception != EXC_BREAKPOINT)
goto unexpected_exception;
{
#if __has_feature (ptrauth_calls)
if (!pc_may_need_fixup_due_to_ptrauth_failure)
#endif
{
if (request->exception == EXC_CRASH)
goto unexpected_exception;
goto forward_to_system;
}
}
if (self->single_stepping >= 0)
{
@@ -3074,6 +3098,27 @@ unexpected_exception:
g_error_free (error);
g_string_free (message, TRUE);
return;
}
forward_to_system:
{
__Reply__exception_raise_t response;
mach_msg_header_t * header;
bzero (&response, sizeof (response));
header = &response.Head;
header->msgh_bits = MACH_MSGH_BITS (MACH_MSG_TYPE_MOVE_SEND_ONCE, 0);
header->msgh_size = sizeof (response);
header->msgh_remote_port = request->Head.msgh_remote_port;
header->msgh_local_port = MACH_PORT_NULL;
header->msgh_reserved = 0;
header->msgh_id = request->Head.msgh_id + 100;
response.NDR = NDR_record;
response.RetCode = KERN_FAILURE;
kr = mach_msg_send (header);
if (kr == KERN_SUCCESS)
request->Head.msgh_remote_port = MACH_PORT_NULL;
return;
}
}
+42 -7
View File
@@ -49,6 +49,7 @@ namespace Frida {
dtrace_agent = DTraceAgent.try_open ();
if (dtrace_agent != null) {
dtrace_agent.gating_cancelled.connect (on_dtrace_agent_gating_cancelled);
dtrace_agent.spawn_added.connect (on_dtrace_agent_spawn_added);
dtrace_agent.spawn_removed.connect (on_dtrace_agent_spawn_removed);
}
@@ -97,6 +98,7 @@ namespace Frida {
yield;
if (dtrace_agent != null) {
dtrace_agent.gating_cancelled.disconnect (on_dtrace_agent_gating_cancelled);
dtrace_agent.spawn_added.disconnect (on_dtrace_agent_spawn_added);
dtrace_agent.spawn_removed.disconnect (on_dtrace_agent_spawn_removed);
yield dtrace_agent.close (cancellable);
@@ -107,8 +109,8 @@ namespace Frida {
public async void preload (Cancellable? cancellable) throws Error, IOError {
}
public async void enable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
get_dtrace_agent ().enable_spawn_gating ();
public async void enable_spawn_gating (SpawnGatingScope scope, Cancellable? cancellable) throws Error, IOError {
get_dtrace_agent ().enable_spawn_gating (scope);
}
public async void disable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
@@ -126,12 +128,13 @@ namespace Frida {
return dtrace_agent;
}
public async uint spawn (string path, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError {
public async uint spawn (string path, HostSpawnOptions options, UnixInputStream? stdin_stream,
UnixOutputStream? stdout_stream, UnixOutputStream? stderr_stream, Cancellable? cancellable) throws Error, IOError {
if (!FileUtils.test (path, EXISTS))
throw new Error.EXECUTABLE_NOT_FOUND ("Unable to find executable at '%s'", path);
StdioPipes? pipes;
var child_pid = _spawn (path, options, out pipes);
var child_pid = _spawn (path, options, stdin_stream, stdout_stream, stderr_stream, out pipes);
ChildWatch.add ((Pid) child_pid, on_child_dead);
@@ -629,6 +632,10 @@ namespace Frida {
}
}
private void on_dtrace_agent_gating_cancelled () {
gating_cancelled ();
}
private void on_dtrace_agent_spawn_added (HostSpawnInfo info) {
spawn_added (info);
}
@@ -664,7 +671,8 @@ namespace Frida {
protected extern void _destroy_dispatch_context ();
protected extern void _schedule_on_dispatch_queue (DispatchWorker worker);
protected extern uint _spawn (string path, HostSpawnOptions options, out StdioPipes? pipes) throws Error;
protected extern uint _spawn (string path, HostSpawnOptions options, UnixInputStream? stdin_stream,
UnixOutputStream? stdout_stream, UnixOutputStream? stderr_stream, out StdioPipes? pipes) throws Error;
protected extern static void _launch (string identifier, HostSpawnOptions options, LaunchCompletionHandler on_complete);
protected extern static bool _is_suspended (uint task) throws Error;
public extern static void resume_process (uint task) throws Error;
@@ -688,15 +696,22 @@ namespace Frida {
}
public sealed class DTraceAgent : Object {
public signal void gating_cancelled ();
public signal void spawn_added (HostSpawnInfo info);
public signal void spawn_removed (HostSpawnInfo info);
private Subprocess? dtrace;
private DataInputStream input;
private SpawnGatingScope scope;
private Gee.HashMap<uint, HostSpawnInfo?> pending_spawn = new Gee.HashMap<uint, HostSpawnInfo?> ();
private SpawnGatingWatchdog watchdog = new SpawnGatingWatchdog ();
private Cancellable io_cancellable = new Cancellable ();
// Present in every path inside an app bundle, so APPLICATIONS gates the whole app
// (helpers, XPC services, ...), not just its entry point.
private const string APP_BUNDLE_MARKER = ".app/";
public static DTraceAgent? try_open () {
if (Posix.getuid () != 0)
return null;
@@ -704,6 +719,15 @@ namespace Frida {
return new DTraceAgent ();
}
construct {
watchdog.expired.connect (on_watchdog_expired);
}
private void on_watchdog_expired () {
disable_spawn_gating.begin (io_cancellable);
gating_cancelled ();
}
public async void close (Cancellable? cancellable) throws IOError {
if (dtrace != null) {
try {
@@ -714,10 +738,12 @@ namespace Frida {
}
}
public void enable_spawn_gating () throws Error {
public void enable_spawn_gating (SpawnGatingScope scope) throws Error {
if (dtrace != null)
throw new Error.INVALID_OPERATION ("Already enabled");
this.scope = scope;
string? predicate = Environment.get_variable ("FRIDA_DTRACE_PREDICATE");
string predicate_clause;
if (predicate != null)
@@ -776,6 +802,7 @@ namespace Frida {
spawn_removed (e.value);
}
pending_spawn.clear ();
watchdog.clear ();
}
public HostSpawnInfo[] enumerate_pending_spawn () {
@@ -788,8 +815,10 @@ namespace Frida {
public void on_resume (uint pid) {
HostSpawnInfo? info;
if (pending_spawn.unset (pid, out info))
if (pending_spawn.unset (pid, out info)) {
watchdog.cancel (pid);
spawn_removed (info);
}
}
private async void process_incoming_messages () {
@@ -837,8 +866,14 @@ namespace Frida {
continue;
}
if (scope != PROCESSES && !path.contains (APP_BUNDLE_MARKER)) {
DarwinHelperBackend.resume_without_validation (pid);
continue;
}
var info = HostSpawnInfo (pid, path);
pending_spawn[pid] = info;
watchdog.arm (pid);
spawn_added (info);
}
} catch (IOError e) {
+3 -2
View File
@@ -23,7 +23,7 @@ namespace Frida {
public async void preload (Cancellable? cancellable) throws Error, IOError {
}
public async void enable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
public async void enable_spawn_gating (SpawnGatingScope scope, Cancellable? cancellable) throws Error, IOError {
throw_not_supported ();
}
@@ -35,7 +35,8 @@ namespace Frida {
throw_not_supported ();
}
public async uint spawn (string path, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError {
public async uint spawn (string path, HostSpawnOptions options, UnixInputStream? stdin_stream,
UnixOutputStream? stdout_stream, UnixOutputStream? stderr_stream, Cancellable? cancellable) throws Error, IOError {
throw_not_supported ();
}
+15 -3
View File
@@ -56,10 +56,10 @@ namespace Frida {
yield obtain (cancellable);
}
public async void enable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
public async void enable_spawn_gating (SpawnGatingScope scope, Cancellable? cancellable) throws Error, IOError {
var helper = yield obtain (cancellable);
try {
yield helper.enable_spawn_gating (cancellable);
yield helper.enable_spawn_gating (scope, cancellable);
} catch (GLib.Error e) {
throw_helper_error (e);
}
@@ -83,9 +83,12 @@ namespace Frida {
}
}
public async uint spawn (string path, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError {
public async uint spawn (string path, HostSpawnOptions options, UnixInputStream? stdin_stream,
UnixOutputStream? stdout_stream, UnixOutputStream? stderr_stream, Cancellable? cancellable) throws Error, IOError {
var helper = yield obtain (cancellable);
try {
if (stdin_stream != null)
return yield helper.spawn_with_stdio (path, options, stdin_stream, stdout_stream, stderr_stream, cancellable);
return yield helper.spawn (path, options, cancellable);
} catch (GLib.Error e) {
throw_helper_error (e);
@@ -340,6 +343,9 @@ namespace Frida {
GLib.SpawnFlags flags =
GLib.SpawnFlags.LEAVE_DESCRIPTORS_OPEN |
GLib.SpawnFlags.DO_NOT_REAP_CHILD |
GLib.SpawnFlags.STDOUT_TO_DEV_NULL |
GLib.SpawnFlags.STDERR_TO_DEV_NULL |
/* GLib.SpawnFlags.STDIN_FROM_DEV_NULL */ 2048 |
/* GLib.SpawnFlags.CLOEXEC_PIPES */ 256;
GLib.Process.spawn_async (null, argv, null, flags, null, out pending_pid);
@@ -420,6 +426,7 @@ namespace Frida {
proxy = pending_proxy;
proxy.output.connect (on_output);
proxy.gating_cancelled.connect (on_gating_cancelled);
proxy.spawn_added.connect (on_spawn_added);
proxy.spawn_removed.connect (on_spawn_removed);
proxy.injected.connect (on_injected);
@@ -439,6 +446,7 @@ namespace Frida {
obtain_request = null;
proxy.output.disconnect (on_output);
proxy.gating_cancelled.disconnect (on_gating_cancelled);
proxy.spawn_added.disconnect (on_spawn_added);
proxy.spawn_removed.disconnect (on_spawn_removed);
proxy.injected.disconnect (on_injected);
@@ -460,6 +468,10 @@ namespace Frida {
output (pid, fd, data);
}
private void on_gating_cancelled () {
gating_cancelled ();
}
private void on_spawn_added (HostSpawnInfo info) {
spawn_added (info);
}
+14 -3
View File
@@ -44,6 +44,7 @@ namespace Frida {
construct {
backend.idle.connect (on_backend_idle);
backend.output.connect (on_backend_output);
backend.gating_cancelled.connect (on_backend_gating_cancelled);
backend.spawn_added.connect (on_backend_spawn_added);
backend.spawn_removed.connect (on_backend_spawn_removed);
backend.injected.connect (on_backend_injected);
@@ -93,6 +94,7 @@ namespace Frida {
}
backend.idle.disconnect (on_backend_idle);
backend.output.disconnect (on_backend_output);
backend.gating_cancelled.disconnect (on_backend_gating_cancelled);
backend.spawn_added.disconnect (on_backend_spawn_added);
backend.spawn_removed.disconnect (on_backend_spawn_removed);
backend.injected.disconnect (on_backend_injected);
@@ -147,8 +149,8 @@ namespace Frida {
shutdown.begin ();
}
public async void enable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
yield backend.enable_spawn_gating (cancellable);
public async void enable_spawn_gating (SpawnGatingScope scope, Cancellable? cancellable) throws Error, IOError {
yield backend.enable_spawn_gating (scope, cancellable);
}
public async void disable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
@@ -160,7 +162,12 @@ namespace Frida {
}
public async uint spawn (string path, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError {
return yield backend.spawn (path, options, cancellable);
return yield backend.spawn (path, options, null, null, null, cancellable);
}
public async uint spawn_with_stdio (string path, HostSpawnOptions options, UnixInputStream stdin_stream,
UnixOutputStream stdout_stream, UnixOutputStream stderr_stream, Cancellable? cancellable) throws Error, IOError {
return yield backend.spawn (path, options, stdin_stream, stdout_stream, stderr_stream, cancellable);
}
public async void launch (string identifier, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError {
@@ -237,6 +244,10 @@ namespace Frida {
output (pid, fd, data);
}
private void on_backend_gating_cancelled () {
gating_cancelled ();
}
private void on_backend_spawn_added (HostSpawnInfo info) {
spawn_added (info);
}
+8 -3
View File
@@ -1,6 +1,7 @@
namespace Frida {
public interface DarwinHelper : Object {
public signal void output (uint pid, int fd, uint8[] data);
public signal void gating_cancelled ();
public signal void spawn_added (HostSpawnInfo info);
public signal void spawn_removed (HostSpawnInfo info);
public signal void injected (uint id, uint pid, bool has_mapped_module, DarwinModuleDetails mapped_module);
@@ -16,10 +17,11 @@ namespace Frida {
public abstract async void preload (Cancellable? cancellable) throws Error, IOError;
public abstract async void enable_spawn_gating (Cancellable? cancellable) throws Error, IOError;
public abstract async void enable_spawn_gating (SpawnGatingScope scope, Cancellable? cancellable) throws Error, IOError;
public abstract async void disable_spawn_gating (Cancellable? cancellable) throws Error, IOError;
public abstract async HostSpawnInfo[] enumerate_pending_spawn (Cancellable? cancellable) throws Error, IOError;
public abstract async uint spawn (string path, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError;
public abstract async uint spawn (string path, HostSpawnOptions options, UnixInputStream? stdin_stream,
UnixOutputStream? stdout_stream, UnixOutputStream? stderr_stream, Cancellable? cancellable) throws Error, IOError;
public abstract async void launch (string identifier, HostSpawnOptions options,
Cancellable? cancellable) throws Error, IOError;
public abstract async void notify_launch_completed (string identifier, uint pid,
@@ -49,6 +51,7 @@ namespace Frida {
[DBus (name = "re.frida.Helper")]
public interface DarwinRemoteHelper : Object {
public signal void output (uint pid, int fd, uint8[] data);
public signal void gating_cancelled ();
public signal void spawn_added (HostSpawnInfo info);
public signal void spawn_removed (HostSpawnInfo info);
public signal void injected (uint id, uint pid, bool has_mapped_module, DarwinModuleDetails mapped_module);
@@ -58,10 +61,12 @@ namespace Frida {
public abstract async void stop (Cancellable? cancellable) throws GLib.Error;
public abstract async void enable_spawn_gating (Cancellable? cancellable) throws GLib.Error;
public abstract async void enable_spawn_gating (SpawnGatingScope scope, Cancellable? cancellable) throws GLib.Error;
public abstract async void disable_spawn_gating (Cancellable? cancellable) throws GLib.Error;
public abstract async HostSpawnInfo[] enumerate_pending_spawn (Cancellable? cancellable) throws GLib.Error;
public abstract async uint spawn (string path, HostSpawnOptions options, Cancellable? cancellable) throws GLib.Error;
public abstract async uint spawn_with_stdio (string path, HostSpawnOptions options, UnixInputStream stdin_stream,
UnixOutputStream stdout_stream, UnixOutputStream stderr_stream, Cancellable? cancellable) throws GLib.Error;
public abstract async void launch (string identifier, HostSpawnOptions options, Cancellable? cancellable) throws GLib.Error;
public abstract async void notify_launch_completed (string identifier, uint pid,
Cancellable? cancellable) throws GLib.Error;
+68
View File
@@ -5,6 +5,7 @@
#include <errno.h>
#include <pwd.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <sys/sysctl.h>
@@ -98,6 +99,7 @@ extern int proc_pidpath (int pid, void * buffer, uint32_t buffer_size);
#endif
static void frida_add_process_metadata (GHashTable * parameters, const struct kinfo_proc * process);
static GVariant * frida_query_process_argv (guint pid);
static struct kinfo_proc * frida_system_query_kinfo_procs (guint * count);
static GVariant * frida_uid_to_name (uid_t uid);
@@ -614,7 +616,15 @@ frida_collect_process_info_from_kinfo (struct kinfo_proc * process, FridaEnumera
info.name = g_path_get_basename (path);
if (scope != FRIDA_SCOPE_MINIMAL)
{
GVariant * argv;
g_hash_table_insert (info.parameters, g_strdup ("path"), g_variant_ref_sink (g_variant_new_string (path)));
argv = frida_query_process_argv (info.pid);
if (argv != NULL)
g_hash_table_insert (info.parameters, g_strdup ("argv"), g_variant_ref_sink (argv));
}
}
if (still_alive)
@@ -822,6 +832,64 @@ frida_add_process_metadata (GHashTable * parameters, const struct kinfo_proc * p
g_date_time_unref (t0);
}
static GVariant *
frida_query_process_argv (guint pid)
{
GVariant * result = NULL;
int argmax;
size_t argmax_size = sizeof (argmax);
int argmax_mib[] = { CTL_KERN, KERN_ARGMAX };
gchar * buffer = NULL;
size_t buffer_size;
int args_mib[] = { CTL_KERN, KERN_PROCARGS2, (int) pid };
int argc;
const gchar * cursor, * end;
GVariantBuilder builder;
int i;
if (sysctl (argmax_mib, G_N_ELEMENTS (argmax_mib), &argmax, &argmax_size, NULL, 0) != 0)
goto beach;
buffer = g_malloc (argmax);
buffer_size = argmax;
if (sysctl (args_mib, G_N_ELEMENTS (args_mib), buffer, &buffer_size, NULL, 0) != 0)
goto beach;
if (buffer_size < sizeof (int))
goto beach;
memcpy (&argc, buffer, sizeof (int));
if (argc <= 0)
goto beach;
cursor = buffer + sizeof (int);
end = buffer + buffer_size;
while (cursor != end && *cursor != '\0')
cursor++;
while (cursor != end && *cursor == '\0')
cursor++;
g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
for (i = 0; i != argc && cursor != end; i++)
{
gsize arg_length = strnlen (cursor, end - cursor);
g_variant_builder_add_value (&builder, g_variant_new_take_string (g_strndup (cursor, arg_length)));
cursor += arg_length;
if (cursor != end)
cursor++;
}
result = g_variant_builder_end (&builder);
beach:
g_free (buffer);
return result;
}
static struct kinfo_proc *
frida_system_query_kinfo_procs (guint * count)
{
+18 -1
View File
@@ -521,6 +521,13 @@ namespace Frida {
}
}
// Forward the error untranslated so the caller can spot UNKNOWN_METHOD from an older remote.
public async void enable_spawn_gating_with_options (HashTable<string, Variant> options,
Cancellable? cancellable) throws GLib.Error {
var server = yield get_remote_server (cancellable);
yield server.session.enable_spawn_gating_with_options (options, cancellable);
}
public async void disable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
var server = yield get_remote_server (cancellable);
try {
@@ -920,6 +927,7 @@ namespace Frida {
server.connection.on_closed.connect (on_remote_connection_closed);
var session = server.session;
session.spawn_gating_disabled.connect (on_remote_spawn_gating_disabled);
session.spawn_added.connect (on_remote_spawn_added);
session.spawn_removed.connect (on_remote_spawn_removed);
session.child_added.connect (on_remote_child_added);
@@ -934,6 +942,7 @@ namespace Frida {
server.connection.on_closed.disconnect (on_remote_connection_closed);
var session = server.session;
session.spawn_gating_disabled.disconnect (on_remote_spawn_gating_disabled);
session.spawn_added.disconnect (on_remote_spawn_added);
session.spawn_removed.disconnect (on_remote_spawn_removed);
session.child_added.disconnect (on_remote_child_added);
@@ -954,6 +963,10 @@ namespace Frida {
on_remote_agent_session_detached (remote_id, CONNECTION_TERMINATED, no_crash);
}
private void on_remote_spawn_gating_disabled (SpawnGatingDisabledReason reason) {
spawn_gating_disabled (reason);
}
private void on_remote_spawn_added (HostSpawnInfo info) {
spawn_added (info);
}
@@ -980,7 +993,11 @@ namespace Frida {
private void on_remote_agent_session_detached (AgentSessionId remote_id, SessionDetachReason reason, CrashInfo crash) {
AgentSessionId? local_id;
if (!remote_agent_sessions.unset (remote_id, out local_id))
bool mapping_found = remote_agent_sessions.unset (remote_id, out local_id);
GLib.info ("detach-trace: Droidy remote session detached remote_id=%s reason=%d mapping_found=%s local_id=%s",
remote_id.handle, reason, mapping_found.to_string (),
mapping_found ? local_id.handle : "-");
if (!mapping_found)
return;
bool agent_session_found = agent_sessions.unset (local_id);
+26
View File
@@ -1,7 +1,21 @@
namespace Frida {
/**
* Watches a filesystem path and reports changes to it.
*/
public sealed class FileMonitor : Object {
/**
* Emitted when a change is observed at the watched path.
*
* @param file_path the path that changed
* @param other_file_path the other path involved, for example the
* destination of a rename, or null
* @param event the kind of change
*/
public signal void change (string file_path, string? other_file_path, FileMonitorEvent event);
/**
* The path being watched.
*/
public string path {
get;
construct;
@@ -9,6 +23,11 @@ namespace Frida {
private GLib.FileMonitor monitor;
/**
* Creates a file monitor for the given path.
*
* @param path the filesystem path to watch
*/
public FileMonitor (string path) {
Object (path: path);
}
@@ -17,6 +36,10 @@ namespace Frida {
clear ();
}
/**
* Starts watching, after which {@link FileMonitor.change} is emitted on
* each change.
*/
public async void enable (Cancellable? cancellable = null) throws Error, IOError {
if (monitor != null)
throw new Error.INVALID_OPERATION ("Already enabled");
@@ -43,6 +66,9 @@ namespace Frida {
}
}
/**
* Stops watching.
*/
public async void disable (Cancellable? cancellable = null) throws Error, IOError {
if (monitor == null)
throw new Error.INVALID_OPERATION ("Already disabled");
+2 -1
View File
@@ -77,7 +77,8 @@ namespace Frida {
return yield process_enumerator.enumerate_processes (ProcessQueryOptions._deserialize (options));
}
public override async void enable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
public override async void enable_spawn_gating_with_options (HashTable<string, Variant> options,
Cancellable? cancellable) throws Error, IOError {
throw new Error.NOT_SUPPORTED ("Not yet supported on this OS");
}
+54
View File
@@ -3,6 +3,7 @@
#include <errno.h>
#include <pwd.h>
#include <signal.h>
#include <string.h>
#include <sys/sysctl.h>
#include <sys/types.h>
#include <sys/user.h>
@@ -23,6 +24,7 @@ static void frida_add_process_metadata (GHashTable * parameters, const struct ki
static struct kinfo_proc * frida_system_query_kinfo_procs (guint * count);
static gboolean frida_system_query_proc_pathname (pid_t pid, gchar * path, gsize size);
static GVariant * frida_query_process_argv (guint pid);
static GVariant * frida_uid_to_name (uid_t uid);
void
@@ -116,7 +118,15 @@ frida_collect_process_info_from_kinfo (struct kinfo_proc * process, FridaEnumera
info.name = g_strdup (process->ki_comm);
if (scope != FRIDA_SCOPE_MINIMAL)
{
GVariant * argv;
g_hash_table_insert (info.parameters, g_strdup ("path"), g_variant_ref_sink (g_variant_new_string (path)));
argv = frida_query_process_argv (info.pid);
if (argv != NULL)
g_hash_table_insert (info.parameters, g_strdup ("argv"), g_variant_ref_sink (argv));
}
}
if (still_alive)
@@ -221,6 +231,50 @@ frida_system_query_proc_pathname (pid_t pid, gchar * path, gsize size)
return success;
}
static GVariant *
frida_query_process_argv (guint pid)
{
GVariant * result = NULL;
int mib[4];
gchar * buffer = NULL;
size_t size;
const gchar * cursor, * end;
GVariantBuilder builder;
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = KERN_PROC_ARGS;
mib[3] = pid;
size = 0;
if (sysctl (mib, G_N_ELEMENTS (mib), NULL, &size, NULL, 0) != 0 || size == 0)
goto beach;
buffer = g_malloc (size);
if (sysctl (mib, G_N_ELEMENTS (mib), buffer, &size, NULL, 0) != 0)
goto beach;
g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
cursor = buffer;
end = buffer + size;
while (cursor != end)
{
gsize arg_length = strnlen (cursor, end - cursor);
g_variant_builder_add_value (&builder, g_variant_new_take_string (g_strndup (cursor, arg_length)));
cursor += arg_length;
if (cursor != end)
cursor++;
}
result = g_variant_builder_end (&builder);
beach:
g_free (buffer);
return result;
}
static GVariant *
frida_uid_to_name (uid_t uid)
{
+899 -6
View File
File diff suppressed because it is too large. Load diff
+13
View File
@@ -742,6 +742,13 @@ namespace Frida {
}
}
// Forward the error untranslated so the caller can spot UNKNOWN_METHOD from an older remote.
public async void enable_spawn_gating_with_options (HashTable<string, Variant> options,
Cancellable? cancellable) throws GLib.Error {
var server = yield get_remote_server (cancellable);
yield server.session.enable_spawn_gating_with_options (options, cancellable);
}
public async void disable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
var server = yield get_remote_server (cancellable);
try {
@@ -1426,6 +1433,7 @@ namespace Frida {
server.connection.on_closed.connect (on_remote_connection_closed);
var session = server.session;
session.spawn_gating_disabled.connect (on_remote_spawn_gating_disabled);
session.spawn_added.connect (on_remote_spawn_added);
session.spawn_removed.connect (on_remote_spawn_removed);
session.child_added.connect (on_remote_child_added);
@@ -1440,6 +1448,7 @@ namespace Frida {
server.connection.on_closed.disconnect (on_remote_connection_closed);
var session = server.session;
session.spawn_gating_disabled.disconnect (on_remote_spawn_gating_disabled);
session.spawn_added.disconnect (on_remote_spawn_added);
session.spawn_removed.disconnect (on_remote_spawn_removed);
session.child_added.disconnect (on_remote_child_added);
@@ -1460,6 +1469,10 @@ namespace Frida {
on_remote_agent_session_detached (remote_id, CONNECTION_TERMINATED, no_crash);
}
private void on_remote_spawn_gating_disabled (SpawnGatingDisabledReason reason) {
spawn_gating_disabled (reason);
}
private void on_remote_spawn_added (HostSpawnInfo info) {
spawn_added (info);
}
+13 -13
View File
@@ -507,6 +507,19 @@ namespace Frida.Fruity.Injector {
yield output.write_all_async (bytes.get_data (), io_priority, cancellable, out bytes_written);
}
foreach (unowned Gum.DarwinSegment segment in module.segments.data) {
uint64 address = segment.vm_address + slide;
var protect_command = lldb.make_buffer_builder ()
.append_uint8 (UploadCommandType.PROTECT)
.append_uint64 (address)
.append_uint32 ((uint32) segment.vm_size)
.append_uint32 ((uint32) segment.protection)
.build ();
yield output.write_all_async (protect_command.get_data (), io_priority, cancellable,
out bytes_written);
}
if (!threaded_items.is_empty) {
var command = lldb.make_buffer_builder ()
.append_uint8 (UploadCommandType.APPLY_THREADED)
@@ -538,19 +551,6 @@ namespace Frida.Fruity.Injector {
out bytes_written);
}
foreach (unowned Gum.DarwinSegment segment in module.segments.data) {
uint64 address = segment.vm_address + slide;
var protect_command = lldb.make_buffer_builder ()
.append_uint8 (UploadCommandType.PROTECT)
.append_uint64 (address)
.append_uint32 ((uint32) segment.vm_size)
.append_uint32 ((uint32) segment.protection)
.build ();
yield output.write_all_async (protect_command.get_data (), io_priority, cancellable,
out bytes_written);
}
var construct_commands = lldb.make_buffer_builder ();
module.enumerate_init_pointers (ptrs => {
construct_commands
+1
View File
@@ -37,6 +37,7 @@ namespace Frida.LLDB {
protected override async void enable_extensions (Cancellable? cancellable) throws Error, IOError {
yield execute_simple ("QThreadSuffixSupported", cancellable);
supported_features.add ("thread-suffix");
yield execute_simple ("QListThreadsInStopReply", cancellable);
yield execute_simple ("QSetDetachOnError:0", cancellable);
}
+164 -57
View File
@@ -55,6 +55,11 @@ namespace Frida.GDB {
private Gee.List<StopObserverEntry> on_stop = new Gee.ArrayList<StopObserverEntry> ();
private size_t max_packet_size = 1024;
private AckMode ack_mode = SEND_ACKS;
internal bool bulk_registers = true;
protected void set_max_packet_size (size_t size) {
max_packet_size = size;
}
private Gee.Queue<Bytes> pending_writes = new Gee.ArrayQueue<Bytes> ();
private Promise<uint>? write_request;
private Gee.Queue<PendingResponse> pending_responses = new Gee.ArrayQueue<PendingResponse> ();
@@ -88,11 +93,6 @@ namespace Frida.GDB {
SKIP_ACKS
}
public enum ChecksumType {
PROPER,
ZEROED
}
protected const char NOTIFICATION_TYPE_EXIT_STATUS = 'W';
protected const char NOTIFICATION_TYPE_EXIT_SIGNAL = 'X';
protected const char NOTIFICATION_TYPE_STOP = 'S';
@@ -141,6 +141,8 @@ namespace Frida.GDB {
process_incoming_packets.begin ();
write_string (ACK_NOTIFICATION);
yield prepare_connection (cancellable);
string supported_response = yield query_property ("Supported", cancellable);
supported_features.add_all_array (supported_response.split (";"));
@@ -156,13 +158,15 @@ namespace Frida.GDB {
ack_mode = SKIP_ACKS;
}
if ("qXfer:features:read+" in supported_features)
yield load_target_properties (cancellable);
yield detect_vendor_features (cancellable);
yield enable_extensions (cancellable);
string attached_response = yield query_property ("Attached", cancellable);
if (attached_response == "1") {
yield load_target_properties (cancellable);
if (_exception == null) {
request_stop_info ();
yield wait_until_stopped (cancellable);
@@ -177,6 +181,33 @@ namespace Frida.GDB {
return true;
}
protected virtual async void prepare_connection (Cancellable? cancellable) throws Error, IOError {
}
protected async void halt (Cancellable? cancellable, uint timeout_msec = 0) throws Error, IOError {
change_state (RUNNING);
write_bytes (new Bytes ({ STOP_CHARACTER }));
request_stop_info ();
yield wait_until_stopped (cancellable, timeout_msec);
}
protected async void start_no_ack_mode (Cancellable? cancellable) throws Error, IOError {
yield execute_simple ("QStartNoAckMode", cancellable);
ack_mode = SKIP_ACKS;
}
protected void install_registers (Gee.List<Register> regs) {
registers = regs;
register_by_name = new Gee.HashMap<string, Register> ();
foreach (var reg in regs) {
register_by_name[reg.name] = reg;
string? altname = reg.altname;
if (altname != null)
register_by_name[altname] = reg;
}
}
protected virtual async void detect_vendor_features (Cancellable? cancellable) throws Error, IOError {
try {
string info = yield run_remote_command ("info", cancellable);
@@ -187,6 +218,16 @@ namespace Frida.GDB {
throw (IOError) e;
}
try {
var response = yield query_simple ("vCont?", cancellable);
unowned string payload = response.payload;
if (payload.length > 0 && payload[0] != 'E')
supported_features.add ("vcont");
} catch (GLib.Error e) {
if (e is IOError.CANCELLED)
throw (IOError) e;
}
try {
string response = yield query_property ("qemu.PhyMemMode", cancellable);
if (response.length == 1)
@@ -345,7 +386,7 @@ namespace Frida.GDB {
yield wait_until_stopped (cancellable);
}
private async void wait_until_stopped (Cancellable? cancellable) throws Error, IOError {
private async void wait_until_stopped (Cancellable? cancellable, uint timeout_msec = 0) throws Error, IOError {
var stop_observer = new StopObserverEntry (() => {
wait_until_stopped.callback ();
return false;
@@ -359,12 +400,28 @@ namespace Frida.GDB {
});
cancel_source.attach (MainContext.get_thread_default ());
bool timed_out = false;
TimeoutSource? timeout_source = null;
if (timeout_msec != 0) {
timeout_source = new TimeoutSource (timeout_msec);
timeout_source.set_callback (() => {
timed_out = true;
wait_until_stopped.callback ();
return false;
});
timeout_source.attach (MainContext.get_thread_default ());
}
yield;
if (timeout_source != null)
timeout_source.destroy ();
cancel_source.destroy ();
on_stop.remove (stop_observer);
if (timed_out)
throw new Error.TIMED_OUT ("Timed out while waiting for target to stop");
if (state == CLOSED)
throw new Error.TRANSPORT ("Connection closed while waiting for target to stop");
}
@@ -398,12 +455,20 @@ namespace Frida.GDB {
check_stopped ();
change_state (RUNNING);
var command = make_packet_builder_sized (16)
.append ("vCont;s:")
.append (thread.id)
.build ();
write_bytes (command);
if ("vcont" in features) {
var command = make_packet_builder_sized (16)
.append ("vCont;s:")
.append (thread.id)
.build ();
write_bytes (command);
} else {
// Expensive roundtrip
yield execute_simple ("Hg" + thread.id, cancellable);
var command = make_packet_builder_sized (16)
.append ("s")
.build ();
write_bytes (command);
}
yield wait_until_stopped (cancellable);
}
@@ -413,12 +478,19 @@ namespace Frida.GDB {
change_state (RUNNING);
var command = make_packet_builder_sized (16)
.append ("vCont;s:")
.append (thread.id)
.append (";c")
.build ();
write_bytes (command);
if ("vcont" in features) {
var command = make_packet_builder_sized (16)
.append ("vCont;s:")
.append (thread.id)
.append (";c")
.build ();
write_bytes (command);
} else {
var command = make_packet_builder_sized (16)
.append ("s")
.build ();
write_bytes (command);
}
}
public virtual async Bytes read_byte_array (uint64 address, size_t size, Cancellable? cancellable = null)
@@ -552,7 +624,7 @@ namespace Frida.GDB {
var output = new Gee.ArrayList<Packet> ();
Packet response = yield query_with_predicate (builder.build (), packet => {
unowned string payload = packet.payload;
if (payload.has_prefix ("OK") || payload[0] == 'E')
if (payload.has_prefix ("OK") || payload[0] == 'E' || payload == "")
return COMPLETE;
if (payload[0] == NOTIFICATION_TYPE_OUTPUT) {
output.add (packet);
@@ -560,6 +632,8 @@ namespace Frida.GDB {
}
return KEEP_TRYING;
}, cancellable);
if (response.payload == "")
throw new Error.NOT_SUPPORTED ("Command not supported by the remote stub");
check_execute_response (response);
var result = new StringBuilder ();
@@ -575,15 +649,7 @@ namespace Frida.GDB {
pointer_size = infer_pointer_size_from_arch (spec.arch);
byte_order = infer_byte_order_from_arch (spec.arch);
registers = spec.registers;
register_by_name = new Gee.HashMap<string, Register> ();
foreach (var reg in registers) {
register_by_name[reg.name] = reg;
string? altname = reg.altname;
if (altname != null)
register_by_name[altname] = reg;
}
install_registers (spec.registers);
}
protected void request_stop_info () {
@@ -996,8 +1062,7 @@ namespace Frida.GDB {
}
public PacketBuilder make_packet_builder_sized (size_t capacity) {
var checksum_type = (ack_mode == SEND_ACKS) ? ChecksumType.PROPER : ChecksumType.ZEROED;
return new PacketBuilder (capacity, checksum_type);
return new PacketBuilder (capacity);
}
private async Packet read_packet () throws Error, IOError {
@@ -1118,11 +1183,9 @@ namespace Frida.GDB {
public sealed class PacketBuilder {
private StringBuilder? buffer;
private size_t initial_capacity;
private ChecksumType checksum_type;
public PacketBuilder (size_t capacity, ChecksumType checksum_type) {
public PacketBuilder (size_t capacity) {
this.initial_capacity = capacity + Packet.OVERHEAD;
this.checksum_type = checksum_type;
reset ();
}
@@ -1206,13 +1269,7 @@ namespace Frida.GDB {
public Bytes build () {
buffer.append_c (CHECKSUM_CHARACTER);
if (checksum_type == PROPER) {
buffer.append_printf ("%02x", compute_checksum (buffer.str, 1, buffer.len - 2));
} else {
buffer.append ("00");
}
buffer.append_printf ("%02x", compute_checksum (buffer.str, 1, buffer.len - 2));
return StringBuilder.free_to_bytes ((owned) buffer);
}
}
@@ -1374,11 +1431,17 @@ namespace Frida.GDB {
private set;
}
public Register (string name, string? altname, uint id, uint bitsize) {
public bool writable {
get;
private set;
}
public Register (string name, string? altname, uint id, uint bitsize, bool writable = true) {
this.name = name;
this.altname = altname;
this.id = id;
this.bitsize = bitsize;
this.writable = writable;
}
}
@@ -1608,7 +1671,14 @@ namespace Frida.GDB {
}
public async Gee.Map<string, Variant> read_registers (Cancellable? cancellable = null) throws Error, IOError {
if (!client.bulk_registers)
return yield read_registers_individually (cancellable);
var response = yield client.query_simple ("g", cancellable);
if (response.payload.length == 0) {
client.bulk_registers = false;
return yield read_registers_individually (cancellable);
}
var result = new Gee.HashMap<string, Variant> ();
@@ -1650,7 +1720,30 @@ namespace Frida.GDB {
return result;
}
private async Gee.Map<string, Variant> read_registers_individually (Cancellable? cancellable) throws Error, IOError {
var result = new Gee.HashMap<string, Variant> ();
foreach (var reg in client.get_registers ()) {
if (reg.bitsize != 64)
continue;
try {
result[reg.name] = yield read_register (reg.name, cancellable);
} catch (Error e) {
// Some system registers are not readable in the current context; skip them.
}
}
return result;
}
public async void write_registers (Gee.Map<string, Variant> regs, Cancellable? cancellable = null) throws Error, IOError {
if (!client.bulk_registers) {
foreach (var e in regs.entries) {
var reg = client.get_register_by_name (e.key);
if (reg.writable && reg.bitsize == 64 && e.value.is_of_type (VariantType.UINT64))
yield write_register (e.key, e.value.get_uint64 (), cancellable);
}
return;
}
var builder = client.make_packet_builder_sized (2048)
.append_c ('G');
@@ -1677,33 +1770,47 @@ namespace Frida.GDB {
public async uint64 read_register (string name, Cancellable? cancellable = null) throws Error, IOError {
var reg = client.get_register_by_name (name);
var request = client.make_packet_builder_sized (32)
var request = client.make_packet_builder_sized (32);
request
.append_c ('p')
.append_register_id (reg.id)
.append (";thread:")
.append (id)
.append_c (';')
.build ();
.append_register_id (reg.id);
if ("thread-suffix" in client.features) {
request
.append (";thread:")
.append (id)
.append_c (';');
} else {
yield client.execute_simple ("Hg" + id, cancellable);
}
var response = yield client.query (request, cancellable);
var response = yield client.query (request.build (), cancellable);
return Protocol.parse_integer_value (response.payload, client.byte_order);
unowned string payload = response.payload;
if (payload.length == 0 || payload[0] == 'E')
throw new Error.NOT_SUPPORTED ("Unable to read register “%s”", name);
return Protocol.parse_integer_value (payload, client.byte_order);
}
public async void write_register (string name, uint64 val, Cancellable? cancellable = null) throws Error, IOError {
var reg = client.get_register_by_name (name);
var command = client.make_packet_builder_sized (48)
var command = client.make_packet_builder_sized (48);
command
.append_c ('P')
.append_register_id (reg.id)
.append_c ('=')
.append (Protocol.unparse_integer_value (val, client.pointer_size, client.byte_order))
.append (";thread:")
.append (id)
.append_c (';')
.build ();
.append (Protocol.unparse_integer_value (val, client.pointer_size, client.byte_order));
if ("thread-suffix" in client.features) {
command
.append (";thread:")
.append (id)
.append_c (';');
} else {
yield client.execute_simple ("Hg" + id, cancellable);
}
yield client.execute (command, cancellable);
yield client.execute (command.build (), cancellable);
}
}
+48 -17
View File
@@ -484,7 +484,12 @@ namespace Frida {
public abstract async HostProcessInfo[] enumerate_processes (HashTable<string, Variant> options,
Cancellable? cancellable) throws Error, IOError;
public abstract async void enable_spawn_gating (Cancellable? cancellable) throws Error, IOError;
public async void enable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
yield enable_spawn_gating_with_options (make_parameters_dict (), cancellable);
}
public abstract async void enable_spawn_gating_with_options (HashTable<string, Variant> options,
Cancellable? cancellable) throws Error, IOError;
public abstract async void disable_spawn_gating (Cancellable? cancellable) throws Error, IOError;
@@ -739,23 +744,29 @@ namespace Frida {
protected string make_agent_parameters (uint pid, string remote_address, HashTable<string, Variant> options) throws Error {
var parameters = new StringBuilder (remote_address);
string[] features = { "exceptor", "exit-monitor", "thread-suspend-monitor" };
bool is_system_session = pid == 0;
foreach (string feature in features) {
bool enabled = true;
if (pid == 0) {
parameters.append ("|exit-monitor:off|thread-suspend-monitor:off");
return parameters.str;
}
if (is_system_session) {
enabled = feature == "exceptor";
} else {
Variant? val = options[feature];
if (val != null) {
if (!val.is_of_type (VariantType.STRING) || val.get_string () != "off")
throw new Error.INVALID_ARGUMENT ("The '%s' option is invalid", feature);
enabled = false;
}
}
Variant? exceptor = options["exceptor"];
if (exceptor != null) {
if (!exceptor.is_of_type (VariantType.STRING))
throw new Error.INVALID_ARGUMENT ("The 'exceptor' option is invalid");
unowned string mode = exceptor.get_string ();
if (mode != "off" && mode != "handler-only")
throw new Error.INVALID_ARGUMENT ("The 'exceptor' option is invalid");
parameters
.append ("|exceptor:")
.append (mode);
}
if (!enabled) {
string[] toggles = { "unwind-broker", "exit-monitor", "thread-suspend-monitor" };
foreach (string feature in toggles) {
Variant? val = options[feature];
if (val != null) {
if (!val.is_of_type (VariantType.STRING) || val.get_string () != "off")
throw new Error.INVALID_ARGUMENT ("The '%s' option is invalid", feature);
parameters
.append_c ('|')
.append (feature)
@@ -763,6 +774,21 @@ namespace Frida {
}
}
Variant? offsets = options["linker-notifier-offsets"];
if (offsets != null) {
if (!offsets.is_of_type (new VariantType ("au")))
throw new Error.INVALID_ARGUMENT ("The 'linker-notifier-offsets' option is invalid");
uint n = (uint) offsets.n_children ();
if (n != 0) {
parameters.append ("|linker-notifier-offsets:");
for (uint i = 0; i != n; i++) {
if (i != 0)
parameters.append_c (',');
parameters.append (offsets.get_child_value (i).get_uint32 ().to_string ());
}
}
}
return parameters.str;
}
@@ -845,6 +871,8 @@ namespace Frida {
private void on_agent_session_provider_closed (AgentSessionId id) {
var closed_after_opening = agent_sessions.unset (id);
GLib.info ("detach-trace: BaseDBusHostSession provider closed id=%s closed_after_opening=%s",
id.handle, closed_after_opening.to_string ());
if (!closed_after_opening)
return;
var reason = SessionDetachReason.APPLICATION_REQUESTED;
@@ -947,7 +975,10 @@ namespace Frida {
var crash_info = (crash != null) ? crash : CrashInfo.empty ();
foreach (var id in entry.sessions) {
if (agent_sessions.unset (id))
bool still_registered = agent_sessions.unset (id);
GLib.info ("detach-trace: BaseDBusHostSession teardown id=%s reason=%d still_registered=%s",
id.handle, reason, still_registered.to_string ());
if (still_registered)
agent_session_detached (id, reason, crash_info);
}
+120 -98
View File
@@ -1,105 +1,127 @@
public sealed class Frida.ActivitySampler : Object {
public uint pid {
get;
construct;
}
private BpfRingbufReader? events_reader;
private Source? events_source;
private Gee.Collection<FileDescriptor> perf_event_fds = new Gee.ArrayList<FileDescriptor> ();
private Gee.Collection<BpfLink> links = new Gee.ArrayList<BpfLink> ();
public ActivitySampler (uint pid) {
Object (pid: pid);
}
protected override void dispose () {
stop ();
base.dispose ();
}
public void start () throws Error {
var obj = BpfObject.open ("activity-sampler.elf", Frida.Data.HelperBackend.get_activity_sampler_elf_blob ().data);
var target_tgid = obj.maps.get_by_name ("target_tgid");
var events = obj.maps.get_by_name ("events");
obj.prepare ();
target_tgid.update_u32_u32 (0, pid);
events_reader = new BpfRingbufReader (events);
obj.load ();
BpfProgram program = obj.programs.get_by_name ("on_perf_event");
uint ncpus = get_num_processors ();
for (uint cpu = 0; cpu != ncpus; cpu++) {
var pea = PerfEventAttr ();
pea.event_type = SOFTWARE;
pea.size = (uint32) sizeof (PerfEventAttr);
pea.config = PERF_EVENT_COUNT_SW_CPU_CLOCK;
pea.sample_period = 1;
var pefd = PerfEvent.open (&pea, -1, (int) cpu, -1, 0);
perf_event_fds.add (pefd);
var link = program.attach_perf_event (pefd);
links.add (link);
namespace Frida {
public sealed class ActivitySampler : Object {
public uint pid {
get;
construct;
}
var ch = new IOChannel.unix_new (events.fd);
var src = new IOSource (ch, IN);
var state = new WatchState (this, events_reader);
src.set_callback (state.on_ready);
src.attach (MainContext.get_thread_default ());
events_source = src;
}
public void stop () {
events_source?.destroy ();
events_source = null;
links.clear ();
events_reader = null;
}
private void handle_sample (SampleEvent * e) {
printerr ("[ActivitySampler %p] tgid=%u tid=%u time=%" + uint64.FORMAT + " stack_err=%d depth=%u\n",
this, e->tgid, e->tid, e->time_ns, e->stack_err, e->depth);
for (uint32 i = 0; i != e->depth; i++)
printerr ("\t0x%lx\n", (ulong) e->ips[i]);
}
private struct SampleEvent {
public uint64 time_ns;
public uint32 tgid;
public uint32 tid;
public int32 stack_err;
public uint32 depth;
public uint64 ips[16];
}
private class WatchState : Object {
public unowned ActivitySampler sampler;
public BpfRingbufReader reader;
public WatchState (ActivitySampler sampler, BpfRingbufReader reader) {
this.sampler = sampler;
this.reader = reader;
public Gee.List<SampledStack> stacks {
get {
return collected_stacks;
}
}
public bool on_ready (IOChannel ch, IOCondition cond) {
reader.drain (payload => {
assert (payload.length == sizeof (SampleEvent));
sampler.handle_sample ((SampleEvent *) payload);
return CONTINUE;
});
return Source.CONTINUE;
private BpfRingbufReader? events_reader;
private Source? events_source;
private Gee.Collection<FileDescriptor> perf_event_fds = new Gee.ArrayList<FileDescriptor> ();
private Gee.Collection<BpfLink> links = new Gee.ArrayList<BpfLink> ();
private Gee.List<SampledStack> collected_stacks = new Gee.ArrayList<SampledStack> ();
private const uint64 SAMPLE_PERIOD_NS = 1000000;
public ActivitySampler (uint pid) {
Object (pid: pid);
}
protected override void dispose () {
stop ();
base.dispose ();
}
public void start () throws Error {
var obj = BpfObject.open ("activity-sampler.elf", Frida.Data.HelperBackend.get_activity_sampler_elf_blob ().data);
var target_tgid = obj.maps.get_by_name ("target_tgid");
var events = obj.maps.get_by_name ("events");
obj.prepare ();
target_tgid.update_u32_u32 (0, pid);
events_reader = new BpfRingbufReader (events);
obj.load ();
BpfProgram program = obj.programs.get_by_name ("on_perf_event");
uint ncpus = get_num_processors ();
for (uint cpu = 0; cpu != ncpus; cpu++) {
var pea = PerfEventAttr ();
pea.event_type = SOFTWARE;
pea.size = (uint32) sizeof (PerfEventAttr);
pea.config = PERF_EVENT_COUNT_SW_CPU_CLOCK;
pea.sample_period = SAMPLE_PERIOD_NS;
var pefd = PerfEvent.open (&pea, -1, (int) cpu, -1, 0);
perf_event_fds.add (pefd);
var link = program.attach_perf_event (pefd);
links.add (link);
}
var ch = new IOChannel.unix_new (events.fd);
var src = new IOSource (ch, IN);
var state = new WatchState (this, events_reader);
src.set_callback (state.on_ready);
src.attach (MainContext.get_thread_default ());
events_source = src;
}
public void stop () {
events_source?.destroy ();
events_source = null;
links.clear ();
events_reader = null;
}
private void handle_sample (SampleEvent * e) {
if (e->depth == 0)
return;
var frames = new uint64[e->depth];
Memory.copy (frames, e->ips, e->depth * sizeof (uint64));
// XXX: Should we trim these?
e->tgid; e->tid; e->time_ns; e->stack_err;
collected_stacks.add (new SampledStack ((owned) frames));
}
private struct SampleEvent {
public uint64 time_ns;
public uint32 tgid;
public uint32 tid;
public int32 stack_err;
public uint32 depth;
public uint64 ips[16];
}
private class WatchState : Object {
public unowned ActivitySampler sampler;
public BpfRingbufReader reader;
public WatchState (ActivitySampler sampler, BpfRingbufReader reader) {
this.sampler = sampler;
this.reader = reader;
}
public bool on_ready (IOChannel ch, IOCondition cond) {
reader.drain (payload => {
assert (payload.length == sizeof (SampleEvent));
sampler.handle_sample ((SampleEvent *) payload);
return CONTINUE;
});
return Source.CONTINUE;
}
}
}
public sealed class SampledStack {
public uint64[] frames;
public SampledStack (owned uint64[] frames) {
this.frames = (owned) frames;
}
}
}
+27 -10
View File
@@ -30,7 +30,8 @@ namespace Frida {
p.close ();
}
public async uint spawn (string path, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError {
public async uint spawn (string path, HostSpawnOptions options, UnixInputStream? stdin_stream,
UnixOutputStream? stdout_stream, UnixOutputStream? stderr_stream, Cancellable? cancellable) throws Error, IOError {
if (!FileUtils.test (path, EXISTS))
throw new Error.EXECUTABLE_NOT_FOUND ("Unable to find executable at '%s'", path);
@@ -56,7 +57,11 @@ namespace Frida {
if (pid == 0) {
Posix.setsid ();
if (pipes != null) {
if (stdin_stream != null) {
Posix.dup2 (stdin_stream.get_fd (), 0);
Posix.dup2 (stdout_stream.get_fd (), 1);
Posix.dup2 (stderr_stream.get_fd (), 2);
} else if (pipes != null) {
Posix.dup2 (in_fd.handle, 0);
Posix.dup2 (out_fd.handle, 1);
Posix.dup2 (err_fd.handle, 2);
@@ -318,13 +323,21 @@ namespace Frida {
PausedSyscallSession? pss = backend.paused_syscalls[pid];
if (pss != null)
yield pss.interrupt (cancellable);
var session = yield InjectSession.open (pid, cancellable);
RemoteAgent agent = yield session.inject (spec, cancellable);
if (session.was_group_stopped)
backend.suspended_by_inject[pid] = session;
else
session.close ();
return agent;
try {
var session = yield InjectSession.open (pid, cancellable);
RemoteAgent agent = yield session.inject (spec, cancellable);
if (session.was_group_stopped)
backend.suspended_by_inject[pid] = session;
else
session.close ();
return agent;
} catch (Error e) {
if (!(e is Error.PERMISSION_DENIED) || !ProcMemInjectSession.is_available ())
throw e;
var session = yield ProcMemInjectSession.open (pid, cancellable);
return yield session.inject (spec, cancellable);
}
}
}
@@ -1897,7 +1910,11 @@ namespace Frida {
case INTERRUPT: {
bool maybe_already_attached = res == -1 && errsv == Posix.EPERM;
if (maybe_already_attached) {
get_regs (&saved_regs);
try {
get_regs (&saved_regs);
} catch (Error e) {
throw new Error.PERMISSION_DENIED ("Process is traced by another process");
}
attach_state = ALREADY_ATTACHED;
} else {
+13 -4
View File
@@ -54,9 +54,10 @@ namespace Frida {
return _resource_store;
}
public async uint spawn (string path, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError {
public async uint spawn (string path, HostSpawnOptions options, UnixInputStream? stdin_stream,
UnixOutputStream? stdout_stream, UnixOutputStream? stderr_stream, Cancellable? cancellable) throws Error, IOError {
var helper = yield obtain_for_path (path, cancellable);
return yield helper.spawn (path, options, cancellable);
return yield helper.spawn (path, options, stdin_stream, stdout_stream, stderr_stream, cancellable);
}
public async void prepare_exec_transition (uint pid, Cancellable? cancellable) throws Error, IOError {
@@ -379,7 +380,12 @@ namespace Frida {
} catch (Error e) {
string[] argv = { helper_file.path, socket_address };
GLib.SpawnFlags flags = GLib.SpawnFlags.LEAVE_DESCRIPTORS_OPEN | /* GLib.SpawnFlags.CLOEXEC_PIPES */ 256;
GLib.SpawnFlags flags =
GLib.SpawnFlags.LEAVE_DESCRIPTORS_OPEN |
GLib.SpawnFlags.STDOUT_TO_DEV_NULL |
GLib.SpawnFlags.STDERR_TO_DEV_NULL |
/* GLib.SpawnFlags.STDIN_FROM_DEV_NULL */ 2048 |
/* GLib.SpawnFlags.CLOEXEC_PIPES */ 256;
GLib.Process.spawn_async (null, argv, envp, flags, null, out pending_pid);
}
@@ -498,8 +504,11 @@ namespace Frida {
proxy.uninjected.disconnect (on_uninjected);
}
public async uint spawn (string path, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError {
public async uint spawn (string path, HostSpawnOptions options, UnixInputStream? stdin_stream,
UnixOutputStream? stdout_stream, UnixOutputStream? stderr_stream, Cancellable? cancellable) throws Error, IOError {
try {
if (stdin_stream != null)
return yield proxy.spawn_with_stdio (path, options, stdin_stream, stdout_stream, stderr_stream, cancellable);
return yield proxy.spawn (path, options, cancellable);
} catch (GLib.Error e) {
throw_dbus_error (e);
+6 -1
View File
@@ -121,7 +121,12 @@ namespace Frida {
}
public async uint spawn (string path, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError {
return yield backend.spawn (path, options, cancellable);
return yield backend.spawn (path, options, null, null, null, cancellable);
}
public async uint spawn_with_stdio (string path, HostSpawnOptions options, UnixInputStream stdin_stream,
UnixOutputStream stdout_stream, UnixOutputStream stderr_stream, Cancellable? cancellable) throws Error, IOError {
return yield backend.spawn (path, options, stdin_stream, stdout_stream, stderr_stream, cancellable);
}
public async void prepare_exec_transition (uint pid, Cancellable? cancellable) throws Error, IOError {
+4 -1
View File
@@ -5,7 +5,8 @@ namespace Frida {
public abstract async void close (Cancellable? cancellable) throws IOError;
public abstract async uint spawn (string path, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError;
public abstract async uint spawn (string path, HostSpawnOptions options, UnixInputStream? stdin_stream,
UnixOutputStream? stdout_stream, UnixOutputStream? stderr_stream, Cancellable? cancellable) throws Error, IOError;
public abstract async void prepare_exec_transition (uint pid, Cancellable? cancellable) throws Error, IOError;
public abstract async void await_exec_transition (uint pid, Cancellable? cancellable) throws Error, IOError;
public abstract async void cancel_exec_transition (uint pid, Cancellable? cancellable) throws Error, IOError;
@@ -50,6 +51,8 @@ namespace Frida {
public abstract async void stop (Cancellable? cancellable) throws GLib.Error;
public abstract async uint spawn (string path, HostSpawnOptions options, Cancellable? cancellable) throws GLib.Error;
public abstract async uint spawn_with_stdio (string path, HostSpawnOptions options, UnixInputStream stdin_stream,
UnixOutputStream stdout_stream, UnixOutputStream stderr_stream, Cancellable? cancellable) throws GLib.Error;
public abstract async void prepare_exec_transition (uint pid, Cancellable? cancellable) throws GLib.Error;
public abstract async void await_exec_transition (uint pid, Cancellable? cancellable) throws GLib.Error;
public abstract async void cancel_exec_transition (uint pid, Cancellable? cancellable) throws GLib.Error;
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+15 -1
View File
@@ -53,6 +53,7 @@ static void frida_enable_close_on_exec (int fd, const FridaLibcApi * libc);
static size_t frida_strlen (const char * str);
static pid_t frida_getpid (void);
static pid_t frida_gettid (void);
__attribute__ ((section (".text.entrypoint")))
@@ -111,7 +112,14 @@ frida_main (void * user_data)
if (agent_codefd == -1)
goto beach;
libc->sprintf (agent_path, "/proc/self/fd/%d", agent_codefd);
/*
* Spell out our PID instead of using /proc/self so the path resolves the same
* from outside the process. An attached debugger handles the dynamic linker's
* library-load event by reading the new object from this path; with /proc/self
* that read hits the debugger's own fd table and fails, wedging the thread mid
* dlopen (), so the agent never finishes loading.
*/
libc->sprintf (agent_path, "/proc/%d/fd/%d", frida_getpid (), agent_codefd);
ctx->agent_handle = libc->dlopen (agent_path, libc->dlopen_flags, pretend_caller_addr);
if (ctx->agent_handle == NULL)
@@ -400,6 +408,12 @@ frida_strlen (const char * str)
return n;
}
static pid_t
frida_getpid (void)
{
return frida_syscall_0 (SYS_getpid);
}
static pid_t
frida_gettid (void)
{
+16 -16
View File
@@ -16,7 +16,7 @@ struct _ZymbioteContext
size_t payload_size;
size_t payload_original_protection;
char * package_name;
char * process_name;
int (* original_setcontext) (uid_t uid, bool is_system_server, const char * seinfo, const char * name);
void (* original_set_argv0) (JNIEnv * env, jobject clazz, jstring name);
@@ -43,7 +43,7 @@ ZymbioteContext zymbiote =
int frida_zymbiote_replacement_setargv0 (JNIEnv * env, jobject clazz, jstring name);
int frida_zymbiote_replacement_setcontext (uid_t uid, bool is_system_server, const char * seinfo, const char * name);
static void frida_wait_for_permission_to_resume (const char * package_name, bool * revert_now);
static void frida_wait_for_permission_to_resume (const char * process_name, bool * revert_now);
static int frida_stop_and_return_from_setargv0 (JNIEnv * env, jobject clazz, jstring name);
@@ -65,10 +65,10 @@ frida_zymbiote_replacement_setcontext (uid_t uid, bool is_system_server, const c
if (res == -1)
return -1;
if (zymbiote.package_name == NULL)
if (zymbiote.process_name == NULL)
{
zymbiote.mprotect (zymbiote.payload_base, zymbiote.payload_size, PROT_READ | PROT_WRITE | PROT_EXEC);
zymbiote.package_name = zymbiote.strdup (name);
zymbiote.process_name = zymbiote.strdup (name);
}
return res;
@@ -84,17 +84,17 @@ frida_zymbiote_replacement_setargv0 (JNIEnv * env, jobject clazz, jstring name)
zymbiote.original_set_argv0 (env, clazz, name);
if (zymbiote.package_name != NULL)
name_utf8 = zymbiote.package_name;
if (zymbiote.process_name != NULL)
name_utf8 = zymbiote.process_name;
else
name_utf8 = (*env)->GetStringUTFChars (env, name, NULL);
frida_wait_for_permission_to_resume (name_utf8, &revert_now);
if (zymbiote.package_name != NULL)
if (zymbiote.process_name != NULL)
{
zymbiote.free (zymbiote.package_name);
zymbiote.package_name = NULL;
zymbiote.free (zymbiote.process_name);
zymbiote.process_name = NULL;
zymbiote.mprotect (zymbiote.payload_base, zymbiote.payload_size, zymbiote.payload_original_protection);
}
else
@@ -112,7 +112,7 @@ frida_zymbiote_replacement_setargv0 (JNIEnv * env, jobject clazz, jstring name)
}
static void
frida_wait_for_permission_to_resume (const char * package_name, bool * revert_now)
frida_wait_for_permission_to_resume (const char * process_name, bool * revert_now)
{
int fd;
struct sockaddr_un addr;
@@ -151,22 +151,22 @@ frida_wait_for_permission_to_resume (const char * package_name, bool * revert_no
{
uint32_t pid;
uint32_t ppid;
uint32_t package_name_len;
uint32_t process_name_len;
} header;
struct iovec iov[2];
header.pid = zymbiote.getpid ();
header.ppid = zymbiote.getppid ();
header.package_name_len = 0;
while (package_name[header.package_name_len] != '\0')
header.package_name_len++;
header.process_name_len = 0;
while (process_name[header.process_name_len] != '\0')
header.process_name_len++;
iov[0].iov_base = &header;
iov[0].iov_len = sizeof (header);
iov[1].iov_base = (void *) package_name;
iov[1].iov_len = header.package_name_len;
iov[1].iov_base = (void *) process_name;
iov[1].iov_len = header.process_name_len;
if (!frida_sendmsg_all (fd, iov, 2, MSG_NOSIGNAL))
goto beach;
+85 -30
View File
@@ -77,6 +77,7 @@ namespace Frida {
#if ANDROID
robo_launcher = new RoboLauncher (this, io_cancellable);
robo_launcher.gating_cancelled.connect (on_spawn_gating_cancelled);
robo_launcher.spawn_added.connect (on_spawn_added);
robo_launcher.spawn_removed.connect (on_spawn_removed);
@@ -97,6 +98,7 @@ namespace Frida {
public override async void close (Cancellable? cancellable) throws IOError {
#if ANDROID
yield robo_launcher.close (cancellable);
robo_launcher.gating_cancelled.disconnect (on_spawn_gating_cancelled);
robo_launcher.spawn_added.disconnect (on_spawn_added);
robo_launcher.spawn_removed.disconnect (on_spawn_removed);
@@ -110,6 +112,7 @@ namespace Frida {
#endif
if (spawn_gater != null) {
spawn_gater.gating_cancelled.disconnect (on_spawn_gating_cancelled);
spawn_gater.spawn_added.disconnect (on_spawn_added);
spawn_gater.spawn_removed.disconnect (on_spawn_removed);
spawn_gater.stop ();
@@ -276,25 +279,28 @@ namespace Frida {
#endif
}
public override async void enable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
public override async void enable_spawn_gating_with_options (HashTable<string, Variant> options,
Cancellable? cancellable) throws Error, IOError {
var helper_process = helper as LinuxHelperProcess;
if (helper_process != null)
yield helper_process.preload (cancellable);
var gater = ensure_spawn_gater ();
if (gater.state == STOPPED) {
#if ANDROID
try {
// RoboLauncher gates app launches; only add the system-wide eBPF gater for PROCESSES.
// Let a start failure propagate — the caller opted in, so don't silently downgrade.
if (SpawnGatingOptions._deserialize (options).scope == PROCESSES) {
var gater = ensure_spawn_gater ();
if (gater.state == STOPPED)
gater.start ();
} catch (Error e) {
}
#else
gater.start ();
#endif
}
#if ANDROID
yield robo_launcher.enable_spawn_gating (cancellable);
#else
// No portable way to tell a GUI app from any other execve here, so both scopes map to
// the same system-wide gater.
var gater = ensure_spawn_gater ();
if (gater.state == STOPPED)
gater.start ();
#endif
}
@@ -304,11 +310,14 @@ namespace Frida {
#if ANDROID
yield robo_launcher.disable_spawn_gating (cancellable);
#endif
spawn_gating_disabled (APPLICATION_REQUESTED);
}
private SpawnGater ensure_spawn_gater () {
if (spawn_gater == null) {
spawn_gater = new SpawnGater (helper);
spawn_gater.gating_cancelled.connect (on_spawn_gating_cancelled);
spawn_gater.spawn_added.connect (on_spawn_added);
spawn_gater.spawn_removed.connect (on_spawn_removed);
}
@@ -338,7 +347,16 @@ namespace Frida {
return yield robo_launcher.spawn (program, options, cancellable);
#endif
return yield helper.spawn (program, options, cancellable);
UnixInputStream? stdin_stream = null;
UnixOutputStream? stdout_stream = null;
UnixOutputStream? stderr_stream = null;
if (options.stdio == INHERIT) {
stdin_stream = new UnixInputStream (0, false);
stdout_stream = new UnixOutputStream (1, false);
stderr_stream = new UnixOutputStream (2, false);
}
return yield helper.spawn (program, options, stdin_stream, stdout_stream, stderr_stream, cancellable);
}
protected override async void prepare_exec_transition (uint pid, Cancellable? cancellable) throws Error, IOError {
@@ -506,6 +524,10 @@ namespace Frida {
}
#endif
private void on_spawn_gating_cancelled () {
spawn_gating_disabled (WATCHDOG_TIMEOUT);
}
private void on_spawn_added (HostSpawnInfo info) {
spawn_added (info);
}
@@ -1344,6 +1366,7 @@ namespace Frida {
}
private sealed class RoboLauncher : Object {
public signal void gating_cancelled ();
public signal void spawn_added (HostSpawnInfo info);
public signal void spawn_removed (HostSpawnInfo info);
@@ -1367,6 +1390,7 @@ namespace Frida {
private bool spawn_gating_enabled = false;
private Gee.HashMap<string, Promise<uint>> spawn_requests = new Gee.HashMap<string, Promise<uint>> ();
private Gee.HashMap<uint, HostSpawnInfo?> pending_spawn = new Gee.HashMap<uint, HostSpawnInfo?> ();
private SpawnGatingWatchdog watchdog = new SpawnGatingWatchdog ();
private delegate void CompletionNotify (GLib.Error? error);
@@ -1379,11 +1403,22 @@ namespace Frida {
);
}
construct {
watchdog.expired.connect (on_watchdog_expired);
}
private void on_watchdog_expired () {
disable_spawn_gating.begin (io_cancellable);
gating_cancelled ();
}
public async void preload (Cancellable? cancellable) throws Error, IOError {
yield ensure_loaded (cancellable);
}
public async void close (Cancellable? cancellable) throws IOError {
watchdog.clear ();
if (ensure_request != null) {
try {
yield ensure_loaded (cancellable);
@@ -1422,6 +1457,8 @@ namespace Frida {
public async void disable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
spawn_gating_enabled = false;
watchdog.clear ();
var pending = pending_spawn.values.to_array ();
pending_spawn.clear ();
foreach (var spawn in pending) {
@@ -1508,8 +1545,10 @@ namespace Frida {
connection.resume.begin (io_cancellable);
HostSpawnInfo? info;
if (pending_spawn.unset (pid, out info))
if (pending_spawn.unset (pid, out info)) {
watchdog.cancel (pid);
spawn_removed (info);
}
return true;
}
@@ -1598,10 +1637,21 @@ namespace Frida {
on_complete (null);
} catch (GLib.Error e) {
if (is_transient_usap_patch_failure (name, e)) {
on_complete (null);
return;
}
on_complete (e);
}
}
private static bool is_transient_usap_patch_failure (string process_name, GLib.Error error) {
return process_name.has_prefix ("usap") &&
error is Error.NOT_SUPPORTED &&
error.message == "Unable to locate android.os.Process.setArgV0() slot; please file a bug";
}
private async void inject_zymbiote (uint pid, Cancellable? cancellable) throws Error, IOError {
var prep = yield prepare_zymbiote_injection (pid, cancellable);
@@ -1709,15 +1759,14 @@ namespace Frida {
foreach (var m in maps) {
unowned string path = m.path;
if (path.has_suffix ("/libstagefright.so") && m.readable && m.executable) {
if (payload_base == 0) {
payload_base = m.end - Gum.query_page_size ();
payload_original_protection = Posix.PROT_READ | Posix.PROT_EXEC;
if (m.writable)
payload_original_protection |= Posix.PROT_WRITE;
payload_path = path;
payload_file_offset = m.file_offset;
}
if (payload_base == 0 && m.executable &&
(path.has_suffix ("/libstagefright.so") || path.has_suffix ("/libmedia.so"))) {
payload_base = m.end - Gum.query_page_size ();
payload_original_protection = Posix.PROT_READ | Posix.PROT_EXEC;
if (m.writable)
payload_original_protection |= Posix.PROT_WRITE;
payload_path = path;
payload_file_offset = m.file_offset;
} else if (path.has_suffix ("/libc.so")) {
if (libc_path == null)
libc_path = path;
@@ -2053,7 +2102,7 @@ namespace Frida {
try {
var hello = yield connection.read_hello (io_cancellable);
if (hello.package_name == CHROME_ZYGOTE_PACKAGE_NAME) {
if (hello.process_name == CHROME_ZYGOTE_PACKAGE_NAME) {
var patches = zymbiote_patches[hello.ppid];
if (patches != null)
zymbiote_patches[hello.pid] = patches;
@@ -2068,13 +2117,19 @@ namespace Frida {
bool needs_resume = false;
string package_name = hello.process_name;
int colon = package_name.index_of_char (':');
if (colon != -1)
package_name = package_name[:colon];
Promise<uint> spawn_request;
if (spawn_requests.unset (hello.package_name, out spawn_request)) {
if (spawn_requests.unset (package_name, out spawn_request)) {
spawn_request.resolve (hello.pid);
needs_resume = true;
} else if (spawn_gating_enabled) {
var spawn_info = HostSpawnInfo (hello.pid, hello.package_name);
var spawn_info = HostSpawnInfo (hello.pid, hello.process_name);
pending_spawn[hello.pid] = spawn_info;
watchdog.arm (hello.pid);
spawn_added (spawn_info);
needs_resume = true;
}
@@ -2139,10 +2194,10 @@ namespace Frida {
try {
yield prepare_to_read (header_size, cancellable);
uint32 package_name_len = 0;
input.peek ((uint8[]) &package_name_len, 8);
uint32 process_name_len = 0;
input.peek ((uint8[]) &process_name_len, 8);
size_t message_size = header_size + package_name_len;
size_t message_size = header_size + process_name_len;
yield prepare_to_read (message_size, cancellable);
@@ -2150,12 +2205,12 @@ namespace Frida {
uint32 pid = r.read_uint32 ();
uint32 ppid = r.read_uint32 ();
r.skip (4);
string package_name = r.read_fixed_string (package_name_len);
string process_name = r.read_fixed_string (process_name_len);
hello = new Hello () {
pid = pid,
ppid = ppid,
package_name = package_name,
process_name = process_name,
};
return hello;
@@ -2169,7 +2224,7 @@ namespace Frida {
public class Hello {
public uint pid;
public uint ppid;
public string package_name;
public string process_name;
}
public async void resume (Cancellable? cancellable) throws Error, IOError {
+1 -1
View File
@@ -417,7 +417,7 @@ namespace Frida {
return;
}
if (!/^([0-9a-f]+)-([0-9a-f]+) (\S{4}) ([0-9a-f]+) ([0-9a-f]{2,}:[0-9a-f]{2,}) (\d+) +([^\n]+)$/m.match (
if (!/^([0-9a-f]+)-([0-9a-f]+) (\S{4}) ([0-9a-f]+) ([0-9a-f]{2,}:[0-9a-f]{2,}) (\d+) *([^\n]*)$/m.match (
contents, 0, out info)) {
assert_not_reached ();
}
File diff suppressed because it is too large. Load diff
+47 -1
View File
@@ -1,5 +1,6 @@
namespace Frida {
public sealed class SpawnGater : Object {
public signal void gating_cancelled ();
public signal void spawn_added (HostSpawnInfo info);
public signal void spawn_removed (HostSpawnInfo info);
@@ -22,6 +23,7 @@ namespace Frida {
private State _state = STOPPED;
private Gee.Map<uint, HostSpawnInfo?> pending_spawn = new Gee.HashMap<uint, HostSpawnInfo?> ();
private SpawnGatingWatchdog watchdog = new SpawnGatingWatchdog ();
private BpfRingbufReader? events_reader;
private IOChannel? events_channel;
@@ -32,16 +34,30 @@ namespace Frida {
private const size_t RINGBUF_SIZE = 1U << 22;
private const size_t MAX_FILENAME = 256;
// bpf_send_signal(SIGSTOP) races our SIGCONT: one that lands first is dropped, so re-send
// until the task is observed running.
private const uint RESUME_MAX_ATTEMPTS = 20;
private const uint RESUME_RETRY_MSEC = 25;
public SpawnGater (LinuxHelper helper) {
Object (helper: helper);
}
construct {
watchdog.expired.connect (on_watchdog_expired);
}
protected override void dispose () {
stop ();
base.dispose ();
}
private void on_watchdog_expired () {
stop ();
gating_cancelled ();
}
public void start () throws Error {
assert (state == STOPPED);
@@ -86,6 +102,7 @@ namespace Frida {
perform_resume.begin (spawn.pid);
}
pending_spawn.clear ();
watchdog.clear ();
events_channel = null;
events_reader = null;
@@ -105,6 +122,7 @@ namespace Frida {
HostSpawnInfo? spawn;
if (!pending_spawn.unset (pid, out spawn))
return false;
watchdog.cancel (pid);
spawn_removed (spawn);
perform_resume.begin (pid);
return true;
@@ -115,10 +133,37 @@ namespace Frida {
yield helper.resume (pid, null);
} catch (GLib.Error e) {
if (e is Error.INVALID_ARGUMENT)
Posix.kill ((Posix.pid_t) pid, Posix.Signal.CONT);
yield resume_external (pid);
}
}
private async void resume_external (uint pid) {
for (uint attempt = 0; attempt != RESUME_MAX_ATTEMPTS; attempt++) {
if (Posix.kill ((Posix.pid_t) pid, Posix.Signal.CONT) != 0)
return;
if (!process_is_stopped (pid))
return;
var source = new TimeoutSource (RESUME_RETRY_MSEC);
source.set_callback (resume_external.callback);
source.attach (MainContext.get_thread_default ());
yield;
}
}
private static bool process_is_stopped (uint pid) {
string contents;
try {
FileUtils.get_contents ("/proc/%u/stat".printf (pid), out contents);
} catch (FileError e) {
return false;
}
// State is the char two past the final ')' (comm can itself contain ')').
char state = contents[contents.last_index_of_char (')') + 2];
return state == 'T' || state == 't';
}
private void process_pending_events () {
events_reader.drain (payload => {
assert (payload.length == sizeof (ExecveEvent));
@@ -130,6 +175,7 @@ namespace Frida {
private void handle_event (ExecveEvent * e) {
var info = HostSpawnInfo (e->pid, (string) e->command);
pending_spawn[e->pid] = info;
watchdog.arm (e->pid);
spawn_added (info);
}
+1 -1
View File
@@ -29,7 +29,7 @@ public sealed class Frida.SymbolResolver : Object {
foreach (var addr in addresses) {
ProcMapsSnapshot.Mapping? m = snap.find_mapping_at_gen (addr, gen);
if (m == null) {
if (m == null || m.path == "") {
on_symbol (addr, uint32.MAX, 0);
continue;
}
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff. Show more