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 Swift, 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.post({ magic: 21 });
  yield script.post({ 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.post({ 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