Compare commits

...
Author SHA1 Message Date
Ole André Vadla Ravnås a6b853d490 Add PHARO as a script runtime
ScriptInstance becomes the abstract peer of JSScriptInstance and
PharoScriptInstance, so Pharo scripts ride the same engine without
going through the GumJS interfaces.
2026-08-14 20:01:56 +02:00
Ole André Vadla Ravnås c3ba7814d3 Link the Pharo bindings when Gum has them
Gum answers frida-gumpharo-1.0 only where the bindings were built,
so ask for it and go without when it is absent.
2026-08-14 18:54:27 +02:00
Ole André Vadla Ravnås f1998c37e1 barebone: Enumerate the guest's processes on Win9x
Win32 threads name the process they belong to in VWIN32's
per-thread block, so the list is the deduplicated set of
those, and the image path is where the command line starts.
The kernel's own processes have neither and stay unnamed.
2026-08-14 17:21:42 +02:00
Ole André Vadla Ravnås 95d8293b39 barebone: Start the agent when the kernel is ready
VMM will not build a thread from the borrowed context the
host enters us on, and only says so by never returning, so
a booted guest left the agent with no worker at all and
every request timed out. Deferring belongs to how we are
entered rather than to making a thread, so _start asks to
run when the kernel is ready; where entry is already safe
that is a direct call.
2026-08-14 17:21:42 +02:00
Ole André Vadla Ravnås ad226053f0 barebone: Resolve VWIN32 and thread services
Name VWIN32's ordinals so its services resolve like VMM's,
and add slots and thunks for walking VMs and threads and
for switching memory context. Together these are what
process enumeration needs from the guest.
2026-08-14 15:13:44 +02:00
Ole André Vadla Ravnås dcbdb0d092 barebone: Run scripts in the Windows 95 kernel
Resolve VMM services by walking the DDB chain and its
service table, enter the agent from a thread already
calling Get_System_Time, and carry the hostlink over
virtio-serial on PCI. Wake on the hostlink IRQ, and hand
ring-0 faults to Exceptor before the kernel sees them.
2026-08-14 12:54:48 +02:00
Ole André Vadla Ravnås 4f125feacb gdb: Write memory with binary packets
Halves what goes on the wire, with a fallback to the hex
form for stubs that decline X.
2026-08-14 12:54:48 +02:00
Ole André Vadla Ravnås e9cd6e5120 barebone: Select kernel flavor from config
Flavor was inferred from the presence of an image config, so any
target supplying symbols got XNU's bring-up dance and its demand
for a kernel_base. Win9x needs neither, yet does need symbols to
fill the agent's service slots.

Add an explicit "kernel" field, defaulting to the old inference.
2026-08-14 12:54:28 +02:00
Ole André Vadla Ravnås deeb15aa24 tests: Cover mappings that outgrow a leaf table
Two adjacent tables, the first all but full, so a run of three has to
carry on into the second. Checks the entries land in both and that
releasing puts each back, and that reprotecting across the boundary
still touches only what was asked for.

The guest cases prove this at agent size, but only when pointed at one.
2026-08-14 12:54:28 +02:00
Ole André Vadla Ravnås 4d1ad3fa81 barebone: Reprotect a leaf table at a time
Every page was costing two walks from the root and a write of its own,
so protecting an agent-sized range spent fifty seconds asking the same
questions. A range covers whole tables, so walk to each one once and
rewrite the entries it holds in a single go.

Five megabytes now takes a second and a half.
2026-08-14 12:54:28 +02:00
Ole André Vadla Ravnås cc96b2d886 barebone: Map runs that outgrow a leaf table
A run has to be contiguous in virtual address space, not in the tables
describing it, so gather it a segment at a time and put each segment
back where it came from on release. A gap anywhere above the leaves
still starts the search over, the addresses either side of it not being
adjacent.

Five megabytes of agent needs twelve hundred pages, where a table holds
a thousand and twenty-four, or half that under PAE.
2026-08-14 12:54:28 +02:00
Ole André Vadla Ravnås b21fc1247f tests: Load a whole agent into an x86 guest
Everything the loader had been shown so far was a page or two, which
says nothing about an image of thousands. Point it at a real agent and
it turns out one cannot be placed at all: the search for somewhere to
put it never leaves a single leaf table, and a table only reaches four
megabytes, or two under PAE.

Skipped unless pointed at a blob, since none is small enough to keep.
2026-08-14 12:54:28 +02:00
Ole André Vadla Ravnås e6bcc3c8af barebone: Add a Win9x flavor of the injected agent
What the host injects is no longer XNU's alone, so the feature naming a
flavor is what asks for one at all, and the linker script laying out its
slots follows the flavor rather than the word size.

VMM answers the kernel facade through slots the host fills in, as it
does for XNU. Nothing here is specific to 95: the same services carry
98 and Me.

Its Gum backend is the short one: these processors have no execute
permission to withhold and no write protection worth the name, so code
is writable where it lies and the aliasing the others do falls away.
Page permissions go through VMM, and there are no exports to walk.

What was XNU's only by accident becomes every injected flavor's: the
entrypoint, the config it parses, and the C library shims that answer
picolibc wherever it is linked. Vsock stays XNU's.
2026-08-14 12:54:23 +02:00
Ole André Vadla Ravnås d7b330d06d barebone: Name virtio's kernel calls in the facade
We drive the device ourselves, so what the host kernel offers has no
bearing on it. Only three calls underneath were XNU's — mapping the
registers, resolving a ring's physical address, and taking the
interrupt — so the facade names them and each kernel answers.

The write barrier is the one place the architecture shows through.
2026-08-13 18:33:02 +02:00
Ole André Vadla Ravnås af6f1eec42 tests: Inject a real ELF into an x86 guest
The relocations an image carries had only ever been applied to made-up
data, and the loader had only ever run against AArch64. Put a genuine
one through the whole path — inject, relocate, call its entry point —
and let the guest report where it ended up.
2026-08-13 18:16:13 +02:00
Ole André Vadla Ravnås 1e4abea59e barebone: Add an x86 helper that reports its bias
Something small enough to inject and prove nothing more than that the
image arrived: it hands back the address of a static, which is only
worth reading once the load bias has been applied, along with what it
finds there.
2026-08-13 18:16:13 +02:00
Ole André Vadla Ravnås bb809b4dbf barebone: Tell ld which x86 to prelink for
A relocatable link defaults to the host's own format, so prelinking a
32-bit agent on a 64-bit builder is refused outright. The arm64 branch
gets its answer from the cross linker and needs no such hint.
2026-08-13 17:57:18 +02:00
Ole André Vadla Ravnås 8ddd84061c tests: Boot a Windows 95 guest for the stub
The image is whatever the caller points at, since it is not ours to
fetch. TCG rather than KVM: the stub behaves there, and it sidesteps
the divide overflow a fast processor provokes in this kernel.

Nothing reaches the serial line to say the boot finished, so give it a
budget and leave the verdict to whoever attaches.
2026-08-13 17:57:18 +02:00
Ole André Vadla Ravnås f0c69bb108 barebone: Separate kernel flavor from injection
Quiescing the target and letting it go again are the kernel's business,
not the injector's: XNU wants EL1, a thread parked in thread_block and
a permission template read off it, and the VZ bridge wants a detach
rather than a continue. A target that is already halted wants none of
this, so it no longer has to supply a kernelcache, a load base or a
thread_block just to be injected into.
2026-08-13 17:57:18 +02:00
Ole André Vadla Ravnås ac65b7213b subprojects: Bump outdated 2026-08-13 17:54:51 +02:00
Ole André Vadla Ravnås 694964fffd submodules: Bump releng 2026-08-13 17:54:50 +02:00
Ole André Vadla Ravnås 77bb47a543 subprojects: Bump outdated 2026-08-13 16:21:08 +02:00
Ole André Vadla Ravnås ca38e91d1c submodules: Bump releng 2026-08-13 16:21:07 +02:00
Jamie 65e713c762 windows: Tweak ACLs to improve injection success rate 2026-08-13 15:05:23 +02:00
Ole André Vadla Ravnås fb701b8a67 tests: Check that an x86 guest hook fires
Hook a stub that answers 42 with a probe that stamps a marker, then
call it: the answer must survive the detour and the marker must show
the probe ran. Disabling it puts the stub back as it was.

Group the guest-driven tests in their own namespace while here.
2026-08-13 14:46:09 +02:00
Ole André Vadla Ravnås ec27b1872c barebone: Hook x86 guests inline
Lay out a GumCpuContext the way pushax already leaves the registers,
with the vector pointer above it and the instruction pointer below,
and hand its address to the probe. What follows the call is the
displaced code, relocated, and a branch back past the redirect.

Both bitnesses differ only in pointer width, so they share this.
2026-08-13 14:46:09 +02:00
Ole André Vadla Ravnås 5339e16d66 tests: Spell QEMU in test paths 2026-08-13 14:45:51 +02:00
Ole André Vadla Ravnås 0fc8ab428f tests: Exercise guest calls and x86-64 mapping
Write a stub that sums its arguments into a mapped page and call it,
which puts the whole register and stack handoff under test, and run
the mapping check on x86-64 as well.

Pin the guest's memory size so the stub lands on a page the kernel
reports as usable rather than in the tables reserved above it.
2026-08-13 14:45:51 +02:00
Ole André Vadla Ravnås ca04379c29 barebone: Call into x86 guests
Hijack a halted vCPU: point it at the callee with the return address
and arguments in place, then breakpoint the instruction it came from
and let it run until it lands back there. i386 passes both on the
stack and has no red zone to step over, x86-64 splits them.

Map pages on x86-64 too, the walk behind it already being shared.
2026-08-13 14:45:51 +02:00
Ole André Vadla Ravnås b0c073d983 gdb: Size register writes from the target
A register's width came from the Variant holding it, so every integer
went out 64 bits wide. On a 32-bit target that doubles each field of
the G packet and shifts the rest, landing ESP's value in EIP. Take the
width from the target's own description instead, and stop skipping the
narrower registers on stubs without bulk transfer.
2026-08-13 14:45:51 +02:00
Ole André Vadla Ravnås 6136bdc869 tests: Check x86 page mapping against QEMU
Map two pages into a booted guest and let QEMU's own view of the tables
confirm where they landed, then check the run is gone once released.
2026-08-13 13:54:43 +02:00
Ole André Vadla Ravnås f6e2605e3f barebone: Map pages into x86 guests
Reuse a hole in the guest's own page tables rather than allocating new
ones: descend the levels that already permit RWX and claim a run of
absent leaf entries. Restoring the entries the run displaced undoes it.

Skip a run starting at VA 0, so nothing lands where a null dereference
would find it.
2026-08-13 13:54:43 +02:00
Ole André Vadla Ravnås 1c04ae8143 barebone: Keep bindgen away from max_align_t
bindgen parses the headers with x87's 80-bit long double while the C
half is compiled -mlong-double-64, so its layout assertion for a type
nothing here uses fails on 32-bit x86.
2026-08-13 13:38:41 +02:00
Ole André Vadla Ravnås e3e1480a69 barebone: Port the agent crate to 32-bit x86
GLib's lock structures are pointer-aligned, so the overlays standing in
for them cannot hardcode 8-byte alignment: on 32-bit that both
over-aligns them and rounds their size past GLib's. Take the alignment
from the target, and give the recursive mutex's owner the pointer-sized
slot it already documents.

Narrow the thread ID where Gum and the overlay want a word, and hand
g_memdup2 a gsize rather than a u64.
2026-08-13 13:38:41 +02:00
Ole André Vadla Ravnås 73bb8d5a04 barebone: Key agent build on word size too
The kernel calls both x86 bitnesses x86, so ARCH alone cannot say
which one to build. Derive BITS alongside it and select the Rust
target, the SDK flavour and the C flags on the pair.

A target given as a spec file reaches cargo behind its own unstable
flag, so carry the whole --target argument in one variable.
2026-08-13 13:38:41 +02:00
Ole André Vadla Ravnås 0bf656dbbd barebone: Add bare-metal i686 Rust target
Rust ships no freestanding 32-bit x86 target, so spell one out. Its
rustc-abi is softfloat, which returns doubles in EAX:EDX and keeps
x87 out of the generated code, matching what clang emits for the C
half with +soft-float.

Widen the page size and cache flush shims to cover both x86
bitnesses.
2026-08-13 13:38:41 +02:00
Ole André Vadla Ravnås 96d2bfcc34 subprojects: Bump outdated 2026-08-13 13:34:47 +02:00
Ole André Vadla Ravnås ca77bf66c2 submodules: Bump releng 2026-08-13 13:34:46 +02:00
Ole André Vadla Ravnås 0e377306c4 subprojects: Bump outdated 2026-08-13 13:01:49 +02:00
Ole André Vadla Ravnås 65639716c5 submodules: Bump releng 2026-08-13 13:01:49 +02:00
Ole André Vadla Ravnås 713d87ddea tests: Bridge the kmod guest to a host socket
Given a socket, QEMU grows a virtio-serial port the guest can be reached
through, and the init copies between it and the device node the module
exposes. Both ends take a single opener, so each gets one descriptor
covering both directions, and a port with nobody attached on the host
side reads EOF, hence the restarts.

The kernel cannot work the port itself: kernel_read wants read_iter,
which virtio-console does not have.
2026-08-13 11:54:36 +02:00
Ole André Vadla Ravnås d9ebcc3622 barebone: Reach a resident agent over a socket
A guest kernel holding a virtio-serial port open puts the other end of
the link on the host as a socket, so let the resident-agent path connect
to one as readily as it opens a device node.
2026-08-13 11:54:33 +02:00
Ole André Vadla Ravnås b1c3e7cb14 subprojects: Bump outdated 2026-08-13 00:18:54 +02:00
Ole André Vadla Ravnås 317cb41703 submodules: Bump releng 2026-08-13 00:18:53 +02:00
Ole André Vadla Ravnås 1ee29ddfb7 tests: Stress the kmod guest's hooked paths
Read /proc from several tasks at once across several CPUs, which is what
puts the hooks under contention on the paths where the caller holds
locks that forbid sleeping. Fail the run when the kernel reports a
voluntary context switch inside an RCU read-side critical section, that
being what such a mistake looks like from the outside.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 0610db53ee tests: Mount devtmpfs in the kmod guest
Without /dev every redirect to /dev/null in the init failed, so the
reads meant to put the module's hooks through their paces were doing
nothing at all.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås a8ae43d771 barebone: Spin rather than sleep in hook context
A hook runs on the stack of whoever called the function it replaced, and
that caller may hold an RCU read lock or a spinlock. Sleeping there is
something the kernel complains about at best and deadlocks on at worst,
so wait by spinning when the context forbids it. An RCU read-side
critical section is still preemptible, which is what lets whoever we are
waiting on make progress.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås c4e01ffbfd barebone: Stop waiving declaration-after-statement
The file has none, so all the waiver did was keep quiet whichever
compilers check for it.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 8a298204fe barebone: Compile the agent without jump tables
An IBT kernel leaves NOTRACK_EN clear, so the NOTRACK jump a compiler
emits for a jump table faults like any other indirect branch landing
somewhere unmarked. The Rust half already avoided them; this is the C
half, and objtool's "notrack prefix found" is what says so.

What survives is libffi's hand-written asm, which nothing on the load
path reaches.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås de7e633df6 barebone: Unseal the code allocator too
It arrived after the rest and was left resolving through the raw lookup,
which meant the first slab allocation faulted rather than the bootstrap.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 05f64d14d6 barebone: Unseal the landing pads we call through
Every function whose address the kernel itself never takes has had its
landing pad poisoned at boot, which is the kernel saying nothing should
reach it through a pointer. Most of what the shim resolves is such a
function, and a kprobe reports the address after the pad either way, so
rewind to the entry and put the pad back.

MODULE_NAME comes along because a module that dies in its init path
stays wedged under its own name until the next boot, and bring-up wants
more attempts than that.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås fcfd2f21a1 barebone: Resolve symbols past the kprobe's ENDBR
A kprobe may not be planted on the ENDBR64 opening a function, so the
address it reports is the one after it, and calling that is what a CPU
with IBT enabled exists to refuse.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 369f26f296 barebone: Allocate code slabs near kernel text
A trampoline reaches the function it hooks with a direct branch only
from within range of the text, and vmalloc space is nowhere near it. Out
of range x86 goes through memory instead, landing mid-function where no
ENDBR64 marks the way, so a kernel with IBT enabled faults rather than
arrives. The module region is what execmem hands out.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 0830507c41 tests: Boot a guest that loads the kmod
The GDB stub guests have no userspace, so nothing there can insmod. This
one boots the kernel the module was built against — by default the one
running, vermagic tying a module to exactly one — loads it over a
busybox initramfs, reads /proc to put the cloak's hooks through their
paces, and leaves a shell on the console.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 3250dd952c barebone: Keep the agent's relocations loadable
x86_64-unknown-none is a PIC target that routes calls to external
symbols through the GOT, and the module loader takes neither: it rejects
R_X86_64_GOTPCREL outright.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 1d2eacff10 barebone: Document the x86_64 kmod flavour
Building for the running kernel is short enough to lead with; the arm64
walkthrough keeps the device-flavoured detour.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 90e3021646 barebone: Port the Linux kmod to x86_64
Makefile covers both architectures now, defaulting to the one it is
built on, and the ABI the kernel imposes differs: ENDBR64 on every
address-taken function for IBT, and switches compiled as comparisons,
objtool being unable to follow a jump table. The blob and the module
object it is linked into both have to be excused from objtool
regardless.

task_struct lost thread_group and fs_struct lost its lock along the way,
and neither is announced by anything the source can test for, so
Makefile asks the headers it is compiled against.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 3ec763cb4e barebone: Port the Linux agent to x86_64
The page size is a constant where arm64 reads it out of tcr_el1, and x86
keeps its caches coherent itself, so there is nothing to clear. Both the
CC variable and bindgen's triple follow from the Rust target.

gchar is signed here, unlike on arm64, which the three call sites
passing Rust bytes straight to GLib had relied on.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 3b35e1bf6e barebone: Fix trimming of the agent's libc
sort and join disagreed on collation, so the members the agent
supersedes were only partly dropped: picolibc's malloc, free, write and
gettimeofday all survived into the prelink alongside the agent's own.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås ac7b7e6744 barebone: Assign in the body, not in declarations
A declaration block should declare and nothing else, so the two
initialisers that called out to do their work now assign where the work
belongs.
2026-08-13 00:09:36 +02:00
Ole André Vadla Ravnås 49ceb4a9fc barebone: Test x86-64 walk against a QEMU guest
Parameterize the guest by architecture, and boot a 64-bit
kernel through the same path. Its direct map is made of
1 GiB pages and its text is the only thing left executable,
so both are checked against what the guest itself reports.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås ec7aed65fa barebone: Apply x86-64 relocations
Handle enough of R_X86_64_* to load an agent linked at zero,
mirroring what the IA32 machine does.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås e534e22bfb barebone: Change x86-64 page protection
Clear NX on the way to granting execute, the guest kernels we
attach to having it enabled throughout.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås 6ee4919615 barebone: Walk x86-64 page tables
Implement enumerate_ranges() and translate_address() on top
of the shared walk, including 1 GiB and 2 MiB pages. Teach
the fake stub to pose as a 64-bit target, so that the kernel
half exercises the sign extension.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås 8552c0573e barebone: Rename the x86-64 call frame
It was called Arm64CallFrame, having been copied from there.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås cd344237d0 barebone: Share x86 page table walking
Drive the walk from a table of levels rather than from the
two-level and PAE cases spelled out, and move it out of the
IA32 machine. Long mode is another entry in that table, PAE
being a subset of its format.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås a03b25adfc barebone: Test IA32 walk against a QEMU guest
Boot a stock Linux kernel with no root filesystem, and check
the walk against that guest's own view of its tables. Skip
the test where QEMU is unavailable.

Wait for the boot on the serial console before attaching, as
QEMU drops every GDB packet but Ctrl-C while the guest runs.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås ee39332362 barebone: Apply IA32 relocations
Handle enough of R_386_* to load an agent linked at zero:
shift the absolute ones by the load bias, and leave the
PC-relative ones alone.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås e6fc64cda4 barebone: Change IA32 page protection
Widen the entries above the leaf as needed, access rights
being the AND of every level's. Refuse large pages rather
than split them.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås c258b1d928 barebone: Walk IA32 page tables
Implement enumerate_ranges() and translate_address() for
two-level and PAE paging, including 4 MiB and 2 MiB pages.

Fall back to parsing the control registers out of the
monitor's register dump, as QEMU's x86 stub leaves them out
of its target description.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås 34ebbbbee6 barebone: Share range coalescing
Move it out of the ARM64 machine, as the IA32 machine needs
the same pass over its walk output.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås d3ff6ed321 barebone: Await restore of stub addressing mode
Hold on to any failure across the restore instead of firing
it and forgetting it: the reply is otherwise still in flight
when the next command goes out, that command's predicate
consumes it, and its caller is left with an empty response
and the stub still in physical mode.

Vala rejects yield inside finally, hence the explicit
handling.
2026-08-12 16:00:19 +02:00
Ole André Vadla Ravnås ed0ac90f59 subprojects: Bump outdated 2026-08-12 13:00:06 +02:00
Ole André Vadla Ravnås fba1f24676 submodules: Bump releng 2026-08-12 13:00:05 +02:00
Ole André Vadla Ravnås 903fb22273 barebone: Refresh the Linux kmod README
Document the prctl injection and cloaking channel, and drop the stale
CONFIG_VSOCKETS requirement — the module uses the /dev/frida chardev.
2026-08-12 12:52:39 +02:00
Ole André Vadla Ravnås 6e41df0484 build: Enable the Barebone backend on Android
Android is the primary target for the in-kernel agent, so pick the
Barebone backend up by default there like the other 64-bit hosts.
2026-08-12 12:41:43 +02:00
Ole André Vadla Ravnås f4959b4f5b linux: Add a kernel-assisted injector
Inject through the Frida kernel module when it is loaded, instead of
ptrace: stage the loader in a fresh region, give the bootstrap thread a
private TLS, spawn it into the target and let the agent connect back.
Cloak the loader's region, stack, TLS and thread so it leaves no trace.
Fall back to the existing paths when the module is absent.
2026-08-12 12:41:43 +02:00
Ole André Vadla Ravnås 16ef053845 barebone: Add injection and cloaking to the kmod
Drive the in-kernel agent from userspace over a token-gated magic-prctl
channel: allocate, free, read and write memory in a target pid, and spawn
a real CLONE_THREAD there, adopting its mm, files, creds, namespaces and
fs so the thread is a native member.

Hide cloaked threads, ranges and fds from /proc and keep thread counts
consistent, with a cloaked thread seeing through its own cloak. Enforce
through the in-kernel Gum Interceptor, installed once the worker thread
has brought up Gum.
2026-08-12 12:41:43 +02:00
Ole André Vadla Ravnås 5baa62b03e barebone: Bump clang-sys for libclang 22
Under libclang 22 the pinned clang-sys 1.8.1 silently lowers records to
opaque one-byte structs, so the agent's bindgen output loses every
field and the Rust build fails against the generated bindings. Bump
clang-sys to 1.9.1 (and bindgen to 0.72.1) to build against modern clang.
2026-08-12 12:41:43 +02:00
Ole André Vadla Ravnås 72a5fdf715 subprojects: Bump outdated 2026-08-11 11:41:38 +02:00
Ole André Vadla Ravnås b0c3df8b76 barebone: Note that deps.py needs releng
It is a submodule, so a clone without --recurse-submodules has no
deps.py to sync the SDK with.
2026-08-05 11:49:47 +02:00
Ole André Vadla Ravnås 4cdc54457f barebone: Document the kmod build from a release
Walks from a published GumJS devkit to a loaded module, so nobody has to
build Gum to get one. The Makefile now derives the compiler flags from
the SDK, since they have to agree with it down to the ABI.
2026-08-05 11:37:01 +02:00
Ole André Vadla Ravnås c812943c45 subprojects: Prepare for release 2026-08-05 00:32:48 +02:00
Ole André Vadla Ravnås 2e9521d006 subprojects: Bump outdated 2026-08-05 00:31:57 +02:00
Ole André Vadla Ravnås 65667a1973 submodules: Bump releng 2026-08-05 00:31:57 +02:00
Ole André Vadla Ravnås 2e50475515 barebone: Build the kmod against the SDK
Retires the script that assembled a libc by hand: the soft-float SDK now
ships one, and what the agent replaces is worked out from the archives
rather than listed. Adds KOUT for building against a kernel tree that
was configured elsewhere.
2026-08-04 17:20:21 +02:00
Ole André Vadla Ravnås fca672a9c1 barebone: Let CC carry flags in the agent build
The probe ran the compiler bare, so a bare-metal clang looked for headers
without a sysroot and found none. Split the variable the way cc-rs does
and pass the rest along.
2026-08-04 12:57:50 +02:00
Ole André Vadla Ravnås a8d9cacb1d barebone: Keep the libc lock object alive
Nothing references it, so it was dropped before the linker saw it and the
C library's own lock implementation came in after all, colliding with
ours. Only visible against a libc whose lock stubs live in a member that
gets pulled for other reasons.
2026-08-04 12:55:35 +02:00
Ole André Vadla Ravnås 76a9ac10f2 subprojects: Bump outdated 2026-08-02 18:29:05 +02:00
Ole André Vadla Ravnås 4a9900d165 barebone: Document how to reach the kmod agent
Serve it from a frida-server on the target with --device barebone, so
the device node is opened right next to itself and the host talks to
an ordinary frida-server over adb forward.

Also write down where a matching KDIR comes from. The kernel config
alone is not enough, but nothing needs a kernel build either.
2026-07-31 19:47:10 +02:00
Ole André Vadla Ravnås 4c29710d97 barebone: Fix the Linux agent's ABI mismatches
The prelink whole-archives the Rust staticlib, so Rust's own
compiler_builtins answered the C library's calls to __muldf3 and
friends. It is built for the ordinary AAPCS and takes its double in
d0, while everything else here is -mabi=aapcs-soft and passes it in
x0. Nothing faulted: the arithmetic simply returned garbage, and
GLib, whose hash tables grow on a floating-point threshold, then
never grew one. The first table to fill turned the next miss into an
endless probe loop. Drop those definitions and let the SDK's
compiler-rt answer instead.

That retires the byte-loop memcpy and memmove, which had been put
there to work around the same hang.

Separately, x18 is the kernel's shadow call stack pointer, and
RUSTFLAGS only reaches crates Cargo compiles: core, alloc and
compiler_builtins ship prebuilt and wrote to it in 269 places.
Rebuild them from source with -Zfixed-x18.
2026-07-31 18:40:00 +02:00
Ole André Vadla Ravnås 883e51bb1e submodules: Bump releng 2026-07-31 15:50:18 +02:00
Ole André Vadla Ravnås a8af49e5b3 barebone: Connect to an agent already resident
An agent loaded from inside the target brings its own transport, so
there is nothing to inject and no debugger stub to drive. Open the
device it offers and speak the same protocol over it, with the framing
details it used to take from the stub supplied directly.
2026-07-31 15:50:18 +02:00
Ole André Vadla Ravnås 872fb83da4 barebone: Run the agent as a Linux kernel module
The agent was written for XNU, where it is injected from outside by a
remote stub. Here it starts from the inside instead: a .ko whose init
path spawns the worker, with a character device standing in for the
hostlink.

Everything the kernel side needs lives in a C shim compiled against the
target's own headers, since struct layouts, inline helpers and which of
kallsyms' entry points happen to be exported all vary by release. The
Rust half reaches it through a facade that each flavour fills in.

Scripts are driven through the asynchronous half of the script API: the
thread pumping the JS context cannot also be the one waiting on it.

Neither flavour is the default one, since neither is what you get by
being on a particular host: both are picked deliberately.
2026-07-31 15:50:18 +02:00
70 changed files with 12990 additions and 1648 deletions

No files matched your search

+4
View File
@@ -3,6 +3,10 @@
/deps/
/src/android-helper/build/
/src/barebone/agent/target/
/src/barebone/agent/target-linux/
/src/barebone/agent/target-win9x/
/src/barebone/agent/linux/*.o
/src/barebone/agent/linux/*.o.bin
/src/barebone/agent/xpacks/
/src/barebone/helpers/memory-scanner/target/
/src/barebone/helpers/target/
+1 -1
View File
@@ -63,7 +63,7 @@ if build_agent
link_depends: extra_link_depends,
vs_module_defs: 'frida-agent.def',
override_options: ['b_asneeded=true'],
dependencies: [gio_dep, gee_dep, gum_dep, gumjs_dep, base_dep, pipe_dep, tls_provider_dep, payload_dep],
dependencies: [gio_dep, gee_dep, gum_dep, gumjs_dep, gumpharo_dep, base_dep, pipe_dep, tls_provider_dep, payload_dep],
)
operations = ['--move', 'constructor', 'frida_libc_shim_init', 'first']
+5 -1
View File
@@ -2274,7 +2274,11 @@ namespace Frida {
/**
* The V8 runtime.
*/
V8;
V8,
/**
* The Pharo runtime.
*/
PHARO;
public static ScriptRuntime from_nick (string nick) throws Error {
return Marshal.enum_from_nick<ScriptRuntime> (nick);
+1 -1
View File
@@ -49,7 +49,7 @@ payload = static_library('frida-payload-' + api_version, payload_sources,
],
vala_header: 'frida-payload.h',
vala_vapi: f'frida-payload-@[email protected]',
dependencies: [gio_dep, gee_dep, json_glib_dep, gum_dep, gumjs_dep, base_dep],
dependencies: [gio_dep, gee_dep, json_glib_dep, gum_dep, gumjs_dep, gumpharo_dep, base_dep],
install: true,
install_dir: [true, header_install_dir, true],
)
+155 -17
View File
@@ -91,7 +91,27 @@ namespace Frida {
if (name == null)
name = "script%u".printf (script_id.handle);
Gum.ScriptBackend backend = pick_backend (options.runtime);
ScriptRuntime runtime = options.runtime;
if (runtime == DEFAULT)
runtime = preferred_runtime;
#if HAVE_GUMPHARO
if (runtime == PHARO) {
if (source == null)
throw new Error.NOT_SUPPORTED ("The Pharo runtime cannot load precompiled scripts");
var instance = new PharoScriptInstance (script_id, new Gum.PharoScript (name, source));
instances[script_id] = instance;
instance.closed.connect (on_instance_closed);
instance.message.connect (on_instance_message);
instance.debug_message.connect (on_instance_debug_message);
return instance;
}
#endif
Gum.ScriptBackend backend = pick_backend (runtime);
Gum.Script script;
try {
@@ -109,7 +129,8 @@ namespace Frida {
return Source.REMOVE;
});
var instance = new ScriptInstance (script_id, script);
var js_instance = new JSScriptInstance (script_id, script);
ScriptInstance instance = js_instance;
instances[script_id] = instance;
instance.closed.connect (on_instance_closed);
@@ -175,7 +196,9 @@ namespace Frida {
}
public Gum.Script eternalize_script (AgentScriptId script_id) throws Error {
var instance = get_instance (script_id);
var instance = get_instance (script_id) as JSScriptInstance;
if (instance == null)
throw new Error.NOT_SUPPORTED ("Only JavaScript scripts may be eternalized");
var script = instance.eternalize ();
@@ -231,7 +254,7 @@ namespace Frida {
source.attach (Gum.ScriptBackend.get_scheduler ().get_js_context ());
}
public sealed class ScriptInstance : Object, RpcPeer {
public abstract class ScriptInstance : Object, RpcPeer {
public signal void closed ();
public signal void message (string json, Bytes? data);
public signal void debug_message (string message);
@@ -241,6 +264,120 @@ namespace Frida {
construct;
}
public abstract async void close ();
public abstract async void flush ();
public abstract async void load () throws Error;
public abstract void post (string json, Bytes? data) throws Error;
public abstract async void prepare_for_termination (TerminationReason reason);
public abstract void unprepare_for_termination ();
public abstract async void post_rpc_message (string json, Bytes? data, Cancellable? cancellable)
throws Error, IOError;
public virtual void interrupt () throws Error {
throw new Error.NOT_SUPPORTED ("Interrupt is not available for this runtime");
}
public virtual async void terminate () {
yield close ();
}
public virtual void enable_debugger () throws Error {
throw new Error.NOT_SUPPORTED ("Debugger is not available for this runtime");
}
public virtual void disable_debugger () {
}
public virtual void post_debug_message (string message) throws Error {
throw new Error.NOT_SUPPORTED ("Debugger is not available for this runtime");
}
}
#if HAVE_GUMPHARO
public sealed class PharoScriptInstance : ScriptInstance {
private Gum.PharoScript? script;
private State state = CREATED;
private enum State {
CREATED,
LOADED,
UNLOADED,
DESTROYED
}
private RpcClient rpc_client;
public PharoScriptInstance (AgentScriptId script_id, Gum.PharoScript script) {
Object (script_id: script_id);
this.script = script;
script.set_message_handler (on_message);
}
construct {
rpc_client = new RpcClient (this);
}
public override async void close () {
if (state == DESTROYED)
return;
if (state == LOADED) {
script.unload ();
state = UNLOADED;
}
script = null;
state = DESTROYED;
closed ();
}
public override async void flush () {
yield close ();
}
public override async void load () throws Error {
if (state != CREATED)
throw new Error.INVALID_OPERATION ("Script cannot be loaded in its current state");
script.load ();
state = LOADED;
}
public override void post (string json, Bytes? data) throws Error {
if (state != LOADED)
throw new Error.INVALID_OPERATION ("Only active scripts may be posted to");
script.post (json, data);
}
public override async void prepare_for_termination (TerminationReason reason) {
}
public override void unprepare_for_termination () {
}
private void on_message (Gum.PharoScript script, string json, Bytes? data) {
bool handled = rpc_client.try_handle_message (json);
if (!handled)
this.message (json, data);
}
public override async void post_rpc_message (string json, Bytes? data, Cancellable? cancellable)
throws Error, IOError {
if (script == null)
throw new Error.INVALID_OPERATION ("Script is destroyed");
script.post (json, data);
}
}
#endif
public sealed class JSScriptInstance : ScriptInstance {
public Gum.Script? script {
get {
return _script;
@@ -274,7 +411,7 @@ namespace Frida {
private RpcClient rpc_client;
public ScriptInstance (AgentScriptId script_id, Gum.Script script) {
public JSScriptInstance (AgentScriptId script_id, Gum.Script script) {
Object (script_id: script_id, script: script);
}
@@ -282,7 +419,7 @@ namespace Frida {
rpc_client = new RpcClient (this);
}
public async void close () {
public override async void close () {
if (close_request != null) {
try {
yield close_request.future.wait_async (null);
@@ -338,7 +475,7 @@ namespace Frida {
close_request.resolve (true);
}
public async void flush () {
public override async void flush () {
if (close_request == null)
close.begin ();
@@ -349,7 +486,7 @@ namespace Frida {
}
}
public async void load () throws Error {
public override async void load () throws Error {
if (state != CREATED)
throw new Error.INVALID_OPERATION ("Script cannot be loaded in its current state");
@@ -362,11 +499,11 @@ namespace Frida {
load_request.resolve (true);
}
public void interrupt () {
public override void interrupt () throws Error {
script.interrupt ();
}
public async void terminate () {
public override async void terminate () {
script.interrupt ();
yield close ();
@@ -397,7 +534,7 @@ namespace Frida {
return result;
}
public async void prepare_for_termination (TerminationReason reason) {
public override async void prepare_for_termination (TerminationReason reason) {
if (state == LOADED) {
schedule_on_js_thread (() => {
script.get_stalker ().flush ();
@@ -408,7 +545,7 @@ namespace Frida {
yield ensure_dispose_called (reason);
}
public void unprepare_for_termination () {
public override void unprepare_for_termination () {
if (state == DISPOSED) {
state = LOADED;
dispose_request = null;
@@ -448,7 +585,7 @@ namespace Frida {
dispose_request.resolve (true);
}
public void post (string json, Bytes? data) throws Error {
public override void post (string json, Bytes? data) throws Error {
switch (state) {
case LOADING:
case LOADED:
@@ -460,17 +597,17 @@ namespace Frida {
}
}
public void enable_debugger () throws Error {
public override void enable_debugger () throws Error {
if (_script != null)
_script.set_debug_message_handler (on_debug_message);
}
public void disable_debugger () {
public override void disable_debugger () {
if (_script != null)
_script.set_debug_message_handler (null);
}
public void post_debug_message (string message) {
public override void post_debug_message (string message) throws Error {
if (_script != null)
_script.post_debug_message (message);
}
@@ -485,7 +622,8 @@ namespace Frida {
this.debug_message (message);
}
private async void post_rpc_message (string json, Bytes? data, Cancellable? cancellable) throws Error, IOError {
public override async void post_rpc_message (string json, Bytes? data, Cancellable? cancellable)
throws Error, IOError {
if (script == null)
throw new Error.INVALID_OPERATION ("Script is destroyed");
script.post (json, data);
+15 -2
View File
@@ -12,14 +12,27 @@ frida_pipe_get_sddl_string_for_pipe (void)
#define DACL_ACE_EVERYONE_RW L"(A;;GRGW;;;WD)"
#define SACL_START L"S:"
#define SACL_ACE_LOWINTEGRITY_NORW L"(ML;;NWNR;;;LW)"
#define DACL_ACE_SYSTEM_RW L"(A;;GRGW;;;SY)"
#define DACL_ACE_ADMINS_RW L"(A;;GRGW;;;BA)"
if (frida_pipe_is_windows_8_or_greater ())
{
return DACL_START_NOINHERIT DACL_ACE_APPCONTAINER_RW DACL_ACE_EVERYONE_RW SACL_START SACL_ACE_LOWINTEGRITY_NORW;
return DACL_START_NOINHERIT
DACL_ACE_APPCONTAINER_RW
DACL_ACE_EVERYONE_RW
DACL_ACE_SYSTEM_RW
DACL_ACE_ADMINS_RW
SACL_START
SACL_ACE_LOWINTEGRITY_NORW;
}
else if (frida_pipe_is_windows_vista_or_greater ())
{
return DACL_START_NOINHERIT DACL_ACE_EVERYONE_RW SACL_START SACL_ACE_LOWINTEGRITY_NORW;
return DACL_START_NOINHERIT
DACL_ACE_EVERYONE_RW
DACL_ACE_SYSTEM_RW
DACL_ACE_ADMINS_RW
SACL_START
SACL_ACE_LOWINTEGRITY_NORW;
}
else
{
+10 -1
View File
@@ -382,6 +382,12 @@ if have_in_tree_payloads
endif
gumjs_inspector_dep = dependency('frida-gumjs-inspector-1.0', default_options: ['default_library=static'])
gumpharo_dep = dependency('frida-gumpharo-' + api_version, required: false)
have_gumpharo = gumpharo_dep.found()
if have_gumpharo
vala_flags += '--define=HAVE_GUMPHARO'
endif
gee_options = [
'default_library=static',
'disable-introspection=true',
@@ -527,7 +533,7 @@ have_droidy_backend = get_option('droidy_backend') \
have_socket_backend = get_option('socket_backend').allowed()
have_barebone_backend = get_option('barebone_backend') \
.disable_if(not (quickjs_dep.found() and lzfse_dep.found())) \
.disable_auto_if(not host_docks_mobile_devices) \
.disable_auto_if(not host_docks_mobile_devices and host_os != 'android') \
.allowed()
compiler_backend_opt = get_option('compiler_backend') \
.disable_auto_if(host_os == 'watchos')
@@ -669,6 +675,9 @@ gumjs_inspector_vala_args = ['--pkg=frida-gumjs-inspector-1.0', gum_vala_args]
if have_in_tree_payloads
gumjs_vala_args = ['--pkg=frida-gumjs-1.0', '--pkg=gio-2.0', gum_vala_args]
if have_gumpharo
gumjs_vala_args += ['--pkg=frida-gumpharo-' + api_version]
endif
have_v8 = gumjs_dep.get_variable('gumjs_v8') == 'enabled'
if have_v8
+1 -1
Submodule releng updated: abb8238970...0ad9eef364.
+99 -46
View File
@@ -5,17 +5,22 @@ namespace Frida.Barebone {
private Cancellable io_cancellable = new Cancellable ();
private SocketConnection hostlink;
private IOStream hostlink;
private BufferedInputStream input;
private OutputStream output;
private ByteOrder byte_order;
private uint pointer_size;
private AgentConfig agent_config;
private VsockTransportConfig? vsock_transport;
private ImageConfig? image_config;
private KernelKind kernel_kind;
private KernelRelocation? relocation;
private uint64 kernel_base;
private Machine machine;
private Allocator allocator;
private Gee.List<SymbolInfo> kernel_symbols;
private Allocation elf_allocation;
private Allocation config_allocation;
@@ -26,15 +31,18 @@ namespace Frida.Barebone {
private const int COMMAND_TIMEOUT_MS = 25000;
public static async AgentConnection open (AgentConfig agent_config, ImageConfig? image_config,
KernelRelocation? relocation, uint64 kernel_base, Machine machine, Allocator allocator,
Cancellable? cancellable) throws Error, IOError {
KernelKind kernel_kind, KernelRelocation? relocation, uint64 kernel_base, Machine machine,
Allocator allocator, Gee.List<SymbolInfo> kernel_symbols, Cancellable? cancellable)
throws Error, IOError {
var connection = new AgentConnection () {
agent_config = agent_config,
image_config = image_config,
kernel_kind = kernel_kind,
relocation = relocation,
kernel_base = kernel_base,
machine = machine,
allocator = allocator,
kernel_symbols = kernel_symbols,
};
try {
@@ -46,15 +54,33 @@ namespace Frida.Barebone {
return connection;
}
/**
* Attaches to an agent already resident in the target, which brought its own
* transport with it, such as the Linux kernel module exposing /dev/frida.
*/
public static async AgentConnection open_resident (IOStream stream, Cancellable? cancellable)
throws Error, IOError {
var connection = new AgentConnection () {
byte_order = ByteOrder.HOST,
pointer_size = (uint) sizeof (void *),
};
connection.adopt_hostlink_streams (stream);
connection.process_incoming_messages.begin ();
return connection;
}
private const uint8 TRANSPORT_KIND_VIRTIO = 0;
private const uint8 TRANSPORT_KIND_VSOCK = 1;
private const uint8 TRANSPORT_KIND_VIRTIO_PCI = 2;
private async bool init_async (int io_priority, Cancellable? cancellable) throws Error, IOError {
var transport_tag = yield resolve_transport (cancellable);
var gdb = machine.gdb;
ByteOrder byte_order = gdb.byte_order;
uint pointer_size = gdb.pointer_size;
byte_order = gdb.byte_order;
pointer_size = gdb.pointer_size;
Layout layout;
uint64 preferred_base = 0;
@@ -73,8 +99,6 @@ namespace Frida.Barebone {
} else {
layout = new Layout.empty ();
}
if (kernel_base == 0)
throw new Error.NOT_SUPPORTED ("Missing kernel_base");
var symbols = new Gee.HashMap<string, SymbolInfo> ();
var hash_builder = new SymbolHashBuilder ();
@@ -101,13 +125,25 @@ namespace Frida.Barebone {
}
}
SymbolInfo? thread_block = symbols["thread_block"];
if (thread_block == null)
throw new Error.NOT_SUPPORTED ("Missing symbol for thread_block");
KernelKind kind = kernel_kind;
if (kind == AUTO)
kind = (image_config != null) ? KernelKind.XNU : KernelKind.BARE;
SymbolInfo? panic = symbols["panic"];
if (panic != null && machine is Arm64Machine)
((Arm64Machine) machine).call_landing_zone = kernel_base + panic.offset;
foreach (var s in kernel_symbols) {
symbols[s.name] = s;
hash_builder.add_symbol (s);
}
KernelFlavor flavor;
if (kind == XNU) {
if (kernel_base == 0)
throw new Error.NOT_SUPPORTED ("Missing kernel_base");
flavor = new XnuKernelFlavor (machine, kernel_base, symbols);
} else if (kind == WIN9X) {
flavor = new Win9xKernelFlavor (machine, symbols);
} else {
flavor = new BareKernelFlavor (machine);
}
Bytes symbol_data = hash_builder.build (byte_order);
@@ -134,14 +170,10 @@ namespace Frida.Barebone {
return true;
});
yield machine.enter_exception_level (1, 1000, cancellable);
yield run_until_thread_block (kernel_base + thread_block.offset, cancellable);
yield flavor.prepare (cancellable);
size_t page_size = yield machine.query_page_size (cancellable);
yield ((Arm64Machine) machine).learn_permission_templates (kernel_base + thread_block.offset, cancellable);
elf_allocation = yield inject_elf (elf, raw_elf.bytes, page_size, machine, allocator, cancellable);
uint64 start_address = 0;
@@ -187,14 +219,7 @@ namespace Frida.Barebone {
},
cancellable);
// The vphone research kernel panics on any synchronous exception taken while a
// debugger is attached, which the worker hits in the allocator during gum_init.
var arm64 = machine as Arm64Machine;
bool post_inject_access_uses_bridge = arm64 != null && arm64.physical_memory != null;
if (post_inject_access_uses_bridge)
yield gdb.detach (cancellable);
else
yield gdb.continue (cancellable);
yield flavor.settle (cancellable);
yield establish_hostlink (cancellable);
process_incoming_messages.begin ();
@@ -202,19 +227,6 @@ namespace Frida.Barebone {
return true;
}
private async void run_until_thread_block (uint64 address, Cancellable? cancellable) throws Error, IOError {
var gdb = machine.gdb;
var bp = yield gdb.add_breakpoint (SOFT, address, 4, cancellable);
GDB.Breakpoint? hit = null;
do {
var exception = yield gdb.continue_until_exception (cancellable);
hit = exception.breakpoint;
} while (hit != bp);
yield bp.remove (cancellable);
}
private async Variant resolve_transport (Cancellable? cancellable) throws Error, IOError {
if (agent_config.transport is HostlinkTransportConfig)
return yield connect_virtio_transport ((HostlinkTransportConfig) agent_config.transport, cancellable);
@@ -257,9 +269,16 @@ namespace Frida.Barebone {
private async Variant connect_virtio_transport (HostlinkTransportConfig config, Cancellable? cancellable)
throws Error, IOError {
var qmp = yield QmpClient.open (config.qmp, 0, cancellable);
var link = yield qmp.open_hostlink (cancellable);
var link = yield qmp.open_hostlink (config.bus, cancellable);
adopt_hostlink_streams (link.connection);
if (config.bus != null) {
return new Variant.tuple ({
new Variant.byte (TRANSPORT_KIND_VIRTIO_PCI),
new Variant.variant (new Variant.tuple ({}))
});
}
Variant[] virtio_cfg = { new Variant.uint64 (link.mmio), new Variant.uint32 (link.irq) };
return new Variant.tuple ({
new Variant.byte (TRANSPORT_KIND_VIRTIO),
@@ -267,7 +286,7 @@ namespace Frida.Barebone {
});
}
private void adopt_hostlink_streams (SocketConnection connection) {
private void adopt_hostlink_streams (IOStream connection) {
hostlink = connection;
input = (BufferedInputStream) Object.new (typeof (BufferedInputStream),
"base-stream", hostlink.get_input_stream (),
@@ -290,6 +309,30 @@ namespace Frida.Barebone {
io_cancellable.cancel ();
}
public async HostProcessInfo[] enumerate_processes (Cancellable? cancellable) throws Error, IOError {
var response = yield execute_command (Command.ENUMERATE_PROCESSES, new Variant.tuple ({}), cancellable);
if (!response.check_format_string ("a(us)", false))
throw new Error.PROTOCOL ("Invalid enumerate_processes response format");
var result = new Gee.ArrayList<HostProcessInfo?> ();
uint32 pid;
unowned string path;
var it = response.iterator ();
while (it.next ("(u&s)", out pid, out path)) {
result.add (HostProcessInfo (pid, basename_of (path),
new HashTable<string, Variant> (str_hash, str_equal)));
}
var processes = new HostProcessInfo[result.size];
for (int i = 0; i != result.size; i++)
processes[i] = result[i];
return processes;
}
private static string basename_of (string path) {
int start = path.last_index_of_char ('\\');
return (start != -1) ? path[start + 1:] : path;
}
public async AgentScriptId create_script (string source, Cancellable? cancellable) throws Error, IOError {
var payload = new Variant ("s", source);
var response = yield execute_command (Command.CREATE_SCRIPT, payload, cancellable);
@@ -341,16 +384,25 @@ namespace Frida.Barebone {
});
timeout_source.attach (MainContext.get_thread_default ());
Variant response = null;
try {
return yield promise.future.wait_async (cancellable);
response = yield promise.future.wait_async (cancellable);
} finally {
timeout_source.destroy ();
}
if (!response.check_format_string ("(bv)", false))
throw new Error.PROTOCOL ("Malformed reply from agent");
bool succeeded;
Variant detail;
response.get ("(bv)", out succeeded, out detail);
if (!succeeded)
throw new Error.NOT_SUPPORTED ("%s", detail.get_string ());
return detail;
}
private async void process_incoming_messages () {
var byte_order = machine.gdb.byte_order;
try {
while (true) {
size_t header_size = 4;
@@ -503,10 +555,10 @@ namespace Frida.Barebone {
private Bytes frame_message (Command command, uint16 request_id, Variant payload) {
var message = new Variant ("(yqv)", (uint8) command, request_id, payload);
if (machine.gdb.byte_order != ByteOrder.HOST)
if (byte_order != ByteOrder.HOST)
message = message.byteswap ();
var message_bytes = message.get_data_as_bytes ();
return machine.gdb.make_buffer_builder ()
return new BufferBuilder (byte_order, pointer_size)
.append_uint32 ((uint32) message_bytes.get_size ())
.append_bytes (message_bytes)
.build ();
@@ -520,6 +572,7 @@ namespace Frida.Barebone {
REMAP_WRITABLE_PAGES = 5,
MEMORY_PROTECT = 6,
PATCH_CODE = 7,
ENUMERATE_PROCESSES = 8,
REPLY = 128,
SCRIPT_MESSAGE = 129
}
+4 -4
View File
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "bindgen"
version = "0.72.0"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags",
"cexpr",
@@ -63,9 +63,9 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "clang-sys"
version = "1.8.1"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
dependencies = [
"glob",
"libc",
+19
View File
@@ -4,6 +4,25 @@ version = "1.0.0"
edition = "2024"
build = "build.rs"
[lib]
name = "frida_barebone_agent"
path = "src/lib.rs"
crate-type = ["rlib", "staticlib"]
# The XNU flavour is a freestanding executable the host injects and calls into.
# The Linux flavour is the staticlib above, linked into a .ko by linux/Kbuild.
[[bin]]
name = "frida-barebone-agent"
path = "src/main.rs"
required-features = ["blob"]
[features]
# Flavors the host injects and calls into, as opposed to the module Linux loads.
blob = []
xnu = ["blob"]
win9x = ["blob"]
linux = []
[profile.release]
panic = "abort"
opt-level = "z"
+20 -1
View File
@@ -21,10 +21,29 @@
export AR_aarch64_unknown_none=aarch64-none-elf-ar
export RANLIB_aarch64_unknown_none=aarch64-none-elf-ranlib
cargo build --release --features xnu
## Targets
The agent supports two kernels. One of them has to be picked; neither is a
default, since neither follows from the host you build on:
- `xnu` — a freestanding ELF executable that the Barebone backend
injects into a running kernel from the outside, over a remote stub.
- `linux` — a static library that becomes a kernel module, loaded from the
inside. See `linux/README.md`.
Everything above the `kernel` module is shared; each backend supplies the same
set of primitives (logging, allocation, threads, waiting, time, symbols) plus its
half of Gum's platform backend (`gum_xnu.rs` / `gum_linux.rs`).
## Development loop
export FRIDA_BAREBONE_CONFIG=$PWD/etc/xnu.json
cargo build --release && make -C ~/src/frida-python && killall -9 qemu-system-aarch64 && sleep 2 && frida -D barebone -p 0
cargo build --release --features xnu \
&& make -C ~/src/frida-python \
&& killall -9 qemu-system-aarch64 && sleep 2 \
&& frida -D barebone -p 0
## Speeding up loop
+63
View File
@@ -0,0 +1,63 @@
SECTIONS {
.text : {
*(.text.entrypoint)
*(.text*)
}
.rodata : {
*(.rodata*)
}
.kernel_addrs : ALIGN(4) {
. = ALIGN(4);
_Get_Cur_VM_Handle = .; . += 4;
_Get_Sys_VM_Handle = .; . += 4;
_Schedule_Global_Event = .; . += 4;
_Create_Semaphore = .; . += 4;
_Wait_Semaphore = .; . += 4;
_Signal_Semaphore = .; . += 4;
_Get_Next_VM_Handle = .; . += 4;
_Release_Time_Slice = .; . += 4;
_Set_Global_Time_Out = .; . += 4;
_Cancel_Time_Out = .; . += 4;
_Get_System_Time = .; . += 4;
__HeapAllocate = .; . += 4;
__HeapFree = .; . += 4;
__PageAllocate = .; . += 4;
__PageFree = .; . += 4;
__CopyPageTable = .; . += 4;
__MapPhysToLinear = .; . += 4;
_Hook_VMM_Fault = .; . += 4;
_Fatal_Error_Handler = .; . += 4;
_VMMCreateThread = .; . += 4;
_Get_Cur_Thread_Handle = .; . += 4;
_Get_Initial_Thread_Handle = .; . += 4;
_Get_Next_Thread_Handle = .; . += 4;
__Debug_Printf_Service = .; . += 4;
__ContextSwitch = .; . += 4;
__PageModifyPermissions = .; . += 4;
__GetCurrentContext = .; . += 4;
_VPICD_Virtualize_IRQ = .; . += 4;
_VPICD_Phys_EOI = .; . += 4;
_VPICD_Physically_Unmask = .; . += 4;
}
.data.rel.ro : {
*(.data.rel.ro*)
}
.got : {
*(.got*)
}
.bss : {
*(.bss*)
}
. = ALIGN(4);
_heap_start = .;
}
VERSION {
{
global:
_start;
local:
*;
};
}
+47 -21
View File
@@ -8,12 +8,17 @@ fn main() {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let devkit_dir = PathBuf::from(env::var("GUMJS_DEVKIT_DIR").unwrap());
let cc_str = env::var("CC_aarch64_unknown_none")
let target = env::var("TARGET").unwrap();
let cc_str = env::var(format!("CC_{}", target.replace('-', "_")))
.or_else(|_| env::var("CC"))
.unwrap_or_else(|_| "cc".to_string());
let cc = Path::new(&cc_str);
let cc_include_paths = detect_gcc_include_paths(cc);
let cc_library_paths = detect_gcc_library_paths(cc);
// Same convention as cc-rs: the variable may carry flags, and a bare-metal
// compiler needs them to find its libc at all.
let mut cc_argv = cc_str.split_whitespace();
let cc = Path::new(cc_argv.next().unwrap());
let cc_args: Vec<&str> = cc_argv.collect();
let cc_include_paths = detect_gcc_include_paths(cc, &cc_args);
let cc_library_paths = detect_gcc_library_paths(cc, &cc_args);
let cc_clang_args: Vec<String> = cc_include_paths
.iter()
@@ -25,9 +30,10 @@ fn main() {
.header(devkit_dir.join("frida-gumjs.h").to_str().unwrap())
.clang_arg("-nostdinc")
.clang_arg("-target")
.clang_arg("aarch64-none-elf")
.clang_arg(format!("{}-none-elf", target.split('-').next().unwrap()))
.clang_args(cc_clang_args)
.clang_arg(format!("-I{}", devkit_dir.to_str().unwrap()))
.blocklist_type("max_align_t")
.merge_extern_blocks(true)
.generate()
.expect("Unable to generate bindings");
@@ -36,25 +42,45 @@ fn main() {
.write_to_file(out_dir.join("bindings.rs"))
.expect("Couldn't write bindings");
println!(
"cargo:rustc-link-search=native={}",
devkit_dir.to_str().unwrap()
);
println!("cargo:rustc-link-lib=static=frida-gumjs");
for path in cc_library_paths {
println!("cargo:rustc-link-search=native={}", path.to_string_lossy());
// The Linux flavour is a staticlib: kbuild owns the final link, so it — not
// us — decides where the devkit and libc archives come from.
if env::var("CARGO_FEATURE_BLOB").is_ok() {
println!(
"cargo:rustc-link-search=native={}",
devkit_dir.to_str().unwrap()
);
println!("cargo:rustc-link-lib=static=frida-gumjs");
for path in cc_library_paths {
println!("cargo:rustc-link-search=native={}", path.to_string_lossy());
}
for arg in &cc_args {
if let Some(path) = arg.strip_prefix("-L") {
println!("cargo:rustc-link-search=native={path}");
}
}
println!("cargo:rustc-link-lib=static=c");
println!("cargo:rustc-link-lib=static=m");
println!("cargo:rustc-link-arg=--export-dynamic");
println!("cargo:rustc-link-arg=--emit-relocs");
let script = if env::var("CARGO_FEATURE_WIN9X").is_ok() {
"agent-win9x.lds"
} else {
"agent.lds"
};
println!("cargo:rustc-link-arg=--script={script}");
println!("cargo:rustc-link-arg=--gc-sections");
// All of the agent lives in the library crate, so nothing in the binary
// references the entrypoint the host calls; keep the linker from
// dropping it along with the rest of the unreferenced archive members.
println!("cargo:rustc-link-arg=--undefined=_start");
}
println!("cargo:rustc-link-lib=static=c");
println!("cargo:rustc-link-lib=static=m");
println!("cargo:rustc-link-arg=--export-dynamic");
println!("cargo:rustc-link-arg=--emit-relocs");
println!("cargo:rustc-link-arg=--script=agent.lds");
println!("cargo:rustc-link-arg=--gc-sections");
println!("cargo:rerun-if-changed=build.rs");
}
pub fn detect_gcc_include_paths(gcc: &Path) -> Vec<PathBuf> {
pub fn detect_gcc_include_paths(gcc: &Path, args: &[&str]) -> Vec<PathBuf> {
let out = Command::new(gcc)
.args(args)
.args(["-xc", "-E", "-v", "-"])
.stdin(Stdio::null())
.output()
@@ -94,8 +120,8 @@ pub fn detect_gcc_include_paths(gcc: &Path) -> Vec<PathBuf> {
paths
}
pub fn detect_gcc_library_paths(gcc: &Path) -> Vec<PathBuf> {
let out = Command::new(gcc).arg("-print-search-dirs").output()
pub fn detect_gcc_library_paths(gcc: &Path, args: &[&str]) -> Vec<PathBuf> {
let out = Command::new(gcc).args(args).arg("-print-search-dirs").output()
.expect("Failed to execute GCC to detect library paths");
let stdout = String::from_utf8_lossy(&out.stdout);
@@ -0,0 +1,8 @@
{
"agent": {
"transport": {
"type": "socket",
"path": "/tmp/frida-guest.sock"
}
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"agent": {
"transport": {
"type": "device",
"path": "/dev/frida"
}
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"connection": {
"host": "127.0.0.1",
"port": 1234
},
"kernel": "win9x",
"agent": {
"path": "/home/oleavr/src/frida-core/src/barebone/agent/target-win9x/i686-unknown-none/release/frida-barebone-agent",
"transport": {
"type": "hostlink",
"qmp": "unix:/tmp/claude-1000/-home-oleavr-src-frida-core/0f3e358c-8f66-4246-b793-deb2cd3727f3/scratchpad/qmp.sock",
"bus": "frida-vserial.0"
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"arch": "x86",
"cpu": "pentium4",
"crt-objects-fallback": "false",
"data-layout": "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-i128:128-f64:32:64-f80:32-n8:16:32-S128",
"features": "-mmx,-sse,-sse2,-sse3,-ssse3,-sse4.1,-sse4.2,-avx,-avx2,+soft-float",
"linker": "rust-lld",
"linker-flavor": "gnu-lld",
"llvm-target": "i686-unknown-none-elf",
"max-atomic-width": 64,
"panic-strategy": "abort",
"plt-by-default": true,
"position-independent-executables": false,
"relocation-model": "static",
"rustc-abi": "softfloat",
"stack-probes": {
"kind": "inline"
},
"target-pointer-width": 32
}
+28
View File
@@ -0,0 +1,28 @@
MODULE_NAME ?= frida-agent
obj-m += $(MODULE_NAME).o
# frida-agent-prelinked.o is not built from source here: it is the Rust agent, the
# GumJS devkit and the C library collapsed into a single relocatable object by the prelink
# step in Makefile. Copy it in through if_changed rather than kbuild's _shipped rule,
# so the .cmd file modpost expects gets written.
$(MODULE_NAME)-y := frida-kmod.o frida-agent-prelinked.o
targets += frida-agent-prelinked.o
quiet_cmd_frida_agent_prelinked = COPY $@
cmd_frida_agent_prelinked = cp $< $@
$(obj)/frida-agent-prelinked.o: $(src)/frida-agent-prelinked.o.bin FORCE
$(call if_changed,frida_agent_prelinked)
# On x86 objtool validates every object going into a module and generates its ORC
# unwind tables. It was not built to read a JavaScript runtime linked against a
# freestanding libc: it gives up on the first Rust switch jump table it cannot
# follow, and the tables would only ever describe frames no kernel stack trace walks
# into. Both the blob and the module object it is linked into have to be excused,
# since objtool runs over each.
OBJECT_FILES_NON_STANDARD_frida-agent-prelinked.o := y
OBJECT_FILES_NON_STANDARD_$(MODULE_NAME).o := y
ccflags-y += $(FRIDA_KMOD_DEFS)
+205
View File
@@ -0,0 +1,205 @@
# Builds frida-agent.ko in two steps, because kbuild insists on owning the final
# link:
#
# 1. Collapse the Rust staticlib plus everything it needs — the GumJS devkit and
# the C library — into one relocatable object, and localize every symbol except
# the entry points frida-kmod.c calls. Localizing matters: Gum and the C library
# define names like memcpy and malloc that must not leak into the kernel's
# namespace.
# 2. Hand that object to kbuild as a shipped object, so vermagic, modinfo and
# CRCs all come from the target kernel's own build system.
#
# See README.md for usage.
# KDIR is the kernel to build against. When it is a source tree that was configured
# elsewhere — which is what Android's modules_prepare tarball gives — point KOUT at
# the prepared tree.
KDIR ?= /lib/modules/$(shell uname -r)/build
PROFILE ?= release
# The kernel's own name for the architecture, and the one Rust knows it by. Both
# follow from the machine this is built for, which defaults to the one building.
ARCH ?= $(shell uname -m \
| sed -e 's/^i[3-6]86$$/x86/' -e 's/^x86_64$$/x86/' -e 's/^aarch64$$/arm64/')
BITS ?= $(shell uname -m \
| sed -e 's/^i[3-6]86$$/32/' -e 's/^x86_64$$/64/' -e 's/^aarch64$$/64/')
ifeq ($(ARCH)-$(BITS),x86-32)
RUST_TARGET ?= i686-unknown-none
AGENT_TARGET_FLAGS ?= --target $(AGENT_DIR)/$(RUST_TARGET).json -Z json-target-spec
SDK_ARCH ?= x86
TOOLPREFIX ?=
AGENT_LD_EMULATION ?= elf_i386
else ifeq ($(ARCH)-$(BITS),x86-64)
RUST_TARGET ?= x86_64-unknown-none
SDK_ARCH ?= x86_64
TOOLPREFIX ?=
AGENT_LD_EMULATION ?= elf_x86_64
else ifeq ($(ARCH)-$(BITS),arm64-64)
RUST_TARGET ?= aarch64-unknown-none
SDK_ARCH ?= arm64
TOOLPREFIX ?= aarch64-none-elf-
else
$(error Unsupported ARCH "$(ARCH)" and BITS "$(BITS)"; this flavour covers \
x86/32, x86/64 and arm64/64)
endif
AGENT_TARGET_FLAGS ?= --target $(RUST_TARGET)
AGENT_DIR := $(CURDIR)/..
AGENT_TARGET_DIR := $(AGENT_DIR)/target-linux
AGENT_LIB := $(AGENT_TARGET_DIR)/$(RUST_TARGET)/$(PROFILE)/libfrida_barebone_agent.a
GUMJS_DEVKIT_DIR ?= $(error Set GUMJS_DEVKIT_DIR to the directory holding libfrida-gumjs.a)
# Any binutils that speak the target's ELF will do; nothing here is compiled with
# them. Building for the host's own architecture, that is the host's binutils.
AGENT_LD ?= $(TOOLPREFIX)ld
AGENT_AR ?= $(TOOLPREFIX)ar
AGENT_NM ?= $(TOOLPREFIX)nm
AGENT_OBJCOPY ?= $(TOOLPREFIX)objcopy
# The ABI the kernel imposes on anything linked into it. The flags only reach crates
# Cargo compiles, and the toolchain ships core, alloc and compiler_builtins prebuilt
# without them, so those three have to be rebuilt from source as well. That needs the
# rust-src component and an unstable flag, hence RUSTC_BOOTSTRAP.
#
# On x86_64 that is endbr64 on every address-taken function, since a kernel with
# CONFIG_X86_KERNEL_IBT faults an indirect call landing anywhere else, and switches
# compiled as comparisons rather than jump tables, which objtool cannot follow into.
# Soft-float, the kernel code model and the absent red zone come with the target
# itself. On arm64
# it is x18, which the kernel reserves for the shadow call stack, so nothing linked in
# may use it as a general-purpose register.
ifeq ($(ARCH)-$(BITS),x86-32)
AGENT_RUSTFLAGS ?=
AGENT_CFLAGS ?= --target=i386-none-elf \
-Xclang -target-feature -Xclang +soft-float -mno-mmx -mno-sse \
-mlong-double-64 -fno-pic --sysroot=$(FRIDA_SDK)
else ifeq ($(ARCH)-$(BITS),x86-64)
AGENT_RUSTFLAGS ?= -Zcf-protection=branch \
-Cllvm-args=--min-jump-table-entries=65536 \
-Crelocation-model=static -Zplt=yes
AGENT_CFLAGS ?= --target=x86_64-none-elf \
-Xclang -target-feature -Xclang +soft-float -mno-mmx -mno-sse -mno-red-zone \
-mlong-double-64 -mcmodel=kernel -fcf-protection=branch -fno-jump-tables \
-fno-pic --sysroot=$(FRIDA_SDK)
else
AGENT_RUSTFLAGS ?= -Zfixed-x18
AGENT_CFLAGS ?= --target=aarch64-none-elf -mabi=aapcs-soft -mgeneral-regs-only \
-ffixed-x18 -fno-pic --sysroot=$(FRIDA_SDK)
endif
AGENT_BUILD_STD ?= -Z build-std=core,alloc,compiler_builtins
# The devkit header reaches for GLib's and capstone's, which a devkit built against
# an SDK leaves in that SDK rather than beside itself. The sysroot's own include
# directory is spelled out because only clang's bare-metal driver, which it selects
# for arm64 but not for x86_64, searches there on its own.
AGENT_CFLAGS += -isystem $(FRIDA_SDK)/include \
-I$(FRIDA_SDK)/include/glib-2.0 \
-I$(FRIDA_SDK)/lib/glib-2.0/include \
-I$(FRIDA_SDK)/include/capstone \
-I$(FRIDA_SDK)/include/json-glib-1.0
# The soft-float SDK, whose libc is picolibc and whose compiler runtime is
# compiler-rt. GCC's own newlib and libgcc are hard-float and cannot be used here.
FRIDA_SDK ?= $(error Set FRIDA_SDK to a none-$(SDK_ARCH)-softfloat SDK prefix)
LIBM = $(FRIDA_SDK)/lib/libm.a
LIBBUILTINS = $(FRIDA_SDK)/lib/libclang_rt.builtins.a
# bindgen reads the devkit header through whatever this compiler reports as its
# include path, so it has to be one that knows where the SDK keeps its libc. It has
# to be clang either way: -mabi=aapcs-soft landed there in 19 and GCC has it at no
# version, and on x86_64 the soft-float ABI is reachable only as an -Xclang feature.
AGENT_CC ?= clang
PRELINKED := frida-agent-prelinked.o.bin
AGENT_LIB_SOFTFLOAT := $(AGENT_TARGET_DIR)/libfrida-barebone-agent-softfloat.a
# On arm64 the toolchain's own compiler_builtins is compiled for the ordinary AAPCS,
# where a double travels in d0: its __muldf3 opens with "fmov x14, d0". Everything
# else there is -mabi=aapcs-soft and passes that same double in x0. Whole-archiving
# the staticlib puts those definitions in front of the SDK's compiler-rt, which has
# the soft-float ones, and every double the C library computes then comes back
# garbage — quietly, since the arithmetic still runs. Drop them and let compiler-rt
# answer instead. x86_64 needs none of this: soft-float is part of the Rust target
# there, so the copy build-std compiles already passes doubles the way the C half
# expects.
ifeq ($(ARCH),arm64)
DROP_HARDFLOAT_BUILTINS = \
$(AGENT_AR) d $@ $$($(AGENT_NM) --print-armap $@ | sed -n 's/^__muldf3 in //p') && \
$(AGENT_AR) s $@
else
DROP_HARDFLOAT_BUILTINS = :
endif
# The agent puts malloc, the C library's locks and its process stubs on the kernel's
# own facilities, so picolibc's versions are duplicate definitions rather than
# fallbacks. Work out which members they live in instead of naming them: the set
# follows from what the agent defines, and picolibc moves things between releases.
LIBC := $(AGENT_TARGET_DIR)/libc-trimmed.a
ENTRY_POINTS := frida_agent_start frida_agent_stop \
gum_interceptor_obtain gum_interceptor_begin_transaction \
gum_interceptor_end_transaction gum_interceptor_replace \
gum_interceptor_revert
KEEP_GLOBAL := $(foreach s,$(ENTRY_POINTS),--keep-global-symbol=$(s))
# Two structures the reparenting path reaches into lost a member upstream, and
# neither change announces itself as anything frida-kmod.c could test for. The
# headers it is compiled against are the authority on both, so ask them.
FRIDA_KMOD_DEFS := \
$(if $(shell grep -l 'list_head[[:space:]]*thread_group;' \
$(KDIR)/include/linux/sched.h),-DFRIDA_HAVE_TASK_THREAD_GROUP) \
$(if $(shell grep -l 'spinlock_t[[:space:]]*lock;' \
$(KDIR)/include/linux/fs_struct.h),-DFRIDA_HAVE_FS_STRUCT_LOCK)
all: modules
# A module that dies in its init path is left wedged under its own name until the
# next boot, so bring-up wants a fresh one per attempt.
MODULE_NAME ?= frida-agent
modules: $(PRELINKED)
$(MAKE) -C $(KDIR) $(if $(KOUT),O=$(KOUT)) M=$(CURDIR) ARCH=$(ARCH) \
FRIDA_KMOD_DEFS="$(FRIDA_KMOD_DEFS)" MODULE_NAME=$(MODULE_NAME) modules
$(PRELINKED): $(AGENT_LIB_SOFTFLOAT) $(LIBC)
$(AGENT_LD) -r $(if $(AGENT_LD_EMULATION),-m $(AGENT_LD_EMULATION)) -o $@.tmp \
--whole-archive $(AGENT_LIB_SOFTFLOAT) --no-whole-archive \
--start-group \
$(GUMJS_DEVKIT_DIR)/libfrida-gumjs.a \
$(LIBC) \
$(LIBM) \
$(LIBBUILTINS) \
--end-group
$(AGENT_OBJCOPY) $(KEEP_GLOBAL) --strip-debug \
--remove-section=.llvmbc --remove-section=.llvmcmd $@.tmp $@
rm -f $@.tmp
$(AGENT_LIB_SOFTFLOAT): agent
cp $(AGENT_LIB) $@
$(DROP_HARDFLOAT_BUILTINS)
$(LIBC): export LC_ALL = C
$(LIBC): $(AGENT_LIB_SOFTFLOAT)
cp $(FRIDA_SDK)/lib/libc.a $@
$(AGENT_NM) --defined-only $(AGENT_LIB_SOFTFLOAT) \
| awk 'NF == 3 && $$2 ~ /^[TDBR]$$/ { print $$3 }' | sort -u > $@.agent-syms
$(AGENT_NM) --print-armap $@ | sed -n 's/^\([^ ]*\) in \(.*\)$$/\1 \2/p' | sort -u > $@.members
$(AGENT_AR) d $@ $$(join $@.agent-syms $@.members | awk '{ print $$2 }' | sort -u)
$(AGENT_AR) s $@
rm -f $@.agent-syms $@.members
agent:
cd $(AGENT_DIR) && RUSTC_BOOTSTRAP=1 RUSTFLAGS="$(AGENT_RUSTFLAGS)" \
CC_$(subst -,_,$(RUST_TARGET))="$(AGENT_CC) $(AGENT_CFLAGS)" \
cargo build --$(PROFILE) \
$(AGENT_TARGET_FLAGS) --features linux \
--target-dir $(AGENT_TARGET_DIR) $(AGENT_BUILD_STD)
clean:
$(MAKE) -C $(KDIR) $(if $(KOUT),O=$(KOUT)) M=$(CURDIR) ARCH=$(ARCH) clean
rm -f $(PRELINKED) $(PRELINKED).tmp $(AGENT_LIB_SOFTFLOAT) $(LIBC)
.PHONY: all modules agent clean
+291
View File
@@ -0,0 +1,291 @@
# Linux kernel module
The same agent that the Barebone backend injects into XNU from the outside, but
loaded from the inside as an ordinary kernel module.
## How it differs from the XNU flavour
| | XNU | Linux |
|-------------------|--------------------------------------------------|-------------------------------------------|
| Delivery | remote stub injects a freestanding ELF | `insmod frida-agent.ko` |
| Kernel primitives | addresses patched into `.kernel_addrs` by the host | direct calls into `frida-kmod.c` |
| Bootstrap config | GVariant blob written into guest memory | module parameters |
| Transport | virtio hostlink or vsock | `/dev/frida` character device |
| Page permissions | host rewrites descriptors over the hostlink | `set_memory_*()` |
| Writable alias | shadow copy, committed via physical-memory bridge | `vmap()` of the same pages |
| Symbols | table supplied by the host | kallsyms |
Because the Linux agent performs its own memory work, it never issues the
`RemapWritablePages`, `MemoryProtect` or `PatchCode` host RPCs. The wire protocol
is otherwise identical, so the host side sees the same `(yqv)` frames.
## Building
Four inputs: this repository, a GumJS devkit built for `none-<arch>-softfloat`, the
soft-float SDK it was built against, and the kernel the module is for. `Makefile`
covers x86_64 and arm64, and defaults to the architecture of the machine building.
Building for the running kernel is a good deal shorter than cross-building for a
device, so that walkthrough comes first; the arm64 one below it is the same steps
with the kernel obtained from elsewhere.
### Toolchain
rustup target add x86_64-unknown-none # or aarch64-unknown-none
rustup component add rust-src
sudo dnf install clang binutils kernel-devel-$(uname -r)
clang must be 19 or newer, since that is where `-mabi=aapcs-soft` landed; GCC does
not implement it at any version, nor does it reach x86_64's soft-float ABI. The
binutils are for the prelink, which reads the archive map that only GNU `nm` prints —
the LLVM one does not; cross-building wants the target's, natively the host's own
will do. The host compiler is for Cargo's own build scripts, which run on the machine
doing the building.
The commands below are run from the top of this repository, with `releng` checked
out — `git submodule update --init releng` if the clone did not recurse.
### For the running x86_64 kernel
No prebuilt bundles are published for `none-x86_64-softfloat`, so both inputs are
built locally, against the same SDK:
releng/deps.py build --bundle=sdk --host=none-x86_64-softfloat
mkdir -p ~/sdk-none-x86_64-softfloat
tar -C ~/sdk-none-x86_64-softfloat -xf deps/sdk-none-x86_64-softfloat.tar.xz
gum=subprojects/frida-gum
mkdir -p $gum/build/none-x86_64-softfloat
(cd $gum/build/none-x86_64-softfloat \
&& FRIDA_DEPS=$PWD/../../../../deps ../../configure \
--host=none-x86_64-softfloat \
--enable-gumjs \
--with-devkits=gumjs \
--with-devkit-symbol-scope=original \
&& make)
make -C src/barebone/agent/linux \
FRIDA_SDK=$HOME/sdk-none-x86_64-softfloat \
GUMJS_DEVKIT_DIR=$PWD/$gum/build/none-x86_64-softfloat/bindings/gumjs/devkit
Everything else — `KDIR`, the Rust target, the binutils, the kernel's own name for the
architecture — follows from the running kernel.
### For an arm64 device
Here there are published bundles to start from, and the kernel has to be assembled.
#### The devkit and the SDK
version=17.17.0
base=https://github.com/frida/frida/releases/download/$version
curl -LO $base/frida-gumjs-devkit-$version-none-arm64-softfloat.tar.xz
mkdir -p ~/gumjs-devkit
tar -C ~/gumjs-devkit -xf frida-gumjs-devkit-$version-none-arm64-softfloat.tar.xz
releng/deps.py sync sdk none-arm64-softfloat ~/sdk-none-arm64-softfloat
The SDK carries picolibc and the compiler-rt builtins the devkit expects to be linked
against, and `releng/deps.toml` pins which one. Mixing a devkit with an SDK built for
a different libc leaves undefined symbols at prelink time.
#### The kernel
Nothing here needs the device's own kernel to be built, but the config alone is not
enough either. `uname -r` ends in the GKI commit and the build number:
adb shell uname -r
6.1.145-android14-11-gc1de4747ac59-ab14219743
which is commit `c1de4747ac59` and build `14219743`. Three pieces are keyed to them:
commit=c1de4747ac59
build=14219743
ci=https://ci.android.com/builds/submitted/$build/kernel_aarch64/latest/raw
mkdir -p ~/kernel-prepared ~/kernel-source
curl -sSL $ci/modules_prepare_outdir.tar.gz | tar -xz -C ~/kernel-prepared
curl -sSL $ci/kernel_aarch64_Module.symvers -o ~/kernel-prepared/Module.symvers
curl -sSL https://android.googlesource.com/kernel/common/+archive/$commit.tar.gz \
| tar -xz -C ~/kernel-source
`modules_prepare_outdir.tar.gz` is the configured build tree with `scripts/` already
compiled as x86-64 binaries, so the kbuild half has to run on an x86-64 Linux host —
an arm64 one gets as far as `CC [M]` and then fails to run `fixdep`. `Module.symvers`
carries the CRCs `CONFIG_MODVERSIONS` checks. The top-level `Makefile` in that prepared tree is a stub
that includes the source's, hence both `KDIR` and `KOUT` below.
A different GKI build of the same branch does not substitute for these: vermagic is
compared verbatim, and it names the exact build.
#### Build
make -C src/barebone/agent/linux \
FRIDA_SDK=$HOME/sdk-none-arm64-softfloat \
GUMJS_DEVKIT_DIR=$HOME/gumjs-devkit \
AGENT_LD=aarch64-linux-gnu-ld \
AGENT_AR=aarch64-linux-gnu-ar \
AGENT_NM=aarch64-linux-gnu-nm \
AGENT_OBJCOPY=aarch64-linux-gnu-objcopy \
KDIR=$HOME/kernel-source \
KOUT=$HOME/kernel-prepared \
LLVM=1
`make` builds the Rust staticlib itself, prelinks it against the devkit and the SDK,
then hands the result to kbuild. The `AGENT_*` overrides are only needed because the
defaults name the `aarch64-none-elf-` toolchain the XNU flavour uses; drop them if
that is what is installed.
The prelink half runs anywhere; the kbuild half needs the x86-64 Linux host above.
Cross-building from macOS means running that step in a container holding the kernel
tree.
adb push src/barebone/agent/linux/frida-agent.ko /data/local/tmp/
## Loading
insmod frida-agent.ko
The module's init path returns as soon as the worker thread is running — watch
`dmesg` for `frida: listening on /dev/frida`. `rmmod frida-agent` unwinds in the
reverse order and does not return until the worker is off the module's text.
## Talking to it
The agent exposes `/dev/frida` and waits: one client at a time, `read`/`write` of the
same length-prefixed frames the other transports carry, with `poll` for readiness.
It never dials out, so there is no network namespace to get right and no ordering
requirement between loading the module and attaching a client.
On Android the node is created by devtmpfs without a policy-specific label, so an
SELinux-enforcing system needs the client's domain granted access to it before it can
open the device. A socket-based transport was the other option considered and is a
worse fit: `AF_VSOCK` only reaches a peer when a hypervisor is on the other side, and
having the kernel connect out to loopback needs something already listening before
the module loads.
### Reaching it from a host
Serve the Barebone device from a frida-server running on the target, so the `open()`
happens next to the device node and the host talks to an ordinary frida-server:
FRIDA_BAREBONE_CONFIG=/data/local/tmp/linux-kmod.json \
frida-server --device barebone -l 127.0.0.1:27042
`etc/linux-kmod.json` is that config. From the host, with `adb forward tcp:27042
tcp:27042` in place:
frida -H 127.0.0.1:27042 -p 0
## Injecting into userspace
Loaded on a live system the module doubles as a ptrace-free injector for ordinary
processes — how the Linux local backend reaches its targets when the module is
present, falling back to `ptrace` when it is not. There is no second device node;
the channel is `prctl()` with a magic option and a token in the fifth argument,
prctl(0x46524944, op, arg1, arg2, 0x1d5f9e6b2c7a4038)
and any call whose option or token does not match falls through to the real
prctl, so probing for the module looks like a stock kernel. `op` 32 pings and
returns the magic.
Allocate, free, read, write and spawn — the primitives the injector drives
against a target pid — require `CAP_SYS_ADMIN`. A spawned thread is a real
`CLONE_THREAD` sibling that adopts the target's mm, files, credentials,
namespaces, fs and SysV semaphore undo list, so it is a native member of the
group rather than a kernel thread wearing its address space.
Cloaking is Gum's own registry mirrored into the kernel so it holds against
`/proc` too: the module intercepts the `maps`, `smaps`, task and status readers
and drops cloaked threads, ranges and fds while keeping the counts consistent. A
process cloaks its own resources with the token alone — no capability — so an
injected agent self-cloaks by forwarding Gum's updates over the same channel,
and the loader adds the footprint the agent itself never sees. A cloaked thread
still sees through the cloak when it reads `/proc`, so the agent is not blind to
its own process.
## What the target kernel has to provide
- **x86_64 or arm64.** The register-level pieces — the page size, cache maintenance,
where the syscall ABI leaves prctl()'s arguments, and how a spawned thread's
registers are set up for its return to userspace — have a half for each. The XNU
flavour remains arm64-only.
- **`CONFIG_KPROBES` and `CONFIG_KALLSYMS_ALL`.** A good deal of what the shim
needs is compiled in but not exported — `kallsyms_lookup_name`,
`kallsyms_on_each_symbol` and `set_memory_*` all are, and GKI trims its export
table to the KMI symbol list on top of that. The shim resolves them by name
through a kprobe, which is the standard way back in. Without kallsyms the module
still loads but cannot look up symbols or change page permissions.
- **Unsigned modules.** `CONFIG_MODULE_SIG_FORCE` must be off. Android's
`CONFIG_MODULE_SIG_PROTECT` is fine: it only demands signatures for modules on
the GKI list.
## Kernel ABI constraints
A hardened kernel imposes an ABI on anything linked into it, and the FP constraint
below is why this flavour is built against a soft-float SDK
(`none-arm64-softfloat`, `none-x86_64-softfloat`) rather than an ordinary bare-metal
one. The remaining constraints are per-architecture.
- **FP/SIMD.** JavaScript numbers are doubles, so a hardfloat build would use FP
for as long as the runtime lives, and neither architecture lets a kernel thread
hold it for free — x86 wants every stretch bracketed by `kernel_fpu_begin()`, and
on arm64 kernels before 6.9 there is no correct way to hold it at all:
`kernel_neon_begin()` runs with preemption disabled so we cannot sleep under it,
and the scheduler does not save a kernel thread's FP registers — live values are
lost on preemption and the FP state of whichever user task was interrupted is
corrupted. Building everything soft-float removes the question rather than
answering it: `-mabi=aapcs-soft -mgeneral-regs-only` on arm64, and on x86_64
LLVM's `+soft-float`, which is also what Rust's own `x86_64-unknown-none` turns
on, so the two halves agree on where a double travels. `-mno-sse` alone does not
reach the ABI; and `long double` has to be narrowed to 64 bits with
`-mlong-double-64`, since soft-float has no lowering for x87's 80-bit format and
LLVM crashes rather than calling out to a builtin.
- **Indirect branch tracking (x86_64).** With `CONFIG_X86_KERNEL_IBT` an indirect
call landing on anything but an `endbr64` faults, so everything linked in is built
`-fcf-protection=branch` and the Rust half `-Zcf-protection=branch`.
- **Kernel code model and red zone (x86_64).** Modules are loaded into the top 2GB
of the address space and their relocations are resolved as if by `-mcmodel=kernel`,
and there is no red zone below the stack pointer for an interrupt to step on. Both
come with the Rust target; the C half is told explicitly.
- **objtool (x86_64).** It validates every object entering a module and builds its
ORC unwind tables, and was not written to read a JavaScript runtime linked against
a freestanding libc. `Kbuild` marks the prelinked blob `OBJECT_FILES_NON_STANDARD`.
- **Shadow call stack (arm64).** With `CONFIG_SHADOW_CALL_STACK` the kernel reserves x18, so
the SDK and Gum are built `-ffixed-x18` and the Rust half with `-Zfixed-x18`.
Anything linked in that uses x18 as a scratch register overwrites the kernel's SCS
pointer, and the thread then returns through whatever that address happens to hold.
The Rust flag only reaches crates Cargo compiles, so `core`, `alloc` and
`compiler_builtins` — which the toolchain ships prebuilt, and which between them
account for every x18 write that used to reach the module — are rebuilt from source
via `-Z build-std`. That needs `rustup component add rust-src`.
## The soft-float libc
The libc is picolibc and the compiler runtime is compiler-rt, both from the
soft-float SDK that `FRIDA_SDK` points at. `Makefile` trims a copy of its
`libc.a` before the prelink: the agent implements `malloc`, the C library's locks and
its process stubs over the kernel's own facilities, so picolibc's are duplicate
definitions rather than fallbacks. The set is derived from what the agent defines
rather than listed, since picolibc moves things between releases.
On arm64 those compiler-rt builtins are load-bearing in a way that is easy to miss.
Rust's toolchain carries its own `compiler_builtins`, compiled for the ordinary AAPCS
where a double travels in `d0`, and the prelink whole-archives the Rust staticlib — so
without the filtering step in `Makefile` those definitions answer the C library's calls
to `__muldf3` and friends, which pass the same double in `x0`. Nothing crashes. The
arithmetic simply returns garbage, and the first thing to notice is GLib deciding a
hash table need not grow. x86_64 needs none of that filtering: soft-float is part of
the Rust target there, so the copy `-Z build-std` compiles already agrees with the C
half.
## What a script may look like
The runtime compiles scripts on a kernel thread's stack, and QuickJS parses
expressions by recursion, so the ceiling is nesting rather than length: roughly
fourteen levels of parentheses or fifteen of object literals on arm64 Linux, whose
kernel stack is 16 KiB — as is x86_64's. A script that exceeds it fails to compile rather than taking
anything down, though the message is not always `stack overflow` — an over-deep
`if`/`else` chain reports `expecting ';'`. Long scripts are fine; deep ones are not.
File diff suppressed because it is too large. Load diff
+7 -9
View File
@@ -1,7 +1,7 @@
use core::ffi::c_void;
use crate::bindings::{gpointer, gsize, GumPageProtection};
use crate::{gum, libc, xnu};
use crate::bindings::{gpointer, gsize, gum_try_mprotect, GumPageProtection};
use crate::{kernel, libc};
const GUM_PAGE_READ: GumPageProtection = 1;
const GUM_PAGE_EXECUTE: GumPageProtection = 4;
@@ -10,7 +10,7 @@ const GUM_PAGE_EXECUTE: GumPageProtection = 4;
// static trampoline `text` made executable, followed by a writable parameter
// table `map_size` bytes later (the trampolines reach their parameters via that
// fixed offset). The two halves are whole pages, so flipping the first to RX
// over the hostlink leaves the parameter table writable.
// leaves the parameter table writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_tramp_embedder_map(
text: *const c_void,
@@ -19,7 +19,7 @@ pub unsafe extern "C" fn ffi_tramp_embedder_map(
parm_table: *mut *mut c_void,
) -> i32 {
unsafe {
let base = xnu::kalloc(map_size * 2);
let base = kernel::alloc_code(map_size * 2);
if base.is_null() {
return 0;
}
@@ -28,9 +28,9 @@ pub unsafe extern "C" fn ffi_tramp_embedder_map(
libc::__clear_cache(base, base.add(map_size));
let made_executable =
gum::gum_try_mprotect(base as gpointer, map_size as gsize, GUM_PAGE_READ | GUM_PAGE_EXECUTE);
gum_try_mprotect(base as gpointer, map_size as gsize, GUM_PAGE_READ | GUM_PAGE_EXECUTE);
if made_executable == 0 {
xnu::free(base, map_size * 2);
kernel::free_code(base, map_size * 2);
return 0;
}
@@ -46,7 +46,5 @@ pub unsafe extern "C" fn ffi_tramp_embedder_unmap(
_parm_table: *mut c_void,
map_size: usize,
) {
unsafe {
xnu::free(code_table as *mut u8, map_size * 2);
}
kernel::free_code(code_table as *mut u8, map_size * 2);
}
+19 -22
View File
@@ -1,7 +1,7 @@
use core::ptr;
use crate::bindings::{g_wait_is_set, gint64, gpointer};
use crate::xnu;
use crate::kernel;
const G_WAIT_INFINITE: gint64 = -1;
@@ -9,38 +9,35 @@ pub static mut WAKEUP_TOKEN: u64 = 0;
#[unsafe(no_mangle)]
pub extern "C" fn g_get_monotonic_time() -> gint64 {
let abstime = xnu::mach_absolute_time();
let time = xnu::absolutetime_to_nanoseconds(abstime);
(time / 1000) as gint64
kernel::monotonic_micros()
}
#[unsafe(no_mangle)]
pub extern "C" fn g_wait_sleep(token: gpointer, timeout_us: gint64) {
let wait_event = ptr::addr_of_mut!(WAKEUP_TOKEN) as *const u8;
let wait_result = if timeout_us == G_WAIT_INFINITE {
xnu::assert_wait(wait_event, xnu::THREAD_INTERRUPTIBLE)
let timeout = if timeout_us == G_WAIT_INFINITE {
None
} else {
xnu::assert_wait_timeout(
wait_event,
xnu::THREAD_INTERRUPTIBLE,
(timeout_us * 1000) as u32,
1, // TODO
)
Some(timeout_us as u64)
};
if wait_result != xnu::THREAD_WAITING {
panic!("assert_wait_timeout failed: {}", wait_result);
}
if unsafe { g_wait_is_set(token) != 0 } {
xnu::thread_wakeup(wait_event);
return;
}
xnu::thread_block(None);
kernel::wait(wait_event, timeout, &mut || unsafe {
g_wait_is_set(token) != 0
});
}
#[unsafe(no_mangle)]
pub extern "C" fn g_wait_wake(_token: gpointer) {
xnu::thread_wakeup(ptr::addr_of_mut!(WAKEUP_TOKEN) as *const u8);
kernel::wake(ptr::addr_of_mut!(WAKEUP_TOKEN) as *const u8);
}
#[unsafe(no_mangle)]
pub extern "C" fn g_io_channel_unix_new(_fd: i32) -> gpointer {
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn g_spawn_async_with_pipes() -> i32 {
0
}
+104 -76
View File
@@ -8,34 +8,39 @@ use crate::bindings::GThreadFunc;
use crate::bindings::{gpointer, gulong};
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use core::arch::asm;
use core::ffi::c_void;
use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use core::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize, Ordering};
// Our implementation structures that overlay the opaque GLib structures
#[repr(C, align(8))]
#[cfg_attr(target_pointer_width = "32", repr(C, align(4)))]
#[cfg_attr(target_pointer_width = "64", repr(C, align(8)))]
struct MutexImpl {
lock: AtomicU32,
_padding: [u8; core::mem::size_of::<GMutex>() - core::mem::size_of::<AtomicU32>()],
}
#[repr(C, align(8))]
#[cfg_attr(target_pointer_width = "32", repr(C, align(4)))]
#[cfg_attr(target_pointer_width = "64", repr(C, align(8)))]
struct RecMutexImpl {
owner: AtomicU64, // Thread ID of the owner (uses the 'p' field)
owner: AtomicUsize, // Thread ID of the owner (uses the 'p' field)
count: AtomicU32, // Recursion count (uses i[0])
_unused: u32, // Unused (uses i[1])
}
#[repr(C, align(8))]
#[cfg_attr(target_pointer_width = "32", repr(C, align(4)))]
#[cfg_attr(target_pointer_width = "64", repr(C, align(8)))]
struct RWLockImpl {
state: AtomicU32, // Lock state (readers count + writer bit)
_padding: [u8; core::mem::size_of::<GRWLock>() - core::mem::size_of::<AtomicU32>()],
}
#[repr(C, align(8))]
// A generation counter rather than a flag: a flag stays set once signalled, so every
// later wait returns at once and the caller's loop spins instead of blocking.
#[cfg_attr(target_pointer_width = "32", repr(C, align(4)))]
#[cfg_attr(target_pointer_width = "64", repr(C, align(8)))]
struct CondImpl {
signal: AtomicU32, // Signal state
generation: AtomicU32,
_padding: [u8; core::mem::size_of::<GCond>() - core::mem::size_of::<AtomicU32>()],
}
@@ -144,10 +149,9 @@ pub extern "C" fn g_rec_mutex_clear(_rec_mutex: *mut GRecMutex) {
pub extern "C" fn g_rec_mutex_lock(rec_mutex: *mut GRecMutex) {
unsafe {
let impl_ = rec_mutex_impl(rec_mutex);
let current_thread = get_current_thread_id();
loop {
if rec_mutex_try_acquire(impl_, current_thread) {
if rec_mutex_try_acquire(impl_) {
break;
}
rec_mutex_wait(impl_);
@@ -159,9 +163,8 @@ pub extern "C" fn g_rec_mutex_lock(rec_mutex: *mut GRecMutex) {
pub extern "C" fn g_rec_mutex_trylock(rec_mutex: *mut GRecMutex) -> u32 {
unsafe {
let impl_ = rec_mutex_impl(rec_mutex);
let current_thread = get_current_thread_id();
if rec_mutex_try_acquire(impl_, current_thread) {
if rec_mutex_try_acquire(impl_) {
1
} else {
0
@@ -183,7 +186,7 @@ pub extern "C" fn g_rec_mutex_unlock(rec_mutex: *mut GRecMutex) {
.is_ok()
{
impl_.owner.store(0, Ordering::Relaxed);
lock_notify_all();
wake_waiters(&impl_.count);
break;
}
} else {
@@ -198,7 +201,10 @@ pub extern "C" fn g_rec_mutex_unlock(rec_mutex: *mut GRecMutex) {
}
}
unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl, current_thread: u64) -> bool {
// Asking the kernel who we are costs more than the uncontended acquisition itself, and
// every allocation takes this path, so the identity is only fetched once the lock turns
// out to be held.
unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl) -> bool {
let current_count = impl_.count.load(Ordering::Relaxed);
if current_count == 0 {
@@ -206,12 +212,12 @@ unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl, current_thread: u64) -
.compare_exchange_weak(0, 1, Ordering::Acquire, Ordering::Relaxed)
.is_ok()
{
impl_.owner.store(current_thread, Ordering::Relaxed);
impl_.owner.store(crate::kernel::current_thread_id() as usize, Ordering::Relaxed);
return true;
}
} else {
let current_owner = impl_.owner.load(Ordering::Relaxed);
if current_owner == current_thread {
if current_owner == crate::kernel::current_thread_id() as usize {
if impl_.count
.compare_exchange_weak(
current_count,
@@ -230,7 +236,7 @@ unsafe fn rec_mutex_try_acquire(impl_: &mut RecMutexImpl, current_thread: u64) -
unsafe fn rec_mutex_wait(impl_: &mut RecMutexImpl) {
while impl_.count.load(Ordering::Relaxed) != 0 {
unsafe { lock_wait_primitive() };
wait_on(&impl_.count, &mut || impl_.count.load(Ordering::Relaxed) == 0);
}
}
@@ -282,7 +288,7 @@ pub extern "C" fn g_rw_lock_writer_unlock(rw_lock: *mut GRWLock) {
unsafe {
let impl_ = rw_lock_impl(rw_lock);
impl_.state.store(0, Ordering::Release);
lock_notify_all();
wake_waiters(&impl_.state);
}
}
@@ -329,7 +335,7 @@ pub extern "C" fn g_rw_lock_reader_unlock(rw_lock: *mut GRWLock) {
.is_ok()
{
if current - 1 == 0 {
lock_notify_all();
wake_waiters(&impl_.state);
}
break;
}
@@ -355,13 +361,15 @@ unsafe fn rw_lock_reader_try_acquire(impl_: &mut RWLockImpl) -> bool {
unsafe fn rw_lock_wait_for_writers(impl_: &mut RWLockImpl) {
while impl_.state.load(Ordering::Relaxed) & RW_WRITER_LOCK_BIT != 0 {
unsafe { lock_wait_primitive() };
wait_on(&impl_.state, &mut || {
impl_.state.load(Ordering::Relaxed) & RW_WRITER_LOCK_BIT == 0
});
}
}
unsafe fn rw_lock_wait_for_all(impl_: &mut RWLockImpl) {
while impl_.state.load(Ordering::Relaxed) != 0 {
unsafe { lock_wait_primitive() };
wait_on(&impl_.state, &mut || impl_.state.load(Ordering::Relaxed) == 0);
}
}
@@ -369,7 +377,7 @@ unsafe fn rw_lock_wait_for_all(impl_: &mut RWLockImpl) {
pub extern "C" fn g_cond_init(cond: *mut GCond) {
unsafe {
let impl_ = cond_impl(cond);
impl_.signal.store(0, Ordering::Relaxed);
impl_.generation.store(0, Ordering::Relaxed);
}
}
@@ -379,11 +387,17 @@ pub extern "C" fn g_cond_clear(_cond: *mut GCond) {}
#[unsafe(no_mangle)]
pub extern "C" fn g_cond_wait(cond: *mut GCond, mutex: *mut GMutex) {
unsafe {
// Read the generation under the mutex, so a signal that lands after the
// unlock below is still observed as a change.
let seen = cond_impl(cond).generation.load(Ordering::Acquire);
g_mutex_unlock(mutex);
let impl_ = cond_impl(cond);
while impl_.signal.load(Ordering::Acquire) == 0 {
lock_wait_primitive();
while impl_.generation.load(Ordering::Acquire) == seen {
wait_on(&impl_.generation, &mut || {
impl_.generation.load(Ordering::Acquire) != seen
});
}
g_mutex_lock(mutex);
@@ -394,8 +408,8 @@ pub extern "C" fn g_cond_wait(cond: *mut GCond, mutex: *mut GMutex) {
pub extern "C" fn g_cond_signal(cond: *mut GCond) {
unsafe {
let impl_ = cond_impl(cond);
impl_.signal.store(1, Ordering::Release);
lock_notify_all();
impl_.generation.fetch_add(1, Ordering::Release);
wake_waiters(&impl_.generation);
}
}
@@ -403,22 +417,37 @@ pub extern "C" fn g_cond_signal(cond: *mut GCond) {
pub extern "C" fn g_cond_broadcast(cond: *mut GCond) {
unsafe {
let impl_ = cond_impl(cond);
impl_.signal.store(1, Ordering::Release);
lock_notify_all();
impl_.generation.fetch_add(1, Ordering::Release);
wake_waiters(&impl_.generation);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn g_cond_wait_until(cond: *mut GCond, mutex: *mut GMutex, _end_time: i64) -> u32 {
// TODO: Implement proper timeout handling
g_cond_wait(cond, mutex);
1
pub extern "C" fn g_cond_wait_until(cond: *mut GCond, mutex: *mut GMutex, end_time: i64) -> u32 {
unsafe {
let seen = cond_impl(cond).generation.load(Ordering::Acquire);
g_mutex_unlock(mutex);
let impl_ = cond_impl(cond);
let mut signalled = impl_.generation.load(Ordering::Acquire) != seen;
while !signalled && crate::kernel::monotonic_micros() < end_time {
wait_on(&impl_.generation, &mut || {
impl_.generation.load(Ordering::Acquire) != seen
});
signalled = impl_.generation.load(Ordering::Acquire) != seen;
}
g_mutex_lock(mutex);
if signalled { 1 } else { 0 }
}
}
#[unsafe(no_mangle)]
pub extern "C" fn g_private_get(key: *mut GPrivate) -> *mut c_void {
unsafe {
let thread_id = get_current_thread_id();
let thread_id = crate::kernel::current_thread_id();
lock_acquire(&TLS_LOCK);
ensure_tls_storage();
@@ -438,7 +467,7 @@ pub extern "C" fn g_private_get(key: *mut GPrivate) -> *mut c_void {
#[unsafe(no_mangle)]
pub extern "C" fn g_private_set(key: *mut GPrivate, value: *mut c_void) {
unsafe {
let thread_id = get_current_thread_id();
let thread_id = crate::kernel::current_thread_id();
lock_acquire(&TLS_LOCK);
ensure_tls_storage();
@@ -468,7 +497,9 @@ pub extern "C" fn _g_system_thread_create(
data: gpointer,
) -> *mut GSystemThread {
unsafe {
let system_thread = crate::xnu::kalloc(core::mem::size_of::<SystemThreadImpl>()) as *mut SystemThreadImpl;
crate::kernel::log("frida: glib is creating a thread\n\0");
let system_thread = crate::kernel::alloc(core::mem::size_of::<SystemThreadImpl>()) as *mut SystemThreadImpl;
g_mutex_init(&mut (*system_thread).mutex);
g_cond_init(&mut (*system_thread).cond);
@@ -478,12 +509,12 @@ pub extern "C" fn _g_system_thread_create(
(*system_thread).finished.store(0, Ordering::Relaxed);
(*system_thread).detached.store(0, Ordering::Relaxed);
let wrapper_data = crate::xnu::kalloc(core::mem::size_of::<ThreadWrapperData>()) as *mut ThreadWrapperData;
let wrapper_data = crate::kernel::alloc(core::mem::size_of::<ThreadWrapperData>()) as *mut ThreadWrapperData;
(*wrapper_data).func = func;
(*wrapper_data).data = data;
(*wrapper_data).system_thread = system_thread;
let _xnu_result = crate::xnu::kernel_thread_start(thread_wrapper, wrapper_data as *mut c_void);
let _xnu_result = crate::kernel::spawn_thread(thread_wrapper, wrapper_data as *mut c_void);
system_thread as *mut GSystemThread
}
@@ -496,8 +527,9 @@ extern "C" fn thread_wrapper(parameter: *mut c_void, _wait_result: i32) {
let data = (*wrapper_data).data;
let system_thread = (*wrapper_data).system_thread;
let current_thread_id = get_current_thread_id();
(*system_thread).thread_id.store(current_thread_id, Ordering::Relaxed);
let current_thread_id = crate::kernel::current_thread_id();
(*system_thread).thread_id.store(current_thread_id, Ordering::Release);
wake_waiters(&(*system_thread).thread_id);
func.unwrap()(data);
@@ -537,12 +569,10 @@ pub extern "C" fn _g_system_thread_wait(thread: *mut GSystemThread) {
unsafe {
let system_thread = thread as *mut SystemThreadImpl;
loop {
let thread_id = (*system_thread).thread_id.load(Ordering::Acquire);
if thread_id != 0 {
break;
}
core::hint::spin_loop();
while (*system_thread).thread_id.load(Ordering::Acquire) == 0 {
wait_on(&(*system_thread).thread_id, &mut || {
(*system_thread).thread_id.load(Ordering::Acquire) != 0
});
}
g_mutex_lock(&mut (*system_thread).mutex);
@@ -556,19 +586,16 @@ pub extern "C" fn _g_system_thread_wait(thread: *mut GSystemThread) {
}
unsafe fn cleanup_system_thread(system_thread: *mut SystemThreadImpl) {
crate::xnu::free(system_thread as *mut u8, core::mem::size_of::<SystemThreadImpl>());
crate::kernel::free(system_thread as *mut u8, core::mem::size_of::<SystemThreadImpl>());
}
unsafe fn cleanup_wrapper_data(wrapper_data: *mut ThreadWrapperData) {
crate::xnu::free(wrapper_data as *mut u8, core::mem::size_of::<ThreadWrapperData>());
crate::kernel::free(wrapper_data as *mut u8, core::mem::size_of::<ThreadWrapperData>());
}
unsafe fn lock_acquire(lock: &AtomicU32) {
loop {
if unsafe { lock_try_acquire(lock) } {
break;
}
unsafe { lock_wait(lock) };
while !unsafe { lock_try_acquire(lock) } {
wait_on(lock, &mut || lock.load(Ordering::Relaxed) == 0);
}
}
@@ -579,35 +606,36 @@ unsafe fn lock_try_acquire(lock: &AtomicU32) -> bool {
unsafe fn lock_release(lock: &AtomicU32) {
lock.store(0, Ordering::Release);
unsafe { lock_notify_all() };
wake_waiters(lock);
}
unsafe fn lock_wait(lock: &AtomicU32) {
while lock.load(Ordering::Relaxed) != 0 {
unsafe { lock_wait_primitive() };
}
// Sleeps until `ready` holds or the slice expires, whichever comes first.
//
// These used to spin on wfe/sev. A kernel thread that spins never reaches the
// scheduler, so a lost wakeup — or a genuine deadlock, which is how we found this —
// starves the CPU it is on until the hardware watchdog takes the machine down. The
// bounded sleep turns both into a stall that can be observed instead.
//
// The address of the word being waited on names the wait channel: XNU pairs
// assert_wait()/thread_wakeup() on it directly, while the Linux shim has a single
// channel and treats it as a hint. Every caller re-checks its own condition in a
// loop, so a spurious wakeup costs nothing either way.
fn wait_on<T>(word: &T, ready: &mut dyn FnMut() -> bool) {
crate::kernel::wait(token_of(word), Some(WAIT_SLICE_US), ready);
// The wait above returns without sleeping whenever `ready` already holds, so on
// its own it is not enough to keep a contended loop off the CPU: every caller
// here loops, and a loop in a kernel thread that never reaches the scheduler
// takes the machine down rather than merely running hot. Yield unconditionally.
crate::kernel::yield_now();
}
unsafe fn lock_wait_primitive() {
unsafe {
asm!("wfe", options(nomem, nostack));
}
fn wake_waiters<T>(word: &T) {
crate::kernel::wake(token_of(word));
}
unsafe fn lock_notify_all() {
unsafe {
asm!("sev", options(nomem, nostack));
}
fn token_of<T>(word: &T) -> *const u8 {
word as *const T as *const u8
}
// The thread pointer is exposed to JavaScript as a GumThreadId, so it must fit
// in a double without losing precision; the low bits keep it unique per thread.
const JS_SAFE_THREAD_ID_MASK: u64 = (1 << 48) - 1;
pub unsafe fn get_current_thread_id() -> u64 {
let thread_ptr: u64;
unsafe {
asm!("mrs {}, tpidr_el1", out(reg) thread_ptr, options(nomem, nostack));
}
thread_ptr & JS_SAFE_THREAD_ID_MASK
}
const WAIT_SLICE_US: u64 = 1000;
+85 -413
View File
@@ -1,87 +1,42 @@
// Gum's platform backend, minus the parts that depend on which kernel we are
// running inside: memory protection, symbols and the module registry live in
// gum_xnu.rs and gum_linux.rs respectively.
use crate::{
bindings::{
_GInterfaceInfo, _GTypeInfo, _GumPageProtection_GUM_PAGE_EXECUTE,
_GumPageProtection_GUM_PAGE_READ, _GumPageProtection_GUM_PAGE_WRITE,
_GumRwxSupport_GUM_RWX_NONE, GArray, GObject, GObjectClass, GPrivate, GType,
GumDebugSymbolDetails, GumExportDetails, GumExportType_GUM_EXPORT_FUNCTION,
GumFoundExportFunc, GumMemoryRange, GumModule, GumModuleInterface, GumModuleRegistry,
GumPageProtection, GumRwxSupport, GumThreadId, GumTlsKey, g_array_append_vals, g_array_new,
g_free, g_object_get_type, g_object_new, g_object_unref, g_once_init_enter,
g_once_init_leave, g_strdup, g_type_add_interface_static, g_type_class_peek_parent,
g_type_register_static, g_variant_get_boolean, g_variant_get_uint64, g_variant_new,
g_variant_new_fixed_array, g_variant_type_free, g_variant_type_new, gboolean, gchar,
gconstpointer, gpointer, gsize, guint,
gum_barebone_register_module,
gum_barebone_try_remap_writable_pages as _gum_barebone_try_remap_writable_pages,
g_variant_unref, gum_module_get_type, gum_mprotect, gum_query_page_size,
_GInterfaceInfo, _GTypeInfo, GObject, GObjectClass, GPrivate, GType, GumExportDetails,
GumExportType_GUM_EXPORT_FUNCTION, GumFoundExportFunc, GumMemoryRange, GumModule,
GumModuleInterface, GumThreadId, GumTlsKey, g_free, g_object_get_type, g_object_new,
g_once_init_enter, g_once_init_leave, g_strdup, g_type_add_interface_static,
g_type_class_peek_parent, g_type_register_static, gchar, gpointer, gsize, guint,
},
gthread, host_rpc, libc, FridaCommand,
gthread, kernel, libc,
};
use alloc::boxed::Box;
use alloc::ffi::CString;
use alloc::format;
use alloc::vec::Vec;
use core::ffi::CStr;
use core::mem::size_of;
use core::ptr;
use core::sync::atomic::{AtomicU32, Ordering};
// Code slabs the agent allocates are normal writable RAM, so gum can keep a real writable alias to
// them across the slab's lifetime. Pre-existing kernel/kext text is CTRR-locked and can only be
// modified through the host's physical-memory bridge, so we shadow it and commit on dispose. We
// distinguish the two by tracking the executable ranges we hand out.
static SLAB_LOCK: AtomicU32 = AtomicU32::new(0);
static mut SLABS: Vec<(u64, u64)> = Vec::new();
const SHADOW_MAGIC: u64 = 0x4644_4f48_5341_4853;
const SHADOW_HEADER: usize = 24;
const SHADOW_MIN_ADDRESS: u64 = 0xffff_f000_0000_0000;
fn slab_lock() {
while SLAB_LOCK
.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
core::hint::spin_loop();
}
}
fn slab_unlock() {
SLAB_LOCK.store(0, Ordering::Release);
}
fn register_slab(start: u64, size: usize) {
slab_lock();
unsafe {
(*core::ptr::addr_of_mut!(SLABS)).push((start, start + size as u64));
}
slab_unlock();
}
fn unregister_slab(start: u64) {
slab_lock();
unsafe {
(*core::ptr::addr_of_mut!(SLABS)).retain(|&(begin, _)| begin != start);
}
slab_unlock();
}
fn is_agent_slab(address: u64) -> bool {
slab_lock();
let found = unsafe {
(*core::ptr::addr_of!(SLABS))
.iter()
.any(|&(begin, end)| address >= begin && address < end)
};
slab_unlock();
found
}
#[cfg(feature = "linux")]
use crate::gum_linux::enumerate_exports_in_range;
#[cfg(feature = "win9x")]
use crate::gum_win9x::enumerate_exports_in_range;
#[cfg(feature = "xnu")]
use crate::gum_xnu::enumerate_exports_in_range;
#[unsafe(no_mangle)]
pub extern "C" fn gum_process_get_current_thread_id() -> GumThreadId {
unsafe { gthread::get_current_thread_id() }
kernel::current_thread_id() as GumThreadId
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_query_page_size() -> guint {
4096
}
#[cfg(target_arch = "aarch64")]
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_query_page_size() -> guint {
unsafe {
@@ -99,200 +54,6 @@ pub extern "C" fn gum_barebone_query_page_size() -> guint {
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_query_rwx_support() -> GumRwxSupport {
_GumRwxSupport_GUM_RWX_NONE
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_can_remap_writable() -> gboolean {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_try_remap_writable_pages(
first_page: gpointer,
n_pages: guint,
) -> gpointer {
if is_agent_slab(first_page as u64) {
return remap_agent_pages(first_page, n_pages);
}
shadow_kernel_pages(first_page, n_pages)
}
fn remap_agent_pages(first_page: gpointer, n_pages: guint) -> gpointer {
unsafe {
let page_size = gum_query_page_size() as usize;
let mut virtual_addrs = Vec::with_capacity(n_pages as usize);
let mut current_page = first_page as u64;
for _ in 0..n_pages {
virtual_addrs.push(current_page as gpointer);
current_page += page_size as u64;
}
_gum_barebone_try_remap_writable_pages(
virtual_addrs.as_ptr() as *mut *const core::ffi::c_void,
virtual_addrs.len() as guint,
)
}
}
fn shadow_kernel_pages(first_page: gpointer, n_pages: guint) -> gpointer {
unsafe {
let total = n_pages as usize * gum_query_page_size() as usize;
let buffer = crate::xnu::kalloc(SHADOW_HEADER + total);
*(buffer as *mut u64) = SHADOW_MAGIC;
*(buffer.add(8) as *mut u64) = first_page as u64;
*(buffer.add(16) as *mut u32) = n_pages;
let body = buffer.add(SHADOW_HEADER);
core::ptr::copy_nonoverlapping(first_page as *const u8, body, total);
body as gpointer
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_dispose_writable_pages(writable: gpointer, _n_pages: guint) {
if (writable as u64) < SHADOW_MIN_ADDRESS {
return;
}
unsafe {
let buffer = (writable as *mut u8).sub(SHADOW_HEADER);
if *(buffer as *const u64) != SHADOW_MAGIC {
return;
}
let first_page = *(buffer.add(8) as *const u64);
let n_pages = *(buffer.add(16) as *const u32);
let total = n_pages as usize * gum_query_page_size() as usize;
commit_kernel_patch(first_page, writable as *const u8, total);
libc::__clear_cache(first_page as *const u8, (first_page + total as u64) as *const u8);
crate::xnu::free(buffer, SHADOW_HEADER + total);
}
}
unsafe fn commit_kernel_patch(address: u64, data: *const u8, len: usize) {
unsafe {
let element_type = g_variant_type_new(c"y".as_ptr());
let bytes = g_variant_new_fixed_array(element_type, data as gconstpointer, len as gsize, 1);
g_variant_type_free(element_type);
let payload = g_variant_new(c"(t@ay)".as_ptr(), address, bytes);
let reply = host_rpc(FridaCommand::PatchCode, payload);
g_variant_unref(reply);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_try_remap_writable_pages(
addrs: *const gpointer,
n_addrs: guint,
) -> gpointer {
if !crate::transport_is_up() {
return ptr::null_mut();
}
unsafe {
let element_type = g_variant_type_new(c"t".as_ptr());
let payload = g_variant_new_fixed_array(
element_type,
addrs as gconstpointer,
n_addrs as gsize,
size_of::<u64>() as gsize,
);
g_variant_type_free(element_type);
let reply = host_rpc(FridaCommand::RemapWritablePages, payload);
let virtual_address = g_variant_get_uint64(reply);
g_variant_unref(reply);
virtual_address as gpointer
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_try_mprotect(
address: gpointer,
size: gsize,
prot: GumPageProtection,
) -> gboolean {
if !crate::transport_is_up() {
return 1;
}
unsafe {
let payload = g_variant_new(c"(ttu)".as_ptr(), address as u64, size as u64, prot as u32);
let reply = host_rpc(FridaCommand::MemoryProtect, payload);
let success = g_variant_get_boolean(reply);
g_variant_unref(reply);
if success != 0 {
flush_tlb_range(address as u64, size as u64);
}
success
}
}
// The host rewrites our page-table descriptors through the physical-memory
// bridge, which leaves this CPU's TLB holding the stale translation. Stalker
// flips a slab page RW then RX in place, so without this the freeze never takes
// effect and executing the page faults with a permission abort.
unsafe fn flush_tlb_range(address: u64, size: u64) {
unsafe {
let page_size = gum_query_page_size() as u64;
let start = address & !(page_size - 1);
let end = (address + size + page_size - 1) & !(page_size - 1);
core::arch::asm!("dsb ish", options(nostack, preserves_flags));
let mut va = start;
while va < end {
core::arch::asm!("tlbi vaae1is, {operand}", operand = in(reg) va >> 12,
options(nostack, preserves_flags));
va += page_size;
}
core::arch::asm!("dsb ish", "isb", options(nostack, preserves_flags));
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_allocate(
_address: gpointer,
size: gsize,
_alignment: gsize,
prot: GumPageProtection,
) -> gpointer {
let ptr = crate::xnu::kalloc(size as usize);
unsafe {
core::ptr::write_bytes(ptr, 0, size as usize);
if (prot & _GumPageProtection_GUM_PAGE_EXECUTE) != 0 {
register_slab(ptr as u64, size as usize);
gum_mprotect(ptr as gpointer, size, prot);
}
}
ptr as gpointer
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_free(address: gpointer, size: gsize) -> gboolean {
// Executable slabs were flipped to RX in the page tables; restore RW before returning them to
// the allocator, otherwise the reclaimed pages stay non-writable and the next consumer faults.
if is_agent_slab(address as u64) {
unsafe {
gum_mprotect(
address,
size,
(_GumPageProtection_GUM_PAGE_READ | _GumPageProtection_GUM_PAGE_WRITE)
as GumPageProtection,
);
}
unregister_slab(address as u64);
}
crate::xnu::free(address as *mut u8, size as usize);
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_clear_cache(address: gpointer, size: gsize) {
unsafe {
@@ -324,40 +85,6 @@ pub extern "C" fn gum_tls_key_set_value(key: GumTlsKey, value: gpointer) {
gthread::g_private_set(key as *mut GPrivate, value)
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_on_registry_activating(registry: *mut GumModuleRegistry) {
let kernel_base = crate::xnu::get_kernel_base();
unsafe {
let module_infos = core::ptr::addr_of!(crate::MODULE_INFO);
let module_infos = &*module_infos;
let mut i = 0;
for module_info in module_infos.iter() {
let module_base = kernel_base + module_info.offset as u64;
let module_path = if i == 0 {
"/System/Library/Kernels/kernel"
} else {
&format!(
"/System/Library/Extensions/{}.kext/{}",
module_info.name, module_info.name
)
};
let module_range = GumMemoryRange {
base_address: module_base,
size: module_info.size as u64,
};
let module = gum_native_module_new(&module_path, &module_info.version, &module_range);
gum_barebone_register_module(registry, module);
g_object_unref(module as gpointer);
i += 1;
}
}
}
#[repr(C)]
pub struct GumNativeModule {
parent: GObject,
@@ -405,7 +132,7 @@ fn gum_native_module_get_type() -> GType {
interface_data: core::ptr::null_mut(),
};
g_type_add_interface_static(new_type, gum_module_get_type(), &interface_info);
g_type_add_interface_static(new_type, crate::bindings::gum_module_get_type(), &interface_info);
g_once_init_leave(
core::ptr::addr_of_mut!(GUM_NATIVE_MODULE_TYPE) as *mut ::core::ffi::c_void,
@@ -448,7 +175,11 @@ unsafe extern "C" fn gum_native_module_finalize(object: *mut GObject) {
}
}
fn gum_native_module_new(path: &str, version: &str, range: &GumMemoryRange) -> *mut GumModule {
pub(crate) fn gum_native_module_new(
path: &str,
version: &str,
range: &GumMemoryRange,
) -> *mut GumModule {
unsafe {
let path_cstr = CString::new(path).unwrap();
let version_cstr = CString::new(version).unwrap();
@@ -499,134 +230,75 @@ unsafe extern "C" fn gum_native_module_enumerate_exports(
) {
unsafe {
let module = self_ as *mut GumNativeModule;
let module_range = &(*module).range;
let symbol_table = core::ptr::addr_of!(crate::SYMBOL_TABLE);
let symbol_table = &*symbol_table;
const N_EXT: u8 = 0x01; // External symbol flag
const N_TYPE: u8 = 0x0e; // Type mask
const N_SECT: u8 = 0x0e; // Defined in section
let start_address = module_range.base_address;
let end_address = module_range.base_address + module_range.size;
for symbol_ref in symbol_table.iter_symbols_in_range(start_address, end_address) {
let is_external = (symbol_ref.symbol_type() & N_EXT) != 0;
let is_defined = (symbol_ref.symbol_type() & N_TYPE) == N_SECT;
if !is_external || !is_defined {
continue;
}
let export_type = GumExportType_GUM_EXPORT_FUNCTION;
let range = (*module).range;
let mut on_export = |name: *const gchar, address: u64| {
let export_details = GumExportDetails {
type_: export_type,
name: g_strdup(symbol_ref.name_ptr()),
address: symbol_ref.address(),
type_: GumExportType_GUM_EXPORT_FUNCTION,
name: g_strdup(name),
address,
size: -1,
};
let should_continue =
func.unwrap()(&export_details as *const GumExportDetails, user_data);
if should_continue == 0 {
break;
}
}
func.unwrap()(&export_details as *const GumExportDetails, user_data) != 0
};
enumerate_exports_in_range(
range.base_address,
range.base_address + range.size as u64,
&mut on_export,
);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_symbol_details_from_address(
address: gpointer,
details: *mut GumDebugSymbolDetails,
) -> gboolean {
/// What the backends implement for `gum_native_module_enumerate_exports`: call
/// `callback` for every exported function in `[start, end)`, stopping early when
/// it returns false.
pub(crate) type FoundExportCallback<'a> = dyn FnMut(*const gchar, u64) -> bool + 'a;
// Code slabs the agent allocates are normal writable RAM, so Gum can keep a real writable alias to
// them across the slab's lifetime. Pre-existing kernel text is write-protected — CTRR-locked on
// Apple silicon, STRICT_KERNEL_RWX on Linux — and can only be modified through a separate path, so
// we treat it differently. We distinguish the two by tracking the executable ranges we hand out.
static SLAB_LOCK: AtomicU32 = AtomicU32::new(0);
static mut SLABS: Vec<(u64, u64)> = Vec::new();
fn slab_lock() {
while SLAB_LOCK
.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
kernel::yield_now();
}
}
fn slab_unlock() {
SLAB_LOCK.store(0, Ordering::Release);
}
pub(crate) fn register_slab(start: u64, size: usize) {
slab_lock();
unsafe {
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
if let Some(symbol) = table.find_symbol_by_address(address as u64) {
let name_bytes = symbol.name().as_bytes();
let copy_len = core::cmp::min(name_bytes.len(), 2048);
core::ptr::copy_nonoverlapping(
name_bytes.as_ptr(),
(*details).symbol_name.as_mut_ptr() as *mut u8,
copy_len,
);
(*details).symbol_name[copy_len] = 0;
(*details).address = symbol.address();
(*details).module_name[0] = 0;
(*details).file_name[0] = 0;
(*details).line_number = 0;
(*details).column = 0;
return 1;
}
0
(*core::ptr::addr_of_mut!(SLABS)).push((start, start + size as u64));
}
slab_unlock();
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_symbol_name_from_address(address: gpointer) -> *mut gchar {
pub(crate) fn unregister_slab(start: u64) {
slab_lock();
unsafe {
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
let name_ptr = table.find_symbol_name_ptr_by_address(address as u64);
g_strdup(name_ptr as *const gchar)
(*core::ptr::addr_of_mut!(SLABS)).retain(|&(begin, _)| begin != start);
}
slab_unlock();
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_function(name: *const gchar) -> gpointer {
unsafe {
let target_name = CStr::from_ptr(name).to_string_lossy();
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
if let Some(symbol) = table.find_symbol_by_name(&target_name) {
return symbol.address() as gpointer;
}
core::ptr::null_mut()
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_functions_named(name: *const gchar) -> *mut GArray {
unsafe {
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
let target_name = CStr::from_ptr(name).to_string_lossy();
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
let symbols = table.find_symbols_by_name(&target_name);
for symbol in symbols {
let addr = symbol.address() as gpointer;
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
}
array
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_functions_matching(pattern: *const gchar) -> *mut GArray {
unsafe {
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
let glob_pattern = CStr::from_ptr(pattern).to_string_lossy();
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
let symbols = table.find_symbols_matching_glob(&glob_pattern);
for symbol in symbols {
let addr = symbol.address() as gpointer;
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
}
array
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_load_symbols(_path: *const gchar) -> gboolean {
0
pub(crate) fn is_agent_slab(address: u64) -> bool {
slab_lock();
let found = unsafe {
(*core::ptr::addr_of!(SLABS))
.iter()
.any(|&(begin, end)| address >= begin && address < end)
};
slab_unlock();
found
}
+226
View File
@@ -0,0 +1,226 @@
// Linux half of Gum's platform backend. Everything is done locally: we are the
// kernel, so page permissions go through set_memory_*(), writable aliases of
// write-protected text come from vmap(), and symbols come from kallsyms.
use crate::{
bindings::{
_GumPageProtection_GUM_PAGE_EXECUTE, _GumPageProtection_GUM_PAGE_READ,
_GumPageProtection_GUM_PAGE_WRITE, _GumRwxSupport_GUM_RWX_NONE, GArray,
GumDebugSymbolDetails, GumMemoryRange, GumModuleRegistry, GumPageProtection, GumRwxSupport,
g_array_append_vals, g_array_new, g_object_unref, g_pattern_spec_free, g_pattern_spec_match_string,
g_pattern_spec_new, g_strdup, gboolean, gchar, gconstpointer, gpointer, gsize, guint,
gum_barebone_register_module, gum_mprotect,
},
gum::{self, FoundExportCallback},
kernel,
};
use alloc::format;
use core::ffi::CStr;
use core::ptr;
#[unsafe(no_mangle)]
pub extern "C" fn gum_query_rwx_support() -> GumRwxSupport {
_GumRwxSupport_GUM_RWX_NONE
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_can_remap_writable() -> gboolean {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_try_remap_writable_pages(
first_page: gpointer,
n_pages: guint,
) -> gpointer {
kernel::remap_writable(first_page as u64, n_pages) as gpointer
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_dispose_writable_pages(writable: gpointer, _n_pages: guint) {
kernel::unmap_writable(writable as *mut core::ffi::c_void);
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_try_mprotect(
address: gpointer,
size: gsize,
prot: GumPageProtection,
) -> gboolean {
if kernel::protect(address as u64, size as usize, prot as u32) {
1
} else {
0
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_allocate(
_address: gpointer,
size: gsize,
_alignment: gsize,
prot: GumPageProtection,
) -> gpointer {
let ptr = kernel::alloc_code(size as usize);
unsafe {
core::ptr::write_bytes(ptr, 0, size as usize);
if (prot & _GumPageProtection_GUM_PAGE_EXECUTE) != 0 {
gum::register_slab(ptr as u64, size as usize);
gum_mprotect(ptr as gpointer, size, prot);
}
}
ptr as gpointer
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_free(address: gpointer, size: gsize) -> gboolean {
// vfree() wants the range writable again: the pages go back to the vmalloc
// allocator, and leaving them RX would fault the next consumer.
if gum::is_agent_slab(address as u64) {
unsafe {
gum_mprotect(
address,
size,
(_GumPageProtection_GUM_PAGE_READ | _GumPageProtection_GUM_PAGE_WRITE)
as GumPageProtection,
);
}
gum::unregister_slab(address as u64);
}
kernel::free_code(address as *mut u8, size as usize);
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_on_registry_activating(registry: *mut GumModuleRegistry) {
unsafe {
let kernel_base = kernel::get_kernel_base();
if kernel_base != 0 {
register_module(registry, "/boot/vmlinux", "", kernel_base, kernel::get_kernel_size());
}
for m in kernel::enumerate_modules() {
let path = format!("/lib/modules/{}.ko", m.name);
register_module(registry, &path, &m.version, m.base, m.size);
}
}
}
unsafe fn register_module(
registry: *mut GumModuleRegistry,
path: &str,
version: &str,
base: u64,
size: u64,
) {
unsafe {
let range = GumMemoryRange {
base_address: base,
size: size as gsize,
};
let module = gum::gum_native_module_new(path, version, &range);
gum_barebone_register_module(registry, module);
g_object_unref(module as gpointer);
}
}
pub(crate) unsafe fn enumerate_exports_in_range(
start_address: u64,
end_address: u64,
callback: &mut FoundExportCallback<'_>,
) {
kernel::enumerate_symbols(&mut |name, address| {
if address < start_address || address >= end_address {
return true;
}
callback(name.as_ptr() as *const gchar, address)
});
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_symbol_details_from_address(
address: gpointer,
details: *mut GumDebugSymbolDetails,
) -> gboolean {
let Some((name, symbol_address)) = kernel::symbol_from_address(address as u64) else {
return 0;
};
unsafe {
let name_bytes = name.as_bytes();
let copy_len = core::cmp::min(name_bytes.len(), (*details).symbol_name.len() - 1);
core::ptr::copy_nonoverlapping(
name_bytes.as_ptr(),
(*details).symbol_name.as_mut_ptr() as *mut u8,
copy_len,
);
(*details).symbol_name[copy_len] = 0;
(*details).address = symbol_address;
(*details).module_name[0] = 0;
(*details).file_name[0] = 0;
(*details).line_number = 0;
(*details).column = 0;
}
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_symbol_name_from_address(address: gpointer) -> *mut gchar {
let Some((name, _)) = kernel::symbol_from_address(address as u64) else {
return ptr::null_mut();
};
let mut name = name;
name.push('\0');
unsafe { g_strdup(name.as_ptr() as *const gchar) }
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_function(name: *const gchar) -> gpointer {
let address = kernel::find_function(unsafe { CStr::from_ptr(name) });
address as gpointer
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_functions_named(name: *const gchar) -> *mut GArray {
let wanted = unsafe { CStr::from_ptr(name) };
collect_addresses(&mut |candidate, _address| candidate == wanted)
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_functions_matching(pattern: *const gchar) -> *mut GArray {
unsafe {
let spec = g_pattern_spec_new(pattern);
let array = collect_addresses(&mut |candidate, _address| {
g_pattern_spec_match_string(spec, candidate.as_ptr() as *const gchar) != 0
});
g_pattern_spec_free(spec);
array
}
}
fn collect_addresses(predicate: &mut dyn FnMut(&CStr, u64) -> bool) -> *mut GArray {
unsafe {
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
kernel::enumerate_symbols(&mut |name, address| {
if predicate(name, address) {
let addr = address as gpointer;
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
}
true
});
array
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_load_symbols(_path: *const gchar) -> gboolean {
0
}
+108
View File
@@ -0,0 +1,108 @@
// Win9x half of Gum's platform backend. The processors this kernel runs on have
// no execute permission to withhold and no supervisor write protection worth
// speaking of, so code is writable where it lies and none of the aliasing the
// other backends need arises.
use crate::{
bindings::{
_GumPageProtection_GUM_PAGE_EXECUTE, _GumRwxSupport_GUM_RWX_FULL, GumMemoryRange,
GumModuleRegistry, GumPageProtection, GumRwxSupport, g_object_unref, gboolean, gpointer,
gsize, guint, gum_barebone_register_module, gum_mprotect,
},
gum::{self, FoundExportCallback},
kernel,
};
use core::ptr;
#[unsafe(no_mangle)]
pub extern "C" fn gum_query_rwx_support() -> GumRwxSupport {
_GumRwxSupport_GUM_RWX_FULL
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_can_remap_writable() -> gboolean {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_try_remap_writable_pages(
first_page: gpointer,
_n_pages: guint,
) -> gpointer {
first_page
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_dispose_writable_pages(_writable: gpointer, _n_pages: guint) {}
#[unsafe(no_mangle)]
pub extern "C" fn gum_try_mprotect(
address: gpointer,
size: gsize,
prot: GumPageProtection,
) -> gboolean {
if kernel::protect(address as u64, size as usize, prot as u32) {
1
} else {
0
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_allocate(
_address: gpointer,
size: gsize,
_alignment: gsize,
prot: GumPageProtection,
) -> gpointer {
let ptr = kernel::alloc_code(size as usize);
if ptr.is_null() {
return ptr::null_mut();
}
unsafe {
core::ptr::write_bytes(ptr, 0, size as usize);
if (prot & _GumPageProtection_GUM_PAGE_EXECUTE) != 0 {
gum::register_slab(ptr as u64, size as usize);
gum_mprotect(ptr as gpointer, size, prot);
}
}
ptr as gpointer
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_free(address: gpointer, size: gsize) -> gboolean {
if gum::is_agent_slab(address as u64) {
gum::unregister_slab(address as u64);
}
kernel::free_code(address as *mut u8, size as usize);
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_on_registry_activating(registry: *mut GumModuleRegistry) {
let kernel_base = kernel::get_kernel_base();
if kernel_base == 0 {
return;
}
unsafe {
let range = GumMemoryRange {
base_address: kernel_base,
size: 0,
};
let module = gum::gum_native_module_new("/VMM32.VXD", "", &range);
gum_barebone_register_module(registry, module);
g_object_unref(module as gpointer);
}
}
// Nothing here has a symbol table to walk: the loader resolves VxD services
// through a dispatch table rather than by name.
pub(crate) unsafe fn enumerate_exports_in_range(
_start: u64,
_end: u64,
_callback: &mut FoundExportCallback,
) {
}
+379
View File
@@ -0,0 +1,379 @@
// XNU half of Gum's platform backend. Memory permissions and kernel-text writes
// go out over the hostlink, because the guest CPU cannot perform them itself:
// the host owns the page tables and the physical-memory bridge.
use crate::{
FridaCommand,
bindings::{
_GumPageProtection_GUM_PAGE_EXECUTE, _GumPageProtection_GUM_PAGE_READ,
_GumPageProtection_GUM_PAGE_WRITE, _GumRwxSupport_GUM_RWX_NONE, GArray,
GumDebugSymbolDetails, GumMemoryRange, GumModuleRegistry, GumPageProtection, GumRwxSupport,
g_array_append_vals, g_array_new, g_object_unref, g_strdup, g_variant_get_boolean,
g_variant_get_uint64, g_variant_new, g_variant_new_fixed_array, g_variant_type_free,
g_variant_type_new, g_variant_unref, gboolean, gchar, gconstpointer, gpointer, gsize, guint,
gum_barebone_register_module,
gum_barebone_try_remap_writable_pages as _gum_barebone_try_remap_writable_pages,
gum_mprotect, gum_query_page_size,
},
gum::{self, FoundExportCallback},
host_rpc, kernel, libc,
};
use alloc::format;
use alloc::vec::Vec;
use core::ffi::CStr;
use core::mem::size_of;
use core::ptr;
const SHADOW_MAGIC: u64 = 0x4644_4f48_5341_4853;
const SHADOW_HEADER: usize = 24;
const SHADOW_MIN_ADDRESS: u64 = 0xffff_f000_0000_0000;
#[unsafe(no_mangle)]
pub extern "C" fn gum_query_rwx_support() -> GumRwxSupport {
_GumRwxSupport_GUM_RWX_NONE
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_can_remap_writable() -> gboolean {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_try_remap_writable_pages(
first_page: gpointer,
n_pages: guint,
) -> gpointer {
if gum::is_agent_slab(first_page as u64) {
return remap_agent_pages(first_page, n_pages);
}
shadow_kernel_pages(first_page, n_pages)
}
fn remap_agent_pages(first_page: gpointer, n_pages: guint) -> gpointer {
unsafe {
let page_size = gum_query_page_size() as usize;
let mut virtual_addrs = Vec::with_capacity(n_pages as usize);
let mut current_page = first_page as u64;
for _ in 0..n_pages {
virtual_addrs.push(current_page as gpointer);
current_page += page_size as u64;
}
_gum_barebone_try_remap_writable_pages(
virtual_addrs.as_ptr() as *mut *const core::ffi::c_void,
virtual_addrs.len() as guint,
)
}
}
fn shadow_kernel_pages(first_page: gpointer, n_pages: guint) -> gpointer {
unsafe {
let total = n_pages as usize * gum_query_page_size() as usize;
let buffer = kernel::alloc(SHADOW_HEADER + total);
*(buffer as *mut u64) = SHADOW_MAGIC;
*(buffer.add(8) as *mut u64) = first_page as u64;
*(buffer.add(16) as *mut u32) = n_pages;
let body = buffer.add(SHADOW_HEADER);
core::ptr::copy_nonoverlapping(first_page as *const u8, body, total);
body as gpointer
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_dispose_writable_pages(writable: gpointer, _n_pages: guint) {
if (writable as u64) < SHADOW_MIN_ADDRESS {
return;
}
unsafe {
let buffer = (writable as *mut u8).sub(SHADOW_HEADER);
if *(buffer as *const u64) != SHADOW_MAGIC {
return;
}
let first_page = *(buffer.add(8) as *const u64);
let n_pages = *(buffer.add(16) as *const u32);
let total = n_pages as usize * gum_query_page_size() as usize;
commit_kernel_patch(first_page, writable as *const u8, total);
libc::__clear_cache(first_page as *const u8, (first_page + total as u64) as *const u8);
kernel::free(buffer, SHADOW_HEADER + total);
}
}
unsafe fn commit_kernel_patch(address: u64, data: *const u8, len: usize) {
unsafe {
let element_type = g_variant_type_new(c"y".as_ptr());
let bytes = g_variant_new_fixed_array(element_type, data as gconstpointer, len as gsize, 1);
g_variant_type_free(element_type);
let payload = g_variant_new(c"(t@ay)".as_ptr(), address, bytes);
let reply = host_rpc(FridaCommand::PatchCode, payload);
g_variant_unref(reply);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_try_remap_writable_pages(
addrs: *const gpointer,
n_addrs: guint,
) -> gpointer {
if !crate::transport_is_up() {
return ptr::null_mut();
}
unsafe {
let element_type = g_variant_type_new(c"t".as_ptr());
let payload = g_variant_new_fixed_array(
element_type,
addrs as gconstpointer,
n_addrs as gsize,
size_of::<u64>() as gsize,
);
g_variant_type_free(element_type);
let reply = host_rpc(FridaCommand::RemapWritablePages, payload);
let virtual_address = g_variant_get_uint64(reply);
g_variant_unref(reply);
virtual_address as gpointer
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_try_mprotect(
address: gpointer,
size: gsize,
prot: GumPageProtection,
) -> gboolean {
if !crate::transport_is_up() {
return 1;
}
unsafe {
let payload = g_variant_new(c"(ttu)".as_ptr(), address as u64, size as u64, prot as u32);
let reply = host_rpc(FridaCommand::MemoryProtect, payload);
let success = g_variant_get_boolean(reply);
g_variant_unref(reply);
if success != 0 {
flush_tlb_range(address as u64, size as u64);
}
success
}
}
// The host rewrites our page-table descriptors through the physical-memory
// bridge, which leaves this CPU's TLB holding the stale translation. Stalker
// flips a slab page RW then RX in place, so without this the freeze never takes
// effect and executing the page faults with a permission abort.
unsafe fn flush_tlb_range(address: u64, size: u64) {
unsafe {
let page_size = gum_query_page_size() as u64;
let start = address & !(page_size - 1);
let end = (address + size + page_size - 1) & !(page_size - 1);
core::arch::asm!("dsb ish", options(nostack, preserves_flags));
let mut va = start;
while va < end {
core::arch::asm!("tlbi vaae1is, {operand}", operand = in(reg) va >> 12,
options(nostack, preserves_flags));
va += page_size;
}
core::arch::asm!("dsb ish", "isb", options(nostack, preserves_flags));
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_allocate(
_address: gpointer,
size: gsize,
_alignment: gsize,
prot: GumPageProtection,
) -> gpointer {
let ptr = kernel::alloc_code(size as usize);
unsafe {
core::ptr::write_bytes(ptr, 0, size as usize);
if (prot & _GumPageProtection_GUM_PAGE_EXECUTE) != 0 {
gum::register_slab(ptr as u64, size as usize);
gum_mprotect(ptr as gpointer, size, prot);
}
}
ptr as gpointer
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_memory_free(address: gpointer, size: gsize) -> gboolean {
// Executable slabs were flipped to RX in the page tables; restore RW before returning them to
// the allocator, otherwise the reclaimed pages stay non-writable and the next consumer faults.
if gum::is_agent_slab(address as u64) {
unsafe {
gum_mprotect(
address,
size,
(_GumPageProtection_GUM_PAGE_READ | _GumPageProtection_GUM_PAGE_WRITE)
as GumPageProtection,
);
}
gum::unregister_slab(address as u64);
}
kernel::free_code(address as *mut u8, size as usize);
1
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_barebone_on_registry_activating(registry: *mut GumModuleRegistry) {
let kernel_base = kernel::get_kernel_base();
unsafe {
let module_infos = core::ptr::addr_of!(crate::MODULE_INFO);
let module_infos = &*module_infos;
let mut i = 0;
for module_info in module_infos.iter() {
let module_base = kernel_base + module_info.offset as u64;
let module_path = if i == 0 {
"/System/Library/Kernels/kernel"
} else {
&format!(
"/System/Library/Extensions/{}.kext/{}",
module_info.name, module_info.name
)
};
let module_range = GumMemoryRange {
base_address: module_base,
size: module_info.size as u64,
};
let module = gum::gum_native_module_new(&module_path, &module_info.version, &module_range);
gum_barebone_register_module(registry, module);
g_object_unref(module as gpointer);
i += 1;
}
}
}
pub(crate) unsafe fn enumerate_exports_in_range(
start_address: u64,
end_address: u64,
callback: &mut FoundExportCallback<'_>,
) {
unsafe {
let symbol_table = core::ptr::addr_of!(crate::SYMBOL_TABLE);
let symbol_table = &*symbol_table;
const N_EXT: u8 = 0x01; // External symbol flag
const N_TYPE: u8 = 0x0e; // Type mask
const N_SECT: u8 = 0x0e; // Defined in section
for symbol_ref in symbol_table.iter_symbols_in_range(start_address, end_address) {
let is_external = (symbol_ref.symbol_type() & N_EXT) != 0;
let is_defined = (symbol_ref.symbol_type() & N_TYPE) == N_SECT;
if !is_external || !is_defined {
continue;
}
if !callback(symbol_ref.name_ptr(), symbol_ref.address()) {
break;
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_symbol_details_from_address(
address: gpointer,
details: *mut GumDebugSymbolDetails,
) -> gboolean {
unsafe {
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
if let Some(symbol) = table.find_symbol_by_address(address as u64) {
let name_bytes = symbol.name().as_bytes();
let copy_len = core::cmp::min(name_bytes.len(), 2048);
core::ptr::copy_nonoverlapping(
name_bytes.as_ptr(),
(*details).symbol_name.as_mut_ptr() as *mut u8,
copy_len,
);
(*details).symbol_name[copy_len] = 0;
(*details).address = symbol.address();
(*details).module_name[0] = 0;
(*details).file_name[0] = 0;
(*details).line_number = 0;
(*details).column = 0;
return 1;
}
0
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_symbol_name_from_address(address: gpointer) -> *mut gchar {
unsafe {
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
let name_ptr = table.find_symbol_name_ptr_by_address(address as u64);
g_strdup(name_ptr as *const gchar)
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_function(name: *const gchar) -> gpointer {
unsafe {
let target_name = CStr::from_ptr(name).to_string_lossy();
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
if let Some(symbol) = table.find_symbol_by_name(&target_name) {
return symbol.address() as gpointer;
}
core::ptr::null_mut()
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_functions_named(name: *const gchar) -> *mut GArray {
unsafe {
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
let target_name = CStr::from_ptr(name).to_string_lossy();
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
let symbols = table.find_symbols_by_name(&target_name);
for symbol in symbols {
let addr = symbol.address() as gpointer;
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
}
array
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_find_functions_matching(pattern: *const gchar) -> *mut GArray {
unsafe {
let array = g_array_new(0, 0, core::mem::size_of::<gpointer>() as guint);
let glob_pattern = CStr::from_ptr(pattern).to_string_lossy();
let table = core::ptr::addr_of!(crate::SYMBOL_TABLE).read();
let symbols = table.find_symbols_matching_glob(&glob_pattern);
for symbol in symbols {
let addr = symbol.address() as gpointer;
g_array_append_vals(array, &addr as *const gpointer as gconstpointer, 1);
}
array
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gum_load_symbols(_path: *const gchar) -> gboolean {
0
}
+114
View File
@@ -0,0 +1,114 @@
// Linux transport: /dev/frida, framed the same way as the XNU hostlinks (4-byte
// little-endian length prefix) so the host-side (yqv) protocol is identical across
// every transport.
//
// The character device itself lives in linux/frida-kmod.c and hands us a byte
// stream; whoever opens the device decides how it reaches the host.
use core::cell::UnsafeCell;
use core::ffi::{c_int, c_void};
use alloc::vec::Vec;
struct Inner {
rx_lenbuf: [u8; 4],
rx_lenhave: usize,
rx_buf: Vec<u8>,
rx_have: usize,
rx_need: usize,
}
pub struct Hostlink {
state: UnsafeCell<Inner>,
on_rx: Option<fn(&[u8])>,
}
unsafe impl Send for Hostlink {}
impl Hostlink {
pub fn init(on_rx: Option<fn(&[u8])>) -> Result<Self, ()> {
if unsafe { frida_kmod_link_open() } != 0 {
return Err(());
}
Ok(Hostlink {
state: UnsafeCell::new(Inner {
rx_lenbuf: [0; 4],
rx_lenhave: 0,
rx_buf: Vec::new(),
rx_have: 0,
rx_need: 0,
}),
on_rx,
})
}
pub fn send(&self, payload: &[u8]) {
let header = (payload.len() as u32).to_le_bytes();
unsafe {
frida_kmod_link_send(header.as_ptr() as *const c_void, header.len());
frida_kmod_link_send(payload.as_ptr() as *const c_void, payload.len());
}
}
pub fn process(&self) {
let s = unsafe { &mut *self.state.get() };
loop {
if s.rx_lenhave < 4 {
let lo = s.rx_lenhave;
let n = recv_nonblocking(&mut s.rx_lenbuf[lo..4]);
if n == 0 {
return;
}
s.rx_lenhave += n;
if s.rx_lenhave < 4 {
continue;
}
s.rx_need = u32::from_le_bytes(s.rx_lenbuf) as usize;
s.rx_have = 0;
s.rx_buf.resize(s.rx_need, 0);
}
while s.rx_have < s.rx_need {
let lo = s.rx_have;
let hi = s.rx_need;
let n = recv_nonblocking(&mut s.rx_buf[lo..hi]);
if n == 0 {
return;
}
s.rx_have += n;
}
// Detach the frame and reset receive state before dispatching: the callback may
// re-enter process() (a synchronous host RPC issued while handling a command), and
// it must start from a clean state instead of re-dispatching this same frame.
let frame = core::mem::take(&mut s.rx_buf);
let need = s.rx_need;
s.rx_lenhave = 0;
s.rx_have = 0;
s.rx_need = 0;
if let Some(cb) = self.on_rx {
cb(&frame[..need]);
}
}
}
}
impl Drop for Hostlink {
fn drop(&mut self) {
unsafe { frida_kmod_link_close() };
}
}
fn recv_nonblocking(dst: &mut [u8]) -> usize {
let n = unsafe { frida_kmod_link_recv(dst.as_mut_ptr() as *mut c_void, dst.len()) };
if n <= 0 { 0 } else { n as usize }
}
unsafe extern "C" {
fn frida_kmod_link_open() -> c_int;
fn frida_kmod_link_close();
fn frida_kmod_link_send(data: *const c_void, size: usize) -> c_int;
fn frida_kmod_link_recv(data: *mut c_void, size: usize) -> isize;
}
+403 -68
View File
@@ -5,7 +5,7 @@ use core::ptr::{read_volatile, write_volatile};
use core::sync::atomic::{AtomicUsize, Ordering};
use crate::gum::gum_barebone_query_page_size;
use crate::xnu;
use crate::kernel;
const MMIO_SIZE: u64 = 0x200;
@@ -51,6 +51,46 @@ const F_MULTIPORT: u64 = 1u64 << 1;
const INT_VRING: u32 = 1;
const PCI_CONFIG_ADDRESS: u16 = 0xcf8;
const PCI_CONFIG_DATA: u16 = 0xcfc;
const PCI_VENDOR_VIRTIO: u16 = 0x1af4;
const PCI_DEVICE_CONSOLE_LEGACY: u16 = 0x1003;
const PCI_DEVICE_CONSOLE_MODERN: u16 = 0x1043;
const PCI_COMMAND: u8 = 0x04;
const PCI_CAP_LIST_POINTER: u8 = 0x34;
const PCI_INTERRUPT_LINE: u8 = 0x3c;
const PCI_BASE_ADDRESS_0: u8 = 0x10;
const PCI_COMMAND_MEMORY: u32 = 1 << 1;
const PCI_COMMAND_BUS_MASTER: u32 = 1 << 2;
const PCI_CAP_ID_VENDOR: u8 = 0x09;
const VIRTIO_CAP_CFG_TYPE: u8 = 3;
const VIRTIO_CAP_BAR: u8 = 4;
const VIRTIO_CAP_OFFSET: u8 = 8;
const VIRTIO_CAP_LENGTH: u8 = 12;
const VIRTIO_CAP_NOTIFY_MULTIPLIER: u8 = 16;
const CFG_TYPE_COMMON: u8 = 1;
const CFG_TYPE_NOTIFY: u8 = 2;
const CFG_TYPE_ISR: u8 = 3;
const COMMON_DEVFEAT_SEL: usize = 0x00;
const COMMON_DEVFEAT: usize = 0x04;
const COMMON_DRVFEAT_SEL: usize = 0x08;
const COMMON_DRVFEAT: usize = 0x0c;
const COMMON_STATUS: usize = 0x14;
const COMMON_QSEL: usize = 0x16;
const COMMON_QNUM: usize = 0x18;
const COMMON_QREADY: usize = 0x1c;
const COMMON_QNOTIFY_OFF: usize = 0x1e;
const COMMON_QDESC: usize = 0x20;
const COMMON_QAVAIL: usize = 0x28;
const COMMON_QUSED: usize = 0x30;
const Q_RX0: u16 = 0;
const Q_TX0: u16 = 1;
const Q_CTRL_RX: u16 = 2;
@@ -107,18 +147,19 @@ struct DmaPage {
fn dma_page_alloc() -> DmaPage {
let len = PAGE_SIZE.load(Ordering::Relaxed);
let va = xnu::kalloc(len);
let pa = xnu::ml_vtophys(va as u64);
let va = kernel::alloc_code(len);
let pa = kernel::virt_to_phys(va as u64);
DmaPage { va, pa }
}
fn dma_page_free(p: DmaPage) {
let len = PAGE_SIZE.load(Ordering::Relaxed);
xnu::free(p.va, len);
kernel::free_code(p.va, len);
}
struct Vq {
sel: u16,
notify_off: u16,
size: u16,
desc_va: *mut u8,
avail_va: *mut u8,
@@ -129,12 +170,7 @@ struct Vq {
free_cnt: u16,
}
impl Vq {
fn new(mmio: *mut u8, sel: u16, size: u16) -> Self {
w32(mmio, QSEL, sel as u32);
let max = r32(mmio, QNUM_MAX) as u16;
debug_assert!(max >= size && max != 0);
w32(mmio, QNUM, size as u32);
fn new(regs: &Regs, sel: u16, size: u16) -> Self {
let d = dma_page_alloc();
let a = dma_page_alloc();
let u = dma_page_alloc();
@@ -146,10 +182,6 @@ impl Vq {
core::ptr::write_bytes(u.va, 0, ps);
}
w64(mmio, QDESC_LO, QDESC_HI, d.pa);
w64(mmio, QAVAIL_LO, QAVAIL_HI, a.pa);
w64(mmio, QUSED_LO, QUSED_HI, u.pa);
let dp = d.va as *mut Desc;
for i in 0..size {
unsafe {
@@ -158,10 +190,11 @@ impl Vq {
}
}
w32(mmio, QREADY, 1);
let notify_off = regs.queue_prepare(sel, size, d.pa, a.pa, u.pa);
Self {
sel,
notify_off,
size,
desc_va: d.va,
avail_va: a.va,
@@ -230,7 +263,7 @@ impl Vq {
}
struct Inner {
mmio: *mut u8,
regs: Regs,
ctrl_rx: Vq,
ctrl_tx: Vq,
@@ -271,27 +304,46 @@ unsafe impl Send for Hostlink {}
impl Hostlink {
pub fn init(mmio_base: u64, irq_line: u32, on_rx: Option<fn(&[u8])>, wake_token: *const u8) -> Result<Self, ()> {
let page_size = gum_barebone_query_page_size();
PAGE_SIZE.store(page_size as usize, Ordering::Relaxed);
let mmio = xnu::ml_io_map(mmio_base, MMIO_SIZE) as *mut u8;
let mmio = kernel::map_io(mmio_base, MMIO_SIZE) as *mut u8;
if mmio.is_null() {
return Err(());
}
let regs = Regs::Mmio(mmio);
w32(mmio, STATUS, 0);
w32(mmio, STATUS, ST_ACK | ST_DRV);
regs.reset();
let magic_ok = r32(mmio, MAGIC) == 0x7472_6976;
let version_ok = r32(mmio, VERSION) == 2;
let device_ok = r32(mmio, DEVICE) == DEV_ID_CONSOLE;
if !(magic_ok && version_ok && device_ok) {
w32(mmio, STATUS, ST_FAILED);
regs.set_status(ST_FAILED);
return Err(());
}
let dev_lo = feat_get(mmio, 0) as u64;
let dev_hi = (feat_get(mmio, 1) as u64) << 32;
Self::start(regs, irq_line, on_rx, wake_token)
}
#[cfg(target_arch = "x86")]
pub fn init_pci(on_rx: Option<fn(&[u8])>, wake_token: *const u8) -> Result<Self, ()> {
let Some(device) = PciDevice::find_console() else {
return Err(());
};
let Some(regs) = device.map_virtio_regs() else {
return Err(());
};
regs.reset();
Self::start(regs, device.irq_line(), on_rx, wake_token)
}
fn start(regs: Regs, irq_line: u32, on_rx: Option<fn(&[u8])>, wake_token: *const u8) -> Result<Self, ()> {
let page_size = gum_barebone_query_page_size();
PAGE_SIZE.store(page_size as usize, Ordering::Relaxed);
let dev_lo = regs.feat_get(0) as u64;
let dev_hi = (regs.feat_get(1) as u64) << 32;
let mut drv: u64 = 0;
if (dev_hi & F_VERSION_1) != 0 {
drv |= F_VERSION_1;
@@ -299,21 +351,24 @@ impl Hostlink {
if (dev_lo & F_MULTIPORT) != 0 {
drv |= F_MULTIPORT;
}
feat_set(mmio, 0, (drv & 0xffff_ffff) as u32);
feat_set(mmio, 1, (drv >> 32) as u32);
w32(mmio, STATUS, r32(mmio, STATUS) | ST_FEAT_OK);
let feats_ok = (r32(mmio, STATUS) & ST_FEAT_OK) != 0;
regs.feat_set(0, (drv & 0xffff_ffff) as u32);
regs.feat_set(1, (drv >> 32) as u32);
regs.set_status(regs.status() | ST_FEAT_OK);
let feats_ok = (regs.status() & ST_FEAT_OK) != 0;
if !feats_ok {
w32(mmio, STATUS, ST_FAILED);
regs.set_status(ST_FAILED);
return Err(());
}
let ctrl_rx = Vq::new(mmio, Q_CTRL_RX, QSZ);
let ctrl_tx = Vq::new(mmio, Q_CTRL_TX, QSZ);
let ctrl_rx = Vq::new(&regs, Q_CTRL_RX, QSZ);
let ctrl_tx = Vq::new(&regs, Q_CTRL_TX, QSZ);
w32(mmio, STATUS, r32(mmio, STATUS) | ST_DRV_OK);
regs.set_status(regs.status() | ST_DRV_OK);
xnu::install_interrupt_handler(
unsafe {
ISR_REGS = Some(regs);
}
kernel::install_interrupt_handler(
irq_line,
wake_token as *mut c_void,
isr_wake,
@@ -321,7 +376,7 @@ impl Hostlink {
);
let inner = Inner {
mmio,
regs,
ctrl_rx,
ctrl_tx,
port_id: None,
@@ -359,7 +414,7 @@ impl Hostlink {
let s = unsafe { &mut *self.state.get() };
let total = 4 + payload.len();
let buf = xnu::kalloc(total);
let buf = kernel::alloc(total);
unsafe {
let len = payload.len() as u32;
*buf.add(0) = (len & 0xFF) as u8;
@@ -370,7 +425,7 @@ impl Hostlink {
}
let frame: &'static [u8] = unsafe { core::slice::from_raw_parts(buf, total) };
let node = xnu::kalloc(size_of::<TxNode>()) as *mut TxNode;
let node = kernel::alloc(size_of::<TxNode>()) as *mut TxNode;
unsafe {
(*node).next = core::ptr::null_mut();
(*node).frame = frame;
@@ -386,15 +441,13 @@ impl Hostlink {
s.tx_tail = node;
}
xnu::thread_wakeup(s.wake_token);
kernel::wake(s.wake_token);
}
pub fn process(&self) {
let s = unsafe { &mut *self.state.get() };
if (r32(s.mmio, ISR) & INT_VRING) != 0 {
w32(s.mmio, ISR_ACK, INT_VRING);
}
s.regs.isr_ack();
self.ctrl_complete();
self.ctrl_prime_rx(QSZ as usize);
@@ -425,8 +478,8 @@ impl Hostlink {
s.ctrl_rx.push_avail(h);
posted += 1;
}
let sel = s.ctrl_rx.sel;
self.kick(sel);
let (sel, notify_off) = (s.ctrl_rx.sel, s.ctrl_rx.notify_off);
self.kick(sel, notify_off);
}
fn ctrl_send(&self, msg: VConsCtrl) {
@@ -445,8 +498,8 @@ impl Hostlink {
}
s.tx_pages[h as usize] = Some(pg);
s.ctrl_tx.push_avail(h);
let sel = s.ctrl_tx.sel;
self.kick(sel);
let (sel, notify_off) = (s.ctrl_tx.sel, s.ctrl_tx.notify_off);
self.kick(sel, notify_off);
}
fn ctrl_complete(&self) {
@@ -488,8 +541,8 @@ impl Hostlink {
s.ctrl_rx.push_avail(h);
}
}
let sel = s.ctrl_rx.sel;
self.kick(sel);
let (sel, notify_off) = (s.ctrl_rx.sel, s.ctrl_rx.notify_off);
self.kick(sel, notify_off);
while let Some(u) = s.ctrl_tx.pop_used() {
let head = u.id as u16;
@@ -511,8 +564,8 @@ impl Hostlink {
let base = 4 + ((id as u16 - 1) * 2);
(base, base + 1)
};
let rx = Vq::new(s.mmio, rx_i, QSZ);
let tx = Vq::new(s.mmio, tx_i, QSZ);
let rx = Vq::new(&s.regs, rx_i, QSZ);
let tx = Vq::new(&s.regs, tx_i, QSZ);
s.rx = Some(rx);
s.tx = Some(tx);
s.port_id = Some(id);
@@ -537,8 +590,8 @@ impl Hostlink {
s.data_rx_pages[h as usize] = Some(pg);
rxq.push_avail(h);
}
let sel = rxq.sel;
self.kick(sel);
let (sel, notify_off) = (rxq.sel, rxq.notify_off);
self.kick(sel, notify_off);
}
fn data_rx_complete(&self) {
@@ -569,8 +622,8 @@ impl Hostlink {
rxq.push_avail(h);
}
}
let sel = rxq.sel;
self.kick(sel);
let (sel, notify_off) = (rxq.sel, rxq.notify_off);
self.kick(sel, notify_off);
}
fn feed_rx_stream(&self, mut chunk: &[u8]) {
@@ -592,7 +645,7 @@ impl Hostlink {
| ((s.rx_lenbuf[3] as usize) << 24);
if s.rx_buf.is_none() && len > 0 {
let buf = xnu::kalloc(len);
let buf = kernel::alloc(len);
let slice: &'static mut [u8] =
unsafe { core::slice::from_raw_parts_mut(buf, len) };
s.rx_buf = Some(slice);
@@ -713,26 +766,278 @@ impl Hostlink {
}
if let Some(h) = head {
let sel = txq.sel;
let (sel, notify_off) = (txq.sel, txq.notify_off);
txq.push_avail(h);
self.kick(sel);
self.kick(sel, notify_off);
}
xnu::free(frame.as_ptr() as *mut u8, frame.len());
xnu::free(node as *mut u8, core::mem::size_of::<TxNode>());
kernel::free(frame.as_ptr() as *mut u8, frame.len());
kernel::free(node as *mut u8, core::mem::size_of::<TxNode>());
}
}
fn kick(&self, sel: u16) {
fn kick(&self, sel: u16, notify_off: u16) {
let s = unsafe { &*self.state.get() };
wmb();
w32(s.mmio, QSEL, sel as u32);
w32(s.mmio, QNOTIFY, sel as u32);
s.regs.notify(sel, notify_off);
}
}
extern "C" fn isr_wake(token: *mut c_void, _refcon: *mut c_void, _nub: *mut c_void, _src: i32) {
xnu::thread_wakeup(token as *const u8);
unsafe {
if let Some(regs) = ISR_REGS {
regs.isr_ack();
}
}
kernel::wake(token as *const u8);
}
static mut ISR_REGS: Option<Regs> = None;
#[derive(Copy, Clone)]
enum Regs {
Mmio(*mut u8),
Pci(PciRegs),
}
#[derive(Copy, Clone)]
struct PciRegs {
common: *mut u8,
notify: *mut u8,
notify_off_multiplier: u32,
isr: *mut u8,
}
impl Regs {
fn reset(&self) {
match self {
Regs::Mmio(base) => {
w32(*base, STATUS, 0);
w32(*base, STATUS, ST_ACK | ST_DRV);
}
Regs::Pci(p) => {
w8(p.common, COMMON_STATUS, 0);
while r8(p.common, COMMON_STATUS) != 0 {}
w8(p.common, COMMON_STATUS, (ST_ACK | ST_DRV) as u8);
}
}
}
fn feat_get(&self, sel: u32) -> u32 {
match self {
Regs::Mmio(base) => {
w32(*base, DEVFEAT_SEL, sel);
r32(*base, DEVFEAT)
}
Regs::Pci(p) => {
w32(p.common, COMMON_DEVFEAT_SEL, sel);
r32(p.common, COMMON_DEVFEAT)
}
}
}
fn feat_set(&self, sel: u32, v: u32) {
match self {
Regs::Mmio(base) => {
w32(*base, DRVFEAT_SEL, sel);
w32(*base, DRVFEAT, v);
}
Regs::Pci(p) => {
w32(p.common, COMMON_DRVFEAT_SEL, sel);
w32(p.common, COMMON_DRVFEAT, v);
}
}
}
fn status(&self) -> u32 {
match self {
Regs::Mmio(base) => r32(*base, STATUS),
Regs::Pci(p) => r8(p.common, COMMON_STATUS) as u32,
}
}
fn set_status(&self, v: u32) {
match self {
Regs::Mmio(base) => w32(*base, STATUS, v),
Regs::Pci(p) => w8(p.common, COMMON_STATUS, v as u8),
}
}
fn queue_prepare(&self, sel: u16, size: u16, desc: u64, avail: u64, used: u64) -> u16 {
match self {
Regs::Mmio(base) => {
let base = *base;
w32(base, QSEL, sel as u32);
debug_assert!(r32(base, QNUM_MAX) as u16 >= size);
w32(base, QNUM, size as u32);
w64(base, QDESC_LO, QDESC_HI, desc);
w64(base, QAVAIL_LO, QAVAIL_HI, avail);
w64(base, QUSED_LO, QUSED_HI, used);
w32(base, QREADY, 1);
0
}
Regs::Pci(p) => {
w16(p.common, COMMON_QSEL, sel);
debug_assert!(r16(p.common, COMMON_QNUM) >= size);
w16(p.common, COMMON_QNUM, size);
w64(p.common, COMMON_QDESC, COMMON_QDESC + 4, desc);
w64(p.common, COMMON_QAVAIL, COMMON_QAVAIL + 4, avail);
w64(p.common, COMMON_QUSED, COMMON_QUSED + 4, used);
let notify_off = r16(p.common, COMMON_QNOTIFY_OFF);
w16(p.common, COMMON_QREADY, 1);
notify_off
}
}
}
fn notify(&self, sel: u16, notify_off: u16) {
match self {
Regs::Mmio(base) => {
w32(*base, QSEL, sel as u32);
w32(*base, QNOTIFY, sel as u32);
}
Regs::Pci(p) => {
let off = notify_off as usize * p.notify_off_multiplier as usize;
w16(p.notify, off, sel);
}
}
}
fn isr_ack(&self) {
match self {
Regs::Mmio(base) => {
if (r32(*base, ISR) & INT_VRING) != 0 {
w32(*base, ISR_ACK, INT_VRING);
}
}
Regs::Pci(p) => {
r8(p.isr, 0);
}
}
}
}
#[cfg(target_arch = "x86")]
#[derive(Copy, Clone)]
struct PciDevice {
bus: u8,
devfn: u8,
}
#[cfg(target_arch = "x86")]
impl PciDevice {
fn find_console() -> Option<Self> {
for devfn in 0..=u8::MAX {
let device = PciDevice { bus: 0, devfn };
let identity = device.read_config(0);
let vendor = (identity & 0xffff) as u16;
let model = (identity >> 16) as u16;
if vendor != PCI_VENDOR_VIRTIO {
continue;
}
if model != PCI_DEVICE_CONSOLE_LEGACY && model != PCI_DEVICE_CONSOLE_MODERN {
continue;
}
return Some(device);
}
None
}
fn map_virtio_regs(&self) -> Option<Regs> {
self.enable_memory_and_bus_mastering();
let mut common = core::ptr::null_mut();
let mut notify = core::ptr::null_mut();
let mut notify_off_multiplier = 0;
let mut isr = core::ptr::null_mut();
let mut cap = (self.read_config(PCI_CAP_LIST_POINTER) & 0xfc) as u8;
while cap != 0 {
let header = self.read_config(cap);
if (header & 0xff) as u8 == PCI_CAP_ID_VENDOR {
let cfg_type = self.read_config_byte(cap + VIRTIO_CAP_CFG_TYPE);
let bar = self.read_config_byte(cap + VIRTIO_CAP_BAR);
let offset = self.read_config(cap + VIRTIO_CAP_OFFSET);
let length = self.read_config(cap + VIRTIO_CAP_LENGTH);
match cfg_type {
CFG_TYPE_COMMON => {
common = self.map_bar_region(bar, offset, length);
}
CFG_TYPE_NOTIFY => {
notify = self.map_bar_region(bar, offset, length);
notify_off_multiplier =
self.read_config(cap + VIRTIO_CAP_NOTIFY_MULTIPLIER);
}
CFG_TYPE_ISR => {
isr = self.map_bar_region(bar, offset, length);
}
_ => {}
}
}
cap = ((header >> 8) & 0xfc) as u8;
}
if common.is_null() || notify.is_null() || isr.is_null() {
return None;
}
Some(Regs::Pci(PciRegs {
common,
notify,
notify_off_multiplier,
isr,
}))
}
fn irq_line(&self) -> u32 {
self.read_config_byte(PCI_INTERRUPT_LINE) as u32
}
fn enable_memory_and_bus_mastering(&self) {
let command = self.read_config(PCI_COMMAND);
self.write_config(
PCI_COMMAND,
command | PCI_COMMAND_MEMORY | PCI_COMMAND_BUS_MASTER,
);
}
fn map_bar_region(&self, bar: u8, offset: u32, length: u32) -> *mut u8 {
let slot = PCI_BASE_ADDRESS_0 + bar * 4;
let lo = self.read_config(slot);
let is_sixty_four_bit = (lo & 0b110) == 0b100;
let base = if is_sixty_four_bit {
((self.read_config(slot + 4) as u64) << 32) | ((lo & !0xf) as u64)
} else {
(lo & !0xf) as u64
};
kernel::map_io(base + offset as u64, length as u64) as *mut u8
}
fn read_config_byte(&self, offset: u8) -> u8 {
(self.read_config(offset) >> ((offset & 3) * 8)) as u8
}
fn read_config(&self, offset: u8) -> u32 {
outl(PCI_CONFIG_ADDRESS, self.config_address(offset));
inl(PCI_CONFIG_DATA)
}
fn write_config(&self, offset: u8, value: u32) {
outl(PCI_CONFIG_ADDRESS, self.config_address(offset));
outl(PCI_CONFIG_DATA, value);
}
fn config_address(&self, offset: u8) -> u32 {
0x8000_0000
| ((self.bus as u32) << 16)
| ((self.devfn as u32) << 8)
| ((offset as u32) & 0xfc)
}
}
fn r32(mmio: *mut u8, off: usize) -> u32 {
@@ -748,16 +1053,46 @@ fn w64(mmio: *mut u8, lo: usize, hi: usize, v: u64) {
w32(mmio, hi, (v >> 32) as u32);
}
fn feat_get(mmio: *mut u8, sel: u32) -> u32 {
w32(mmio, DEVFEAT_SEL, sel);
r32(mmio, DEVFEAT)
fn r16(mmio: *mut u8, off: usize) -> u16 {
unsafe { read_volatile(mmio.add(off) as *const u16) }
}
fn feat_set(mmio: *mut u8, sel: u32, v: u32) {
w32(mmio, DRVFEAT_SEL, sel);
w32(mmio, DRVFEAT, v)
fn w16(mmio: *mut u8, off: usize, val: u16) {
unsafe { write_volatile(mmio.add(off) as *mut u16, val) }
}
fn r8(mmio: *mut u8, off: usize) -> u8 {
unsafe { read_volatile(mmio.add(off)) }
}
fn w8(mmio: *mut u8, off: usize, val: u8) {
unsafe { write_volatile(mmio.add(off), val) }
}
#[cfg(target_arch = "x86")]
fn inl(port: u16) -> u32 {
let value: u32;
unsafe {
core::arch::asm!("in eax, dx", out("eax") value, in("dx") port,
options(nomem, nostack, preserves_flags));
}
value
}
#[cfg(target_arch = "x86")]
fn outl(port: u16, value: u32) {
unsafe {
core::arch::asm!("out dx, eax", in("dx") port, in("eax") value,
options(nomem, nostack, preserves_flags));
}
}
#[cfg(target_arch = "x86")]
fn wmb() {
unsafe { core::arch::asm!("sfence", options(nostack, preserves_flags)) }
}
#[cfg(target_arch = "aarch64")]
fn wmb() {
unsafe { core::arch::asm!("dmb ishst", options(nostack, preserves_flags)) }
}
+2 -2
View File
@@ -8,7 +8,7 @@ use core::sync::atomic::{AtomicU32, Ordering};
use alloc::vec::Vec;
use crate::xnu;
use crate::kernel;
const AF_VSOCK: c_int = 40;
const SOCK_STREAM: c_int = 1;
@@ -262,6 +262,6 @@ unsafe fn recv_nonblocking(so: SocketT, dst: &mut [u8]) -> usize {
unsafe extern "C" fn upcall(_so: SocketT, cookie: *mut c_void, _waitf: c_int) {
UPCALL_PENDING.fetch_add(1, Ordering::Release);
if !cookie.is_null() {
xnu::thread_wakeup(cookie as *const u8);
kernel::wake(cookie as *const u8);
}
}
+19
View File
@@ -0,0 +1,19 @@
// Facade over the host kernel the agent is running inside of. Both backends
// expose the same primitives, so the rest of the agent never names a specific
// kernel.
//
// XNU is reached from the outside: a remote stub injects the agent and patches
// the addresses it needs into .kernel_addrs. Linux is reached from the inside:
// the agent is a kernel module, so its glue is an ordinary C translation unit
// (linux/frida-kmod.c) compiled against the target kernel's headers by kbuild.
#[cfg(feature = "linux")]
pub use crate::linux::*;
#[cfg(feature = "win9x")]
pub use crate::win9x::*;
#[cfg(feature = "xnu")]
pub use crate::xnu::*;
use core::ffi::c_void;
pub type ThreadEntry = unsafe extern "C" fn(parameter: *mut c_void, wait_result: i32);
File diff suppressed because it is too large. Load diff
+545 -84
View File
@@ -1,35 +1,69 @@
// The C library's lower edge. Nothing here does real work: the agent has no
// filesystem and never exits, so these exist to satisfy references and to give
// malloc some memory to hand out.
//
// The two flavours link against different C libraries, and they disagree on what
// this layer is called. newlib asks for the underscore-prefixed syscall names;
// picolibc, which the soft-float flavour needs because newlib has no soft-float
// build for arm64, calls them without the prefix. The bodies are shared and the
// exported names live in the two `exports` modules at the end.
use core::ptr;
use core::sync::atomic::Ordering;
use crate::bindings::{GMutex, GRecMutex};
use crate::gthread;
static mut MALLOC_LOCK: GRecMutex = unsafe { core::mem::zeroed() };
// Serialises the C library. Which hooks the library reaches for differs: newlib asks
// for __malloc_lock/__malloc_unlock, picolibc for the retargetable lock API below,
// whose default implementation is an empty function — so on picolibc malloc runs
// unlocked unless we supply these, and any second thread corrupts the heap.
static mut LIBC_LOCK: GRecMutex = unsafe { core::mem::zeroed() };
#[unsafe(no_mangle)]
pub extern "C" fn __malloc_lock(_reent: *mut core::ffi::c_void) {
gthread::g_rec_mutex_lock(ptr::addr_of_mut!(MALLOC_LOCK));
fn libc_lock() {
gthread::g_rec_mutex_lock(ptr::addr_of_mut!(LIBC_LOCK));
}
#[unsafe(no_mangle)]
pub extern "C" fn __malloc_unlock(_reent: *mut core::ffi::c_void) {
gthread::g_rec_mutex_unlock(ptr::addr_of_mut!(MALLOC_LOCK));
fn libc_unlock() {
gthread::g_rec_mutex_unlock(ptr::addr_of_mut!(LIBC_LOCK));
offer_preemption_point();
}
#[unsafe(no_mangle)]
pub extern "C" fn _fini() {
fn offer_preemption_point() {
let total = ALLOCATIONS_TOTAL.fetch_add(1, Ordering::Relaxed) + 1;
if total % ALLOCATIONS_PER_YIELD == 0 {
crate::kernel::yield_now();
}
}
static ALLOCATIONS_TOTAL: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
const ALLOCATIONS_PER_YIELD: u32 = 1024;
// GLib's g_get_num_processors() probes this; report a single CPU so the
// parallel scanners fall back to their inline path.
#[unsafe(no_mangle)]
pub extern "C" fn _exit(_status: i32) -> ! {
loop {}
pub extern "C" fn sysconf(_name: i32) -> isize {
1
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[unsafe(no_mangle)]
pub extern "C" fn _kill(_pid: i32, _sig: i32) -> i32 {
-1
pub extern "C" fn __clear_cache(_start: *const u8, _end: *const u8) {}
#[cfg(target_arch = "aarch64")]
#[unsafe(no_mangle)]
pub extern "C" fn __clear_cache(_start: *const u8, _end: *const u8) {
unsafe {
core::arch::asm!(
"ic iallu", // Invalidate all instruction caches
"dsb sy", // Data synchronization barrier
"isb", // Instruction synchronization barrier
options(nomem, nostack),
);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
fn sbrk_impl(incr: isize) -> *mut u8 {
const HEAP_SIZE: usize = 32 * 1024 * 1024; // 32 MB
static mut HEAP_START: *mut u8 = ptr::null_mut();
@@ -40,7 +74,7 @@ pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
gthread::g_mutex_lock(ptr::addr_of_mut!(HEAP_MUTEX));
if HEAP_START.is_null() {
HEAP_START = crate::xnu::kalloc(HEAP_SIZE);
HEAP_START = crate::kernel::alloc(HEAP_SIZE);
if HEAP_START.is_null() {
gthread::g_mutex_unlock(ptr::addr_of_mut!(HEAP_MUTEX));
panic!("Failed to allocate 32 MB heap");
@@ -49,7 +83,8 @@ pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
}
let prev_heap_current = HEAP_CURRENT;
let new_heap_current = HEAP_CURRENT.add(incr as usize);
// Signed: the allocator hands memory back by asking for a negative increment.
let new_heap_current = HEAP_CURRENT.offset(incr);
if new_heap_current > HEAP_START.add(HEAP_SIZE) {
gthread::g_mutex_unlock(ptr::addr_of_mut!(HEAP_MUTEX));
@@ -65,20 +100,14 @@ pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
}
}
#[unsafe(no_mangle)]
pub extern "C" fn _getpid() -> i32 {
0
}
#[repr(C)]
struct Timeval {
tv_sec: i64,
tv_usec: i64,
}
#[unsafe(no_mangle)]
pub extern "C" fn _gettimeofday(tp: *mut core::ffi::c_void, _tzp: *mut core::ffi::c_void) -> i32 {
let (secs, microsecs) = crate::xnu::clock_get_calendar_microtime();
fn gettimeofday_impl(tp: *mut core::ffi::c_void) -> i32 {
let (secs, microsecs) = crate::kernel::wall_clock_micros();
let timeval = Timeval {
tv_sec: secs as i64,
tv_usec: microsecs as i64,
@@ -93,68 +122,500 @@ pub extern "C" fn _gettimeofday(tp: *mut core::ffi::c_void, _tzp: *mut core::ffi
0
}
#[unsafe(no_mangle)]
pub extern "C" fn _isatty(_fd: i32) -> i32 {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn _open(_path: *const u8, _flags: i32, _mode: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn _close(_fd: i32) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn _read(_fd: i32, _buf: *mut u8, _count: usize) -> isize {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn _write(_fd: i32, _buf: *const u8, count: usize) -> isize {
fn write_impl(count: usize) -> isize {
count as isize
}
#[unsafe(no_mangle)]
pub extern "C" fn _lseek(_fd: i32, _offset: isize, _whence: i32) -> isize {
0
}
#[cfg(feature = "xnu")]
mod exports {
use super::*;
#[unsafe(no_mangle)]
pub extern "C" fn _fstat(_fd: i32, _st: *mut core::ffi::c_void) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn _link(_old: *const u8, _new: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn _unlink(_path: *const u8) -> i32 {
-1
}
// GLib's g_get_num_processors() probes this; report a single CPU so the
// parallel scanners fall back to their inline path.
#[unsafe(no_mangle)]
pub extern "C" fn sysconf(_name: i32) -> isize {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn __clear_cache(_start: *const u8, _end: *const u8) {
// On AArch64, we need to flush the instruction cache
// This is a simplified implementation for the kernel environment
unsafe {
core::arch::asm!(
"ic iallu", // Invalidate all instruction caches
"dsb sy", // Data synchronization barrier
"isb", // Instruction synchronization barrier
options(nomem, nostack),
);
#[unsafe(no_mangle)]
pub extern "C" fn __malloc_lock(_reent: *mut core::ffi::c_void) {
libc_lock();
}
#[unsafe(no_mangle)]
pub extern "C" fn __malloc_unlock(_reent: *mut core::ffi::c_void) {
libc_unlock();
}
#[unsafe(no_mangle)]
pub extern "C" fn _fini() {}
#[unsafe(no_mangle)]
pub extern "C" fn _exit(_status: i32) -> ! {
loop {}
}
#[unsafe(no_mangle)]
pub extern "C" fn _kill(_pid: i32, _sig: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn _sbrk(incr: isize) -> *mut u8 {
sbrk_impl(incr)
}
#[unsafe(no_mangle)]
pub extern "C" fn _getpid() -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn _gettimeofday(
tp: *mut core::ffi::c_void,
_tzp: *mut core::ffi::c_void,
) -> i32 {
gettimeofday_impl(tp)
}
#[unsafe(no_mangle)]
pub extern "C" fn _isatty(_fd: i32) -> i32 {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn _open(_path: *const u8, _flags: i32, _mode: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn _close(_fd: i32) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn _read(_fd: i32, _buf: *mut u8, _count: usize) -> isize {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn _write(_fd: i32, _buf: *const u8, count: usize) -> isize {
write_impl(count)
}
#[unsafe(no_mangle)]
pub extern "C" fn _lseek(_fd: i32, _offset: isize, _whence: i32) -> isize {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn _fstat(_fd: i32, _st: *mut core::ffi::c_void) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn _stat(_path: *const u8, _st: *mut core::ffi::c_void) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn _link(_old: *const u8, _new: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn _unlink(_path: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn creat(_path: *const u8, _mode: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn access(_path: *const u8, _mode: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn chmod(_path: *const u8, _mode: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn chdir(_path: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn rmdir(_path: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn gethostname(_name: *mut u8, _len: usize) -> i32 {
-1
}
// newlib wires these up for exit(), which we never reach: the agent runs until
// the kernel goes down or the module is unloaded.
#[unsafe(no_mangle)]
pub static __libc_fini: Option<extern "C" fn()> = None;
#[unsafe(no_mangle)]
pub static __fini_array_start: [extern "C" fn(); 0] = [];
#[unsafe(no_mangle)]
pub static __fini_array_end: [extern "C" fn(); 0] = [];
}
#[cfg(any(feature = "linux", feature = "win9x"))]
mod exports {
use super::*;
// The C library's own allocator sits on a fixed sbrk heap and carries its own
// locking; inside a kernel module the kernel's allocator is already there, already
// safe to share between threads, and not capped at a size we picked up front.
// Every C allocation goes here instead.
//
// The sixteen bytes before the returned pointer hold the block the kernel gave us
// and its total size, so a pointer that was moved forward to satisfy an alignment
// request frees exactly like one that was not.
use core::ffi::c_void;
const HEAP_HEADER_SIZE: usize = 16;
const MALLOC_ALIGNMENT: usize = 2 * core::mem::size_of::<*const u8>();
#[unsafe(no_mangle)]
pub extern "C" fn malloc(size: usize) -> *mut c_void {
let total = size + HEAP_HEADER_SIZE + MALLOC_ALIGNMENT;
let block = crate::kernel::alloc(total);
if block.is_null() {
return ptr::null_mut();
}
offer_preemption_point();
unsafe {
let lowest = block.add(HEAP_HEADER_SIZE) as usize;
let aligned = (lowest + MALLOC_ALIGNMENT - 1) & !(MALLOC_ALIGNMENT - 1);
attach_header(block, total, aligned as *mut u8)
}
}
#[unsafe(no_mangle)]
pub extern "C" fn posix_memalign(
result: *mut *mut c_void,
alignment: usize,
size: usize,
) -> i32 {
let total = size + alignment + HEAP_HEADER_SIZE;
let block = crate::kernel::alloc(total);
if block.is_null() {
return 12; // ENOMEM
}
unsafe {
let lowest = block.add(HEAP_HEADER_SIZE) as usize;
let aligned = (lowest + alignment - 1) & !(alignment - 1);
*result = attach_header(block, total, aligned as *mut u8);
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn free(p: *mut c_void) {
if p.is_null() {
return;
}
unsafe {
let (block, total) = header_of(p);
crate::kernel::free(block, total);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn malloc_usable_size(p: *mut c_void) -> usize {
unsafe { usable_size(p) }
}
#[unsafe(no_mangle)]
pub extern "C" fn calloc(count: usize, size: usize) -> *mut c_void {
let total = count * size;
let p = malloc(total);
if !p.is_null() {
unsafe { ptr::write_bytes(p as *mut u8, 0, total) };
}
p
}
#[unsafe(no_mangle)]
pub extern "C" fn realloc(p: *mut c_void, size: usize) -> *mut c_void {
if p.is_null() {
return malloc(size);
}
let previous_size = unsafe { usable_size(p) };
let grown = malloc(size);
if !grown.is_null() {
unsafe {
ptr::copy_nonoverlapping(p as *const u8, grown as *mut u8, previous_size.min(size))
};
free(p);
}
grown
}
unsafe fn attach_header(block: *mut u8, total: usize, p: *mut u8) -> *mut c_void {
unsafe {
let header = p.sub(HEAP_HEADER_SIZE) as *mut usize;
*header = block as usize;
*header.add(1) = total;
}
p as *mut c_void
}
unsafe fn header_of(p: *mut c_void) -> (*mut u8, usize) {
unsafe {
let header = (p as *mut u8).sub(HEAP_HEADER_SIZE) as *const usize;
(*header as *mut u8, *header.add(1))
}
}
unsafe fn usable_size(p: *mut c_void) -> usize {
unsafe {
let (block, total) = header_of(p);
total - (p as usize - block as usize)
}
}
// One recursive mutex covers every lock the C library asks for. They are coarse
// and few, and over-serialising them is safe where getting it wrong is not.
type Lock = *mut core::ffi::c_void;
// The lock object itself, which we define so the C library's own no-op lock
// implementation is never pulled out of its archive. Its contents are never
// read: the address is just a token, and one mutex covers them all.
// Nothing here references it, so without #[used] it is dropped before the linker
// ever sees it, and the archive member arrives after all to supply it.
#[allow(non_upper_case_globals)]
#[unsafe(no_mangle)]
#[used]
pub static mut __lock___libc_recursive_mutex: u8 = 0;
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_init(_lock: *mut Lock) {}
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_init_recursive(_lock: *mut Lock) {}
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_close(_lock: Lock) {}
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_close_recursive(_lock: Lock) {}
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_acquire(_lock: Lock) {
libc_lock();
}
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_acquire_recursive(_lock: Lock) {
libc_lock();
}
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_release(_lock: Lock) {
libc_unlock();
}
#[unsafe(no_mangle)]
pub extern "C" fn __retarget_lock_release_recursive(_lock: Lock) {
libc_unlock();
}
#[unsafe(no_mangle)]
pub extern "C" fn exit(_status: i32) -> ! {
loop {}
}
#[unsafe(no_mangle)]
pub extern "C" fn _exit(_status: i32) -> ! {
loop {}
}
#[unsafe(no_mangle)]
pub extern "C" fn kill(_pid: i32, _sig: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn sbrk(incr: isize) -> *mut u8 {
sbrk_impl(incr)
}
#[unsafe(no_mangle)]
pub extern "C" fn getpid() -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn gettimeofday(
tp: *mut core::ffi::c_void,
_tzp: *mut core::ffi::c_void,
) -> i32 {
gettimeofday_impl(tp)
}
#[unsafe(no_mangle)]
pub extern "C" fn isatty(_fd: i32) -> i32 {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn open(_path: *const u8, _flags: i32, _mode: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn close(_fd: i32) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn read(_fd: i32, _buf: *mut u8, _count: usize) -> isize {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn write(_fd: i32, _buf: *const u8, count: usize) -> isize {
write_impl(count)
}
#[unsafe(no_mangle)]
pub extern "C" fn lseek(_fd: i32, _offset: isize, _whence: i32) -> isize {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fstat(_fd: i32, _st: *mut core::ffi::c_void) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn stat(_path: *const u8, _st: *mut core::ffi::c_void) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn unlink(_path: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn rename(_old: *const u8, _new: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn access(_path: *const u8, _mode: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn chmod(_path: *const u8, _mode: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn chdir(_path: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn rmdir(_path: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn gethostname(_name: *mut u8, _len: usize) -> i32 {
-1
}
#[repr(C)]
pub struct Timespec {
tv_sec: i64,
tv_nsec: i64,
}
const CLOCK_MONOTONIC: i32 = 1;
#[unsafe(no_mangle)]
pub extern "C" fn clock_gettime(clock_id: i32, tp: *mut Timespec) -> i32 {
let (secs, nsecs) = if clock_id == CLOCK_MONOTONIC {
let micros = crate::kernel::monotonic_micros();
(micros / 1_000_000, (micros % 1_000_000) * 1000)
} else {
let (secs, micros) = crate::kernel::wall_clock_micros();
(secs as i64, (micros as i64) * 1000)
};
unsafe {
(*tp).tv_sec = secs;
(*tp).tv_nsec = nsecs;
}
0
}
// picolibc consults this for errno values it does not know itself.
#[unsafe(no_mangle)]
pub extern "C" fn _user_strerror(_errnum: i32) -> *const core::ffi::c_char {
ptr::null()
}
// No dynamic loader here. Gum calls dladdr() to describe an address and already
// falls back to printing it raw when the lookup fails.
#[unsafe(no_mangle)]
pub extern "C" fn dladdr(_address: *const core::ffi::c_void, _info: *mut core::ffi::c_void) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn dlopen(_file: *const u8, _mode: i32) -> *mut core::ffi::c_void {
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn dlsym(
_handle: *mut core::ffi::c_void,
_name: *const u8,
) -> *mut core::ffi::c_void {
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn dlclose(_handle: *mut core::ffi::c_void) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn dlerror() -> *mut core::ffi::c_char {
ptr::null_mut()
}
// picolibc declares the standard streams and leaves defining them to the
// application. The agent has no I/O — its logging goes to the kernel's own log —
// so nothing should ever reach for one of these.
#[repr(transparent)]
pub struct Stream(*mut core::ffi::c_void);
unsafe impl Sync for Stream {}
#[unsafe(no_mangle)]
pub static stdin: Stream = Stream(ptr::null_mut());
#[unsafe(no_mangle)]
pub static stdout: Stream = Stream(ptr::null_mut());
#[unsafe(no_mangle)]
pub static stderr: Stream = Stream(ptr::null_mut());
}
+244
View File
@@ -0,0 +1,244 @@
// Linux backend. Unlike XNU — where a remote stub injects the agent and patches
// resolved kernel addresses into .kernel_addrs — here the agent is loaded from
// the inside as a kernel module, so every primitive is an ordinary call into
// linux/frida-kmod.c. That shim is compiled by kbuild against the target
// kernel's headers, which is what keeps us out of the business of guessing
// struct layouts, config-dependent macros and per-version symbol names.
use core::ffi::{CStr, c_char, c_int, c_void};
use alloc::string::String;
use alloc::vec::Vec;
use crate::kernel::ThreadEntry;
pub fn log(msg: &str) {
unsafe { frida_kmod_log(msg.as_ptr() as *const c_char) };
}
pub fn panic(msg: &str) -> ! {
unsafe { frida_kmod_panic(msg.as_ptr() as *const c_char) }
}
pub fn run_when_ready(action: fn()) {
action();
}
pub fn spawn_thread(entry: ThreadEntry, parameter: *mut c_void) -> isize {
unsafe { frida_kmod_spawn_thread(entry, parameter) as isize }
}
pub fn install_hooks() {
unsafe { frida_kmod_install_hooks() };
}
pub fn alloc(size: usize) -> *mut u8 {
unsafe { frida_kmod_alloc(size) }
}
pub fn free(ptr: *mut u8, size: usize) {
unsafe { frida_kmod_free(ptr, size) };
}
pub fn alloc_code(size: usize) -> *mut u8 {
unsafe { frida_kmod_alloc_code(size) }
}
pub fn free_code(ptr: *mut u8, size: usize) {
unsafe { frida_kmod_free_code(ptr, size) };
}
pub fn own_range() -> (u64, u64) {
let mut base: u64 = 0;
let mut size: u64 = 0;
unsafe { frida_kmod_own_range(&mut base, &mut size) };
(base, size)
}
// The sequence number is taken before `check` runs, so a wakeup racing with the
// condition we are about to observe still makes the commit return immediately.
pub fn wait(_token: *const u8, timeout_us: Option<u64>, check: &mut dyn FnMut() -> bool) {
let seq = unsafe { frida_kmod_wait_prepare() };
if check() {
return;
}
unsafe { frida_kmod_wait_commit(seq, timeout_us.map_or(-1, |us| us as i64)) };
}
pub fn wake(_token: *const u8) {
unsafe { frida_kmod_wake() };
}
/// Gives the scheduler a chance to run something else. A kernel thread that loops
/// without doing this is a hard lockup waiting to happen.
pub fn yield_now() {
unsafe { frida_kmod_yield() };
}
pub fn monotonic_micros() -> i64 {
unsafe { frida_kmod_monotonic_micros() }
}
pub fn wall_clock_micros() -> (u32, u32) {
let mut secs: u32 = 0;
let mut micros: u32 = 0;
unsafe { frida_kmod_wall_clock_micros(&mut secs, &mut micros) };
(secs, micros)
}
pub fn current_thread_id() -> u64 {
unsafe { frida_kmod_current_thread_id() }
}
pub fn protect(address: u64, size: usize, gum_prot: u32) -> bool {
unsafe { frida_kmod_protect(address, size, gum_prot) != 0 }
}
pub fn remap_writable(first_page: u64, n_pages: u32) -> *mut c_void {
unsafe { frida_kmod_remap_writable(first_page, n_pages) }
}
/// Tears down an alias handed out by [`remap_writable`] and flushes the icache
/// over the range it aliased; the shim remembers which range that was.
pub fn unmap_writable(mapping: *mut c_void) {
unsafe { frida_kmod_unmap_writable(mapping) };
}
pub fn get_kernel_base() -> u64 {
unsafe { frida_kmod_kernel_base() }
}
pub fn get_kernel_size() -> u64 {
unsafe { frida_kmod_kernel_size() }
}
pub fn enumerate_modules() -> Vec<LoadedModule> {
unsafe extern "C" fn on_module(
name: *const c_char,
version: *const c_char,
base: u64,
size: u64,
user_data: *mut c_void,
) -> c_int {
unsafe {
let modules = &mut *(user_data as *mut Vec<LoadedModule>);
modules.push(LoadedModule {
name: String::from(CStr::from_ptr(name).to_str().unwrap_or("?")),
version: String::from(CStr::from_ptr(version).to_str().unwrap_or("")),
base,
size,
});
1
}
}
let mut modules: Vec<LoadedModule> = Vec::new();
unsafe {
frida_kmod_enumerate_modules(on_module, &mut modules as *mut _ as *mut c_void);
}
modules
}
#[derive(Debug, Clone)]
pub struct LoadedModule {
pub name: String,
pub version: String,
pub base: u64,
pub size: u64,
}
pub fn find_symbol(name: &CStr) -> u64 {
unsafe { frida_kmod_find_symbol(name.as_ptr()) }
}
pub fn find_function(name: &CStr) -> u64 {
unsafe { frida_kmod_find_function(name.as_ptr()) }
}
/// Resolves `address` to `(name, symbol_address)`, or `None` when it falls
/// outside every known symbol.
pub fn symbol_from_address(address: u64) -> Option<(String, u64)> {
const NAME_CAPACITY: usize = 256;
let mut buffer = [0u8; NAME_CAPACITY];
let mut symbol_address: u64 = 0;
let found = unsafe {
frida_kmod_symbol_name_from_address(
address,
buffer.as_mut_ptr() as *mut c_char,
NAME_CAPACITY,
&mut symbol_address,
)
};
if found == 0 {
return None;
}
let name = CStr::from_bytes_until_nul(&buffer).unwrap();
Some((String::from(name.to_str().unwrap()), symbol_address))
}
/// Visits every symbol the running kernel knows about. Returns false when
/// kallsyms is unavailable, which is the case on kernels built without
/// CONFIG_KALLSYMS or when the shim could not resolve its entry points.
pub fn enumerate_symbols(callback: &mut dyn FnMut(&CStr, u64) -> bool) -> bool {
unsafe extern "C" fn on_symbol(
name: *const c_char,
address: u64,
user_data: *mut c_void,
) -> c_int {
unsafe {
let callback = &mut *(user_data as *mut &mut dyn FnMut(&CStr, u64) -> bool);
if callback(CStr::from_ptr(name), address) { 1 } else { 0 }
}
}
let mut boxed: &mut dyn FnMut(&CStr, u64) -> bool = callback;
unsafe { frida_kmod_enumerate_symbols(on_symbol, &mut boxed as *mut _ as *mut c_void) != 0 }
}
type FoundSymbolFunc =
unsafe extern "C" fn(name: *const c_char, address: u64, user_data: *mut c_void) -> c_int;
type FoundModuleFunc = unsafe extern "C" fn(
name: *const c_char,
version: *const c_char,
base: u64,
size: u64,
user_data: *mut c_void,
) -> c_int;
unsafe extern "C" {
fn frida_kmod_log(message: *const c_char);
fn frida_kmod_panic(message: *const c_char) -> !;
fn frida_kmod_spawn_thread(entry: ThreadEntry, parameter: *mut c_void) -> c_int;
fn frida_kmod_install_hooks();
fn frida_kmod_alloc(size: usize) -> *mut u8;
fn frida_kmod_free(ptr: *mut u8, size: usize);
fn frida_kmod_alloc_code(size: usize) -> *mut u8;
fn frida_kmod_free_code(ptr: *mut u8, size: usize);
fn frida_kmod_own_range(base: *mut u64, size: *mut u64);
fn frida_kmod_wait_prepare() -> u32;
fn frida_kmod_wait_commit(seq: u32, timeout_us: i64);
fn frida_kmod_wake();
fn frida_kmod_yield();
fn frida_kmod_monotonic_micros() -> i64;
fn frida_kmod_wall_clock_micros(secs: *mut u32, micros: *mut u32);
fn frida_kmod_current_thread_id() -> u64;
fn frida_kmod_protect(address: u64, size: usize, gum_prot: u32) -> c_int;
fn frida_kmod_remap_writable(first_page: u64, n_pages: u32) -> *mut c_void;
fn frida_kmod_unmap_writable(mapping: *mut c_void);
fn frida_kmod_kernel_base() -> u64;
fn frida_kmod_kernel_size() -> u64;
fn frida_kmod_enumerate_modules(func: FoundModuleFunc, user_data: *mut c_void);
fn frida_kmod_find_symbol(name: *const c_char) -> u64;
fn frida_kmod_find_function(name: *const c_char) -> u64;
fn frida_kmod_symbol_name_from_address(
address: u64,
buffer: *mut c_char,
size: usize,
symbol_address: *mut u64,
) -> c_int;
fn frida_kmod_enumerate_symbols(func: FoundSymbolFunc, user_data: *mut c_void) -> c_int;
}
+10 -691
View File
@@ -1,693 +1,12 @@
#![no_main]
// The XNU flavour ships as a freestanding ELF executable: the host injects the
// image and calls its `_start`. All of the agent lives in the library crate;
// this target exists only to produce that link. build.rs passes `--undefined
// _start` so the entrypoint survives --gc-sections.
//
// The Linux flavour is built as a static library instead and linked into a
// kernel module by kbuild — see linux/Kbuild.
#![no_std]
#![no_main]
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::alloc::{GlobalAlloc, Layout};
use core::ffi::{CStr, c_void};
use core::ptr;
use core::ptr::null_mut;
use core::sync::atomic::{AtomicU32, Ordering};
use bindings::{
GBytes, GCancellable, GError, GVariant, GVariantIter, GumScript, g_error_free, g_free,
g_main_context_default, g_main_context_iteration, g_memdup2, g_object_unref,
g_variant_check_format_string, g_variant_get, g_variant_get_byte, g_variant_get_child_value,
g_variant_get_data, g_variant_get_size, g_variant_get_string, g_variant_get_uint32,
g_variant_get_uint64, g_variant_get_variant, g_variant_iter_init, g_variant_iter_next,
g_variant_new, g_variant_new_from_data, g_variant_new_string, g_variant_new_tuple,
g_variant_new_uint32, g_variant_type_free, g_variant_type_new, g_variant_unref, gchar,
gpointer, gsize, gum_script_backend_create_sync, gum_script_backend_obtain_qjs,
gum_script_get_stalker, gum_script_load_sync, gum_script_post, gum_script_set_message_handler,
gum_script_unload_sync, gum_stalker_exclude, GumMemoryRange,
};
use symbols::SymbolTable;
mod ffi;
mod glib;
mod gthread;
mod gum;
mod hostlink_virtio;
mod hostlink_vsock;
mod libc;
mod pac;
mod symbols;
mod xnu;
mod bindings {
#![allow(
dead_code,
improper_ctypes,
non_camel_case_types,
non_snake_case,
non_upper_case_globals,
unused_imports
)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}
#[repr(u8)]
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum FridaCommand {
CreateScript = 1,
LoadScript = 2,
DestroyScript = 3,
PostScriptMessage = 4,
RemapWritablePages = 5,
MemoryProtect = 6,
PatchCode = 7,
Reply = 128,
ScriptMessage = 129,
}
impl core::fmt::Display for FridaCommand {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
FridaCommand::CreateScript => write!(f, "CreateScript"),
FridaCommand::LoadScript => write!(f, "LoadScript"),
FridaCommand::DestroyScript => write!(f, "DestroyScript"),
FridaCommand::PostScriptMessage => write!(f, "PostScriptMessage"),
FridaCommand::RemapWritablePages => write!(f, "RemapWritablePages"),
FridaCommand::MemoryProtect => write!(f, "MemoryProtect"),
FridaCommand::PatchCode => write!(f, "PatchCode"),
FridaCommand::Reply => write!(f, "Reply"),
FridaCommand::ScriptMessage => write!(f, "ScriptMessage"),
}
}
}
#[derive(Debug)]
struct HandlerResponse {
variant: *mut GVariant,
}
impl HandlerResponse {
fn success(variant: *mut GVariant) -> Self {
Self { variant }
}
fn success_empty() -> Self {
let variant = unsafe { g_variant_new_tuple(ptr::null(), 0) };
Self { variant }
}
fn error(message: &str) -> Self {
let mut c_message = String::from(message);
c_message.push('\0');
let error_variant = unsafe { g_variant_new_string(c_message.as_ptr()) };
Self {
variant: error_variant,
}
}
}
static mut CONFIG_DATA: &'static [u8] = &[];
pub static mut MODULE_INFO: Vec<ModuleInfo> = Vec::new();
pub static mut SYMBOL_TABLE: SymbolTable = SymbolTable::empty();
pub enum Transport {
Virtio(hostlink_virtio::Hostlink),
Vsock(hostlink_vsock::Hostlink),
}
impl Transport {
pub fn send(&self, payload: &[u8]) {
match self {
Transport::Virtio(h) => h.send(payload),
Transport::Vsock(h) => h.send(payload),
}
}
pub fn process(&self) {
match self {
Transport::Virtio(h) => h.process(),
Transport::Vsock(h) => h.process(),
}
}
}
pub enum TransportConfig {
Virtio { mmio: u64, irq: u32 },
Vsock { host_port: u32 },
}
static mut TRANSPORT_DRIVER: *mut Transport = core::ptr::null_mut();
#[inline(always)]
fn transport_set(driver: Transport) {
unsafe {
let boxed = Box::into_raw(Box::new(driver));
TRANSPORT_DRIVER = boxed;
}
}
#[inline(always)]
fn transport_get_unchecked() -> &'static Transport {
unsafe {
debug_assert!(!TRANSPORT_DRIVER.is_null());
&*TRANSPORT_DRIVER
}
}
#[inline(always)]
fn transport_is_up() -> bool {
unsafe { !TRANSPORT_DRIVER.is_null() }
}
static mut SCRIPTS: BTreeMap<u32, *mut GumScript> = BTreeMap::new();
static NEXT_SCRIPT_ID: AtomicU32 = AtomicU32::new(1);
static mut OWN_RANGE: GumMemoryRange = GumMemoryRange {
base_address: 0,
size: 0,
};
static NEXT_REQUEST_ID: AtomicU32 = AtomicU32::new(1);
static mut PENDING_REPLIES: BTreeMap<u16, *mut GVariant> = BTreeMap::new();
#[derive(Debug, Clone)]
pub struct ModuleInfo {
pub name: String,
pub version: String,
pub offset: u32,
pub size: u32,
pub start_func_offset: u32,
pub stop_func_offset: u32,
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn _start(config_data: *const u8, config_size: usize) {
unsafe {
xnu::io_log("frida: _start entry\n\0");
CONFIG_DATA = core::slice::from_raw_parts(config_data, config_size);
xnu::io_log("frida: _start config stored\n\0");
let _r = xnu::kernel_thread_start(frida_agent_worker, 12345usize as *mut core::ffi::c_void);
}
}
// KNOWN ISSUE (vphone research kernel): the worker panics during gum_init_embedded with
// "debug exceptions enabled in kernel mode". The worker itself is healthy — it runs with
// PSTATE.D=1 (debug masked) throughout. The panic is the research kernel's synchronous-
// exception handler reacting to ANY exception the agent takes in kernel mode while the VZ
// debug stub is active. Two confirmed sources: (1) the host's breakpoint-based Callbacks
// patch BRK at gum_try_mprotect/remap, and the BRK is a debug exception; (2) a stray jump
// into the agent's data region during GObject init (instruction abort) — looks like a
// misapplied relocation for a function pointer stored in .data/.rodata.
unsafe extern "C" fn frida_agent_worker(_parameter: *mut core::ffi::c_void, _wait_result: i32) {
unsafe {
xnu::io_log("frida: worker entry\n\0");
bindings::g_set_panic_handler(Some(frida_panic_handler), ptr::null_mut());
bindings::gum_init_embedded();
xnu::io_log("frida: gum_init_embedded done\n\0");
bindings::g_log_set_default_handler(Some(frida_log_handler), ptr::null_mut());
let (transport_config, kernel_base, module_info, symbol_table, own_range) =
parse_config(core::ptr::addr_of!(CONFIG_DATA).read());
xnu::set_kernel_base(kernel_base);
MODULE_INFO = module_info;
SYMBOL_TABLE = symbol_table;
OWN_RANGE = own_range;
xnu::io_log("frida: config parsed, init transport\n\0");
let wake_token = ptr::addr_of_mut!(glib::WAKEUP_TOKEN) as *const u8;
let transport = match transport_config {
TransportConfig::Virtio { mmio, irq } => Transport::Virtio(
hostlink_virtio::Hostlink::init(mmio, irq, Some(on_frame_from_host), wake_token)
.unwrap(),
),
TransportConfig::Vsock { host_port } => Transport::Vsock(
hostlink_vsock::Hostlink::init(host_port, Some(on_frame_from_host), wake_token)
.unwrap(),
),
};
transport_set(transport);
xnu::io_log("frida: transport up, entering main loop\n\0");
let main_context = g_main_context_default();
loop {
transport_get_unchecked().process();
g_main_context_iteration(main_context, 1);
}
}
}
fn on_frame_from_host(frame: &[u8]) {
if let Some(variant) = deserialize_message(&frame) {
process_incoming_message(variant);
unsafe {
g_variant_unref(variant);
}
}
}
unsafe fn parse_config(
config: &[u8],
) -> (TransportConfig, u64, Vec<ModuleInfo>, SymbolTable, GumMemoryRange) {
unsafe {
let type_string = c"((tt)yvta(ssuuuu)ay)".as_ptr() as *const gchar;
let variant_type = g_variant_type_new(type_string);
let root_variant = g_variant_new_from_data(
variant_type,
config.as_ptr() as *const core::ffi::c_void,
config.len() as gsize,
1,
None,
ptr::null_mut(),
);
let transport_kind_variant = g_variant_get_child_value(root_variant, 1);
let transport_kind = g_variant_get_byte(transport_kind_variant);
let transport_cfg_outer = g_variant_get_child_value(root_variant, 2);
let transport_cfg_inner = g_variant_get_variant(transport_cfg_outer);
let transport_config = match transport_kind {
0 => {
let mmio_variant = g_variant_get_child_value(transport_cfg_inner, 0);
let mmio = g_variant_get_uint64(mmio_variant);
let irq_variant = g_variant_get_child_value(transport_cfg_inner, 1);
let irq = g_variant_get_uint32(irq_variant);
g_variant_unref(irq_variant);
g_variant_unref(mmio_variant);
TransportConfig::Virtio { mmio, irq }
}
1 => {
let host_port = g_variant_get_uint32(transport_cfg_inner);
TransportConfig::Vsock { host_port }
}
_ => panic!("Unsupported transport kind: {}", transport_kind),
};
g_variant_unref(transport_cfg_inner);
g_variant_unref(transport_cfg_outer);
g_variant_unref(transport_kind_variant);
let kernel_base_variant = g_variant_get_child_value(root_variant, 3);
let kernel_base = g_variant_get_uint64(kernel_base_variant);
xnu::set_kernel_base(kernel_base);
let module_info_variant = g_variant_get_child_value(root_variant, 4);
let mut iter: GVariantIter = core::mem::zeroed();
g_variant_iter_init(&mut iter as *mut GVariantIter, module_info_variant);
let mut module_info: Vec<ModuleInfo> = Vec::new();
let mut raw_name: *mut gchar = null_mut();
let mut raw_version: *mut gchar = null_mut();
let mut offset: u32 = 0;
let mut size: u32 = 0;
let mut start_func_offset: u32 = 0;
let mut stop_func_offset: u32 = 0;
while g_variant_iter_next(
&mut iter as *mut GVariantIter,
c"(&s&suuuu)".as_ptr(),
&mut raw_name,
&mut raw_version,
&mut offset,
&mut size,
&mut start_func_offset,
&mut stop_func_offset,
) != 0
{
module_info.push(ModuleInfo {
name: String::from(CStr::from_ptr(raw_name).to_str().unwrap()),
version: String::from(CStr::from_ptr(raw_version).to_str().unwrap()),
offset,
size,
start_func_offset,
stop_func_offset,
});
}
let symbol_array_variant = g_variant_get_child_value(root_variant, 5);
let symbol_data_ptr = g_variant_get_data(symbol_array_variant) as *const u8;
let symbol_data_size = g_variant_get_size(symbol_array_variant) as usize;
let symbol_table = SymbolTable::new(core::slice::from_raw_parts(
symbol_data_ptr,
symbol_data_size,
));
let own_range_variant = g_variant_get_child_value(root_variant, 0);
let mut own_base: u64 = 0;
let mut own_size: u64 = 0;
g_variant_get(
own_range_variant,
c"(tt)".as_ptr(),
&mut own_base,
&mut own_size,
);
let own_range = GumMemoryRange {
base_address: own_base,
size: own_size as gsize,
};
g_variant_unref(own_range_variant);
g_variant_unref(symbol_array_variant);
g_variant_unref(module_info_variant);
g_variant_unref(kernel_base_variant);
g_variant_unref(root_variant);
g_variant_type_free(variant_type);
(transport_config, kernel_base, module_info, symbol_table, own_range)
}
}
unsafe fn serialize_message(variant: *mut GVariant) -> Option<Vec<u8>> {
unsafe {
let size = g_variant_get_size(variant) as usize;
if size == 0 {
return None;
}
let data_ptr = g_variant_get_data(variant) as *const u8;
let mut result = Vec::with_capacity(size);
result.resize(size, 0);
core::ptr::copy_nonoverlapping(data_ptr, result.as_mut_ptr(), size);
Some(result)
}
}
fn deserialize_message(data: &[u8]) -> Option<*mut GVariant> {
unsafe {
if data.is_empty() {
return None;
}
let variant_type = g_variant_type_new(c"(yqv)".as_ptr());
let data_copy = g_memdup2(data.as_ptr() as *const c_void, data.len() as u64);
let variant = g_variant_new_from_data(
variant_type,
data_copy,
data.len() as u64,
0,
Some(g_free),
data_copy,
);
g_variant_type_free(variant_type);
if variant.is_null() {
None
} else {
Some(variant)
}
}
}
fn process_incoming_message(variant: *mut GVariant) {
{
let mut cmd_value: u8 = 0;
let mut request_id: u16 = 0;
let mut payload_variant: *mut GVariant = ptr::null_mut();
let cmd = unsafe {
g_variant_get(
variant,
c"(yqv)".as_ptr(),
&mut cmd_value,
&mut request_id,
&mut payload_variant,
);
core::mem::transmute::<u8, FridaCommand>(cmd_value)
};
if cmd == FridaCommand::Reply {
unsafe {
core::ptr::addr_of_mut!(PENDING_REPLIES)
.as_mut()
.unwrap()
.insert(request_id, payload_variant);
}
return;
}
let response = match cmd {
FridaCommand::CreateScript => handle_create_script(payload_variant),
FridaCommand::LoadScript => handle_load_script(payload_variant),
FridaCommand::DestroyScript => handle_destroy_script(payload_variant),
FridaCommand::PostScriptMessage => handle_post_script_message(payload_variant),
_ => HandlerResponse::error("Unknown command"),
};
send_command_reply(request_id, response);
unsafe { g_variant_unref(payload_variant) };
}
}
fn send_command_reply(request_id: u16, response: HandlerResponse) {
unsafe {
let message = g_variant_new(
c"(yqv)".as_ptr(),
FridaCommand::Reply as u8 as u32,
request_id as u32,
response.variant,
);
if let Some(serialized) = serialize_message(message) {
transport_get_unchecked().send(&serialized);
}
g_variant_unref(message);
}
}
pub fn host_rpc(command: FridaCommand, payload: *mut GVariant) -> *mut GVariant {
unsafe {
let request_id = NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed) as u16;
let message = g_variant_new(
c"(yqv)".as_ptr(),
command as u8 as u32,
request_id as u32,
payload,
);
let transport = transport_get_unchecked();
transport.send(&serialize_message(message).unwrap());
g_variant_unref(message);
let wait_event = ptr::addr_of_mut!(glib::WAKEUP_TOKEN) as *const u8;
loop {
xnu::assert_wait(wait_event, xnu::THREAD_INTERRUPTIBLE);
transport.process();
if let Some(reply) = take_pending_reply(request_id) {
xnu::thread_wakeup(wait_event);
return reply;
}
xnu::thread_block(None);
}
}
}
fn take_pending_reply(request_id: u16) -> Option<*mut GVariant> {
unsafe {
core::ptr::addr_of_mut!(PENDING_REPLIES)
.as_mut()
.unwrap()
.remove(&request_id)
}
}
fn handle_create_script(payload_variant: *mut GVariant) -> HandlerResponse {
unsafe {
if g_variant_check_format_string(payload_variant, c"s".as_ptr(), 0) == 0 {
return HandlerResponse::error("Invalid payload format: expected string");
}
let source = g_variant_get_string(payload_variant, core::ptr::null_mut());
let backend = gum_script_backend_obtain_qjs();
let cancellable: *mut GCancellable = ptr::null_mut();
let mut error: *mut GError = ptr::null_mut();
let script = gum_script_backend_create_sync(
backend,
c"agent.js".as_ptr(),
source,
ptr::null_mut(),
cancellable,
&mut error,
);
if !error.is_null() {
let error_msg = CStr::from_ptr((*error).message).to_str().unwrap();
let error_string = String::from(error_msg);
g_error_free(error);
return HandlerResponse::error(&error_string);
}
exclude_own_range_from_stalker(script);
let script_id = NEXT_SCRIPT_ID.fetch_add(1, Ordering::Relaxed);
let script_id_ptr = Box::into_raw(Box::new(script_id));
gum_script_set_message_handler(
script,
Some(frida_message_handler),
script_id_ptr as *mut c_void,
None,
);
core::ptr::addr_of_mut!(SCRIPTS)
.as_mut()
.unwrap()
.insert(script_id, script);
HandlerResponse::success(g_variant_new_uint32(script_id))
}
}
// Stalker must not instrument our own runtime: when it follows the current
// thread into the hostlink transport it would deadlock on the locks that the
// RPC it depends on is holding. The host injected the image, so it tells us our
// range as part of the config.
unsafe fn exclude_own_range_from_stalker(script: *mut GumScript) {
unsafe {
gum_stalker_exclude(gum_script_get_stalker(script), core::ptr::addr_of!(OWN_RANGE));
}
}
unsafe extern "C" fn frida_message_handler(
message: *const gchar,
_data: *mut GBytes,
user_data: gpointer,
) {
unsafe {
let script_id = *(user_data as *const u32);
let message_variant = g_variant_new(
c"(yqv)".as_ptr(),
FridaCommand::ScriptMessage as u8 as u32,
0u32,
g_variant_new(c"(us)".as_ptr(), script_id, message),
);
if let Some(serialized) = serialize_message(message_variant) {
transport_get_unchecked().send(&serialized);
}
g_variant_unref(message_variant);
}
}
fn handle_load_script(payload_variant: *mut GVariant) -> HandlerResponse {
unsafe {
if g_variant_check_format_string(payload_variant, c"u".as_ptr(), 0) == 0 {
return HandlerResponse::error("Invalid payload format: expected uint32");
}
let script_id = g_variant_get_uint32(payload_variant);
let Some(script) = get_script_by_id(script_id) else {
return HandlerResponse::error(&format!("Script with ID {} not found", script_id));
};
gum_script_load_sync(script, ptr::null_mut());
HandlerResponse::success_empty()
}
}
fn handle_destroy_script(payload_variant: *mut GVariant) -> HandlerResponse {
unsafe {
if g_variant_check_format_string(payload_variant, c"u".as_ptr(), 0) == 0 {
return HandlerResponse::error("Invalid payload format: expected uint32");
}
let script_id = g_variant_get_uint32(payload_variant);
let scripts = core::ptr::addr_of_mut!(SCRIPTS).as_mut().unwrap();
let Some(script) = scripts.remove(&script_id) else {
return HandlerResponse::error(&format!("Script with ID {} not found", script_id));
};
gum_script_unload_sync(script, ptr::null_mut());
g_object_unref(script as *mut c_void);
HandlerResponse::success_empty()
}
}
fn handle_post_script_message(payload_variant: *mut GVariant) -> HandlerResponse {
unsafe {
if g_variant_check_format_string(payload_variant, c"(us)".as_ptr(), 0) == 0 {
return HandlerResponse::error("Invalid payload format: expected (uint32, string)");
}
let mut script_id: u32 = 0;
let mut message: *const gchar = ptr::null();
g_variant_get(
payload_variant,
c"(u&s)".as_ptr(),
&mut script_id,
&mut message,
);
let Some(script) = get_script_by_id(script_id) else {
return HandlerResponse::error(&format!("Script with ID {} not found", script_id));
};
gum_script_post(script, message, ptr::null_mut());
HandlerResponse::success_empty()
}
}
unsafe fn get_script_by_id(script_id: u32) -> Option<*mut GumScript> {
unsafe {
let scripts = core::ptr::addr_of_mut!(SCRIPTS).as_mut().unwrap();
scripts.get(&script_id).copied()
}
}
unsafe extern "C" fn frida_panic_handler(message: *const u8, _user_data: *mut core::ffi::c_void) {
let msg = unsafe { core::ffi::CStr::from_ptr(message).to_str().unwrap() };
panic!("[Frida] {}", msg);
}
unsafe extern "C" fn frida_log_handler(
_log_domain: *const core::ffi::c_char,
_log_level: i32,
message: *const core::ffi::c_char,
_user_data: *mut core::ffi::c_void,
) {
let msg = unsafe { core::ffi::CStr::from_ptr(message).to_str().unwrap() };
kprintln!("[Frida] {}", msg);
}
#[panic_handler]
fn panic(info: &core::panic::PanicInfo<'_>) -> ! {
let mut s = format!("{}", info);
s.push('\0');
xnu::panic(s.as_str());
loop {}
}
pub struct XnuAllocator;
unsafe impl GlobalAlloc for XnuAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
xnu::kalloc(layout.size())
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
xnu::free(ptr, layout.size());
}
}
#[global_allocator]
static GLOBAL: XnuAllocator = XnuAllocator;
extern crate alloc;
#[macro_export]
macro_rules! kprintln {
($($arg:tt)*) => {{
use core::fmt::Write;
let mut buf = alloc::string::String::new();
write!(&mut buf, $($arg)*).unwrap();
buf.push('\n');
buf.push('\0');
crate::xnu::io_log(&buf)
}};
}
use frida_barebone_agent as _;
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::bindings::{
GPatternSpec, g_pattern_spec_free, g_pattern_spec_match_string, g_pattern_spec_new,
};
use crate::xnu::get_kernel_base;
use crate::kernel::get_kernel_base;
use alloc::ffi::CString;
use core::cmp::Ordering;
use core::ffi::{CStr, c_char};
+922
View File
@@ -0,0 +1,922 @@
// The VMM side of the blob flavor. Reached the way XNU is: the host fills in a
// slot per service before calling _start, so nothing here is a link-time import.
//
// The signatures follow the VxD service documentation; none of them has been
// exercised against a running kernel yet.
use core::ffi::c_void;
use core::sync::atomic::{AtomicU32, Ordering};
use crate::kernel::ThreadEntry;
const PAGE_SIZE: u32 = 4096;
const PG_SYS: u32 = 1;
const PAGE_FLAGS_CODE: u32 = PAGE_FIXED | PAGE_LOCKED | PAGE_ZERO_INIT;
const PAGE_FIXED: u32 = 0x8;
const PAGE_LOCKED: u32 = 0x80;
const PAGE_ZERO_INIT: u32 = 0x1;
const PAGE_PRESENT: u32 = 0x1;
const PAGE_WRITEABLE: u32 = 0x2;
const GUM_PAGE_WRITE: u32 = 0x2;
const BLOCK_SVC_INTS: u32 = 1 << 0;
const DEBUG_CONSOLE_PORT: u16 = 0xe9;
pub fn log(msg: &str) {
for byte in msg.bytes() {
if byte == 0 {
break;
}
write_debug_byte(byte);
}
}
pub fn log_hex(value: u32) {
let mut shift = 32;
while shift != 0 {
shift -= 4;
let digit = ((value >> shift) & 0xf) as u8;
write_debug_byte(if digit < 10 { b'0' + digit } else { b'a' + digit - 10 });
}
write_debug_byte(b'\n');
}
fn write_debug_byte(byte: u8) {
unsafe {
core::arch::asm!("out dx, al", in("dx") DEBUG_CONSOLE_PORT, in("al") byte,
options(nomem, nostack, preserves_flags));
}
}
pub fn panic(msg: &str) {
log(msg);
unsafe {
fatal_error_handler(msg.as_ptr(), 0);
}
}
pub fn alloc(size: usize) -> *mut u8 {
unsafe { __HeapAllocate(size as u32, 0) }
}
pub fn free(ptr: *mut u8, _size: usize) {
unsafe {
__HeapFree(ptr, 0);
}
}
pub fn alloc_code(size: usize) -> *mut u8 {
let pages = size.div_ceil(PAGE_SIZE as usize) as u32;
unsafe { __PageAllocate(pages, PG_SYS, 0, 0, 0, 0, core::ptr::null_mut(), PAGE_FLAGS_CODE) }
}
pub fn free_code(ptr: *mut u8, _size: usize) {
unsafe {
__PageFree(ptr, 0);
}
}
pub fn spawn_thread(entry: ThreadEntry, parameter: *mut c_void) -> isize {
let stack = alloc_code(THREAD_STACK_SIZE);
if stack.is_null() {
return 0;
}
unsafe {
THREAD_ENTRY = Some(entry);
THREAD_PARAMETER = parameter;
vmm_create_thread(
segment_ss(),
stack as u32 + THREAD_STACK_SIZE as u32,
segment_cs(),
frida_win9x_thread_thunk as u32,
segment_ds(),
segment_es(),
0,
)
}
}
// VMM refuses to build a thread from the borrowed context the host enters us on, and only
// says so by never returning, so hand the work to a point where VMM is between jobs.
pub fn run_when_ready(action: fn()) {
unsafe {
READY_ACTION = Some(action);
schedule_global_event(frida_win9x_event_thunk);
}
}
#[unsafe(no_mangle)]
extern "C" fn frida_win9x_on_event() {
if let Some(action) = unsafe { READY_ACTION } {
action();
}
}
static mut READY_ACTION: Option<fn()> = None;
#[unsafe(no_mangle)]
extern "C" fn frida_win9x_thread_start() {
unsafe {
if let Some(entry) = THREAD_ENTRY {
entry(THREAD_PARAMETER, 0);
}
}
}
static mut THREAD_ENTRY: Option<ThreadEntry> = None;
static mut THREAD_PARAMETER: *mut c_void = core::ptr::null_mut();
const THREAD_STACK_SIZE: usize = 64 * 1024;
fn segment_cs() -> u32 {
let selector: u32;
unsafe { core::arch::asm!("mov {0:e}, cs", out(reg) selector, options(nomem, nostack, preserves_flags)) };
selector
}
fn segment_ss() -> u32 {
let selector: u32;
unsafe { core::arch::asm!("mov {0:e}, ss", out(reg) selector, options(nomem, nostack, preserves_flags)) };
selector
}
fn segment_ds() -> u32 {
let selector: u32;
unsafe { core::arch::asm!("mov {0:e}, ds", out(reg) selector, options(nomem, nostack, preserves_flags)) };
selector
}
fn segment_es() -> u32 {
let selector: u32;
unsafe { core::arch::asm!("mov {0:e}, es", out(reg) selector, options(nomem, nostack, preserves_flags)) };
selector
}
pub fn wait(token: *const u8, timeout_us: Option<u64>, check: &mut dyn FnMut() -> bool) {
let semaphore = semaphore_for(token);
if check() {
return;
}
match timeout_us {
None => unsafe { wait_semaphore(semaphore, BLOCK_SVC_INTS) },
Some(us) => block_until_signalled_or_timed_out(semaphore, us),
}
}
fn block_until_signalled_or_timed_out(semaphore: u32, timeout_us: u64) {
let timeout_ms = (timeout_us / 1000).max(1) as u32;
let timeout = unsafe { set_global_time_out(timeout_ms, semaphore) };
unsafe {
wait_semaphore(semaphore, BLOCK_SVC_INTS);
}
if timeout != 0 {
unsafe {
cancel_time_out(timeout);
}
}
}
pub fn wake(token: *const u8) {
unsafe {
signal_semaphore(semaphore_for(token));
}
}
pub fn yield_now() {
unsafe {
_Release_Time_Slice();
}
}
pub fn monotonic_micros() -> i64 {
unsafe { _Get_System_Time() as i64 * 1000 }
}
pub fn wall_clock_micros() -> (u32, u32) {
let micros = monotonic_micros() as u64;
((micros / 1_000_000) as u32, (micros % 1_000_000) as u32)
}
pub fn current_thread_id() -> u64 {
unsafe { get_cur_thread_handle() as u64 }
}
pub fn protect(address: u64, size: usize, gum_prot: u32) -> bool {
let first_page = address / PAGE_SIZE as u64;
let pages = size.div_ceil(PAGE_SIZE as usize) as u32;
let mut set = PAGE_PRESENT;
if (gum_prot & GUM_PAGE_WRITE) != 0 {
set |= PAGE_WRITEABLE;
}
let clear = PAGE_WRITEABLE & !set;
unsafe { __PageModifyPermissions(first_page as u32, pages, !clear, set) != 0xffff_ffff }
}
pub fn map_io(phys_addr: u64, size: u64) -> *mut c_void {
let pages = (size as usize).div_ceil(PAGE_SIZE as usize) as u32;
unsafe { __MapPhysToLinear(phys_addr as u32, pages * PAGE_SIZE, 0) as *mut c_void }
}
pub fn virt_to_phys(vaddr: u64) -> u64 {
let mut entry: u32 = 0;
let page = (vaddr / PAGE_SIZE as u64) as u32;
unsafe {
__CopyPageTable(page, 1, &mut entry, 0);
}
((entry & !(PAGE_SIZE - 1)) as u64) | (vaddr & (PAGE_SIZE - 1) as u64)
}
pub fn install_interrupt_handler(
irq: u32,
target: *mut c_void,
handler: InterruptHandler,
refcon: *mut c_void,
) -> i32 {
unsafe {
HW_INT_HANDLER = Some(handler);
HW_INT_TARGET = target;
HW_INT_REFCON = refcon;
}
unsafe {
IRQ_DESCRIPTOR = VpicdIrqDescriptor {
irq_number: irq as u16,
options: VPICD_OPT_REF_DATA | VPICD_OPT_CAN_SHARE,
hw_int_proc: frida_win9x_hw_int_thunk,
virt_int_proc: 0,
eoi_proc: 0,
mask_change_proc: 0,
iret_proc: 0,
iret_time_out: VPICD_DEFAULT_IRET_TIME_OUT,
hw_int_ref: 0,
};
}
let handle = unsafe { vpicd_virtualize_irq(core::ptr::addr_of_mut!(IRQ_DESCRIPTOR)) };
if handle != 0 {
unsafe {
IRQ_HANDLE = handle;
vpicd_physically_unmask(handle);
}
}
if handle == 0 { -1 } else { 0 }
}
pub struct ProcessInfo {
pub id: u32,
pub path: *const u8,
}
// Win32 threads carry the process they belong to in VWIN32's per-thread block, so the
// process list is the deduplicated set of those, and the image path is where the command
// line starts.
pub fn enumerate_processes(found: &mut dyn FnMut(ProcessInfo)) {
let slot = unsafe { (0xc00211ccu32 as *const u32).read() };
let vm = unsafe { get_sys_vm_handle() };
let first = unsafe { get_initial_thread_handle(vm) };
let mut seen: [u32; 64] = [0; 64];
let mut count = 0usize;
let mut thread = first;
while thread != 0 && count < seen.len() {
if unsafe { (thread as *const u32).add(0x2c / 4).read() } == WIN32_THREAD {
let block = unsafe { (thread as *const u32).byte_add(slot as usize).read() };
let pdb = unsafe { (block as *const u32).add(1).read() };
if !seen[..count].contains(&pdb) {
seen[count] = pdb;
count += 1;
found(ProcessInfo {
id: pdb,
path: image_path(pdb),
});
}
}
let next = unsafe { get_next_thread_handle(thread) };
thread = if next == first { 0 } else { next };
}
}
fn image_path(pdb: u32) -> *const u8 {
let env_db = unsafe { (pdb as *const u32).byte_add(0x40).read() };
if env_db < ARENA_FLOOR {
return core::ptr::null();
}
let command_line = unsafe { (env_db as *const u32).byte_add(0x08).read() };
if command_line < ARENA_FLOOR || unsafe { (command_line as *const u8).add(1).read() } != b':' {
return core::ptr::null();
}
command_line as *const u8
}
const WIN32_THREAD: u32 = 0x2a;
const ARENA_FLOOR: u32 = 0x10000;
pub fn install_fault_reporter() {
unsafe {
FAULT_CHAIN[INVALID_OPCODE as usize] = hook_vmm_fault(INVALID_OPCODE, frida_win9x_fault_thunk_ud);
FAULT_CHAIN[GENERAL_PROTECTION as usize] =
hook_vmm_fault(GENERAL_PROTECTION, frida_win9x_fault_thunk_gp);
FAULT_CHAIN[PAGE_FAULT as usize] = hook_vmm_fault(PAGE_FAULT, frida_win9x_fault_thunk_pf);
}
}
#[unsafe(no_mangle)]
extern "C" fn frida_win9x_on_fault(fault: u32, frame: *mut u32) -> u32 {
let mut cpu_context = unsafe {
crate::bindings::_GumIA32CpuContext {
eip: frame.add(fault_frame_eip_slot(fault)).read(),
edi: frame.read(),
esi: frame.add(1).read(),
ebp: frame.add(2).read(),
esp: frame.add(3).read(),
ebx: frame.add(4).read(),
edx: frame.add(5).read(),
ecx: frame.add(6).read(),
eax: frame.add(7).read(),
xmm: core::ptr::null_mut(),
}
};
let handled = unsafe {
crate::bindings::gum_barebone_handle_exception(
exception_type_for(fault),
cpu_context.eip as *mut c_void,
faulting_address() as *mut c_void,
&mut cpu_context,
)
};
if handled == 0 {
return unsafe { FAULT_CHAIN[fault as usize] };
}
unsafe {
frida_win9x_resume = [
cpu_context.eip,
cpu_context.edi,
cpu_context.esi,
cpu_context.ebp,
cpu_context.esp,
cpu_context.ebx,
cpu_context.edx,
cpu_context.ecx,
cpu_context.eax,
];
}
0
}
#[unsafe(no_mangle)]
static mut frida_win9x_resume: [u32; 9] = [0; 9];
fn exception_type_for(fault: u32) -> crate::bindings::GumExceptionType {
use crate::bindings::*;
match fault {
DIVIDE_ERROR | OVERFLOW | BOUND_RANGE_EXCEEDED | X87_FLOATING_POINT
| SIMD_FLOATING_POINT => _GumExceptionType_GUM_EXCEPTION_ARITHMETIC,
DEBUG => _GumExceptionType_GUM_EXCEPTION_SINGLE_STEP,
BREAKPOINT => _GumExceptionType_GUM_EXCEPTION_BREAKPOINT,
INVALID_OPCODE => _GumExceptionType_GUM_EXCEPTION_ILLEGAL_INSTRUCTION,
STACK_SEGMENT_FAULT => _GumExceptionType_GUM_EXCEPTION_STACK_OVERFLOW,
GENERAL_PROTECTION | PAGE_FAULT => _GumExceptionType_GUM_EXCEPTION_ACCESS_VIOLATION,
_ => _GumExceptionType_GUM_EXCEPTION_SYSTEM,
}
}
const DIVIDE_ERROR: u32 = 0;
const DEBUG: u32 = 1;
const BREAKPOINT: u32 = 3;
const OVERFLOW: u32 = 4;
const BOUND_RANGE_EXCEEDED: u32 = 5;
const STACK_SEGMENT_FAULT: u32 = 12;
const X87_FLOATING_POINT: u32 = 16;
const SIMD_FLOATING_POINT: u32 = 19;
fn fault_frame_eip_slot(fault: u32) -> usize {
const PUSHED_REGISTERS: usize = 8;
const VECTOR: usize = 1;
let error_code = match fault {
8 | 10 | 11 | 12 | 13 | 14 | 17 | 21 => 1,
_ => 0,
};
PUSHED_REGISTERS + VECTOR + error_code
}
fn faulting_address() -> u32 {
let address: u32;
unsafe { core::arch::asm!("mov {0:e}, cr2", out(reg) address, options(nomem, nostack, preserves_flags)) };
address
}
static mut FAULT_CHAIN: [u32; 32] = [0; 32];
#[unsafe(no_mangle)]
static mut frida_win9x_fault_chain: u32 = 0;
const INVALID_OPCODE: u32 = 6;
const GENERAL_PROTECTION: u32 = 13;
const PAGE_FAULT: u32 = 14;
#[unsafe(no_mangle)]
extern "C" fn frida_win9x_on_hw_int(_ref_data: *mut c_void) {
unsafe {
if let Some(handler) = HW_INT_HANDLER {
handler(HW_INT_TARGET, HW_INT_REFCON, core::ptr::null_mut(), 0);
}
vpicd_phys_eoi(IRQ_HANDLE);
}
}
static mut IRQ_HANDLE: u32 = 0;
static mut IRQ_DESCRIPTOR: VpicdIrqDescriptor = VpicdIrqDescriptor {
irq_number: 0,
options: 0,
hw_int_proc: frida_win9x_hw_int_thunk,
virt_int_proc: 0,
eoi_proc: 0,
mask_change_proc: 0,
iret_proc: 0,
iret_time_out: 0,
hw_int_ref: 0,
};
static mut HW_INT_HANDLER: Option<InterruptHandler> = None;
static mut HW_INT_TARGET: *mut c_void = core::ptr::null_mut();
static mut HW_INT_REFCON: *mut c_void = core::ptr::null_mut();
#[repr(C)]
struct VpicdIrqDescriptor {
irq_number: u16,
options: u16,
hw_int_proc: unsafe extern "C" fn(),
virt_int_proc: u32,
eoi_proc: u32,
mask_change_proc: u32,
iret_proc: u32,
iret_time_out: u32,
hw_int_ref: u32,
}
const VPICD_OPT_CAN_SHARE: u16 = 0x02;
const VPICD_OPT_REF_DATA: u16 = 0x04;
const VPICD_DEFAULT_IRET_TIME_OUT: u32 = 500;
pub type InterruptHandler =
unsafe extern "C" fn(target: *mut c_void, refcon: *mut c_void, nub: *mut c_void, source: i32);
pub fn get_kernel_base() -> u64 {
KERNEL_BASE.load(Ordering::Relaxed) as u64
}
pub fn set_kernel_base(base: u64) {
KERNEL_BASE.store(base as u32, Ordering::Relaxed);
}
static KERNEL_BASE: AtomicU32 = AtomicU32::new(0);
// One semaphore per waited-on address, created on first use. VMM has no
// futex-alike, so the token has to be mapped onto something it does have.
fn semaphore_for(token: *const u8) -> u32 {
let slot = (token as usize / core::mem::align_of::<usize>()) % SEMAPHORES.len();
let existing = SEMAPHORES[slot].load(Ordering::Acquire);
if existing != 0 {
return existing;
}
let created = unsafe { create_semaphore(0) };
match SEMAPHORES[slot].compare_exchange(0, created, Ordering::AcqRel, Ordering::Acquire) {
Ok(_) => created,
Err(raced) => raced,
}
}
const NUM_SEMAPHORES: usize = 64;
static SEMAPHORES: [AtomicU32; NUM_SEMAPHORES] = [const { AtomicU32::new(0) }; NUM_SEMAPHORES];
unsafe extern "C" {
fn wait_semaphore(semaphore: u32, flags: u32);
fn signal_semaphore(semaphore: u32);
fn create_semaphore(token_count: u32) -> u32;
fn get_cur_thread_handle() -> u32;
fn fatal_error_handler(message: *const u8, flags: u32);
fn vpicd_virtualize_irq(descriptor: *mut VpicdIrqDescriptor) -> u32;
fn vpicd_physically_unmask(handle: u32);
fn vpicd_phys_eoi(handle: u32);
fn set_global_time_out(milliseconds: u32, semaphore: u32) -> u32;
fn cancel_time_out(timeout: u32);
fn hook_vmm_fault(fault: u32, handler: unsafe extern "C" fn()) -> u32;
fn frida_win9x_fault_thunk_ud();
fn frida_win9x_fault_thunk_gp();
fn frida_win9x_fault_thunk_pf();
fn frida_win9x_time_out_thunk();
fn get_cur_vm_handle() -> u32;
fn get_sys_vm_handle() -> u32;
fn schedule_global_event(callback: unsafe extern "C" fn()) -> u32;
fn frida_win9x_event_thunk();
fn get_next_vm_handle(vm: u32) -> u32;
fn get_initial_thread_handle(vm: u32) -> u32;
fn get_next_thread_handle(thread: u32) -> u32;
fn frida_win9x_hw_int_thunk();
fn frida_win9x_thread_thunk();
fn vmm_create_thread(ss: u32, esp: u32, cs: u32, eip: u32, ds: u32, es: u32,
ref_data: u32) -> isize;
}
core::arch::global_asm!(
r#"
.intel_syntax noprefix
.global wait_semaphore
wait_semaphore:
push ebp
mov ebp, esp
push ebx
push esi
push edi
mov eax, [ebp + 8]
mov ecx, [ebp + 12]
call dword ptr [_Wait_Semaphore]
pop edi
pop esi
pop ebx
pop ebp
ret
.global signal_semaphore
signal_semaphore:
push ebp
mov ebp, esp
push ebx
push esi
push edi
mov eax, [ebp + 8]
call dword ptr [_Signal_Semaphore]
pop edi
pop esi
pop ebx
pop ebp
ret
.global create_semaphore
create_semaphore:
push ebp
mov ebp, esp
push ebx
push esi
push edi
mov ecx, [ebp + 8]
call dword ptr [_Create_Semaphore]
cmc
sbb ecx, ecx
and eax, ecx
pop edi
pop esi
pop ebx
pop ebp
ret
.global get_cur_thread_handle
get_cur_thread_handle:
push ebp
mov ebp, esp
push ebx
push esi
push edi
call dword ptr [_Get_Cur_Thread_Handle]
mov eax, edi
pop edi
pop esi
pop ebx
pop ebp
ret
.global fatal_error_handler
fatal_error_handler:
push ebp
mov ebp, esp
push ebx
push esi
push edi
mov esi, [ebp + 8]
mov eax, [ebp + 12]
call dword ptr [_Fatal_Error_Handler]
pop edi
pop esi
pop ebx
pop ebp
ret
.global schedule_global_event
schedule_global_event:
push ebp
mov ebp, esp
push ebx
push esi
push edi
mov esi, [ebp + 8]
call dword ptr [_Schedule_Global_Event]
mov eax, ebx
pop edi
pop esi
pop ebx
pop ebp
ret
.global frida_win9x_event_thunk
frida_win9x_event_thunk:
pushad
call frida_win9x_on_event
popad
ret
.global get_sys_vm_handle
get_sys_vm_handle:
push ebx
call dword ptr [_Get_Sys_VM_Handle]
mov eax, ebx
pop ebx
ret
.global get_next_vm_handle
get_next_vm_handle:
push ebp
mov ebp, esp
push ebx
mov ebx, [ebp + 8]
call dword ptr [_Get_Next_VM_Handle]
mov eax, ebx
pop ebx
pop ebp
ret
.global get_initial_thread_handle
get_initial_thread_handle:
push ebp
mov ebp, esp
push ebx
push edi
mov ebx, [ebp + 8]
call dword ptr [_Get_Initial_Thread_Handle]
mov eax, edi
pop edi
pop ebx
pop ebp
ret
.global get_next_thread_handle
get_next_thread_handle:
push ebp
mov ebp, esp
push ebx
push edi
mov edi, [ebp + 8]
call dword ptr [_Get_Next_Thread_Handle]
mov eax, edi
pop edi
pop ebx
pop ebp
ret
.global get_cur_vm_handle
get_cur_vm_handle:
push ebx
call dword ptr [_Get_Cur_VM_Handle]
mov eax, ebx
pop ebx
ret
.global hook_vmm_fault
hook_vmm_fault:
push ebp
mov ebp, esp
push ebx
push esi
push edi
mov eax, [ebp + 8]
mov esi, [ebp + 12]
call dword ptr [_Hook_VMM_Fault]
mov eax, esi
pop edi
pop esi
pop ebx
pop ebp
ret
.global frida_win9x_fault_thunk_ud
frida_win9x_fault_thunk_ud:
push 6
jmp frida_win9x_fault_common
.global frida_win9x_fault_thunk_gp
frida_win9x_fault_thunk_gp:
push 13
jmp frida_win9x_fault_common
.global frida_win9x_fault_thunk_pf
frida_win9x_fault_thunk_pf:
push 14
jmp frida_win9x_fault_common
frida_win9x_fault_common:
pushad
mov eax, [esp + 32]
push esp
push eax
call frida_win9x_on_fault
add esp, 8
mov [frida_win9x_fault_chain], eax
popad
add esp, 4
cmp dword ptr [frida_win9x_fault_chain], 0
je frida_win9x_fault_resume
jmp dword ptr [frida_win9x_fault_chain]
frida_win9x_fault_resume:
mov esp, [frida_win9x_resume + 16]
mov edi, [frida_win9x_resume + 4]
mov esi, [frida_win9x_resume + 8]
mov ebp, [frida_win9x_resume + 12]
mov ebx, [frida_win9x_resume + 20]
mov edx, [frida_win9x_resume + 24]
mov ecx, [frida_win9x_resume + 28]
mov eax, [frida_win9x_resume + 32]
push dword ptr [frida_win9x_resume]
ret
.global set_global_time_out
set_global_time_out:
push ebp
mov ebp, esp
push ebx
push esi
push edi
mov eax, [ebp + 8]
mov edx, [ebp + 12]
lea esi, [frida_win9x_time_out_thunk]
call dword ptr [_Set_Global_Time_Out]
mov eax, esi
pop edi
pop esi
pop ebx
pop ebp
ret
.global cancel_time_out
cancel_time_out:
push ebp
mov ebp, esp
push ebx
push esi
push edi
mov esi, [ebp + 8]
call dword ptr [_Cancel_Time_Out]
pop edi
pop esi
pop ebx
pop ebp
ret
.global frida_win9x_time_out_thunk
frida_win9x_time_out_thunk:
pushad
mov eax, edx
call dword ptr [_Signal_Semaphore]
popad
ret
.global vpicd_phys_eoi
vpicd_phys_eoi:
push ebp
mov ebp, esp
push ebx
push esi
push edi
mov eax, [ebp + 8]
call dword ptr [_VPICD_Phys_EOI]
pop edi
pop esi
pop ebx
pop ebp
ret
.global vpicd_physically_unmask
vpicd_physically_unmask:
push ebp
mov ebp, esp
push ebx
push esi
push edi
mov eax, [ebp + 8]
call dword ptr [_VPICD_Physically_Unmask]
pop edi
pop esi
pop ebx
pop ebp
ret
.global vpicd_virtualize_irq
vpicd_virtualize_irq:
push ebp
mov ebp, esp
push ebx
push esi
push edi
call dword ptr [_Get_Cur_VM_Handle]
mov edi, [ebp + 8]
call dword ptr [_VPICD_Virtualize_IRQ]
cmc
sbb ecx, ecx
and eax, ecx
pop edi
pop esi
pop ebx
pop ebp
ret
.global vmm_create_thread
vmm_create_thread:
push ebp
mov ebp, esp
push ebx
push esi
push edi
push dword ptr [ebp + 32]
push dword ptr [ebp + 28]
push dword ptr [ebp + 24]
push dword ptr [ebp + 20]
push dword ptr [ebp + 16]
push dword ptr [ebp + 12]
push dword ptr [ebp + 8]
call dword ptr [_VMMCreateThread]
add esp, 28
pop edi
pop esi
pop ebx
pop ebp
ret
.global frida_win9x_thread_thunk
frida_win9x_thread_thunk:
call frida_win9x_thread_start
1:
jmp 1b
.global frida_win9x_hw_int_thunk
frida_win9x_hw_int_thunk:
pushad
push edx
call frida_win9x_on_hw_int
add esp, 4
popad
clc
ret
"#
);
unsafe extern "C" {
static _Get_Cur_VM_Handle: unsafe extern "C" fn();
static _Get_Sys_VM_Handle: unsafe extern "C" fn();
static _Schedule_Global_Event: unsafe extern "C" fn();
static _Create_Semaphore: unsafe extern "C" fn();
static _Wait_Semaphore: unsafe extern "C" fn();
static _Signal_Semaphore: unsafe extern "C" fn();
static _Get_Next_VM_Handle: unsafe extern "C" fn();
static _Release_Time_Slice: unsafe extern "C" fn();
static _Set_Global_Time_Out: unsafe extern "C" fn();
static _Cancel_Time_Out: unsafe extern "C" fn();
static _Get_System_Time: unsafe extern "C" fn() -> u32;
static __HeapAllocate: unsafe extern "C" fn(u32, u32) -> *mut u8;
static __HeapFree: unsafe extern "C" fn(*mut u8, u32);
static __PageAllocate:
unsafe extern "C" fn(u32, u32, u32, u32, u32, u32, *mut c_void, u32) -> *mut u8;
static __PageFree: unsafe extern "C" fn(*mut u8, u32);
static __CopyPageTable: unsafe extern "C" fn(u32, u32, *mut u32, u32) -> u32;
static __MapPhysToLinear: unsafe extern "C" fn(u32, u32, u32) -> u32;
static _Hook_VMM_Fault: unsafe extern "C" fn();
static _Fatal_Error_Handler: unsafe extern "C" fn();
static _VMMCreateThread: unsafe extern "C" fn();
static _Get_Cur_Thread_Handle: unsafe extern "C" fn();
static _Get_Initial_Thread_Handle: unsafe extern "C" fn();
static _Get_Next_Thread_Handle: unsafe extern "C" fn();
static __Debug_Printf_Service: unsafe extern "C" fn(*const u8, ...);
static __ContextSwitch: unsafe extern "C" fn(u32);
static __PageModifyPermissions: unsafe extern "C" fn(u32, u32, u32, u32) -> u32;
static __GetCurrentContext: unsafe extern "C" fn() -> u32;
static _VPICD_Virtualize_IRQ: unsafe extern "C" fn();
static _VPICD_Phys_EOI: unsafe extern "C" fn();
static _VPICD_Physically_Unmask: unsafe extern "C" fn();
}
+125 -53
View File
@@ -1,7 +1,87 @@
use core::ffi::c_void;
use core::sync::atomic::{AtomicU64, Ordering};
static KERNEL_BASE: AtomicU64 = AtomicU64::new(0);
use crate::kernel::ThreadEntry;
pub fn log(msg: &str) {
unsafe {
_IOLog(msg.as_ptr());
}
}
pub fn panic(msg: &str) {
unsafe { _panic(msg.as_ptr()) };
}
pub fn run_when_ready(action: fn()) {
action();
}
pub fn spawn_thread(entry: ThreadEntry, parameter: *mut c_void) -> isize {
kernel_thread_start(entry, parameter)
}
pub fn alloc(size: usize) -> *mut u8 {
kalloc(size)
}
// Executable slabs come out of the same allocator; the host flips their page
// permissions for us through its physical-memory bridge.
pub fn alloc_code(size: usize) -> *mut u8 {
kalloc(size)
}
pub fn free_code(ptr: *mut u8, size: usize) {
free(ptr, size);
}
// Arms the wait, gives `check` a chance to observe the condition, and only then
// commits to sleeping — the three-phase Mach protocol, which is race-free
// because a wakeup landing after assert_wait() cancels the pending block.
pub fn wait(token: *const u8, timeout_us: Option<u64>, check: &mut dyn FnMut() -> bool) {
let wait_result = match timeout_us {
None => assert_wait(token, THREAD_INTERRUPTIBLE),
Some(us) => assert_wait_timeout(token, THREAD_INTERRUPTIBLE, (us * 1000) as u32, 1),
};
if wait_result != THREAD_WAITING {
panic!("assert_wait failed: {}", wait_result);
}
if check() {
thread_wakeup(token);
return;
}
thread_block(None);
}
pub fn wake(token: *const u8) {
thread_wakeup(token);
}
// XNU reschedules a kernel thread that stays runnable, so there is nothing this has
// to do here. It exists because Linux does not forgive a loop that never yields.
pub fn yield_now() {}
pub fn monotonic_micros() -> i64 {
(absolutetime_to_nanoseconds(mach_absolute_time()) / 1000) as i64
}
pub fn wall_clock_micros() -> (u32, u32) {
clock_get_calendar_microtime()
}
// The thread pointer is exposed to JavaScript as a GumThreadId, so it must fit
// in a double without losing precision; the low bits keep it unique per thread.
const JS_SAFE_THREAD_ID_MASK: u64 = (1 << 48) - 1;
pub fn current_thread_id() -> u64 {
let thread_ptr: u64;
unsafe {
core::arch::asm!("mrs {}, tpidr_el1", out(reg) thread_ptr, options(nomem, nostack));
}
thread_ptr & JS_SAFE_THREAD_ID_MASK
}
pub fn get_kernel_base() -> u64 {
KERNEL_BASE.load(Ordering::Relaxed)
@@ -11,54 +91,7 @@ pub fn set_kernel_base(base: u64) {
KERNEL_BASE.store(base, Ordering::Relaxed);
}
type ContinuationFn = unsafe extern "C" fn(_parameter: *mut c_void, _wait_result: i32);
unsafe extern "C" {
static _panic: unsafe extern "C" fn(*const u8);
static _IOLog: unsafe extern "C" fn(*const u8, ...);
// QEMU XNU exports plain kalloc/kfree; iOS XNU exports kalloc_data/kfree_data
// (modern data-typed allocator KPIs). The host fills in whichever pair the
// running kernel provides; the other slot stays null.
static _kalloc: Option<unsafe extern "C" fn(usize) -> *mut u8>;
static _kfree: Option<unsafe extern "C" fn(*mut u8, usize) -> *mut u8>;
static _kalloc_data: Option<unsafe extern "C" fn(usize, u32) -> *mut u8>;
static _kfree_data: Option<unsafe extern "C" fn(*mut u8, usize)>;
static _kernel_thread_start:
unsafe extern "C" fn(*const (), *mut c_void, *mut *mut c_void) -> isize;
static _assert_wait: unsafe extern "C" fn(*const u8, u32) -> i32;
static _assert_wait_timeout: unsafe extern "C" fn(*const u8, u32, u32, u32) -> i32;
static _thread_block: unsafe extern "C" fn(Option<ContinuationFn>) -> i32;
// QEMU XNU exports thread_wakeup; iOS XNU exports the BSD-style wakeup wrapper.
static _thread_wakeup: Option<unsafe extern "C" fn(*const u8) -> i32>;
static _wakeup: Option<unsafe extern "C" fn(*const u8)>;
static _mach_absolute_time: unsafe extern "C" fn() -> u64;
static _absolutetime_to_nanoseconds: unsafe extern "C" fn(u64, *mut u64);
static _clock_get_calendar_microtime: unsafe extern "C" fn(*mut u32, *mut u32);
static _ml_io_map: unsafe extern "C" fn(u64, u64) -> *mut c_void;
static _ml_vtophys: unsafe extern "C" fn(u64) -> u64;
static __ZN9IOService11getPlatformEv: unsafe extern "C" fn() -> *mut c_void;
static __ZN9IOServiceC2Ev: unsafe extern "C" fn(*mut core::ffi::c_void);
static __ZN8OSSymbol17withCStringNoCopyEPKc:
unsafe extern "C" fn(*const core::ffi::c_char) -> *const OSSymbol;
static __ZN6OSData9withBytesEPKvj:
unsafe extern "C" fn(*const core::ffi::c_void, u32) -> *mut OSData;
}
const IO_SERVICE_VTABLE_LENGTH: isize = 168;
const VT_LOOKUP_IC: isize = IO_SERVICE_VTABLE_LENGTH + 25; // IOPlatformExpert
const VT_REGISTER_INT: isize = IO_SERVICE_VTABLE_LENGTH + 0; // IOInterruptController
const VT_ENABLE_INT: isize = IO_SERVICE_VTABLE_LENGTH + 3; // IOInterruptController
pub fn panic(msg: &str) {
unsafe { _panic(msg.as_ptr()) };
}
pub fn io_log(msg: &str) {
unsafe {
_IOLog(msg.as_ptr());
}
}
static KERNEL_BASE: AtomicU64 = AtomicU64::new(0);
// Z_WAITOK (modern XNU): block until allocation succeeds.
const Z_WAITOK: u32 = 0x0200;
@@ -98,8 +131,8 @@ pub fn kernel_thread_start(continuation: ContinuationFn, thread_parameter: *mut
};
}
pub const THREAD_INTERRUPTIBLE: u32 = 1;
pub const THREAD_WAITING: i32 = -1;
const THREAD_INTERRUPTIBLE: u32 = 1;
const THREAD_WAITING: i32 = -1;
pub fn assert_wait(event: *const u8, interruptible: u32) -> i32 {
unsafe { _assert_wait(event, interruptible) }
@@ -152,11 +185,11 @@ pub fn clock_get_calendar_microtime() -> (u32, u32) {
(secs, microsecs)
}
pub fn ml_io_map(phys_addr: u64, size: u64) -> *mut c_void {
pub fn map_io(phys_addr: u64, size: u64) -> *mut c_void {
unsafe { _ml_io_map(phys_addr, size) }
}
pub fn ml_vtophys(vaddr: u64) -> u64 {
pub fn virt_to_phys(vaddr: u64) -> u64 {
unsafe { _ml_vtophys(vaddr) }
}
@@ -268,3 +301,42 @@ where
let entry_ptr = unsafe { crate::pac::ptrauth_strip_data(entry as *const u8) };
unsafe { core::mem::transmute_copy::<*const u8, T>(&entry_ptr) }
}
const IO_SERVICE_VTABLE_LENGTH: isize = 168;
const VT_LOOKUP_IC: isize = IO_SERVICE_VTABLE_LENGTH + 25; // IOPlatformExpert
const VT_REGISTER_INT: isize = IO_SERVICE_VTABLE_LENGTH + 0; // IOInterruptController
const VT_ENABLE_INT: isize = IO_SERVICE_VTABLE_LENGTH + 3; // IOInterruptController
type ContinuationFn = ThreadEntry;
unsafe extern "C" {
static _panic: unsafe extern "C" fn(*const u8);
static _IOLog: unsafe extern "C" fn(*const u8, ...);
// QEMU XNU exports plain kalloc/kfree; iOS XNU exports kalloc_data/kfree_data
// (modern data-typed allocator KPIs). The host fills in whichever pair the
// running kernel provides; the other slot stays null.
static _kalloc: Option<unsafe extern "C" fn(usize) -> *mut u8>;
static _kfree: Option<unsafe extern "C" fn(*mut u8, usize) -> *mut u8>;
static _kalloc_data: Option<unsafe extern "C" fn(usize, u32) -> *mut u8>;
static _kfree_data: Option<unsafe extern "C" fn(*mut u8, usize)>;
static _kernel_thread_start:
unsafe extern "C" fn(*const (), *mut c_void, *mut *mut c_void) -> isize;
static _assert_wait: unsafe extern "C" fn(*const u8, u32) -> i32;
static _assert_wait_timeout: unsafe extern "C" fn(*const u8, u32, u32, u32) -> i32;
static _thread_block: unsafe extern "C" fn(Option<ContinuationFn>) -> i32;
// QEMU XNU exports thread_wakeup; iOS XNU exports the BSD-style wakeup wrapper.
static _thread_wakeup: Option<unsafe extern "C" fn(*const u8) -> i32>;
static _wakeup: Option<unsafe extern "C" fn(*const u8)>;
static _mach_absolute_time: unsafe extern "C" fn() -> u64;
static _absolutetime_to_nanoseconds: unsafe extern "C" fn(u64, *mut u64);
static _clock_get_calendar_microtime: unsafe extern "C" fn(*mut u32, *mut u32);
static _ml_io_map: unsafe extern "C" fn(u64, u64) -> *mut c_void;
static _ml_vtophys: unsafe extern "C" fn(u64) -> u64;
static __ZN9IOService11getPlatformEv: unsafe extern "C" fn() -> *mut c_void;
static __ZN9IOServiceC2Ev: unsafe extern "C" fn(*mut core::ffi::c_void);
static __ZN8OSSymbol17withCStringNoCopyEPKc:
unsafe extern "C" fn(*const core::ffi::c_char) -> *const OSSymbol;
static __ZN6OSData9withBytesEPKvj:
unsafe extern "C" fn(*const core::ffi::c_void, u32) -> *mut OSData;
}
+65 -79
View File
@@ -32,11 +32,6 @@ namespace Frida.Barebone {
private uint64 code_template_descriptor;
private bool code_template_known = false;
private enum AddressingMode {
VIRTUAL,
PHYSICAL
}
private const uint NUM_ARGS_IN_REGS = 8;
private const uint64 INT2_MASK = 0x3ULL;
@@ -90,11 +85,14 @@ namespace Frida.Barebone {
MMUParameters p = yield MMUParameters.load (gdb, cancellable);
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
GLib.Error? failure = null;
try {
yield collect_ranges_in_table (p.tt1, p.first_level, p.upper_bits, p, result, cancellable);
} finally {
set_addressing_mode.begin (gdb, VIRTUAL, null);
} catch (GLib.Error e) {
failure = e;
}
yield set_addressing_mode (gdb, VIRTUAL, cancellable);
throw_if_failed (failure);
return result;
}
@@ -154,53 +152,26 @@ namespace Frida.Barebone {
return result;
}
private Gee.List<RangeDetails> coalesce_ranges (Gee.List<RangeDetails> ranges) {
var result = new Gee.ArrayList<RangeDetails> ();
RangeDetails? pending = null;
foreach (RangeDetails r in ranges) {
if (pending == null) {
pending = r.clone ();
continue;
}
if (r.base_va == pending.base_va + pending.size &&
r.base_pa == pending.base_pa + pending.size &&
r.protection == pending.protection &&
r.type == pending.type) {
pending.size += r.size;
continue;
}
result.add (pending);
pending = r.clone ();
}
if (pending != null)
result.add (pending);
return result;
}
public async Allocation allocate_pages (Gee.List<uint64?> physical_addresses, Cancellable? cancellable)
throws Error, IOError {
MMUParameters p = yield load_mmu_parameters (cancellable);
// The bridge reads and writes guest physical memory directly, so the stub's
// physical-addressing mode (which requires a halted target) is unnecessary.
bool needs_stub_addressing = physical_memory == null;
if (needs_stub_addressing)
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
yield begin_physical_addressing (cancellable);
Allocation? allocation = null;
GLib.Error? failure = null;
try {
Allocation? allocation = yield maybe_insert_descriptor_in_table (physical_addresses, p.tt1, p.first_level,
allocation = yield maybe_insert_descriptor_in_table (physical_addresses, p.tt1, p.first_level,
p.upper_bits, p, cancellable);
if (allocation == null)
throw new Error.NOT_SUPPORTED ("Unable to insert page table mapping; please file a bug");
return allocation;
} finally {
if (needs_stub_addressing)
set_addressing_mode.begin (gdb, VIRTUAL, null);
} catch (GLib.Error e) {
failure = e;
}
yield end_physical_addressing (cancellable);
throw_if_failed (failure);
if (allocation == null)
throw new Error.NOT_SUPPORTED ("Unable to insert page table mapping; please file a bug");
return allocation;
}
private async Allocation? maybe_insert_descriptor_in_table (Gee.List<uint64?> physical_addresses,
@@ -339,27 +310,36 @@ namespace Frida.Barebone {
return;
}
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
GLib.Error? failure = null;
try {
yield gdb.write_byte_array (pa, new Bytes (data), cancellable);
} finally {
set_addressing_mode.begin (gdb, VIRTUAL, null);
} catch (GLib.Error e) {
failure = e;
}
yield set_addressing_mode (gdb, VIRTUAL, cancellable);
throw_if_failed (failure);
}
public async uint8[] read_physical_via_stub (uint64 pa, size_t size, Cancellable? cancellable) throws Error, IOError {
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
uint8[]? data = null;
GLib.Error? failure = null;
try {
return (yield gdb.read_byte_array (pa, size, cancellable)).get_data ();
} finally {
set_addressing_mode.begin (gdb, VIRTUAL, null);
data = (yield gdb.read_byte_array (pa, size, cancellable)).get_data ();
} catch (GLib.Error e) {
failure = e;
}
yield set_addressing_mode (gdb, VIRTUAL, cancellable);
throw_if_failed (failure);
return data;
}
public async uint64 read_level3_descriptor (uint64 va, Cancellable? cancellable) throws Error, IOError {
MMUParameters p = yield load_mmu_parameters (cancellable);
bool needs_stub_addressing = physical_memory == null;
if (needs_stub_addressing)
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
yield begin_physical_addressing (cancellable);
uint64 descriptor = 0;
GLib.Error? failure = null;
try {
var cache = new TableWalkCache ();
uint64 table_pa = yield find_level3_table (va, p, cache, cancellable);
@@ -369,20 +349,22 @@ namespace Frida.Barebone {
uint64 index = (va >> index_shift) & index_mask;
uint64 slot_pa = table_pa + (index * Descriptor.SIZE);
var buf = yield read_physical_buffer (slot_pa, Descriptor.SIZE, cancellable);
return buf.read_uint64 (0);
} finally {
if (needs_stub_addressing)
set_addressing_mode.begin (gdb, VIRTUAL, null);
descriptor = buf.read_uint64 (0);
} catch (GLib.Error e) {
failure = e;
}
yield end_physical_addressing (cancellable);
throw_if_failed (failure);
return descriptor;
}
public async void protect_pages (uint64 virtual_address, size_t size, Gum.PageProtection prot, Cancellable? cancellable)
throws Error, IOError {
MMUParameters p = yield load_mmu_parameters (cancellable);
bool needs_stub_addressing = physical_memory == null;
if (needs_stub_addressing)
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
yield begin_physical_addressing (cancellable);
GLib.Error? failure = null;
try {
uint64 page_mask = p.granule - 1;
uint64 aligned_va = virtual_address & ~page_mask;
@@ -390,10 +372,23 @@ namespace Frida.Barebone {
uint num_pages = (uint) ((aligned_end - aligned_va) / p.granule);
yield perform_protect_pages (aligned_va, num_pages, prot, p, cancellable);
} finally {
if (needs_stub_addressing)
set_addressing_mode.begin (gdb, VIRTUAL, null);
} catch (GLib.Error e) {
failure = e;
}
yield end_physical_addressing (cancellable);
throw_if_failed (failure);
}
// The bridge reads and writes guest physical memory directly, so the stub's
// physical-addressing mode, which requires a halted target, is unnecessary.
private async void begin_physical_addressing (Cancellable? cancellable) throws Error, IOError {
if (physical_memory == null)
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
}
private async void end_physical_addressing (Cancellable? cancellable) throws Error, IOError {
if (physical_memory == null)
yield set_addressing_mode (gdb, VIRTUAL, cancellable);
}
private async MMUParameters load_mmu_parameters (Cancellable? cancellable) throws Error, IOError {
@@ -1051,18 +1046,6 @@ namespace Frida.Barebone {
}
}
private static async void set_addressing_mode (GDB.Client gdb, AddressingMode mode, Cancellable? cancellable)
throws Error, IOError {
Gee.Set<string> features = gdb.features;
string enabled = (mode == PHYSICAL) ? "1" : "0";
if ("qemu-phy-mem-mode" in features)
yield gdb.execute_simple ("Qqemu.PhyMemMode:" + enabled, cancellable);
else if ("vf-phy-mem-mode" in features)
yield gdb.execute_simple ("Qvf.PhyMemMode:" + enabled, cancellable);
else
throw new Error.NOT_SUPPORTED ("Unsupported GDB remote stub; please file a bug");
}
private class MMUParameters {
public Granule granule;
public uint first_level;
@@ -1280,11 +1263,14 @@ namespace Frida.Barebone {
Bytes d = old_descriptors;
old_descriptors = null;
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
GLib.Error? failure = null;
try {
yield gdb.write_byte_array (first_slot, d, cancellable);
} finally {
set_addressing_mode.begin (gdb, VIRTUAL, null);
} catch (GLib.Error e) {
failure = e;
}
yield set_addressing_mode (gdb, VIRTUAL, cancellable);
throw_if_failed (failure);
}
}
+87 -6
View File
@@ -14,8 +14,21 @@ namespace Frida.Barebone {
get { return "small"; }
}
public PhysicalMemory? physical_memory {
get {
return page_tables.physical_memory;
}
set {
page_tables.physical_memory = value;
}
}
private X86PageTables page_tables;
public IA32Machine (GDB.Client gdb) {
Object (gdb: gdb);
page_tables = new X86PageTables (gdb);
}
public async size_t query_page_size (Cancellable? cancellable) throws Error, IOError {
@@ -28,17 +41,22 @@ namespace Frida.Barebone {
public async void enumerate_ranges (Gum.PageProtection prot, FoundRangeFunc func, Cancellable? cancellable)
throws Error, IOError {
throw_not_supported ();
foreach (RangeDetails r in coalesce_ranges (yield page_tables.collect_ranges (cancellable))) {
if ((r.protection & prot) != prot)
continue;
if (!func (r))
return;
}
}
public async Allocation allocate_pages (Gee.List<uint64?> physical_addresses, Cancellable? cancellable)
throws Error, IOError {
throw_not_supported ();
return yield page_tables.map (physical_addresses, cancellable);
}
public async void protect_pages (uint64 virtual_address, size_t size, Gum.PageProtection prot,
Cancellable? cancellable) throws Error, IOError {
throw_not_supported ();
yield page_tables.protect (virtual_address, size, prot, cancellable);
}
public async Gee.List<uint64?> scan_ranges (Gee.List<Gum.MemoryRange?> ranges, MatchPattern pattern, uint max_matches,
@@ -47,11 +65,70 @@ namespace Frida.Barebone {
}
public void apply_relocation (Gum.ElfRelocationDetails r, uint64 base_va, Buffer relocated) throws Error {
throw_not_supported ();
Gum.ElfIA32Relocation type = (Gum.ElfIA32Relocation) r.type;
switch (type) {
case NONE:
break;
case @32:
case RELATIVE:
// The in-place value is already the link-time target, the image being linked
// at zero and loaded as a unit.
relocated.write_uint32 ((size_t) r.address,
(uint32) (base_va + relocated.read_uint32 ((size_t) r.address)));
break;
case PC32:
case PLT32:
case GOTPC:
// Both ends of the displacement move by the same amount.
break;
default:
throw new Error.NOT_SUPPORTED ("Unsupported relocation type: %s",
Marshal.enum_to_nick<Gum.ElfIA32Relocation> (type));
}
}
public async uint64 invoke (uint64 impl, uint64[] args, Cancellable? cancellable) throws Error, IOError {
throw_not_supported ();
bool was_running = gdb.state != STOPPED;
if (was_running)
yield gdb.stop (cancellable);
GDB.Thread thread = gdb.exception.thread;
Gee.Map<string, Variant> saved_regs = yield thread.read_registers (cancellable);
var regs = new Gee.HashMap<string, Variant> ();
regs.set_all (saved_regs);
uint64 landing_zone = saved_regs["eip"].get_uint64 ();
uint64 sp = saved_regs["esp"].get_uint64 () - ((1 + args.length) * 4);
sp = (sp & ~15ULL) - 4;
var builder = gdb.make_buffer_builder ();
builder.append_uint32 ((uint32) landing_zone);
foreach (uint64 arg in args)
builder.append_uint32 ((uint32) arg);
yield gdb.write_byte_array (sp, builder.build (), cancellable);
regs["eip"] = impl;
regs["esp"] = sp;
yield thread.write_registers (regs, cancellable);
GDB.Breakpoint bp = yield gdb.add_breakpoint (SOFT, landing_zone, 1, cancellable);
GDB.Exception ex = null;
do {
ex = yield gdb.continue_until_exception (cancellable);
} while (ex.breakpoint != bp);
yield bp.remove (cancellable);
GDB.Thread landed = ex.thread;
uint64 retval = yield landed.read_register ("eax", cancellable);
yield landed.write_registers (saved_regs, cancellable);
if (was_running)
yield gdb.continue (cancellable);
return retval;
}
public async CallFrame load_call_frame (GDB.Thread thread, uint arity, Cancellable? cancellable) throws Error, IOError {
@@ -162,7 +239,11 @@ namespace Frida.Barebone {
public async InlineHook create_inline_hook (uint64 target, uint64 handler, Allocator allocator, Cancellable? cancellable)
throws Error, IOError {
throw_not_supported ();
return yield X86InlineHook.create (target, handler, IA32, allocator, gdb, cancellable);
}
public async uint64 translate_address (uint64 va, Cancellable? cancellable) throws Error, IOError {
return yield page_tables.translate (va, cancellable);
}
}
}
+96 -10
View File
@@ -14,10 +14,25 @@ namespace Frida.Barebone {
get { return "small"; }
}
public PhysicalMemory? physical_memory {
get {
return page_tables.physical_memory;
}
set {
page_tables.physical_memory = value;
}
}
private X86PageTables page_tables;
private const uint NUM_ARGS_IN_REGS = 6;
private const string[] ARG_REG_NAMES = { "rdi", "rsi", "rdx", "rcx", "r8", "r9" };
private const size_t RED_ZONE_SIZE = 128;
public X64Machine (GDB.Client gdb) {
Object (gdb: gdb);
page_tables = new X86PageTables (gdb);
}
public async size_t query_page_size (Cancellable? cancellable) throws Error, IOError {
@@ -30,17 +45,22 @@ namespace Frida.Barebone {
public async void enumerate_ranges (Gum.PageProtection prot, FoundRangeFunc func, Cancellable? cancellable)
throws Error, IOError {
throw_not_supported ();
foreach (RangeDetails r in coalesce_ranges (yield page_tables.collect_ranges (cancellable))) {
if ((r.protection & prot) != prot)
continue;
if (!func (r))
return;
}
}
public async Allocation allocate_pages (Gee.List<uint64?> physical_addresses, Cancellable? cancellable)
throws Error, IOError {
throw_not_supported ();
return yield page_tables.map (physical_addresses, cancellable);
}
public async void protect_pages (uint64 virtual_address, size_t size, Gum.PageProtection prot,
Cancellable? cancellable) throws Error, IOError {
throw_not_supported ();
yield page_tables.protect (virtual_address, size, prot, cancellable);
}
public async Gee.List<uint64?> scan_ranges (Gee.List<Gum.MemoryRange?> ranges, MatchPattern pattern, uint max_matches,
@@ -49,11 +69,74 @@ namespace Frida.Barebone {
}
public void apply_relocation (Gum.ElfRelocationDetails r, uint64 base_va, Buffer relocated) throws Error {
throw_not_supported ();
Gum.ElfX64Relocation type = (Gum.ElfX64Relocation) r.type;
switch (type) {
case NONE:
break;
case @64:
case RELATIVE:
// The in-place value is already the link-time target, the image being linked
// at zero and loaded as a unit.
relocated.write_uint64 ((size_t) r.address,
base_va + relocated.read_uint64 ((size_t) r.address));
break;
case PC32:
case PLT32:
case GOTPCREL:
// Both ends of the displacement move by the same amount.
break;
default:
throw new Error.NOT_SUPPORTED ("Unsupported relocation type: %s",
Marshal.enum_to_nick<Gum.ElfX64Relocation> (type));
}
}
public async uint64 invoke (uint64 impl, uint64[] args, Cancellable? cancellable) throws Error, IOError {
throw_not_supported ();
if (args.length > NUM_ARGS_IN_REGS)
throw new Error.NOT_SUPPORTED ("Unsupported number of arguments; please open a PR");
bool was_running = gdb.state != STOPPED;
if (was_running)
yield gdb.stop (cancellable);
GDB.Thread thread = gdb.exception.thread;
Gee.Map<string, Variant> saved_regs = yield thread.read_registers (cancellable);
var regs = new Gee.HashMap<string, Variant> ();
regs.set_all (saved_regs);
uint64 landing_zone = saved_regs["rip"].get_uint64 ();
uint64 sp = saved_regs["rsp"].get_uint64 () - RED_ZONE_SIZE - 8;
sp = (sp & ~15ULL) - 8;
var builder = gdb.make_buffer_builder ();
builder.append_uint64 (landing_zone);
yield gdb.write_byte_array (sp, builder.build (), cancellable);
for (uint i = 0; i != args.length; i++)
regs[ARG_REG_NAMES[i]] = args[i];
regs["rip"] = impl;
regs["rsp"] = sp;
yield thread.write_registers (regs, cancellable);
GDB.Breakpoint bp = yield gdb.add_breakpoint (SOFT, landing_zone, 1, cancellable);
GDB.Exception ex = null;
do {
ex = yield gdb.continue_until_exception (cancellable);
} while (ex.breakpoint != bp);
yield bp.remove (cancellable);
GDB.Thread landed = ex.thread;
uint64 retval = yield landed.read_register ("rax", cancellable);
yield landed.write_registers (saved_regs, cancellable);
if (was_running)
yield gdb.continue (cancellable);
return retval;
}
public async CallFrame load_call_frame (GDB.Thread thread, uint arity, Cancellable? cancellable) throws Error, IOError {
@@ -63,10 +146,10 @@ namespace Frida.Barebone {
var num_stack_args = int.max ((int) arity - (int) NUM_ARGS_IN_REGS, 0);
var stack = yield gdb.read_buffer (original_rsp, (1 + num_stack_args) * 8, cancellable);
return new Arm64CallFrame (thread, regs, stack, original_rsp);
return new X64CallFrame (thread, regs, stack, original_rsp);
}
private class Arm64CallFrame : Object, CallFrame {
private class X64CallFrame : Object, CallFrame {
public uint64 return_address {
get { return stack.read_uint64 (0); }
}
@@ -83,14 +166,13 @@ namespace Frida.Barebone {
private uint64 original_rsp;
private State stack_state = PRISTINE;
private const string[] ARG_REG_NAMES = { "rdi", "rsi", "rdx", "rcx", "r8", "r9" };
private enum State {
PRISTINE,
MODIFIED
}
public Arm64CallFrame (GDB.Thread thread, Gee.Map<string, Variant> regs, Buffer stack, uint64 original_rsp) {
public X64CallFrame (GDB.Thread thread, Gee.Map<string, Variant> regs, Buffer stack, uint64 original_rsp) {
this.thread = thread;
this.regs = regs;
@@ -191,7 +273,11 @@ namespace Frida.Barebone {
public async InlineHook create_inline_hook (uint64 target, uint64 handler, Allocator allocator, Cancellable? cancellable)
throws Error, IOError {
throw_not_supported ();
return yield X86InlineHook.create (target, handler, AMD64, allocator, gdb, cancellable);
}
public async uint64 translate_address (uint64 va, Cancellable? cancellable) throws Error, IOError {
return yield page_tables.translate (va, cancellable);
}
}
}
+90 -10
View File
@@ -56,9 +56,17 @@ namespace Frida {
if (host_session != null)
throw new Error.INVALID_OPERATION ("Already created");
Barebone.Config config;
Barebone.Config? config = null;
if (options != null) {
Value? val = options.map["config"];
if (val != null) {
config = (Barebone.Config) val.get_object ();
config.check ();
}
}
unowned string? config_path = Environment.get_variable ("FRIDA_BAREBONE_CONFIG");
if (config_path != null) {
if (config == null && config_path != null) {
try {
var config_data = yield FS.read_all_text (File.new_for_path (config_path), cancellable);
var cfg = (Barebone.Config) Json.gobject_from_data (typeof (Barebone.Config), config_data);
@@ -67,8 +75,18 @@ namespace Frida {
} catch (GLib.Error e) {
throw new Error.INVALID_ARGUMENT ("Unable to load %s: %s", config_path, e.message);
}
} else {
}
if (config == null)
config = new Barebone.Config ();
Barebone.AgentConfig? resident_agent = config.agent;
if (resident_agent != null && (resident_agent.transport is Barebone.DeviceTransportConfig
|| resident_agent.transport is Barebone.SocketTransportConfig)) {
host_session = yield attach_to_resident_agent (resident_agent.transport, cancellable);
host_session.agent_session_detached.connect (on_agent_session_detached);
return host_session;
}
SocketConnectable connectable;
@@ -148,8 +166,14 @@ namespace Frida {
}
}
Gee.List<Barebone.SymbolInfo> kernel_symbols = new Gee.ArrayList<Barebone.SymbolInfo> ();
if (config.kernel == WIN9X)
kernel_symbols = yield Barebone.collect_win9x_symbols (machine, cancellable);
Barebone.Allocator allocator;
Barebone.AllocatorConfig? ac = config.allocator;
if (ac == null)
ac = infer_allocator_config (config.kernel, kernel_symbols);
if (ac == null) {
allocator = new Barebone.NullAllocator (page_size);
} else if (ac is Barebone.PhysicalAllocatorConfig) {
@@ -171,8 +195,8 @@ namespace Frida {
Barebone.AgentConnection? agent_connection = null;
Barebone.AgentConfig? agent_config = config.agent;
if (agent_config != null) {
agent_connection = yield Barebone.AgentConnection.open (agent_config, config.image, relocation,
kernel_base, machine, allocator, cancellable);
agent_connection = yield Barebone.AgentConnection.open (agent_config, config.image, config.kernel,
relocation, kernel_base, machine, allocator, kernel_symbols, cancellable);
}
var interceptor = new Barebone.Interceptor (machine, allocator);
@@ -185,6 +209,56 @@ namespace Frida {
return host_session;
}
private async BareboneHostSession attach_to_resident_agent (Barebone.TransportConfig transport,
Cancellable? cancellable) throws Error, IOError {
#if WINDOWS
throw new Error.NOT_SUPPORTED ("Resident agents are not available on this OS");
#else
IOStream stream;
if (transport is Barebone.SocketTransportConfig) {
string path = ((Barebone.SocketTransportConfig) transport).path;
var client = new SocketClient ();
try {
stream = yield client.connect_async (new UnixSocketAddress (path), cancellable);
} catch (GLib.Error e) {
throw new Error.TRANSPORT ("Unable to connect to %s: %s", path, e.message);
}
} else {
string path = ((Barebone.DeviceTransportConfig) transport).path;
int fd = Posix.open (path, Posix.O_RDWR);
if (fd == -1)
throw new Error.TRANSPORT ("Unable to open %s: %s", path, Posix.strerror (Posix.errno));
stream = new SimpleIOStream (new UnixInputStream (fd, true), new UnixOutputStream (fd, false));
}
var connection = yield Barebone.AgentConnection.open_resident (stream, cancellable);
return new BareboneHostSession (connection, null);
#endif
}
private static Barebone.AllocatorConfig? infer_allocator_config (Barebone.KernelKind kind,
Gee.List<Barebone.SymbolInfo> kernel_symbols) {
if (kind != WIN9X)
return null;
Barebone.SymbolInfo? alloc = null;
Barebone.SymbolInfo? free = null;
foreach (var s in kernel_symbols) {
if (s.name == "_HeapAllocate")
alloc = s;
else if (s.name == "_HeapFree")
free = s;
}
if (alloc == null || free == null)
return null;
return new Barebone.TargetFunctionsAllocatorConfig () {
alloc_function = new Barebone.NonNullMemoryAddress ("allocator.alloc_function", alloc.offset),
free_function = new Barebone.NonNullMemoryAddress ("allocator.free_function", free.offset),
};
}
public async void destroy (HostSession session, Cancellable? cancellable) throws Error, IOError {
if (session != host_session)
throw new Error.INVALID_ARGUMENT ("Invalid host session");
@@ -236,7 +310,8 @@ namespace Frida {
construct;
}
public Barebone.Services services {
/** Absent for a resident agent: every service here is built on a debugger stub. */
public Barebone.Services? services {
get;
construct;
}
@@ -244,7 +319,7 @@ namespace Frida {
private Gee.Map<AgentSessionId?, BareboneAgentSession> agent_sessions =
new Gee.HashMap<AgentSessionId?, BareboneAgentSession> (AgentSessionId.hash, AgentSessionId.equal);
public BareboneHostSession (Barebone.AgentConnection? connection, Barebone.Services services) {
public BareboneHostSession (Barebone.AgentConnection? connection, Barebone.Services? services) {
Object (connection: connection, services: services);
}
@@ -279,7 +354,9 @@ namespace Frida {
public async HostProcessInfo[] enumerate_processes (HashTable<string, Variant> options,
Cancellable? cancellable) throws Error, IOError {
throw_not_supported ();
if (connection == null)
throw_not_supported ();
return yield connection.enumerate_processes (cancellable);
}
public async void enable_spawn_gating (Cancellable? cancellable) throws Error, IOError {
@@ -333,10 +410,13 @@ namespace Frida {
MainContext dbus_context = yield get_dbus_context ();
BareboneAgentSession session;
if (connection != null)
if (connection != null) {
session = new RemoteBareboneAgentSession (connection, session_id, opts.persist_timeout, dbus_context);
else
} else {
if (services == null)
throw new Error.NOT_SUPPORTED ("Barebone target has neither an agent nor a debugger stub");
session = new LocalBareboneAgentSession (services, session_id, opts.persist_timeout, dbus_context);
}
agent_sessions[session_id] = session;
session.closed.connect (on_agent_session_closed);
+81 -3
View File
@@ -76,6 +76,12 @@ namespace Frida.Barebone {
set;
}
public KernelKind kernel {
get;
set;
default = AUTO;
}
public void check () throws Error {
if (allocator != null)
allocator.check ();
@@ -111,9 +117,36 @@ namespace Frida.Barebone {
return true;
}
if (property_name == "kernel") {
var v = Value (typeof (KernelKind));
v.set_enum (parse_kernel_kind (property_node.get_string ()));
value = v;
return true;
}
value = Value (pspec.value_type);
return false;
}
private static KernelKind parse_kernel_kind (string? name) {
switch (name) {
case "bare": return KernelKind.BARE;
case "xnu": return KernelKind.XNU;
case "win9x": return KernelKind.WIN9X;
default: return KernelKind.AUTO;
}
}
}
/**
* Selects which bring-up dance the target kernel needs before the agent can be
* injected. AUTO infers XNU when an image is configured, and BARE otherwise.
*/
public enum KernelKind {
AUTO,
BARE,
XNU,
WIN9X
}
public sealed class ConnectionConfig : Object, Json.Serializable {
@@ -261,12 +294,13 @@ namespace Frida.Barebone {
}
public void check () throws Error {
if (path == null)
throw new Error.NOT_SUPPORTED ("Config for 'agent.path' is missing");
if (transport == null)
throw new Error.NOT_SUPPORTED ("Config for 'agent.transport' is missing");
transport.check ();
if (path == null && !(transport is DeviceTransportConfig)
&& !(transport is SocketTransportConfig))
throw new Error.NOT_SUPPORTED ("Config for 'agent.path' is missing");
}
public bool deserialize_property (string property_name, out Value value, ParamSpec pspec, Json.Node property_node) {
@@ -281,6 +315,12 @@ namespace Frida.Barebone {
case "vsock":
t = typeof (VsockTransportConfig);
break;
case "device":
t = typeof (DeviceTransportConfig);
break;
case "socket":
t = typeof (SocketTransportConfig);
break;
default:
break;
}
@@ -316,6 +356,11 @@ namespace Frida.Barebone {
set;
}
public string? bus {
get;
set;
}
public override void check () throws Error {
if (qmp == null)
throw new Error.NOT_SUPPORTED ("Config for 'agent.transport.qmp' is missing");
@@ -351,6 +396,39 @@ namespace Frida.Barebone {
}
}
/**
* An agent already resident in the target, reached through a device node it
* exposes. Nothing is injected, so there is no image and no stub.
*/
public sealed class DeviceTransportConfig : TransportConfig {
public string path {
get;
set;
}
public override void check () throws Error {
if (path == null)
throw new Error.NOT_SUPPORTED ("Config for 'agent.transport.path' is missing");
}
}
/**
* An agent already resident in the target, reached over a UNIX socket that the
* hypervisor has bridged to a port the guest kernel holds open. Nothing is
* injected, so there is no image and no stub.
*/
public sealed class SocketTransportConfig : TransportConfig {
public string path {
get;
set;
}
public override void check () throws Error {
if (path == null)
throw new Error.NOT_SUPPORTED ("Config for 'agent.transport.path' is missing");
}
}
public sealed class ImageConfig : Object, Json.Serializable {
public string file {
get;
+5
View File
@@ -9,6 +9,11 @@ name = "memory-scanner"
path = "bin/memory_scanner.rs"
test = false
[[bin]]
name = "marker"
path = "bin/marker.rs"
test = false
[features]
panic-info = []
+10 -1
View File
@@ -1,4 +1,4 @@
build: memory-scanner-arm64.elf
build: memory-scanner-arm64.elf marker-x86.elf
memory-scanner-arm64.elf:
RUSTFLAGS="-C code-model=tiny -C link-arg=--emit-relocs -C link-arg=-n -C link-arg=--discard-all" \
@@ -10,4 +10,13 @@ memory-scanner-arm64.elf:
$@.tmp
mv $@.tmp $@
marker-x86.elf:
RUSTC_BOOTSTRAP=1 RUSTFLAGS="-C link-arg=--emit-relocs -C link-arg=-n -C link-arg=--discard-all" \
cargo build -r --bin marker \
--target ../agent/i686-unknown-none.json -Z json-target-spec \
-Z build-std=core
cp target/i686-unknown-none/release/marker $@.tmp
rust-strip --strip-unneeded $@.tmp
mv $@.tmp $@
.PHONY: build
+17
View File
@@ -0,0 +1,17 @@
#![no_main]
#![no_std]
static ANSWER: u32 = 0x1234abcd;
#[no_mangle]
pub extern "C" fn _start(destination: *mut u32, _size: usize) {
unsafe {
*destination = &ANSWER as *const u32 as u32;
*destination.add(1) = core::ptr::read_volatile(&ANSWER);
}
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo<'_>) -> ! {
loop {}
}
Binary file not shown.
+121
View File
@@ -0,0 +1,121 @@
[CCode (gir_namespace = "FridaBarebone", gir_version = "1.0")]
namespace Frida.Barebone {
/**
* What differs between the kernels we inject into: how the target is brought to a
* quiescent state beforehand, and how it is let go afterwards.
*/
internal interface KernelFlavor : Object {
public abstract async void prepare (Cancellable? cancellable) throws Error, IOError;
public abstract async void settle (Cancellable? cancellable) throws Error, IOError;
}
internal sealed class BareKernelFlavor : Object, KernelFlavor {
private Machine machine;
public BareKernelFlavor (Machine machine) {
this.machine = machine;
}
public async void prepare (Cancellable? cancellable) throws Error, IOError {
}
public async void settle (Cancellable? cancellable) throws Error, IOError {
yield machine.gdb.continue (cancellable);
}
}
internal sealed class XnuKernelFlavor : Object, KernelFlavor {
private Machine machine;
private uint64 kernel_base;
private SymbolInfo thread_block;
private SymbolInfo? panic;
public XnuKernelFlavor (Machine machine, uint64 kernel_base, Gee.Map<string, SymbolInfo> symbols)
throws Error {
this.machine = machine;
this.kernel_base = kernel_base;
thread_block = symbols["thread_block"];
if (thread_block == null)
throw new Error.NOT_SUPPORTED ("Missing symbol for thread_block");
panic = symbols["panic"];
}
public async void prepare (Cancellable? cancellable) throws Error, IOError {
uint64 thread_block_address = kernel_base + thread_block.offset;
var arm64 = machine as Arm64Machine;
if (panic != null && arm64 != null)
arm64.call_landing_zone = kernel_base + panic.offset;
yield machine.enter_exception_level (1, 1000, cancellable);
yield run_until_thread_block (thread_block_address, cancellable);
if (arm64 != null)
yield arm64.learn_permission_templates (thread_block_address, cancellable);
}
public async void settle (Cancellable? cancellable) throws Error, IOError {
GDB.Client gdb = machine.gdb;
// The vphone research kernel panics on any synchronous exception taken while a
// debugger is attached, which the worker hits in the allocator during gum_init.
var arm64 = machine as Arm64Machine;
bool post_inject_access_uses_bridge = arm64 != null && arm64.physical_memory != null;
if (post_inject_access_uses_bridge)
yield gdb.detach (cancellable);
else
yield gdb.continue (cancellable);
}
private async void run_until_thread_block (uint64 address, Cancellable? cancellable) throws Error, IOError {
GDB.Client gdb = machine.gdb;
var bp = yield gdb.add_breakpoint (SOFT, address, 4, cancellable);
GDB.Breakpoint? hit = null;
do {
var exception = yield gdb.continue_until_exception (cancellable);
hit = exception.breakpoint;
} while (hit != bp);
yield bp.remove (cancellable);
}
}
internal sealed class Win9xKernelFlavor : Object, KernelFlavor {
private Machine machine;
private uint64 yield_point;
public Win9xKernelFlavor (Machine machine, Gee.Map<string, SymbolInfo> symbols) throws Error {
this.machine = machine;
SymbolInfo? get_system_time = symbols["Get_System_Time"];
if (get_system_time == null)
throw new Error.NOT_SUPPORTED ("Missing symbol for Get_System_Time");
yield_point = get_system_time.offset;
}
public async void prepare (Cancellable? cancellable) throws Error, IOError {
yield run_until_yield_point (cancellable);
}
public async void settle (Cancellable? cancellable) throws Error, IOError {
yield machine.gdb.continue (cancellable);
}
private async void run_until_yield_point (Cancellable? cancellable) throws Error, IOError {
GDB.Client gdb = machine.gdb;
var bp = yield gdb.add_breakpoint (SOFT, yield_point, 1, cancellable);
GDB.Breakpoint? hit = null;
do {
var exception = yield gdb.continue_until_exception (cancellable);
hit = exception.breakpoint;
} while (hit != bp);
yield bp.remove (cancellable);
}
}
}
+56
View File
@@ -192,6 +192,62 @@ namespace Frida.Barebone {
public abstract async void disable (Cancellable? cancellable) throws Error, IOError;
}
internal enum AddressingMode {
VIRTUAL,
PHYSICAL
}
// Restoring the mode must be awaited rather than fired and forgotten: a reply left in flight
// arrives while the next command is pending, and that command consumes it instead — leaving
// its caller with an empty response and the stub still in the old mode. Vala rejects a yield
// inside a finally block, so callers hold on to any failure across the restore.
internal static async void set_addressing_mode (GDB.Client gdb, AddressingMode mode, Cancellable? cancellable)
throws Error, IOError {
Gee.Set<string> features = gdb.features;
string enabled = (mode == PHYSICAL) ? "1" : "0";
if ("qemu-phy-mem-mode" in features)
yield gdb.execute_simple ("Qqemu.PhyMemMode:" + enabled, cancellable);
else if ("vf-phy-mem-mode" in features)
yield gdb.execute_simple ("Qvf.PhyMemMode:" + enabled, cancellable);
else
throw new Error.NOT_SUPPORTED ("Unsupported GDB remote stub; please file a bug");
}
internal static void throw_if_failed (GLib.Error? failure) throws Error, IOError {
if (failure == null)
return;
if (failure is IOError)
throw (IOError) failure;
throw (Error) failure;
}
internal static Gee.List<RangeDetails> coalesce_ranges (Gee.List<RangeDetails> ranges) {
var result = new Gee.ArrayList<RangeDetails> ();
RangeDetails? pending = null;
foreach (RangeDetails r in ranges) {
if (pending == null) {
pending = r.clone ();
continue;
}
if (r.base_va == pending.base_va + pending.size &&
r.base_pa == pending.base_pa + pending.size &&
r.protection == pending.protection &&
r.type == pending.type) {
pending.size += r.size;
continue;
}
result.add (pending);
pending = r.clone ();
}
if (pending != null)
result.add (pending);
return result;
}
internal static uint64 round_address_up (uint64 address, size_t n) {
return (address + n - 1) & ~((uint64) n - 1);
}
+5
View File
@@ -17,6 +17,11 @@ backend_sources += files(
'lzfse.vala',
'qmp-client.vala',
'kernel-relocation.vala',
'kernel-flavor.vala',
'win9x.vala',
'win9x-service-names.vala',
'x86-paging.vala',
'x86-inline-hook.vala',
'vz-stub.vala',
'arch-x86' / 'machine.vala',
'arch-x86_64' / 'machine.vala',
+17 -5
View File
@@ -48,7 +48,13 @@ namespace Frida.Barebone {
}
private async bool init_async (int io_priority, Cancellable? cancellable) throws Error, IOError {
var connectable = parse_socket_address (address, port, "localhost", 4444);
SocketConnectable connectable;
if (address.has_prefix ("unix:")) {
connectable = new UnixSocketAddress.with_type (address.substring (5), -1,
UnixSocketAddressType.PATH);
} else {
connectable = parse_socket_address (address, port, "localhost", 4444);
}
try {
var client = new SocketClient ();
@@ -101,13 +107,19 @@ namespace Frida.Barebone {
}
}
public async Hostlink open_hostlink (Cancellable? cancellable = null) throws Error, IOError {
public async Hostlink open_hostlink (string? preferred_bus = null, Cancellable? cancellable = null)
throws Error, IOError {
#if WINDOWS
throw new Error.NOT_SUPPORTED ("Missing open_hostlink() for Windows");
#else
uint64 mmio = (uint64) yield get_qom_property_int ("/machine", "hostlink-mmio", cancellable);
uint irq = (uint) yield get_qom_property_int ("/machine", "hostlink-irq", cancellable);
string bus = yield get_qom_property_string ("/machine", "hostlink-bus", cancellable);
uint64 mmio = 0;
uint irq = 0;
string bus = preferred_bus;
if (bus == null) {
mmio = (uint64) yield get_qom_property_int ("/machine", "hostlink-mmio", cancellable);
irq = (uint) yield get_qom_property_int ("/machine", "hostlink-irq", cancellable);
bus = yield get_qom_property_string ("/machine", "hostlink-bus", cancellable);
}
int fds[2];
if (Posix.socketpair (Posix.AF_UNIX, Posix.SOCK_STREAM, 0, fds) != 0)
+550
View File
@@ -0,0 +1,550 @@
[CCode (gir_namespace = "FridaBarebone", gir_version = "1.0")]
namespace Frida.Barebone {
private const string[] VMM_SERVICE_NAMES = {
"Get_VMM_Version",
"Get_Cur_VM_Handle",
"Test_Cur_VM_Handle",
"Get_Sys_VM_Handle",
"Test_Sys_VM_Handle",
"Validate_VM_Handle",
"Get_VMM_Reenter_Count",
"Begin_Reentrant_Execution",
"End_Reentrant_Execution",
"Install_V86_Break_Point",
"Remove_V86_Break_Point",
"Allocate_V86_Call_Back",
"Allocate_PM_Call_Back",
"Call_When_VM_Returns",
"Schedule_Global_Event",
"Schedule_VM_Event",
"Call_Global_Event",
"Call_VM_Event",
"Cancel_Global_Event",
"Cancel_VM_Event",
"Call_Priority_VM_Event",
"Cancel_Priority_VM_Event",
"Get_NMI_Handler_Addr",
"Set_NMI_Handler_Addr",
"Hook_NMI_Event",
"Call_When_VM_Ints_Enabled",
"Enable_VM_Ints",
"Disable_VM_Ints",
"Map_Flat",
"Map_Lin_To_VM_Addr",
"Adjust_Exec_Priority",
"Begin_Critical_Section",
"End_Critical_Section",
"End_Crit_And_Suspend",
"Claim_Critical_Section",
"Release_Critical_Section",
"Call_When_Not_Critical",
"Create_Semaphore",
"Destroy_Semaphore",
"Wait_Semaphore",
"Signal_Semaphore",
"Get_Crit_Section_Status",
"Call_When_Task_Switched",
"Suspend_VM",
"Resume_VM",
"No_Fail_Resume_VM",
"Nuke_VM",
"Crash_Cur_VM",
"Get_Execution_Focus",
"Set_Execution_Focus",
"Get_Time_Slice_Priority",
"Set_Time_Slice_Priority",
"Get_Time_Slice_Granularity",
"Set_Time_Slice_Granularity",
"Get_Time_Slice_Info",
"Adjust_Execution_Time",
"Release_Time_Slice",
"Wake_Up_VM",
"Call_When_Idle",
"Get_Next_VM_Handle",
"Set_Global_Time_Out",
"Set_VM_Time_Out",
"Cancel_Time_Out",
"Get_System_Time",
"Get_VM_Exec_Time",
"Hook_V86_Int_Chain",
"Get_V86_Int_Vector",
"Set_V86_Int_Vector",
"Get_PM_Int_Vector",
"Set_PM_Int_Vector",
"Simulate_Int",
"Simulate_Iret",
"Simulate_Far_Call",
"Simulate_Far_Jmp",
"Simulate_Far_Ret",
"Simulate_Far_Ret_N",
"Build_Int_Stack_Frame",
"Simulate_Push",
"Simulate_Pop",
"_HeapAllocate",
"_HeapReAllocate",
"_HeapFree",
"_HeapGetSize",
"_PageAllocate",
"_PageReAllocate",
"_PageFree",
"_PageLock",
"_PageUnLock",
"_PageGetSizeAddr",
"_PageGetAllocInfo",
"_GetFreePageCount",
"_GetSysPageCount",
"_GetVMPgCount",
"_MapIntoV86",
"_PhysIntoV86",
"_TestGlobalV86Mem",
"_ModifyPageBits",
"_CopyPageTable",
"_LinMapIntoV86",
"_LinPageLock",
"_LinPageUnLock",
"_SetResetV86Pageable",
"_GetV86PageableArray",
"_PageCheckLinRange",
"_PageOutDirtyPages",
"_PageDiscardPages",
"_GetNulPageHandle",
"_GetFirstV86Page",
"_MapPhysToLinear",
"_GetAppFlatDSAlias",
"_SelectorMapFlat",
"_GetDemandPageInfo",
"_GetSetPageOutCount",
"Hook_V86_Page",
"_Assign_Device_V86_Pages",
"_DeAssign_Device_V86_Pages",
"_Get_Device_V86_Pages_Array",
"MMGR_SetNULPageAddr",
"_Allocate_GDT_Selector",
"_Free_GDT_Selector",
"_Allocate_LDT_Selector",
"_Free_LDT_Selector",
"_BuildDescriptorDWORDs",
"_GetDescriptor",
"_SetDescriptor",
"_MMGR_Toggle_HMA",
"Get_Fault_Hook_Addrs",
"Hook_V86_Fault",
"Hook_PM_Fault",
"Hook_VMM_Fault",
"Begin_Nest_V86_Exec",
"Begin_Nest_Exec",
"Exec_Int",
"Resume_Exec",
"End_Nest_Exec",
"Allocate_PM_App_CB_Area",
"Get_Cur_PM_App_CB",
"Set_V86_Exec_Mode",
"Set_PM_Exec_Mode",
"Begin_Use_Locked_PM_Stack",
"End_Use_Locked_PM_Stack",
"Save_Client_State",
"Restore_Client_State",
"Exec_VxD_Int",
"Hook_Device_Service",
"Hook_Device_V86_API",
"Hook_Device_PM_API",
"System_Control",
"Simulate_IO",
"Install_Mult_IO_Handlers",
"Install_IO_Handler",
"Enable_Global_Trapping",
"Enable_Local_Trapping",
"Disable_Global_Trapping",
"Disable_Local_Trapping",
"List_Create",
"List_Destroy",
"List_Allocate",
"List_Attach",
"List_Attach_Tail",
"List_Insert",
"List_Remove",
"List_Deallocate",
"List_Get_First",
"List_Get_Next",
"List_Remove_First",
"_AddInstanceItem",
"_Allocate_Device_CB_Area",
"_Allocate_Global_V86_Data_Area",
"_Allocate_Temp_V86_Data_Area",
"_Free_Temp_V86_Data_Area",
"Get_Profile_Decimal_Int",
"Convert_Decimal_String",
"Get_Profile_Fixed_Point",
"Convert_Fixed_Point_String",
"Get_Profile_Hex_Int",
"Convert_Hex_String",
"Get_Profile_Boolean",
"Convert_Boolean_String",
"Get_Profile_String",
"Get_Next_Profile_String",
"Get_Environment_String",
"Get_Exec_Path",
"Get_Config_Directory",
"OpenFile",
"Get_PSP_Segment",
"GetDOSVectors",
"Get_Machine_Info",
"GetSet_HMA_Info",
"RESERVED_Set_System_Exit_Code",
"Fatal_Error_Handler",
"Fatal_Memory_Error",
"Update_System_Clock",
"Test_Debug_Installed",
"Out_Debug_String",
"Out_Debug_Chr",
"In_Debug_Chr",
"Debug_Convert_Hex_Binary",
"Debug_Convert_Hex_Decimal",
"Debug_Test_Valid_Handle",
"Validate_Client_Ptr",
"Test_Reenter",
"Queue_Debug_String",
"Log_Proc_Call",
"Debug_Test_Cur_VM",
"Get_PM_Int_Type",
"Set_PM_Int_Type",
"Get_Last_Updated_System_Time",
"Get_Last_Updated_VM_Exec_Time",
"Test_DBCS_Lead_Byte",
"_AddFreePhysPage",
"_PageResetHandlePAddr",
"_SetLastV86Page",
"_GetLastV86Page",
"_MapFreePhysReg",
"_UnmapFreePhysReg",
"_XchgFreePhysReg",
"_SetFreePhysRegCalBk",
"Get_Next_Arena",
"Get_Name_Of_Ugly_TSR",
"Get_Debug_Options",
"Set_Physical_HMA_Alias",
"_GetGlblRng0V86IntBase",
"_Add_Global_V86_Data_Area",
"GetSetDetailedVMError",
"Is_Debug_Chr",
"Clear_Mono_Screen",
"Out_Mono_Chr",
"Out_Mono_String",
"Set_Mono_Cur_Pos",
"Get_Mono_Cur_Pos",
"Get_Mono_Chr",
"Locate_Byte_In_ROM",
"Hook_Invalid_Page_Fault",
"Unhook_Invalid_Page_Fault",
"RESERVED_Set_Delete_On_Exit_File",
"Close_VM",
"Enable_Touch_1st_Meg",
"Disable_Touch_1st_Meg",
"Install_Exception_Handler",
"Remove_Exception_Handler",
"Get_Crit_Status_No_Block",
"_GetLastUpdatedThreadExecTime",
"_Trace_Out_Service",
"_Debug_Out_Service",
"_Debug_Flags_Service",
"VMMAddImportModuleName",
"VMM_Add_DDB",
"VMM_Remove_DDB",
"Test_VM_Ints_Enabled",
"_BlockOnID",
"Schedule_Thread_Event",
"Cancel_Thread_Event",
"Set_Thread_Time_Out",
"Set_Async_Time_Out",
"_AllocateThreadDataSlot",
"_FreeThreadDataSlot",
"_CreateMutex",
"_DestroyMutex",
"_GetMutexOwner",
"Call_When_Thread_Switched",
"VMMCreateThread",
"_GetThreadExecTime",
"VMMTerminateThread",
"Get_Cur_Thread_Handle",
"Test_Cur_Thread_Handle",
"Get_Sys_Thread_Handle",
"Test_Sys_Thread_Handle",
"Validate_Thread_Handle",
"Get_Initial_Thread_Handle",
"Test_Initial_Thread_Handle",
"Debug_Test_Valid_Thread_Handle",
"Debug_Test_Cur_Thread",
"VMM_GetSystemInitState",
"Cancel_Call_When_Thread_Switched",
"Get_Next_Thread_Handle",
"Adjust_Thread_Exec_Priority",
"_Deallocate_Device_CB_Area",
"Remove_IO_Handler",
"Remove_Mult_IO_Handlers",
"Unhook_V86_Int_Chain",
"Unhook_V86_Fault",
"Unhook_PM_Fault",
"Unhook_VMM_Fault",
"Unhook_Device_Service",
"_PageReserve",
"_PageCommit",
"_PageDecommit",
"_PagerRegister",
"_PagerQuery",
"_PagerDeregister",
"_ContextCreate",
"_ContextDestroy",
"_PageAttach",
"_PageFlush",
"_SignalID",
"_PageCommitPhys",
"_Register_Win32_Services",
"Cancel_Call_When_Not_Critical",
"Cancel_Call_When_Idle",
"Cancel_Call_When_Task_Switched",
"_Debug_Printf_Service",
"_EnterMutex",
"_LeaveMutex",
"Simulate_VM_IO",
"Signal_Semaphore_No_Switch",
"_ContextSwitch",
"_PageModifyPermissions",
"_PageQuery",
"_EnterMustComplete",
"_LeaveMustComplete",
"_ResumeExecMustComplete",
"_GetThreadTerminationStatus",
"_GetInstanceInfo",
"_ExecIntMustComplete",
"_ExecVxDIntMustComplete",
"Begin_V86_Serialization",
"Unhook_V86_Page",
"VMM_GetVxDLocationList",
"VMM_GetDDBList",
"Unhook_NMI_Event",
"Get_Instanced_V86_Int_Vector",
"Get_Set_Real_DOS_PSP",
"Call_Priority_Thread_Event",
"Get_System_Time_Address",
"Get_Crit_Status_Thread",
"Get_DDB",
"Directed_Sys_Control",
"_RegOpenKey",
"_RegCloseKey",
"_RegCreateKey",
"_RegDeleteKey",
"_RegEnumKey",
"_RegQueryValue",
"_RegSetValue",
"_RegDeleteValue",
"_RegEnumValue",
"_RegQueryValueEx",
"_RegSetValueEx",
"_CallRing3",
"Exec_PM_Int",
"_RegFlushKey",
"_PageCommitContig",
"_GetCurrentContext",
"_LocalizeSprintf",
"_LocalizeStackSprintf",
"Call_Restricted_Event",
"Cancel_Restricted_Event",
"Register_PEF_Provider",
"_GetPhysPageInfo",
"_RegQueryInfoKey",
"MemArb_Reserve_Pages",
"Time_Slice_Sys_VM_Idle",
"Time_Slice_Sleep",
"Boost_With_Decay",
"Set_Inversion_Pri",
"Reset_Inversion_Pri",
"Release_Inversion_Pri",
"Get_Thread_Win32_Pri",
"Set_Thread_Win32_Pri",
"Set_Thread_Static_Boost",
"Set_VM_Static_Boost",
"Release_Inversion_Pri_ID",
"Attach_Thread_To_Group",
"Detach_Thread_From_Group",
"Set_Group_Static_Boost",
"_GetRegistryPath",
"_GetRegistryKey",
"Cleanup_Thread_State",
"_RegRemapPreDefKey",
"End_V86_Serialization",
"_Assert_Range",
"_Sprintf",
"_PageChangePager",
"_RegCreateDynKey",
"_RegQueryMultipleValues",
"Boost_Thread_With_VM",
"Get_Boot_Flags",
"Set_Boot_Flags",
"_lstrcpyn",
"_lstrlen",
"_lmemcpy",
"_GetVxDName",
"Force_Mutexes_Free",
"Restore_Forced_Mutexes",
"_AddReclaimableItem",
"_SetReclaimableItem",
"_EnumReclaimableItem",
"Time_Slice_Wake_Sys_VM",
"VMM_Replace_Global_Environment",
"Begin_Non_Serial_Nest_V86_Exec",
"Get_Nest_Exec_Status",
"Open_Boot_Log",
"Write_Boot_Log",
"Close_Boot_Log",
"EnableDisable_Boot_Log",
"_Call_On_My_Stack",
"Get_Inst_V86_Int_Vec_Base",
"_lstrcmpi",
"_strupr",
"Log_Fault_Call_Out",
"_AtEventTime",
"_PageOutPages",
"_Call_On_My_Not_Flat_Stack",
"_LinRegionLock",
"_LinRegionUnLock",
"_AttemptingSomethingDangerous",
"_Vsprintf",
"_Vsprintfw",
"Load_FS_Service",
"Assert_FS_Service",
"ObsoleteRtlUnwind",
"ObsoleteRtlRaiseException",
"ObsoleteRtlRaiseStatus",
"ObsoleteKeGetCurrentIrql",
"ObsoleteKfRaiseIrql",
"ObsoleteKfLowerIrql",
"_Begin_Preemptable_Code",
"_End_Preemptable_Code",
"Set_Preemptable_Count",
"ObsoleteKeInitializeDpc",
"ObsoleteKeInsertQueueDpc",
"ObsoleteKeRemoveQueueDpc",
"HeapAllocateEx",
"HeapReAllocateEx",
"HeapGetSizeEx",
"HeapFreeEx",
"_Get_CPUID_Flags",
"KeCheckDivideByZeroTrap",
"_RegisterGARTHandler",
"_GARTReserve",
"_GARTCommit",
"_GARTUnCommit",
"_GARTFree",
"_GARTMemAttributes",
"KfRaiseIrqlToDpcLevel",
"VMMCreateThreadEx",
"_FlushCaches",
"Set_Thread_Win32_Pri_NoYield",
"_FlushMappedCacheBlock",
"_ReleaseMappedCacheBlock",
"Run_Preemptable_Events",
"_MMPreSystemExit",
"_MMPageFileShutDown",
"_Set_Global_Time_Out_Ex",
"Query_Thread_Priority",
"_UnmapPhysToLinear",
"_VmmRtInfo",
"_MPGetProcessorCount",
"_MPEnterSingleProcessor",
"_MPLeaveSingleProcessor",
};
private const string[] VWIN32_SERVICE_NAMES = {
"VWIN32_Get_Version",
"VWIN32_DIOCCompletionRoutine",
"_VWIN32_QueueUserApc",
"_VWIN32_Get_Thread_Context",
"_VWIN32_Set_Thread_Context",
"_VWIN32_CopyMem",
"_VWIN32_Npx_Exception",
"_VWIN32_Emulate_Npx",
"_VWIN32_CheckDelayedNpxTrap",
"VWIN32_EnterCrstR0",
"VWIN32_LeaveCrstR0",
"_VWIN32_FaultPopup",
"VWIN32_GetContextHandle",
"VWIN32_GetCurrentProcessHandle",
"_VWIN32_SetWin32Event",
"_VWIN32_PulseWin32Event",
"_VWIN32_ResetWin32Event",
"_VWIN32_WaitSingleObject",
"_VWIN32_WaitMultipleObjects",
"_VWIN32_CreateRing0Thread",
"_VWIN32_CloseVxDHandle",
"VWIN32_ActiveTimeBiasSet",
"VWIN32_GetCurrentDirectory",
"VWIN32_BlueScreenPopup",
"VWIN32_TerminateApp",
"_VWIN32_QueueKernelAPC",
"VWIN32_SysErrorBox",
"_VWIN32_IsClientWin32",
"VWIN32_IFSRIPWhenLev2Taken",
"_VWIN32_InitWin32Event",
"_VWIN32_InitWin32Mutex",
"_VWIN32_ReleaseWin32Mutex",
"_VWIN32_BlockThreadEx",
"VWIN32_GetProcessHandle",
"_VWIN32_InitWin32Semaphore",
"_VWIN32_SignalWin32Sem",
"_VWIN32_QueueUserApcEx",
"_VWIN32_OpenVxDHandle",
"_VWIN32_CloseWin32Handle",
"_VWIN32_AllocExternalHandle",
"_VWIN32_UseExternalHandle",
"_VWIN32_UnuseExternalHandle",
"KeInitializeTimer",
"KeSetTimer",
"KeCancelTimer",
"KeReadStateTimer",
"_VWIN32_ReferenceObject",
"_VWIN32_GetExternalHandle",
"VWIN32_ConvertNtTimeout",
"_VWIN32_SetWin32EventBoostPriority",
"_VWIN32_GetRing3Flat32Selectors",
"_VWIN32_GetCurThreadCondition",
"VWIN32_Init_FP",
"R0SetWaitableTimer",
};
private const string[] VPICD_SERVICE_NAMES = {
"VPICD_Get_Version",
"VPICD_Virtualize_IRQ",
"VPICD_Set_Int_Request",
"VPICD_Clear_Int_Request",
"VPICD_Phys_EOI",
"VPICD_Get_Complete_Status",
"VPICD_Get_Status",
"VPICD_Test_Phys_Request",
"VPICD_Physically_Mask",
"VPICD_Physically_Unmask",
"VPICD_Set_Auto_Masking",
"VPICD_Get_IRQ_Complete_Status",
"VPICD_Convert_Handle_To_IRQ",
"VPICD_Convert_IRQ_To_Int",
"VPICD_Convert_Int_To_IRQ",
"VPICD_Call_When_Hw_Int",
"VPICD_Force_Default_Owner",
"VPICD_Force_Default_Behavior",
"VPICD_Auto_Mask_At_Inst_Swap",
"VPICD_Begin_Inst_Page_Swap",
"VPICD_End_Inst_Page_Swap",
"VPICD_Virtual_EOI",
"VPICD_Get_Virtualization_Count",
"VPICD_Post_Sys_Critical_Init",
"VPICD_VM_SlavePIC_Mask_Change",
"_VPICD_Clear_IR_Bits",
"RESERVED_VPICD_Get_Level_Mask",
"RESERVED_VPICD_Set_Level_Mask",
"_VPICD_Set_Irql_Mask",
"_VPICD_Set_Channel_Irql",
"_VPICD_Prepare_For_Shutdown",
"_VPICD_Register_Trigger_Handler",
};
}
+187
View File
@@ -0,0 +1,187 @@
[CCode (gir_namespace = "FridaBarebone", gir_version = "1.0")]
namespace Frida.Barebone {
public static async Gee.List<SymbolInfo> collect_win9x_symbols (Machine machine, Cancellable? cancellable)
throws Error, IOError {
var symbols = new Gee.ArrayList<SymbolInfo> ();
foreach (DeviceDescriptorBlock ddb in yield find_descriptor_blocks (machine, cancellable)) {
unowned string[]? names = known_service_names (ddb.name);
if (names == null)
continue;
var addresses = yield read_service_table (machine, ddb, cancellable);
for (int ordinal = 0; ordinal != addresses.size; ordinal++) {
uint64 address = addresses[ordinal];
if (!is_implemented (address))
continue;
symbols.add (new SymbolInfo () {
name = (ordinal < names.length)
? names[ordinal]
: "%s_service_%d".printf (ddb.name, ordinal),
offset = (uint32) address,
symbol_type = 0xf,
section = 0x10,
});
}
}
return symbols;
}
private static async Gee.List<DeviceDescriptorBlock> find_descriptor_blocks (Machine machine, Cancellable? cancellable)
throws Error, IOError {
var arena = new Gee.ArrayList<RangeDetails> ();
yield machine.enumerate_ranges (Gum.PageProtection.READ, r => {
if (r.base_va >= SYSTEM_ARENA_BASE && r.base_va < SYSTEM_ARENA_LIMIT)
arena.add (r.clone ());
return true;
}, cancellable);
var blocks = new Gee.ArrayList<DeviceDescriptorBlock> ();
foreach (RangeDetails r in arena) {
foreach (DeviceDescriptorBlock candidate in yield harvest_candidates (machine, r, cancellable)) {
if (yield candidate.is_credible (machine, cancellable))
blocks.add (candidate);
}
}
return blocks;
}
private static async Gee.List<DeviceDescriptorBlock> harvest_candidates (Machine machine, RangeDetails range,
Cancellable? cancellable) throws Error, IOError {
var candidates = new Gee.ArrayList<DeviceDescriptorBlock> ();
GDB.Client gdb = machine.gdb;
for (uint64 offset = 0; offset < range.size; offset += SCAN_CHUNK_SIZE) {
uint64 chunk_start = range.base_va + offset;
uint64 remaining = range.size - offset;
size_t readable_size = (size_t) uint64.min (SCAN_CHUNK_SIZE + STRADDLE_ALLOWANCE, remaining);
size_t claimed_size = (size_t) uint64.min (SCAN_CHUNK_SIZE, remaining);
Bytes chunk;
try {
chunk = yield gdb.read_byte_array (chunk_start, readable_size, cancellable);
} catch (Error e) {
continue;
}
Buffer buf = gdb.make_buffer (chunk);
for (size_t pos = 0; pos < claimed_size && pos + DDB_SIZE <= readable_size; pos += 4) {
DeviceDescriptorBlock? ddb = DeviceDescriptorBlock.parse (buf, pos, chunk_start + pos);
if (ddb != null)
candidates.add (ddb);
}
}
return candidates;
}
private static async Gee.List<uint64?> read_service_table (Machine machine, DeviceDescriptorBlock ddb,
Cancellable? cancellable) throws Error, IOError {
Bytes raw = yield machine.gdb.read_byte_array (ddb.service_table, ddb.service_count * 4, cancellable);
Buffer buf = machine.gdb.make_buffer (raw);
var addresses = new Gee.ArrayList<uint64?> ();
for (uint i = 0; i != ddb.service_count; i++)
addresses.add (buf.read_uint32 (i * 4));
return addresses;
}
private class DeviceDescriptorBlock {
public string name;
public uint64 address;
public uint64 service_table;
public uint32 service_count;
public static DeviceDescriptorBlock? parse (Buffer buf, size_t offset, uint64 address) {
string? name = parse_name (buf, offset + NAME_OFFSET);
if (name == null)
return null;
uint32 count = buf.read_uint32 (offset + SERVICE_TABLE_SIZE_OFFSET);
if (count == 0 || count > MAX_SERVICES)
return null;
uint32 table = buf.read_uint32 (offset + SERVICE_TABLE_PTR_OFFSET);
if (!is_arena_address (table))
return null;
uint32 control_proc = buf.read_uint32 (offset + CONTROL_PROC_OFFSET);
if (control_proc != 0 && !is_arena_address (control_proc))
return null;
return new DeviceDescriptorBlock () {
name = name,
address = address,
service_table = table,
service_count = count,
};
}
public async bool is_credible (Machine machine, Cancellable? cancellable) throws Error, IOError {
Bytes first_entry;
try {
first_entry = yield machine.gdb.read_byte_array (service_table, 4, cancellable);
} catch (Error e) {
return false;
}
return is_arena_address (machine.gdb.make_buffer (first_entry).read_uint32 (0));
}
private static string? parse_name (Buffer buf, size_t offset) {
var name = new StringBuilder ();
bool padding_reached = false;
for (size_t i = 0; i != NAME_SIZE; i++) {
char c = (char) buf.read_uint8 (offset + i);
if (c == ' ') {
padding_reached = true;
continue;
}
if (padding_reached)
return null;
if (!c.isupper () && !c.isdigit () && c != '_' && c != '$')
return null;
name.append_c (c);
}
if (name.len == 0)
return null;
return name.str;
}
private const size_t CONTROL_PROC_OFFSET = 0x18;
private const size_t NAME_OFFSET = 0x0c;
private const size_t NAME_SIZE = 8;
private const size_t SERVICE_TABLE_PTR_OFFSET = 0x30;
private const size_t SERVICE_TABLE_SIZE_OFFSET = 0x34;
}
private static unowned string[]? known_service_names (string vxd) {
if (vxd == "VMM")
return VMM_SERVICE_NAMES;
if (vxd == "VPICD")
return VPICD_SERVICE_NAMES;
if (vxd == "VWIN32")
return VWIN32_SERVICE_NAMES;
return null;
}
private static bool is_arena_address (uint64 address) {
return address >= SYSTEM_ARENA_BASE && address < SYSTEM_ARENA_LIMIT;
}
private static bool is_implemented (uint64 service_address) {
return service_address != 0;
}
private const size_t DDB_SIZE = 0x38;
private const size_t STRADDLE_ALLOWANCE = DDB_SIZE;
private const uint64 SYSTEM_ARENA_BASE = 0xc0000000;
private const uint64 SYSTEM_ARENA_LIMIT = 0xc4000000;
private const uint32 MAX_SERVICES = 0x400;
private const size_t SCAN_CHUNK_SIZE = 256 * 1024;
}
+125
View File
@@ -0,0 +1,125 @@
[CCode (gir_namespace = "FridaBarebone", gir_version = "1.0")]
namespace Frida.Barebone {
internal class X86InlineHook : Object, InlineHook {
private uint64 target;
private Bytes old_target_code;
private Bytes new_target_code;
private Allocation allocation;
private GDB.Client gdb;
private State state = DISABLED;
private enum State {
DISABLED,
ENABLED,
DESTROYED
}
private const size_t SCRATCH_SIZE = 512;
private const size_t DIRECT_REDIRECT_SIZE = 5;
private const size_t INDIRECT_REDIRECT_SIZE = 16;
private const size_t MAX_INSTRUCTION_SIZE = 15;
public static async InlineHook create (uint64 target, uint64 handler, Gum.CpuType cpu, Allocator allocator,
GDB.Client gdb, Cancellable? cancellable) throws Error, IOError {
Allocation allocation = yield allocator.allocate (allocator.page_size, allocator.page_size, cancellable);
uint64 code_va = allocation.virtual_address;
var scratch = new uint8[SCRATCH_SIZE];
size_t pointer_size = (cpu == IA32) ? 4 : 8;
var cw = new Gum.X86Writer (scratch);
cw.set_target_cpu (cpu);
cw.pc = code_va;
emit_prolog (cw, target);
cw.put_mov_reg_reg (Gum.X86Reg.XAX, Gum.X86Reg.XSP);
cw.put_call_address_with_arguments (CAPI, handler, 1, Gum.ArgType.REGISTER, Gum.X86Reg.XAX);
emit_epilog (cw, pointer_size);
size_t redirect_size = Gum.X86Writer.can_branch_directly_between (target, code_va)
? DIRECT_REDIRECT_SIZE
: INDIRECT_REDIRECT_SIZE;
Bytes displaced = yield gdb.read_byte_array (target, redirect_size + MAX_INSTRUCTION_SIZE, cancellable);
var rl = new Gum.X86Relocator (displaced.get_data (), cw);
rl.input_pc = target;
uint reloc_bytes = 0;
do
reloc_bytes = rl.read_one ();
while (reloc_bytes < redirect_size);
rl.write_all ();
if (!rl.eoi)
cw.put_jmp_address (target + reloc_bytes);
cw.flush ();
var trampoline_code = new Bytes (scratch[:cw.offset ()]);
yield gdb.write_byte_array (code_va, trampoline_code, cancellable);
cw.reset (scratch);
cw.set_target_cpu (cpu);
cw.pc = target;
cw.put_jmp_address (code_va);
cw.flush ();
var new_target_code = new Bytes (scratch[:cw.offset ()]);
var old_target_code = new Bytes (displaced.get_data ()[:new_target_code.get_size ()]);
return new X86InlineHook (target, old_target_code, new_target_code, allocation, gdb);
}
private static void emit_prolog (Gum.X86Writer cw, uint64 target) {
cw.put_push_u32 (0);
cw.put_pushax ();
cw.put_mov_reg_address (Gum.X86Reg.XAX, target);
cw.put_push_reg (Gum.X86Reg.XAX);
}
private static void emit_epilog (Gum.X86Writer cw, size_t pointer_size) {
cw.put_add_reg_imm (Gum.X86Reg.XSP, (ssize_t) pointer_size);
cw.put_popax ();
cw.put_add_reg_imm (Gum.X86Reg.XSP, (ssize_t) pointer_size);
}
public X86InlineHook (uint64 target, Bytes old_target_code, Bytes new_target_code, Allocation allocation,
GDB.Client gdb) {
this.target = target;
this.old_target_code = old_target_code;
this.new_target_code = new_target_code;
this.allocation = allocation;
this.gdb = gdb;
}
public async void destroy (Cancellable? cancellable) throws Error, IOError {
if (state == DESTROYED)
return;
bool was_running = gdb.state != STOPPED;
if (was_running)
yield gdb.stop (cancellable);
yield disable (cancellable);
yield allocation.deallocate (cancellable);
state = DESTROYED;
if (was_running)
yield gdb.continue (cancellable);
}
public async void enable (Cancellable? cancellable) throws Error, IOError {
if (state == ENABLED)
return;
if (state != DISABLED)
throw new Error.INVALID_OPERATION ("Invalid operation");
yield gdb.write_byte_array (target, new_target_code, cancellable);
state = ENABLED;
}
public async void disable (Cancellable? cancellable) throws Error, IOError {
if (state != ENABLED)
return;
yield gdb.write_byte_array (target, old_target_code, cancellable);
state = DISABLED;
}
}
}
+716
View File
@@ -0,0 +1,716 @@
[CCode (gir_namespace = "FridaBarebone", gir_version = "1.0")]
namespace Frida.Barebone {
internal class X86PageTables : Object {
public GDB.Client gdb {
get;
construct;
}
public PhysicalMemory? physical_memory {
get;
set;
}
// Fixed for the kernel we are attached to.
private MMUParameters? cached_parameters;
private const size_t PAGE_SIZE = 4096;
private const Gum.PageProtection MAPPING_PROTECTION =
Gum.PageProtection.READ | Gum.PageProtection.WRITE | Gum.PageProtection.EXECUTE;
private const uint64 PRESENT_BIT = 1ULL << 0;
private const uint64 WRITABLE_BIT = 1ULL << 1;
private const uint64 LARGE_PAGE_BIT = 1ULL << 7;
private const uint64 NX_BIT = 1ULL << 63;
private const uint64 LEGACY_ADDRESS_MASK = 0xfffff000ULL;
private const uint64 EXTENDED_ADDRESS_MASK = 0x000ffffffffff000ULL;
private const uint64 LEGACY_LARGE_PAGE_ADDRESS_MASK = 0xffc00000ULL;
private const uint64 PSE36_ADDRESS_MASK = 0x001fe000ULL;
private const uint PSE36_ADDRESS_SHIFT = 19;
public X86PageTables (GDB.Client gdb) {
Object (gdb: gdb);
}
public async Gee.List<RangeDetails> collect_ranges (Cancellable? cancellable) throws Error, IOError {
var result = new Gee.ArrayList<RangeDetails> ();
MMUParameters p = yield load_parameters (cancellable);
if (!p.paging_enabled) {
result.add (new RangeDetails (0, 0, 1ULL << 32, READ | WRITE | EXECUTE, MappingType.UNKNOWN));
return result;
}
yield begin_access (cancellable);
GLib.Error? failure = null;
try {
yield collect_ranges_in_table (p.root_table, 0, 0, READ | WRITE | EXECUTE, p, result, cancellable);
} catch (GLib.Error e) {
failure = e;
}
yield end_access (cancellable);
throw_if_failed (failure);
return result;
}
private async void collect_ranges_in_table (uint64 table_pa, uint level, uint64 upper_bits,
Gum.PageProtection inherited, MMUParameters p, Gee.List<RangeDetails> ranges,
Cancellable? cancellable) throws Error, IOError {
Level l = p.levels[level];
Buffer entries = yield read_buffer (table_pa, l.num_entries * p.entry_size, cancellable);
for (uint i = 0; i != l.num_entries; i++) {
uint64 entry = read_entry (entries, i * p.entry_size, p);
if ((entry & PRESENT_BIT) == 0)
continue;
uint64 va = upper_bits | ((uint64) i << l.shift);
Gum.PageProtection prot = inherited & protection_from_entry (entry, level, p);
if (is_leaf_entry (entry, level, p)) {
ranges.add (new RangeDetails (canonicalize (va, p), leaf_address (entry, level, p),
1ULL << l.shift, prot, MappingType.UNKNOWN));
continue;
}
yield collect_ranges_in_table (table_address (entry, p), level + 1, va, prot, p, ranges,
cancellable);
}
}
public async uint64 translate (uint64 va, Cancellable? cancellable) throws Error, IOError {
MMUParameters p = yield load_parameters (cancellable);
if (!p.paging_enabled)
return va;
yield begin_access (cancellable);
uint64 pa = 0;
GLib.Error? failure = null;
try {
LeafEntry leaf = yield find_leaf_entry (va, p, cancellable);
uint64 page_mask = (1ULL << p.levels[leaf.level].shift) - 1;
pa = leaf_address (leaf.value, leaf.level, p) | (va & page_mask);
} catch (GLib.Error e) {
failure = e;
}
yield end_access (cancellable);
throw_if_failed (failure);
return pa;
}
public async void protect (uint64 va, size_t size, Gum.PageProtection prot, Cancellable? cancellable)
throws Error, IOError {
MMUParameters p = yield load_parameters (cancellable);
if (!p.paging_enabled)
throw new Error.INVALID_OPERATION ("Unable to change protection while paging is disabled");
uint64 start_va = page_start (va, PAGE_SIZE);
uint64 end_va = round_address_up (va + size, PAGE_SIZE);
yield begin_access (cancellable);
GLib.Error? failure = null;
try {
var relaxed_slots = new Gee.HashSet<uint64?> (Numeric.uint64_hash, Numeric.uint64_equal);
Level leaf = p.levels[p.leaf_level];
uint64 page_va = start_va;
while (page_va != end_va) {
uint64 table_pa = yield find_leaf_table (page_va, p, cancellable);
uint index = (uint) ((page_va >> leaf.shift) & (leaf.num_entries - 1));
uint64 remaining_here = (uint64) (leaf.num_entries - index) * PAGE_SIZE;
uint64 chunk_end = uint64.min (end_va, page_va + remaining_here);
uint num_pages = (uint) ((chunk_end - page_va) / PAGE_SIZE);
yield relax_parent_entries (page_va, prot, p, relaxed_slots, cancellable);
uint64 first_slot = table_pa + ((uint64) index * p.entry_size);
Buffer current = yield read_buffer (first_slot, num_pages * p.entry_size,
cancellable);
var builder = gdb.make_buffer_builder ();
bool changed = false;
for (uint i = 0; i != num_pages; i++) {
uint64 entry = read_entry (current, i * p.entry_size, p);
uint64 updated = apply_protection_bits (entry, prot, p);
changed = changed || updated != entry;
if (p.entry_size == 8)
builder.append_uint64 (updated);
else
builder.append_uint32 ((uint32) updated);
}
if (changed)
yield write_buffer (first_slot, builder.build (), cancellable);
page_va = chunk_end;
}
} catch (GLib.Error e) {
failure = e;
}
yield end_access (cancellable);
throw_if_failed (failure);
}
private async uint64 find_leaf_table (uint64 va, MMUParameters p, Cancellable? cancellable)
throws Error, IOError {
uint64 table_pa = p.root_table;
for (uint level = 0; level != p.leaf_level; level++) {
uint64 entry = yield read_entry_at (slot_address (va, table_pa, level, p), p, cancellable);
if ((entry & PRESENT_BIT) == 0) {
throw new Error.NOT_SUPPORTED ("Unable to resolve 0x%" + uint64.FORMAT_MODIFIER + "x",
va);
}
if (is_leaf_entry (entry, level, p)) {
throw new Error.NOT_SUPPORTED (
"Unable to change protection of the large page mapping at 0x%" +
uint64.FORMAT_MODIFIER + "x", va);
}
table_pa = table_address (entry, p);
}
return table_pa;
}
private async LeafEntry find_leaf_entry (uint64 va, MMUParameters p, Cancellable? cancellable)
throws Error, IOError {
uint64 table_pa = p.root_table;
for (uint level = 0; ; level++) {
uint64 slot_pa = slot_address (va, table_pa, level, p);
uint64 entry = yield read_entry_at (slot_pa, p, cancellable);
if ((entry & PRESENT_BIT) == 0) {
throw new Error.NOT_SUPPORTED ("Address 0x%" + uint64.FORMAT_MODIFIER + "x is not mapped",
va);
}
if (is_leaf_entry (entry, level, p))
return { slot_pa, entry, level };
table_pa = table_address (entry, p);
}
}
private struct LeafEntry {
public uint64 slot_pa;
public uint64 value;
public uint level;
}
private async void relax_parent_entries (uint64 va, Gum.PageProtection prot, MMUParameters p,
Gee.HashSet<uint64?> already_relaxed, Cancellable? cancellable) throws Error, IOError {
uint64 table_pa = p.root_table;
for (uint level = 0; level != p.leaf_level; level++) {
uint64 slot_pa = slot_address (va, table_pa, level, p);
uint64 entry = yield read_entry_at (slot_pa, p, cancellable);
if (!already_relaxed.contains (slot_pa) && p.levels[level].has_protection_bits) {
uint64 new_entry = entry;
if ((prot & Gum.PageProtection.WRITE) != 0)
new_entry |= WRITABLE_BIT;
if ((prot & Gum.PageProtection.EXECUTE) != 0 && p.nx_enabled)
new_entry &= ~NX_BIT;
if (new_entry != entry) {
yield write_entry (slot_pa, new_entry, p, cancellable);
entry = new_entry;
}
already_relaxed.add (slot_pa);
}
table_pa = table_address (entry, p);
}
}
public async Allocation map (Gee.List<uint64?> physical_addresses, Cancellable? cancellable)
throws Error, IOError {
MMUParameters p = yield load_parameters (cancellable);
if (!p.paging_enabled)
throw new Error.NOT_SUPPORTED ("Paging is disabled");
var run = new Run (physical_addresses.size);
yield begin_access (cancellable);
Allocation? allocation = null;
GLib.Error? failure = null;
try {
if (yield scan_for_run (run, p.root_table, 0, 0, p, cancellable))
allocation = yield occupy (run, physical_addresses, p, cancellable);
} catch (GLib.Error e) {
failure = e;
}
yield end_access (cancellable);
throw_if_failed (failure);
if (allocation == null)
throw new Error.NOT_SUPPORTED ("Unable to find a free run of page table entries");
return allocation;
}
// A run has to be contiguous in virtual address space but not in the tables
// describing it, so anything longer than a table holds is gathered a segment
// at a time and a gap anywhere above the leaves starts the search over.
private async bool scan_for_run (Run run, uint64 table_pa, uint level, uint64 upper_bits,
MMUParameters p, Cancellable? cancellable) throws Error, IOError {
Level l = p.levels[level];
bool at_leaf_level = level == p.leaf_level;
Buffer entries = yield read_buffer (table_pa, l.num_entries * p.entry_size, cancellable);
for (uint i = 0; i != l.num_entries; i++) {
uint64 entry = read_entry (entries, i * p.entry_size, p);
uint64 prefix = upper_bits | ((uint64) i << l.shift);
if (!at_leaf_level) {
bool usable = (entry & PRESENT_BIT) != 0
&& !is_leaf_entry (entry, level, p)
&& (protection_from_entry (entry, level, p) & MAPPING_PROTECTION)
== MAPPING_PROTECTION;
if (!usable) {
run.reset ();
continue;
}
if (yield scan_for_run (run, table_address (entry, p), level + 1, prefix, p,
cancellable))
return true;
continue;
}
uint64 va = canonicalize (prefix, p);
if ((entry & PRESENT_BIT) != 0 || va == 0) {
run.reset ();
continue;
}
run.take (va, table_pa + ((uint64) i * p.entry_size), p.entry_size);
if (run.is_complete ())
return true;
}
return false;
}
private async Allocation occupy (Run run, Gee.List<uint64?> physical_addresses, MMUParameters p,
Cancellable? cancellable) throws Error, IOError {
var displaced = new Gee.ArrayList<DisplacedEntries> ();
uint next_page = 0;
foreach (RunSegment segment in run.segments) {
var builder = gdb.make_buffer_builder ();
for (uint i = 0; i != segment.num_entries; i++) {
uint64 pa = physical_addresses[(int) (next_page + i)];
uint64 entry = apply_protection_bits (pa | PRESENT_BIT, MAPPING_PROTECTION, p);
if (p.entry_size == 8)
builder.append_uint64 (entry);
else
builder.append_uint32 ((uint32) entry);
}
Bytes new_entries = builder.build ();
Bytes old_entries = (yield read_buffer (segment.slot_pa, new_entries.get_size (),
cancellable)).bytes;
yield write_buffer (segment.slot_pa, new_entries, cancellable);
displaced.add (new DisplacedEntries (segment.slot_pa, old_entries));
next_page += segment.num_entries;
}
return new EntryAllocation (run.va, run.count * PAGE_SIZE, displaced, this);
}
private async void restore_entries (Gee.List<DisplacedEntries> displaced, Cancellable? cancellable)
throws Error, IOError {
yield begin_access (cancellable);
GLib.Error? failure = null;
try {
foreach (DisplacedEntries d in displaced)
yield write_buffer (d.slot_pa, d.entries, cancellable);
} catch (GLib.Error e) {
failure = e;
}
yield end_access (cancellable);
throw_if_failed (failure);
}
private class Run {
public uint needed;
public uint64 va;
public uint count;
public Gee.ArrayList<RunSegment> segments = new Gee.ArrayList<RunSegment> ();
private uint64 next_va;
private size_t entry_size;
public Run (uint needed) {
this.needed = needed;
}
public void reset () {
va = 0;
count = 0;
next_va = 0;
segments.clear ();
}
public void take (uint64 page_va, uint64 slot_pa, size_t entry_size) {
if (count != 0 && page_va != next_va)
reset ();
this.entry_size = entry_size;
if (count == 0) {
va = page_va;
segments.add (new RunSegment (slot_pa));
} else {
RunSegment last = segments[segments.size - 1];
if (slot_pa == last.slot_pa + (last.num_entries * entry_size))
last.num_entries++;
else
segments.add (new RunSegment (slot_pa));
}
count++;
next_va = page_va + PAGE_SIZE;
}
public bool is_complete () {
return count == needed;
}
}
private class RunSegment {
public uint64 slot_pa;
public uint num_entries = 1;
public RunSegment (uint64 slot_pa) {
this.slot_pa = slot_pa;
}
}
private class DisplacedEntries {
public uint64 slot_pa;
public Bytes entries;
public DisplacedEntries (uint64 slot_pa, Bytes entries) {
this.slot_pa = slot_pa;
this.entries = entries;
}
}
private class EntryAllocation : Object, Allocation {
public uint64 virtual_address {
get { return va; }
}
public size_t size {
get { return allocated_size; }
}
private uint64 va;
private size_t allocated_size;
private Gee.List<DisplacedEntries> displaced;
private X86PageTables page_tables;
public EntryAllocation (uint64 va, size_t allocated_size, Gee.List<DisplacedEntries> displaced,
X86PageTables page_tables) {
this.va = va;
this.allocated_size = allocated_size;
this.displaced = displaced;
this.page_tables = page_tables;
}
public async void deallocate (Cancellable? cancellable) throws Error, IOError {
yield page_tables.restore_entries (displaced, cancellable);
}
}
private static uint64 slot_address (uint64 va, uint64 table_pa, uint level, MMUParameters p) {
Level l = p.levels[level];
uint index = (uint) ((va >> l.shift) & (l.num_entries - 1));
return table_pa + ((uint64) index * p.entry_size);
}
private static bool is_leaf_entry (uint64 entry, uint level, MMUParameters p) {
if (level == p.leaf_level)
return true;
return p.levels[level].may_hold_large_page && (entry & LARGE_PAGE_BIT) != 0;
}
private static uint64 table_address (uint64 entry, MMUParameters p) {
return entry & ((p.entry_size == 8) ? EXTENDED_ADDRESS_MASK : LEGACY_ADDRESS_MASK);
}
private static uint64 leaf_address (uint64 entry, uint level, MMUParameters p) {
if (p.entry_size == 8)
return entry & EXTENDED_ADDRESS_MASK & ~((1ULL << p.levels[level].shift) - 1);
if (level == p.leaf_level)
return entry & LEGACY_ADDRESS_MASK;
uint64 low_bits = entry & LEGACY_LARGE_PAGE_ADDRESS_MASK;
uint64 pse36_high_bits = (entry & PSE36_ADDRESS_MASK) << PSE36_ADDRESS_SHIFT;
return low_bits | pse36_high_bits;
}
private static uint64 canonicalize (uint64 va, MMUParameters p) {
if (p.canonical_bits == 0)
return va;
uint64 sign = 1ULL << (p.canonical_bits - 1);
return (va ^ sign) - sign;
}
// What a single entry contributes is a ceiling, not the effective right: the bits are ANDed
// across levels.
private static Gum.PageProtection protection_from_entry (uint64 entry, uint level, MMUParameters p) {
if (!p.levels[level].has_protection_bits)
return READ | WRITE | EXECUTE;
Gum.PageProtection prot = READ;
if ((entry & WRITABLE_BIT) != 0)
prot |= WRITE;
if (!p.nx_enabled || (entry & NX_BIT) == 0)
prot |= EXECUTE;
return prot;
}
private static uint64 apply_protection_bits (uint64 entry, Gum.PageProtection prot, MMUParameters p) {
uint64 result = entry;
if ((prot & Gum.PageProtection.WRITE) != 0)
result |= WRITABLE_BIT;
else
result &= ~WRITABLE_BIT;
if (p.nx_enabled) {
if ((prot & Gum.PageProtection.EXECUTE) != 0)
result &= ~NX_BIT;
else
result |= NX_BIT;
}
return result;
}
private async uint64 read_entry_at (uint64 slot_pa, MMUParameters p, Cancellable? cancellable)
throws Error, IOError {
Buffer buf = yield read_buffer (slot_pa, p.entry_size, cancellable);
return read_entry (buf, 0, p);
}
private async void write_entry (uint64 slot_pa, uint64 entry, MMUParameters p, Cancellable? cancellable)
throws Error, IOError {
Buffer buf = gdb.make_buffer (new Bytes (new uint8[p.entry_size]));
if (p.entry_size == 8)
buf.write_uint64 (0, entry);
else
buf.write_uint32 (0, (uint32) entry);
yield write_buffer (slot_pa, buf.bytes, cancellable);
}
private static uint64 read_entry (Buffer entries, size_t offset, MMUParameters p) {
return (p.entry_size == 8) ? entries.read_uint64 (offset) : entries.read_uint32 (offset);
}
private async void begin_access (Cancellable? cancellable) throws Error, IOError {
if (physical_memory == null)
yield set_addressing_mode (gdb, PHYSICAL, cancellable);
}
private async void end_access (Cancellable? cancellable) throws Error, IOError {
if (physical_memory == null)
yield set_addressing_mode (gdb, VIRTUAL, cancellable);
}
private async Buffer read_buffer (uint64 pa, size_t size, Cancellable? cancellable) throws Error, IOError {
if (physical_memory != null && physical_memory.contains (pa))
return gdb.make_buffer (new Bytes (physical_memory.read (pa, size)));
return yield gdb.read_buffer (pa, size, cancellable);
}
private async void write_buffer (uint64 pa, Bytes data, Cancellable? cancellable) throws Error, IOError {
if (physical_memory != null && physical_memory.contains (pa)) {
physical_memory.write (pa, data.get_data ());
return;
}
yield gdb.write_byte_array (pa, data, cancellable);
}
private async MMUParameters load_parameters (Cancellable? cancellable) throws Error, IOError {
if (cached_parameters == null)
cached_parameters = yield MMUParameters.load (gdb, cancellable);
return cached_parameters;
}
private class MMUParameters {
public bool paging_enabled;
public bool nx_enabled;
public uint canonical_bits;
public size_t entry_size;
public uint64 root_table;
public Level[] levels;
public uint leaf_level {
get { return levels.length - 1; }
}
private const uint64 CR0_PG = 1ULL << 31;
private const uint64 CR3_PAE_ROOT_MASK = ~0x1fULL;
private const uint64 CR4_PSE = 1ULL << 4;
private const uint64 CR4_PAE = 1ULL << 5;
private const uint64 CR4_LA57 = 1ULL << 12;
private const uint64 EFER_LMA = 1ULL << 10;
private const uint64 EFER_NXE = 1ULL << 11;
public static async MMUParameters load (GDB.Client gdb, Cancellable? cancellable) throws Error, IOError {
ControlRegisters regs = yield ControlRegisters.read (gdb, cancellable);
var parameters = new MMUParameters ();
parameters.paging_enabled = (regs.cr0 & CR0_PG) != 0;
bool pae = (regs.cr4 & CR4_PAE) != 0;
parameters.nx_enabled = pae && (regs.efer & EFER_NXE) != 0;
if ((regs.efer & EFER_LMA) != 0)
parameters.adopt_long_mode ((regs.cr4 & CR4_LA57) != 0, regs.cr3);
else if (pae)
parameters.adopt_pae (regs.cr3);
else
parameters.adopt_legacy ((regs.cr4 & CR4_PSE) != 0, regs.cr3);
return parameters;
}
private void adopt_long_mode (bool la57, uint64 cr3) {
var levels = new Gee.ArrayList<Level> ();
if (la57)
levels.add (new Level (48, 512, false, true));
levels.add (new Level (39, 512, false, true));
levels.add (new Level (30, 512, true, true));
levels.add (new Level (21, 512, true, true));
levels.add (new Level (12, 512, false, true));
this.levels = levels.to_array ();
canonical_bits = la57 ? 57 : 48;
entry_size = 8;
root_table = cr3 & EXTENDED_ADDRESS_MASK;
}
// A page-directory-pointer entry has reserved bits where the others keep RW and NX.
private void adopt_pae (uint64 cr3) {
levels = {
new Level (30, 4, false, false),
new Level (21, 512, true, true),
new Level (12, 512, false, true)
};
entry_size = 8;
root_table = cr3 & CR3_PAE_ROOT_MASK;
}
private void adopt_legacy (bool pse_enabled, uint64 cr3) {
levels = {
new Level (22, 1024, pse_enabled, true),
new Level (12, 1024, false, true)
};
entry_size = 4;
root_table = cr3 & LEGACY_ADDRESS_MASK;
}
}
private class Level {
public uint shift;
public uint num_entries;
public bool may_hold_large_page;
public bool has_protection_bits;
public Level (uint shift, uint num_entries, bool may_hold_large_page, bool has_protection_bits) {
this.shift = shift;
this.num_entries = num_entries;
this.may_hold_large_page = may_hold_large_page;
this.has_protection_bits = has_protection_bits;
}
}
private class ControlRegisters {
public uint64 cr0;
public uint64 cr3;
public uint64 cr4;
public uint64 efer;
public static async ControlRegisters read (GDB.Client gdb, Cancellable? cancellable) throws Error, IOError {
var regs = new ControlRegisters ();
if (yield regs.try_read_from_registers (gdb, cancellable))
return regs;
yield regs.read_from_monitor (gdb, cancellable);
return regs;
}
private async bool try_read_from_registers (GDB.Client gdb, Cancellable? cancellable) throws Error, IOError {
GDB.Exception? exception = gdb.exception;
if (exception == null)
throw new Error.INVALID_OPERATION ("Unable to query in current state");
GDB.Thread thread = exception.thread;
try {
cr0 = yield thread.read_register ("cr0", cancellable);
cr3 = yield thread.read_register ("cr3", cancellable);
cr4 = yield thread.read_register ("cr4", cancellable);
} catch (Error e) {
return false;
}
try {
efer = yield thread.read_register ("efer", cancellable);
} catch (Error e) {
}
return true;
}
private async void read_from_monitor (GDB.Client gdb, Cancellable? cancellable) throws Error, IOError {
string dump = yield gdb.run_remote_command ("info registers", cancellable);
bool found_cr3 = false;
foreach (string token in dump.split_set (" \t\r\n")) {
string[] parts = token.split ("=");
if (parts.length != 2)
continue;
uint64 val = uint64.parse (parts[1], 16);
switch (parts[0]) {
case "CR0":
cr0 = val;
break;
case "CR3":
cr3 = val;
found_cr3 = true;
break;
case "CR4":
cr4 = val;
break;
case "EFER":
efer = val;
break;
default:
break;
}
}
if (!found_cr3)
throw new Error.NOT_SUPPORTED ("Unable to determine the target's control registers");
}
}
}
}
+39
View File
@@ -381,6 +381,45 @@ namespace Frida {
return task.execute (cancellable);
}
public async Device add_barebone_device (Barebone.Config config, Cancellable? cancellable = null)
throws Error, IOError {
#if HAVE_BAREBONE_BACKEND
check_open ();
var barebone_device = yield get_device ((device) => {
return device.provider is BareboneHostSessionProvider;
}, 0, cancellable);
var raw_options = new HostSessionOptions ();
raw_options.map["config"] = config;
var device = new Device (this, barebone_device.provider, barebone_device.provider.id,
barebone_device.provider.name, raw_options);
devices.add (device);
added (device);
changed ();
return device;
#else
throw new Error.NOT_SUPPORTED ("Barebone backend not available");
#endif
}
public Device add_barebone_device_sync (Barebone.Config config, Cancellable? cancellable = null)
throws Error, IOError {
var task = create<AddBareboneDeviceTask> ();
task.config = config;
return task.execute (cancellable);
}
private class AddBareboneDeviceTask : ManagerTask<Device> {
public Barebone.Config config;
protected override async Device perform_operation () throws Error, IOError {
return yield parent.add_barebone_device (config, cancellable);
}
}
private class AddRemoteDeviceTask : ManagerTask<Device> {
public string address;
public RemoteDeviceOptions? options;
+53 -11
View File
@@ -99,6 +99,8 @@ namespace Frida.GDB {
protected const char NOTIFICATION_TYPE_STOP_WITH_PROPERTIES = 'T';
protected const char NOTIFICATION_TYPE_OUTPUT = 'O';
private bool binary_writes_supported = true;
private const char STOP_CHARACTER = 0x03;
private const string ACK_NOTIFICATION = "+";
private const string NACK_NOTIFICATION = "-";
@@ -526,7 +528,8 @@ namespace Frida.GDB {
public async void write_byte_array (uint64 address, Bytes bytes, Cancellable? cancellable = null)
throws Error, IOError {
size_t max_bytes_per_packet = (max_packet_size - 1 - 16 - 1 - 8 - 1 - Packet.OVERHEAD) / 2;
size_t header_size = 1 + 16 + 1 + 8 + 1 + Packet.OVERHEAD;
size_t budget = max_packet_size - header_size;
var data = bytes.get_data ();
size_t offset = 0;
@@ -535,22 +538,47 @@ namespace Frida.GDB {
var builder = make_packet_builder_sized (32 + (remaining * 2));
while (remaining != 0) {
uint64 slice_address = address + offset;
size_t slice_size = size_t.min (remaining, max_bytes_per_packet);
size_t slice_size, cost;
if (binary_writes_supported) {
slice_size = 0;
cost = 0;
while (slice_size != remaining) {
size_t byte_cost = needs_escaping (data[offset + slice_size]) ? 2 : 1;
if (cost + byte_cost > budget)
break;
cost += byte_cost;
slice_size++;
}
} else {
slice_size = size_t.min (remaining, budget / 2);
}
builder
.append_c ('M')
.append_address (slice_address)
.append_c (binary_writes_supported ? 'X' : 'M')
.append_address (address + offset)
.append_c (',')
.append_size (slice_size)
.append_c (':');
for (size_t i = 0; i != slice_size; i++) {
uint8 byte = data[offset + i];
builder.append_hexbyte (byte);
if (binary_writes_supported)
builder.append_c ((char) byte);
else
builder.append_hexbyte (byte);
}
yield execute (builder.build (), cancellable);
if (binary_writes_supported) {
try {
yield execute (builder.build (), cancellable);
} catch (Error e) {
binary_writes_supported = false;
builder.reset ();
continue;
}
} else {
yield execute (builder.build (), cancellable);
}
builder.reset ();
@@ -559,6 +587,18 @@ namespace Frida.GDB {
}
}
private static bool needs_escaping (uint8 byte) {
switch ((char) byte) {
case PACKET_CHARACTER:
case CHECKSUM_CHARACTER:
case ESCAPE_CHARACTER:
case REPEAT_CHARACTER:
return true;
default:
return false;
}
}
public async uint64 read_pointer (uint64 address, Cancellable? cancellable = null) throws Error, IOError {
var buffer = yield read_buffer (address, pointer_size, cancellable);
return buffer.read_pointer (0);
@@ -1738,7 +1778,7 @@ namespace Frida.GDB {
if (!client.bulk_registers) {
foreach (var e in regs.entries) {
var reg = client.get_register_by_name (e.key);
if (reg.writable && reg.bitsize == 64 && e.value.is_of_type (VariantType.UINT64))
if (reg.writable && e.value.is_of_type (VariantType.UINT64))
yield write_register (e.key, e.value.get_uint64 (), cancellable);
}
return;
@@ -1756,9 +1796,11 @@ namespace Frida.GDB {
throw new Error.INVALID_ARGUMENT ("Missing %s", reg.name);
if (val.is_of_type (VariantType.UINT64)) {
builder.append (Protocol.unparse_integer_value (val.get_uint64 (), sizeof (uint64), byte_order));
builder.append (Protocol.unparse_integer_value (val.get_uint64 (), reg.bitsize / 8,
byte_order));
} else if (val.is_of_type (VariantType.UINT32)) {
builder.append (Protocol.unparse_integer_value (val.get_uint64 (), sizeof (uint32), byte_order));
builder.append (Protocol.unparse_integer_value (val.get_uint32 (), reg.bitsize / 8,
byte_order));
} else {
builder.append (Protocol.unparse_hex_bytes (val.get_data_as_bytes ()));
}
@@ -1800,7 +1842,7 @@ namespace Frida.GDB {
.append_c ('P')
.append_register_id (reg.id)
.append_c ('=')
.append (Protocol.unparse_integer_value (val, client.pointer_size, client.byte_order));
.append (Protocol.unparse_integer_value (val, reg.bitsize / 8, client.byte_order));
if ("thread-suffix" in client.features) {
command
.append (";thread:")
+5
View File
@@ -324,6 +324,11 @@ namespace Frida {
if (pss != null)
yield pss.interrupt (cancellable);
if (KernelInjectSession.is_available ()) {
var session = yield KernelInjectSession.open (pid, cancellable);
return yield session.inject (spec, cancellable);
}
try {
var session = yield InjectSession.open (pid, cancellable);
RemoteAgent agent = yield session.inject (spec, cancellable);
+420
View File
@@ -0,0 +1,420 @@
namespace Frida {
// Bridges to the Frida kernel module over its magic-prctl control channel. The module
// exposes ptrace-free memory and thread primitives against an arbitrary pid, which the
// kernel-assisted injector drives instead of ptrace or /proc/<pid>/mem.
internal sealed class KernelSession : Object {
public uint pid {
get;
construct;
}
private const long CONTROL_MAGIC = 0x46524944;
private const uint64 CONTROL_TOKEN = 0x1d5f9e6b2c7a4038;
private enum Op {
PING = 32,
ALLOC = 64,
FREE,
WRITE,
READ,
SPAWN,
CLOAK_THREAD,
CLOAK_RANGE,
}
private struct AllocArgs {
uint64 size;
uint32 pid;
uint32 prot;
}
private struct FreeArgs {
uint64 address;
uint64 size;
uint32 pid;
}
private struct XferArgs {
uint64 address;
uint64 buffer;
uint64 size;
uint32 pid;
}
private struct SpawnArgs {
uint64 entry;
uint64 stack;
uint64 arg;
uint64 tls;
uint32 pid;
}
private struct CloakThreadArgs {
uint32 tgid;
uint32 tid;
}
private struct CloakRangeArgs {
uint64 start;
uint64 size;
uint32 tgid;
}
private KernelSession (uint pid) {
Object (pid: pid);
}
public static bool is_available () {
return control (Op.PING, 0, 0) == CONTROL_MAGIC;
}
public static KernelSession open (uint pid) throws Error {
if (!is_available ())
throw new Error.NOT_SUPPORTED ("Frida kernel module is not loaded");
return new KernelSession (pid);
}
public uint64 alloc (uint64 size, int prot) throws Error {
var args = AllocArgs () {
size = size,
pid = pid,
prot = prot,
};
long result = control (Op.ALLOC, (ulong) (uintptr) (&args), 0);
if (result <= 0)
throw new Error.NOT_SUPPORTED ("Unable to allocate %s bytes in the target", size.to_string ());
return (uint64) result;
}
public void free (uint64 address, uint64 size) throws Error {
var args = FreeArgs () {
address = address,
size = size,
pid = pid,
};
control (Op.FREE, (ulong) (uintptr) (&args), 0);
}
public void write_memory (uint64 address, uint8[] data) throws Error {
var args = XferArgs () {
address = address,
buffer = (uint64) (uintptr) (uint8 *) data,
size = data.length,
pid = pid,
};
long result = control (Op.WRITE, (ulong) (uintptr) (&args), 0);
if (result < 0 || (uint64) result != data.length)
throw new Error.NOT_SUPPORTED ("Unable to write %d bytes at 0x%" + uint64.FORMAT_MODIFIER + "x",
data.length, address);
}
public uint8[] read_memory (uint64 address, size_t size) throws Error {
var result = new uint8[size];
var args = XferArgs () {
address = address,
buffer = (uint64) (uintptr) (uint8 *) result,
size = size,
pid = pid,
};
long n = control (Op.READ, (ulong) (uintptr) (&args), 0);
if (n < 0 || (size_t) n != size)
throw new Error.NOT_SUPPORTED ("Unable to read %s bytes at 0x%" + uint64.FORMAT_MODIFIER + "x",
size.to_string (), address);
return result;
}
public uint64 read_pointer (uint64 address) throws Error {
uint8[] raw = read_memory (address, sizeof (uint64));
return *((uint64 *) raw);
}
// entry/stack/arg/tls seed the new thread's PC/SP/x0 and TPIDR_EL0. The thread joins
// the target's thread group; tls must point at a private block the caller prepared.
public uint spawn_thread (uint64 entry, uint64 stack, uint64 arg, uint64 tls) throws Error {
var args = SpawnArgs () {
entry = entry,
stack = stack,
arg = arg,
tls = tls,
pid = pid,
};
long result = control (Op.SPAWN, (ulong) (uintptr) (&args), 0);
if (result <= 0)
throw new Error.NOT_SUPPORTED ("Unable to spawn a thread in the target");
return (uint) result;
}
// Hide the loader's own footprint (which Gum doesn't track and so wouldn't self-cloak):
// the bootstrap thread plus the region/stack/tls we mapped for it.
public void cloak_thread (uint tid) {
var args = CloakThreadArgs () { tgid = pid, tid = tid };
control (Op.CLOAK_THREAD, (ulong) (uintptr) (&args), 0);
}
public void cloak_range (uint64 address, uint64 size) {
var args = CloakRangeArgs () { start = address, size = size, tgid = pid };
control (Op.CLOAK_RANGE, (ulong) (uintptr) (&args), 0);
}
// prctl (FRIDA_CONTROL_MAGIC, op, arg2, arg3, FRIDA_CONTROL_TOKEN). Absent the token
// the module falls through to the real prctl, so probing the magic looks like a stock
// kernel. We bind syscall() with a 64-bit return since the libc prctl() wrapper — and
// the vapi's syscall() — would truncate an allocated address.
private static long control (Op op, ulong arg2, ulong arg3) {
return syscall ((long) LinuxSyscall.PRCTL, CONTROL_MAGIC, (ulong) op, arg2, arg3, CONTROL_TOKEN);
}
[CCode (cname = "syscall", cheader_filename = "unistd.h,sys/syscall.h")]
private extern static long syscall (long number, ...);
}
// Injects frida-agent.so using the kernel module's ptrace-free primitives: stage the loader +
// context in a fresh region, then spawn a bootstrap thread straight into it. The loader hands
// off to a real pthread it creates, so the only libc call on the kernel-spawned thread is
// pthread_create — which we give a private, isolated bionic TLS so it never touches the
// target's live per-thread state.
internal sealed class KernelInjectSession : Object {
public uint pid {
get;
construct;
}
private KernelSession kernel;
private const int PROT_RW = 3;
private const int PROT_RWX = 7;
private const uint64 STACK_SIZE = 128 * 1024;
private const uint64 TLS_SIZE = 8 * 1024;
private const uint SETTLE_TIMEOUT_SECONDS = 10;
private struct RegionLayout {
public size_t entry_offset;
public size_t libc_offset;
public size_t entrypoint_offset;
public size_t data_offset;
public size_t fallback_offset;
public size_t context_offset;
public size_t page_size;
public size_t total;
public static RegionLayout compute (InjectSpec spec, string data, string fallback_address) {
size_t loader_size = Frida.Data.HelperBackend.get_loader_bin_blob ().data.length;
size_t entrypoint_size = make_cstring (spec.entrypoint).length;
size_t data_size = make_cstring (data).length;
size_t fallback_size = make_cstring (fallback_address).length;
size_t page_size = Gum.query_page_size ();
var l = RegionLayout ();
l.entry_offset = (size_t) align_up (loader_size, 16);
size_t o = (size_t) align_up (l.entry_offset + 256, 16);
l.libc_offset = o; o += sizeof (HelperLibcApi);
l.entrypoint_offset = o; o += entrypoint_size;
l.data_offset = o; o += data_size;
l.fallback_offset = o; o += fallback_size;
l.context_offset = (size_t) align_up (o, page_size);
l.page_size = page_size;
l.total = l.context_offset + page_size;
return l;
}
}
private KernelInjectSession (uint pid, KernelSession kernel) {
Object (pid: pid);
this.kernel = kernel;
}
public static bool is_available () {
return KernelSession.is_available ();
}
public static async KernelInjectSession open (uint pid, Cancellable? cancellable) throws Error, IOError {
return new KernelInjectSession (pid, KernelSession.open (pid));
}
public async RemoteAgent inject (InjectSpec spec, Cancellable? cancellable) throws Error, IOError {
var maps = ProcMapsSnapshot.from_pid (pid);
var libc = RemoteLibcApi.resolve (maps);
string data = spec.data;
string fallback_address = make_fallback_address ();
var region = RegionLayout.compute (spec, data, fallback_address);
Future<RemoteAgent> future_agent = establish_connection (spec, fallback_address, cancellable);
uint64 region_base = kernel.alloc (region.total, PROT_RWX);
uint64 stack_base = kernel.alloc (STACK_SIZE, PROT_RW);
uint64 tls_base;
uint64 tls = synthesize_tls (out tls_base);
write_region (region_base, region, spec, data, fallback_address, libc);
kernel.cloak_range (region_base, region.total);
kernel.cloak_range (stack_base, STACK_SIZE);
kernel.cloak_range (tls_base, TLS_SIZE);
uint64 entry = region_base + region.entry_offset;
uint64 context = region_base + region.context_offset;
uint64 stack_top = (stack_base + STACK_SIZE - 16) & ~((uint64) 15);
uint tid = kernel.spawn_thread (entry, stack_top, context, tls);
kernel.cloak_thread (tid);
return yield await_agent (future_agent, cancellable);
}
private void write_region (uint64 region_base, RegionLayout l, InjectSpec spec, string data,
string fallback_address, RemoteLibcApi libc) throws Error {
kernel.write_memory (region_base, Frida.Data.HelperBackend.get_loader_bin_blob ().data);
kernel.write_memory (region_base + l.entry_offset, build_bootstrap_code (region_base, l));
var ctx = HelperLoaderContext ();
ctx.ctrlfds[0] = -1;
ctx.ctrlfds[1] = -1;
ctx.agent_entrypoint = (string *) (region_base + l.entrypoint_offset);
ctx.agent_data = (string *) (region_base + l.data_offset);
ctx.fallback_address = (string *) (region_base + l.fallback_offset);
ctx.libc = (HelperLibcApi *) (region_base + l.libc_offset);
kernel.write_memory (region_base + l.context_offset, (uint8[]) &ctx);
kernel.write_memory (region_base + l.libc_offset, (uint8[]) &libc.table);
kernel.write_memory (region_base + l.entrypoint_offset, make_cstring (spec.entrypoint));
kernel.write_memory (region_base + l.data_offset, make_cstring (data));
kernel.write_memory (region_base + l.fallback_offset, make_cstring (fallback_address));
}
private uint8[] build_bootstrap_code (uint64 region_base, RegionLayout l) throws Error {
#if ARM64
var buffer = new uint8[64];
var writer = new Gum.Arm64Writer ((void *) buffer);
writer.pc = region_base + l.entry_offset;
// x0 already holds the loader context (passed as the spawn argument). Call frida_load,
// then terminate this bootstrap thread; the agent runs on the pthread the loader spawned.
if (!writer.put_bl_imm ((Gum.Address) region_base))
throw new Error.NOT_SUPPORTED ("Loader is out of branch range of the bootstrap");
writer.put_instruction ((uint32) 0xd2800ba8); // movz x8, #93 (__NR_exit)
writer.put_instruction ((uint32) 0xd4000001); // svc #0
writer.flush ();
return buffer[:writer.offset ()];
#else
throw new Error.NOT_SUPPORTED ("Kernel-assisted injection is only implemented for arm64");
#endif
}
// Give the bootstrap thread a private, isolated bionic TLS so pthread_create's stack-guard
// check and errno access land in our own block, never the target's live per-thread state.
// Slot indices per bionic tls_defines.h (arm64): BIONIC_TLS=-1, THREAD_ID=1, STACK_GUARD=5,
// each at tp + index * 8. The guard check is self-consistent within our block, so any stable
// value passes and we never read the target's real canary.
private uint64 synthesize_tls (out uint64 block_base) throws Error {
uint64 block = kernel.alloc (TLS_SIZE, PROT_RW);
block_base = block;
uint64 tp = block + (TLS_SIZE / 2);
uint64 pthread_scratch = block + (TLS_SIZE / 4);
uint64 bionic_scratch = block + (3 * TLS_SIZE / 4);
write_slot (tp, -1, bionic_scratch);
write_slot (tp, 1, pthread_scratch);
write_slot (tp, 5, 0x1122334455667788);
return tp;
}
private void write_slot (uint64 tp, int index, uint64 value) throws Error {
uint8[] raw = new uint8[8];
*((uint64 *) raw) = value;
kernel.write_memory ((uint64) ((int64) tp + (int64) index * 8), raw);
}
private Future<RemoteAgent> establish_connection (InjectSpec spec, string fallback_address,
Cancellable? cancellable) throws Error {
var promise = new Promise<RemoteAgent> ();
var server_address = new UnixSocketAddress.with_type (fallback_address, -1, UnixSocketAddressType.ABSTRACT);
Socket server_socket;
try {
var socket = new Socket (SocketFamily.UNIX, SocketType.STREAM, SocketProtocol.DEFAULT);
socket.bind (server_address, true);
socket.listen ();
server_socket = socket;
} catch (GLib.Error e) {
throw new Error.TRANSPORT ("%s", e.message);
}
accept_agent.begin (server_socket, spec, promise, cancellable);
return promise.future;
}
private async void accept_agent (Socket server_socket, InjectSpec spec, Promise<RemoteAgent> promise,
Cancellable? cancellable) {
var listener = new SocketListener ();
try {
listener.add_socket (server_socket, null);
var connection = (UnixConnection) yield listener.accept_async (cancellable);
var agent = yield RemoteAgent.start (FROM_SCRATCH, spec, pid, new BootstrapResult (), connection, null,
cancellable);
promise.resolve (agent);
} catch (Error e) {
promise.reject (e);
} catch (IOError e) {
promise.reject (e);
} catch (GLib.Error e) {
promise.reject (new Error.TRANSPORT ("%s", e.message));
} finally {
listener.close ();
}
}
private async RemoteAgent await_agent (Future<RemoteAgent> future_agent, Cancellable? cancellable)
throws Error, IOError {
var settle_cancellable = new Cancellable ();
var main_context = MainContext.get_thread_default ();
var timeout_source = new TimeoutSource.seconds (SETTLE_TIMEOUT_SECONDS);
timeout_source.set_callback (() => {
settle_cancellable.cancel ();
return Source.REMOVE;
});
timeout_source.attach (main_context);
var cancel_source = new CancellableSource (cancellable);
cancel_source.set_callback (() => {
settle_cancellable.cancel ();
return Source.REMOVE;
});
cancel_source.attach (main_context);
RemoteAgent? agent = null;
try {
agent = yield future_agent.wait_async (settle_cancellable);
} catch (IOError e) {
cancellable.set_error_if_cancelled ();
throw new Error.PROCESS_NOT_RESPONDING ("Timed out waiting for the injected agent to connect back");
} finally {
cancel_source.destroy ();
timeout_source.destroy ();
}
agent.ack ();
return agent;
}
private static string make_fallback_address () {
return "/frida-" + Uuid.string_random ();
}
private static uint8[] make_cstring (string str) {
unowned uint8[] bytes = str.data;
var result = new uint8[bytes.length + 1];
Memory.copy (result, bytes, bytes.length);
return result;
}
private static uint64 align_up (uint64 value, size_t alignment) {
return (value + (alignment - 1)) & ~((uint64) alignment - 1);
}
}
}
+1
View File
@@ -359,6 +359,7 @@ if have_local_backend
'linux' / 'symbol-resolver.vala',
'linux' / 'proc-maps.vala',
'linux' / 'proc-mem-injector.vala',
'linux' / 'kernel-injector.vala',
]
if host_abi == 'armhf'
host_lowlevel_abi = 'arm'
+8 -1
View File
@@ -8,10 +8,17 @@ frida_access_get_sddl_string_for_temp_directory (void)
{
#define DACL_START_INHERIT L"D:AI"
#define DACL_ACE_APPCONTAINER_RWX_WITH_CHILD_INHERIT L"(A;OICI;GRGWGX;;;AC)"
#define DACL_ACE_SYSTEM_RWX_WITH_CHILD_INHERIT L"(A;OICI;GA;;;SY)"
#define DACL_ACE_ADMINS_RWX_WITH_CHILD_INHERIT L"(A;OICI;GA;;;BA)"
#define DACL_ACE_LOCAL_SERVICE_RX_WITH_CHILD_INHERIT L"(A;OICI;GRGX;;;LS)"
if (frida_access_is_windows_8_or_greater ())
{
return DACL_START_INHERIT DACL_ACE_APPCONTAINER_RWX_WITH_CHILD_INHERIT;
return DACL_START_INHERIT
DACL_ACE_APPCONTAINER_RWX_WITH_CHILD_INHERIT
DACL_ACE_SYSTEM_RWX_WITH_CHILD_INHERIT
DACL_ACE_ADMINS_RWX_WITH_CHILD_INHERIT
DACL_ACE_LOCAL_SERVICE_RX_WITH_CHILD_INHERIT;
}
else
{
+1 -1
View File
@@ -1,6 +1,6 @@
[wrap-git]
url = https://github.com/frida/frida-gum.git
revision = 17.16.4
revision = 04f4c1499cf04e9c1004cbb02b8ff874a8dfc376
depth = 1
[provide]
+1 -1
View File
@@ -1,6 +1,6 @@
[wrap-git]
url = https://github.com/frida/glib.git
revision = 92d65736b198c2cf131f691dd83db0b25f7b02d2
revision = 9ba86eaeb758aed3a6411a2d508be8b257c36f5f
depth = 1
[provide]
+1 -1
View File
@@ -1,6 +1,6 @@
[wrap-git]
url = https://github.com/frida/lwip.git
revision = a9f759897f26347c1a41d8d2a6d3a89aba9ce57b
revision = 0217c5d9e5193cea0a02151305e8bc3a9b5e579f
depth = 1
[provide]
+5
View File
@@ -41,6 +41,10 @@ if have_local_backend
]
endif
if have_barebone_backend
test_sources += 'test-barebone.vala'
endif
if have_compiler_backend
test_sources += 'test-compiler.vala'
endif
@@ -54,6 +58,7 @@ else
endif
frida_tests = static_library('frida-tests', test_sources,
c_args: ['-DFRIDA_TESTS_SRCDIR="' + meson.current_source_dir() + '"'],
vala_args: [
'--pkg=gmodule-2.0',
test_vala_args,
+4
View File
@@ -82,6 +82,10 @@ namespace Frida.Test {
#endif
Frida.HostSessionTest.add_tests ();
#if HAVE_BAREBONE_BACKEND
Frida.BareboneTest.add_tests ();
#endif
#if HAVE_COMPILER_BACKEND && !QNX
Frida.CompilerTest.add_tests ();
#endif
File diff suppressed because it is too large. Load diff
+416 -4
View File
@@ -1,11 +1,84 @@
#!/usr/bin/env python3
import pexpect
import argparse
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import sys
import tempfile
import time
from typing import NamedTuple
import urllib.request
def main():
if len(sys.argv) >= 2 and sys.argv[1].split("-", maxsplit=1)[0] in ("check", "boot"):
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command", required=True)
for arch in GUESTS:
subparsers.add_parser(f"check-{arch}",
help=f"verify that the {arch} guest can be booted, fetching it if needed")
boot = subparsers.add_parser(f"boot-{arch}",
help=f"boot a minimal {arch} guest with the GDB stub enabled")
boot.add_argument("--gdb-port", type=int, required=True)
boot.add_argument("--memory", type=int, default=256)
for command, help_text in [
("check-win95", "verify that the Windows 95 guest can be booted"),
("boot-win95", "boot a Windows 95 disk image with the GDB stub enabled"),
]:
win95 = subparsers.add_parser(command, help=help_text)
win95.add_argument("--image", type=Path, required=True)
win95.add_argument("--format", default="qcow2")
if command.startswith("boot"):
win95.add_argument("--gdb-port", type=int, required=True)
win95.add_argument("--memory", type=int, default=128)
win95.add_argument("--boot-seconds", type=int, default=120)
win95.add_argument("--qmp", type=Path)
win95.add_argument("--debugcon", type=Path)
subparsers.add_parser("check-kmod",
help="verify that a kmod guest can be booted, fetching what it needs")
boot_kmod = subparsers.add_parser("boot-kmod",
help="boot the running kernel in a guest and load a module into it")
boot_kmod.add_argument("--module", type=Path, required=True)
boot_kmod.add_argument("--kernel", type=Path)
boot_kmod.add_argument("--memory", type=int, default=512)
boot_kmod.add_argument("--cpus", type=int, default=4)
boot_kmod.add_argument("--socket", type=Path)
boot_kmod.add_argument("--ibt", action=argparse.BooleanOptionalAction, default=True)
args = parser.parse_args()
action, target = args.command.split("-", maxsplit=1)
if target == "kmod":
if action == "check":
check_kmod_guest()
else:
boot_kmod_guest(args)
elif target == "win95":
if action == "check":
check_win95_guest(args)
else:
boot_win95_guest(args)
elif action == "check":
check_guest(target)
else:
boot_guest(target, args)
return
arch = sys.argv[1]
args = sys.argv[2:] if len(sys.argv) >= 3 else []
run(arch, args)
def run(arch: str, args: [str]):
import pexpect
child = pexpect.spawn("arm_now", ["start", arch, "--sync"])
child.expect("buildroot login: ")
@@ -16,7 +89,346 @@ def run(arch: str, args: [str]):
child.interact()
def check_guest(arch: str):
"""
Do everything booting needs except the boot itself, so that a caller can tell a missing
prerequisite apart from a guest that failed to come up — and pay for the download here
rather than inside its connect timeout.
"""
guest = GUESTS[arch]
require_qemu(guest)
fetch_kernel(guest)
print("ok")
def boot_guest(arch: str, args):
"""
Boot a stock 32-bit Linux kernel far enough to have its page tables up, and hand it to
QEMU's GDB stub. There is no root filesystem, so the kernel panics once it goes looking
for one; panic=0 then parks it forever with paging still enabled, which is exactly the
quiescent state we want to inspect.
QEMU discards every GDB packet except Ctrl-C while the guest is running, so the caller
cannot attach and then wait for the boot — it has to wait first. Watch the serial console
on its behalf and announce readiness once the kernel has parked itself.
"""
guest = GUESTS[arch]
qemu = require_qemu(guest)
kernel = fetch_kernel(guest)
process = subprocess.Popen([
qemu,
"-m", str(args.memory),
"-kernel", str(kernel),
"-append", "console=ttyS0 panic=0",
"-display", "none",
"-serial", "stdio",
"-monitor", "none",
"-no-reboot",
"-gdb", f"tcp::{args.gdb_port}",
], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
try:
for raw_line in process.stdout:
line = raw_line.decode(errors="replace").rstrip()
print(line, file=sys.stderr)
if BOOT_COMPLETE_MARKER in line:
print("ready", flush=True)
process.wait()
return
raise Unavailable("guest exited before it finished booting")
finally:
process.kill()
def check_win95_guest(args):
require_win95(args)
print("ok")
def boot_win95_guest(args):
"""
Boot a Windows 95 disk image and hand it to QEMU's GDB stub.
Windows 95 says nothing on the serial line, so there is no marker to wait for the way the
Linux guests provide one. Give the boot a fixed budget instead and let the caller decide
whether what it finds looks like a kernel with its page tables up.
TCG rather than KVM: the stub is well-behaved there, and it sidesteps the divide overflow
that fast processors provoke in this kernel.
"""
qemu = require_win95(args)
control = []
if args.qmp is not None:
control = [
"-qmp", f"unix:{args.qmp},server=on,wait=off",
"-device", "virtio-serial-pci,id=" + VIRTIO_SERIAL_ID,
]
diagnostics = []
if args.debugcon is not None:
diagnostics = [
"-chardev", f"file,id={DEBUGCON_ID},path={args.debugcon}",
"-device", f"isa-debugcon,iobase={DEBUGCON_PORT:#x},chardev={DEBUGCON_ID}",
]
process = subprocess.Popen([
qemu,
"-machine", "pc,accel=tcg",
"-cpu", GUEST_CPU,
"-m", str(args.memory),
"-drive", f"file={args.image},format={args.format},if=ide",
"-vga", "cirrus",
"-nic", "none",
"-display", "none",
"-monitor", "none",
"-no-shutdown",
"-chardev", f"socket,id={GDB_ID},host=127.0.0.1,port={args.gdb_port},server=on,wait=off",
"-gdb", f"chardev:{GDB_ID}",
] + control + diagnostics, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
for _ in range(args.boot_seconds):
if process.poll() is not None:
raise Unavailable("guest exited before it finished booting")
time.sleep(1)
print("ready", flush=True)
process.wait()
finally:
process.kill()
def require_win95(args) -> str:
qemu = shutil.which("qemu-system-i386")
if qemu is None:
raise Unavailable("qemu-system-i386 is not installed")
if not args.image.exists():
raise Unavailable(f"{args.image} does not exist")
return qemu
def check_kmod_guest():
guest = GUESTS[KMOD_GUEST_ARCH]
require_qemu(guest)
require_program("cpio")
fetch_busybox(guest)
kernel = host_kernel()
if not os.access(kernel, os.R_OK):
raise Unavailable(f"{kernel} is not readable")
print("ok")
def boot_kmod_guest(args):
guest = GUESTS[KMOD_GUEST_ARCH]
qemu = require_qemu(guest)
busybox = fetch_busybox(guest)
kernel = args.kernel if args.kernel is not None else host_kernel()
cmdline = ["console=ttyS0", "panic=-1"]
if not args.ibt:
cmdline.append("ibt=off")
port = PORT_NAME if args.socket is not None else None
with tempfile.TemporaryDirectory() as staging_dir:
initramfs = build_initramfs(Path(staging_dir), busybox, args.module, port)
channel = []
if args.socket is not None:
channel = [
"-device", "virtio-serial",
"-chardev", f"socket,id=frida,path={args.socket},server=on,wait=off",
"-device", f"virtserialport,chardev=frida,name={port}",
]
process = subprocess.Popen([
qemu,
"-m", str(args.memory),
"-smp", str(args.cpus),
"-enable-kvm",
"-cpu", "host",
"-kernel", str(kernel),
"-initrd", str(initramfs),
"-append", " ".join(cmdline),
"-display", "none",
"-serial", "stdio",
"-monitor", "none",
"-no-reboot",
] + channel, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
listening = False
try:
for raw_line in process.stdout:
line = raw_line.decode(errors="replace").rstrip()
print(line, file=sys.stderr)
if MODULE_LOAD_FAILED_MARKER in line:
raise Unavailable("the module failed to load")
if SLEPT_WHERE_FORBIDDEN_MARKER in line:
raise Unavailable("the module slept where the caller forbids it")
if not listening and AGENT_READY_MARKER in line:
print("ready", flush=True)
listening = True
if not listening:
raise Unavailable("guest exited before the module was listening")
finally:
process.kill()
def require_qemu(guest: "Guest") -> str:
qemu = shutil.which(guest.qemu_binary)
if qemu is None:
raise Unavailable(f"{guest.qemu_binary} is not installed")
return qemu
def fetch_kernel(guest: "Guest") -> Path:
return fetch_cached(guest, "vmlinuz-lts", guest.kernel_url)
def require_program(name: str) -> str:
program = shutil.which(name)
if program is None:
raise Unavailable(f"{name} is not installed")
return program
def fetch_busybox(guest: "Guest") -> Path:
return fetch_cached(guest, "busybox", BUSYBOX_URL)
def host_kernel() -> Path:
return Path("/boot") / ("vmlinuz-" + os.uname().release)
def build_initramfs(staging_dir: Path, busybox: Path, module: Path,
port: str | None) -> Path:
root = staging_dir / "root"
(root / "bin").mkdir(parents=True)
(root / "dev").mkdir()
(root / "proc").mkdir()
shutil.copy(busybox, root / "bin" / "busybox")
(root / "bin" / "busybox").chmod(0o755)
for applet in ("sh", "insmod", "dmesg", "mount", "poweroff", "cat", "seq"):
(root / "bin" / applet).symlink_to("busybox")
shutil.copy(module, root / module.name)
init = root / "init"
init.write_text("\n".join([
"#!/bin/sh",
"mount -t proc proc /proc",
"mount -t devtmpfs dev /dev",
f"insmod /{module.name} || echo {MODULE_LOAD_FAILED_MARKER}",
"for i in 1 2 3 4 5 6 7 8; do",
" (for j in $(seq 200); do cat /proc/self/maps /proc/1/status > /dev/null; done) &",
"done",
"wait",
*(BRIDGE if port is not None else []),
"dmesg",
"exec sh",
"",
]))
init.chmod(0o755)
initramfs = staging_dir / "initramfs.cpio"
names = subprocess.run(["find", "."], cwd=root, check=True, capture_output=True)
with initramfs.open("wb") as f:
subprocess.run(["cpio", "--create", "--format=newc", "--quiet"],
cwd=root, input=names.stdout, stdout=f, check=True)
return initramfs
def fetch_cached(guest: "Guest", name: str, url: str) -> Path:
cached = cache_dir(guest) / name
if cached.exists():
return cached
cached.parent.mkdir(parents=True, exist_ok=True)
staging = cached.with_suffix(".partial")
try:
with urllib.request.urlopen(url, timeout=60) as response, staging.open("wb") as f:
shutil.copyfileobj(response, f)
except Exception as e:
staging.unlink(missing_ok=True)
raise Unavailable(f"unable to download {url}: {e}")
staging.replace(cached)
return cached
def cache_dir(guest: "Guest") -> Path:
base = os.environ.get("XDG_CACHE_HOME")
root = Path(base) if base is not None else Path.home() / ".cache"
return root / "frida-tests" / f"qemu-{guest.arch}"
class Unavailable(Exception):
pass
class Guest(NamedTuple):
arch: str
qemu_binary: str
kernel_url: str
# Paging comes up long before this, so the panic that follows the missing root filesystem is a
# safely late — and unmistakable — sign that the kernel has finished with its page tables.
BOOT_COMPLETE_MARKER = "Kernel panic"
# Alpine's netboot kernels are the smallest stock ones that are trivially fetchable. They are
# not versioned in-place, so treat whatever the mirror currently serves as the fixture; the
# tests only care that it is a Linux kernel that enables paging.
ALPINE_NETBOOT_URL = "https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/{0}/netboot/vmlinuz-lts"
GUESTS = {
guest.arch: guest
for guest in [
Guest("x86", "qemu-system-i386", ALPINE_NETBOOT_URL.format("x86")),
Guest("x86_64", "qemu-system-x86_64", ALPINE_NETBOOT_URL.format("x86_64")),
]
}
KMOD_GUEST_ARCH = "x86_64"
BUSYBOX_URL = "https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox"
VIRTIO_SERIAL_ID = "frida-vserial"
GDB_ID = "frida-gdb"
# Newest model that boots 95; older ones lack the CMOVs the agent emits.
GUEST_CPU = "pentium3"
DEBUGCON_ID = "frida-debugcon"
DEBUGCON_PORT = 0xe9
PORT_NAME = "frida"
PORT_PATH = "/dev/vport0p1"
BRIDGE = [
"exec 3<> /dev/frida",
f"exec 4<> {PORT_PATH}",
"while :; do cat <&3 >&4; done &",
"while :; do cat <&4 >&3; sleep 1; done &",
]
AGENT_READY_MARKER = "frida: listening on /dev/"
MODULE_LOAD_FAILED_MARKER = "frida-kmod-load-failed"
SLEPT_WHERE_FORBIDDEN_MARKER = "Voluntary context switch within RCU"
if __name__ == "__main__":
arch = sys.argv[1]
args = sys.argv[2:] if len(sys.argv) >= 3 else []
run(arch, args)
try:
main()
except Unavailable as e:
print(f"unavailable: {e}", file=sys.stderr)
sys.exit(2)