Frida hook/invoke iOS and memory search and black box call

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

6

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))
    })
})

7

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))
    })
})

8

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类

9

ios hooking list class_methods ViewController 列出所有方法签名
 --include-parents是否包含父类方法

10

ios hooking watch method "*[ViewController buttonClick:]" --dump-args --dump-backtrace --dump-return
hook 相应方法

11

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

12

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

13

    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

14

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

15

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

16

③ 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');

17

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 列举内存中加载的模块

18

memory list exports UnCrackable\ Level\ 1 列出模块中的导出符号
memory list exports Foundation

19

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

20

8. Objection memory roaming search all objects

First use objection to search for the ViewController object

ios heap search instances ViewController

21

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

22

The reason why ObjC.chooseSync is used here is because ObjC.chooseis an asynchronous API but ObjC.chooseSynca 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.chooseSyncmore 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 methods23

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

24

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

25

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())

26

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.

Guess you like

Origin blog.csdn.net/u010559109/article/details/131960707