Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 89 additions & 3 deletions objection/utils/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import click
import frida
import frida_tools

from objection.state.app import app_state
from objection.state.connection import state_connection
Expand All @@ -32,6 +33,53 @@ class AgentConfig(object):
uid: int = None


# Prepended to every script loaded through attach_script(). Since Frida 17 the
# Java / ObjC / Swift bridges are no longer built into the runtime, so a plain
# session.create_script() has no `Java` global. frida-tools solves this by
# defining lazy getters that ask the host for the bridge source; scripts we
# attach ourselves need the same prelude.
BRIDGE_PRELUDE = """
(function () {
const bridges = {};

function load(name) {
if (bridges[name] !== undefined)
return bridges[name];

send({ type: 'frida:load-bridge', name: name });
recv('frida:bridge-loaded', message => {
bridges[name] = Script.evaluate(`/frida/bridges/${message.filename}`,
'(function () {' + [
message.source,
`Object.defineProperty(globalThis, '${name}', { value: bridge });`,
'return bridge;'
].join('\\n') + '})();');
}).wait();

return bridges[name];
}

['Java', 'ObjC', 'Swift'].forEach(name => {
// Frida before 17 already provides these globals. Leave them alone so
// older runtimes keep working unchanged.
if (typeof globalThis[name] !== 'undefined')
return;

Object.defineProperty(globalThis, name, {
enumerable: true,
configurable: true,
get() { return load(name); }
});
});
})();
"""


def _bridge_path(name: str):
""" Returns the path to a frida-tools bridge bundle, if it exists. """
return Path(frida_tools.__file__).parent / 'bridges' / f'{name}.js'


class OutputHandlers(object):
""" Output handlers for an Agent instance """

Expand Down Expand Up @@ -76,17 +124,51 @@ def session_on_detached(message: str, crash):
raise e

@staticmethod
def script_on_message(message: dict, data):
def try_handle_bridge_request(message: dict, script) -> bool:
"""
Answers a frida:load-bridge request from a script.

:param message:
:param script:

:return: True if the message was a bridge request and was handled.
"""

if script is None or message.get('type') != 'send':
return False

payload = message.get('payload')
if not isinstance(payload, dict) or payload.get('type') != 'frida:load-bridge':
return False

bridge = _bridge_path(str(payload.get('name', '')).lower())
if not bridge.exists():
return False

script.post({
'type': 'frida:bridge-loaded',
'filename': bridge.name,
'source': bridge.read_text(encoding='utf-8')
})

return True

@staticmethod
def script_on_message(message: dict, data, script=None):
"""
The callback to run when a message is received from the agent.

:param message:
:param data:
:param script: The script the message came from, if known.

:return:
"""

try:
if OutputHandlers.try_handle_bridge_request(message, script):
return

# log the hook response if needed
if app_state.should_debug():
click.secho('- [incoming message] ' + '-' * 18, dim=True)
Expand All @@ -104,6 +186,9 @@ def script_on_message(message: dict, data):
click.secho('Dumping unknown agent message', fg='yellow')
pprint(message['payload'])

if message.get('type') == 'error':
click.secho('(script error) ' + str(message.get('description', '')), fg='red')

except Exception as e:
click.secho('Failed to process an incoming message from agent: {0}'.format(e), fg='red', bold=True)
raise e
Expand Down Expand Up @@ -311,8 +396,9 @@ def attach_script(self, job_name, source):
"""

session: frida.core.Session = self.device.attach(self.pid)
script: frida.core.Script = session.create_script(source=source)
script.on('message', self.handlers.script_on_message)
script: frida.core.Script = session.create_script(source=BRIDGE_PRELUDE + source)
script.on('message',
lambda message, data: self.handlers.script_on_message(message, data, script))
script.load()

script_job = Job(job_name, 'script', script)
Expand Down
41 changes: 41 additions & 0 deletions tests/utils/test_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import unittest

from objection.utils.agent import OutputHandlers


class FakeScript(object):
def __init__(self):
self.posted = []

def post(self, message):
self.posted.append(message)


class TestBridgeRequests(unittest.TestCase):
def test_ignores_messages_without_a_script(self):
message = {'type': 'send', 'payload': {'type': 'frida:load-bridge', 'name': 'java'}}

self.assertFalse(OutputHandlers.try_handle_bridge_request(message, None))

def test_ignores_messages_that_are_not_bridge_requests(self):
script = FakeScript()

self.assertFalse(OutputHandlers.try_handle_bridge_request(
{'type': 'send', 'payload': {'type': 'something-else'}}, script))
self.assertEqual(script.posted, [])

def test_posts_bridge_source(self):
script = FakeScript()
message = {'type': 'send', 'payload': {'type': 'frida:load-bridge', 'name': 'Java'}}

self.assertTrue(OutputHandlers.try_handle_bridge_request(message, script))
self.assertEqual(script.posted[0]['type'], 'frida:bridge-loaded')
self.assertEqual(script.posted[0]['filename'], 'java.js')
self.assertTrue(script.posted[0]['source'])

def test_ignores_unknown_bridges(self):
script = FakeScript()
message = {'type': 'send', 'payload': {'type': 'frida:load-bridge', 'name': 'nope'}}

self.assertFalse(OutputHandlers.try_handle_bridge_request(message, script))
self.assertEqual(script.posted, [])