πŸ’‰ Quick Start with Frida to Reverse-Engineer Any iOS Application

How to start with reverse-engineering of iOS application using the Frida toolkit. Tracing network communication and filesystem requests of a third-party iOS application. Tips and tricks.

πŸ’‰ Quick Start with Frida to Reverse-Engineer Any iOS Application

Quick Start

If you don't know what is Frida, skip the "Quick Start" and move forward to the next chapter: "What Frida Is".

Basically, you have to install frida-server on a jailbroken iPhone and frida-tools on a personal computer using instructions from the official site: https://www.frida.re/docs/installation/. Then follow these 3 steps:

1. Find the running application on the iOS device

$ frida-ps -U | grep -i targetapp
3688  TargetApp

You can actively use grep -i, where -i stands for case insensitive mode.

2. Start tracing the iOS process

$ frida-trace -i CCCrypt -U TargetApp
CCCrypt: Auto-generated handler: __handlers__/libSystem.B.dylib/CCCrypt.js

$ frida-trace -m "-[NSFileManager *System*]" -U 3688 # pid
fileSystemRepresentationWithPath: Auto-generated handler: __handlers__/__NSFileManager_getFileSystemRep_6756a454.js
  • -i is to hook C function
  • -m is to hook Objective-C function
  • -U stands for "USB" (find device attached by USB)
  • You can use either pid or application name retrieved from the frida-ps -U output

3. Modify auto-generated JS scripts

# "code" stands for Visual Studio Code
$ code __handlers__\libSystem.B.dylib\CCCrypt.js

frida-trace generates JavaScript templates which you should modify further. For example, __handlers__\libSystem.B.dylib\CCCrypt.js has been generated. So you can open it and modify.

βœ”οΈ What Frida Is

Frida is a dynamic code instrumentation toolkit. Frida allows you to write tweaks in JavaScript:

  1. to catch function invocation
  2. to print and modify incoming arguments
  3. to print and modify returns
  4. to inject your own code before and after function invocation

❌ What Frida Is Not

  1. Frida is not a debugger.
    So you cannot modify executable code in runtime, pause process execution, change opcodes, etc.
  2. Frida is not a disassembler.
    Frida doesn't stand for a static analysis, it is about dynamic analysis.

How Frida is Implemented

  1. Frida injects Google’s V8 engine into a targeted process.
  2. JS runs inside the targeted process (thanks to injected engine).
  3. You write JS tweaks which are injected into the targeted process.

Basic Workflow on iOS with Jailbreak

If one process needs to get attached to another in order to "trace one" or to get "injected into" - jailbreak is implied. Because it is all about superuser privileges.

  1. Take your iPhone. πŸ“±
  2. Take a 3rd party application. πŸ“
  3. Jailbreak your iPhone. πŸ”¨ (This is legal).
  4. Install Frida (server) to the jailbroken iPhone. πŸ“²
  5. Install Frida (client) to your computer. πŸ’»
  6. Trace a third party application with Frida command-line tools. πŸ‘¨β€πŸ’»
    6.1. frida-ps
    6.2. frida-trace
  7. Trace functions, print data, tweak the execution on-the-fly. πŸ“±πŸšΆ

There is also an option to use a dynamic linker feature like LD_PRELOAD or DYLD_INSERT_LIBRARIES to inject Frida’s Gadget into your application. It needs superuser privileges (jailbreak).

Workflow on iOS Without a Jailbreak

Without Jailbreak you need to embed Frida’s Gadget into your application. (See how to inject Frida Gadget in Android application).

Case 1. Your own application

Add a frida-gadget shared library into your project. Then load the library. Frida server is bringed up in the address space of the process, and jailbreak is not needed in such case.

Case 2. A third party application

Patch the application binary or one of its libraries, e.g. by using a tool like insert_dylib.

Want to Trace? What to Trace?

If you don't know where to start from, always start from a threat model. (See my other article about threat modeling of mobile application).

You can imagine application as a black box that interacts with the outer world via inputs and outputs. Ordinary mobile application has following simplified threat model. Β 

Particularly, filesystem and network communication are pretty interesting interaction points which can give you a lot of useful information. These point are the

Trace Everything

$ frida-discover -U TargetApp

frida-discover could be useful to get at least some idea where to start from. A small part of frida-discover output:

libcommonCrypto.dylib
        Calls           Function
        15              sub_bec0
        15              sub_bee4
        8               sub_96f8
        8               sub_befc
        3               CCDigest

Caveats

  1. Application UI is decently lugging while being traced, because frida-discover collects statistics about a huge amount of API.
  2. Don't rely on frida-discover too much: often important calls are missed in the report, e.g. CCCrypt in the example above. The report is a good thing to get some ideas where to start from, kind of "libcorecrypto.dylib is in the report therefore cryptography is used", but then you need to carefully research a particular scope.

Tracing Filesystem

  • Print each file that have been open in runtime.
$ frida-trace -m "-[NSFileManager fileExistsAtPath:]" -U TargetApp

-[NSFileManager fileExistsAtPath:]: Loaded handler at ".\__handlers__\__NSFileManager_fileExistsAtPath__.js"
  • Modify auto-generated tweak.
# Visual Studio Code
$ code __handlers__/__NSFileManager_fileExistsAtPath__.js
  • Print the path.
onEnter: function (args) {
    log('open' , ObjC.Object(args[2]).toString());
}
__NSFileManager_fileExistsAtPath__.js

args[2] stands for the 1st argument NSString *path of the fileExistsAtPath. To print NSString object as a string the JavaScript object args[2] must be wrapped into Objective-C object: ObjC.Object(args[2]).

  • Re-run frida-trace again. You're going to see the following output:
 11230 ms  -[NSFileManager fileExistsAtPath:/var/.../somefile isDirectory:0x16b0f296f]
           /* TID 0x7b03 */
 11807 ms  -[NSFileManager fileExistsAtPath:/var/.../SC_Info]
  • What should you do next:
  1. Install sshd (SSH Daemon) on the iPhone.
  2. Copy file to your computer via ssh:
$ scp root@192.168.77.102:/path/to/printed/file ./
Password: alpine

Tracing the Network (HTTP/ S)

NSURLSession is widely used to communicate with a server (e.g. REST API) in iOS applications.

  • Let's trace it.
$ frida-trace -m "-[NSURLSession dataTaskWithRequest*]" -U <pid/name>

It will generate 2 handlers: for a function with completionHandler and without.

-[NSURLSession dataTaskWithRequest:]: Loaded handler at "__handlers__/__NSURLSession_dataTaskWithRequest__.js"
-[NSURLSession dataTaskWithRequest:completionHandler:]: Loaded handler at "__handlers__/__NSURLSession_dataTaskWithReque_bbc2edc2.js"
  • Modify auto-generated tweaks.
onEnter: function (log, args, state) {
    var request = new ObjC.Object(args[2]);
    log('-[NSURLSession dataTaskWithRequest:' + args[2] + ' completionHandler:' + args[3] + ']');
    log(' --- URL:');
    log(request.URL().toString());
    log(' --- headers:');
    log(request.allHTTPHeaderFields().toString());
    log(' --- body:');
    if (request.HTTPBody()) log(request.HTTPBody().toString());
    log(' --- method:');
    log(request.HTTPMethod().toString());
  }
__NSURLSession_dataTaskWithReque_bbc2edc2.js
  • Re-run frida-trace again. Then you will see the following output:
           /* TID 0xa933 */
  7952 ms  -[NSURLSession dataTaskWithRequest:0x2836ea5c0 completionHandler:0x16b17ebd8]
  7952 ms   --- URL:
  7952 ms  https://targetapp.com/v1/some/api
  7952 ms   --- headers:
  7952 ms  {
    "Content-Type" = "application/octet-stream";
}
  7952 ms   --- body:
  7952 ms  <793d8f2f c103e547 ....>
  7952 ms   --- method:
  7952 ms  POST

Tracing Encryption

If you have any suspicion about encryption used inside the application to hide the contents (files, network protocol), try to catch CCCrypt function, or anything else from CommonCrypto.h.

$ frida-trace -i CCCrypt -U TargetApp

Modify auto-generated tweak.

  onEnter: function (log, args, state) {
    log('CCCrypt()');
    log('  ----- algo:' + args[1].toInt32());
    log('  ----- key:');
    log(hexdump(args[3], {length: args[4].toInt32()}));
    log('  ----- in iv:');
    log(hexdump(args[5], { length: 16 }));
    log('  ----- in data:');
    log(hexdump(args[6], {length: args[7].toInt32()}));
    this.args = args;
  },

  onLeave: function (log, retval, state) {
    log('  ----- out data:');
    log(hexdump(this.args[8], {length: this.args[9].toInt32()}));
  }
libcommonCrypto.dylib\CCCrypt.js

Decrypted/encrypted data appears in the onLeave function.

You can see the trick here: arguments are not accessible in the onLeave handler, and the way to have arguments there is to pass them via this pointer.

this.args = args;

Other Tweaks

There is a decent amount of JS tweaks for Frida: https://github.com/iddoeldor/frida-snippets. You can take a look to get more insights what you can in your investigations.


Tips and Tricks

  1. In the most cases you don't know exactly function used in the traced application. I would suggest using wildcard instead of fully qualified prototype to catch several API functions.
    Use wildcard
    frida-trace \
      -m "-[NSURLSession dataTaskWithRequest*]" \
      -U TargetApp
    
    instead of
    frida-trace \
        -m "-[NSURLSession dataTaskWithRequest:]" \
        -m "-[NSURLSession dataTaskWithRequest:completionHandler:]" \
        -U TargetApp
    
  2. Don't forget that JavaScript tweak receives JavaScript objects as input. Therefore in order to access methods and fields of Obective-C object represented by JavaScript object the Objective-C wrapper must be used:
    // Wrap
    var request = new ObjC.Object(args[2]);
    // Access
    request.toString();
    request.URL().toString();
    
  3. onLeave callback doesn't have an access to the arguments of onEnter callback. Often a traced function returns data via the output parameters, as in a case of CCCrypt. In order to print a result when the function quits, you can pass arguments from onEnter to onLeave via this pointer:
    onEnter: function (log, args, state) {
      this.args=args;
    },
    
    onLeave: function (log, retval, state) {
      log(this.args[2].toInt32());
    }