Frida in Practice: Combining the API Resolver with the Interceptor
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.
Frida in Practice: Combining the API Resolver with the Interceptor
This is the fourth post in a hands-on Frida series, covering the API Resolver (ApiResolver) combined with the Interceptor, plus an alternative way to hook by replacing an implementation.
The first post covered analyzing an app on iOS, the second covered the Interceptor, and the third covered intercepting sub_xxxx functions.
In the earlier posts we used the Interceptor to hook Objective-C methods and C functions. That’s convenient when you’re hooking a handful of known functions — but what if you need to hook in bulk? Say you want to hook every method of a class, or every function matching a name pattern in a module, when you don’t know the exact names and only have a rough keyword. That’s where the API Resolver comes in: use it to find the functions you’re interested in, get their addresses, and then hook them.
The API Resolver handles both Objective-C methods and C functions. Let’s write a script that finds every method of the NSFileManager class. Create an ApiResolver — pass objc to search Objective-C, or module to search C functions. Then call enumerateMatches to enumerate; each match triggers onMatch, whose match argument has name and address properties. After the whole enumeration, onComplete runs:
var resolver = new ApiResolver('objc');
resolver.enumerateMatches('*[NSFileManager *]', {
onMatch: function(match) {
console.log(match['name'] + ":" + match['address']);
},
onComplete: function() {}
});
Attach to Safari and load the script; it prints every NSFileManager method:
frida -U -l hook_objc.js Safari
-[NSFileManager dealloc]:0x1e2324034
-[NSFileManager setDelegate:]:0x1e23b2744
-[NSFileManager delegate]:0x1e2333ab8
-[NSFileManager _info]:0x1e2333ae0
-[NSFileManager removeItemAtURL:error:]:0x1e235583c
-[NSFileManager copyItemAtURL:toURL:error:]:0x1e23b2ab0
-[NSFileManager fileExistsAtPath:isDirectory:]:0x1e2301d50
-[NSFileManager fileExistsAtPath:]:0x1e22e5398
-[NSFileManager setAttributes:ofItemAtPath:error:]:0x1e23180bc
-[NSFileManager removeItemAtPath:error:]:0x1e23336bc
-[NSFileManager enumeratorAtPath:]:0x1e2326024
-[NSFileManager isWritableFileAtPath:]:0x1e23240dc
-[NSFileManager moveItemAtPath:toPath:uniquePath:error:]:0x1ec6f4ec4
-[NSFileManager copyItemAtPath:toPath:uniquePath:error:]:0x1ec6f4ed4
......
Once you have the method names, pick the interesting ones and hook them with the Interceptor. Say we want a simple file monitor that logs which files a process touches. The relevant file operations are removeItemAtPath, moveItemAtPath, copyItemAtPath, createFileAtPath, createDirectoryAtPath, enumeratorAtPath, and contentsOfDirectoryAtPath — delete, move, copy, create file, create directory, enumerate directory (with subdirectories), and enumerate directory (without subdirectories):
var resolver = new ApiResolver('objc');
resolver.enumerateMatches('*[NSFileManager *]', {
onMatch: function(match) {
var method = match['name'];
var implementation = match['address'];
// Filter the methods to intercept
if (//(method.indexOf("fileExistsAtPath") != -1)
(method.indexOf("removeItemAtPath") != -1)
|| (method.indexOf("moveItemAtPath") != -1)
|| (method.indexOf("copyItemAtPath") != -1)
|| (method.indexOf("createFileAtPath") != -1)
|| (method.indexOf("createDirectoryAtPath") != -1)
|| (method.indexOf("enumeratorAtPath") != -1)
|| (method.indexOf("contentsOfDirectoryAtPath") != -1)) {
console.log(match['name'] + ":" + match['address']);
try {
Interceptor.attach(implementation, {
onEnter: function(args) {
var className = ObjC.Object(args[0]);
var methodName = args[1];
var filePath = ObjC.Object(args[2]);
console.log("className: " + className.toString());
console.log("methodName: " + methodName.readUtf8String());
console.log("filePath: " + filePath.toString());
},
onLeave: function(retval) {
}
});
} catch (err) {
console.log("[!] Exception: " + err.message);
}
}
},
onComplete: function() {
}
});
Attach to SpringBoard, load the script, then uninstall an app. The script prints the file operations as they happen — you can see SpringBoard delete /var/mobile/Library/UserNotifications and the app’s sandbox directory:
frida -U -l hook_objc.js SpringBoard
[iPhone::SpringBoard]->
className: <NSFileManager: 0x282954250>
methodName: removeItemAtPath:error:
filePath: /var/mobile/Library/UserNotifications/com.example.app/Attachments
className: <NSFileManager: 0x282954250>
methodName: filesystemItemRemoveOperation:shouldRemoveItemAtPath:
filePath: <NSFilesystemItemRemoveOperation: 0x283674f40>
className: <NSFileManager: 0x282954250>
methodName: enumeratorAtPath:
filePath: /private/var/mobile/Containers/Data/Application/C8A23CD4-6857-4BF4-876E-7FD104B0E799/Library/Caches/Snapshots/com.example.app
className: <NSFileManager: 0x282954250>
methodName: removeItemAtPath:error:
filePath: /private/var/mobile/Containers/Data/Application/C8A23CD4-6857-4BF4-876E-7FD104B0E799/Library/Caches/Snapshots/com.example.app
className: <NSFileManager: 0x282954250>
methodName: filesystemItemRemoveOperation:shouldRemoveItemAtPath:
filePath: <NSFilesystemItemRemoveOperation: 0x283677e80>
className: <NSFileManager: 0x282954250>
methodName: removeItemAtPath:error:
filePath: /private/var/mobile/Containers/Data/Application/C8A23CD4-6857-4BF4-876E-7FD104B0E799/Library/Caches/Snapshots
className: <NSFileManager: 0x282954250>
methodName: filesystemItemRemoveOperation:shouldRemoveItemAtPath:
filePath: <NSFilesystemItemRemoveOperation: 0x283677e80>
.......
To hook every NSFileManager method except a few, filter those out instead:
var resolver = new ApiResolver('objc');
resolver.enumerateMatches('*[NSFileManager *]', {
onMatch: function(match) {
var method = match['name'];
var implementation = match['address'];
// Filter out methods we don't want; intercept everything else
if (method.indexOf("dealloc") == -1) {
// Hook it
//......
}
},
});
Now let’s see how the API Resolver finds C functions. Three formats:
format is: exports:*!open*, exports:libc.so!* or imports:notepad.exe!*
To find fopen:
var resolver = new ApiResolver('module');
resolver.enumerateMatches('exports:*!fopen*', {
onMatch: function(match) {
var name = match['name'];
var address = match['address'];
console.log(name + ":" + address);
},
onComplete: function() {}
});
Attach to SpringBoard and load it; the name includes the dynamic library path. Once you have the address, hook it with the Interceptor:
frida -U -l hook_objc.js SpringBoard
[iPhone::SpringBoard]->
/usr/lib/libSystem.B.dylib!fopen:0x1e145d4d0
/usr/lib/libSystem.B.dylib!fopen$DARWIN_EXTSN:0x1e1495d04
/usr/lib/system/libsystem_c.dylib!fopen$DARWIN_EXTSN:0x1e1495d04
/usr/lib/system/libsystem_c.dylib!fopen:0x1e145d4d0
/usr/lib/libSystem.B.dylib!fopen:0x1e145d4d0
/usr/lib/libSystem.B.dylib!fopen$DARWIN_EXTSN:0x1e1495d04
One more thing: beyond hooking with the Interceptor, Objective-C lets you hook by replacing the implementation — essentially Apple’s Method Swizzling. Hooking +[NSURL URLWithString:] this way:
var method = ObjC.classes.NSURL['+ URLWithString:'];
var origImp = method.implementation;
method.implementation = ObjC.implement(method, function (self, sel, url){
console.log("+ [NSURL URLWithString:]");
var urlString = ObjC.Object(url);
console.log("url: " + urlString.toString());
return origImp(self, sel, url); // Call the original method; skip this to prevent it from running
// Replace the argument: swap the URL for http://www.example.com
//var newUrl = ObjC.classes.NSString.stringWithString_("http://www.example.com");
//return origImp(self, sel, newUrl);
});
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