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:

  1. Get the target’s class.
  2. Find that class’s method cache.
  3. 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.
  4. 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:

fishhook-based objc_msgSend hook

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:

Inline-hook dead loop

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

Hook output: all ObjC method names and arguments

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