Dynamic (offset based) hooking with Frida
Introduction
Recently, I’ve been reversing some iOS functions inside private frameworks. I needed to clearly understand what arguments they took and why (since it’s Objective-C, it can sometimes be tricky), so I’ve decided to jump into Frida and hook them. One common problem you will usually encounter is not finding the symbol you want because it’s not exported. This post will cover how I managed to bypass this problem using some logic.
Methodology
Like we’ve said before, sometimes, you cannot directly hook onto a function with frida-trace like you would do
normally. The function I was interested in was _updateClientStateCondition:newValue: from the FigCaptureClientSessionMonitor
class inside the CMCapture private framework. But sadly, I couldn’t find it when tracing all the method calls inside the binary.
However, we are not forced to use the resolved symbols from Frida, we can also directly use full addresses.
In order to do this, we will need two things to proceed: our module’s base address at runtime and the offset to our function. Once that’s done, we will just need to write our script, dynamically resolving our function’s address to hook onto it (if you are familiar with binary exploitation and bypassing ASLR, it’s kind of the same thing, instead here our ’leak’ of the base address is given by Frida).
Example
Here we will hook onto -[FigCaptureClientSessionMonitor _updateClientStateCondition:newValue:] inside the CMCapture
private Framework.
In this example we’re going to manually calculate the offset by substracting the function’s address to the module base.
To obtain the CMCapture base address inside Binary Ninja, you can open the DSC Triage view (where you load your frameworks)
and get the address. In our case it will be 0x1a3ce1000.
Once that’s done you can open the framework and search for the function you want. Here _updateClientStateCondition is at 0x1a3d55470.
Finally to obtain our base address through Frida, you can use Process.findModuleByName("your process") followed by module.base.
Everything can be packed inside a little script that will look like the following:
|
|
(here we print the args with the correct types corresponding to ObjC objects (selector & class)).
We can then attach to our process and launch our script:
|
|
And voilà! We have now hooked onto our function, and we are able to see its arguments. Keep in mind that every Objective-C function call implicitly includes the class and the selector as its first two arguments.
Conclusion
Although being pretty simple, this technique can be really useful during reversing as you can dynamically validate (or no) your hypothesis at runtime. However don’t forget that frameworks can change from an iOS version to another so be sure to use the correct iOS version!