- foreword
- 1.ios hooking search source code analysis
- 2. ApiResolver searches all symbols in memory
- 3. Enumerate search for all classes/all methods/all overloads
- 4.hook all classes/all methods/all overloads
- 5. Output (modify) parse parameters/call stack/return value
- 6. Enumerate all modules/symbols/addresses in memory
- 7. All functions under the brainless automation hook application package
- 8. Objection memory roaming search all objects
- 9.ObjC.choose enumerates all class output properties
- 10. Actively call object methods to obtain algorithm execution results
- 11 Configure the RPC black box call algorithm for remote batch offline
foreword
Finally learned the part of Frida, in this article we will start from the source code of objection, an automated hook framework, and learn to use Frida's built-in ApiResolver scraper to enumerate all symbols in memory, including all classes/all methods/all heavy Load, and then hook it to output the return value of the parameter call stack, and modify the parameters and return value to achieve the purpose of modifying the function logic.
Hooking one function at a time is not the ultimate goal. It is really awesome to hook hundreds of functions at a time in batch automation. In this article, we also introduce a method to search all classes of App and hook them all, and use Frida Traverse the characteristics of the memory to search for ObjC objects, directly call methods, obtain the execution results of the algorithm, and finally configure RPC for remote batch offline calls, so that team members can still obtain the algorithm execution results even if they do not know the details of the algorithm implementation Purpose.
The attached APP, code, etc. are located in my project, and you can pick them up by yourself:
https://github.com/r0ysue/AndroidSecurityStudy
1.ios hooking search source code analysis
Here we go directly to github to download the source code of objection, and check how its hook for ios is written
The ios-related hook code in objection is on the agent–>src–>ios–>hooking.ts file. Here we mainly check how its search and watch are implemented.
const objcEnumerate = (pattern: string): ApiResolverMatch[] => {
return new ApiResolver('objc').enumerateMatches(pattern);
};
export const search = (patternOrClass: string): ApiResolverMatch[] => {
// if we didnt get a pattern, make one assuming its meant to be a class
if (!patternOrClass.includes('['))
patternOrClass = `*[*${patternOrClass}* *]`;
return objcEnumerate(patternOrClass);
};
Here, first check the search source code, you can see that the parameters we pass in are used as patternOrclass parameters, first check whether it is in the corresponding format, if not, complete it, and then use ApiResolverMatch to search the symbols in the memory to find the method to be hooked The address, the hook of ios and the hook of the so layer in android both directly hook the address, and then look at the watch method
export const watch = (patternOrClass: string, dargs: boolean = false, dbt: boolean = false,
dret: boolean = false, watchParents: boolean = false): void => {
// Add the job
// We init a new job here as the child watch* calls will be grouped in a single job.
// mostly commandline fluff
const job: IJob = {
identifier: jobs.identifier(),
invocations: [],
type: `ios-watch for: ${patternOrClass}`,
};
jobs.add(job);
const isPattern = patternOrClass.includes('[');
// if we have a patterm we'll loop the methods, hook and push a listener to the job
if (isPattern === true) {
const matches = objcEnumerate(patternOrClass);
matches.forEach((match: ApiResolverMatch) => {
watchMethod(match.name, job, dargs, dbt, dret);
});
return;
}
watchClass(patternOrClass, job, dargs, dbt, dret, watchParents);
};
When the watch parameter is used, it will be added to the jobs list and the watchClass method will be called at the same time
const watchClass = (clazz: string, job: IJob, dargs: boolean = false, dbt: boolean = false,
dret: boolean = false, parents: boolean = false): void => {
const target = ObjC.classes[clazz];
if (!target) {
send(`${c.red(`Error!`)} Unable to find class ${c.redBright(clazz)}!`);
return;
}
// with parents as true, include methods from a parent class,
// otherwise simply hook the target class' own methods
(parents ? target.$methods : target.$ownMethods).forEach((method) => {
// filter and make sure we have a type and name. Looks like some methods can
// have '' as name... am expecting something like "- isJailBroken"
const fullMethodName = `${method[0]}[${clazz} ${method.substring(2)}]`;
watchMethod(fullMethodName, job, dargs, dbt, dret);
});
};
It can be seen that in the watchClass method, first pass in the parameter clazz as a parameter and use api ObjC.classes[clazz] to obtain the class object, and then traverse the class methods to call the watchMethod method for method hooking.
const watchMethod = (selector: string, job: IJob, dargs: boolean, dbt: boolean,
dret: boolean): void => {
const resolver = new ApiResolver("objc");
let matchedMethod = {
address: undefined,
name: undefined,
};
// handle the resolvers error it may throw if the selector format is off.
try {
// select the first match
const resolved = resolver.enumerateMatches(selector);
if (resolved.length <= 0) {
send(`${c.red(`Error:`)} No matches for selector ${c.redBright(`${selector}`)}. ` +
`Double check the name, or try "ios hooking list class_methods" first.`);
return;
}
// not sure if this will ever be the case... but lets log it
// anyways
if (resolved.length > 1) {
send(`${c.yellow(`Warning:`)} More than one result for selector ${c.redBright(`${selector}`)}!`);
}
matchedMethod = resolved[0];
} catch (error) {
send(
`${c.red(`Error:`)} Unable to find address for selector ${c.redBright(`${selector}`)}! ` +
`The error was:\n` + c.red((error as Error).message),
);
return;
}
// Attach to the discovered match
// TODO: loop correctly when globbing
send(`Found selector at ${c.green(matchedMethod.address.toString())} as ${c.green(matchedMethod.name)}`);
const watchInvocation: InvocationListener = Interceptor.attach(matchedMethod.address, {
// tslint:disable-next-line:object-literal-shorthand
onEnter: function (args) {
// how many arguments do we have in this selector?
const argumentCount: number = (selector.match(/:/g) || []).length;
const receiver = new ObjC.Object(args[0]);
send(
c.blackBright(`[${job.identifier}] `) +
`Called: ${c.green(`${selector}`)} ${c.blue(`${argumentCount}`)} arguments` +
`(Kind: ${c.cyan(receiver.$kind)}) (Super: ${c.cyan(receiver.$superClass.$className)})`,
);
// if we should include a backtrace to here, do that.
if (dbt) {1
send(
c.blackBright(`[${job.identifier}] `) +
`${c.green(`${selector}`)} Backtrace:\n\t` +
Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join("\n\t"),
);
}
if (dargs && argumentCount > 0) {
const methodSplit = ObjC.selectorAsString(args[1]).split(":").filter((val) => val);
const r = methodSplit.map((argName, position) => {
// As this is an ObjectiveC method, the arguments are as follows:
// 0. 'self'
// 1. The selector (object.name:)
// 2. The first arg
//
// For this reason do we shift it by 2 positions to get an 'instance' for
// the argument value.
const t = new ObjC.Object(args[position + 2]);
return `${argName}: ${c.greenBright(`${t}`)}`;
});
send(c.blackBright(`[${job.identifier}] `) +
`Argument dump: [${c.green(receiver.$className)} ${r.join(" ")}]`);
}
},
onLeave: (retval) => {
// do nothing if we are not expected to dump return values
if (!dret) { return; }
send(c.blackBright(`[${job.identifier}] `) + `Return Value: ${c.red(retval.toString())}`);
},
});
job.invocations.push(watchInvocation);
};
Observing the code, we can find that the api Interceptor.attach of frida is used here to hook and judge whether to print out the parameters and call stack according to the incoming parameters dargs and dbt
2. ApiResolver searches all symbols in memory
First go to the official website to view the introduction of the api
You can see that the api supports module modules and objc and the return value is an object array containing the api name and address. Next, we use this api to write hook code
Supplement: Here we can see that the ObjC hook code does not have Java.perform() when it is written. The reason is that when Frida injects, it will automatically create a runtime environment for the injected program. OC's Runtime java's jvm, but because jvm is an independent The "library" is libart.so, so after frida is injected, it will hook this art.so and use its built-in API for java hooking, so what frida injects creates its own jvm virtual machine process, but we want to operate the application class or method will enter his jvm
setImmediate(() => {
const resolver = new ApiResolver('objc');
const matches = resolver.enumerateMatches('*[ViewController *]');
matches.forEach((match)=>{
console.log(JSON.stringify(match))
})
})
3. Enumerate search for all classes/all methods/all overloads
In fact, you can still use ApiResolver to search for all classes and all methods and overloads, you only need to modify the string matching the enumeration
setImmediate(() => {
const resolver = new ApiResolver('objc');
const matches = resolver.enumerateMatches('*[* *]');
matches.forEach((match)=>{
console.log(JSON.stringify(match))
})
})
Here you can see that all the api and attribute names and addresses have been enumerated and printed out
4.hook all classes/all methods/all overloads
First of all, we use objection to hook the following ViewController class
frida-ps -U 查看应用名
objection -g UnCrackable1 explore objection 注入应用进程
ios hooking watch class ViewController hook ViewController类
ios hooking list class_methods ViewController 列出所有方法签名
--include-parents是否包含父类方法
ios hooking watch method "*[ViewController buttonClick:]" --dump-args --dump-backtrace --dump-return
hook 相应方法
At this point, we want to check the specific location of the code in the objection. You can copy the prompt string printed out when the hook is found. Selector to search in the objection to see how to implement it
The search found that the prompt string is printed in the method watchMethod. The specific hook implementation logic has also been mentioned above. In fact, it is watchClass–>watchMethod, and then use the Interceptor.attach API to hook the method address in watchMethod. Let’s go to the official website to see Take a look at the simple use of this api
const resolver = new ApiResolver('objc');
const matches = resolver.enumerateMatches('*[* isEqualToString:*]');
matches.forEach((match) => {
console.log(JSON.stringify(match))
Interceptor.attach(match.address,{
onEnter:function(args){
},onLeave:function(ret){
}
})
})
Looking at the code, we can see that we use ApiResolver to enumerate characters and use JSON.stringify(match) to convert the convenient method into a string and print it out. The reason for the conversion here is also mentioned above, because the object array is obtained. This The object contains the method name and address attributes, and then use Interceptor.attach its match.address to hook, onEnter is before the original method is executed, onLeave is after the method is executed, args is the method execution parameters, and ret is the return value. You can name it yourself
5. Output (modify) parse parameters/call stack/return value
① Modify the return value to achieve the purpose
Here we look at the demo source code and we can see that it compares the input string with the hidden string to get the result
So we directly hook the return value of the isEqualToString method to make it return correctly
setImmediate(() => {
console.log("hello world!r0ysue! objc =>", ObjC.available)
const resolver = new ApiResolver('objc');
const matches = resolver.enumerateMatches('*[* isEqualToString:*]');
matches.forEach((match) => {
console.log(JSON.stringify(match))
Interceptor.attach(match.address,{
onEnter:function(args){
this.change = false;
if(receiver.toString().indexOf("aaaabbbb") >= 0){
this.change = true;
console.log("need change");
}
},onLeave:function(ret){
console.log("ret=>",ret)
if(this.change){
ret.replace(new NativePointer(0x1))
}
}
})
})
})
Here we directly modify the return value to true in the code to make the return result correct. Here are two points to note:
(1) ret is a reference object, if we want to modify its value, we need to use the official replace method, as shown above.
(2) The parameters and return values in Interceptor.attach are both pointers, even if it is a numeric type, we need to use ptr() to convert it to a pointer type assignment
② Modify the parameters to achieve the purpose
setImmediate(() => {
// console.log("hello world!r0ysue! objc =>", ObjC.available)
const resolver = new ApiResolver('objc');
const matches = resolver.enumerateMatches('*[* isEqualToString:*]');
matches.forEach((match) => {
console.log(JSON.stringify(match))
Interceptor.attach(match.address,{
onEnter:function(args){
const receiver = new ObjC.Object(args[0])
console.log("receiver is =>",receiver.$className, " =>",receiver.toString());
if(receiver.toString().indexOf("aaabbb") >= 0){
const { NSString } = ObjC.classes;
var newString = NSString.stringWithString_("aaabbb");
args[2] = newString;
}
},onLeave:function(ret){
}
})
})
It is still to analyze the demo case and modify the parameters of the isEqualToString method to make it true. There are two points to note here:
(1) We said in the ObjC basic grammar above that the message mechanism in the OC language will be converted into objc_msgSend (receiver, selector, arg1, arg2, ...) to call, that is to say, the first parameter when the method is called is the caller itself , the second parameter is the selector, which is the name of the method, and the third and subsequent parameters are its real parameters. Here we can see that we have obtained the caller through const receiver = new ObjC.Object(args[0]) itself and convert it into an OC object, after converting it into an OC object, we can call the api of frida, as shown on the official website
We can use a series of APIs to get its class name, method, etc. based on this object
(2) We want to assign a pointer to the parameter, so we create a new NSString to assign it. This is also the implementation found on the official website
③ View the call stack
Printing the call stack is still directly using the api provided by the official website
console.log('CCCryptorCreate called from:\n' +
Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join('\n') + '\n');
There are still a few parsing APIs used here:
jsonStringfy: Convert js objects to strings in js
tostring : convert the object to a string
objc.object: converted to OC object
④ Replacement function
The above modified parameter return value is based on the fact that we want the function to be executed, but sometimes we may not want the method to be executed, then we can use the following method to directly replace its execution
setImmediate(() => {
const ViewController = ObjC.classes.NSString;
const buttonClick = ViewController['- buttonClick:'];
const oldImpl = buttonClick.implementation;
buttonClick.implementation = ObjC.implement(buttonClick,(handle, selector,args)=>{
console.log("handle selector args =>",handle,selector,args)
console.log(Thread.backtrace(this.AudioContext,Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('\n\t'))
oldImpl(handle,selector,args);
})
})
You can see that the above code saves the method address in oldImpl in advance and then uses ObjC.implement to replace the method. Whether to execute the original method inside can be implemented by yourself
6. Enumerate all modules/symbols/addresses in memory
frida-ps -Ua 查看运行的应用
objection -g UnCrackable1 explore objection注入应用进程
memory list modules 列举内存中加载的模块
memory list exports UnCrackable\ Level\ 1 列出模块中的导出符号
memory list exports Foundation
Let's take a look at how objection is implemented. The enumeration module and the exported symbols in the enumeration module. The code is in agent–>generic–>memory.ts
export const listModules = (): Module[] => {
return Process.enumerateModules();
};
export const listExports = (name: string): ModuleExportDetails[] | null => {
const mod: Module[] = Process.enumerateModules().filter((m) => m.name === name);
if (mod.length <= 0) {
return null;
}
return mod[0].enumerateExports();
};
It can be seen that the implementation logic is actually very simple, that is, use frida's api Process.enumerateModules() to get the array of module objects, and then call enumerateExports() according to the module objects to get the exported symbols. The enumeration of symbol addresses related to c in Android is actually Similarly, implement the code yourself as follows:
function listExports(name) {
//①可以在枚举模块是保存模块的name属性作为参数然后作为参数给Module.enumerateExports(name)
// var exporttArr = Module.enumerateExports(name);
//②也可以在枚举模块是保存模块对象当作参数,然后用模块对象调用name.enumerateExports()
var exporttArr = name.enumerateExports();
// var exporttArr = name.enumerateSymbols();
console.log("枚举模块==》",name.name)
for(var i = 0;i<exporttArr.length;i++){
console.log("exports: ",exporttArr[i].name,"----address:",exporttArr[i].address)
}
}
function listModules(){
var modules = Process.enumerateModules()
for(var i = 0;i<modules.length;i++){
// console.log("moulename =>",modules[i].name,"--address:",JSON.stringify(modules[i].base))
console.log("=======================================")
console.log("模块名称:",JSON.stringify(modules[i].name));
console.log("模块地址:",JSON.stringify(modules[i].base));
console.log("大小:",JSON.stringify(modules[i].size));
console.log("文件系统路径",JSON.stringify(modules[i].path));
}
return modules[0]
}
function main(){
var modulename = listModules();
console.log(modulename)
listExports(modulename)
}
setImmediate(main)
7. All functions under the brainless automation hook application package
For all functions under the hook application package, we can search for relevant cases on the Internet to see how to write them
function get_timestamp()
{
var today = new Date();
var timestamp = today.getFullYear() + '-' + (today.getMonth()+1) + '-' + today.getDate() + ' ' + today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds() + ":" + today.getMilliseconds();
return timestamp;
}
function hook_class_method(class_name, method_name)
{
var hook = eval('ObjC.classes.'+class_name+'["'+method_name+'"]');
Interceptor.attach(hook.implementation, {
onEnter: function(args) {
console.log("[*] [" + get_timestamp() + " ] Detected call to: " + class_name + " -> " + method_name);
}
});
}
function run_hook_all_methods_of_classes_app_only()
{
console.log("[*] Started: Hook all methods of all app only classes");
var free = new NativeFunction(Module.findExportByName(null, 'free'), 'void', ['pointer'])
var copyClassNamesForImage = new NativeFunction(Module.findExportByName(null, 'objc_copyClassNamesForImage'), 'pointer', ['pointer', 'pointer'])
var p = Memory.alloc(Process.pointerSize)
Memory.writeUInt(p, 0)
var path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String()
var pPath = Memory.allocUtf8String(path)
var pClasses = copyClassNamesForImage(pPath, p)
var count = Memory.readUInt(p)
var classesArray = new Array(count)
for (var i = 0; i < count; i++)
{
var pClassName = Memory.readPointer(pClasses.add(i * Process.pointerSize))
classesArray[i] = Memory.readUtf8String(pClassName)
var className = classesArray[i]
if (ObjC.classes.hasOwnProperty(className))
{
//console.log("[+] Class: " + className);
//var methods = ObjC.classes[className].$methods;
var methods = ObjC.classes[className].$ownMethods;
for (var j = 0; j < methods.length; j++)
{
try
{
var className2 = className;
var funcName2 = methods[j];
//console.log("[-] Method: " + methods[j]);
hook_class_method(className2, funcName2);
//console.log("[*] [" + get_timestamp() + "] Hooking successful: " + className2 + " -> " + funcName2);
}
catch(err)
{
console.log("[*] [" + get_timestamp() + "] Hooking Error: " + err.message);
}
}
}
}
free(pClasses)
console.log("[*] Completed: Hook all methods of all app only classes");
}
function hook_all_methods_of_classes_app_only()
{
setImmediate(run_hook_all_methods_of_classes_app_only)
}
hook_all_methods_of_classes_app_only()
It can be seen that the main implementation logic of the script is roughly as follows:
① Obtain all classes in the run_hook_all_methods_of_classes_app_only method. The specific acquisition method is to use copyClassNamesForImage, a private API in the Objective-C runtime, to obtain all class names defined in the current App, and save them in a memory array
② Then traverse the class methods, and use the js API of hasOwnProperty to judge whether the class belongs to the current app, and if so, traverse the methods in the class
③Use the hook_class_method method for the actual hook of each method
8. Objection memory roaming search all objects
First use objection to search for the ViewController object
ios heap search instances ViewController
Then we go to the objection code to check its specific implementation, the location is agent–>src–>ios–>heap.ts
const enumerateInstances = (clazz: string): ObjC.Object[] => {
if (!ObjC.classes.hasOwnProperty(clazz)) {
c.log(`Unknown Objective-C class: ${c.redBright(clazz)}`);
return [];
}
const specifier: ObjC.DetailedChooseSpecifier = {
class: ObjC.classes[clazz],
subclasses: true, // don't skip subclasses
};
return ObjC.chooseSync(specifier);
};
export const getInstances = (clazz: string): IHeapObject[] => {
c.log(`${c.blackBright(`Enumerating live instances of`)} ${c.greenBright(clazz)}...`);
return enumerateInstances(clazz).map((instance): IHeapObject => {
try {
return {
className: instance.$className,
handle: instance.handle.toString(),
ivars: instance.$ivars,
kind: instance.$kind,
methods: instance.$ownMethods,
superClass: instance.$superClass.$className,
};
} catch (err) {
c.log(`Warning: ${c.yellowBright((err as Error).message)}`);
}
});
};
It can be seen that in the end, ObjC.chooseSync, an api provided by frida, is actually called to search for objects
The reason why ObjC.chooseSync is used here is because ObjC.choose
is an asynchronous API but ObjC.chooseSync
a synchronous API, if we need to quickly enumerate a large number of objects, and we can accept that some objects may be missed, then we can use it ObjC.choose
. But if we need to guarantee that all objects are enumerated, and slower speed is acceptable, calling is ObjC.chooseSync
more suitable.
9.ObjC.choose enumerates all class output properties
When we use ObjC.choose to enumerate to the actual instance, we can directly print the properties of this instance. Frida official website also provides these methods
The implementation code is as follows:
setImmediate(() => {
if(ObjC.available){
const specifier = {
class: ObjC.classes['ViewController'],
subclasses: true,
};
ObjC.choose(specifier,{
onMatch:function(ins){
console.log("found ins =>",ins)
console.log("ivars =>",ins.$ivars["_theLabel"].toString())
console.log("methods =>",ins.$ownMethods)
},onComplete(){
console.log("Search Completed")
}
})
}
})
You can see that after we enumerate the object ins, we can directly call its $ivars to get the attribute object, and use toString() to convert it into a string and print it out
10. Actively call object methods to obtain algorithm execution results
Now view how to actively call in objection
ios heap search instances ViewController
ios heap print methods 0x15dd08220
ios heap execute 0x15dd08220 theLabel --return-string
It can be seen that these methods are all an object, but in the demo, the do_it method is a function implemented in c, and there is no object. The calling code at this time is as follows:
function callcmethod(name){
var doit = new NativeFunction(Module.findExportByName(null, name),'pointer', [])
console.log("doit result => ",doit().readCString())
}
function main(){
// var modulename = listModules();
// console.log(modulename)
// listExports(modulename)
callcmethod('do_it')
}
setImmediate(main)
Specifically, it can be described as the following steps:
① Call the previously written enumeration module and the list of characters everywhere in the module to view the method to export characters
② Call Module.findExportByName(null, name) to export characters as the second parameter, and the first parameter is the module name to get the function address
③ Call frida's api NativeFunction(). The api accepts three parameters to generate a js function object. The three parameters are function address, native function return value type, and native function parameter type array
④ Executing the js function object generated above calls the original function
11 Configure the RPC black box call algorithm for remote batch offline
The js code is as follows:
function callSecretFunc(){
return new NativeFunction(Module.findExportByName(null, 'do_it'),'pointer', [])().readCString()
}
rpc.exports={
callSecretFunction:callSecretFunc
}
Manual hook injection process call test
The execution is successful, indicating that the export is successful
The py code is as follows:
import time
import frida
def my_message_handler(message, payload):
print(message)
print(payload)
device = frida.get_usb_device()
#frida.get_usb_device() 相当于手工注入式的 -U参数 通过USB获取设备
#device.get_frontmost_application() 相当于-F 获取当前页面进程
#创建一个session对象准备注入
session = device.attach(device.get_frontmost_application().pid)
#读取js文件并通过session开始注入
with open("ios2.js") as f :
script = session.create_script(f.read())
script.on("message", my_message_handler)
script.load()
command = ""
while 1 ==1 :
command = input("Enter Command:")
if command == "1":
break
elif command == "2":
print(script.exports.callSecretFunctionon())
as follows:
import time
import frida
def my_message_handler(message, payload):
print(message)
print(payload)
device = frida.get_usb_device()
#frida.get_usb_device() 相当于手工注入式的 -U参数 通过USB获取设备
#device.get_frontmost_application() 相当于-F 获取当前页面进程
#创建一个session对象准备注入
session = device.attach(device.get_frontmost_application().pid)
#读取js文件并通过session开始注入
with open("ios2.js") as f :
script = session.create_script(f.read())
script.on("message", my_message_handler)
script.load()
command = ""
while 1 ==1 :
command = input("Enter Command:")
if command == "1":
break
elif command == "2":
print(script.exports.callSecretFunctionon())
[External link image transfer...(img-wIWHhJwN-1690441675043)]
The above is the whole content of this article, and the next article will continue to learn how frida adds ios trace function to r0tracer.