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
  • 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

#include <stdio.h>
#include <unistd.h>

void
f (int n)
{
  printf ("Number: %d\n", n);
}

int
main ()
{
  int i = 0;

  printf ("f() is at %p\n", f);

  while (1)
  {
    f (i++);
    sleep (1);
  }
}

1) Build and run the test app that we will instrument:

$ clang hello.c -o hello
$ ./hello
f() is at 0x106a81ec0
Number: 0
Number: 1
Number: 2
…

2) Make note of the address of f(), which is 0x106a81ec0 here.

Hooking f() from Node.js

$ # install Node.js 5.1
$ npm install co frida frida-load
$ node app.js
{ type: 'send', payload: 531 }
{ type: 'send', payload: 532 }
…
'use strict';

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

let session, script;
co(function *() {
  session = yield frida.attach('hello');
  const source = yield load(
      require.resolve('./agent.js'));
  script = yield session.createScript(source);
  script.events.listen('message', message => {
    console.log(message);
  });
  yield script.load();
});

Address of f() goes here

'use strict';

Interceptor.attach(ptr('0x106a81ec0'), {
  onEnter(args) {
    send(args[0].toInt32());
  }
});

Hooking f() from Python

$ pip install frida
$ python app.py
{'type': 'send', 'payload': 531}
{'type': 'send', 'payload': 532}
…
import frida
import sys

session = frida.attach("hello")
script = session.create_script("""
Interceptor.attach(ptr("0x106a81ec0"), {
    onEnter: function(args) {
        send(args[0].toInt32());
    }
});
""")
def on_message(message, data):
    print(message)
script.on('message', on_message)
script.load()
sys.stdin.read()

Address of f() goes here

There are also language-bindings for QML, .NET, etc. The API is the same except for local conventions like create_script() vs createScript().

 

We will stick to the Node.js bindings for the remainder of this presentation.

Modifying function arguments

$ node app.js
'use strict';

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

let session, script;
co(function *() {
  session = yield frida.attach('hello');
  const source = yield load(
      require.resolve('./agent.js'));
  script = yield session.createScript(source);
  yield script.load();
});

Address of f() goes here

Number: 1281
Number: 1282
Number: 1337
Number: 1337
Number: 1337
Number: 1337
Number: 1296
Number: 1297
Number: 1298
…

Once we stop it the target is back to normal

'use strict';

Interceptor.attach(ptr('0x106a81ec0'), {
  onEnter(args) {
    args[0] = ptr("1337");
  }
});

Calling functions

$ node app.js
'use strict';

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

let session, script;
co(function *() {
  session = yield frida.attach('hello');
  const source = yield load(
      require.resolve('./agent.js'));
  script = yield session.createScript(source);
  yield script.load();
});

Address of f() goes here

Number: 1879
Number: 1911
Number: 1911
Number: 1911
Number: 1880
…
'use strict';

const f = new NativeFunction(
    ptr('0x106a81ec0'), 'void', ['int']);
f(1911);
f(1911);
f(1911);

Sending messages

$ node app.js
'use strict';

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

let session, script;
co(function *() {
  session = yield frida.attach('hello');
  const source = yield load(
      require.resolve('./agent.js'));
  script = yield session.createScript(source);
  script.events.listen('message', message => {
    console.log(message);
  });
  yield script.load();
});
{ type: 'send',
  payload: { user: { name: 'john.doe' }, key: '1234' } }
{ type: 'error',
  description: 'ReferenceError: oops is not defined',
  stack: 'ReferenceError: oops is not defined\n    at Object.1 (agent.js:10:1)\n    at s (../../node_modules/browser-pack/_prelude.js:1:1)\n    at e (../../node_modules/browser-pack/_prelude.js:1:1)\n    at ../../node_modules/browser-pack/_prelude.js:1:1',
  fileName: 'agent.js',
  lineNumber: 10,
  columnNumber: 1 }
'use strict';

send({
  user: {
    name: 'john.doe'
  },
  key: '1234'
});

oops;

Receiving messages

$ node app.js
'use strict';

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

let session, script;
co(function *() {
  session = yield frida.attach('hello');
  const source = yield load(
      require.resolve('./agent.js'));
  script = yield session.createScript(source);
  script.events.listen('message', message => {
    console.log(message);
  });
  yield script.load();
  yield script.postMessage({ magic: 21 });
  yield script.postMessage({ magic: 12 });
});
{ type: 'send', payload: 42 }
{ type: 'send', payload: 36 }
'use strict';

let i = 2;
function handleMessage(message) {
  send(message.magic * i);
  i++;
  recv(handleMessage);
}
recv(handleMessage);

Blocking receives

$ node app.js
'use strict';

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

let session, script;
co(function *() {
  session = yield frida.attach('hello');
  const source = yield load(
      require.resolve('./agent.js'));
  script = yield session.createScript(source);
  script.events.listen('message', message => {
    const number = message.payload.number;
    script.postMessage({ number: number * 2 });
  });
  yield script.load();
});
Number: 2183
Number: 2184
Number: 4370
Number: 4372
Number: 4374
Number: 4376
Number: 4378
Number: 2190
Number: 2191
Number: 2192
…

Address of f() goes here

Once we stop it the target is back to normal

'use strict';

Interceptor.attach(ptr('0x106a81ec0'), {
  onEnter: args => {
    send({ number: args[0].toInt32() });
    const op = recv(reply => {
      args[0] = ptr(reply.number);
    });
    op.wait();
  }
});

Launch and spy on iOS app

'use strict';

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

let session, script;
co(function *() {
  const device = yield frida.getUsbDevice();
  const pid = yield device.spawn(['com.apple.AppStore']);
  session = yield device.attach(pid);
  const source = yield load(
      require.resolve('./agent.js'));
  script = yield session.createScript(source);
  script.events.listen('message', message => {
    if (message.type === 'send' && message.payload.event === 'ready')
      device.resume(pid);
    else
      console.log(message);
  });
  yield script.load();
})
.catch(console.error);
$ node app.js
{ type: 'send', payload: { event: 'call', name: 'CC_MD5' } }
{ type: 'send', payload: { event: 'call', name: 'CCDigest' } }
{ type: 'send', payload: { event: 'call', name: 'CNEncode' } }
…
'use strict';

Module.enumerateExports('libcommonCrypto.dylib', {
  onMatch: e => {
    if (e.type === 'function') {
      try {
        Interceptor.attach(e.address, {
          onEnter: args => {
            send({ event: 'call', name: e.name });
          }
        });
      } catch (error) {
        console.log('Ignoring ' + e.name + ': ' + error.message);
      }
    }
  },
  onComplete: () => {
    send({ event: 'ready' });
  }
});

But there's an app for that

$ sudo easy_install frida
$ frida-trace -U -f com.apple.AppStore -I libcommonCrypto.dylib

Dump iOS UI

'use strict';

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

let session, script;
co(function *() {
  const device = yield frida.getUsbDevice();
  const app = yield device.getFrontmostApplication();
  if (app === null)
    throw new Error("No app in foreground");
  session = yield device.attach(app.pid);
  const source = yield load(
      require.resolve('./agent.js'));
  script = yield session.createScript(source);
  script.events.listen('message', message => {
    console.log(message.payload.ui);
    session.detach();
  });
  yield script.load();
});
$ node --harmony dump-ui.js
<UIWindow: 0x15fe3ca40; frame = (0 0; 375 667); autoresize = W+H; gestureRecognizers = <NSArray: 0x17424c1e0>; layer = <UIWindowLayer: 0x17023dcc0>>
   | <UIView: 0x15fd2dbd0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x174432320>>
   |    | <UIView: 0x15fe64250; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x170235340>>
   |    |    | <UIView: 0x15fd506e0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x174222320>>
   |    |    |    | <UILayoutContainerView: 0x15fe65380; frame = (0 0; 375 667); autoresize = W+H; gestureRecognizers = <NSArray: 0x170646b70>; layer = <CALayer: 0x170237160>>
   |    |    |    |    | <UINavigationTransitionView: 0x15fe66390; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x1702347a0>>
   |    |    |    |    |    | <UIViewControllerWrapperView: 0x15fe7e3d0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x1706326a0>>
   |    |    |    |    |    |    | <UIView: 0x15fe75cd0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x170632580>>
   |    |    |    |    |    |    |    | <UIView: 0x15fe16b40; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x170632540>>
   |    |    |    |    |    |    |    |    | <TFNSwipeScrollView: 0x15fd36250; baseClass = UIScrollView; frame = (0 0; 385 667); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x17425f530>; layer = <CALayer: 0x174424860>; contentOffset: {0, 0}; contentSize: {375, 0}>
   |    |    |    |    |    |    |    |    |    | <UIActivityIndicatorView: 0x15fe54230; frame = (177.5 76.5; 20 20); hidden = YES; layer = <CALayer: 0x170237100>>
   |    |    |    |    |    |    |    |    |    |    | <UIImageView: 0x15fe55260; frame = (0 0; 20 20); opaque = NO; userInteractionEnabled = NO; animations = { contents=<CAKeyframeAnimation: 0x170238440>; }; layer = <CALayer: 0x170233900>>
   |    |    |    |    |    |    |    |    |    | <TFNCustomHitTestView: 0x15fe7e050; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x170632740>>
   |    |    |    |    |    |    |    |    |    |    | <TFNPullToRefreshControl: 0x15fe78c80; baseClass = UIControl; frame = (0 14; 375 50); alpha = 0; layer = <CALayer: 0x170628c20>>
   |    |    |    |    |    |    |    |    |    |    |    | <UIImageView: 0x15fe77ed0; frame = (176 1; 22 48); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x17062de40>>
   |    |    |    |    |    |    |    |    |    |    |    | <UIActivityIndicatorView: 0x15fe87850; frame = (177 15; 20 20); alpha = 0; hidden = YES; layer = <CALayer: 0x17062de20>>
   |    |    |    |    |    |    |    |    |    |    |    |    | <UIImageView: 0x15fe879f0; frame = (0 0; 20 20); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x17062e000>>
   |    |    |    |    |    |    |    |    |    |    | <TFNTableView: 0x1608db000; baseClass = UITableView; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x17444d500>; layer = <CALayer: 0x170232a00>; contentOffset: {0, 909.5}; contentSize: {375, 43735.5}>
   |    |    |    |    |    |    |    |    |    |    |    | <UITableViewWrapperView: 0x15fd53070; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x17444f480>; layer = <CALayer: 0x174426200>; contentOffset: {0, 0}; contentSize: {375, 667}>
   |    |    |    |    |    |    |    |    |    |    |    |    | <T1StatusCell: 0x16013ac00; baseClass = UITableViewCell; frame = (0 1228; 375 428); autoresize = W; layer = <CALayer: 0x174a30880>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    | <UITableViewCellContentView: 0x15fdb0df0; frame = (0 0; 375 428); gestureRecognizers = <NSArray: 0x174650f50>; layer = <CALayer: 0x174a30a60>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <T1StatusView: 0x15fdd0440; frame = (0 0; 375 428); layer = <CALayer: 0x174a309c0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <T1StatusBodyTextView: 0x15fdc5750; frame = (69.5 46.5; 295.5 50); layer = <CALayer: 0x174a30800>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <TFNAttributedTextView: 0x15fdd06b0; frame = (0 0; 295.5 50); text = 'When will ENOUGH BE ENOUG...'; opaque = NO; layer = <CALayer: 0x174a30380>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <T1StatusAuthorView: 0x15fdcf150; frame = (71.5 29.5; 291.5 18); layer = <CALayer: 0x174a30740>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UILabel: 0x15fdcf2e0; frame = (0 0; 99 18); text = 'Hilary $wank .'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x17428d5c0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UILabel: 0x15fdcf490; frame = (103 2; 60 16); text = '@XXXlLex'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x17428c8a0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UILabel: 0x15fdcf640; frame = (268.5 2; 23 16); text = '15h'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x17428c300>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UIImageView: 0x15fdcf7f0; frame = (0 0; 0 0); hidden = YES; userInteractionEnabled = NO; layer = <CALayer: 0x174a30620>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <TFNPaddedButton: 0x16115df00; baseClass = UIButton; frame = (280.5 3.5; 11 11); hidden = YES; opaque = NO; layer = <CALayer: 0x1716201a0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UIImageView: 0x16123a990; frame = (0 0; 11 11); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x174430480>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <T1StatusInlineActionsView: 0x15fdcf930; frame = (71.5 401.5; 211.5 14); layer = <CALayer: 0x174a305c0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <T1StatusInlineReplyButton: 0x15fdcbc30; baseClass = UIButton; frame = (-6 -5; 35 24); opaque = NO; layer = <CALayer: 0x174a2fea0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UIImageView: 0x15fdc8ef0; frame = (6 2; 23 20); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x174a2c7e0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <T1StatusInlineRetweetButton: 0x15fdcc540; baseClass = UIButton; frame = (64 -5; 58 24); opaque = NO; layer = <CALayer: 0x174a301c0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UILabel: 0x15fdd1850; frame = (36 4.5; 31.5 14.5); text = '1 612'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x17428e920>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UIImageView: 0x15fdc6bd0; frame = (6 2; 27.5 20); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x174a2fbc0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <T1StatusInlineFavoriteButton: 0x15fdc64e0; baseClass = UIButton; frame = (137 -5; 40 24); opaque = NO; layer = <CALayer: 0x174a2f360>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UILabel: 0x15fdc8170; frame = (29 4.5; 21 14.5); text = '582'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x17428e7e0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <TFNAnimatableImageView: 0x15fdc8320; baseClass = UIImageView; frame = (6 2; 20 20); opaque = NO; userInteractionEnabled = NO; layer = <TFNAnimatableImageViewLayer: 0x174a2dd40>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <TFNTwitterFollowControl: 0x15fedfff0; frame = (319.5 396.5; 43.5 24); layer = <CALayer: 0x170824e00>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <TFNButton: 0x161171f10; baseClass = UIButton; frame = (0 0; 43.5 24); opaque = NO; layer = <CALayer: 0x170835860>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UIImageView: 0x16115ba50; frame = (0 0; 43.5 24); userInteractionEnabled = NO; layer = <CALayer: 0x171820500>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UIImageView: 0x15fdc8960; frame = (6 2; 31.5 20); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x174a2d1a0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <TFNTwitterAvatarImageView: 0x1611725e0; baseClass = UIImageView; frame = (12 32.5; 51.5 51.5); opaque = NO; layer = <CALayer: 0x1714385e0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UIImageView: 0x15fdc8670; frame = (47.5 9.5; 16 16); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x174a2f380>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UILabel: 0x15fdc87b0; frame = (71.5 9.5; 291.5 15.5); text = '‎LM retweeted'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x17428ee70>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <T1NativeCardContainerView: 0x16114f3b0; frame = (71.5 115.5; 291.5 220); hidden = YES; layer = <CALayer: 0x17083e4c0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <T1NativeWebCardView: 0x16123aaf0; frame = (0 0; 291.5 220); clipsToBounds = YES; layer = <CALayer: 0x174c23f60>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <TFNHighlightControl: 0x16123b100; baseClass = UIControl; frame = (0 0; 291.5 220); gestureRecognizers = <NSArray: 0x174658360>; layer = <CALayer: 0x174c23d80>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <UIImageView: 0x16123b2a0; frame = (0 0; 291.5 146); clipsToBounds = YES; layer = <CALayer: 0x174c23ec0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <TFNHighlightControl: 0x16123b3e0; baseClass = UIControl; frame = (0 0; 291.5 146); gestureRecognizers = <NSArray: 0x174a498a0>; layer = <CALayer: 0x174c23fc0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <TFNAttributedTextView: 0x16123b580; frame = (11 154; 269.5 19); opaque = NO; layer = <CALayer: 0x174c24100>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <TFNHighlightControl: 0x16123b6e0; baseClass = UIControl; frame = (0 0; 269.5 19); gestureRecognizers = <NSArray: 0x174a522a0>; layer = <CALayer: 0x174c23f20>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <TFNAttributedTextView: 0x16123b880; frame = (11 174; 267 19); opaque = NO; layer = <CALayer: 0x174c24060>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <TFNHighlightControl: 0x16123b9e0; baseClass = UIControl; frame = (0 0; 267 19); gestureRecognizers = <NSArray: 0x174a52090>; layer = <CALayer: 0x174c242a0>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <TFNAttributedTextView: 0x16123bb80; frame = (11 194; 116 18); opaque = NO; layer = <CALayer: 0x174c24000>>
   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    | <TFNHighlightControl: 0x16123bce0; baseClass = UIControl; frame = (0 0; 116 18); gestureRecognizers = <NSArray: 0x174a452b0>; layer = <CALayer: 0x174c24420>>
…
'use strict';

ObjC.schedule(ObjC.mainQueue, () => {
  const window = ObjC.classes.UIWindow.keyWindow();
  const ui = window.recursiveDescription().toString();
  send({ ui: ui });
});

Android instrumentation

'use strict';

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

let session, script;
co(function *() {
  const device = yield frida.getUsbDevice();
  session = yield device.attach('re.frida.helloworld');
  const source = yield load(
      require.resolve('./agent.js'));
  script = yield session.createScript(source);
  script.events.listen('message', message => {
    console.log(message);
  });
  yield script.load();
});
'use strict';

Dalvik.perform(() => {
  const MainActivity = Dalvik.use(
      're.frida.helloworld.MainActivity');
  MainActivity.isRegistered.implementation = () => {
    console.log('isRegistered() w00t');
    return true;
  };
});

Injecting errors

'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 => {
    console.log(message);
  });
  yield script.load();
})
.catch(console.error);
$ 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);
}

Questions?

 

Twitter: @oleavr

Thanks!

Please drop by #frida on FreeNode, and don't forget to join our mailing list:

https://groups.google.com/d/forum/frida-dev

Frida: Putting the 'Open' Back into Closed Software

By Ole André Vadla Ravnås

Frida: Putting the 'Open' Back into Closed Software

Have this black box process that you're just dying to peek inside of? Is this process perhaps running on your cell phone, or on a closed-source OS, and you just got to interoperate with it? Is the company behind this proprietary software being less than forthcoming with APIs and docs? Well, if you know a little JavaScript and have a little persistence, perhaps we can help... In this talk, we show what you can do with Frida, a scriptable dynamic binary instrumentation toolkit for Windows, Mac, Linux, iOS, Android, and QNX. We show by example how to write snippets of custom debugging code in JavaScript, and then dynamically insert these scripts into running processes. Hook any function, spy on crypto APIs or trace private application code. No source code, no permission needed!

  • 7,173