Our Blog

mettle your ios with frida

Reading time ~9 min

For a long time I have wondered about getting Meterpreter running on an iOS device using Frida. It wasn’t until I had a Twitter conversation with @timwr that I was reminded of Mettle. It was finally time to give it a try. I built an objection plugin that would load it for you, which you can find here.

My talk at DEF CON 27 mainly covered some ideas on how we could interact with live object instances in interesting ways. However, there were also some examples of how we could use Frida’s Module.load() API to side load existing external tooling that come in the form of shared libraries (either by default or wrapping them ourselves). With Mettle targeting low-resource or embedded devices, its native code approach meant it also supported iOS. So if we could get a compiled Mettle dylib, we could load it with Frida. You don’t need Frida to load a dylib of course. Using something like insert_dylib would work just as well. The nice thing about using something like Frida though is that we have some external control over the loading process and any post processing that we may need.

Exploring Mettle

Before we dive into all of that, lets first explore Mettle a bit. I cloned the Mettle repository and had a look around. First the README, then the Makefile and so forth. Compiling Mettle was really, really simple. In fact, all I had to do on macOS was type make. Not specifying a target will have Mettle compile for the current host OS. The result was a mettle binary in the build directory for darwin.x86_64.

Mettle built for macOS, showing its -h output.

Using the resultant mettle binary just like that was not particularly useful. I tried out the console -c but quickly realised I needed to specify where to connect to with the --uri flag. Alright, to Metasploit I go, start a new multi handler and use a macOS meterpreter payload. Next I start mettle with ./mettle -u tcp:// It took a few tries to get the URI format right, but in the end I got a new meterpreter session to open.

Meterpreter session 1 opened from macOS, using mettle.

Neat! As you would expect, whatever macOS post exploitation modules exist, should work now. With some basics down, it was time to explore how to get this going on iOS.

Compiling for iOS

Mettle has a few make targets defined using a TARGET value. You can see them in the README file here. For a modern iOS device, the target would be aarch64-iphone-darwin. So to build Mettle for iOS, run make TARGET=aarch64-iphone-darwin.

Build complete for an iOS dylib, reminding you to codesign the resultant binaries.

Moments later, a new dylib should appear in build/aarch64-iphone-darwin/bin.

arm64 shared library for Mettle

Using Mettle on iOS

Neat! But how do I use it? The shared library had a ton of exports, so it was not immediately clear to me how to initiate a connection from the outside. Maybe I could just invoke main()?

dylib export count. :O

I looked at existing implementations of using Mettle from Metasploit, and saw this payload for the watchOS exploit, CVE-2017-13861. While the exploit itself is not really important here, the invocation of Mettle was interesting in that main() simply got invoked with the arguments it needs. Alright, I could do that with Frida!

We have a dylib, but we need to get it to a device. Using objections filemanager on a patched application, we can make quick work of that. I uploaded the codesigned dylib to my target applications documents directory.

Uploading the mettle.dylib to a target applications Documents directory.

Now to load and execute it. For this I wen’t back to the Frida command line and a fresh script to play with the mettle.dylib. As previously mentioned loading the dylib is already possible using the Module.load() Frida API. So, the first part of the new script simply determined the path to the applications Documents directory and built up a variable to mettle.dylib. I tested the script itself with the Frida command line using frida -U Gadget --runtime=v8 -l mettle.js.

Frida path calculation to the target applications Document’s directory.

With the path at hand, a simple Module.load(dylibPath); was all that was needed to get Mettle loaded. Unfortunately, loading Mettle like this actually triggered a Frida bug (which I am hoping to have a patch for soon). Nothing a try{}catch{} couldn’t solve though! :D Instead of Module.load() returning a handle to mettle.dylib, we just had to resolve that manually after the load with var mettle = Process.getModuleByName("mettle.dylib");. I confirmed that the load was actually successful by running a mettle.enumerateExports() and checking that it corresponds with my earlier r2 output.

Loading mettle and getting a handle to it using Frida

With the dylib loaded, it was time to execute main(). As you would expect for most main() invocations, you either have no arguments, or an argc, and an argv[]. I wanted to pass along three arguments, so argc would be 3. The arguments themselves would be the binary name, mettle and then -u with a target.

At this point I was having quite a bit of trouble replicating a pointer to an array of string pointers for *argv[] using the Frida API, but thankfully I remembered about Frida’s new CModule! This meant I could just write a function in C that would get me the correct structure for an argv[] and pass that to Mettle’s main(). The CModule uses tcc under the hood, so I just had to make sure my function compiled fine using that. I created a test program locally just to make sure everything actually worked. That looked as follows:

get_args() function used to generate a pointer to an array of string pointers.

Testing this function was also really easy. I just called it in a main() function recursively, and ran it with tcc -run main.c. With that out of the way, I slapped get_args() into my Frida script and tested it from there. I needed to change my call from malloc() to g_malloc() as Frida was only including a very minimal set of headers available in its implementation of the embedded compiler.

Frida CModule usage to generate an argv[] array.

Using this new CModule variable with a new NativeFunction() definition, I now had an argv() JavaScript function I could call which should give me the pointer I need for *argv[] in Mettles main. How cool is that! The only thing left to do was to was to configure Metasploit to listen (like we did previously, but this time choosing the apple_ios/aarch64/meterpreter_reverse_tcp payload) and to get a pointer to main() to call. We can get that by simply calling findExportByName("main"); and wrapping it in a new NativeFunction().

Final initialisation code to execute Mettle.

So to bring all of this together, I launched my test application, uploaded mettle.dylib to the apps Documents directory and ran my new Frida script with the correct IP:PORT combination in the C module where Metasploit was listening.

mettle.dylib uploaded using objection and mettle.js executed using Frida
Metepreter session 1 opened!

At this point, whatever features Meterpreter has for you are available using the session that just connected. For example, we could setup a port forward to another web server (think a webserver the device can access from the internal Wireless network it may be connected to..).

Port forward configuration, to forward incoming port 80 requests to Google
Port forwarded request to Google via an iPhone using Metasploit :)


Let’s recap. We modified an iOS app to load Frida (something objection can help you with), then compiled and loaded the Mettle shared library and finally called the main entrypoint for Mettle and let it connect to Metasploit. Neat! While you can totally do this process manually every time, I instead added a new objection plugin that will automatically load a mettle.dylib and connect it for you.

Mettle objection plugin loading and connecting