Dynamic Reverse Engineering with Frida

Debuggee

Debugger

Debuggee

Debugger

bootstrapper

Debuggee

Debugger

bootstrapper

bootstrapper-thread

Debuggee

Debugger

bootstrapper

bootstrapper-thread

frida-agent.so

Debuggee

Debugger

bootstrapper

bootstrapper-thread

frida-agent.so

Comm. Channel

Debuggee

Debugger

bootstrapper

bootstrapper-thread

frida-agent.so

Comm. Channel

JavaScript

Motivation

  • Existing tools often not a good fit for the task at hand
  • Creating a new tool usually takes too much effort
  • Short feedback loop: reversing is an iterative process
  • Use one toolkit for multi-platform instrumentation
  • Future remake of oSpy (see below)

What is Frida?

  • Dynamic instrumentation toolkit
    • Debug live processes
  • Scriptable
    • Execute your own debug scripts inside another process
  • Multi-platform
    • Windows, Mac, Linux, iOS, Android, QNX
  • Highly modular, JavaScript is optional
  • Open Source

Why would you need Frida?

  • For reverse-engineering
  • For programmable debugging
  • For dynamic instrumentation
  • But ultimately: To enable rapid development of new tools for the task at hand

Let's explore the basics

1) Build and run a simple program that calls f(n) every second with n increasing with each call.

2) Let's figure out what n is.

Frida has a REPL. Let's use it.

It live-reloads!

3) Let's modify what n is. How about +9000?

4) Let's speed up time.

5) Let's call f() ourselves.

6) rpc, send() and recv().

Let's see what files Telegram open()s on macOS

Let's try interacting with Objective-C

Let's figure out who is calling open().

Let's inspect registers.

Let's try frida-gadget.

Let's explore early instrumentation.

Injecting errors

$ node app.js Spotify
connect() family=2 ip=78.31.9.101 port=80
  blocking!
connect() family=2 ip=193.182.7.242 port=80
  blocking!
connect() family=2 ip=194.132.162.4 port=443
  blocking!
connect() family=2 ip=194.132.162.4 port=80
  blocking!
connect() family=2 ip=194.132.162.212 port=80
  blocking!
connect() family=2 ip=194.132.162.196 port=4070
  blocking!
connect() family=2 ip=193.182.7.226 port=443
  blocking!
'use strict';

const AF_INET = 2;
const AF_INET6 = 30;
const ECONNREFUSED = 61;

['connect', 'connect$NOCANCEL'].forEach(funcName => {
  const connect = new NativeFunction(
    Module.findExportByName('libsystem_kernel.dylib', funcName),
    'int',
    ['int', 'pointer', 'int']);
  Interceptor.replace(connect, new NativeCallback((socket, address, addressLen) => {
    const family = Memory.readU8(address.add(1));
    if (family == AF_INET || family == AF_INET6) {
      const port = (Memory.readU8(address.add(2)) << 8) | Memory.readU8(address.add(3));

      let ip = '';
      if (family == AF_INET) {
        for (let offset = 4; offset != 8; offset++) {
          if (ip.length > 0)
            ip += '.';
          ip += Memory.readU8(address.add(offset));
        }
      } else {
        for (let offset = 8; offset !== 24; offset += 2) {
          if (ip.length > 0)
            ip += ':';
          ip += toHex(Memory.readU8(address.add(offset))) +
              toHex(Memory.readU8(address.add(offset + 1)));
        }
      }

      console.log('connect() family=' + family + ' ip=' + ip + ' port=' + port);
      if (port === 80 || port === 443 || port === 4070) {
        console.log('  blocking!');
        this.errno = ECONNREFUSED;
        return -1;
      } else {
        console.log('  accepting!');
        return connect(socket, address, addressLen);
      }
    } else {
      return connect(socket, address, addressLen);
    }
  }, 'int', ['int', 'pointer', 'int']));

  send('ready');
});

function toHex(v) {
  let result = v.toString(16);
  if (result.length === 1)
    result = '0' + result;
  return result;
}

All calls between two recv() calls

'use strict';

const co = require('co');
const frida = require('frida');
const load = require('frida-load');

let session, script;
co(function *() {
  session = yield frida.attach(process.argv[2]);
  const source = yield load(
      require.resolve('./agent.js'));
  script = yield session.createScript(source);
  script.events.listen('message', message => {
    if (message.type === 'send') {
      const stanza = message.payload;
      switch (stanza.name) {
        case '+ready':
          console.log('Waiting for application to call recv()...');
          break;
        case '+result': {
          console.log('Results received:');
          const events = stanza.payload.events;
          events.forEach(ev => {
            const location = ev[0];
            const target = ev[1];
            const depth = ev[2];
            let indent = '';
            for (let i = 0; i !== depth; i++)
              indent += '   | ';
            console.log('\t' + indent + location + '\tCALL ' + target);
          });
          session.detach();
          break;
        }
      }
    } else {
      console.log(message);
    }
  });
  yield script.load();
});
$ node app.js Spotify
Waiting for application to call recv()...
Results received:
	0x119875dc7	CALL 0x119887527
	0x119875e7	CALL 0x11989a1e6
	0x1197f4df	CALL 0x11992f934
	0x1197f4f3	CALL 0x1197edd7d
	0x7fff8acdf6ad	CALL 0x7fff8ace32dc
	   | 0x7fff95355059	CALL 0x7fff9535c08b
	0x7fff937774be	CALL 0x7fff9375d5a0
	   | 0x7fff9376e76	CALL 0x7fff93788d6e
	   | 0x7fff9376e722	CALL 0x7fff93788d2c
	   |    | 0x7fff8d1e9754	CALL 0x7fff8d1e721
	   |    | 0x7fff8d1e9765	CALL 0x7fff8d1e721
	   |    | 0x7fff8d1e9421	CALL 0x7fff8d1e955c
	   |    |    | 0x7fff8d1e95bf	CALL 0x7fff8d1e747
	   |    |    |    | 0x7fff8d1e7417	CALL 0x7fff8d203db4
	   |    |    | 0x7fff8d1e95eb	CALL 0x7fff8d203db4
	0x7fff9377752c	CALL 0x7fff93788d9e
	   | 0x7fff8d1ed7c8	CALL 0x7fff8d1e721
	0x7fff9377754e	CALL 0x7fff93788b5e
	   | 0x7fff8acdfd10	CALL 0x7fff8acdec91
	   |    | 0x7fff8acded53	CALL 0x7fff8ace32e2
	   |    |    | 0x7fff95352182	CALL 0x7fff95353620
	   |    |    |    | 0x7fff95353663	CALL 0x14ce8580
	   |    |    |    |    | 0x14ce858a	CALL 0x7fff9535bb30
	   |    |    |    |    |    | 0x7fff9535bb4e	CALL 0x7fff9535bb71
	   |    |    |    |    |    |    | 0x7fff9535bbe0	CALL 0x7fff9536a46
	   | 0x7fff8acdfd20	CALL 0x7fff8ace3348
	   | 0x7fff8acdfd48	CALL 0x7fff8acde877
	   |    | 0x7fff8acde8ce	CALL 0x7fff8ace32e2
	   |    |    | 0x7fff95352182	CALL 0x7fff95353620
	   |    |    |    | 0x7fff95353663	CALL 0x14ce8580
	   |    |    |    |    | 0x14ce858a	CALL 0x7fff9535bb30
	   |    |    |    |    |    | 0x7fff9535bb4e	CALL 0x7fff9535bb71
	   |    |    |    |    |    |    | 0x7fff9535bbe0	CALL 0x7fff9536a46
	   |    | 0x7fff8acde8e1	CALL 0x7fff8ace32c4
	   |    | 0x7fff8acde923	CALL 0x7fff8acdd68f
	   |    |    | 0x7fff8acdd6ad	CALL 0x7fff8ace333c
	   |    |    |    | 0x7fff968e0ef	CALL 0x7fff969637a6
	   |    |    | 0x7fff8acdd6b5	CALL 0x7fff8ace3348
	   | 0x7fff8acdfd60	CALL 0x7fff8acdd5d4
	   | 0x7fff8acdfd6b	CALL 0x7fff8acdd5d4
	   | 0x7fff8acdfd76	CALL 0x7fff8acdd5d4
	   | 0x7fff8acdfd81	CALL 0x7fff8acdd68f
	   |    | 0x7fff8acdd6ad	CALL 0x7fff8ace333c
	   |    |    | 0x7fff968e0ef	CALL 0x7fff969637a6
	   |    | 0x7fff8acdd6b5	CALL 0x7fff8ace3348
	   | 0x7fff8acdfd8d	CALL 0x7fff8acdf12
	   |    | 0x7fff8acdf1ac	CALL 0x7fff8ace32b8
	   |    | 0x7fff8acdf1ed	CALL 0x7fff8ace32a6
	   |    | 0x7fff8acdf3c	CALL 0x7fff8acdd779
	   |    | 0x7fff8acdf322	CALL 0x7fff8acde966
	   |    |    | 0x7fff8acde994	CALL 0x7fff8ace332a
	   |    | 0x7fff8acdf4e5	CALL 0x7fff8ace32a0
	   |    | 0x7fff8acdf4f2	CALL 0x7fff8ace3234
	   |    | 0x7fff8acdf618	CALL 0x7fff8ace329a
	   |    | 0x7fff8acdf639	CALL 0x7fff8acde2f3
	   |    |    | 0x7fff8acde33d	CALL 0x7fff8ace3324
	   |    |    |    | 0x7fff587ab51	CALL 0x1197f279
	   |    |    |    |    | 0x1197f29a	CALL 0x11992f934
	   |    |    |    |    | 0x1197f2ae	CALL 0x1197f145
	   |    |    |    |    | 0x1197f2ee	CALL 0x1197edd7d
	   |    |    |    |    | 0x1197f3b5	CALL 0x11938fd9c
	   |    |    |    |    |    | 0x1193982ee	CALL 0x1197f884
	   |    |    |    |    |    | 0x1193982f5	CALL 0x119397261
	   |    |    |    |    |    | 0x11939835	CALL 0x1197f89f
	   |    |    |    |    |    | 0x119398315	CALL 0x1197f890
	   |    |    |    |    |    | 0x119398335	CALL 0x11939dc8a
	   |    |    |    |    |    | 0x119398364	CALL 0x1193a7710
	   |    |    |    |    |    | 0x119398380	CALL 0x119398482
	   |    |    |    |    |    | 0x1193983a0	CALL 0x1193a7710
	   |    |    |    |    |    | 0x1193983a8	CALL 0x1193aed20
	   |    |    |    |    |    | 0x1193983b8	CALL 0x1193afea0
	   |    |    |    |    |    | 0x1193983d4	CALL 0x1193af70
	   |    |    |    |    |    | 0x1193983df	CALL 0x1193984f2
	   |    |    |    |    |    | 0x1193983f0	CALL 0x11992f8e0
	   |    |    |    |    |    | 0x11939849	CALL 0x1193a7520
	   |    |    |    |    |    | 0x119398419	CALL 0x1197f8ab
	   |    |    |    |    |    | 0x119398428	CALL 0x11939def0
$
'use strict';

const WAITING = 1;
const STALKING = 2;
const COLLECTING = 3;
const DONE = 4;

let state = WAITING;
let stalkedThreadId = null;
let blobs = [];

['recv', 'recv$NOCANCEL'].forEach(funcName => {
  Interceptor.attach(Module.findExportByName('libsystem_c.dylib', funcName), {
    onEnter: args => {
      if (state === STALKING && this.threadId === stalkedThreadId) {
        state = COLLECTING;
        Stalker.unfollow();
      }
    },
    onLeave: retval => {
      if (state === WAITING) {
        state = STALKING;
        stalkedThreadId = this.threadId;
        Stalker.follow({
          events: {
            call: true
          },
          onReceive: events => {
            blobs.push(events);
            if (state === COLLECTING) {
              sendResult();
              state = DONE;
            }
          }
        });
      }
    }
  });
});

send({
  name: '+ready'
});

function sendResult() {
  const events = blobs.reduce((result, blob) => {
    const cursor = {
      data: blob,
      offset: 0
    };
    let e;
    while ((e = nextEvent(cursor))) {
      result.push(e);
    }

    return result;
  }, []);
  send({
    name: '+result',
    payload: {
      events: events
    }
  });
}

function nextEvent(cursor) {
  // FIXME: 32-bit support
  const data = cursor.data;
  if (cursor.offset === data.length)
    return null;
  skipEventType(cursor);
  const location = readPointer(cursor);
  const target = readPointer(cursor);
  const depth = readDepth(cursor);
  return [location, target, depth];
}

function skipEventType(cursor) {
  cursor.offset += 8;
}

function readPointer(cursor) {
  const data = cursor.data;
  const offset = cursor.offset;
  cursor.offset += 8;
  return ptr('0x' +
      data[offset + 7].toString(16) +
      data[offset + 6].toString(16) +
      data[offset + 5].toString(16) +
      data[offset + 4].toString(16) +
      data[offset + 3].toString(16) +
      data[offset + 2].toString(16) +
      data[offset + 1].toString(16) +
      data[offset + 0].toString(16));
}

function readDepth(cursor) {
  const data = cursor.data;
  const offset = cursor.offset;
  cursor.offset += 8;
  // FIXME: sign extend
  return (data[offset + 3] << 24) |
      (data[offset + 2] << 16) |
      (data[offset + 1] << 8) |
      (data[offset + 0] << 0);
}

EOF!

Please drop by https://t.me/fridadotre

(or #frida on FreeNode)