Today we are going to talk about mid function hooking on iOS. While being fairly straightforward with a jailbreak it can be pretty tricky on jailed iOS devices because iOS prevents any changes to the text section during runtime by default unless you have access to the JIT entitlement which is highly restricted and can only be enabled using a PC. So what can we do if we need modifications in the middle of functions on jailed iOS devices? In the following I will explain multiple methods to achieve this.
1. Statically patching the binary
The first way of achieving this would be statically patching the mach-o binary file. For this we would first write the assembly code of our mid function hook into a codecave, place a branch to it in the function we want to hook and branch back afterwards. This is common practice across various platforms so im not going to elaborate any further.
Advantages
Disadvantages
Seems to be very straightforward
The space that codecaves provide you with is limited
Pretty much no performance impact
You are forced to write everything in assembly and don’t have any c interop
Unlimited amount of hooks possible as long the codecave space is big enough
Calling libc functions that aren’t imported by the binary already is a pain in the ass
2. Abusing exceptions (via software breakpoints)
While the first way was probably known to most people, time to start with the more interesting stuff. The next way of achieving mid function hooks is by abusing exceptions via software breakpoints. The way this works is by placing a software breakpoint(BRK instruction on arm64) at the location where we want to enter our hook and catch the exception with a sigaction handler. From our sigaction handler we can then redirect code execution to our own code by editing the thread context.
Advantages
Disadvantages
Unlimited amount of hooks
Can need quite some effort to get it to work properly
C interop
The performance can suffer depending on how often the function is called, as every call triggers an exception/hardware interrupt
Not needing to patch the binary manually every single time
You can’t use it to hook functions in iOS system libraries as you cannot patch a breakpoint into them pre-runtime
Guide:
First we inject our own dynamic library into the iOS app bundle and add a dyld_load command to the main binary so our library gets loaded on app startup. This process is pretty common and there are a lot of resources out there on how to do this, so I’m not going to elaborate on this any further.
The next thing we need to do is register our sigaction handler.
#include<signal.h>#define _XOPEN_SOURCE 600 //ucontext is deprecated, using this define we can continue using it
#include<ucontext.h>voidhandler(int sig, siginfo_t *info, void*ucontext) {
}
__attribute((constructor)) staticvoid setup() {
structsigaction sa;
sa.sa_flags = SA_SIGINFO;
sa.sa_sigaction = handler;
sigemptyset(&sa.sa_mask);
sigaction(SIGTRAP, &sa, NULL);
}
Now our sigaction handler is registered, and once our software breakpoint gets hit and throws a SIGTRAP exception we will receive it in our handler. Next we need a way to differentiate our hooks. for this we can use the 16 bit immediate value from the BRK instruction and use it as an hook id. To extract it we can use a bitmask.
So for example this hook would have the id 69. Next we will actually implement the logic for our sigaction handler to redirect code execution to the right function based on the hook id extracted from the BRK instruction.
#include<signal.h>#define _XOPEN_SOURCE 600 //ucontext is deprecated, using this define we can continue using it
#include<ucontext.h>structhook {
uintptr_t *original;
uintptr_t func;
};
std::map<int, hook> hooks;
voidhandler(int sig, siginfo_t *info, void*ucontext) {
NSLog(@"EINTIM: CALLED");
ucontext_t *context = (ucontext_t *)ucontext;
uint32_t instruction =*(uint32_t*)context->uc_mcontext->__ss.__pc; //extract the instruction by dereferencing the value of the program counter register.
if (is_brk_instruction(instruction)) {
uint16_t hookid = extract_hook_id(instruction); // if its a break instruction extract our hook id
if (hooks.find(hookid) == hooks.end()) { //if the hook doesn't have a corresponding handler, ignore. This is not one of our hooks
NSLog(@"EINTIM: Handler for %d not found!\n", hookid);
return;
}
hook h = hooks[hookid];
if (h.original &&*h.original ==0)
*h.original = (uintptr_t)(context->uc_mcontext->__ss.__pc +4); //Backup the return address and skip our placed 4 byte long breakpoint instruction while doing so
context->uc_mcontext->__ss.__pc = h.func; //lastly set the program counter register to our hook address to actually redirect the execution
}
}
uintptr_t originalAddressisSubscribed =0;
__attribute__((naked)) uint64_t isSubscribedHook() {
//run our mid function hook code
__asm__("mov x0, 12");
// execute original instruction we overwrote with our breakpoint instruction
__asm__("svc #0x80");
// branch back to original function
asmvolatile(
"ldr x1, %[addr]\n""br x1\n":: [addr] "m"(originalAddressisSubscribed)
:"x1");
}
__attribute((constructor)) staticvoid setup() {
hooks = std::map<int, hook>();
structsigaction sa;
sa.sa_flags = SA_SIGINFO;
sa.sa_sigaction = handler;
sigemptyset(&sa.sa_mask);
sigaction(SIGTRAP, &sa, NULL);
//isSubscribed hook
hook isSubscribed{};
isSubscribed.func = (uintptr_t)isSubscribedHook;
isSubscribed.original =&originalAddressisSubscribed;
hooks[69] = isSubscribed;
}
Now we have a fully working implementation of this that registers our sigaction handler, registers our hook, finds the right hook handler based on the hook id, redirects code execution to it, then executes our code and the original instruction we overwrote with our BRK instruction and branches back to the programs code :). Easy!
Now you might think that this is quite annoying as you always need to patch the breakpoint into your target binary where you want the hook to run and write the actual hook registration and copy paste the original instruction. And while this is true i wrote a small python script to automate this process. It takes hook definitions from a json file, automatically searches the functions inside of the target binary, places the breakpoint and generates the hook handlers including the execution of the original function. Note that this is very dirty and was only meant for my personal use, you might still want to take some inspiration from it tho.