Search code examples
objective-cfrida

What parameter should I feed to Frida `ObjC.api.class_addMethod()` to make it happy?


I want to use Frida to add a class method to the existing Objective C class on Mac OS. After I read the Frida docs, I tried the following code:

const NSString = ObjC.classes.NSString

function func (n) { console.log(n) }

var nativeCb = new NativeCallback(func, 'void', ['int'])

ObjC.api.class_addMethod(
  NSString.handle,
  ObjC.selector('onTest:'),
  nativeCb,
  ObjC.api.method_getTypeEncoding(nativeCb)
)

The above code looks straightforward. However, after the ObjC.api.class_addMethod() call, the attached App and the Frida REPL both froze, it looks that the pointers are not right.

I have tried many possible parameter values for a whole night but still can figure the problem out. What's wrong with my code?


Solution

  • Only two issues:

    • method_getTypeEncoding() can only be called on a Method, which the NativeCallback is not. You could pass it the handle of an existing Objective-C method that has the same signature as the one you're adding, or use Memory.allocUtf8String() to specify your own signature from scratch.
    • Objective-C methods, at the C ABI level, have two implicit arguments preceding the method's arguments. These are:
        1. self: The class/instance the method is being invoked on.
        1. _cmd: The selector.

    Here's a complete example in TypeScript:

    const { NSAutoreleasePool, NSString } = ObjC.classes;
    
    const onTest = new NativeCallback(onTestImpl, "void", ["pointer", "pointer", "int"]);
    
    function onTestImpl(selfHandle: NativePointer, cmd: NativePointer, n: number): void {
        const self = new ObjC.Object(selfHandle);
        console.log(`-[NSString onTestImpl]\n\tself="${self.toString()}"\n\tn=${n}`);
    }
    
    function register(): void {
        ObjC.api.class_addMethod(
            NSString,
            ObjC.selector("onTest:"),
            onTest,
            Memory.allocUtf8String("v@:i"));
    }
    
    function test(): void {
        const pool = NSAutoreleasePool.alloc().init();
        try {
            const s = NSString.stringWithUTF8String_(Memory.allocUtf8String("yo"));
            s.onTest_(42);
        } finally {
            pool.release();
        }
    }
    
    function exposeToRepl(): void {
        const g = global as any;
        g.register = register;
        g.test = test;
    }
    
    exposeToRepl();
    

    You can paste it into https://github.com/oleavr/frida-agent-example, and then with one terminal running npm run watch you can load it into a running app using the REPL: frida -n Telegram -l _agent.js. From the REPL you can then call register() to plug in the new method, and test() to take it for a spin.