├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── code-style.yml │ └── linting.yml ├── .gitignore ├── .gitmodules ├── BSDmakefile ├── COPYING ├── MANIFEST.in ├── Makefile ├── README.md ├── configure ├── configure.bat ├── examples ├── bytecode.py ├── channels.py ├── child_gating.py ├── cpushark │ ├── AppDelegate.py │ ├── Capture.py │ ├── CpuShark.py │ ├── MainMenu.xib │ ├── MainWindow.xib │ ├── MainWindowController.py │ ├── Makefile │ ├── ProcessList.py │ └── setup.py ├── crash_reporting.py ├── detached.py ├── enumerate_applications.py ├── enumerate_processes.py ├── get_frontmost_application.py ├── inject_library │ ├── example.c │ ├── inject_blob.py │ └── inject_file.py ├── open_service │ ├── dtx │ │ ├── deviceinfo.py │ │ ├── opengl.py │ │ ├── processcontrol.py │ │ ├── screenshot.py │ │ └── sysmontap.py │ ├── plist.py │ └── xpc │ │ ├── launchapplication.py │ │ └── listprocesses.py ├── portal_client.py ├── portal_server.py ├── query_system_parameters.py ├── rpc.py ├── session_persist_timeout.py ├── snapshot.py ├── unpair.py └── web_client │ ├── .gitignore │ ├── babel.config.js │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ └── HelloWorld.vue │ ├── main.ts │ ├── shims-tsx.d.ts │ ├── shims-vue.d.ts │ ├── shims │ │ ├── abstract-socket.js │ │ └── x11.js │ └── store │ │ ├── index.ts │ │ ├── modules │ │ └── frida.ts │ │ └── plugins │ │ ├── frida.ts │ │ └── message-event-stream.ts │ ├── tsconfig.json │ └── vue.config.js ├── frida ├── __init__.py ├── _frida │ ├── __init__.pyi │ ├── extension.c │ ├── extension.version │ ├── meson.build │ └── py.typed ├── core.py ├── meson.build └── py.typed ├── make.bat ├── meson.build ├── pyproject.toml ├── setup.cfg ├── setup.py ├── subprojects └── frida-core.wrap └── tests ├── __init__.py ├── data ├── __init__.py ├── unixvictim-linux-x86 ├── unixvictim-linux-x86_64 └── unixvictim-macos ├── test_core.py ├── test_pep503_page_parser.py └── test_rpc.py /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.py] 2 | indent_style = space 3 | indent_size = 4 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: frida 2 | -------------------------------------------------------------------------------- /.github/workflows/code-style.yml: -------------------------------------------------------------------------------- 1 | name: code-style 2 | on: [push, pull_request] 3 | jobs: 4 | black: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - uses: psf/black@stable 9 | isort: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-python@v4 14 | with: 15 | python-version: 3.8 16 | - uses: jamescurtin/isort-action@master 17 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: linting 2 | on: [push, pull_request] 3 | jobs: 4 | pyflakes: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - uses: lgeiger/pyflakes-action@master 9 | mypy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: jpetrucciani/mypy-check@master 14 | with: 15 | mypy_flags: '--exclude examples --exclude setup' 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /*.egg-info/ 3 | /build/ 4 | /deps/ 5 | /dist/ 6 | /subprojects/* 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "releng"] 2 | path = releng 3 | url = https://github.com/frida/releng.git 4 | -------------------------------------------------------------------------------- /BSDmakefile: -------------------------------------------------------------------------------- 1 | all: .DEFAULT 2 | 3 | .DEFAULT: 4 | @gmake ${.MAKEFLAGS} ${.TARGETS} 5 | 6 | .PHONY: all 7 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | wxWindows Library Licence, Version 3.1 2 | ====================================== 3 | 4 | Copyright (c) 1998-2005 Julian Smart, Robert Roebling et al 5 | 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this licence document, but changing it is not allowed. 8 | 9 | WXWINDOWS LIBRARY LICENCE 10 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 11 | 12 | This library is free software; you can redistribute it and/or modify it 13 | under the terms of the GNU Library General Public Licence as published by 14 | the Free Software Foundation; either version 2 of the Licence, or (at your 15 | option) any later version. 16 | 17 | This library is distributed in the hope that it will be useful, but WITHOUT 18 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public 20 | Licence for more details. 21 | 22 | You should have received a copy of the GNU Library General Public Licence 23 | along with this software, usually in a file named COPYING.LIB. If not, 24 | write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth 25 | Floor, Boston, MA 02110-1301 USA. 26 | 27 | EXCEPTION NOTICE 28 | 29 | 1. As a special exception, the copyright holders of this library give 30 | permission for additional uses of the text contained in this release of the 31 | library as licenced under the wxWindows Library Licence, applying either 32 | version 3.1 of the Licence, or (at your option) any later version of the 33 | Licence as published by the copyright holders of version 3.1 of the Licence 34 | document. 35 | 36 | 2. The exception is that you may use, copy, link, modify and distribute 37 | under your own terms, binary object code versions of works based on the 38 | Library. 39 | 40 | 3. If you copy code from files distributed under the terms of the GNU 41 | General Public Licence or the GNU Library General Public Licence into a 42 | copy of this library, as this licence permits, the exception does not apply 43 | to the code that you add in this way. To avoid misleading anyone as to the 44 | status of such modified files, you must delete this exception notice from 45 | such code and/or adjust the licensing conditions notice accordingly. 46 | 47 | 4. If you write modifications of your own for this library, it is your 48 | choice whether to permit this exception to apply to your modifications. If 49 | you do not wish that, you must delete the exception notice from such code 50 | and/or adjust the licensing conditions notice accordingly. 51 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include meson.build 2 | include frida/meson.build 3 | include frida/_frida/extension.c 4 | include frida/_frida/extension.version 5 | include frida/_frida/meson.build 6 | include subprojects/frida-core.wrap 7 | 8 | include BSDmakefile 9 | include Makefile 10 | include configure 11 | include configure.bat 12 | include make.bat 13 | include releng/*.py 14 | include releng/*.toml 15 | graft releng/devkit-assets/ 16 | graft releng/meson-scripts/ 17 | include releng/meson/meson.py 18 | graft releng/meson/mesonbuild/ 19 | graft releng/tomlkit/tomlkit/ 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON ?= $(shell which python3 >/dev/null && echo python3 || echo python) 2 | 3 | all $(MAKECMDGOALS): 4 | @$(PYTHON) \ 5 | -c "import sys; sys.path.insert(0, sys.argv[1]); from releng.meson_make import main; main()" \ 6 | "$(shell pwd)" \ 7 | ./build \ 8 | $(MAKECMDGOALS) 9 | 10 | git-submodules: 11 | @if [ ! -f releng/meson/meson.py ]; then \ 12 | git submodule update --init --recursive --depth 1; \ 13 | fi 14 | -include git-submodules 15 | 16 | .PHONY: all $(MAKECMDGOALS) 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # frida-python 2 | 3 | Python bindings for [Frida](https://frida.re). 4 | 5 | # Some tips during development 6 | 7 | To build and test your own wheel, do something along the following lines: 8 | 9 | ``` 10 | set FRIDA_VERSION=16.0.1-dev.7 # from C:\src\frida\build\tmp-windows\frida-version.h 11 | set FRIDA_EXTENSION=C:\src\frida\build\frida-windows\x64-Release\lib\python3.10\site-packages\_frida.pyd 12 | cd C:\src\frida\frida-python\ 13 | pip wheel . 14 | pip uninstall frida 15 | pip install frida-16.0.1.dev7-cp34-abi3-win_amd64.whl 16 | ``` 17 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -z "$PYTHON" ] && PYTHON=$(which python3 >/dev/null && echo python3 || echo python) 4 | 5 | cd $(dirname $0) 6 | 7 | srcroot=$(pwd) 8 | 9 | if [ ! -f releng/meson/meson.py ]; then 10 | git submodule update --init --recursive --depth 1 || exit $? 11 | fi 12 | 13 | cd - >/dev/null 14 | 15 | exec "$PYTHON" \ 16 | -c "import sys; sys.path.insert(0, sys.argv[1]); from releng.meson_configure import main; main()" \ 17 | "$srcroot" \ 18 | "$@" 19 | -------------------------------------------------------------------------------- /configure.bat: -------------------------------------------------------------------------------- 1 | @setlocal 2 | @echo off 3 | rem:: Based on: https://github.com/microsoft/terminal/issues/217#issuecomment-737594785 4 | goto :_start_ 5 | 6 | :set_real_dp0 7 | set dp0=%~dp0 8 | set "dp0=%dp0:~0,-1%" 9 | goto :eof 10 | 11 | :_start_ 12 | call :set_real_dp0 13 | 14 | if not exist "%dp0%\releng\meson\meson.py" ( 15 | pushd "%dp0%" & git submodule update --init --recursive --depth 1 & popd 16 | if %errorlevel% neq 0 exit /b %errorlevel% 17 | ) 18 | 19 | endlocal & goto #_undefined_# 2>nul || title %COMSPEC% & python ^ 20 | -c "import sys; sys.path.insert(0, sys.argv[1]); from releng.meson_configure import main; main()" ^ 21 | "%dp0%" ^ 22 | %* 23 | -------------------------------------------------------------------------------- /examples/bytecode.py: -------------------------------------------------------------------------------- 1 | import frida 2 | 3 | system_session = frida.attach(0) 4 | bytecode = system_session.compile_script( 5 | name="bytecode-example", 6 | source="""\ 7 | rpc.exports = { 8 | listThreads: function () { 9 | return Process.enumerateThreadsSync(); 10 | } 11 | }; 12 | """, 13 | ) 14 | 15 | session = frida.attach("Twitter") 16 | script = session.create_script_from_bytes(bytecode) 17 | script.load() 18 | api = script.exports_sync 19 | print("api.list_threads() =>", api.list_threads()) 20 | -------------------------------------------------------------------------------- /examples/channels.py: -------------------------------------------------------------------------------- 1 | import frida 2 | 3 | device = frida.get_usb_device() 4 | 5 | channel = device.open_channel("tcp:21") 6 | print("Got channel:", channel) 7 | 8 | welcome = channel.read(512) 9 | print("Got welcome message:", welcome) 10 | 11 | channel.write_all(b"CWD foo") 12 | reply = channel.read(512) 13 | print("Got reply:", reply) 14 | 15 | channel.close() 16 | print("Channel now:", channel) 17 | -------------------------------------------------------------------------------- /examples/child_gating.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from frida_tools.application import Reactor 4 | 5 | import frida 6 | 7 | 8 | class Application: 9 | def __init__(self): 10 | self._stop_requested = threading.Event() 11 | self._reactor = Reactor(run_until_return=lambda reactor: self._stop_requested.wait()) 12 | 13 | self._device = frida.get_local_device() 14 | self._sessions = set() 15 | 16 | self._device.on("child-added", lambda child: self._reactor.schedule(lambda: self._on_child_added(child))) 17 | self._device.on("child-removed", lambda child: self._reactor.schedule(lambda: self._on_child_removed(child))) 18 | self._device.on("output", lambda pid, fd, data: self._reactor.schedule(lambda: self._on_output(pid, fd, data))) 19 | 20 | def run(self): 21 | self._reactor.schedule(lambda: self._start()) 22 | self._reactor.run() 23 | 24 | def _start(self): 25 | argv = ["/bin/sh", "-c", "cat /etc/hosts"] 26 | env = { 27 | "BADGER": "badger-badger-badger", 28 | "SNAKE": "mushroom-mushroom", 29 | } 30 | print(f"✔ spawn(argv={argv})") 31 | pid = self._device.spawn(argv, env=env, stdio="pipe") 32 | self._instrument(pid) 33 | 34 | def _stop_if_idle(self): 35 | if len(self._sessions) == 0: 36 | self._stop_requested.set() 37 | 38 | def _instrument(self, pid): 39 | print(f"✔ attach(pid={pid})") 40 | session = self._device.attach(pid) 41 | session.on("detached", lambda reason: self._reactor.schedule(lambda: self._on_detached(pid, session, reason))) 42 | print("✔ enable_child_gating()") 43 | session.enable_child_gating() 44 | print("✔ create_script()") 45 | script = session.create_script( 46 | """\ 47 | Interceptor.attach(Module.getExportByName(null, 'open'), { 48 | onEnter: function (args) { 49 | send({ 50 | type: 'open', 51 | path: Memory.readUtf8String(args[0]) 52 | }); 53 | } 54 | }); 55 | """ 56 | ) 57 | script.on("message", lambda message, data: self._reactor.schedule(lambda: self._on_message(pid, message))) 58 | print("✔ load()") 59 | script.load() 60 | print(f"✔ resume(pid={pid})") 61 | self._device.resume(pid) 62 | self._sessions.add(session) 63 | 64 | def _on_child_added(self, child): 65 | print(f"⚡ child_added: {child}") 66 | self._instrument(child.pid) 67 | 68 | def _on_child_removed(self, child): 69 | print(f"⚡ child_removed: {child}") 70 | 71 | def _on_output(self, pid, fd, data): 72 | print(f"⚡ output: pid={pid}, fd={fd}, data={repr(data)}") 73 | 74 | def _on_detached(self, pid, session, reason): 75 | print(f"⚡ detached: pid={pid}, reason='{reason}'") 76 | self._sessions.remove(session) 77 | self._reactor.schedule(self._stop_if_idle, delay=0.5) 78 | 79 | def _on_message(self, pid, message): 80 | print(f"⚡ message: pid={pid}, payload={message['payload']}") 81 | 82 | 83 | app = Application() 84 | app.run() 85 | -------------------------------------------------------------------------------- /examples/cpushark/AppDelegate.py: -------------------------------------------------------------------------------- 1 | from Cocoa import NSApp 2 | from Foundation import NSObject 3 | from MainWindowController import MainWindowController 4 | 5 | 6 | class AppDelegate(NSObject): 7 | def applicationDidFinishLaunching_(self, notification): 8 | window = MainWindowController() 9 | window.showWindow_(window) 10 | NSApp.activateIgnoringOtherApps_(True) 11 | 12 | def applicationShouldTerminateAfterLastWindowClosed_(self, sender): 13 | return True 14 | -------------------------------------------------------------------------------- /examples/cpushark/Capture.py: -------------------------------------------------------------------------------- 1 | import bisect 2 | import re 3 | 4 | from Foundation import NSAutoreleasePool, NSObject, NSThread 5 | from PyObjCTools import AppHelper 6 | 7 | PROBE_CALLS = re.compile(r"^\/stalker\/probes\/(.*?)\/calls$") 8 | 9 | 10 | class Capture(NSObject): 11 | def __new__(cls, device): 12 | return cls.alloc().initWithDevice_(device) 13 | 14 | def initWithDevice_(self, device): 15 | self = self.init() 16 | self.state = CaptureState.DETACHED 17 | self.device = device 18 | self._delegate = None 19 | self.session = None 20 | self.script = None 21 | self.modules = Modules() 22 | self.recvTotal = 0 23 | self.calls = Calls(self) 24 | return self 25 | 26 | def delegate(self): 27 | return self._delegate 28 | 29 | def setDelegate_(self, delegate): 30 | self._delegate = delegate 31 | 32 | def attachToProcess_triggerPort_(self, process, triggerPort): 33 | assert self.state == CaptureState.DETACHED 34 | self._updateState_(CaptureState.ATTACHING) 35 | NSThread.detachNewThreadSelector_toTarget_withObject_("_doAttachWithParams:", self, (process.pid, triggerPort)) 36 | 37 | def detach(self): 38 | assert self.state == CaptureState.ATTACHED 39 | session = self.session 40 | script = self.script 41 | self.session = None 42 | self.script = None 43 | self._updateState_(CaptureState.DETACHED) 44 | NSThread.detachNewThreadSelector_toTarget_withObject_("_doDetachWithParams:", self, (session, script)) 45 | 46 | def _post(self, message): 47 | NSThread.detachNewThreadSelector_toTarget_withObject_("_doPostWithParams:", self, (self.script, message)) 48 | 49 | def _updateState_(self, newState): 50 | self.state = newState 51 | self._delegate.captureStateDidChange() 52 | 53 | def _doAttachWithParams_(self, params): 54 | pid, triggerPort = params 55 | pool = NSAutoreleasePool.alloc().init() 56 | session = None 57 | script = None 58 | error = None 59 | try: 60 | session = self.device.attach(pid) 61 | session.on("detached", self._onSessionDetached) 62 | script = session.create_script(name="cpushark", source=SCRIPT_TEMPLATE % {"trigger_port": triggerPort}) 63 | script.on("message", self._onScriptMessage) 64 | script.load() 65 | except Exception as e: 66 | if session is not None: 67 | try: 68 | session.detach() 69 | except: 70 | pass 71 | session = None 72 | script = None 73 | error = e 74 | AppHelper.callAfter(self._attachDidCompleteWithSession_script_error_, session, script, error) 75 | del pool 76 | 77 | def _doDetachWithParams_(self, params): 78 | session, script = params 79 | pool = NSAutoreleasePool.alloc().init() 80 | try: 81 | script.unload() 82 | except: 83 | pass 84 | try: 85 | session.detach() 86 | except: 87 | pass 88 | del pool 89 | 90 | def _doPostWithParams_(self, params): 91 | script, message = params 92 | pool = NSAutoreleasePool.alloc().init() 93 | try: 94 | script.post(message) 95 | except Exception as e: 96 | print("Failed to post to script:", e) 97 | del pool 98 | 99 | def _attachDidCompleteWithSession_script_error_(self, session, script, error): 100 | if self.state == CaptureState.ATTACHING: 101 | self.session = session 102 | self.script = script 103 | if error is None: 104 | self._updateState_(CaptureState.ATTACHED) 105 | else: 106 | self._updateState_(CaptureState.DETACHED) 107 | self._delegate.captureFailedToAttachWithError_(error) 108 | 109 | def _sessionDidDetach(self): 110 | if self.state == CaptureState.ATTACHING or self.state == CaptureState.ATTACHED: 111 | self.session = None 112 | self._updateState_(CaptureState.DETACHED) 113 | 114 | def _sessionDidReceiveMessage_data_(self, message, data): 115 | if message["type"] == "send": 116 | stanza = message["payload"] 117 | fromAddress = stanza["from"] 118 | name = stanza["name"] 119 | if fromAddress == "/process/modules" and name == "+sync": 120 | self.modules._sync(stanza["payload"]) 121 | elif fromAddress == "/stalker/calls" and name == "+add": 122 | self.calls._add_(stanza["payload"]) 123 | elif fromAddress == "/interceptor/functions" and name == "+add": 124 | self.recvTotal += 1 125 | self._delegate.captureRecvTotalDidChange() 126 | else: 127 | if not self.calls._handleStanza_(stanza): 128 | print(f"Woot! Got stanza: {stanza['name']} from={stanza['from']}") 129 | else: 130 | print("Unhandled message:", message) 131 | 132 | def _onSessionDetached(self): 133 | AppHelper.callAfter(self._sessionDidDetach) 134 | 135 | def _onScriptMessage(self, message, data): 136 | AppHelper.callAfter(self._sessionDidReceiveMessage_data_, message, data) 137 | 138 | 139 | class CaptureState: 140 | DETACHED = 1 141 | ATTACHING = 2 142 | ATTACHED = 3 143 | 144 | 145 | class Modules: 146 | def __init__(self): 147 | self._modules = [] 148 | self._indices = [] 149 | 150 | def _sync(self, payload): 151 | modules = [] 152 | for item in payload["items"]: 153 | modules.append(Module(item["name"], int(item["base"], 16), item["size"])) 154 | modules.sort(lambda x, y: x.address - y.address) 155 | self._modules = modules 156 | self._indices = [m.address for m in modules] 157 | 158 | def lookup(self, addr): 159 | idx = bisect.bisect(self._indices, addr) 160 | if idx == 0: 161 | return None 162 | m = self._modules[idx - 1] 163 | if addr >= m.address + m.size: 164 | return None 165 | return m 166 | 167 | 168 | class Module: 169 | def __init__(self, name, address, size): 170 | self.name = name 171 | self.address = address 172 | self.size = size 173 | 174 | def __repr__(self): 175 | return "(%d, %d, %s)" % (self.address, self.size, self.name) 176 | 177 | 178 | class Calls(NSObject): 179 | def __new__(cls, capture): 180 | return cls.alloc().initWithCapture_(capture) 181 | 182 | def initWithCapture_(self, capture): 183 | self = self.init() 184 | self.capture = capture 185 | self.targetModules = [] 186 | self._targetModuleByAddress = {} 187 | self._delegate = None 188 | self._probes = {} 189 | return self 190 | 191 | def delegate(self): 192 | return self._delegate 193 | 194 | def setDelegate_(self, delegate): 195 | self._delegate = delegate 196 | 197 | def addProbe_(self, func): 198 | self.capture._post({"to": "/stalker/probes", "name": "+add", "payload": {"address": "0x%x" % func.address}}) 199 | self._probes[func.address] = func 200 | 201 | def removeProbe_(self, func): 202 | self.capture._post({"to": "/stalker/probes", "name": "+remove", "payload": {"address": "0x%x" % func.address}}) 203 | self._probes.pop(func.address, None) 204 | 205 | def _add_(self, data): 206 | modules = self.capture.modules 207 | for rawTarget, count in data["summary"].items(): 208 | target = int(rawTarget, 16) 209 | tm = self.getTargetModuleByModule_(modules.lookup(target)) 210 | if tm is not None: 211 | tm.total += count 212 | tf = tm.getTargetFunctionByAddress_(target) 213 | tf.total += count 214 | 215 | self.targetModules.sort(key=lambda tm: tm.total, reverse=True) 216 | for tm in self.targetModules: 217 | tm.functions.sort(self._compareFunctions) 218 | self._delegate.callsDidChange() 219 | 220 | def _compareFunctions(self, x, y): 221 | if x.hasProbe == y.hasProbe: 222 | return x.total - y.total 223 | elif x.hasProbe: 224 | return -1 225 | elif y.hasProbe: 226 | return 1 227 | else: 228 | return x.total - y.total 229 | 230 | def _handleStanza_(self, stanza): 231 | m = PROBE_CALLS.match(stanza["from"]) 232 | if m is not None: 233 | func = self._probes.get(int(m.groups()[0], 16), None) 234 | if func is not None: 235 | if len(func.calls) == 3: 236 | func.calls.pop(0) 237 | func.calls.append(FunctionCall(func, stanza["payload"]["args"])) 238 | self._delegate.callItemDidChange_(func) 239 | return True 240 | return False 241 | 242 | def getTargetModuleByModule_(self, module): 243 | if module is None: 244 | return None 245 | tm = self._targetModuleByAddress.get(module.address, None) 246 | if tm is None: 247 | tm = TargetModule(module) 248 | self.targetModules.append(tm) 249 | self._targetModuleByAddress[module.address] = tm 250 | return tm 251 | 252 | def outlineView_numberOfChildrenOfItem_(self, outlineView, item): 253 | if item is None: 254 | return len(self.targetModules) 255 | elif isinstance(item, TargetModule): 256 | return len(item.functions) 257 | elif isinstance(item, TargetFunction): 258 | return len(item.calls) 259 | else: 260 | return 0 261 | 262 | def outlineView_isItemExpandable_(self, outlineView, item): 263 | if item is None: 264 | return False 265 | elif isinstance(item, TargetModule): 266 | return len(item.functions) > 0 267 | elif isinstance(item, TargetFunction): 268 | return len(item.calls) > 0 269 | else: 270 | return False 271 | 272 | def outlineView_child_ofItem_(self, outlineView, index, item): 273 | if item is None: 274 | return self.targetModules[index] 275 | elif isinstance(item, TargetModule): 276 | return item.functions[index] 277 | elif isinstance(item, TargetFunction): 278 | return item.calls[index] 279 | else: 280 | return None 281 | 282 | def outlineView_objectValueForTableColumn_byItem_(self, outlineView, tableColumn, item): 283 | identifier = tableColumn.identifier() 284 | if isinstance(item, TargetModule): 285 | if identifier == "name": 286 | return item.module.name 287 | elif identifier == "total": 288 | return item.total 289 | else: 290 | return False 291 | elif isinstance(item, TargetFunction): 292 | if identifier == "name": 293 | return item.name 294 | elif identifier == "total": 295 | return item.total 296 | else: 297 | return item.hasProbe 298 | else: 299 | if identifier == "name": 300 | return item.summary 301 | elif identifier == "total": 302 | return "" 303 | else: 304 | return False 305 | 306 | 307 | class TargetModule(NSObject): 308 | def __new__(cls, module): 309 | return cls.alloc().initWithModule_(module) 310 | 311 | def initWithModule_(self, module): 312 | self = self.init() 313 | self.module = module 314 | self.functions = [] 315 | self._functionByAddress = {} 316 | self.total = 0 317 | return self 318 | 319 | def getTargetFunctionByAddress_(self, address): 320 | f = self._functionByAddress.get(address, None) 321 | if f is None: 322 | f = TargetFunction(self, address - self.module.address) 323 | self.functions.append(f) 324 | self._functionByAddress[address] = f 325 | return f 326 | 327 | 328 | class TargetFunction(NSObject): 329 | def __new__(cls, module, offset): 330 | return cls.alloc().initWithModule_offset_(module, offset) 331 | 332 | def initWithModule_offset_(self, targetModule, offset): 333 | self = self.init() 334 | self.name = "sub_%x" % offset 335 | self.module = targetModule 336 | self.address = targetModule.module.address + offset 337 | self.offset = offset 338 | self.total = 0 339 | self.hasProbe = False 340 | self.calls = [] 341 | return self 342 | 343 | 344 | class FunctionCall(NSObject): 345 | def __new__(cls, func, args): 346 | return cls.alloc().initWithFunction_args_(func, args) 347 | 348 | def initWithFunction_args_(self, func, args): 349 | self = self.init() 350 | self.func = func 351 | self.args = args 352 | self.summary = f"{func.name}({', '.join(args)})" 353 | return self 354 | 355 | 356 | SCRIPT_TEMPLATE = """ 357 | var probes = Object.create(null); 358 | 359 | var initialize = function initialize() { 360 | Stalker.trustThreshold = 2000; 361 | Stalker.queueCapacity = 1000000; 362 | Stalker.queueDrainInterval = 250; 363 | 364 | sendModules(function () { 365 | interceptReadFunction('recv'); 366 | interceptReadFunction('read$UNIX2003'); 367 | interceptReadFunction('readv$UNIX2003'); 368 | }); 369 | 370 | recv(onStanza); 371 | }; 372 | 373 | var onStanza = function onStanza(stanza) { 374 | if (stanza.to === "/stalker/probes") { 375 | var address = stanza.payload.address, 376 | probeId; 377 | switch (stanza.name) { 378 | case '+add': 379 | if (probes[address] === undefined) { 380 | var probeAddress = "/stalker/probes/" + address + "/calls"; 381 | probeId = Stalker.addCallProbe(ptr(address), function probe(args) { 382 | var data = [ 383 | "0x" + args[0].toString(16), 384 | "0x" + args[1].toString(16), 385 | "0x" + args[2].toString(16), 386 | "0x" + args[3].toString(16) 387 | ]; 388 | send({ from: probeAddress, name: '+add', payload: { args: data } }); 389 | }); 390 | probes[address] = probeId; 391 | } 392 | break; 393 | case '+remove': 394 | probeId = probes[address]; 395 | if (probeId !== undefined) { 396 | Stalker.removeCallProbe(probeId); 397 | delete probes[address]; 398 | } 399 | break; 400 | } 401 | } 402 | 403 | recv(onStanza); 404 | }; 405 | 406 | var sendModules = function sendModules(callback) { 407 | var modules = []; 408 | Process.enumerateModules({ 409 | onMatch: function onMatch(module) { 410 | modules.push(module); 411 | }, 412 | onComplete: function onComplete() { 413 | send({ name: '+sync', from: "/process/modules", payload: { items: modules } }); 414 | callback(); 415 | } 416 | }); 417 | }; 418 | 419 | var stalkedThreadId = null; 420 | var interceptReadFunction = function interceptReadFunction(functionName) { 421 | Interceptor.attach(Module.getExportByName('libSystem.B.dylib', functionName), { 422 | onEnter: function(args) { 423 | this.fd = args[0].toInt32(); 424 | }, 425 | onLeave: function (retval) { 426 | var fd = this.fd; 427 | if (Socket.type(fd) === 'tcp') { 428 | var address = Socket.peerAddress(fd); 429 | if (address !== null && address.port === %(trigger_port)d) { 430 | send({ name: '+add', from: "/interceptor/functions", payload: { items: [{ name: functionName }] } }); 431 | if (stalkedThreadId === null) { 432 | stalkedThreadId = Process.getCurrentThreadId(); 433 | Stalker.follow(stalkedThreadId, { 434 | events: { 435 | call: true 436 | }, 437 | onCallSummary: function onCallSummary(summary) { 438 | send({ name: '+add', from: "/stalker/calls", payload: { summary: summary } }); 439 | } 440 | }); 441 | } 442 | } 443 | } 444 | } 445 | }); 446 | } 447 | 448 | setTimeout(initialize, 0); 449 | """ 450 | -------------------------------------------------------------------------------- /examples/cpushark/CpuShark.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.insert(0, "/Users/oleavr/src/frida/build/frida-macos-universal/lib/python2.7/site-packages") 4 | 5 | if __name__ == "__main__": 6 | from PyObjCTools import AppHelper 7 | 8 | AppHelper.runEventLoop() 9 | -------------------------------------------------------------------------------- /examples/cpushark/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /examples/cpushark/MainWindow.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 98 | 99 | 100 | 101 | 102 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 197 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /examples/cpushark/MainWindowController.py: -------------------------------------------------------------------------------- 1 | from Capture import Capture, CaptureState, TargetFunction 2 | from Cocoa import NSRunCriticalAlertPanel, NSUserDefaults, NSWindowController, objc 3 | from ProcessList import ProcessList 4 | 5 | import frida 6 | 7 | 8 | class MainWindowController(NSWindowController): 9 | processCombo = objc.IBOutlet() 10 | triggerField = objc.IBOutlet() 11 | attachProgress = objc.IBOutlet() 12 | attachButton = objc.IBOutlet() 13 | detachButton = objc.IBOutlet() 14 | recvTotalLabel = objc.IBOutlet() 15 | callTableView = objc.IBOutlet() 16 | 17 | def __new__(cls): 18 | return cls.alloc().initWithTitle_("CpuShark") 19 | 20 | def initWithTitle_(self, title): 21 | self = self.initWithWindowNibName_("MainWindow") 22 | self.window().setTitle_(title) 23 | 24 | self.retain() 25 | 26 | return self 27 | 28 | def windowDidLoad(self): 29 | NSWindowController.windowDidLoad(self) 30 | 31 | device = [device for device in frida.get_device_manager().enumerate_devices() if device.type == "local"][0] 32 | self.processList = ProcessList(device) 33 | self.capture = Capture(device) 34 | self.processCombo.setUsesDataSource_(True) 35 | self.processCombo.setDataSource_(self.processList) 36 | self.capture.setDelegate_(self) 37 | 38 | self.callTableView.setDataSource_(self.capture.calls) 39 | self.capture.calls.setDelegate_(self) 40 | 41 | self.loadDefaults() 42 | 43 | self.updateAttachForm_(self) 44 | 45 | def windowWillClose_(self, notification): 46 | self.saveDefaults() 47 | 48 | self.autorelease() 49 | 50 | def loadDefaults(self): 51 | defaults = NSUserDefaults.standardUserDefaults() 52 | targetProcess = defaults.stringForKey_("targetProcess") 53 | if targetProcess is not None: 54 | for i, process in enumerate(self.processList.processes): 55 | if process.name == targetProcess: 56 | self.processCombo.selectItemAtIndex_(i) 57 | break 58 | triggerPort = defaults.integerForKey_("triggerPort") or 80 59 | self.triggerField.setStringValue_(str(triggerPort)) 60 | 61 | def saveDefaults(self): 62 | defaults = NSUserDefaults.standardUserDefaults() 63 | process = self.selectedProcess() 64 | if process is not None: 65 | defaults.setObject_forKey_(process.name, "targetProcess") 66 | defaults.setInteger_forKey_(self.triggerField.integerValue(), "triggerPort") 67 | 68 | def selectedProcess(self): 69 | index = self.processCombo.indexOfSelectedItem() 70 | if index != -1: 71 | return self.processList.processes[index] 72 | return None 73 | 74 | def triggerPort(self): 75 | return self.triggerField.integerValue() 76 | 77 | @objc.IBAction 78 | def attach_(self, sender): 79 | self.capture.attachToProcess_triggerPort_(self.selectedProcess(), self.triggerPort()) 80 | 81 | @objc.IBAction 82 | def detach_(self, sender): 83 | self.capture.detach() 84 | 85 | @objc.IBAction 86 | def toggleTracing_(self, sender): 87 | item = sender.itemAtRow_(sender.selectedRow()) 88 | if isinstance(item, TargetFunction): 89 | func = item 90 | if func.hasProbe: 91 | self.capture.calls.removeProbe_(func) 92 | else: 93 | self.capture.calls.addProbe_(func) 94 | func.hasProbe = not func.hasProbe 95 | self.callTableView.reloadItem_(func) 96 | 97 | def updateAttachForm_(self, sender): 98 | isDetached = self.capture.state == CaptureState.DETACHED 99 | hasProcess = self.selectedProcess() is not None 100 | hasTrigger = len(self.triggerField.stringValue()) > 0 101 | self.processCombo.setEnabled_(isDetached) 102 | self.triggerField.setEnabled_(isDetached) 103 | self.attachProgress.setHidden_(self.capture.state != CaptureState.ATTACHING) 104 | self.attachButton.setHidden_(self.capture.state == CaptureState.ATTACHED) 105 | self.attachButton.setEnabled_(isDetached and hasProcess and hasTrigger) 106 | self.detachButton.setHidden_(self.capture.state != CaptureState.ATTACHED) 107 | if self.capture.state == CaptureState.ATTACHING: 108 | self.attachProgress.startAnimation_(self) 109 | else: 110 | self.attachProgress.stopAnimation_(self) 111 | 112 | def controlTextDidChange_(self, notification): 113 | self.updateAttachForm_(self) 114 | 115 | def comboBoxSelectionDidChange_(self, notification): 116 | self.updateAttachForm_(self) 117 | 118 | def captureStateDidChange(self): 119 | self.updateAttachForm_(self) 120 | 121 | def captureFailedToAttachWithError_(self, error): 122 | NSRunCriticalAlertPanel("Error", "Failed to attach: %s" % error, None, None, None) 123 | 124 | def captureRecvTotalDidChange(self): 125 | self.recvTotalLabel.setStringValue_(self.capture.recvTotal) 126 | 127 | def callsDidChange(self): 128 | self.callTableView.reloadData() 129 | 130 | def callItemDidChange_(self, item): 131 | self.callTableView.reloadItem_reloadChildren_(item, True) 132 | -------------------------------------------------------------------------------- /examples/cpushark/Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | clean: 4 | rm -rf build dist 5 | 6 | build: clean 7 | python setup.py py2app 8 | 9 | dev: clean 10 | python setup.py py2app -A 11 | 12 | devrun: dev 13 | ./dist/CpuShark.app/Contents/MacOS/CpuShark 14 | 15 | .PHONY: clean build dev devrun 16 | -------------------------------------------------------------------------------- /examples/cpushark/ProcessList.py: -------------------------------------------------------------------------------- 1 | from Foundation import NSNotFound, NSObject 2 | 3 | 4 | class ProcessList(NSObject): 5 | def __new__(cls, device): 6 | return cls.alloc().initWithDevice_(device) 7 | 8 | def initWithDevice_(self, device): 9 | self = self.init() 10 | self.processes = sorted(device.enumerate_processes(), key=lambda d: d.name.lower()) 11 | self._processNames = [] 12 | self._processIndexByName = {} 13 | for i, process in enumerate(self.processes): 14 | lowerName = process.name.lower() 15 | self._processNames.append(lowerName) 16 | self._processIndexByName[lowerName] = i 17 | return self 18 | 19 | def numberOfItemsInComboBox_(self, comboBox): 20 | return len(self.processes) 21 | 22 | def comboBox_objectValueForItemAtIndex_(self, comboBox, index): 23 | return self.processes[index].name 24 | 25 | def comboBox_completedString_(self, comboBox, uncompletedString): 26 | lowerName = uncompletedString.lower() 27 | for i, name in enumerate(self._processNames): 28 | if name.startswith(lowerName): 29 | return self.processes[i].name 30 | return None 31 | 32 | def comboBox_indexOfItemWithStringValue_(self, comboBox, value): 33 | return self._processIndexByName.get(value.lower(), NSNotFound) 34 | -------------------------------------------------------------------------------- /examples/cpushark/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a setup.py script generated by py2applet 3 | 4 | Usage: 5 | python setup.py py2app 6 | """ 7 | 8 | from setuptools import setup 9 | 10 | plist = dict( 11 | CFBundleShortVersionString="CpuShark v1", 12 | CFBundleIconFile="CpuShark.icns", 13 | CFBundleGetInfoString="CpuShark v1", 14 | CFBundleIdentifier="com.tillitech.CpuShark", 15 | CFBundleDocumentTypes=[], 16 | CFBundleName="CpuShark", 17 | ) 18 | 19 | setup( 20 | app=["CpuShark.py"], 21 | data_files=["MainMenu.xib", "MainWindow.xib"], 22 | options={"py2app": {"plist": plist}}, 23 | setup_requires=["py2app"], 24 | ) 25 | -------------------------------------------------------------------------------- /examples/crash_reporting.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import frida 4 | 5 | 6 | def on_process_crashed(crash): 7 | print("on_process_crashed") 8 | print("\tcrash:", crash) 9 | 10 | 11 | def on_detached(reason, crash): 12 | print("on_detached()") 13 | print("\treason:", reason) 14 | print("\tcrash:", crash) 15 | 16 | 17 | device = frida.get_usb_device() 18 | device.on("process-crashed", on_process_crashed) 19 | session = device.attach("Hello") 20 | session.on("detached", on_detached) 21 | print("[*] Ready") 22 | sys.stdin.read() 23 | -------------------------------------------------------------------------------- /examples/detached.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import frida 4 | 5 | 6 | def on_detached(): 7 | print("on_detached") 8 | 9 | 10 | def on_detached_with_reason(reason): 11 | print("on_detached_with_reason:", reason) 12 | 13 | 14 | def on_detached_with_varargs(*args): 15 | print("on_detached_with_varargs:", args) 16 | 17 | 18 | session = frida.attach("Twitter") 19 | print("attached") 20 | session.on("detached", on_detached) 21 | session.on("detached", on_detached_with_reason) 22 | session.on("detached", on_detached_with_varargs) 23 | sys.stdin.read() 24 | -------------------------------------------------------------------------------- /examples/enumerate_applications.py: -------------------------------------------------------------------------------- 1 | from pprint import pformat 2 | 3 | from pygments import highlight 4 | from pygments.formatters import Terminal256Formatter 5 | from pygments.lexers import PythonLexer 6 | 7 | import frida 8 | 9 | device = frida.get_usb_device() 10 | 11 | 12 | def trim_icon(icon): 13 | result = dict(icon) 14 | result["image"] = result["image"][0:16] + b"..." 15 | return result 16 | 17 | 18 | apps = device.enumerate_applications(scope="full") 19 | for app in apps: 20 | params = dict(app.parameters) 21 | if "icons" in params: 22 | params["icons"] = [trim_icon(icon) for icon in params["icons"]] 23 | parameters = highlight(pformat(params), PythonLexer(), Terminal256Formatter()).rstrip() 24 | print(f'Application(identifier="{app.identifier}", name="{app.name}", pid={app.pid}, parameters={parameters})') 25 | -------------------------------------------------------------------------------- /examples/enumerate_processes.py: -------------------------------------------------------------------------------- 1 | from pprint import pformat 2 | 3 | from pygments import highlight 4 | from pygments.formatters import Terminal256Formatter 5 | from pygments.lexers import PythonLexer 6 | 7 | import frida 8 | 9 | device = frida.get_usb_device() 10 | 11 | 12 | def trim_icon(icon): 13 | result = dict(icon) 14 | result["image"] = result["image"][0:16] + b"..." 15 | return result 16 | 17 | 18 | processes = device.enumerate_processes(scope="full") 19 | for proc in processes: 20 | params = dict(proc.parameters) 21 | if "icons" in params: 22 | params["icons"] = [trim_icon(icon) for icon in params["icons"]] 23 | print( 24 | f'Process(pid={proc.pid}, name="{proc.name}", parameters={highlight(pformat(params), PythonLexer(), Terminal256Formatter()).rstrip()})' 25 | ) 26 | -------------------------------------------------------------------------------- /examples/get_frontmost_application.py: -------------------------------------------------------------------------------- 1 | from pprint import pformat 2 | 3 | from pygments import highlight 4 | from pygments.formatters import Terminal256Formatter 5 | from pygments.lexers import PythonLexer 6 | 7 | import frida 8 | 9 | device = frida.get_usb_device() 10 | 11 | 12 | def trim_icon(icon): 13 | result = dict(icon) 14 | result["image"] = result["image"][0:16] + b"..." 15 | return result 16 | 17 | 18 | app = device.get_frontmost_application(scope="full") 19 | if app is not None: 20 | params = dict(app.parameters) 21 | if "icons" in params: 22 | params["icons"] = [trim_icon(icon) for icon in params["icons"]] 23 | print(f"{app.identifier}:", highlight(pformat(params), PythonLexer(), Terminal256Formatter())) 24 | else: 25 | print("No frontmost application") 26 | -------------------------------------------------------------------------------- /examples/inject_library/example.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void 4 | example_main (const char * data) 5 | { 6 | printf ("example_main called with data='%s'\n", data); 7 | } 8 | -------------------------------------------------------------------------------- /examples/inject_library/inject_blob.py: -------------------------------------------------------------------------------- 1 | # 2 | # Compile example.dylib like this: 3 | # $ clang -shared example.c -o example.dylib 4 | # 5 | # Then run: 6 | # $ python inject_blob.py Twitter example.dylib 7 | # 8 | 9 | import sys 10 | 11 | import frida 12 | 13 | 14 | def on_uninjected(id): 15 | print("on_uninjected id=%u" % id) 16 | 17 | 18 | (target, library_path) = sys.argv[1:] 19 | 20 | device = frida.get_local_device() 21 | device.on("uninjected", on_uninjected) 22 | with open(library_path, "rb") as library_file: 23 | library_blob = library_file.read() 24 | id = device.inject_library_blob(target, library_blob, "example_main", "w00t") 25 | print("*** Injected, id=%u -- hit Ctrl+D to exit!" % id) 26 | sys.stdin.read() 27 | -------------------------------------------------------------------------------- /examples/inject_library/inject_file.py: -------------------------------------------------------------------------------- 1 | # 2 | # Compile example.dylib like this: 3 | # $ clang -shared example.c -o ~/.Trash/example.dylib 4 | # 5 | # Then run: 6 | # $ python inject_file.py Twitter ~/.Trash/example.dylib 7 | # 8 | 9 | import sys 10 | 11 | import frida 12 | 13 | 14 | def on_uninjected(id): 15 | print("on_uninjected id=%u" % id) 16 | 17 | 18 | (target, library_path) = sys.argv[1:] 19 | 20 | device = frida.get_local_device() 21 | device.on("uninjected", on_uninjected) 22 | id = device.inject_library_file(target, library_path, "example_main", "w00t") 23 | print("*** Injected, id=%u -- hit Ctrl+D to exit!" % id) 24 | sys.stdin.read() 25 | -------------------------------------------------------------------------------- /examples/open_service/dtx/deviceinfo.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | 3 | import frida 4 | 5 | device = frida.get_usb_device() 6 | 7 | deviceinfo = device.open_service("dtx:com.apple.instruments.server.services.deviceinfo") 8 | response = deviceinfo.request({"method": "runningProcesses"}) 9 | pprint.pp(response) 10 | -------------------------------------------------------------------------------- /examples/open_service/dtx/opengl.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import frida 4 | 5 | 6 | def on_message(message): 7 | print("on_message:", message) 8 | 9 | 10 | device = frida.get_usb_device() 11 | 12 | opengl = device.open_service("dtx:com.apple.instruments.server.services.graphics.opengl") 13 | opengl.on("message", on_message) 14 | opengl.request( 15 | { 16 | "method": "setSamplingRate:", 17 | "args": [5.0], 18 | } 19 | ) 20 | opengl.request( 21 | { 22 | "method": "startSamplingAtTimeInterval:", 23 | "args": [0.0], 24 | } 25 | ) 26 | 27 | sys.stdin.read() 28 | -------------------------------------------------------------------------------- /examples/open_service/dtx/processcontrol.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import frida 4 | 5 | 6 | def on_message(message): 7 | print("on_message:", message) 8 | 9 | 10 | device = frida.get_usb_device() 11 | 12 | control = device.open_service("dtx:com.apple.instruments.server.services.processcontrol") 13 | control.on("message", on_message) 14 | pid = control.request( 15 | { 16 | "method": "launchSuspendedProcessWithDevicePath:bundleIdentifier:environment:arguments:options:", 17 | "args": [ 18 | "", 19 | "no.oleavr.HelloIOS", 20 | {}, 21 | [], 22 | { 23 | "StartSuspendedKey": False, 24 | }, 25 | ], 26 | } 27 | ) 28 | control.request({"method": "startObservingPid:", "args": [pid]}) 29 | 30 | print(f"App spawned, PID: {pid}. Kill it to see an example message being emitted.") 31 | sys.stdin.read() 32 | -------------------------------------------------------------------------------- /examples/open_service/dtx/screenshot.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import frida 4 | 5 | if len(sys.argv) != 2: 6 | print(f"Usage: {sys.argv[0]} outfile.png", file=sys.stderr) 7 | sys.exit(1) 8 | outfile = sys.argv[1] 9 | 10 | device = frida.get_usb_device() 11 | 12 | screenshot = device.open_service("dtx:com.apple.instruments.server.services.screenshot") 13 | png = screenshot.request({"method": "takeScreenshot"}) 14 | with open(outfile, "wb") as f: 15 | f.write(png) 16 | -------------------------------------------------------------------------------- /examples/open_service/dtx/sysmontap.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import frida 4 | 5 | 6 | def on_message(message): 7 | print("on_message:", message) 8 | 9 | 10 | device = frida.get_usb_device() 11 | 12 | sysmon = device.open_service("dtx:com.apple.instruments.server.services.sysmontap") 13 | sysmon.on("message", on_message) 14 | sysmon.request( 15 | { 16 | "method": "setConfig:", 17 | "args": [ 18 | { 19 | "ur": 1000, 20 | "cpuUsage": True, 21 | "sampleInterval": 1000000000, 22 | }, 23 | ], 24 | } 25 | ) 26 | sysmon.request({"method": "start"}) 27 | time.sleep(5) 28 | sysmon.request({"method": "stop"}) 29 | time.sleep(1) 30 | -------------------------------------------------------------------------------- /examples/open_service/plist.py: -------------------------------------------------------------------------------- 1 | import frida 2 | 3 | device = frida.get_usb_device() 4 | 5 | diag = device.open_service("plist:com.apple.mobile.diagnostics_relay") 6 | diag.request({"type": "query", "payload": {"Request": "Sleep", "WaitForDisconnect": True}}) 7 | diag.request({"type": "query", "payload": {"Request": "Goodbye"}}) 8 | -------------------------------------------------------------------------------- /examples/open_service/xpc/launchapplication.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | import sys 3 | from threading import Thread 4 | 5 | import frida 6 | 7 | 8 | def main(): 9 | device = frida.get_usb_device() 10 | 11 | stdout_uuid, stdout_stream = create_stdio_socket(device) 12 | stderr_uuid, stderr_stream = create_stdio_socket(device) 13 | 14 | appservice = device.open_service("xpc:com.apple.coredevice.appservice") 15 | response = appservice.request( 16 | { 17 | "CoreDevice.featureIdentifier": "com.apple.coredevice.feature.launchapplication", 18 | "CoreDevice.action": {}, 19 | "CoreDevice.input": { 20 | "applicationSpecifier": { 21 | "bundleIdentifier": {"_0": "no.oleavr.HelloIOS"}, 22 | }, 23 | "options": { 24 | "arguments": [], 25 | "environmentVariables": {}, 26 | "standardIOUsesPseudoterminals": True, 27 | "startStopped": False, 28 | "terminateExisting": True, 29 | "user": {"active": True}, 30 | "platformSpecificOptions": b'', 31 | }, 32 | "standardIOIdentifiers": { 33 | "standardOutput": ("uuid", stdout_uuid), 34 | "standardError": ("uuid", stderr_uuid), 35 | }, 36 | }, 37 | } 38 | ) 39 | pprint.pp(response) 40 | 41 | workers = set() 42 | for stream, sink in {(stdout_stream, sys.stdout), (stderr_stream, sys.stderr)}: 43 | t = Thread(target=process_console_output, args=(stream, sink)) 44 | t.start() 45 | workers.add(t) 46 | for worker in workers: 47 | worker.join() 48 | 49 | 50 | def create_stdio_socket(device): 51 | stream = device.open_channel("tcp:com.apple.coredevice.openstdiosocket") 52 | return (stream.read_all(16), stream) 53 | 54 | 55 | def process_console_output(stream, sink): 56 | while True: 57 | chunk = stream.read(4096) 58 | if not chunk: 59 | break 60 | sink.write(chunk.decode("utf-8")) 61 | 62 | 63 | if __name__ == "__main__": 64 | main() 65 | -------------------------------------------------------------------------------- /examples/open_service/xpc/listprocesses.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | 3 | import frida 4 | 5 | device = frida.get_usb_device() 6 | 7 | appservice = device.open_service("xpc:com.apple.coredevice.appservice") 8 | response = appservice.request( 9 | { 10 | "CoreDevice.featureIdentifier": "com.apple.coredevice.feature.listprocesses", 11 | "CoreDevice.action": {}, 12 | "CoreDevice.input": {}, 13 | } 14 | ) 15 | pprint.pp(response) 16 | -------------------------------------------------------------------------------- /examples/portal_client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | from frida_tools.application import Reactor 5 | 6 | import frida 7 | 8 | 9 | class Application: 10 | def __init__(self, nick): 11 | self._reactor = Reactor(run_until_return=self._process_input) 12 | 13 | token = {"nick": nick, "secret": "knock-knock"} 14 | self._device = frida.get_device_manager().add_remote_device("::1", token=json.dumps(token)) 15 | 16 | self._bus = self._device.bus 17 | self._bus.on("message", lambda *args: self._reactor.schedule(lambda: self._on_bus_message(*args))) 18 | 19 | self._channel = None 20 | self._prompt = "> " 21 | 22 | def run(self): 23 | self._reactor.schedule(self._start) 24 | self._reactor.run() 25 | 26 | def _start(self): 27 | self._bus.attach() 28 | 29 | def _process_input(self, reactor): 30 | while True: 31 | sys.stdout.write("\r") 32 | try: 33 | text = input(self._prompt).strip() 34 | except: 35 | self._reactor.cancel_io() 36 | return 37 | sys.stdout.write("\033[1A\033[K") 38 | sys.stdout.flush() 39 | 40 | if len(text) == 0: 41 | self._print("Processes:", self._device.enumerate_processes()) 42 | continue 43 | 44 | if text.startswith("/join "): 45 | if self._channel is not None: 46 | self._bus.post({"type": "part", "channel": self._channel}) 47 | channel = text[6:] 48 | self._channel = channel 49 | self._prompt = f"{channel} > " 50 | self._bus.post({"type": "join", "channel": channel}) 51 | continue 52 | 53 | if text.startswith("/announce "): 54 | self._bus.post({"type": "announce", "text": text[10:]}) 55 | continue 56 | 57 | if self._channel is not None: 58 | self._bus.post({"channel": self._channel, "type": "say", "text": text}) 59 | else: 60 | self._print("*** Need to /join a channel first") 61 | 62 | def _on_bus_message(self, message, data): 63 | mtype = message["type"] 64 | if mtype == "welcome": 65 | self._print("*** Welcome! Available channels:", repr(message["channels"])) 66 | elif mtype == "membership": 67 | self._print("*** Joined", message["channel"]) 68 | self._print( 69 | "- Members:\n\t" 70 | + "\n\t".join([f"{m['nick']} (connected from {m['address']})" for m in message["members"]]) 71 | ) 72 | for item in message["history"]: 73 | self._print(f"<{item['sender']}> {item['text']}") 74 | elif mtype == "join": 75 | user = message["user"] 76 | self._print(f"👋 {user['nick']} ({user['address']}) joined {message['channel']}") 77 | elif mtype == "part": 78 | user = message["user"] 79 | self._print(f"🚪 {user['nick']} ({user['address']}) left {message['channel']}") 80 | elif mtype == "chat": 81 | self._print(f"<{message['sender']}> {message['text']}") 82 | elif mtype == "announce": 83 | self._print(f"📣 <{message['sender']}> {message['text']}") 84 | else: 85 | self._print("Unhandled message:", message) 86 | 87 | def _print(self, *words): 88 | print("\r\033[K" + " ".join([str(word) for word in words])) 89 | sys.stdout.write(self._prompt) 90 | sys.stdout.flush() 91 | 92 | 93 | if __name__ == "__main__": 94 | nick = sys.argv[1] 95 | app = Application(nick) 96 | app.run() 97 | -------------------------------------------------------------------------------- /examples/portal_server.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | import json 4 | from pathlib import Path 5 | 6 | from frida_tools.application import Reactor 7 | 8 | import frida 9 | 10 | ENABLE_CONTROL_INTERFACE = True 11 | 12 | 13 | class Application: 14 | def __init__(self): 15 | self._reactor = Reactor(run_until_return=self._process_input) 16 | 17 | cluster_params = frida.EndpointParameters( 18 | address="unix:/Users/oleavr/src/cluster", 19 | certificate="/Users/oleavr/src/identity2.pem", 20 | authentication=("token", "wow-such-secret"), 21 | ) 22 | 23 | if ENABLE_CONTROL_INTERFACE: 24 | www = Path(__file__).parent.resolve() / "web_client" / "dist" 25 | control_params = frida.EndpointParameters( 26 | address="::1", port=27042, authentication=("callback", self._authenticate), asset_root=www 27 | ) 28 | else: 29 | control_params = None 30 | 31 | service = frida.PortalService(cluster_params, control_params) 32 | self._service = service 33 | self._device = service.device 34 | self._peers = {} 35 | self._nicks = set() 36 | self._channels = {} 37 | 38 | service.on("node-connected", lambda *args: self._reactor.schedule(lambda: self._on_node_connected(*args))) 39 | service.on("node-joined", lambda *args: self._reactor.schedule(lambda: self._on_node_joined(*args))) 40 | service.on("node-left", lambda *args: self._reactor.schedule(lambda: self._on_node_left(*args))) 41 | service.on("node-disconnected", lambda *args: self._reactor.schedule(lambda: self._on_node_disconnected(*args))) 42 | service.on( 43 | "controller-connected", lambda *args: self._reactor.schedule(lambda: self._on_controller_connected(*args)) 44 | ) 45 | service.on( 46 | "controller-disconnected", 47 | lambda *args: self._reactor.schedule(lambda: self._on_controller_disconnected(*args)), 48 | ) 49 | service.on("authenticated", lambda *args: self._reactor.schedule(lambda: self._on_authenticated(*args))) 50 | service.on("subscribe", lambda *args: self._reactor.schedule(lambda: self._on_subscribe(*args))) 51 | service.on("message", lambda *args: self._reactor.schedule(lambda: self._on_message(*args))) 52 | 53 | def run(self): 54 | self._reactor.schedule(self._start) 55 | self._reactor.run() 56 | 57 | def _start(self): 58 | self._service.start() 59 | 60 | self._device.enable_spawn_gating() 61 | 62 | def _stop(self): 63 | self._service.stop() 64 | 65 | def _process_input(self, reactor): 66 | while True: 67 | try: 68 | command = input("Enter command: ").strip() 69 | except KeyboardInterrupt: 70 | self._reactor.cancel_io() 71 | return 72 | 73 | if len(command) == 0: 74 | print("Processes:", self._device.enumerate_processes()) 75 | continue 76 | 77 | if command == "stop": 78 | self._reactor.schedule(self._stop) 79 | break 80 | 81 | def _authenticate(self, raw_token): 82 | try: 83 | token = json.loads(raw_token) 84 | nick = str(token["nick"]) 85 | secret = token["secret"].encode("utf-8") 86 | except: 87 | raise ValueError("invalid request") 88 | 89 | provided = hashlib.sha1(secret).digest() 90 | expected = hashlib.sha1(b"knock-knock").digest() 91 | if not hmac.compare_digest(provided, expected): 92 | raise ValueError("get outta here") 93 | 94 | return { 95 | "nick": nick, 96 | } 97 | 98 | def _on_node_connected(self, connection_id, remote_address): 99 | print("on_node_connected()", connection_id, remote_address) 100 | 101 | def _on_node_joined(self, connection_id, application): 102 | print("on_node_joined()", connection_id, application) 103 | print("\ttags:", self._service.enumerate_tags(connection_id)) 104 | 105 | def _on_node_left(self, connection_id, application): 106 | print("on_node_left()", connection_id, application) 107 | 108 | def _on_node_disconnected(self, connection_id, remote_address): 109 | print("on_node_disconnected()", connection_id, remote_address) 110 | 111 | def _on_controller_connected(self, connection_id, remote_address): 112 | print("on_controller_connected()", connection_id, remote_address) 113 | self._peers[connection_id] = Peer(connection_id, remote_address) 114 | 115 | def _on_controller_disconnected(self, connection_id, remote_address): 116 | print("on_controller_disconnected()", connection_id, remote_address) 117 | peer = self._peers.pop(connection_id) 118 | for channel in list(peer.memberships): 119 | channel.remove_member(peer) 120 | if peer.nick is not None: 121 | self._release_nick(peer.nick) 122 | 123 | def _on_authenticated(self, connection_id, session_info): 124 | print("on_authenticated()", connection_id, session_info) 125 | peer = self._peers.get(connection_id, None) 126 | if peer is None: 127 | return 128 | peer.nick = self._acquire_nick(session_info["nick"]) 129 | 130 | def _on_subscribe(self, connection_id): 131 | print("on_subscribe()", connection_id) 132 | self._service.post(connection_id, {"type": "welcome", "channels": list(self._channels.keys())}) 133 | 134 | def _on_message(self, connection_id, message, data): 135 | peer = self._peers[connection_id] 136 | 137 | mtype = message["type"] 138 | if mtype == "join": 139 | self._get_channel(message["channel"]).add_member(peer) 140 | elif mtype == "part": 141 | channel = self._channels.get(message["channel"], None) 142 | if channel is None: 143 | return 144 | channel.remove_member(peer) 145 | elif mtype == "say": 146 | channel = self._channels.get(message["channel"], None) 147 | if channel is None: 148 | return 149 | channel.post(message["text"], peer) 150 | elif mtype == "announce": 151 | self._service.broadcast({"type": "announce", "sender": peer.nick, "text": message["text"]}) 152 | else: 153 | print("Unhandled message:", message) 154 | 155 | def _acquire_nick(self, requested): 156 | candidate = requested 157 | serial = 2 158 | while candidate in self._nicks: 159 | candidate = requested + str(serial) 160 | serial += 1 161 | 162 | nick = candidate 163 | self._nicks.add(nick) 164 | 165 | return nick 166 | 167 | def _release_nick(self, nick): 168 | self._nicks.remove(nick) 169 | 170 | def _get_channel(self, name): 171 | channel = self._channels.get(name, None) 172 | if channel is None: 173 | channel = Channel(name, self._service) 174 | self._channels[name] = channel 175 | return channel 176 | 177 | 178 | class Peer: 179 | def __init__(self, connection_id, remote_address): 180 | self.nick = None 181 | self.connection_id = connection_id 182 | self.remote_address = remote_address 183 | self.memberships = set() 184 | 185 | def to_json(self): 186 | return {"nick": self.nick, "address": self.remote_address[0]} 187 | 188 | 189 | class Channel: 190 | def __init__(self, name, service): 191 | self.name = name 192 | self.members = set() 193 | self.history = [] 194 | 195 | self._service = service 196 | 197 | def add_member(self, peer): 198 | if self in peer.memberships: 199 | return 200 | 201 | peer.memberships.add(self) 202 | self.members.add(peer) 203 | 204 | self._service.narrowcast(self.name, {"type": "join", "channel": self.name, "user": peer.to_json()}) 205 | self._service.tag(peer.connection_id, self.name) 206 | 207 | self._service.post( 208 | peer.connection_id, 209 | { 210 | "type": "membership", 211 | "channel": self.name, 212 | "members": [peer.to_json() for peer in self.members], 213 | "history": self.history, 214 | }, 215 | ) 216 | 217 | def remove_member(self, peer): 218 | if self not in peer.memberships: 219 | return 220 | 221 | peer.memberships.remove(self) 222 | self.members.remove(peer) 223 | 224 | self._service.untag(peer.connection_id, self.name) 225 | self._service.narrowcast(self.name, {"type": "part", "channel": self.name, "user": peer.to_json()}) 226 | 227 | def post(self, text, peer): 228 | if self not in peer.memberships: 229 | return 230 | 231 | item = {"type": "chat", "sender": peer.nick, "text": text} 232 | 233 | self._service.narrowcast(self.name, item) 234 | 235 | history = self.history 236 | history.append(item) 237 | if len(history) == 20: 238 | history.pop(0) 239 | 240 | 241 | if __name__ == "__main__": 242 | app = Application() 243 | app.run() 244 | -------------------------------------------------------------------------------- /examples/query_system_parameters.py: -------------------------------------------------------------------------------- 1 | from pprint import pformat 2 | 3 | from pygments import highlight 4 | from pygments.formatters import Terminal256Formatter 5 | from pygments.lexers import PythonLexer 6 | 7 | import frida 8 | 9 | print("Local parameters:", highlight(pformat(frida.query_system_parameters()), PythonLexer(), Terminal256Formatter())) 10 | print( 11 | "USB device parameters:", 12 | highlight(pformat(frida.get_usb_device().query_system_parameters()), PythonLexer(), Terminal256Formatter()), 13 | ) 14 | -------------------------------------------------------------------------------- /examples/rpc.py: -------------------------------------------------------------------------------- 1 | import frida 2 | 3 | session = frida.attach("Twitter") 4 | script = session.create_script( 5 | """\ 6 | rpc.exports = { 7 | hello: function () { 8 | return 'Hello'; 9 | }, 10 | failPlease: function () { 11 | oops; 12 | } 13 | }; 14 | """ 15 | ) 16 | script.load() 17 | api = script.exports_sync 18 | print("api.hello() =>", api.hello()) 19 | api.fail_please() 20 | -------------------------------------------------------------------------------- /examples/session_persist_timeout.py: -------------------------------------------------------------------------------- 1 | from frida_tools.application import Reactor 2 | 3 | import frida 4 | 5 | 6 | class Application: 7 | def __init__(self): 8 | self._reactor = Reactor(run_until_return=self._process_input) 9 | 10 | self._device = None 11 | self._session = None 12 | 13 | def run(self): 14 | self._reactor.schedule(lambda: self._start()) 15 | self._reactor.run() 16 | 17 | def _start(self): 18 | device = frida.get_remote_device() 19 | self._device = device 20 | 21 | session = self._device.attach("hello2", persist_timeout=30) 22 | self._session = session 23 | session.on("detached", lambda *args: self._reactor.schedule(lambda: self._on_detached(*args))) 24 | 25 | script = session.create_script( 26 | """ 27 | let _puts = null; 28 | 29 | Interceptor.attach(DebugSymbol.getFunctionByName('f'), { 30 | onEnter(args) { 31 | const n = args[0].toInt32(); 32 | send(n); 33 | } 34 | }); 35 | 36 | rpc.exports.dispose = () => { 37 | puts('Script unloaded'); 38 | }; 39 | 40 | let serial = 1; 41 | setInterval(() => { 42 | puts(`Agent still here! serial=${serial++}`); 43 | }, 5000); 44 | 45 | function puts(s) { 46 | if (_puts === null) { 47 | _puts = new NativeFunction(Module.getExportByName(null, 'puts'), 'int', ['pointer']); 48 | } 49 | _puts(Memory.allocUtf8String(s)); 50 | } 51 | """ 52 | ) 53 | self._script = script 54 | script.on("message", lambda *args: self._reactor.schedule(lambda: self._on_message(*args))) 55 | script.load() 56 | 57 | def _process_input(self, reactor): 58 | while True: 59 | try: 60 | command = input("> ").strip() 61 | except: 62 | self._reactor.cancel_io() 63 | return 64 | 65 | if command == "resume": 66 | try: 67 | self._session.resume() 68 | except Exception as e: 69 | print("Failed to resume:", e) 70 | else: 71 | print("Unknown command") 72 | 73 | def _on_detached(self, reason, crash): 74 | print(f"⚡ detached: reason={reason}, crash={crash}") 75 | 76 | def _on_message(self, message, data): 77 | print(f"⚡ message: {message}") 78 | 79 | 80 | app = Application() 81 | app.run() 82 | -------------------------------------------------------------------------------- /examples/snapshot.py: -------------------------------------------------------------------------------- 1 | import frida 2 | 3 | embed_script = """ 4 | const button = { 5 | color: 'blue', 6 | }; 7 | 8 | function mutateButton() { 9 | button.color = 'red'; 10 | } 11 | """ 12 | 13 | warmup_script = """ 14 | mutateButton(); 15 | """ 16 | 17 | test_script = """ 18 | console.log('Button before:', JSON.stringify(button)); 19 | mutateButton(); 20 | console.log('Button after:', JSON.stringify(button)); 21 | """ 22 | 23 | runtime = "v8" 24 | 25 | 26 | session = frida.attach(0) 27 | 28 | snapshot = session.snapshot_script(embed_script, warmup_script=warmup_script, runtime=runtime) 29 | 30 | 31 | def on_message(message, data): 32 | print("on_message:", message) 33 | 34 | 35 | script = session.create_script(test_script, snapshot=snapshot, runtime=runtime) 36 | script.on("message", on_message) 37 | script.load() 38 | -------------------------------------------------------------------------------- /examples/unpair.py: -------------------------------------------------------------------------------- 1 | import frida 2 | 3 | device = frida.get_usb_device() 4 | device.unpair() 5 | -------------------------------------------------------------------------------- /examples/web_client/.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /examples/web_client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /examples/web_client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frida-web-client-example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "vue-cli-service build", 7 | "watch": "vue-cli-service build --watch" 8 | }, 9 | "dependencies": { 10 | "core-js": "^3.6.5", 11 | "dbus-next": "frida/node-dbus-next", 12 | "duplexify": "^4.1.1", 13 | "vue": "^2.6.11", 14 | "vue-class-component": "^7.2.3", 15 | "vue-property-decorator": "^9.1.2", 16 | "vuex": "^3.4.0" 17 | }, 18 | "devDependencies": { 19 | "@babel/plugin-transform-object-assign": "^7.12.13", 20 | "@types/duplexify": "^3.6.0", 21 | "@vue/cli-plugin-babel": "~5.0.8", 22 | "@vue/cli-plugin-typescript": "~5.0.8", 23 | "@vue/cli-plugin-vuex": "~5.0.8", 24 | "@vue/cli-service": "~5.0.8", 25 | "typescript": "~4.1.5", 26 | "vue-template-compiler": "^2.6.11" 27 | }, 28 | "browserslist": [ 29 | "> 1%", 30 | "last 2 versions", 31 | "not dead" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /examples/web_client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frida/frida-python/82ee16d299671558728fd1cbc9680f404d04fc92/examples/web_client/public/favicon.ico -------------------------------------------------------------------------------- /examples/web_client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/web_client/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 30 | -------------------------------------------------------------------------------- /examples/web_client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frida/frida-python/82ee16d299671558728fd1cbc9680f404d04fc92/examples/web_client/src/assets/logo.png -------------------------------------------------------------------------------- /examples/web_client/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 42 | 43 | 44 | 60 | -------------------------------------------------------------------------------- /examples/web_client/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import store from './store' 4 | 5 | Vue.config.productionTip = false; 6 | 7 | new Vue({ 8 | store, 9 | render: h => h(App) 10 | }).$mount('#app'); 11 | -------------------------------------------------------------------------------- /examples/web_client/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/web_client/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /examples/web_client/src/shims/abstract-socket.js: -------------------------------------------------------------------------------- 1 | // Intentionally left blank. 2 | -------------------------------------------------------------------------------- /examples/web_client/src/shims/x11.js: -------------------------------------------------------------------------------- 1 | // Intentionally left blank. 2 | -------------------------------------------------------------------------------- /examples/web_client/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | import frida from './modules/frida'; 5 | import fridaBus from './plugins/frida'; 6 | 7 | Vue.use(Vuex); 8 | 9 | export default new Vuex.Store({ 10 | modules: { 11 | frida 12 | }, 13 | plugins: [ 14 | fridaBus() 15 | ] 16 | }); 17 | -------------------------------------------------------------------------------- /examples/web_client/src/store/modules/frida.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'vuex'; 2 | 3 | interface FridaState { 4 | processes: Process[], 5 | } 6 | 7 | type Process = [number, string]; 8 | 9 | const fridaModule: Module = { 10 | state: { 11 | processes: [] 12 | }, 13 | 14 | mutations: { 15 | } 16 | }; 17 | 18 | export default fridaModule; 19 | -------------------------------------------------------------------------------- /examples/web_client/src/store/plugins/frida.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'vuex'; 2 | 3 | import dbus from 'dbus-next'; 4 | import events from 'events'; 5 | import wrapEventStream from './message-event-stream'; 6 | 7 | const { 8 | Interface, method, 9 | } = dbus.interface; 10 | 11 | const { Variant } = dbus; 12 | 13 | async function start() { 14 | const ws = wrapEventStream(new WebSocket(`ws://${location.host}/ws`)); 15 | const bus = dbus.peerBus(ws, { 16 | authMethods: [], 17 | }); 18 | let peerBus: dbus.MessageBus | null = null; 19 | 20 | const authServiceObj = await bus.getProxyObject('re.frida.AuthenticationService15', '/re/frida/AuthenticationService'); 21 | const authService = authServiceObj.getInterface('re.frida.AuthenticationService15'); 22 | 23 | const token = JSON.stringify({ 24 | nick: 'SomeoneOnTheWeb', 25 | secret: 'knock-knock' 26 | }); 27 | await authService.authenticate(token); 28 | 29 | const hostSessionObj = await bus.getProxyObject('re.frida.HostSession15', '/re/frida/HostSession'); 30 | const hostSession = hostSessionObj.getInterface('re.frida.HostSession15'); 31 | 32 | const processes: HostProcessInfo[] = await hostSession.enumerateProcesses({}); 33 | console.log('Got processes:', processes); 34 | 35 | const target = processes.find(([, name]) => name === 'hello2'); 36 | if (target === undefined) { 37 | throw new Error('Target process not found'); 38 | } 39 | const [pid] = target; 40 | console.log('Got PID:', pid); 41 | 42 | const sessionId: AgentSessionId = await hostSession.attach(pid, { 'persist-timeout': new Variant('u', 300) }); 43 | 44 | let agentSessionObj = await bus.getProxyObject('re.frida.AgentSession15', '/re/frida/AgentSession/' + sessionId[0]); 45 | let agentSession = agentSessionObj.getInterface('re.frida.AgentSession15'); 46 | 47 | const sink = new MessageSink('re.frida.AgentMessageSink15'); 48 | bus.export('/re/frida/AgentMessageSink/' + sessionId[0], sink); 49 | 50 | const scriptId: AgentScriptId = await agentSession.createScript(` 51 | const _puts = new NativeFunction(Module.getExportByName(null, 'puts'), 'int', ['pointer']); 52 | 53 | function puts(s) { 54 | _puts(Memory.allocUtf8String(s)); 55 | } 56 | 57 | send({ name: 'Joe' }); 58 | puts('Hello World from the browser!'); 59 | 60 | let n = 1; 61 | setInterval(() => { 62 | send({ n }); 63 | n++; 64 | }, 1000); 65 | `, {}); 66 | await agentSession.loadScript(scriptId); 67 | 68 | await agentSession.beginMigration(); 69 | 70 | const peerConnection = new RTCPeerConnection(); 71 | 72 | const pendingLocalCandidates = new IceCandidateQueue(); 73 | pendingLocalCandidates.on('add', (candidates: RTCIceCandidate[]) => { 74 | agentSession.addCandidates(candidates.map(c => c.candidate)); 75 | }); 76 | pendingLocalCandidates.once('done', () => { 77 | agentSession.notifyCandidateGatheringDone(); 78 | }); 79 | 80 | const pendingRemoteCandidates = new IceCandidateQueue(); 81 | pendingRemoteCandidates.on('add', candidates => { 82 | for (const candidate of candidates) { 83 | peerConnection.addIceCandidate(candidate); 84 | } 85 | }); 86 | pendingRemoteCandidates.once('done', () => { 87 | peerConnection.addIceCandidate(new RTCIceCandidate({ 88 | candidate: "", 89 | sdpMid: "0", 90 | sdpMLineIndex: 0 91 | })); 92 | }); 93 | 94 | peerConnection.oniceconnectionstatechange = e => { 95 | console.log("ICE connection state changed:", peerConnection.iceConnectionState); 96 | }; 97 | peerConnection.onicegatheringstatechange = e => { 98 | console.log("ICE gathering state changed:", peerConnection.iceGatheringState); 99 | }; 100 | peerConnection.onicecandidate = e => { 101 | pendingLocalCandidates.add(e.candidate); 102 | }; 103 | agentSession.on('newCandidates', (sdps: string[]) => { 104 | for (const sdp of sdps) { 105 | pendingRemoteCandidates.add(new RTCIceCandidate({ 106 | candidate: sdp, 107 | sdpMid: "0", 108 | sdpMLineIndex: 0 109 | })); 110 | } 111 | }); 112 | agentSession.on('candidateGatheringDone', () => { 113 | pendingRemoteCandidates.add(null); 114 | }); 115 | 116 | const peerChannel = peerConnection.createDataChannel('session'); 117 | peerChannel.onopen = async event => { 118 | console.log('[PeerChannel] onopen()'); 119 | 120 | peerBus = dbus.peerBus(wrapEventStream(peerChannel), { 121 | authMethods: [], 122 | }); 123 | 124 | const peerAgentSessionObj = await peerBus.getProxyObject('re.frida.AgentSession15', '/re/frida/AgentSession'); 125 | const peerAgentSession = peerAgentSessionObj.getInterface('re.frida.AgentSession15'); 126 | 127 | peerBus.export('/re/frida/AgentMessageSink', sink); 128 | 129 | await agentSession.commitMigration(); 130 | 131 | agentSessionObj = peerAgentSessionObj; 132 | agentSession = peerAgentSession; 133 | 134 | console.log('Yay, migrated to p2p!'); 135 | }; 136 | peerChannel.onclose = event => { 137 | console.log('[PeerChannel] onclose()'); 138 | }; 139 | peerChannel.onerror = event => { 140 | console.log('[PeerChannel] onerror()'); 141 | }; 142 | const offer = await peerConnection.createOffer(); 143 | await peerConnection.setLocalDescription(offer); 144 | 145 | const answerSdp = await agentSession.offerPeerConnection(offer.sdp, {}); 146 | const answer = new RTCSessionDescription({ type: "answer", sdp: answerSdp }); 147 | await peerConnection.setRemoteDescription(answer); 148 | 149 | pendingLocalCandidates.notifySessionStarted(); 150 | pendingRemoteCandidates.notifySessionStarted(); 151 | } 152 | 153 | type HostProcessInfo = [pid: string, name: string, smallIcon: ImageData, largeIcon: ImageData]; 154 | type ImageData = [width: number, height: number, rowstride: number, pixels: string]; 155 | type AgentSessionId = [handle: string]; 156 | type AgentScriptId = [handle: string]; 157 | type AgentMessage = [kind: number, scriptId: AgentScriptId, text: string, hasData: boolean, data: number[]]; 158 | enum AgentMessageKind { 159 | Script = 1, 160 | Debugger 161 | } 162 | 163 | class MessageSink extends Interface { 164 | @method({ inSignature: 'a(i(u)sbay)u' }) 165 | postMessages(messages: AgentMessage[], batchId: number): void { 166 | for (const [kind, scriptId, text, hasData, data] of messages) { 167 | if (kind === AgentMessageKind.Script) { 168 | const message = JSON.parse(text); 169 | console.log("Got message:", message); 170 | } 171 | } 172 | } 173 | } 174 | 175 | class IceCandidateQueue extends events.EventEmitter { 176 | private sessionState: 'starting' | 'started' = 'starting'; 177 | private gatheringState: 'gathering' | 'gathered' | 'notified' = 'gathering'; 178 | private pending: RTCIceCandidate[] = []; 179 | private timer: NodeJS.Timeout | null = null; 180 | 181 | add(candidate: RTCIceCandidate | null) { 182 | if (candidate !== null) { 183 | this.pending.push(candidate); 184 | } else { 185 | this.gatheringState = 'gathered'; 186 | } 187 | 188 | if (this.timer === null) { 189 | this.timer = setTimeout(this.maybeEmitCandidates, 10); 190 | } 191 | } 192 | 193 | notifySessionStarted() { 194 | this.sessionState = 'started'; 195 | 196 | if (this.timer !== null) { 197 | clearTimeout(this.timer); 198 | this.timer = null; 199 | } 200 | 201 | this.maybeEmitCandidates(); 202 | } 203 | 204 | private maybeEmitCandidates = () => { 205 | this.timer = null; 206 | 207 | if (this.sessionState !== 'started') { 208 | return; 209 | } 210 | 211 | if (this.pending.length > 0) { 212 | this.emit('add', this.pending.splice(0)); 213 | } 214 | 215 | if (this.gatheringState === 'gathered') { 216 | this.emit('done'); 217 | this.gatheringState = 'notified'; 218 | } 219 | }; 220 | } 221 | 222 | export default function createFridaBusPlugin(): Plugin { 223 | return (store: any) => { 224 | start().catch(e => { 225 | console.error(e); 226 | }); 227 | }; 228 | } -------------------------------------------------------------------------------- /examples/web_client/src/store/plugins/message-event-stream.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Browser-only version of https://github.com/maxogden/websocket-stream, 3 | * adapted to support both WebSocket and RTCDataChannel. 4 | */ 5 | 6 | import duplexify from "duplexify"; 7 | import { Duplex, Transform, TransformCallback } from "stream"; 8 | 9 | export interface Options { 10 | bufferSize?: number; 11 | bufferTimeout?: number; 12 | } 13 | 14 | export default function wrapEventStream(target: WebSocket | RTCDataChannel, options: Options = {}): Duplex { 15 | const proxy = new Transform(); 16 | proxy._write = write; 17 | proxy._writev = writev; 18 | proxy._flush = flush; 19 | 20 | const bufferSize = options.bufferSize ?? 1024 * 512; 21 | const bufferTimeout = options.bufferTimeout ?? 1000; 22 | 23 | let stream: Transform | duplexify.Duplexify; 24 | const openStateValue = ('OPEN' in target) ? target.OPEN : 'open'; 25 | if (target.readyState === openStateValue) { 26 | stream = proxy; 27 | } else { 28 | stream = duplexify(); 29 | stream._writev = writev; 30 | 31 | target.addEventListener('open', onOpen); 32 | } 33 | 34 | target.binaryType = 'arraybuffer'; 35 | 36 | target.addEventListener('close', onClose); 37 | target.addEventListener('error', onError as EventListener); 38 | target.addEventListener('message', onMessage as EventListener); 39 | 40 | proxy.on('close', destroy); 41 | 42 | function write(chunk: ArrayBuffer | string, encoding: BufferEncoding, callback: (error?: Error | null) => void) { 43 | if (target.bufferedAmount > bufferSize) { 44 | setTimeout(write, bufferTimeout, chunk, encoding, callback); 45 | return; 46 | } 47 | 48 | if (typeof chunk === 'string') { 49 | chunk = Buffer.from(chunk, 'utf8'); 50 | } 51 | 52 | try { 53 | target.send(chunk); 54 | } catch (e) { 55 | return callback(e); 56 | } 57 | 58 | callback(); 59 | } 60 | 61 | function writev (this: Duplex, chunks: { chunk: any, encoding: BufferEncoding }[], callback: (error?: Error | null) => void) { 62 | const buffers = chunks.map(({ chunk }) => (typeof chunk === 'string') ? Buffer.from(chunk, 'utf8') : chunk); 63 | this._write(Buffer.concat(buffers), 'binary', callback); 64 | } 65 | 66 | function flush(callback: TransformCallback) { 67 | target.close(); 68 | callback(); 69 | } 70 | 71 | function onOpen() { 72 | const ds = stream as duplexify.Duplexify; 73 | ds.setReadable(proxy); 74 | ds.setWritable(proxy); 75 | stream.emit('connect'); 76 | } 77 | 78 | function onClose() { 79 | stream.end(); 80 | stream.destroy(); 81 | } 82 | 83 | function onError(event: ErrorEvent) { 84 | stream.destroy(new Error(event.message)); 85 | } 86 | 87 | function onMessage(event: MessageEvent) { 88 | const { data } = event; 89 | proxy.push((data instanceof ArrayBuffer) ? Buffer.from(data) : Buffer.from(data, 'utf8')); 90 | } 91 | 92 | function destroy() { 93 | target.close(); 94 | } 95 | 96 | return stream; 97 | } 98 | -------------------------------------------------------------------------------- /examples/web_client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /examples/web_client/vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | configureWebpack: { 5 | resolve: { 6 | alias: { 7 | 'abstract-socket': path.resolve(__dirname, 'src', 'shims', 'abstract-socket.js'), 8 | 'x11': path.resolve(__dirname, 'src', 'shims', 'x11.js') 9 | } 10 | } 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /frida/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict, List, Optional, Tuple, Union 2 | 3 | try: 4 | from . import _frida 5 | except Exception as ex: 6 | print("") 7 | print("***") 8 | if str(ex).startswith("No module named "): 9 | print("Frida native extension not found") 10 | print("Please check your PYTHONPATH.") 11 | else: 12 | print(f"Failed to load the Frida native extension: {ex}") 13 | print("Please ensure that the extension was compiled correctly") 14 | print("***") 15 | print("") 16 | raise ex 17 | from . import core 18 | 19 | __version__: str = _frida.__version__ 20 | 21 | get_device_manager = core.get_device_manager 22 | Relay = _frida.Relay 23 | PortalService = core.PortalService 24 | EndpointParameters = core.EndpointParameters 25 | Compiler = core.Compiler 26 | FileMonitor = _frida.FileMonitor 27 | Cancellable = core.Cancellable 28 | 29 | ServerNotRunningError = _frida.ServerNotRunningError 30 | ExecutableNotFoundError = _frida.ExecutableNotFoundError 31 | ExecutableNotSupportedError = _frida.ExecutableNotSupportedError 32 | ProcessNotFoundError = _frida.ProcessNotFoundError 33 | ProcessNotRespondingError = _frida.ProcessNotRespondingError 34 | InvalidArgumentError = _frida.InvalidArgumentError 35 | InvalidOperationError = _frida.InvalidOperationError 36 | PermissionDeniedError = _frida.PermissionDeniedError 37 | AddressInUseError = _frida.AddressInUseError 38 | TimedOutError = _frida.TimedOutError 39 | NotSupportedError = _frida.NotSupportedError 40 | ProtocolError = _frida.ProtocolError 41 | TransportError = _frida.TransportError 42 | OperationCancelledError = _frida.OperationCancelledError 43 | 44 | 45 | def query_system_parameters() -> Dict[str, Any]: 46 | """ 47 | Returns a dictionary of information about the host system 48 | """ 49 | 50 | return get_local_device().query_system_parameters() 51 | 52 | 53 | def spawn( 54 | program: Union[str, List[Union[str, bytes]], Tuple[Union[str, bytes]]], 55 | argv: Union[None, List[Union[str, bytes]], Tuple[Union[str, bytes]]] = None, 56 | envp: Optional[Dict[str, str]] = None, 57 | env: Optional[Dict[str, str]] = None, 58 | cwd: Optional[str] = None, 59 | stdio: Optional[str] = None, 60 | **kwargs: Any, 61 | ) -> int: 62 | """ 63 | Spawn a process into an attachable state 64 | """ 65 | 66 | return get_local_device().spawn(program=program, argv=argv, envp=envp, env=env, cwd=cwd, stdio=stdio, **kwargs) 67 | 68 | 69 | def resume(target: core.ProcessTarget) -> None: 70 | """ 71 | Resume a process from the attachable state 72 | :param target: the PID or name of the process 73 | """ 74 | 75 | get_local_device().resume(target) 76 | 77 | 78 | def kill(target: core.ProcessTarget) -> None: 79 | """ 80 | Kill a process 81 | :param target: the PID or name of the process 82 | """ 83 | 84 | get_local_device().kill(target) 85 | 86 | 87 | def attach( 88 | target: core.ProcessTarget, realm: Optional[str] = None, persist_timeout: Optional[int] = None 89 | ) -> core.Session: 90 | """ 91 | Attach to a process 92 | :param target: the PID or name of the process 93 | """ 94 | 95 | return get_local_device().attach(target, realm=realm, persist_timeout=persist_timeout) 96 | 97 | 98 | def inject_library_file(target: core.ProcessTarget, path: str, entrypoint: str, data: str) -> int: 99 | """ 100 | Inject a library file to a process. 101 | :param target: the PID or name of the process 102 | """ 103 | 104 | return get_local_device().inject_library_file(target, path, entrypoint, data) 105 | 106 | 107 | def inject_library_blob(target: core.ProcessTarget, blob: bytes, entrypoint: str, data: str) -> int: 108 | """ 109 | Inject a library blob to a process 110 | :param target: the PID or name of the process 111 | """ 112 | 113 | return get_local_device().inject_library_blob(target, blob, entrypoint, data) 114 | 115 | 116 | def get_local_device() -> core.Device: 117 | """ 118 | Get the local device 119 | """ 120 | 121 | return get_device_manager().get_local_device() 122 | 123 | 124 | def get_remote_device() -> core.Device: 125 | """ 126 | Get the first remote device in the devices list 127 | """ 128 | 129 | return get_device_manager().get_remote_device() 130 | 131 | 132 | def get_usb_device(timeout: int = 0) -> core.Device: 133 | """ 134 | Get the first device connected over USB in the devices list 135 | """ 136 | 137 | return get_device_manager().get_usb_device(timeout) 138 | 139 | 140 | def get_device(id: Optional[str], timeout: int = 0) -> core.Device: 141 | """ 142 | Get a device by its id 143 | """ 144 | 145 | return get_device_manager().get_device(id, timeout) 146 | 147 | 148 | def get_device_matching(predicate: Callable[[core.Device], bool], timeout: int = 0) -> core.Device: 149 | """ 150 | Get device matching predicate. 151 | :param predicate: a function to filter the devices 152 | :param timeout: operation timeout in seconds 153 | """ 154 | 155 | return get_device_manager().get_device_matching(predicate, timeout) 156 | 157 | 158 | def enumerate_devices() -> List[core.Device]: 159 | """ 160 | Enumerate all the devices from the device manager 161 | """ 162 | 163 | return get_device_manager().enumerate_devices() 164 | 165 | 166 | @core.cancellable 167 | def shutdown() -> None: 168 | """ 169 | Shutdown the main device manager 170 | """ 171 | 172 | get_device_manager()._impl.close() 173 | -------------------------------------------------------------------------------- /frida/_frida/__init__.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union 2 | 3 | # Exceptions 4 | class AddressInUseError(Exception): ... 5 | class ExecutableNotFoundError(Exception): ... 6 | class ExecutableNotSupportedError(Exception): ... 7 | class ServerNotRunningError(Exception): ... 8 | class TimedOutError(Exception): ... 9 | class TransportError(Exception): ... 10 | class ProcessNotFoundError(Exception): ... 11 | class ProcessNotRespondingError(Exception): ... 12 | class ProtocolError(Exception): ... 13 | class InvalidArgumentError(Exception): ... 14 | class InvalidOperationError(Exception): ... 15 | class NotSupportedError(Exception): ... 16 | class OperationCancelledError(Exception): ... 17 | class PermissionDeniedError(Exception): ... 18 | 19 | class Object: 20 | def __init__(self, *args: Any, **kwargs: Any) -> None: ... 21 | def on(self, signal: str, callback: Callable[..., Any]) -> None: 22 | """ 23 | Add a signal handler. 24 | """ 25 | ... 26 | 27 | def off(self, signal: str, callback: Callable[..., Any]) -> None: 28 | """ 29 | Remove a signal handler. 30 | """ 31 | ... 32 | 33 | class Application(Object): 34 | @property 35 | def identifier(self) -> str: 36 | """ 37 | Application identifier. 38 | """ 39 | ... 40 | 41 | @property 42 | def name(self) -> str: 43 | """ 44 | Human-readable application name. 45 | """ 46 | ... 47 | 48 | @property 49 | def parameters(self) -> Dict[str, Any]: 50 | """ 51 | Parameters. 52 | """ 53 | ... 54 | 55 | @property 56 | def pid(self) -> int: 57 | """ 58 | Process ID, or 0 if not running. 59 | """ 60 | ... 61 | 62 | class Bus(Object): 63 | def attach(self) -> None: 64 | """ 65 | Attach to the bus. 66 | """ 67 | ... 68 | 69 | def post(self, message: str, data: Optional[Union[bytes, str]]) -> None: 70 | """ 71 | Post a JSON-encoded message to the bus. 72 | """ 73 | ... 74 | 75 | class Cancellable(Object): 76 | def cancel(self) -> None: 77 | """ 78 | Set cancellable to cancelled. 79 | """ 80 | ... 81 | 82 | def connect(self, callback: Callable[..., Any]) -> int: 83 | """ 84 | Register notification callback. 85 | """ 86 | ... 87 | 88 | def disconnect(self, handler_id: int) -> None: 89 | """ 90 | Unregister notification callback. 91 | """ 92 | ... 93 | 94 | @classmethod 95 | def get_current(cls) -> "Cancellable": 96 | """ 97 | Get the top cancellable from the stack. 98 | """ 99 | ... 100 | 101 | def get_fd(self) -> int: 102 | """ 103 | Get file descriptor for integrating with an event loop. 104 | """ 105 | ... 106 | 107 | def is_cancelled(self) -> bool: 108 | """ 109 | Query whether cancellable has been cancelled. 110 | """ 111 | ... 112 | 113 | def pop_current(self) -> None: 114 | """ 115 | Pop cancellable off the cancellable stack. 116 | """ 117 | ... 118 | 119 | def push_current(self) -> None: 120 | """ 121 | Push cancellable onto the cancellable stack. 122 | """ 123 | ... 124 | 125 | def raise_if_cancelled(self) -> None: 126 | """ 127 | Raise an exception if cancelled. 128 | """ 129 | ... 130 | 131 | def release_fd(self) -> None: 132 | """ 133 | Release a resource previously allocated by get_fd(). 134 | """ 135 | ... 136 | 137 | class Child(Object): 138 | @property 139 | def argv(self) -> List[str]: 140 | """ 141 | Argument vector. 142 | """ 143 | ... 144 | 145 | @property 146 | def envp(self) -> Dict[str, str]: 147 | """ 148 | Environment vector. 149 | """ 150 | ... 151 | 152 | @property 153 | def identifier(self) -> str: 154 | """ 155 | Application identifier. 156 | """ 157 | ... 158 | 159 | @property 160 | def origin(self) -> str: 161 | """ 162 | Origin. 163 | """ 164 | ... 165 | 166 | @property 167 | def parent_pid(self) -> int: 168 | """ 169 | Parent Process ID. 170 | """ 171 | ... 172 | 173 | @property 174 | def path(self) -> str: 175 | """ 176 | Path of executable. 177 | """ 178 | ... 179 | 180 | @property 181 | def pid(self) -> int: 182 | """ 183 | Process ID. 184 | """ 185 | ... 186 | 187 | class Crash(Object): 188 | @property 189 | def parameters(self) -> Dict[str, Any]: 190 | """ 191 | Parameters. 192 | """ 193 | ... 194 | 195 | @property 196 | def pid(self) -> int: 197 | """ 198 | Process ID. 199 | """ 200 | ... 201 | 202 | @property 203 | def process_name(self) -> str: 204 | """ 205 | Process name. 206 | """ 207 | ... 208 | 209 | @property 210 | def report(self) -> str: 211 | """ 212 | Human-readable crash report. 213 | """ 214 | ... 215 | 216 | @property 217 | def summary(self) -> str: 218 | """ 219 | Human-readable crash summary. 220 | """ 221 | ... 222 | 223 | class Device(Object): 224 | @property 225 | def id(self) -> Optional[str]: 226 | """ 227 | Device ID. 228 | """ 229 | ... 230 | 231 | @property 232 | def name(self) -> Optional[str]: 233 | """ 234 | Human-readable device name. 235 | """ 236 | ... 237 | 238 | @property 239 | def icon(self) -> Optional[Any]: 240 | """ 241 | Icon. 242 | """ 243 | ... 244 | 245 | @property 246 | def type(self) -> Optional[str]: 247 | """ 248 | Device type. One of: local, remote, usb. 249 | """ 250 | ... 251 | 252 | @property 253 | def bus(self) -> Optional[Bus]: 254 | """ 255 | Message bus. 256 | """ 257 | ... 258 | 259 | def attach(self, pid: int, realm: Optional[str] = None, persist_timeout: Optional[int] = None) -> "Session": 260 | """ 261 | Attach to a PID. 262 | """ 263 | ... 264 | 265 | def disable_spawn_gating(self) -> None: 266 | """ 267 | Disable spawn gating. 268 | """ 269 | ... 270 | 271 | def enable_spawn_gating(self) -> None: 272 | """ 273 | Enable spawn gating. 274 | """ 275 | ... 276 | 277 | def enumerate_applications( 278 | self, identifiers: Optional[Sequence[str]] = None, scope: Optional[str] = None 279 | ) -> List[Application]: 280 | """ 281 | Enumerate applications. 282 | """ 283 | ... 284 | 285 | def enumerate_pending_children(self) -> List[Child]: 286 | """ 287 | Enumerate pending children. 288 | """ 289 | ... 290 | 291 | def enumerate_pending_spawn(self) -> List["Spawn"]: 292 | """ 293 | Enumerate pending spawn. 294 | """ 295 | ... 296 | 297 | def enumerate_processes(self, pids: Optional[Sequence[int]] = None, scope: Optional[str] = None) -> List[Process]: 298 | """ 299 | Enumerate processes. 300 | """ 301 | ... 302 | 303 | def get_frontmost_application(self, scope: Optional[str] = None) -> Optional[Application]: 304 | """ 305 | Get details about the frontmost application. 306 | """ 307 | ... 308 | 309 | def inject_library_blob(self, pid: int, blob_buffer: bytes, entrypoint: str, data: str) -> int: 310 | """ 311 | Inject a library blob to a PID. 312 | """ 313 | ... 314 | 315 | def inject_library_file(self, pid: int, path: str, entrypoint: str, data: str) -> int: 316 | """ 317 | Inject a library file to a PID. 318 | """ 319 | ... 320 | 321 | def input(self, pid: int, data: bytes) -> None: 322 | """ 323 | Input data on stdin of a spawned process. 324 | """ 325 | ... 326 | 327 | def is_lost(self) -> bool: 328 | """ 329 | Query whether the device has been lost. 330 | """ 331 | ... 332 | 333 | def kill(self, pid: int) -> None: 334 | """ 335 | Kill a PID. 336 | """ 337 | ... 338 | 339 | def open_channel(self, address: str) -> "IOStream": 340 | """ 341 | Open a device-specific communication channel. 342 | """ 343 | ... 344 | 345 | def open_service(self, address: str) -> Service: 346 | """ 347 | Open a device-specific service. 348 | """ 349 | ... 350 | 351 | def unpair(self) -> None: 352 | """ 353 | Unpair device. 354 | """ 355 | ... 356 | 357 | def query_system_parameters(self) -> Dict[str, Any]: 358 | """ 359 | Returns a dictionary of information about the host system. 360 | """ 361 | ... 362 | 363 | def resume(self, pid: int) -> None: 364 | """ 365 | Resume a process from the attachable state. 366 | """ 367 | ... 368 | 369 | def spawn( 370 | self, 371 | program: str, 372 | argv: Union[None, List[Union[str, bytes]], Tuple[Union[str, bytes]]] = None, 373 | envp: Optional[Dict[str, str]] = None, 374 | env: Optional[Dict[str, str]] = None, 375 | cwd: Optional[str] = None, 376 | stdio: Optional[str] = None, 377 | **kwargs: Any, 378 | ) -> int: 379 | """ 380 | Spawn a process into an attachable state. 381 | """ 382 | ... 383 | 384 | class DeviceManager(Object): 385 | def add_remote_device( 386 | self, 387 | address: str, 388 | certificate: Optional[str] = None, 389 | origin: Optional[str] = None, 390 | token: Optional[str] = None, 391 | keepalive_interval: Optional[int] = None, 392 | ) -> Device: 393 | """ 394 | Add a remote device. 395 | """ 396 | ... 397 | 398 | def close(self) -> None: 399 | """ 400 | Close the device manager. 401 | """ 402 | ... 403 | 404 | def enumerate_devices(self) -> List[Device]: 405 | """ 406 | Enumerate devices. 407 | """ 408 | ... 409 | 410 | def get_device_matching(self, predicate: Callable[[Device], bool], timeout: int) -> Device: 411 | """ 412 | Get device matching predicate. 413 | """ 414 | ... 415 | 416 | def remove_remote_device(self, address: str) -> None: 417 | """ 418 | Remove a remote device. 419 | """ 420 | ... 421 | 422 | class EndpointParameters(Object): ... 423 | 424 | class FileMonitor(Object): 425 | def disable(self) -> None: 426 | """ 427 | Disable the file monitor. 428 | """ 429 | ... 430 | 431 | def enable(self) -> None: 432 | """ 433 | Enable the file monitor. 434 | """ 435 | ... 436 | 437 | class IOStream(Object): 438 | def close(self) -> None: 439 | """ 440 | Close the stream. 441 | """ 442 | ... 443 | 444 | def is_closed(self) -> bool: 445 | """ 446 | Query whether the stream is closed. 447 | """ 448 | ... 449 | 450 | def read(self, size: int) -> bytes: 451 | """ 452 | Read up to the specified number of bytes from the stream. 453 | """ 454 | ... 455 | 456 | def read_all(self, size: int) -> bytes: 457 | """ 458 | Read exactly the specified number of bytes from the stream. 459 | """ 460 | ... 461 | 462 | def write(self, data: bytes) -> int: 463 | """ 464 | Write as much as possible of the provided data to the stream. 465 | """ 466 | ... 467 | 468 | def write_all(self, data: bytes) -> None: 469 | """ 470 | Write all of the provided data to the stream. 471 | """ 472 | ... 473 | 474 | class PortalMembership(Object): 475 | def terminate(self) -> None: 476 | """ 477 | Terminate the membership. 478 | """ 479 | ... 480 | 481 | class PortalService(Object): 482 | @property 483 | def device(self) -> Device: 484 | """ 485 | Device for in-process control. 486 | """ 487 | ... 488 | 489 | def broadcast(self, message: str, data: Optional[Union[str, bytes]] = None) -> None: 490 | """ 491 | Broadcast a message to all control channels. 492 | """ 493 | ... 494 | 495 | def enumerate_tags(self, connection_id: int) -> List[str]: 496 | """ 497 | Enumerate tags of a specific connection. 498 | """ 499 | ... 500 | 501 | def kick(self, connection_id: int) -> None: 502 | """ 503 | Kick out a specific connection. 504 | """ 505 | ... 506 | 507 | def narrowcast(self, tag: str, message: str, data: Optional[Union[str, bytes]] = None) -> None: 508 | """ 509 | Post a message to control channels with a specific tag. 510 | """ 511 | ... 512 | 513 | def post(self, connection_id: int, message: str, data: Optional[Union[str, bytes]] = None) -> None: 514 | """ 515 | Post a message to a specific control channel. 516 | """ 517 | ... 518 | 519 | def start(self) -> None: 520 | """ 521 | Start listening for incoming connections. 522 | """ 523 | ... 524 | 525 | def stop(self) -> None: 526 | """ 527 | Stop listening for incoming connections, and kick any connected clients. 528 | """ 529 | ... 530 | 531 | def tag(self, connection_id: int, tag: str) -> None: 532 | """ 533 | Tag a specific control channel. 534 | """ 535 | ... 536 | 537 | def untag(self, connection_id: int, tag: str) -> None: 538 | """ 539 | Untag a specific control channel. 540 | """ 541 | ... 542 | 543 | class Process(Object): 544 | @property 545 | def pid(self) -> int: 546 | """ 547 | Process ID. 548 | """ 549 | ... 550 | 551 | @property 552 | def name(self) -> str: 553 | """ 554 | Human-readable process name. 555 | """ 556 | ... 557 | 558 | @property 559 | def parameters(self) -> Dict[str, Any]: 560 | """ 561 | Parameters. 562 | """ 563 | ... 564 | 565 | class Relay(Object): 566 | def __init__(self, address: str, username: str, password: str, kind: str) -> None: ... 567 | @property 568 | def address(self) -> str: 569 | """ 570 | Network address or address:port of the TURN server. 571 | """ 572 | ... 573 | 574 | @property 575 | def kind(self) -> str: 576 | """ 577 | Relay kind. One of: turn-udp, turn-tcp, turn-tls. 578 | """ 579 | ... 580 | 581 | @property 582 | def password(self) -> str: 583 | """ 584 | The TURN password to use for the allocate request. 585 | """ 586 | ... 587 | 588 | @property 589 | def username(self) -> str: 590 | """ 591 | The TURN username to use for the allocate request. 592 | """ 593 | ... 594 | 595 | class Script(Object): 596 | def eternalize(self) -> None: 597 | """ 598 | Eternalize the script. 599 | """ 600 | ... 601 | 602 | def is_destroyed(self) -> bool: 603 | """ 604 | Query whether the script has been destroyed. 605 | """ 606 | ... 607 | 608 | def load(self) -> None: 609 | """ 610 | Load the script. 611 | """ 612 | ... 613 | 614 | def post(self, message: str, data: Optional[Union[str, bytes]] = None) -> None: 615 | """ 616 | Post a JSON-encoded message to the script. 617 | """ 618 | ... 619 | 620 | def unload(self) -> None: 621 | """ 622 | Unload the script. 623 | """ 624 | ... 625 | 626 | def enable_debugger(self, port: Optional[int]) -> None: 627 | """ 628 | Enable the Node.js compatible script debugger 629 | """ 630 | ... 631 | 632 | def disable_debugger(self) -> None: 633 | """ 634 | Disable the Node.js compatible script debugger 635 | """ 636 | ... 637 | 638 | class Service(Object): 639 | def activate(self) -> None: 640 | """ 641 | Activate the service. 642 | """ 643 | ... 644 | 645 | def cancel(self) -> None: 646 | """ 647 | Cancel the service. 648 | """ 649 | ... 650 | 651 | def request(self, parameters: Any) -> Any: 652 | """ 653 | Perform a request. 654 | """ 655 | ... 656 | 657 | class Session(Object): 658 | @property 659 | def pid(self) -> int: 660 | """ 661 | Process ID. 662 | """ 663 | ... 664 | 665 | def compile_script(self, source: str, name: Optional[str] = None, runtime: Optional[str] = None) -> bytes: 666 | """ 667 | Compile script source code to bytecode. 668 | """ 669 | ... 670 | 671 | def create_script(self, source: str, name: Optional[str] = None, runtime: Optional[str] = None) -> Script: 672 | """ 673 | Create a new script. 674 | """ 675 | ... 676 | 677 | def create_script_from_bytes( 678 | self, data: bytes, name: Optional[str] = None, runtime: Optional[str] = None 679 | ) -> Script: 680 | """ 681 | Create a new script from bytecode. 682 | """ 683 | ... 684 | 685 | def snapshot_script(self, embed_script: str, warmup_script: Optional[str], runtime: Optional[str] = None) -> bytes: 686 | """ 687 | Evaluate script and snapshot the resulting VM state 688 | """ 689 | ... 690 | 691 | def detach(self) -> None: 692 | """ 693 | Detach session from the process. 694 | """ 695 | ... 696 | 697 | def disable_child_gating(self) -> None: 698 | """ 699 | Disable child gating. 700 | """ 701 | ... 702 | 703 | def enable_child_gating(self) -> None: 704 | """ 705 | Enable child gating. 706 | """ 707 | ... 708 | 709 | def is_detached(self) -> bool: 710 | """ 711 | Query whether the session is detached. 712 | """ 713 | ... 714 | 715 | def join_portal( 716 | self, address: str, certificate: Optional[str] = None, token: Optional[str] = None, acl: Optional[Any] = None 717 | ) -> PortalMembership: 718 | """ 719 | Join a portal. 720 | """ 721 | ... 722 | 723 | def resume(self) -> None: 724 | """ 725 | Resume session after network error. 726 | """ 727 | ... 728 | 729 | def setup_peer_connection( 730 | self, stun_server: Optional[str] = None, relays: Optional[Sequence[Relay]] = None 731 | ) -> None: 732 | """ 733 | Set up a peer connection with the target process. 734 | """ 735 | ... 736 | 737 | class Spawn(Object): 738 | @property 739 | def identifier(self) -> str: 740 | """ 741 | Application identifier. 742 | """ 743 | ... 744 | 745 | @property 746 | def pid(self) -> int: 747 | """ 748 | Process ID. 749 | """ 750 | ... 751 | 752 | class Compiler(Object): 753 | def build( 754 | self, 755 | entrypoint: str, 756 | project_root: Optional[str] = None, 757 | source_maps: Optional[str] = None, 758 | compression: Optional[str] = None, 759 | ) -> str: 760 | """ 761 | Build an agent. 762 | """ 763 | ... 764 | 765 | def watch( 766 | self, 767 | entrypoint: str, 768 | project_root: Optional[str] = None, 769 | source_maps: Optional[str] = None, 770 | compression: Optional[str] = None, 771 | ) -> None: 772 | """ 773 | Continuously build an agent. 774 | """ 775 | ... 776 | 777 | __version__: str 778 | -------------------------------------------------------------------------------- /frida/_frida/extension.version: -------------------------------------------------------------------------------- 1 | { 2 | global: 3 | PyInit__frida; 4 | 5 | local: 6 | *; 7 | }; 8 | -------------------------------------------------------------------------------- /frida/_frida/meson.build: -------------------------------------------------------------------------------- 1 | py_sources = [ 2 | '__init__.pyi', 3 | 'py.typed', 4 | ] 5 | python.install_sources(py_sources, subdir: 'frida' / '_frida', pure: false) 6 | 7 | extra_link_args = [] 8 | if host_os_family == 'darwin' 9 | extra_link_args += '-Wl,-exported_symbol,_PyInit__frida' 10 | elif host_os_family != 'windows' 11 | extra_link_args += '-Wl,--version-script,' + meson.current_source_dir() / 'extension.version' 12 | endif 13 | 14 | extension = python.extension_module('_frida', 'extension.c', 15 | limited_api: '3.7', 16 | c_args: frida_component_cflags, 17 | link_args: extra_link_args, 18 | dependencies: [python_dep, frida_core_dep, os_deps], 19 | install: true, 20 | subdir: 'frida', 21 | ) 22 | -------------------------------------------------------------------------------- /frida/_frida/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frida/frida-python/82ee16d299671558728fd1cbc9680f404d04fc92/frida/_frida/py.typed -------------------------------------------------------------------------------- /frida/core.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import dataclasses 5 | import fnmatch 6 | import functools 7 | import json 8 | import sys 9 | import threading 10 | import traceback 11 | import warnings 12 | from types import TracebackType 13 | from typing import ( 14 | Any, 15 | AnyStr, 16 | Awaitable, 17 | Callable, 18 | Dict, 19 | List, 20 | Mapping, 21 | MutableMapping, 22 | Optional, 23 | Sequence, 24 | Tuple, 25 | Type, 26 | TypeVar, 27 | Union, 28 | overload, 29 | ) 30 | 31 | if sys.version_info >= (3, 8): 32 | from typing import Literal, TypedDict 33 | else: 34 | from typing_extensions import Literal, TypedDict 35 | 36 | if sys.version_info >= (3, 11): 37 | from typing import NotRequired 38 | else: 39 | from typing_extensions import NotRequired 40 | 41 | from . import _frida 42 | 43 | _device_manager = None 44 | 45 | _Cancellable = _frida.Cancellable 46 | 47 | ProcessTarget = Union[int, str] 48 | Spawn = _frida.Spawn 49 | 50 | 51 | @dataclasses.dataclass 52 | class RPCResult: 53 | finished: bool = False 54 | value: Any = None 55 | error: Optional[Exception] = None 56 | 57 | 58 | def get_device_manager() -> "DeviceManager": 59 | """ 60 | Get or create a singleton DeviceManager that let you manage all the devices 61 | """ 62 | 63 | global _device_manager 64 | if _device_manager is None: 65 | _device_manager = DeviceManager(_frida.DeviceManager()) 66 | return _device_manager 67 | 68 | 69 | def _filter_missing_kwargs(d: MutableMapping[Any, Any]) -> None: 70 | for key in list(d.keys()): 71 | if d[key] is None: 72 | d.pop(key) 73 | 74 | 75 | R = TypeVar("R") 76 | 77 | 78 | def cancellable(f: Callable[..., R]) -> Callable[..., R]: 79 | @functools.wraps(f) 80 | def wrapper(*args: Any, **kwargs: Any) -> R: 81 | cancellable = kwargs.pop("cancellable", None) 82 | if cancellable is not None: 83 | with cancellable: 84 | return f(*args, **kwargs) 85 | 86 | return f(*args, **kwargs) 87 | 88 | return wrapper 89 | 90 | 91 | class IOStream: 92 | """ 93 | Frida's own implementation of an input/output stream 94 | """ 95 | 96 | def __init__(self, impl: _frida.IOStream) -> None: 97 | self._impl = impl 98 | 99 | def __repr__(self) -> str: 100 | return repr(self._impl) 101 | 102 | @property 103 | def is_closed(self) -> bool: 104 | """ 105 | Query whether the stream is closed 106 | """ 107 | 108 | return self._impl.is_closed() 109 | 110 | @cancellable 111 | def close(self) -> None: 112 | """ 113 | Close the stream. 114 | """ 115 | 116 | self._impl.close() 117 | 118 | @cancellable 119 | def read(self, count: int) -> bytes: 120 | """ 121 | Read up to the specified number of bytes from the stream 122 | """ 123 | 124 | return self._impl.read(count) 125 | 126 | @cancellable 127 | def read_all(self, count: int) -> bytes: 128 | """ 129 | Read exactly the specified number of bytes from the stream 130 | """ 131 | 132 | return self._impl.read_all(count) 133 | 134 | @cancellable 135 | def write(self, data: bytes) -> int: 136 | """ 137 | Write as much as possible of the provided data to the stream 138 | """ 139 | 140 | return self._impl.write(data) 141 | 142 | @cancellable 143 | def write_all(self, data: bytes) -> None: 144 | """ 145 | Write all of the provided data to the stream 146 | """ 147 | 148 | self._impl.write_all(data) 149 | 150 | 151 | class PortalMembership: 152 | def __init__(self, impl: _frida.PortalMembership) -> None: 153 | self._impl = impl 154 | 155 | @cancellable 156 | def terminate(self) -> None: 157 | """ 158 | Terminate the membership 159 | """ 160 | 161 | self._impl.terminate() 162 | 163 | 164 | class ScriptExportsSync: 165 | """ 166 | Proxy object that expose all the RPC exports of a script as attributes on this class 167 | 168 | A method named exampleMethod in a script will be called with instance.example_method on this object 169 | """ 170 | 171 | def __init__(self, script: "Script") -> None: 172 | self._script = script 173 | 174 | def __getattr__(self, name: str) -> Callable[..., Any]: 175 | script = self._script 176 | js_name = _to_camel_case(name) 177 | 178 | def method(*args: Any, **kwargs: Any) -> Any: 179 | request, data = make_rpc_call_request(js_name, args) 180 | return script._rpc_request(request, data, **kwargs) 181 | 182 | return method 183 | 184 | def __dir__(self) -> List[str]: 185 | return self._script.list_exports_sync() 186 | 187 | 188 | ScriptExports = ScriptExportsSync 189 | 190 | 191 | class ScriptExportsAsync: 192 | """ 193 | Proxy object that expose all the RPC exports of a script as attributes on this class 194 | 195 | A method named exampleMethod in a script will be called with instance.example_method on this object 196 | """ 197 | 198 | def __init__(self, script: "Script") -> None: 199 | self._script = script 200 | 201 | def __getattr__(self, name: str) -> Callable[..., Awaitable[Any]]: 202 | script = self._script 203 | js_name = _to_camel_case(name) 204 | 205 | async def method(*args: Any, **kwargs: Any) -> Any: 206 | request, data = make_rpc_call_request(js_name, args) 207 | return await script._rpc_request_async(request, data, **kwargs) 208 | 209 | return method 210 | 211 | def __dir__(self) -> List[str]: 212 | return self._script.list_exports_sync() 213 | 214 | 215 | def make_rpc_call_request(js_name: str, args: Sequence[Any]) -> Tuple[List[Any], Optional[bytes]]: 216 | if args and isinstance(args[-1], bytes): 217 | raw_args = args[:-1] 218 | data = args[-1] 219 | else: 220 | raw_args = args 221 | data = None 222 | return (["call", js_name, raw_args], data) 223 | 224 | 225 | class ScriptErrorMessage(TypedDict): 226 | type: Literal["error"] 227 | description: str 228 | stack: NotRequired[str] 229 | fileName: NotRequired[str] 230 | lineNumber: NotRequired[int] 231 | columnNumber: NotRequired[int] 232 | 233 | 234 | class ScriptPayloadMessage(TypedDict): 235 | type: Literal["send"] 236 | payload: NotRequired[Any] 237 | 238 | 239 | ScriptMessage = Union[ScriptPayloadMessage, ScriptErrorMessage] 240 | ScriptMessageCallback = Callable[[ScriptMessage, Optional[bytes]], None] 241 | ScriptDestroyedCallback = Callable[[], None] 242 | 243 | 244 | class RPCException(Exception): 245 | """ 246 | Wraps remote errors from the script RPC 247 | """ 248 | 249 | def __str__(self) -> str: 250 | return str(self.args[2]) if len(self.args) >= 3 else str(self.args[0]) 251 | 252 | 253 | class Script: 254 | def __init__(self, impl: _frida.Script) -> None: 255 | self.exports_sync = ScriptExportsSync(self) 256 | self.exports_async = ScriptExportsAsync(self) 257 | 258 | self._impl = impl 259 | 260 | self._on_message_callbacks: List[ScriptMessageCallback] = [] 261 | self._log_handler: Callable[[str, str], None] = self.default_log_handler 262 | 263 | self._pending: Dict[ 264 | int, Callable[[Optional[Any], Optional[Union[RPCException, _frida.InvalidOperationError]]], None] 265 | ] = {} 266 | self._next_request_id = 1 267 | self._cond = threading.Condition() 268 | 269 | impl.on("destroyed", self._on_destroyed) 270 | impl.on("message", self._on_message) 271 | 272 | @property 273 | def exports(self) -> ScriptExportsSync: 274 | """ 275 | The old way of retrieving the synchronous exports caller 276 | """ 277 | 278 | warnings.warn( 279 | "Script.exports will become asynchronous in the future, use the explicit Script.exports_sync instead", 280 | DeprecationWarning, 281 | stacklevel=2, 282 | ) 283 | return self.exports_sync 284 | 285 | def __repr__(self) -> str: 286 | return repr(self._impl) 287 | 288 | @property 289 | def is_destroyed(self) -> bool: 290 | """ 291 | Query whether the script has been destroyed 292 | """ 293 | 294 | return self._impl.is_destroyed() 295 | 296 | @cancellable 297 | def load(self) -> None: 298 | """ 299 | Load the script. 300 | """ 301 | 302 | self._impl.load() 303 | 304 | @cancellable 305 | def unload(self) -> None: 306 | """ 307 | Unload the script 308 | """ 309 | 310 | self._impl.unload() 311 | 312 | @cancellable 313 | def eternalize(self) -> None: 314 | """ 315 | Eternalize the script 316 | """ 317 | 318 | self._impl.eternalize() 319 | 320 | def post(self, message: Any, data: Optional[AnyStr] = None) -> None: 321 | """ 322 | Post a JSON-encoded message to the script 323 | """ 324 | 325 | raw_message = json.dumps(message) 326 | kwargs = {"data": data} 327 | _filter_missing_kwargs(kwargs) 328 | self._impl.post(raw_message, **kwargs) 329 | 330 | @cancellable 331 | def enable_debugger(self, port: Optional[int] = None) -> None: 332 | """ 333 | Enable the Node.js compatible script debugger 334 | """ 335 | 336 | kwargs = {"port": port} 337 | _filter_missing_kwargs(kwargs) 338 | self._impl.enable_debugger(**kwargs) 339 | 340 | @cancellable 341 | def disable_debugger(self) -> None: 342 | """ 343 | Disable the Node.js compatible script debugger 344 | """ 345 | 346 | self._impl.disable_debugger() 347 | 348 | @overload 349 | def on(self, signal: Literal["destroyed"], callback: ScriptDestroyedCallback) -> None: ... 350 | 351 | @overload 352 | def on(self, signal: Literal["message"], callback: ScriptMessageCallback) -> None: ... 353 | 354 | @overload 355 | def on(self, signal: str, callback: Callable[..., Any]) -> None: ... 356 | 357 | def on(self, signal: str, callback: Callable[..., Any]) -> None: 358 | """ 359 | Add a signal handler 360 | """ 361 | 362 | if signal == "message": 363 | self._on_message_callbacks.append(callback) 364 | else: 365 | self._impl.on(signal, callback) 366 | 367 | @overload 368 | def off(self, signal: Literal["destroyed"], callback: ScriptDestroyedCallback) -> None: ... 369 | 370 | @overload 371 | def off(self, signal: Literal["message"], callback: ScriptMessageCallback) -> None: ... 372 | 373 | @overload 374 | def off(self, signal: str, callback: Callable[..., Any]) -> None: ... 375 | 376 | def off(self, signal: str, callback: Callable[..., Any]) -> None: 377 | """ 378 | Remove a signal handler 379 | """ 380 | 381 | if signal == "message": 382 | self._on_message_callbacks.remove(callback) 383 | else: 384 | self._impl.off(signal, callback) 385 | 386 | def get_log_handler(self) -> Callable[[str, str], None]: 387 | """ 388 | Get the method that handles the script logs 389 | """ 390 | 391 | return self._log_handler 392 | 393 | def set_log_handler(self, handler: Callable[[str, str], None]) -> None: 394 | """ 395 | Set the method that handles the script logs 396 | :param handler: a callable that accepts two parameters: 397 | 1. the log level name 398 | 2. the log message 399 | """ 400 | 401 | self._log_handler = handler 402 | 403 | def default_log_handler(self, level: str, text: str) -> None: 404 | """ 405 | The default implementation of the log handler, prints the message to stdout 406 | or stderr, depending on the level 407 | """ 408 | 409 | if level == "info": 410 | print(text, file=sys.stdout) 411 | else: 412 | print(text, file=sys.stderr) 413 | 414 | async def list_exports_async(self) -> List[str]: 415 | """ 416 | Asynchronously list all the exported attributes from the script's rpc 417 | """ 418 | 419 | result = await self._rpc_request_async(["list"]) 420 | assert isinstance(result, list) 421 | return result 422 | 423 | def list_exports_sync(self) -> List[str]: 424 | """ 425 | List all the exported attributes from the script's rpc 426 | """ 427 | 428 | result = self._rpc_request(["list"]) 429 | assert isinstance(result, list) 430 | return result 431 | 432 | def list_exports(self) -> List[str]: 433 | """ 434 | List all the exported attributes from the script's rpc 435 | """ 436 | 437 | warnings.warn( 438 | "Script.list_exports will become asynchronous in the future, use the explicit Script.list_exports_sync instead", 439 | DeprecationWarning, 440 | stacklevel=2, 441 | ) 442 | return self.list_exports_sync() 443 | 444 | def _rpc_request_async(self, args: Any, data: Optional[bytes] = None) -> asyncio.Future[Any]: 445 | loop = asyncio.get_event_loop() 446 | future: asyncio.Future[Any] = asyncio.Future() 447 | 448 | def on_complete(value: Any, error: Optional[Union[RPCException, _frida.InvalidOperationError]]) -> None: 449 | if error is not None: 450 | loop.call_soon_threadsafe(future.set_exception, error) 451 | else: 452 | loop.call_soon_threadsafe(future.set_result, value) 453 | 454 | request_id = self._append_pending(on_complete) 455 | 456 | if not self.is_destroyed: 457 | self._send_rpc_call(request_id, args, data) 458 | else: 459 | self._on_destroyed() 460 | 461 | return future 462 | 463 | @cancellable 464 | def _rpc_request(self, args: Any, data: Optional[bytes] = None) -> Any: 465 | result = RPCResult() 466 | 467 | def on_complete(value: Any, error: Optional[Union[RPCException, _frida.InvalidOperationError]]) -> None: 468 | with self._cond: 469 | result.finished = True 470 | result.value = value 471 | result.error = error 472 | self._cond.notify_all() 473 | 474 | def on_cancelled() -> None: 475 | self._pending.pop(request_id, None) 476 | on_complete(None, None) 477 | 478 | request_id = self._append_pending(on_complete) 479 | 480 | if not self.is_destroyed: 481 | self._send_rpc_call(request_id, args, data) 482 | 483 | cancellable = Cancellable.get_current() 484 | cancel_handler = cancellable.connect(on_cancelled) 485 | try: 486 | with self._cond: 487 | while not result.finished: 488 | self._cond.wait() 489 | finally: 490 | cancellable.disconnect(cancel_handler) 491 | 492 | cancellable.raise_if_cancelled() 493 | else: 494 | self._on_destroyed() 495 | 496 | if result.error is not None: 497 | raise result.error 498 | 499 | return result.value 500 | 501 | def _append_pending( 502 | self, callback: Callable[[Any, Optional[Union[RPCException, _frida.InvalidOperationError]]], None] 503 | ) -> int: 504 | with self._cond: 505 | request_id = self._next_request_id 506 | self._next_request_id += 1 507 | self._pending[request_id] = callback 508 | return request_id 509 | 510 | def _send_rpc_call(self, request_id: int, args: Any, data: Optional[bytes]) -> None: 511 | self.post(["frida:rpc", request_id, *args], data) 512 | 513 | def _on_rpc_message(self, request_id: int, operation: str, params: List[Any], data: Optional[Any]) -> None: 514 | if operation in ("ok", "error"): 515 | callback = self._pending.pop(request_id, None) 516 | if callback is None: 517 | return 518 | 519 | value = None 520 | error = None 521 | if operation == "ok": 522 | if data is not None: 523 | value = (params[1], data) if len(params) > 1 else data 524 | else: 525 | value = params[0] 526 | else: 527 | error = RPCException(*params[0:3]) 528 | 529 | callback(value, error) 530 | 531 | def _on_destroyed(self) -> None: 532 | while True: 533 | next_pending = None 534 | 535 | with self._cond: 536 | pending_ids = list(self._pending.keys()) 537 | if len(pending_ids) > 0: 538 | next_pending = self._pending.pop(pending_ids[0]) 539 | 540 | if next_pending is None: 541 | break 542 | 543 | next_pending(None, _frida.InvalidOperationError("script has been destroyed")) 544 | 545 | def _on_message(self, raw_message: str, data: Optional[bytes]) -> None: 546 | message = json.loads(raw_message) 547 | 548 | mtype = message["type"] 549 | payload = message.get("payload", None) 550 | if mtype == "log": 551 | level = message["level"] 552 | text = payload 553 | self._log_handler(level, text) 554 | elif mtype == "send" and isinstance(payload, list) and len(payload) > 0 and payload[0] == "frida:rpc": 555 | request_id = payload[1] 556 | operation = payload[2] 557 | params = payload[3:] 558 | self._on_rpc_message(request_id, operation, params, data) 559 | else: 560 | for callback in self._on_message_callbacks[:]: 561 | try: 562 | callback(message, data) 563 | except: 564 | traceback.print_exc() 565 | 566 | 567 | SessionDetachedCallback = Callable[ 568 | [ 569 | Literal[ 570 | "application-requested", "process-replaced", "process-terminated", "connection-terminated", "device-lost" 571 | ], 572 | Optional[_frida.Crash], 573 | ], 574 | None, 575 | ] 576 | 577 | 578 | class Session: 579 | def __init__(self, impl: _frida.Session) -> None: 580 | self._impl = impl 581 | 582 | def __repr__(self) -> str: 583 | return repr(self._impl) 584 | 585 | @property 586 | def is_detached(self) -> bool: 587 | """ 588 | Query whether the session is detached 589 | """ 590 | 591 | return self._impl.is_detached() 592 | 593 | @cancellable 594 | def detach(self) -> None: 595 | """ 596 | Detach session from the process 597 | """ 598 | 599 | self._impl.detach() 600 | 601 | @cancellable 602 | def resume(self) -> None: 603 | """ 604 | Resume session after network error 605 | """ 606 | 607 | self._impl.resume() 608 | 609 | @cancellable 610 | def enable_child_gating(self) -> None: 611 | """ 612 | Enable child gating 613 | """ 614 | 615 | self._impl.enable_child_gating() 616 | 617 | @cancellable 618 | def disable_child_gating(self) -> None: 619 | """ 620 | Disable child gating 621 | """ 622 | 623 | self._impl.disable_child_gating() 624 | 625 | @cancellable 626 | def create_script( 627 | self, source: str, name: Optional[str] = None, snapshot: Optional[bytes] = None, runtime: Optional[str] = None 628 | ) -> Script: 629 | """ 630 | Create a new script 631 | """ 632 | 633 | kwargs = {"name": name, "snapshot": snapshot, "runtime": runtime} 634 | _filter_missing_kwargs(kwargs) 635 | return Script(self._impl.create_script(source, **kwargs)) # type: ignore 636 | 637 | @cancellable 638 | def create_script_from_bytes( 639 | self, data: bytes, name: Optional[str] = None, snapshot: Optional[bytes] = None, runtime: Optional[str] = None 640 | ) -> Script: 641 | """ 642 | Create a new script from bytecode 643 | """ 644 | 645 | kwargs = {"name": name, "snapshot": snapshot, "runtime": runtime} 646 | _filter_missing_kwargs(kwargs) 647 | return Script(self._impl.create_script_from_bytes(data, **kwargs)) # type: ignore 648 | 649 | @cancellable 650 | def compile_script(self, source: str, name: Optional[str] = None, runtime: Optional[str] = None) -> bytes: 651 | """ 652 | Compile script source code to bytecode 653 | """ 654 | 655 | kwargs = {"name": name, "runtime": runtime} 656 | _filter_missing_kwargs(kwargs) 657 | return self._impl.compile_script(source, **kwargs) 658 | 659 | @cancellable 660 | def snapshot_script(self, embed_script: str, warmup_script: Optional[str], runtime: Optional[str] = None) -> bytes: 661 | """ 662 | Evaluate script and snapshot the resulting VM state 663 | """ 664 | kwargs = {"warmup_script": warmup_script, "runtime": runtime} 665 | _filter_missing_kwargs(kwargs) 666 | return self._impl.snapshot_script(embed_script, **kwargs) 667 | 668 | @cancellable 669 | def setup_peer_connection( 670 | self, stun_server: Optional[str] = None, relays: Optional[Sequence[_frida.Relay]] = None 671 | ) -> None: 672 | """ 673 | Set up a peer connection with the target process 674 | """ 675 | 676 | kwargs = {"stun_server": stun_server, "relays": relays} 677 | _filter_missing_kwargs(kwargs) 678 | self._impl.setup_peer_connection(**kwargs) # type: ignore 679 | 680 | @cancellable 681 | def join_portal( 682 | self, 683 | address: str, 684 | certificate: Optional[str] = None, 685 | token: Optional[str] = None, 686 | acl: Union[None, List[str], Tuple[str]] = None, 687 | ) -> PortalMembership: 688 | """ 689 | Join a portal 690 | """ 691 | 692 | kwargs: Dict[str, Any] = {"certificate": certificate, "token": token, "acl": acl} 693 | _filter_missing_kwargs(kwargs) 694 | return PortalMembership(self._impl.join_portal(address, **kwargs)) 695 | 696 | @overload 697 | def on( 698 | self, 699 | signal: Literal["detached"], 700 | callback: SessionDetachedCallback, 701 | ) -> None: ... 702 | 703 | @overload 704 | def on(self, signal: str, callback: Callable[..., Any]) -> None: ... 705 | 706 | def on(self, signal: str, callback: Callable[..., Any]) -> None: 707 | """ 708 | Add a signal handler 709 | """ 710 | 711 | self._impl.on(signal, callback) 712 | 713 | @overload 714 | def off( 715 | self, 716 | signal: Literal["detached"], 717 | callback: SessionDetachedCallback, 718 | ) -> None: ... 719 | 720 | @overload 721 | def off(self, signal: str, callback: Callable[..., Any]) -> None: ... 722 | 723 | def off(self, signal: str, callback: Callable[..., Any]) -> None: 724 | """ 725 | Remove a signal handler 726 | """ 727 | 728 | self._impl.off(signal, callback) 729 | 730 | 731 | BusDetachedCallback = Callable[[], None] 732 | BusMessageCallback = Callable[[Mapping[Any, Any], Optional[bytes]], None] 733 | 734 | 735 | class Bus: 736 | def __init__(self, impl: _frida.Bus) -> None: 737 | self._impl = impl 738 | self._on_message_callbacks: List[Callable[..., Any]] = [] 739 | 740 | impl.on("message", self._on_message) 741 | 742 | @cancellable 743 | def attach(self) -> None: 744 | """ 745 | Attach to the bus 746 | """ 747 | 748 | self._impl.attach() 749 | 750 | def post(self, message: Any, data: Optional[Union[str, bytes]] = None) -> None: 751 | """ 752 | Post a JSON-encoded message to the bus 753 | """ 754 | 755 | raw_message = json.dumps(message) 756 | kwargs = {"data": data} 757 | _filter_missing_kwargs(kwargs) 758 | self._impl.post(raw_message, **kwargs) 759 | 760 | @overload 761 | def on(self, signal: Literal["detached"], callback: BusDetachedCallback) -> None: ... 762 | 763 | @overload 764 | def on(self, signal: Literal["message"], callback: BusMessageCallback) -> None: ... 765 | 766 | @overload 767 | def on(self, signal: str, callback: Callable[..., Any]) -> None: ... 768 | 769 | def on(self, signal: str, callback: Callable[..., Any]) -> None: 770 | """ 771 | Add a signal handler 772 | """ 773 | 774 | if signal == "message": 775 | self._on_message_callbacks.append(callback) 776 | else: 777 | self._impl.on(signal, callback) 778 | 779 | @overload 780 | def off(self, signal: Literal["detached"], callback: BusDetachedCallback) -> None: ... 781 | 782 | @overload 783 | def off(self, signal: Literal["message"], callback: BusMessageCallback) -> None: ... 784 | 785 | @overload 786 | def off(self, signal: str, callback: Callable[..., Any]) -> None: ... 787 | 788 | def off(self, signal: str, callback: Callable[..., Any]) -> None: 789 | """ 790 | Remove a signal handler 791 | """ 792 | 793 | if signal == "message": 794 | self._on_message_callbacks.remove(callback) 795 | else: 796 | self._impl.off(signal, callback) 797 | 798 | def _on_message(self, raw_message: str, data: Any) -> None: 799 | message = json.loads(raw_message) 800 | 801 | for callback in self._on_message_callbacks[:]: 802 | try: 803 | callback(message, data) 804 | except: 805 | traceback.print_exc() 806 | 807 | 808 | ServiceCloseCallback = Callable[[], None] 809 | ServiceMessageCallback = Callable[[Any], None] 810 | 811 | 812 | class Service: 813 | def __init__(self, impl: _frida.Service) -> None: 814 | self._impl = impl 815 | 816 | @cancellable 817 | def activate(self) -> None: 818 | """ 819 | Activate the service 820 | """ 821 | 822 | self._impl.activate() 823 | 824 | @cancellable 825 | def cancel(self) -> None: 826 | """ 827 | Cancel the service 828 | """ 829 | 830 | self._impl.cancel() 831 | 832 | def request(self, parameters: Any) -> Any: 833 | """ 834 | Perform a request 835 | """ 836 | 837 | return self._impl.request(parameters) 838 | 839 | @overload 840 | def on(self, signal: Literal["close"], callback: ServiceCloseCallback) -> None: ... 841 | 842 | @overload 843 | def on(self, signal: Literal["message"], callback: ServiceMessageCallback) -> None: ... 844 | 845 | @overload 846 | def on(self, signal: str, callback: Callable[..., Any]) -> None: ... 847 | 848 | def on(self, signal: str, callback: Callable[..., Any]) -> None: 849 | """ 850 | Add a signal handler 851 | """ 852 | 853 | self._impl.on(signal, callback) 854 | 855 | @overload 856 | def off(self, signal: Literal["close"], callback: ServiceCloseCallback) -> None: ... 857 | 858 | @overload 859 | def off(self, signal: Literal["message"], callback: ServiceMessageCallback) -> None: ... 860 | 861 | @overload 862 | def off(self, signal: str, callback: Callable[..., Any]) -> None: ... 863 | 864 | def off(self, signal: str, callback: Callable[..., Any]) -> None: 865 | """ 866 | Remove a signal handler 867 | """ 868 | 869 | self._impl.off(signal, callback) 870 | 871 | 872 | DeviceSpawnAddedCallback = Callable[[_frida.Spawn], None] 873 | DeviceSpawnRemovedCallback = Callable[[_frida.Spawn], None] 874 | DeviceChildAddedCallback = Callable[[_frida.Child], None] 875 | DeviceChildRemovedCallback = Callable[[_frida.Child], None] 876 | DeviceProcessCrashedCallback = Callable[[_frida.Crash], None] 877 | DeviceOutputCallback = Callable[[int, int, bytes], None] 878 | DeviceUninjectedCallback = Callable[[int], None] 879 | DeviceLostCallback = Callable[[], None] 880 | 881 | 882 | class Device: 883 | """ 884 | Represents a device that Frida connects to 885 | """ 886 | 887 | def __init__(self, device: _frida.Device) -> None: 888 | assert device.bus is not None 889 | self.id = device.id 890 | self.name = device.name 891 | self.icon = device.icon 892 | self.type = device.type 893 | self.bus = Bus(device.bus) 894 | 895 | self._impl = device 896 | 897 | def __repr__(self) -> str: 898 | return repr(self._impl) 899 | 900 | @property 901 | def is_lost(self) -> bool: 902 | """ 903 | Query whether the device has been lost 904 | """ 905 | 906 | return self._impl.is_lost() 907 | 908 | @cancellable 909 | def query_system_parameters(self) -> Dict[str, Any]: 910 | """ 911 | Returns a dictionary of information about the host system 912 | """ 913 | 914 | return self._impl.query_system_parameters() 915 | 916 | @cancellable 917 | def get_frontmost_application(self, scope: Optional[str] = None) -> Optional[_frida.Application]: 918 | """ 919 | Get details about the frontmost application 920 | """ 921 | 922 | kwargs = {"scope": scope} 923 | _filter_missing_kwargs(kwargs) 924 | return self._impl.get_frontmost_application(**kwargs) 925 | 926 | @cancellable 927 | def enumerate_applications( 928 | self, identifiers: Optional[Sequence[str]] = None, scope: Optional[str] = None 929 | ) -> List[_frida.Application]: 930 | """ 931 | Enumerate applications 932 | """ 933 | 934 | kwargs = {"identifiers": identifiers, "scope": scope} 935 | _filter_missing_kwargs(kwargs) 936 | return self._impl.enumerate_applications(**kwargs) # type: ignore 937 | 938 | @cancellable 939 | def enumerate_processes( 940 | self, pids: Optional[Sequence[int]] = None, scope: Optional[str] = None 941 | ) -> List[_frida.Process]: 942 | """ 943 | Enumerate processes 944 | """ 945 | 946 | kwargs = {"pids": pids, "scope": scope} 947 | _filter_missing_kwargs(kwargs) 948 | return self._impl.enumerate_processes(**kwargs) # type: ignore 949 | 950 | @cancellable 951 | def get_process(self, process_name: str) -> _frida.Process: 952 | """ 953 | Get the process with the given name 954 | :raises ProcessNotFoundError: if the process was not found or there were more than one process with the given name 955 | """ 956 | 957 | process_name_lc = process_name.lower() 958 | matching = [ 959 | process 960 | for process in self._impl.enumerate_processes() 961 | if fnmatch.fnmatchcase(process.name.lower(), process_name_lc) 962 | ] 963 | if len(matching) == 1: 964 | return matching[0] 965 | elif len(matching) > 1: 966 | matches_list = ", ".join([f"{process.name} (pid: {process.pid})" for process in matching]) 967 | raise _frida.ProcessNotFoundError(f"ambiguous name; it matches: {matches_list}") 968 | else: 969 | raise _frida.ProcessNotFoundError(f"unable to find process with name '{process_name}'") 970 | 971 | @cancellable 972 | def enable_spawn_gating(self) -> None: 973 | """ 974 | Enable spawn gating 975 | """ 976 | 977 | self._impl.enable_spawn_gating() 978 | 979 | @cancellable 980 | def disable_spawn_gating(self) -> None: 981 | """ 982 | Disable spawn gating 983 | """ 984 | 985 | self._impl.disable_spawn_gating() 986 | 987 | @cancellable 988 | def enumerate_pending_spawn(self) -> List[_frida.Spawn]: 989 | """ 990 | Enumerate pending spawn 991 | """ 992 | 993 | return self._impl.enumerate_pending_spawn() 994 | 995 | @cancellable 996 | def enumerate_pending_children(self) -> List[_frida.Child]: 997 | """ 998 | Enumerate pending children 999 | """ 1000 | 1001 | return self._impl.enumerate_pending_children() 1002 | 1003 | @cancellable 1004 | def spawn( 1005 | self, 1006 | program: Union[str, List[Union[str, bytes]], Tuple[Union[str, bytes]]], 1007 | argv: Union[None, List[Union[str, bytes]], Tuple[Union[str, bytes]]] = None, 1008 | envp: Optional[Dict[str, str]] = None, 1009 | env: Optional[Dict[str, str]] = None, 1010 | cwd: Optional[str] = None, 1011 | stdio: Optional[str] = None, 1012 | **kwargs: Any, 1013 | ) -> int: 1014 | """ 1015 | Spawn a process into an attachable state 1016 | """ 1017 | 1018 | if not isinstance(program, str): 1019 | argv = program 1020 | if isinstance(argv[0], bytes): 1021 | program = argv[0].decode() 1022 | else: 1023 | program = argv[0] 1024 | if len(argv) == 1: 1025 | argv = None 1026 | 1027 | kwargs = {"argv": argv, "envp": envp, "env": env, "cwd": cwd, "stdio": stdio, "aux": kwargs} 1028 | _filter_missing_kwargs(kwargs) 1029 | return self._impl.spawn(program, **kwargs) 1030 | 1031 | @cancellable 1032 | def input(self, target: ProcessTarget, data: bytes) -> None: 1033 | """ 1034 | Input data on stdin of a spawned process 1035 | :param target: the PID or name of the process 1036 | """ 1037 | 1038 | self._impl.input(self._pid_of(target), data) 1039 | 1040 | @cancellable 1041 | def resume(self, target: ProcessTarget) -> None: 1042 | """ 1043 | Resume a process from the attachable state 1044 | :param target: the PID or name of the process 1045 | """ 1046 | 1047 | self._impl.resume(self._pid_of(target)) 1048 | 1049 | @cancellable 1050 | def kill(self, target: ProcessTarget) -> None: 1051 | """ 1052 | Kill a process 1053 | :param target: the PID or name of the process 1054 | """ 1055 | self._impl.kill(self._pid_of(target)) 1056 | 1057 | @cancellable 1058 | def attach( 1059 | self, 1060 | target: ProcessTarget, 1061 | realm: Optional[str] = None, 1062 | persist_timeout: Optional[int] = None, 1063 | ) -> Session: 1064 | """ 1065 | Attach to a process 1066 | :param target: the PID or name of the process 1067 | """ 1068 | 1069 | kwargs = {"realm": realm, "persist_timeout": persist_timeout} 1070 | _filter_missing_kwargs(kwargs) 1071 | return Session(self._impl.attach(self._pid_of(target), **kwargs)) # type: ignore 1072 | 1073 | @cancellable 1074 | def inject_library_file(self, target: ProcessTarget, path: str, entrypoint: str, data: str) -> int: 1075 | """ 1076 | Inject a library file to a process 1077 | :param target: the PID or name of the process 1078 | """ 1079 | 1080 | return self._impl.inject_library_file(self._pid_of(target), path, entrypoint, data) 1081 | 1082 | @cancellable 1083 | def inject_library_blob(self, target: ProcessTarget, blob: bytes, entrypoint: str, data: str) -> int: 1084 | """ 1085 | Inject a library blob to a process 1086 | :param target: the PID or name of the process 1087 | """ 1088 | 1089 | return self._impl.inject_library_blob(self._pid_of(target), blob, entrypoint, data) 1090 | 1091 | @cancellable 1092 | def open_channel(self, address: str) -> IOStream: 1093 | """ 1094 | Open a device-specific communication channel 1095 | """ 1096 | 1097 | return IOStream(self._impl.open_channel(address)) 1098 | 1099 | @cancellable 1100 | def open_service(self, address: str) -> Service: 1101 | """ 1102 | Open a device-specific service 1103 | """ 1104 | 1105 | return Service(self._impl.open_service(address)) 1106 | 1107 | @cancellable 1108 | def unpair(self) -> None: 1109 | """ 1110 | Unpair device 1111 | """ 1112 | 1113 | self._impl.unpair() 1114 | 1115 | @cancellable 1116 | def get_bus(self) -> Bus: 1117 | """ 1118 | Get the message bus of the device 1119 | """ 1120 | 1121 | return self.bus 1122 | 1123 | @overload 1124 | def on(self, signal: Literal["spawn-added"], callback: DeviceSpawnAddedCallback) -> None: ... 1125 | 1126 | @overload 1127 | def on(self, signal: Literal["spawn-removed"], callback: DeviceSpawnRemovedCallback) -> None: ... 1128 | 1129 | @overload 1130 | def on(self, signal: Literal["child-added"], callback: DeviceChildAddedCallback) -> None: ... 1131 | 1132 | @overload 1133 | def on(self, signal: Literal["child-removed"], callback: DeviceChildRemovedCallback) -> None: ... 1134 | 1135 | @overload 1136 | def on(self, signal: Literal["process-crashed"], callback: DeviceProcessCrashedCallback) -> None: ... 1137 | 1138 | @overload 1139 | def on(self, signal: Literal["output"], callback: DeviceOutputCallback) -> None: ... 1140 | 1141 | @overload 1142 | def on(self, signal: Literal["uninjected"], callback: DeviceUninjectedCallback) -> None: ... 1143 | 1144 | @overload 1145 | def on(self, signal: Literal["lost"], callback: DeviceLostCallback) -> None: ... 1146 | 1147 | @overload 1148 | def on(self, signal: str, callback: Callable[..., Any]) -> None: ... 1149 | 1150 | def on(self, signal: str, callback: Callable[..., Any]) -> None: 1151 | """ 1152 | Add a signal handler 1153 | """ 1154 | 1155 | self._impl.on(signal, callback) 1156 | 1157 | @overload 1158 | def off(self, signal: Literal["spawn-added"], callback: DeviceSpawnAddedCallback) -> None: ... 1159 | 1160 | @overload 1161 | def off(self, signal: Literal["spawn-removed"], callback: DeviceSpawnRemovedCallback) -> None: ... 1162 | 1163 | @overload 1164 | def off(self, signal: Literal["child-added"], callback: DeviceChildAddedCallback) -> None: ... 1165 | 1166 | @overload 1167 | def off(self, signal: Literal["child-removed"], callback: DeviceChildRemovedCallback) -> None: ... 1168 | 1169 | @overload 1170 | def off(self, signal: Literal["process-crashed"], callback: DeviceProcessCrashedCallback) -> None: ... 1171 | 1172 | @overload 1173 | def off(self, signal: Literal["output"], callback: DeviceOutputCallback) -> None: ... 1174 | 1175 | @overload 1176 | def off(self, signal: Literal["uninjected"], callback: DeviceUninjectedCallback) -> None: ... 1177 | 1178 | @overload 1179 | def off(self, signal: Literal["lost"], callback: DeviceLostCallback) -> None: ... 1180 | 1181 | @overload 1182 | def off(self, signal: str, callback: Callable[..., Any]) -> None: ... 1183 | 1184 | def off(self, signal: str, callback: Callable[..., Any]) -> None: 1185 | """ 1186 | Remove a signal handler 1187 | """ 1188 | 1189 | self._impl.off(signal, callback) 1190 | 1191 | def _pid_of(self, target: ProcessTarget) -> int: 1192 | if isinstance(target, str): 1193 | return self.get_process(target).pid 1194 | else: 1195 | return target 1196 | 1197 | 1198 | DeviceManagerAddedCallback = Callable[[_frida.Device], None] 1199 | DeviceManagerRemovedCallback = Callable[[_frida.Device], None] 1200 | DeviceManagerChangedCallback = Callable[[], None] 1201 | 1202 | 1203 | class DeviceManager: 1204 | def __init__(self, impl: _frida.DeviceManager) -> None: 1205 | self._impl = impl 1206 | 1207 | def __repr__(self) -> str: 1208 | return repr(self._impl) 1209 | 1210 | def get_local_device(self) -> Device: 1211 | """ 1212 | Get the local device 1213 | """ 1214 | 1215 | return self.get_device_matching(lambda d: d.type == "local", timeout=0) 1216 | 1217 | def get_remote_device(self) -> Device: 1218 | """ 1219 | Get the first remote device in the devices list 1220 | """ 1221 | 1222 | return self.get_device_matching(lambda d: d.type == "remote", timeout=0) 1223 | 1224 | def get_usb_device(self, timeout: int = 0) -> Device: 1225 | """ 1226 | Get the first device connected over USB in the devices list 1227 | """ 1228 | 1229 | return self.get_device_matching(lambda d: d.type == "usb", timeout) 1230 | 1231 | def get_device(self, id: Optional[str], timeout: int = 0) -> Device: 1232 | """ 1233 | Get a device by its id 1234 | """ 1235 | 1236 | return self.get_device_matching(lambda d: d.id == id, timeout) 1237 | 1238 | @cancellable 1239 | def get_device_matching(self, predicate: Callable[[Device], bool], timeout: int = 0) -> Device: 1240 | """ 1241 | Get device matching predicate 1242 | :param predicate: a function to filter the devices 1243 | :param timeout: operation timeout in seconds 1244 | """ 1245 | 1246 | if timeout < 0: 1247 | raw_timeout = -1 1248 | elif timeout == 0: 1249 | raw_timeout = 0 1250 | else: 1251 | raw_timeout = int(timeout * 1000.0) 1252 | return Device(self._impl.get_device_matching(lambda d: predicate(Device(d)), raw_timeout)) 1253 | 1254 | @cancellable 1255 | def enumerate_devices(self) -> List[Device]: 1256 | """ 1257 | Enumerate devices 1258 | """ 1259 | 1260 | return [Device(device) for device in self._impl.enumerate_devices()] 1261 | 1262 | @cancellable 1263 | def add_remote_device( 1264 | self, 1265 | address: str, 1266 | certificate: Optional[str] = None, 1267 | origin: Optional[str] = None, 1268 | token: Optional[str] = None, 1269 | keepalive_interval: Optional[int] = None, 1270 | ) -> Device: 1271 | """ 1272 | Add a remote device 1273 | """ 1274 | 1275 | kwargs: Dict[str, Any] = { 1276 | "certificate": certificate, 1277 | "origin": origin, 1278 | "token": token, 1279 | "keepalive_interval": keepalive_interval, 1280 | } 1281 | _filter_missing_kwargs(kwargs) 1282 | return Device(self._impl.add_remote_device(address, **kwargs)) 1283 | 1284 | @cancellable 1285 | def remove_remote_device(self, address: str) -> None: 1286 | """ 1287 | Remove a remote device 1288 | """ 1289 | 1290 | self._impl.remove_remote_device(address=address) 1291 | 1292 | @overload 1293 | def on(self, signal: Literal["added"], callback: DeviceManagerAddedCallback) -> None: ... 1294 | 1295 | @overload 1296 | def on(self, signal: Literal["removed"], callback: DeviceManagerRemovedCallback) -> None: ... 1297 | 1298 | @overload 1299 | def on(self, signal: Literal["changed"], callback: DeviceManagerChangedCallback) -> None: ... 1300 | 1301 | @overload 1302 | def on(self, signal: str, callback: Callable[..., Any]) -> None: ... 1303 | 1304 | def on(self, signal: str, callback: Callable[..., Any]) -> None: 1305 | """ 1306 | Add a signal handler 1307 | """ 1308 | 1309 | self._impl.on(signal, callback) 1310 | 1311 | @overload 1312 | def off(self, signal: Literal["added"], callback: DeviceManagerAddedCallback) -> None: ... 1313 | 1314 | @overload 1315 | def off(self, signal: Literal["removed"], callback: DeviceManagerRemovedCallback) -> None: ... 1316 | 1317 | @overload 1318 | def off(self, signal: Literal["changed"], callback: DeviceManagerChangedCallback) -> None: ... 1319 | 1320 | @overload 1321 | def off(self, signal: str, callback: Callable[..., Any]) -> None: ... 1322 | 1323 | def off(self, signal: str, callback: Callable[..., Any]) -> None: 1324 | """ 1325 | Remove a signal handler 1326 | """ 1327 | 1328 | self._impl.off(signal, callback) 1329 | 1330 | 1331 | class EndpointParameters: 1332 | def __init__( 1333 | self, 1334 | address: Optional[str] = None, 1335 | port: Optional[int] = None, 1336 | certificate: Optional[str] = None, 1337 | origin: Optional[str] = None, 1338 | authentication: Optional[Tuple[str, Union[str, Callable[[str], Any]]]] = None, 1339 | asset_root: Optional[str] = None, 1340 | ): 1341 | kwargs: Dict[str, Any] = {"address": address, "port": port, "certificate": certificate, "origin": origin} 1342 | if asset_root is not None: 1343 | kwargs["asset_root"] = str(asset_root) 1344 | _filter_missing_kwargs(kwargs) 1345 | 1346 | if authentication is not None: 1347 | (auth_scheme, auth_data) = authentication 1348 | if auth_scheme == "token": 1349 | kwargs["auth_token"] = auth_data 1350 | elif auth_scheme == "callback": 1351 | if not callable(auth_data): 1352 | raise ValueError( 1353 | "Authentication data must provide a Callable if the authentication scheme is callback" 1354 | ) 1355 | kwargs["auth_callback"] = make_auth_callback(auth_data) 1356 | else: 1357 | raise ValueError("invalid authentication scheme") 1358 | 1359 | self._impl = _frida.EndpointParameters(**kwargs) 1360 | 1361 | 1362 | PortalServiceNodeJoinedCallback = Callable[[int, _frida.Application], None] 1363 | PortalServiceNodeLeftCallback = Callable[[int, _frida.Application], None] 1364 | PortalServiceNodeConnectedCallback = Callable[[int, Tuple[str, int]], None] 1365 | PortalServiceNodeDisconnectedCallback = Callable[[int, Tuple[str, int]], None] 1366 | PortalServiceControllerConnectedCallback = Callable[[int, Tuple[str, int]], None] 1367 | PortalServiceControllerDisconnectedCallback = Callable[[int, Tuple[str, int]], None] 1368 | PortalServiceAuthenticatedCallback = Callable[[int, Mapping[Any, Any]], None] 1369 | PortalServiceSubscribeCallback = Callable[[int], None] 1370 | PortalServiceMessageCallback = Callable[[int, Mapping[Any, Any], Optional[bytes]], None] 1371 | 1372 | 1373 | class PortalService: 1374 | def __init__( 1375 | self, 1376 | cluster_params: EndpointParameters = EndpointParameters(), 1377 | control_params: Optional[EndpointParameters] = None, 1378 | ) -> None: 1379 | args = [cluster_params._impl] 1380 | if control_params is not None: 1381 | args.append(control_params._impl) 1382 | impl = _frida.PortalService(*args) 1383 | 1384 | self.device = impl.device 1385 | self._impl = impl 1386 | self._on_authenticated_callbacks: List[PortalServiceAuthenticatedCallback] = [] 1387 | self._on_message_callbacks: List[PortalServiceMessageCallback] = [] 1388 | 1389 | impl.on("authenticated", self._on_authenticated) 1390 | impl.on("message", self._on_message) 1391 | 1392 | @cancellable 1393 | def start(self) -> None: 1394 | """ 1395 | Start listening for incoming connections 1396 | :raises InvalidOperationError: if the service isn't stopped 1397 | :raises AddressInUseError: if the given address is already in use 1398 | """ 1399 | 1400 | self._impl.start() 1401 | 1402 | @cancellable 1403 | def stop(self) -> None: 1404 | """ 1405 | Stop listening for incoming connections, and kick any connected clients 1406 | :raises InvalidOperationError: if the service is already stopped 1407 | """ 1408 | 1409 | self._impl.stop() 1410 | 1411 | def post(self, connection_id: int, message: Any, data: Optional[Union[str, bytes]] = None) -> None: 1412 | """ 1413 | Post a message to a specific control channel. 1414 | """ 1415 | 1416 | raw_message = json.dumps(message) 1417 | kwargs = {"data": data} 1418 | _filter_missing_kwargs(kwargs) 1419 | self._impl.post(connection_id, raw_message, **kwargs) 1420 | 1421 | def narrowcast(self, tag: str, message: Any, data: Optional[Union[str, bytes]] = None) -> None: 1422 | """ 1423 | Post a message to control channels with a specific tag 1424 | """ 1425 | 1426 | raw_message = json.dumps(message) 1427 | kwargs = {"data": data} 1428 | _filter_missing_kwargs(kwargs) 1429 | self._impl.narrowcast(tag, raw_message, **kwargs) 1430 | 1431 | def broadcast(self, message: Any, data: Optional[Union[str, bytes]] = None) -> None: 1432 | """ 1433 | Broadcast a message to all control channels 1434 | """ 1435 | 1436 | raw_message = json.dumps(message) 1437 | kwargs = {"data": data} 1438 | _filter_missing_kwargs(kwargs) 1439 | self._impl.broadcast(raw_message, **kwargs) 1440 | 1441 | def enumerate_tags(self, connection_id: int) -> List[str]: 1442 | """ 1443 | Enumerate tags of a specific connection 1444 | """ 1445 | 1446 | return self._impl.enumerate_tags(connection_id) 1447 | 1448 | def tag(self, connection_id: int, tag: str) -> None: 1449 | """ 1450 | Tag a specific control channel 1451 | """ 1452 | 1453 | self._impl.tag(connection_id, tag) 1454 | 1455 | def untag(self, connection_id: int, tag: str) -> None: 1456 | """ 1457 | Untag a specific control channel 1458 | """ 1459 | 1460 | self._impl.untag(connection_id, tag) 1461 | 1462 | @overload 1463 | def on(self, signal: Literal["node-joined"], callback: PortalServiceNodeJoinedCallback) -> None: ... 1464 | 1465 | @overload 1466 | def on(self, signal: Literal["node-left"], callback: PortalServiceNodeLeftCallback) -> None: ... 1467 | 1468 | @overload 1469 | def on( 1470 | self, signal: Literal["controller-connected"], callback: PortalServiceControllerConnectedCallback 1471 | ) -> None: ... 1472 | 1473 | @overload 1474 | def on( 1475 | self, signal: Literal["controller-disconnected"], callback: PortalServiceControllerDisconnectedCallback 1476 | ) -> None: ... 1477 | 1478 | @overload 1479 | def on(self, signal: Literal["node-connected"], callback: PortalServiceNodeConnectedCallback) -> None: ... 1480 | 1481 | @overload 1482 | def on(self, signal: Literal["node-disconnected"], callback: PortalServiceNodeDisconnectedCallback) -> None: ... 1483 | 1484 | @overload 1485 | def on(self, signal: Literal["authenticated"], callback: PortalServiceAuthenticatedCallback) -> None: ... 1486 | 1487 | @overload 1488 | def on(self, signal: Literal["subscribe"], callback: PortalServiceSubscribeCallback) -> None: ... 1489 | 1490 | @overload 1491 | def on(self, signal: Literal["message"], callback: PortalServiceMessageCallback) -> None: ... 1492 | 1493 | @overload 1494 | def on(self, signal: str, callback: Callable[..., Any]) -> None: ... 1495 | 1496 | def on(self, signal: str, callback: Callable[..., Any]) -> None: 1497 | """ 1498 | Add a signal handler 1499 | """ 1500 | 1501 | if signal == "authenticated": 1502 | self._on_authenticated_callbacks.append(callback) 1503 | elif signal == "message": 1504 | self._on_message_callbacks.append(callback) 1505 | else: 1506 | self._impl.on(signal, callback) 1507 | 1508 | def off(self, signal: str, callback: Callable[..., Any]) -> None: 1509 | """ 1510 | Remove a signal handler 1511 | """ 1512 | 1513 | if signal == "authenticated": 1514 | self._on_authenticated_callbacks.remove(callback) 1515 | elif signal == "message": 1516 | self._on_message_callbacks.remove(callback) 1517 | else: 1518 | self._impl.off(signal, callback) 1519 | 1520 | def _on_authenticated(self, connection_id: int, raw_session_info: str) -> None: 1521 | session_info = json.loads(raw_session_info) 1522 | 1523 | for callback in self._on_authenticated_callbacks[:]: 1524 | try: 1525 | callback(connection_id, session_info) 1526 | except: 1527 | traceback.print_exc() 1528 | 1529 | def _on_message(self, connection_id: int, raw_message: str, data: Optional[bytes]) -> None: 1530 | message = json.loads(raw_message) 1531 | 1532 | for callback in self._on_message_callbacks[:]: 1533 | try: 1534 | callback(connection_id, message, data) 1535 | except: 1536 | traceback.print_exc() 1537 | 1538 | 1539 | class CompilerDiagnosticFile(TypedDict): 1540 | path: str 1541 | line: int 1542 | character: int 1543 | 1544 | 1545 | class CompilerDiagnostic(TypedDict): 1546 | category: str 1547 | code: int 1548 | file: NotRequired[CompilerDiagnosticFile] 1549 | text: str 1550 | 1551 | 1552 | CompilerStartingCallback = Callable[[], None] 1553 | CompilerFinishedCallback = Callable[[], None] 1554 | CompilerOutputCallback = Callable[[str], None] 1555 | CompilerDiagnosticsCallback = Callable[[List[CompilerDiagnostic]], None] 1556 | 1557 | 1558 | class Compiler: 1559 | def __init__(self) -> None: 1560 | self._impl = _frida.Compiler(get_device_manager()._impl) 1561 | 1562 | def __repr__(self) -> str: 1563 | return repr(self._impl) 1564 | 1565 | @cancellable 1566 | def build( 1567 | self, 1568 | entrypoint: str, 1569 | project_root: Optional[str] = None, 1570 | output_format: Optional[str] = None, 1571 | bundle_format: Optional[str] = None, 1572 | type_check: Optional[str] = None, 1573 | source_maps: Optional[str] = None, 1574 | compression: Optional[str] = None, 1575 | ) -> str: 1576 | kwargs = { 1577 | "project_root": project_root, 1578 | "output_format": output_format, 1579 | "bundle_format": bundle_format, 1580 | "type_check": type_check, 1581 | "source_maps": source_maps, 1582 | "compression": compression, 1583 | } 1584 | _filter_missing_kwargs(kwargs) 1585 | return self._impl.build(entrypoint, **kwargs) 1586 | 1587 | @cancellable 1588 | def watch( 1589 | self, 1590 | entrypoint: str, 1591 | project_root: Optional[str] = None, 1592 | output_format: Optional[str] = None, 1593 | bundle_format: Optional[str] = None, 1594 | type_check: Optional[str] = None, 1595 | source_maps: Optional[str] = None, 1596 | compression: Optional[str] = None, 1597 | ) -> None: 1598 | kwargs = { 1599 | "project_root": project_root, 1600 | "output_format": output_format, 1601 | "bundle_format": bundle_format, 1602 | "type_check": type_check, 1603 | "source_maps": source_maps, 1604 | "compression": compression, 1605 | } 1606 | _filter_missing_kwargs(kwargs) 1607 | return self._impl.watch(entrypoint, **kwargs) 1608 | 1609 | @overload 1610 | def on(self, signal: Literal["starting"], callback: CompilerStartingCallback) -> None: ... 1611 | 1612 | @overload 1613 | def on(self, signal: Literal["finished"], callback: CompilerFinishedCallback) -> None: ... 1614 | 1615 | @overload 1616 | def on(self, signal: Literal["output"], callback: CompilerOutputCallback) -> None: ... 1617 | 1618 | @overload 1619 | def on(self, signal: Literal["diagnostics"], callback: CompilerDiagnosticsCallback) -> None: ... 1620 | 1621 | @overload 1622 | def on(self, signal: str, callback: Callable[..., Any]) -> None: ... 1623 | 1624 | def on(self, signal: str, callback: Callable[..., Any]) -> None: 1625 | self._impl.on(signal, callback) 1626 | 1627 | @overload 1628 | def off(self, signal: Literal["starting"], callback: CompilerStartingCallback) -> None: ... 1629 | 1630 | @overload 1631 | def off(self, signal: Literal["finished"], callback: CompilerFinishedCallback) -> None: ... 1632 | 1633 | @overload 1634 | def off(self, signal: Literal["output"], callback: CompilerOutputCallback) -> None: ... 1635 | 1636 | @overload 1637 | def off(self, signal: Literal["diagnostics"], callback: CompilerDiagnosticsCallback) -> None: ... 1638 | 1639 | @overload 1640 | def off(self, signal: str, callback: Callable[..., Any]) -> None: ... 1641 | 1642 | def off(self, signal: str, callback: Callable[..., Any]) -> None: 1643 | self._impl.off(signal, callback) 1644 | 1645 | 1646 | class CancellablePollFD: 1647 | def __init__(self, cancellable: _Cancellable) -> None: 1648 | self.handle = cancellable.get_fd() 1649 | self._cancellable: Optional[_Cancellable] = cancellable 1650 | 1651 | def __del__(self) -> None: 1652 | self.release() 1653 | 1654 | def release(self) -> None: 1655 | if self._cancellable is not None: 1656 | if self.handle != -1: 1657 | self._cancellable.release_fd() 1658 | self.handle = -1 1659 | self._cancellable = None 1660 | 1661 | def __repr__(self) -> str: 1662 | return repr(self.handle) 1663 | 1664 | def __enter__(self) -> int: 1665 | return self.handle 1666 | 1667 | def __exit__( 1668 | self, 1669 | exc_type: Optional[Type[BaseException]], 1670 | exc_value: Optional[BaseException], 1671 | trace: Optional[TracebackType], 1672 | ) -> None: 1673 | self.release() 1674 | 1675 | 1676 | class Cancellable: 1677 | def __init__(self) -> None: 1678 | self._impl = _Cancellable() 1679 | 1680 | def __repr__(self) -> str: 1681 | return repr(self._impl) 1682 | 1683 | @property 1684 | def is_cancelled(self) -> bool: 1685 | """ 1686 | Query whether cancellable has been cancelled 1687 | """ 1688 | 1689 | return self._impl.is_cancelled() 1690 | 1691 | def raise_if_cancelled(self) -> None: 1692 | """ 1693 | Raise an exception if cancelled 1694 | :raises OperationCancelledError: 1695 | """ 1696 | 1697 | self._impl.raise_if_cancelled() 1698 | 1699 | def get_pollfd(self) -> CancellablePollFD: 1700 | return CancellablePollFD(self._impl) 1701 | 1702 | @classmethod 1703 | def get_current(cls) -> _frida.Cancellable: 1704 | """ 1705 | Get the top cancellable from the stack 1706 | """ 1707 | 1708 | return _Cancellable.get_current() 1709 | 1710 | def __enter__(self) -> None: 1711 | self._impl.push_current() 1712 | 1713 | def __exit__( 1714 | self, 1715 | exc_type: Optional[Type[BaseException]], 1716 | exc_value: Optional[BaseException], 1717 | trace: Optional[TracebackType], 1718 | ) -> None: 1719 | self._impl.pop_current() 1720 | 1721 | def connect(self, callback: Callable[..., Any]) -> int: 1722 | """ 1723 | Register notification callback 1724 | :returns: the created handler id 1725 | """ 1726 | 1727 | return self._impl.connect(callback) 1728 | 1729 | def disconnect(self, handler_id: int) -> None: 1730 | """ 1731 | Unregister notification callback. 1732 | """ 1733 | 1734 | self._impl.disconnect(handler_id) 1735 | 1736 | def cancel(self) -> None: 1737 | """ 1738 | Set cancellable to cancelled 1739 | """ 1740 | 1741 | self._impl.cancel() 1742 | 1743 | 1744 | def make_auth_callback(callback: Callable[[str], Any]) -> Callable[[Any], str]: 1745 | """ 1746 | Wraps authenticated callbacks with JSON marshaling 1747 | """ 1748 | 1749 | def authenticate(token: str) -> str: 1750 | session_info = callback(token) 1751 | return json.dumps(session_info) 1752 | 1753 | return authenticate 1754 | 1755 | 1756 | def _to_camel_case(name: str) -> str: 1757 | result = "" 1758 | uppercase_next = False 1759 | for c in name: 1760 | if c == "_": 1761 | uppercase_next = True 1762 | elif uppercase_next: 1763 | result += c.upper() 1764 | uppercase_next = False 1765 | else: 1766 | result += c.lower() 1767 | return result 1768 | -------------------------------------------------------------------------------- /frida/meson.build: -------------------------------------------------------------------------------- 1 | subdir('_frida') 2 | 3 | py_sources = [ 4 | '__init__.py', 5 | 'core.py', 6 | 'py.typed', 7 | ] 8 | python.install_sources(py_sources, subdir: 'frida', pure: false) 9 | -------------------------------------------------------------------------------- /frida/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frida/frida-python/82ee16d299671558728fd1cbc9680f404d04fc92/frida/py.typed -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @setlocal 2 | @echo off 3 | rem:: Based on: https://github.com/microsoft/terminal/issues/217#issuecomment-737594785 4 | goto :_start_ 5 | 6 | :set_real_dp0 7 | set dp0=%~dp0 8 | set "dp0=%dp0:~0,-1%" 9 | goto :eof 10 | 11 | :_start_ 12 | call :set_real_dp0 13 | 14 | if not exist "%dp0%\releng\meson\meson.py" ( 15 | pushd "%dp0%" & git submodule update --init --recursive --depth 1 & popd 16 | if %errorlevel% neq 0 exit /b %errorlevel% 17 | ) 18 | 19 | endlocal & goto #_undefined_# 2>nul || title %COMSPEC% & python ^ 20 | -c "import sys; sys.path.insert(0, sys.argv[1]); from releng.meson_make import main; main()" ^ 21 | "%dp0%" ^ 22 | .\build ^ 23 | %* 24 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('frida-python', 'c', 2 | version: run_command(find_program('python3'), files('setup.py'), '-V', 3 | capture: true, 4 | check: true).stdout().strip(), 5 | meson_version: '>=1.3.0', 6 | ) 7 | 8 | python = import('python').find_installation() 9 | 10 | cc = meson.get_compiler('c') 11 | 12 | frida_component_cflags = [] 13 | ndebug = get_option('b_ndebug') 14 | if ndebug == 'true' or (ndebug == 'if-release' and not get_option('debug')) 15 | frida_component_cflags += [ 16 | '-DG_DISABLE_ASSERT', 17 | '-DG_DISABLE_CHECKS', 18 | '-DG_DISABLE_CAST_CHECKS', 19 | ] 20 | endif 21 | 22 | python_dep = python.dependency() 23 | frida_core_dep = dependency('frida-core-1.0', default_options: [ 24 | 'frida_version=' + meson.project_version().replace('.dev', '-dev.'), 25 | ]) 26 | 27 | os_deps = [] 28 | host_os_family = host_machine.system() 29 | if host_os_family != 'windows' 30 | os_deps += dependency('gio-unix-2.0') 31 | endif 32 | 33 | subdir('frida') 34 | 35 | test('frida-python', python, 36 | args: ['-m', 'unittest', 'discover'], 37 | workdir: meson.current_source_dir(), 38 | env: {'PYTHONPATH': meson.current_build_dir() / 'src'}, 39 | timeout: 30, 40 | ) 41 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 120 7 | 8 | [tool.isort] 9 | profile = "black" 10 | line_length = 120 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | py_limited_api = cp37 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import shutil 4 | import subprocess 5 | import sys 6 | from pathlib import Path 7 | from typing import Iterator 8 | 9 | from setuptools import setup 10 | from setuptools.command.build_ext import build_ext 11 | from setuptools.extension import Extension 12 | 13 | SOURCE_ROOT = Path(__file__).resolve().parent 14 | FRIDA_EXTENSION = os.environ.get("FRIDA_EXTENSION", None) 15 | 16 | 17 | def main(): 18 | setup( 19 | name="frida", 20 | version=detect_version(), 21 | description="Dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers", 22 | long_description=compute_long_description(), 23 | long_description_content_type="text/markdown", 24 | author="Frida Developers", 25 | author_email="oleavr@frida.re", 26 | url="https://frida.re", 27 | install_requires=["typing_extensions; python_version<'3.11'"], 28 | python_requires=">=3.7", 29 | license="wxWindows Library Licence, Version 3.1", 30 | keywords="frida debugger dynamic instrumentation inject javascript windows macos linux ios iphone ipad android qnx", 31 | classifiers=[ 32 | "Development Status :: 5 - Production/Stable", 33 | "Environment :: Console", 34 | "Environment :: MacOS X", 35 | "Environment :: Win32 (MS Windows)", 36 | "Intended Audience :: Developers", 37 | "Intended Audience :: Science/Research", 38 | "License :: OSI Approved", 39 | "Natural Language :: English", 40 | "Operating System :: MacOS :: MacOS X", 41 | "Operating System :: Microsoft :: Windows", 42 | "Operating System :: POSIX :: Linux", 43 | "Programming Language :: Python :: 3", 44 | "Programming Language :: Python :: 3.7", 45 | "Programming Language :: Python :: 3.8", 46 | "Programming Language :: Python :: 3.9", 47 | "Programming Language :: Python :: 3.10", 48 | "Programming Language :: Python :: Implementation :: CPython", 49 | "Programming Language :: JavaScript", 50 | "Topic :: Software Development :: Debuggers", 51 | "Topic :: Software Development :: Libraries :: Python Modules", 52 | ], 53 | packages=["frida", "frida._frida"], 54 | package_data={"frida": ["py.typed"], "frida._frida": ["py.typed", "__init__.pyi"]}, 55 | ext_modules=[ 56 | Extension( 57 | name="frida._frida", 58 | sources=["frida/_frida/extension.c"], 59 | py_limited_api=True, 60 | ) 61 | ], 62 | cmdclass={"build_ext": FridaPrebuiltExt if FRIDA_EXTENSION is not None else FridaDemandBuiltExt}, 63 | zip_safe=False, 64 | ) 65 | 66 | 67 | def detect_version() -> str: 68 | pkg_info = SOURCE_ROOT / "PKG-INFO" 69 | in_source_package = pkg_info.exists() 70 | if in_source_package: 71 | version_line = [ 72 | line for line in pkg_info.read_text(encoding="utf-8").split("\n") if line.startswith("Version: ") 73 | ][0].strip() 74 | return version_line[9:] 75 | 76 | version = os.environ.get("FRIDA_VERSION") 77 | if version is not None: 78 | return version 79 | 80 | releng_location = next(enumerate_releng_locations(), None) 81 | if releng_location is not None: 82 | sys.path.insert(0, str(releng_location.parent)) 83 | from releng.frida_version import detect 84 | 85 | return detect(SOURCE_ROOT).name.replace("-dev.", ".dev") 86 | 87 | return "0.0.0" 88 | 89 | 90 | def compute_long_description() -> str: 91 | return (SOURCE_ROOT / "README.md").read_text(encoding="utf-8") 92 | 93 | 94 | def enumerate_releng_locations() -> Iterator[Path]: 95 | val = os.environ.get("MESON_SOURCE_ROOT") 96 | if val is not None: 97 | parent_releng = Path(val) / "releng" 98 | if releng_location_exists(parent_releng): 99 | yield parent_releng 100 | 101 | local_releng = SOURCE_ROOT / "releng" 102 | if releng_location_exists(local_releng): 103 | yield local_releng 104 | 105 | 106 | def releng_location_exists(location: Path) -> bool: 107 | return (location / "frida_version.py").exists() 108 | 109 | 110 | class FridaPrebuiltExt(build_ext): 111 | def build_extension(self, ext): 112 | target = self.get_ext_fullpath(ext.name) 113 | Path(target).parent.mkdir(parents=True, exist_ok=True) 114 | shutil.copy(FRIDA_EXTENSION, target) 115 | 116 | 117 | class FridaDemandBuiltExt(build_ext): 118 | def build_extension(self, ext): 119 | make = SOURCE_ROOT / "make.bat" if platform.system() == "Windows" else "make" 120 | subprocess.run([make], check=True) 121 | 122 | outputs = [entry for entry in (SOURCE_ROOT / "build" / "frida" / "_frida").glob("_frida.*") if entry.is_file()] 123 | assert len(outputs) == 1 124 | target = self.get_ext_fullpath(ext.name) 125 | Path(target).parent.mkdir(parents=True, exist_ok=True) 126 | shutil.copy(outputs[0], target) 127 | 128 | 129 | if __name__ == "__main__": 130 | main() 131 | -------------------------------------------------------------------------------- /subprojects/frida-core.wrap: -------------------------------------------------------------------------------- 1 | [wrap-git] 2 | url = https://github.com/frida/frida-core.git 3 | revision = b7284691cad6da7219b47a97cbee30f1505dd4b7 4 | depth = 1 5 | 6 | [provide] 7 | dependency_names = frida-core-1.0 8 | 9 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_core import TestCore 2 | from .test_rpc import TestRpc 3 | 4 | __all__ = ["TestCore", "TestRpc"] 5 | -------------------------------------------------------------------------------- /tests/data/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import sys 4 | 5 | system = platform.system() 6 | if system == "Windows": 7 | target_program = r"C:\Windows\notepad.exe" 8 | elif system == "Darwin": 9 | target_program = os.path.join(os.path.dirname(__file__), "unixvictim-macos") 10 | elif system == "Linux" and platform.machine() == "x86_64": 11 | arch = "x86_64" if sys.maxsize > 2**32 else "x86" 12 | target_program = os.path.join(os.path.dirname(__file__), "unixvictim-" + system.lower() + "-" + arch) 13 | else: 14 | target_program = "/bin/cat" 15 | 16 | 17 | __all__ = ["target_program"] 18 | -------------------------------------------------------------------------------- /tests/data/unixvictim-linux-x86: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frida/frida-python/82ee16d299671558728fd1cbc9680f404d04fc92/tests/data/unixvictim-linux-x86 -------------------------------------------------------------------------------- /tests/data/unixvictim-linux-x86_64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frida/frida-python/82ee16d299671558728fd1cbc9680f404d04fc92/tests/data/unixvictim-linux-x86_64 -------------------------------------------------------------------------------- /tests/data/unixvictim-macos: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frida/frida-python/82ee16d299671558728fd1cbc9680f404d04fc92/tests/data/unixvictim-macos -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | import unittest 4 | 5 | import frida 6 | 7 | 8 | class TestCore(unittest.TestCase): 9 | def test_enumerate_devices(self): 10 | devices = frida.get_device_manager().enumerate_devices() 11 | self.assertTrue(len(devices) > 0) 12 | 13 | def test_get_existing_device(self): 14 | device = frida.get_device_matching(lambda d: d.id == "local") 15 | self.assertEqual(device.name, "Local System") 16 | 17 | device = frida.get_device_manager().get_device_matching(lambda d: d.id == "local") 18 | self.assertEqual(device.name, "Local System") 19 | 20 | def test_get_nonexistent_device(self): 21 | def get_nonexistent(): 22 | frida.get_device_manager().get_device_matching(lambda device: device.type == "lol") 23 | 24 | self.assertRaisesRegex(frida.InvalidArgumentError, "device not found", get_nonexistent) 25 | 26 | def test_wait_for_nonexistent_device(self): 27 | def wait_for_nonexistent(): 28 | frida.get_device_manager().get_device_matching(lambda device: device.type == "lol", timeout=0.1) 29 | 30 | self.assertRaisesRegex(frida.InvalidArgumentError, "device not found", wait_for_nonexistent) 31 | 32 | def test_cancel_wait_for_nonexistent_device(self): 33 | cancellable = frida.Cancellable() 34 | 35 | def wait_for_nonexistent(): 36 | frida.get_device_manager().get_device_matching( 37 | lambda device: device.type == "lol", timeout=-1, cancellable=cancellable 38 | ) 39 | 40 | def cancel_after_100ms(): 41 | time.sleep(0.1) 42 | cancellable.cancel() 43 | 44 | threading.Thread(target=cancel_after_100ms).start() 45 | self.assertRaisesRegex(frida.OperationCancelledError, "operation was cancelled", wait_for_nonexistent) 46 | 47 | 48 | if __name__ == "__main__": 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /tests/test_pep503_page_parser.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frida/frida-python/82ee16d299671558728fd1cbc9680f404d04fc92/tests/test_pep503_page_parser.py -------------------------------------------------------------------------------- /tests/test_rpc.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import threading 3 | import time 4 | import unittest 5 | 6 | import frida 7 | 8 | from .data import target_program 9 | 10 | 11 | class TestRpc(unittest.TestCase): 12 | target: subprocess.Popen 13 | session: frida.core.Session 14 | 15 | @classmethod 16 | def setUp(cls): 17 | cls.target = subprocess.Popen([target_program], stdin=subprocess.PIPE) 18 | # TODO: improve injectors to handle injection into a process that hasn't yet finished initializing 19 | time.sleep(0.05) 20 | cls.session = frida.attach(cls.target.pid) 21 | 22 | @classmethod 23 | def tearDown(cls): 24 | cls.session.detach() 25 | cls.target.terminate() 26 | cls.target.stdin.close() 27 | cls.target.wait() 28 | 29 | def test_basics(self): 30 | script = self.session.create_script( 31 | name="test-rpc", 32 | source="""\ 33 | rpc.exports = { 34 | add(a, b) { 35 | const result = a + b; 36 | if (result < 0) 37 | throw new Error("No"); 38 | return result; 39 | }, 40 | sub(a, b) { 41 | return a - b; 42 | }, 43 | speak() { 44 | const buf = Memory.allocUtf8String("Yo"); 45 | return Memory.readByteArray(buf, 2); 46 | }, 47 | speakWithMetadata() { 48 | const buf = Memory.allocUtf8String("Yo"); 49 | return ['soft', Memory.readByteArray(buf, 2)]; 50 | }, 51 | processData(val, data) { 52 | return { val, dump: hexdump(data, { header: false }) }; 53 | }, 54 | }; 55 | """, 56 | ) 57 | script.load() 58 | agent = script.exports_sync 59 | self.assertEqual(agent.add(2, 3), 5) 60 | self.assertEqual(agent.sub(5, 3), 2) 61 | self.assertRaises(Exception, lambda: agent.add(1, -2)) 62 | self.assertEqual(agent.speak(), b"\x59\x6f") 63 | meta, data = agent.speak_with_metadata() 64 | self.assertEqual(meta, "soft") 65 | self.assertEqual(data, b"\x59\x6f") 66 | result = agent.process_data(1337, b"\x13\x37") 67 | self.assertEqual(result["val"], 1337) 68 | self.assertEqual(result["dump"], "00000000 13 37 .7") 69 | 70 | def test_post_failure(self): 71 | script = self.session.create_script( 72 | name="test-rpc", 73 | source="""\ 74 | rpc.exports = { 75 | init: function () { 76 | }, 77 | }; 78 | """, 79 | ) 80 | script.load() 81 | agent = script.exports_sync 82 | 83 | self.session.detach() 84 | self.assertRaisesScriptDestroyed(lambda: agent.init()) 85 | self.assertEqual(script._pending, {}) 86 | 87 | def test_unload_mid_request(self): 88 | script = self.session.create_script( 89 | name="test-rpc", 90 | source="""\ 91 | rpc.exports = { 92 | waitForever: function () { 93 | return new Promise(function () {}); 94 | }, 95 | }; 96 | """, 97 | ) 98 | script.load() 99 | agent = script.exports_sync 100 | 101 | def unload_script_after_100ms(): 102 | time.sleep(0.1) 103 | script.unload() 104 | 105 | threading.Thread(target=unload_script_after_100ms).start() 106 | self.assertRaisesScriptDestroyed(lambda: agent.wait_forever()) 107 | self.assertEqual(script._pending, {}) 108 | 109 | def test_detach_mid_request(self): 110 | script = self.session.create_script( 111 | name="test-rpc", 112 | source="""\ 113 | rpc.exports = { 114 | waitForever: function () { 115 | return new Promise(function () {}); 116 | }, 117 | }; 118 | """, 119 | ) 120 | script.load() 121 | agent = script.exports_sync 122 | 123 | def terminate_target_after_100ms(): 124 | time.sleep(0.1) 125 | self.target.terminate() 126 | 127 | threading.Thread(target=terminate_target_after_100ms).start() 128 | self.assertRaisesScriptDestroyed(lambda: agent.wait_forever()) 129 | self.assertEqual(script._pending, {}) 130 | 131 | def test_cancellation_mid_request(self): 132 | script = self.session.create_script( 133 | name="test-rpc", 134 | source="""\ 135 | rpc.exports = { 136 | waitForever: function () { 137 | return new Promise(function () {}); 138 | }, 139 | }; 140 | """, 141 | ) 142 | script.load() 143 | agent = script.exports_sync 144 | 145 | def cancel_after_100ms(): 146 | time.sleep(0.1) 147 | cancellable.cancel() 148 | 149 | cancellable = frida.Cancellable() 150 | threading.Thread(target=cancel_after_100ms).start() 151 | self.assertRaisesOperationCancelled(lambda: agent.wait_forever(cancellable=cancellable)) 152 | self.assertEqual(script._pending, {}) 153 | 154 | def call_wait_forever_with_cancellable(): 155 | with cancellable: 156 | agent.wait_forever() 157 | 158 | cancellable = frida.Cancellable() 159 | threading.Thread(target=cancel_after_100ms).start() 160 | self.assertRaisesOperationCancelled(call_wait_forever_with_cancellable) 161 | self.assertEqual(script._pending, {}) 162 | 163 | def assertRaisesScriptDestroyed(self, operation): 164 | self.assertRaisesRegex(frida.InvalidOperationError, "script has been destroyed", operation) 165 | 166 | def assertRaisesOperationCancelled(self, operation): 167 | self.assertRaisesRegex(frida.OperationCancelledError, "operation was cancelled", operation) 168 | 169 | 170 | if __name__ == "__main__": 171 | unittest.main() 172 | --------------------------------------------------------------------------------