Hooking objc_msgSend to Capture Objective-C Method Arguments
About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.
This research is published for educational and defensive purposes. The target sample is anonymized throughout.
Hooking objc_msgSend to Capture Objective-C Method Arguments
Hooking objc_msgSend
objc_msgSend is the most important function in iOS. The idea is simple: hook objc_msgSend, and you capture the name and arguments of every Objective-C method call in the app. Every ObjC method call goes through objc_msgSend’s message dispatch to find the method’s IMP.
A quick look at objc_msgSend
The objc_msgSend source is on Apple’s open source site. For efficiency it’s written in assembly, and it has roughly four key steps:
- Get the target’s class.
- Find that class’s method cache.
- Try to find the concrete implementation in the cache buckets. If it’s not cached, fall into message forwarding for a deeper lookup, then cache the method and its IMP.
- Call the implementation directly.
In short, it’s a “trampoline”, and it’s thread-safe. Of the four steps, 1, 2, and 4 are thread-safe; step 3 uses tricks to stay thread-safe — beyond this post’s scope. There are good line-by-line assembly walkthroughs of objc_msgSend if you want the details.
Idea 1 — hook it like an ordinary C function
Per #include <objc/message.h>, objc_msgSend is declared as:
id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
So treat it as an ordinary C function, hook it with fishhook or cydia substrate, and analyze the incoming arguments with va_list — right?
No. As documented elsewhere, the va_list structure changed on arm64, so you can’t use va_list. This approach doesn’t work. (I haven’t verified its correctness independently.)
Idea 2 — fishhook + assembly hook
Since va_list is a dead end, look at the objc_msgSend source again. On arm64, x0–x8 hold the arguments, so with a bit of assembly we can hook it. This has been done successfully before.
fishhook is a good fit because it hooks the export table — it doesn’t modify the original function’s implementation.
The rough implementation:

To save the registers, use stp to push them onto the stack. To raise the stack frame, lower SP — on iOS the stack grows downward, so later arguments sit at lower addresses:
"stp x8, x9, [sp, #-16]!\n"
Note the ! after the offset — after the stp executes, SP is also offset. Likewise, restore the registers with ldp to lower the stack frame back.
Idea 3 — inline hook + assembly hook
Since fishhook works, an inline hook works too. The difference is that an inline hook modifies the original function’s first three instructions; a backup function keeps those three instructions, then br jumps to the original function’s fourth instruction. Calling the backup behaves like calling the original objc_msgSend. I won’t rehash how inline hooking patches instructions.
The flow matches the fishhook version, but there’s a gotcha I hit in practice: inside before_objc_msgSend_inline and after_objc_msgSend_inline you temporarily can’t call ObjC methods, or you get a dead loop like this:

Reading objc_msgSend’s arguments
Since the arguments live in x0–x8, analyzing the registers in before_objc_msgSend gives you the inputs:
// Print the target object, selector, and up to two arguments.
void printSpecificParam_fish(id self, SEL _cmd, uintptr_t param1, uintptr_t param2,uintptr_t lr)
{
// Only C functions are safe here for now — using ObjC methods can clobber registers and crash;
// in testing, it crashed when the same method called itself.
// NSLog is fine.
const char * className = object_getClassName(self);
const char * selector = sel_getName(_cmd);
//NSLog(@"class : %s, methodname : %s",className,selector);
if ( strcmp( selector, "fileExistsAtPath:" ) == 0) {
NSLog(@"class : %s, methodname : %s, param1 : %@",className,selector,param1);
}else {
NSLog(@"class : %s, methodname : %s",className,selector);
}
}
There’s another gotcha: the self argument triggers reference-counting problems in some apps, causing crashes. I haven’t dug into why — leaving it as a TODO. For now, drop self from the hook function’s parameters:
void printSpecificParam_fish( SEL _cmd, uintptr_t param1, uintptr_t param2,uintptr_t lr)
{
const char * selector = sel_getName(_cmd);
NSLog(@"methodname : %s",selector);
}
Result

Every ObjC method name and its arguments are printed.
Wrapping up
The complete code is available on GitHub.
Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).
Get in touch