Is there any way to list all swizzled methods in an iOS app?
Asked Answered
H

1

12

I'm essentially looking for a way to detect when/what third party libraries swizzle. I recently ran into a situation where an ad library used an oddball fork of AFNetworking. AFNetworking swizzles NSURLSessionTask, and the two swizzles didn't play nicely under certain circumstances. I'd really like to be able to detect and sanity check this kind of thing, and ideally even keep a versioned dump of every swizzled method in the app so we have some visibility into who's monkey patching what and what the risks are. Google and stack overflow search have turned up nothing but a bunch of tutorial on how to swizzle. Anybody run into this issue or have a solution? It looks like I might be able to code something up using objc/runtime.h but I can't imagine I'm the first person to need this.

Heisenberg answered 20/7, 2016 at 18:35 Comment(1)
You could attempt to use something like mach_override in the iOS simulator (won't work on-device), and hook method_exchangeImplementation/method_setImplementation, although you'd need to ensure that you don't use that code path during release builds.Theophylline
T
11

Here's the closest I was able to get, with a few hours of tinkering. It involves using a fork of mach_override, a couple of DYLD quirks regarding load order, and a stomach for crazy hacks.

It will only work on the simulator, but that should suffice for the use case you seem to have (I certainly hope you don't have device-only dependencies).

The meat of the code looks something like this:

#include <objc/runtime.h>
#include <mach_override/mach_override.h>

// It is extremely important that we have DYLD run this constructor as soon as the binary loads. If we were to hook
// this at any other point (for example, another category on NSObject, in the main application), what could potentially
// happen is other `+load` implementations get invoked before we have a chance to hook `method_exchangeImplementation`,
// and we don't get to see those swizzles.
// It is also extremely important that this exists inside its own dylib, which will be loaded by DYLD before _main() is
// initialized. You must also make sure that this gets loaded BEFORE any other userland dylibs, which can be enforced by
// looking at the order of the 'link binary with libraries' phase.
__attribute__((constructor))
static void _hook_objc_runtime() {
  kern_return_t err;
  MACH_OVERRIDE(void, method_exchangeImplementations, (Method m1, Method m2), &err) {
    printf("Exchanging implementations for method %s and %s.\n", sel_getName(method_getName(m1)), sel_getName(method_getName(m2)));

    method_exchangeImplementations_reenter(m1, m2);
  }
  END_MACH_OVERRIDE(method_exchangeImplementations);

  MACH_OVERRIDE(void, method_setImplementation, (Method method, IMP imp), &err) {
    printf("Setting new implementation for method %s.\n", sel_getName(method_getName(method)));

    method_setImplementation_reenter(method, imp);
  }
  END_MACH_OVERRIDE(method_setImplementation);
}

Which is surprisingly simple, and produces output like this:

Exchanging implementations for method description and custom_description.

There is no good way (without using a breakpoint and looking through the stack trace) to figure out which class is being swizzled, but for most things, just taking a peek at the selectors should give you a hint about where to go from there.

It appears to work with a couple of categories that I've created that swizzle during +load, and from my reading of mach_override and DYLD's internals, as long as you have your library load order properly setup, you can expect this to be initialized before any other user-land code, if you put it in it's own dynamic library.

Now, I can't vouch for safety of this, but it seems useful to keep around as a debugging tool, so I've published my example to github:

https://github.com/richardjrossiii/mach_override_example

It's MIT licensed, so feel free to use as you see fit.

Theophylline answered 20/7, 2016 at 21:16 Comment(8)
Nice hack; if one were terribly motivated, one could cache the new IMP and then search all classes to find where it was injected. It would be brute force and potentially trigger unexpected behavior (because you'll end up triggering the +initialize of ever class in the runtime unless you're really careful).Azriel
@Azriel That's certainly one approach, though considering most swizzling happens from directly inside +load (barring dynamic swizzling libraries like OCMock and similar), it probably would be simpler to just use backtrace() combined with dladdr() to simply grab the class & category that invoked method_exchangeImplementation :)Theophylline
That'll sometimes work if you want to know where the swizzle came from, but not the class being swizzled? When I've used swizzling, I'm oft bulk swizzling a bunch of methods across a bunch of classes for debug purposes. ?Azriel
@Azriel fair enough point. I had forgotten that open-source libraries like to use monstrosities like this rather than deal with the runtime directly.Theophylline
heh, that monstrosity you linked to is precisely the thing that bit us. Thanks for going above and beyond on this. I was unaware of mach_override and some initial experimentation with the objc runtime hadn't yielded anything terribly useful. Your solution is a lot cleaner than anything I would have cobbled together.Heisenberg
@RichardJ.RossIII I tried cloning your project per the instructions but getting an unauthorized error when cloning the submodule.Alchemist
@AvnerBarr Set up SSH keys with github, or change the submodule url to be https.Theophylline
I'm rejected with both HTTPS and SSH when attempting to clone.Sealer

© 2022 - 2024 — McMap. All rights reserved.