Frida 16.1.0 Released ∞release
For quite some years I’ve been dreaming of taking Frida beyond user-space software, to also support instrumenting OS kernels as well as barebone systems. Perhaps even microcontrollers…
Earlier this year my family’s cat door broke down. After some back and forth with the retailer, double-checking the installation and such, it would work for a little while before it eventually started malfunctioning.
This was obviously not much fun for our cats:
It’s also no surprise that they’d end up making a lot of noise, in turn making it hard to get a good night’s sleep when having to get up to let them back in manually.
I ended up buying a second cat door and, lo and behold, no more issues. The old one ended up collecting dust for a while. Something I kept thinking of was whether I could debug it, and perhaps even extend the software to do more useful things.
Feeling the urge to open it up to poke at the electronics inside of it, I eventually gave in:
That looked like an STM32F030C6T6, which is an ARM Cortex M0-based MCU. My first thought was whether I could dump the flash memory to do some static analysis.
After quickly skimming MCU docs, and a little bit of multimeter probing, I figured out the JP12 pads:
|PAD 1/2||PAD 7/8|
This made it easy to pull BOOT0 high, so the MCU boots into its internal bootloader instead of user code.
By hooking up a USB to 3.3V TTL device to the USART1 pads, I could dump the flash:
And perform some static analysis:
Seeing as two of the other pads are connected to SWDIO and SWCLK, used for Serial Wire Debug (SWD), the natural next step was to hook up a Raspberry Pi Debug Probe to those. After getting that set up, I fired up OpenOCD:
I initially started sketching this where the backend would talk to the OpenOCD daemon through its telnet interface. But I quickly realized that it would be better to talk to its GDB-compatible remote stub. In this way, Frida will be able to instrument any target with an available remote stub. Whether that’s OpenOCD, Corellium (iOS kernel instrumentation!), QEMU, etc.
After some initial sketching, I was able to run the following script:
Using the Frida REPL:
A few things to note here:
- We set Interceptor.breakpointKind to hard, as our target’s code resides in flash, which means software-breakpoints won’t work. This would not have been necessary if I was using a J-Link or similar SWD interface, which transparently reflashes as software-breakpoints are added.
- We set the least significant bit to indicate to Interceptor that the target function uses Thumb instruction encoding. This part may already be familiar to you if you have previously used Frida’s conventional backends on 32-bit ARM.
- The new Barebone backend connects to the GDB-compatible remote stub at 127.0.0.1:3333 by default. This matches what OpenOCD typically defaults to, but can be overridden by setting the FRIDA_BAREBONE_ADDRESS environment variable.
While my fun little cat door side-quest is a great test case for the tiny part of the spectrum, there’s also a lot of potential in supporting larger systems.
One of the cooler use-cases there is definitely Corellium, as it means we can instrument the iOS kernel. Using a Tamarin Cable it should even be possible to get this working on a checkm8-exploitable physical device.
Before we touch on that though, let’s see if we can get things going with QEMU and a live Linux kernel.
First, we’ll fire up a VM we can play with:
Next, we’ll use the Frida REPL to look around:
You might wonder how we’ve implemented Process.enumerateRanges(). This part is for now only implemented on arm64, and it is accomplished by parsing the page tables. (And if we’re talking to Corellium’s remote stub we use a vendor-specific monitor command to save ourselves a lot of network roundtrips.)
So now that we’re peeking into a running kernel, one of the things we might want to do is find internal functions and data structures. This is where the memory- scanning API comes handy:
Here we’re looking for the Linux kernel’s arm64 syscall handler, matching on its first three instructions. We use the masking feature to mask out the immediates of the ADRP and MOV instructions (first and third instruction).
Let’s take it for a spin:
So now we’ve dynamically detected the kernel’s internal syscall handler! 🚀
Re-implementing the memory scanning feature was one of the highlights for me personally, as @hsorbo and I had a lot of fun pair-programming on it. The implementation is conceptually very similar to what we’re doing in our Fruity backend for jailed iOS, and our new Linux injector: instead of transferring data to the host, and searching that, we can get away with transferring only the search algorithm, to run that on the target.
The memory scanner implementation is written in Rust, and helped prepare the groundwork for a new cool feature I’m going to cover a bit later in this post.
So, now that we know where the Linux kernel’s syscall handler is, we can use Interceptor to install an instruction-level hook:
And try that out on our running VM:
And that’s it – we are monitoring system calls across the entire system! 💥
Not to worry though. If we write our callback in machine code and pass a NativePointer instead, Interceptor will pick a different strategy: it will modify the target’s machine code to redirect execution to a trampoline, which in turn calls the function at the address that we specify.
So that’s great. We only need to get our machine code into memory. Some of you may be familiar with our CModule API. We don’t yet implement that one in this new Barebone backend (and we will, eventually!), but we have something even better. Enter RustModule:
The RustModule implementation uses a local Rust toolchain, assumed to be on your PATH, to compile the code you provide it into a no_std self-contained ELF. It relocates this ELF and writes it into the target’s memory. As part of this it will also parse the MMU’s page tables and insert new entries there so the uploaded code becomes part of the virtual address space, where the pages are read/write/execute.
In this example we’re hooking proc_pid_status() in our live Linux kernel.
Now, with our Rust-powered agent, let’s take it for a spin:
Oops! That didn’t quite work. There is still one piece missing in our new backend: we don’t yet have any “kernel bridges” in place that automatically fingerprint the internals of known kernels in order to find a suitable internal memory allocator we can use. This will also be needed to implement APIs such as Process.enumerateModules(), which will allow listing the loaded kernel modules/kexts. We could also locate the kernel’s process list and implement enumerate_processes(), so frida-ps works. And those are only a couple of examples… How about injecting frida-gadget into a user-space process? That would be super-useful for an embedded system where we want to avoid modifying the flash. Anyway, I digress 😊
So, on MCUs and unknown kernels you will have to tell Frida where, in physical memory, we may clobber if you want to use intrusive features such as RustModule, Interceptor in its inline hooking mode, Memory.alloc(), etc.
With that in mind, let’s retry our example, but this time we’ll set the FRIDA_BAREBONE_HEAP_BASE environment variable:
Yay! 🎉 So now, in the terminal where we have QEMU running, let’s try accessing /proc/$pid/status three times, so the hooked function gets called:
Over in our REPL, we should see our hook() getting hit three times:
It works! 🥳
The next thing you might want to do is pass external symbols into your RustModule. For example if you want to call internal kernel functions from your Rust code. This is accomplished by declaring them like this:
Then when constructing the RustModule, pass it in through the second argument:
Last but not least, you may also want to import existing Rust crates from crates.io. This is also supported:
What’s so exciting is that all of the Linux bits above all “just work” on Corellium as well. All you need to do is point FRIDA_BAREBONE_ADDRESS at the endpoint shown in Corellium’s UI under “Advanced Options” -> “gdb”.
Shout-out to the awesome folks at Corellium for their support while doing this. They even implemented new protocol features to improve interoperability 🔥
This new backend should be considered alpha quality for now, but I think it’s already capable of so many useful things that it would be a shame to keep it sitting on a branch.
You may notice that the JS APIs implemented only cover a subset, and not all features are available on non-arm64 targets yet. But all of this will improve as the backend matures. (Pull-requests are super-welcome!)
And as a fun aside, here’s Frida attached to the BeOS kernel:
There’s also a bunch of other exciting changes, so definitely check out the changelog below.
- Add Barebone backend. (Covered extensively above.)
- objc: Handle modifiers. This makes type parsing more reliable, especially when dealing with ivars which often times have the “atomic” modifier: exceptions were thrown because the modifiers ended up being treated as unknown types. Thanks @mrmacete!
- android: Fix support for Android 14. Thanks @gsingh93! Also thanks @jayluxferro for contributing a follow-up fix for a mistake I made while merging @gsingh93’s PR.
- gum-graft: Add support for chained imports. Thanks @mrmacete!
- gumjs: Add NativePointer#readVolatile(), to provide a safe way to read from memory that may become unmapped or have its memory protection changed midway through. Thanks @hsorbo!
- darwin: Improve tvOS support to also cover frida-server. Thanks @tmm1!
- darwin: Plug memory leak in fallback kill() logic. Thanks @tmm1!
- fruity: Handle failure to fetch dyld symbols.
- compiler: Bump frida-compile to 16.2.2. Dependencies’ source maps are now also bundled – thanks @vfsfitvnm!
- compiler: Bump @types/frida-gum to 18.3.2, now with improved typings for hexdump().
- gdb: Add GDB.Client, by factoring out the guts of Fruity’s LLDB.Client, with many protocol enhancements and interoperability fixes added on top.
- elf-module: Improve API and make it cross-platform. Support loading from a blob, expose relocations, and improve overall robustness.
- capstone: Fix crash on x86 when building with MSVC.