├── frida_tools ├── __init__.py ├── units.py ├── meson.build ├── kill.py ├── cli_formatting.py ├── rm.py ├── model.py ├── join.py ├── reactor.py ├── ls.py ├── lsd.py ├── compiler.py ├── stream_controller.py ├── _repl_magic.py ├── creator.py ├── push.py ├── pull.py ├── discoverer.py ├── pm.py └── ps.py ├── .github ├── FUNDING.yml └── workflows │ ├── linting.yml │ └── code-style.yml ├── apps ├── tracer │ ├── src │ │ ├── vite-env.d.ts │ │ ├── AddTargetsDialog.css │ │ ├── HandlerList.css │ │ ├── DisassemblyView.css │ │ ├── main.tsx │ │ ├── MemoryView.css │ │ ├── index.css │ │ ├── EventView.css │ │ ├── App.css │ │ ├── MemoryView.tsx │ │ ├── HandlerList.tsx │ │ ├── AddTargetsDialog.tsx │ │ ├── HandlerEditor.tsx │ │ ├── App.tsx │ │ └── EventView.tsx │ ├── public │ │ └── favicon.ico │ ├── tsconfig.json │ ├── index.html │ ├── tsconfig.node.json │ ├── tsconfig.app.json │ ├── package.json │ ├── meson.build │ └── vite.config.ts ├── meson.build └── build.py ├── bridges ├── java.ts ├── objc.ts ├── swift.ts ├── tsconfig.json ├── meson.build ├── package.json ├── rollup.config.ts └── build.py ├── .gitmodules ├── BSDmakefile ├── tests ├── data │ ├── unixvictim-macos │ └── __init__.py ├── __init__.py ├── test_tracer.py ├── test_discoverer.py └── test_arguments.py ├── agents ├── meson.build ├── fs │ ├── tsconfig.json │ ├── meson.build │ └── package.json ├── repl │ ├── tsconfig.json │ ├── meson.build │ ├── package.json │ └── agent.ts ├── itracer │ ├── tsconfig.json │ ├── meson.build │ ├── package.json │ └── agent.ts ├── tracer │ ├── tsconfig.json │ ├── meson.build │ └── package.json └── build.py ├── completions └── meson.build ├── pyproject.toml ├── scripts ├── script.in └── meson.build ├── .gitignore ├── configure ├── Makefile ├── configure.bat ├── make.bat ├── meson.build ├── COPYING ├── README.md └── setup.py /frida_tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: frida 2 | -------------------------------------------------------------------------------- /apps/tracer/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /bridges/java.ts: -------------------------------------------------------------------------------- 1 | import Java from "frida-java-bridge"; 2 | 3 | export default Java; 4 | -------------------------------------------------------------------------------- /bridges/objc.ts: -------------------------------------------------------------------------------- 1 | import ObjC from "frida-objc-bridge"; 2 | 3 | export default ObjC; 4 | -------------------------------------------------------------------------------- /apps/meson.build: -------------------------------------------------------------------------------- 1 | build_app = [python, files('build.py'), npm] 2 | 3 | subdir('tracer') 4 | -------------------------------------------------------------------------------- /bridges/swift.ts: -------------------------------------------------------------------------------- 1 | import Swift from "frida-swift-bridge"; 2 | 3 | export default Swift; 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "releng"] 2 | path = releng 3 | url = https://github.com/frida/releng.git 4 | -------------------------------------------------------------------------------- /apps/tracer/src/AddTargetsDialog.css: -------------------------------------------------------------------------------- 1 | .staged-item { 2 | justify-content: space-between; 3 | } 4 | -------------------------------------------------------------------------------- /frida_tools/units.py: -------------------------------------------------------------------------------- 1 | def bytes_to_megabytes(b: float) -> float: 2 | return b / (1024 * 1024) 3 | -------------------------------------------------------------------------------- /BSDmakefile: -------------------------------------------------------------------------------- 1 | all: .DEFAULT 2 | 3 | .DEFAULT: 4 | @gmake ${.MAKEFLAGS} ${.TARGETS} 5 | 6 | .PHONY: all 7 | -------------------------------------------------------------------------------- /tests/data/unixvictim-macos: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frida/frida-tools/HEAD/tests/data/unixvictim-macos -------------------------------------------------------------------------------- /apps/tracer/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frida/frida-tools/HEAD/apps/tracer/public/favicon.ico -------------------------------------------------------------------------------- /apps/tracer/src/HandlerList.css: -------------------------------------------------------------------------------- 1 | .handler-list { 2 | min-width: 200px; 3 | } 4 | 5 | .handler-node-muted svg { 6 | color: #cd4246; 7 | } -------------------------------------------------------------------------------- /agents/meson.build: -------------------------------------------------------------------------------- 1 | build_agent = [python, files('build.py'), npm] 2 | 3 | subdir('fs') 4 | subdir('repl') 5 | subdir('tracer') 6 | subdir('itracer') 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_discoverer import TestDiscoverer 2 | from .test_tracer import TestTracer 3 | 4 | __all__ = ["TestDiscoverer", "TestTracer"] 5 | -------------------------------------------------------------------------------- /completions/meson.build: -------------------------------------------------------------------------------- 1 | completions_dir = get_option('datadir') / 'fish' / 'vendor_completions.d' 2 | install_data(['frida.fish'], install_dir: completions_dir) 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | 4 | [tool.isort] 5 | profile = "black" 6 | line_length = 120 7 | 8 | [tool.mypy] 9 | strict = true 10 | -------------------------------------------------------------------------------- /apps/tracer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /scripts/script.in: -------------------------------------------------------------------------------- 1 | #!@PYTHON@ 2 | 3 | import sys 4 | sys.path.insert(1, '@pythondir@') 5 | 6 | import frida_tools.@module@ 7 | 8 | if __name__ == '__main__': 9 | frida_tools.@module@.main() 10 | -------------------------------------------------------------------------------- /bridges/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2022"], 5 | "module": "Node16", 6 | "strict": true, 7 | "noEmit": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /agents/fs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": [ 5 | "ES2022" 6 | ], 7 | "module": "Node16", 8 | "strict": true, 9 | "noEmit": true 10 | } 11 | } -------------------------------------------------------------------------------- /agents/repl/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": [ 5 | "ES2022" 6 | ], 7 | "module": "Node16", 8 | "strict": true, 9 | "noEmit": true 10 | } 11 | } -------------------------------------------------------------------------------- /agents/itracer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": [ 5 | "ES2022" 6 | ], 7 | "module": "Node16", 8 | "strict": true, 9 | "noEmit": true 10 | } 11 | } -------------------------------------------------------------------------------- /agents/tracer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": [ 5 | "ES2022" 6 | ], 7 | "module": "Node16", 8 | "strict": true, 9 | "noEmit": true 10 | } 11 | } -------------------------------------------------------------------------------- /apps/tracer/src/DisassemblyView.css: -------------------------------------------------------------------------------- 1 | .disassembly-view { 2 | padding: 5px; 3 | overflow: auto; 4 | font-size: 10px; 5 | user-select: text; 6 | } 7 | 8 | a.disassembly-address-has-handler { 9 | font-weight: bold; 10 | color: white; 11 | } 12 | 13 | a.disassembly-menu-open { 14 | background-color: #ef6456; 15 | } 16 | -------------------------------------------------------------------------------- /tests/data/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | 4 | system = platform.system() 5 | if system == "Windows": 6 | target_program = r"C:\Windows\notepad.exe" 7 | elif system == "Darwin": 8 | target_program = os.path.join(os.path.dirname(__file__), "unixvictim-macos") 9 | else: 10 | target_program = "/bin/cat" 11 | 12 | __all__ = ["target_program"] 13 | -------------------------------------------------------------------------------- /apps/tracer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | frida-trace 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/tracer/src/main.tsx: -------------------------------------------------------------------------------- 1 | import "normalize.css"; 2 | import "@blueprintjs/core/lib/css/blueprint.css"; 3 | import "@blueprintjs/icons/lib/css/blueprint-icons.css"; 4 | import "./index.css"; 5 | 6 | import { StrictMode } from "react"; 7 | import { createRoot } from "react-dom/client"; 8 | import App from "./App.tsx"; 9 | 10 | createRoot(document.getElementById("root")!).render( 11 | 12 | 13 | , 14 | ); 15 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.tsbuildinfo 3 | /*.egg-info/ 4 | /agents/*/node_modules/ 5 | /apps/*/.vite/ 6 | /apps/*/dist/ 7 | /apps/*/node_modules/ 8 | /apps/*/yarn.lock 9 | /bridges/node_modules/ 10 | /build/ 11 | /deps/ 12 | /dist/ 13 | /frida_tools/*.zip 14 | /frida_tools/*_agent.js 15 | /frida_tools/bridges/ 16 | /subprojects/frida-core.wrap 17 | /subprojects/frida-core/ 18 | /subprojects/frida-gum.wrap 19 | /subprojects/frida-gum/ 20 | /subprojects/frida-python/ 21 | -------------------------------------------------------------------------------- /agents/fs/meson.build: -------------------------------------------------------------------------------- 1 | entrypoint = 'agent.ts' 2 | 3 | sources = [ 4 | entrypoint, 5 | 'package.json', 6 | 'package-lock.json', 7 | 'tsconfig.json', 8 | ] 9 | 10 | custom_target('fs-agent.js', 11 | input: sources, 12 | output: ['fs_agent.js'], 13 | command: [ 14 | build_agent, 15 | '@INPUT@', 16 | '@OUTPUT0@', 17 | '@PRIVATE_DIR@', 18 | ], 19 | install: true, 20 | install_dir: python.get_install_dir() / 'frida_tools', 21 | ) 22 | -------------------------------------------------------------------------------- /agents/repl/meson.build: -------------------------------------------------------------------------------- 1 | entrypoint = 'agent.ts' 2 | 3 | sources = [ 4 | entrypoint, 5 | 'package.json', 6 | 'package-lock.json', 7 | 'tsconfig.json', 8 | ] 9 | 10 | custom_target('repl-agent.js', 11 | input: sources, 12 | output: ['repl_agent.js'], 13 | command: [ 14 | build_agent, 15 | '@INPUT@', 16 | '@OUTPUT0@', 17 | '@PRIVATE_DIR@', 18 | ], 19 | install: true, 20 | install_dir: python.get_install_dir() / 'frida_tools', 21 | ) 22 | -------------------------------------------------------------------------------- /agents/tracer/meson.build: -------------------------------------------------------------------------------- 1 | entrypoint = 'agent.ts' 2 | 3 | sources = [ 4 | entrypoint, 5 | 'package.json', 6 | 'package-lock.json', 7 | 'tsconfig.json', 8 | ] 9 | 10 | custom_target('tracer-agent.js', 11 | input: sources, 12 | output: ['tracer_agent.js'], 13 | command: [ 14 | build_agent, 15 | '@INPUT@', 16 | '@OUTPUT0@', 17 | '@PRIVATE_DIR@', 18 | ], 19 | install: true, 20 | install_dir: python.get_install_dir() / 'frida_tools', 21 | ) 22 | -------------------------------------------------------------------------------- /agents/itracer/meson.build: -------------------------------------------------------------------------------- 1 | entrypoint = 'agent.ts' 2 | 3 | sources = [ 4 | entrypoint, 5 | 'package.json', 6 | 'package-lock.json', 7 | 'tsconfig.json', 8 | ] 9 | 10 | custom_target('itracer-agent.js', 11 | input: sources, 12 | output: ['itracer_agent.js'], 13 | command: [ 14 | build_agent, 15 | '@INPUT@', 16 | '@OUTPUT0@', 17 | '@PRIVATE_DIR@', 18 | ], 19 | install: true, 20 | install_dir: python.get_install_dir() / 'frida_tools', 21 | ) 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frida_tools/meson.build: -------------------------------------------------------------------------------- 1 | sources = [ 2 | '__init__.py', 3 | '_repl_magic.py', 4 | 'apk.py', 5 | 'application.py', 6 | 'cli_formatting.py', 7 | 'compiler.py', 8 | 'creator.py', 9 | 'discoverer.py', 10 | 'join.py', 11 | 'kill.py', 12 | 'ls.py', 13 | 'lsd.py', 14 | 'model.py', 15 | 'pm.py', 16 | 'ps.py', 17 | 'pull.py', 18 | 'push.py', 19 | 'reactor.py', 20 | 'repl.py', 21 | 'rm.py', 22 | 'tracer.py', 23 | ] 24 | 25 | python.install_sources(sources, subdir: 'frida_tools') 26 | -------------------------------------------------------------------------------- /agents/repl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repl-agent", 3 | "version": "1.0.0", 4 | "description": "Agent used by the Frida REPL", 5 | "private": true, 6 | "main": "agent.ts", 7 | "type": "module", 8 | "scripts": { 9 | "build": "frida-compile agent.ts -S -c -o ../../frida_tools/repl_agent.js", 10 | "watch": "frida-compile agent.ts -w -o ../../frida_tools/repl_agent.js" 11 | }, 12 | "devDependencies": { 13 | "@types/frida-gum": "^19.0.0", 14 | "@types/node": "^18.11.9", 15 | "frida-compile": "^17.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/tracer/src/MemoryView.css: -------------------------------------------------------------------------------- 1 | .memory-view { 2 | overflow: auto; 3 | font-size: 10px; 4 | } 5 | 6 | .memory-view-toolbar { 7 | display: flex; 8 | background-color: white; 9 | padding: 5px 7px; 10 | border: 1px solid; 11 | } 12 | 13 | .memory-view-data { 14 | padding: 5px 7px; 15 | user-select: text; 16 | } 17 | 18 | .memory-view .bp5-control-group .bp5-input { 19 | font-size: 12px; 20 | } 21 | 22 | .memory-view .bp5-segmented-control { 23 | margin-left: 20px; 24 | } 25 | 26 | .memory-view .bp5-segmented-control .bp5-button { 27 | font-size: 12px; 28 | } 29 | -------------------------------------------------------------------------------- /apps/tracer/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /bridges/meson.build: -------------------------------------------------------------------------------- 1 | custom_target('bridges.bundle', 2 | input: [ 3 | 'objc.ts', 4 | 'swift.ts', 5 | 'java.ts', 6 | 'package.json', 7 | 'package-lock.json', 8 | 'tsconfig.json', 9 | 'rollup.config.ts', 10 | ], 11 | output: [ 12 | 'bridges.bundle', 13 | 'objc.js', 14 | 'swift.js', 15 | 'java.js', 16 | ], 17 | command: [ 18 | python, 19 | files('build.py'), 20 | meson.current_build_dir(), 21 | '@PRIVATE_DIR@', 22 | npm, 23 | '@INPUT@', 24 | ], 25 | install: true, 26 | install_dir: python.get_install_dir() / 'frida_tools' / 'bridges', 27 | ) 28 | -------------------------------------------------------------------------------- /agents/fs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fs-agent", 3 | "version": "1.0.0", 4 | "description": "Agent used by frida-ls and friends", 5 | "private": true, 6 | "main": "agent.ts", 7 | "type": "module", 8 | "scripts": { 9 | "build": "frida-compile agent.ts -S -c -o ../../frida_tools/fs_agent.js", 10 | "watch": "frida-compile agent.ts -w -o ../../frida_tools/fs_agent.js" 11 | }, 12 | "devDependencies": { 13 | "@types/frida-gum": "^19.0.0", 14 | "@types/node": "^18.11.9", 15 | "frida-compile": "^17.0.0", 16 | "frida-remote-stream": "^6.0.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /agents/tracer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tracer-agent", 3 | "version": "1.0.0", 4 | "description": "Agent used by frida-trace", 5 | "private": true, 6 | "main": "agent.ts", 7 | "type": "module", 8 | "scripts": { 9 | "build": "frida-compile agent.ts -S -c -o ../../frida_tools/tracer_agent.js", 10 | "watch": "frida-compile agent.ts -w -o ../../frida_tools/tracer_agent.js" 11 | }, 12 | "devDependencies": { 13 | "@types/frida-gum": "^19.0.0", 14 | "@types/node": "^18.11.9", 15 | "frida-compile": "^17.0.0", 16 | "frida-java-bridge": "^7.0.10" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /agents/itracer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "itracer-agent", 3 | "version": "1.0.0", 4 | "description": "Agent used by frida-itrace", 5 | "private": true, 6 | "main": "agent.ts", 7 | "type": "module", 8 | "scripts": { 9 | "build": "frida-compile agent.ts -S -c -o ../../frida_tools/itracer_agent.js", 10 | "watch": "frida-compile agent.ts -w -o ../../frida_tools/itracer_agent.js" 11 | }, 12 | "devDependencies": { 13 | "@types/frida-gum": "^19.0.0", 14 | "@types/node": "^18.11.9", 15 | "frida-compile": "^17.0.0" 16 | }, 17 | "dependencies": { 18 | "frida-itrace": "^3.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/tracer/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bridges/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frida-bridges", 3 | "version": "1.0.0", 4 | "description": "Bridges shipped with frida-tools for convenience", 5 | "type": "module", 6 | "scripts": { 7 | "build": "rollup --config rollup.config.ts --configPlugin typescript" 8 | }, 9 | "devDependencies": { 10 | "@frida/rollup-plugin-node-polyfills": "^3.0.1", 11 | "@rollup/plugin-node-resolve": "^16.0.1", 12 | "@rollup/plugin-terser": "^0.4.4", 13 | "@rollup/plugin-typescript": "^12.1.2", 14 | "rollup": "^4.41.0", 15 | "tslib": "^2.8.1", 16 | "typescript": "^5.8.3" 17 | }, 18 | "dependencies": { 19 | "frida-java-bridge": "^7.0.10", 20 | "frida-objc-bridge": "^8.0.5", 21 | "frida-swift-bridge": "^3.0.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/tracer/src/index.css: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | height: 100%; 3 | user-select: none; 4 | } 5 | 6 | #root { 7 | display: flex; 8 | } 9 | 10 | .ansi-cyan { 11 | color: #06989a !important; 12 | } 13 | .ansi-cyan-bright { 14 | color: #34e2e2 !important; 15 | } 16 | 17 | .ansi-magenta { 18 | color: #75507b !important; 19 | } 20 | .ansi-magenta-bright { 21 | color: #ad7fa8 !important; 22 | } 23 | 24 | .ansi-yellow { 25 | color: #c4a000 !important; 26 | } 27 | .ansi-yellow-bright { 28 | color: #fce94f !important; 29 | } 30 | 31 | .ansi-green { 32 | color: #4e9a06 !important; 33 | } 34 | .ansi-green-bright { 35 | color: #8ae234 !important; 36 | } 37 | 38 | .ansi-red { 39 | color: #cc0000 !important; 40 | } 41 | .ansi-red-bright { 42 | color: #ef2929 !important; 43 | } 44 | 45 | .ansi-blue { 46 | color: #3465a4 !important; 47 | } 48 | .ansi-blue-bright { 49 | color: #729fcf !important; 50 | } 51 | -------------------------------------------------------------------------------- /apps/tracer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tracer", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@blueprintjs/core": "^5.12.0", 13 | "@curvenote/ansi-to-react": "^7.0.0", 14 | "@frida/react-use-r2": "^1.0.2", 15 | "@monaco-editor/react": "^4.6.0", 16 | "@types/react-window": "^1.8.8", 17 | "monaco-editor": "^0.51.0", 18 | "pretty-ms": "^9.1.0", 19 | "react": "^18.3.1", 20 | "react-dom": "^18.3.1", 21 | "react-resplit": "^1.3.2-alpha.0", 22 | "react-use-websocket": "^4.8.1", 23 | "react-virtualized-auto-sizer": "^1.0.24", 24 | "react-window": "^1.8.10", 25 | "use-debounce": "^10.0.3" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^22.7.4", 29 | "@types/react": "^18.3.3", 30 | "@types/react-dom": "^18.3.0", 31 | "@vitejs/plugin-react": "^4.3.1", 32 | "typescript": "^5.5.3", 33 | "vite": "^5.4.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/tracer/meson.build: -------------------------------------------------------------------------------- 1 | sources = [ 2 | 'package.json', 3 | 'package-lock.json', 4 | 'index.html', 5 | 'tsconfig.json', 6 | 'tsconfig.app.json', 7 | 'tsconfig.node.json', 8 | 'vite.config.ts', 9 | 'public' / 'favicon.ico', 10 | 'src' / 'AddTargetsDialog.css', 11 | 'src' / 'AddTargetsDialog.tsx', 12 | 'src' / 'App.css', 13 | 'src' / 'App.tsx', 14 | 'src' / 'DisassemblyView.css', 15 | 'src' / 'DisassemblyView.tsx', 16 | 'src' / 'EventView.css', 17 | 'src' / 'EventView.tsx', 18 | 'src' / 'HandlerEditor.tsx', 19 | 'src' / 'HandlerList.css', 20 | 'src' / 'HandlerList.tsx', 21 | 'src' / 'MemoryView.css', 22 | 'src' / 'MemoryView.tsx', 23 | 'src' / 'index.css', 24 | 'src' / 'main.tsx', 25 | 'src' / 'model.ts', 26 | 'src' / 'vite-env.d.ts', 27 | ] 28 | 29 | custom_target('tracer-app', 30 | input: sources, 31 | output: ['tracer_ui.zip'], 32 | command: [ 33 | build_app, 34 | '@INPUT@', 35 | '@OUTPUT0@', 36 | '@PRIVATE_DIR@', 37 | ], 38 | install: true, 39 | install_dir: python.get_install_dir() / 'frida_tools' / 'tracer_ui', 40 | ) 41 | -------------------------------------------------------------------------------- /scripts/meson.build: -------------------------------------------------------------------------------- 1 | scripts = [ 2 | ['frida', 'repl'], 3 | ['frida-apk', 'apk'], 4 | ['frida-compile', 'compiler'], 5 | ['frida-create', 'create'], 6 | ['frida-discover', 'discover'], 7 | ['frida-itrace', 'itracer'], 8 | ['frida-join', 'join'], 9 | ['frida-kill', 'kill'], 10 | ['frida-ls', 'ls'], 11 | ['frida-ls-devices', 'lsd'], 12 | ['frida-pm', 'pm'], 13 | ['frida-ps', 'ps'], 14 | ['frida-pull', 'pull'], 15 | ['frida-push', 'push'], 16 | ['frida-rm', 'rm'], 17 | ['frida-trace', 'tracer'], 18 | ] 19 | 20 | common_cdata = configuration_data() 21 | common_cdata.set('PYTHON', python.full_path()) 22 | common_cdata.set('pythondir', python.get_install_dir()) 23 | 24 | foreach s : scripts 25 | cdata = configuration_data() 26 | cdata.merge_from(common_cdata) 27 | cdata.set('module', s.get(1)) 28 | 29 | generated_script = configure_file( 30 | input: 'script.in', 31 | output: s.get(0), 32 | configuration: cdata, 33 | ) 34 | 35 | install_data(generated_script, 36 | install_dir: get_option('bindir'), 37 | install_mode: 'rwxr-xr-x', 38 | ) 39 | endforeach 40 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('frida-tools', 'c', 2 | version: run_command(find_program('python3'), files('setup.py'), '-V', 3 | capture: true, 4 | check: true).stdout().strip(), 5 | meson_version: '>=1.1.0', 6 | ) 7 | 8 | python = import('python').find_installation() 9 | 10 | node = find_program('node', version: '>=18.0.0', native: true, required: false) 11 | if not node.found() 12 | error('Need Node.js >= 18.0.0 to process JavaScript code at build time') 13 | endif 14 | npm = find_program('npm', native: true, required: false) 15 | if not npm.found() 16 | error('Need npm to process JavaScript code at build time') 17 | endif 18 | 19 | subdir('agents') 20 | subdir('bridges') 21 | subdir('apps') 22 | subdir('frida_tools') 23 | subdir('scripts') 24 | subdir('completions') 25 | 26 | pathsep = host_machine.system() == 'windows' ? ';' : ':' 27 | 28 | test('frida-tools', python, 29 | args: ['-m', 'unittest', 'discover'], 30 | workdir: meson.current_source_dir(), 31 | env: { 32 | 'PYTHONPATH': pathsep.join([ 33 | meson.current_source_dir() / 'subprojects' / 'frida-python', 34 | meson.current_build_dir() / 'subprojects' / 'frida-python' / 'src', 35 | ]), 36 | }, 37 | timeout: 30, 38 | ) 39 | -------------------------------------------------------------------------------- /frida_tools/kill.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from typing import List 3 | 4 | import frida 5 | 6 | from frida_tools.application import ConsoleApplication, expand_target, infer_target 7 | 8 | 9 | class KillApplication(ConsoleApplication): 10 | def _usage(self) -> str: 11 | return "%(prog)s [options] process" 12 | 13 | def _add_options(self, parser: argparse.ArgumentParser) -> None: 14 | parser.add_argument("process", help="process name or pid") 15 | 16 | def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None: 17 | process = expand_target(infer_target(options.process)) 18 | if process[0] == "file": 19 | parser.error("process name or pid must be specified") 20 | 21 | self._process = process[1] 22 | 23 | def _start(self) -> None: 24 | try: 25 | assert self._device is not None 26 | self._device.kill(self._process) 27 | except frida.ProcessNotFoundError: 28 | self._update_status(f"unable to find process: {self._process}") 29 | self._exit(1) 30 | self._exit(0) 31 | 32 | 33 | def main() -> None: 34 | app = KillApplication() 35 | app.run() 36 | 37 | 38 | if __name__ == "__main__": 39 | try: 40 | main() 41 | except KeyboardInterrupt: 42 | pass 43 | -------------------------------------------------------------------------------- /tests/test_tracer.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import threading 3 | import time 4 | import unittest 5 | 6 | import frida 7 | 8 | from frida_tools.reactor import Reactor 9 | from frida_tools.tracer import UI, MemoryRepository, Tracer, TracerProfileBuilder 10 | 11 | from .data import target_program 12 | 13 | 14 | class TestTracer(unittest.TestCase): 15 | @classmethod 16 | def setUpClass(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 tearDownClass(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 | done = threading.Event() 31 | reactor = Reactor(lambda reactor: done.wait()) 32 | 33 | def start(): 34 | tp = TracerProfileBuilder().include("open*") 35 | t = Tracer(reactor, MemoryRepository(), tp.build()) 36 | t.start_trace(self.session, "late", {}, "qjs", UI()) 37 | t.stop() 38 | reactor.stop() 39 | done.set() 40 | 41 | reactor.schedule(start) 42 | reactor.run() 43 | 44 | 45 | if __name__ == "__main__": 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /bridges/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import polyfills from "@frida/rollup-plugin-node-polyfills"; 2 | import terser from "@rollup/plugin-terser"; 3 | import typescript from "@rollup/plugin-typescript"; 4 | import resolve from "@rollup/plugin-node-resolve"; 5 | import { defineConfig } from "rollup"; 6 | import type { RollupOptions } from "rollup"; 7 | 8 | const BRIDGES = ["objc", "swift", "java"]; 9 | 10 | export default defineConfig(BRIDGES.map(name => { 11 | return { 12 | input: `${name}.ts`, 13 | output: { 14 | file: `${name}.js`, 15 | format: "iife", 16 | name: "bridge", 17 | generatedCode: { 18 | preset: "es2015", 19 | }, 20 | strict: false, 21 | }, 22 | plugins: [ 23 | ({ 24 | name: "disable-treeshake", 25 | transform (code, id) { 26 | if (/node_modules\/frida-objc-bridge/.test(id)) { 27 | return { 28 | code, 29 | map: null, 30 | moduleSideEffects: "no-treeshake", 31 | }; 32 | } 33 | 34 | return null; 35 | }, 36 | }), 37 | typescript(), 38 | polyfills(), 39 | resolve(), 40 | terser({ ecma: 2022 }), 41 | ], 42 | } as RollupOptions; 43 | })); 44 | -------------------------------------------------------------------------------- /apps/tracer/vite.config.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { defineConfig, Plugin } from "vite"; 4 | import react from "@vitejs/plugin-react"; 5 | 6 | const R2_WASM_PATH = path.join(import.meta.dirname, "node_modules", "@frida", "react-use-r2", "dist", "r2.wasm"); 7 | 8 | const tracerPortStr = process.env.FRIDA_TRACE_PORT; 9 | const tracerPort = (tracerPortStr !== undefined) ? parseInt(tracerPortStr) : 5172; 10 | 11 | const r2WasmPlugin: Plugin = { 12 | name: "r2-wasm-plugin", 13 | configureServer(server) { 14 | server.middlewares.use((req, res, next) => { 15 | if (req.originalUrl?.endsWith("/r2.wasm")) { 16 | const data = fs.readFileSync(R2_WASM_PATH); 17 | res.setHeader("Content-Length", data.length); 18 | res.setHeader("Content-Type", "application/wasm"); 19 | res.end(data, "binary"); 20 | return; 21 | } 22 | next(); 23 | }); 24 | }, 25 | }; 26 | 27 | export default defineConfig({ 28 | plugins: [react(), r2WasmPlugin], 29 | assetsInclude: "**/*.wasm", 30 | build: { 31 | rollupOptions: { 32 | output: { 33 | inlineDynamicImports: true, 34 | entryFileNames: "assets/[name].js", 35 | chunkFileNames: "assets/[name].js", 36 | assetFileNames: "assets/[name].[ext]" 37 | } 38 | } 39 | }, 40 | server: { 41 | port: tracerPort + 1, 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /bridges/build.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import subprocess 3 | import sys 4 | from pathlib import Path 5 | from typing import List 6 | 7 | 8 | def main(argv): 9 | output_dir, priv_dir, npm, *inputs = [Path(d).resolve() for d in argv[1:]] 10 | 11 | try: 12 | compile_bridges(inputs, output_dir, priv_dir, npm) 13 | except subprocess.CalledProcessError as e: 14 | print(e, file=sys.stderr) 15 | print("Output:\n\t| " + "\n\t| ".join(e.output.strip().split("\n")), file=sys.stderr) 16 | sys.exit(2) 17 | except Exception as e: 18 | print(e, file=sys.stderr) 19 | sys.exit(1) 20 | 21 | 22 | def compile_bridges(inputs: List[Path], output_dir: Path, priv_dir: Path, npm: Path): 23 | pkg_file = next((f for f in inputs if f.name == "package.json")) 24 | pkg_parent = pkg_file.parent 25 | 26 | for srcfile in inputs: 27 | subpath = srcfile.relative_to(pkg_parent) 28 | 29 | dstfile = priv_dir / subpath 30 | dstdir = dstfile.parent 31 | if not dstdir.exists(): 32 | dstdir.mkdir() 33 | 34 | shutil.copy(srcfile, dstfile) 35 | 36 | run_kwargs = { 37 | "cwd": priv_dir, 38 | "stdout": subprocess.PIPE, 39 | "stderr": subprocess.STDOUT, 40 | "encoding": "utf-8", 41 | "check": True, 42 | } 43 | 44 | subprocess.run([npm, "install"], **run_kwargs) 45 | subprocess.run([npm, "run", "build"], **run_kwargs) 46 | 47 | for outname in [p.stem + ".js" for p in inputs if p.stem != "rollup.config" and p.suffix == ".ts"]: 48 | shutil.copy(priv_dir / outname, output_dir / outname) 49 | (output_dir / "bridges.bundle").write_bytes(b"") 50 | 51 | 52 | if __name__ == "__main__": 53 | main(sys.argv) 54 | -------------------------------------------------------------------------------- /agents/build.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 List 8 | 9 | 10 | def main(argv: List[str]): 11 | npm = argv[1] 12 | paths = [Path(p).resolve() for p in argv[2:]] 13 | inputs = paths[:-2] 14 | output_js = paths[-2] 15 | priv_dir = paths[-1] 16 | 17 | try: 18 | build(npm, inputs, output_js, priv_dir) 19 | except subprocess.CalledProcessError as e: 20 | print(e, file=sys.stderr) 21 | print("Output:\n\t| " + "\n\t| ".join(e.output.strip().split("\n")), file=sys.stderr) 22 | sys.exit(1) 23 | except Exception as e: 24 | print(e, file=sys.stderr) 25 | sys.exit(1) 26 | 27 | 28 | def build(npm: Path, inputs: List[Path], output_js: Path, priv_dir: Path): 29 | pkg_file = next((f for f in inputs if f.name == "package.json")) 30 | pkg_parent = pkg_file.parent 31 | entrypoint = inputs[0].relative_to(pkg_parent) 32 | 33 | for srcfile in inputs: 34 | subpath = Path(os.path.relpath(srcfile, pkg_parent)) 35 | 36 | dstfile = priv_dir / subpath 37 | dstdir = dstfile.parent 38 | if not dstdir.exists(): 39 | dstdir.mkdir() 40 | 41 | shutil.copy(srcfile, dstfile) 42 | 43 | subprocess.run( 44 | [npm, "install"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8", cwd=priv_dir, check=True 45 | ) 46 | 47 | frida_compile = priv_dir / "node_modules" / ".bin" / f"frida-compile{script_suffix()}" 48 | subprocess.run([frida_compile, entrypoint, "-S", "-c", "-o", output_js], cwd=priv_dir, check=True) 49 | 50 | 51 | def script_suffix() -> str: 52 | return ".cmd" if platform.system() == "Windows" else "" 53 | 54 | 55 | if __name__ == "__main__": 56 | main(sys.argv) 57 | -------------------------------------------------------------------------------- /apps/tracer/src/EventView.css: -------------------------------------------------------------------------------- 1 | .event-list { 2 | font-size: 12px; 3 | scrollbar-color: #e4e4e4 #555; 4 | } 5 | 6 | .event-list code { 7 | font-size: 12px; 8 | } 9 | 10 | .event-item { 11 | overflow: hidden; 12 | } 13 | 14 | .event-heading { 15 | margin-left: 71px; 16 | user-select: text; 17 | } 18 | 19 | .event-spacer { 20 | height: 5px; 21 | } 22 | 23 | .event-summary .bp5-button { 24 | padding-top: 7px; 25 | white-space: pre; 26 | user-select: text; 27 | } 28 | 29 | .event-details { 30 | display: inline-block; 31 | margin: 0 0 5px 86px; 32 | } 33 | 34 | .event-timestamp { 35 | display: inline-block; 36 | min-width: 60px; 37 | padding: 7px; 38 | color: #555; 39 | vertical-align: top; 40 | text-align: right; 41 | } 42 | 43 | .event-indent { 44 | display: inline-block; 45 | padding-top: 7px; 46 | vertical-align: top; 47 | } 48 | 49 | .event-message { 50 | font-size: 12px; 51 | font-weight: bold; 52 | } 53 | 54 | .event-message:focus { 55 | outline: 0; 56 | } 57 | 58 | .event-item .bp5-card { 59 | color: #1e1e1e; 60 | } 61 | 62 | .event-details td { 63 | padding: 5px; 64 | } 65 | 66 | .event-details td:nth-child(1) { 67 | vertical-align: top; 68 | text-align: right; 69 | font-weight: bold; 70 | } 71 | 72 | .event-details td:nth-child(2) { 73 | display: flex; 74 | flex-direction: column; 75 | gap: 4px; 76 | user-select: text; 77 | } 78 | 79 | .event-details .bp5-button { 80 | font-size: 10px; 81 | } 82 | 83 | .event-selected { 84 | background: linear-gradient(to right, #ef6456, #1e1e1e); 85 | background-size: 90px; 86 | background-repeat: no-repeat; 87 | } 88 | 89 | .event-selected .event-timestamp { 90 | color: white; 91 | } 92 | 93 | .event-dismiss { 94 | margin-top: 10px; 95 | } 96 | -------------------------------------------------------------------------------- /apps/build.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | import sys 5 | from pathlib import Path 6 | from typing import List 7 | from zipfile import ZipFile 8 | 9 | 10 | def main(argv: List[str]): 11 | npm = argv[1] 12 | paths = [Path(p).resolve() for p in argv[2:]] 13 | inputs = paths[:-2] 14 | output_zip = paths[-2] 15 | priv_dir = paths[-1] 16 | 17 | try: 18 | build(npm, inputs, output_zip, priv_dir) 19 | except subprocess.CalledProcessError as e: 20 | print(e, file=sys.stderr) 21 | print("Output:\n\t| " + "\n\t| ".join(e.output.strip().split("\n")), file=sys.stderr) 22 | sys.exit(1) 23 | except Exception as e: 24 | print(e, file=sys.stderr) 25 | sys.exit(1) 26 | 27 | 28 | def build(npm: Path, inputs: List[Path], output_zip: Path, priv_dir: Path): 29 | pkg_file = next((f for f in inputs if f.name == "package.json")) 30 | pkg_parent = pkg_file.parent 31 | 32 | for srcfile in inputs: 33 | subpath = Path(os.path.relpath(srcfile, pkg_parent)) 34 | 35 | dstfile = priv_dir / subpath 36 | dstdir = dstfile.parent 37 | if not dstdir.exists(): 38 | dstdir.mkdir() 39 | 40 | shutil.copy(srcfile, dstfile) 41 | 42 | npm_opts = { 43 | "cwd": priv_dir, 44 | "stdout": subprocess.PIPE, 45 | "stderr": subprocess.STDOUT, 46 | "encoding": "utf-8", 47 | "check": True, 48 | } 49 | subprocess.run([npm, "install"], **npm_opts) 50 | subprocess.run([npm, "run", "build"], **npm_opts) 51 | 52 | with ZipFile(output_zip, "w") as outzip: 53 | dist_dir = priv_dir / "dist" 54 | for filepath in dist_dir.rglob("*"): 55 | outzip.write(filepath, filepath.relative_to(dist_dir)) 56 | 57 | 58 | if __name__ == "__main__": 59 | main(sys.argv) 60 | -------------------------------------------------------------------------------- /tests/test_discoverer.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import threading 3 | import time 4 | import unittest 5 | 6 | import frida 7 | 8 | from frida_tools.discoverer import UI, Discoverer 9 | from frida_tools.reactor import Reactor 10 | 11 | from .data import target_program 12 | 13 | 14 | class TestDiscoverer(unittest.TestCase): 15 | @classmethod 16 | def setUpClass(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 tearDownClass(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 | test_ui = TestUI() 31 | reactor = Reactor(lambda reactor: test_ui.on_result.wait()) 32 | 33 | def start(): 34 | d = Discoverer(reactor) 35 | d.start(self.session, "qjs", test_ui) 36 | reactor.schedule(d.stop, 0.1) 37 | 38 | reactor.schedule(start) 39 | reactor.run() 40 | self.assertIsInstance(test_ui.module_functions, dict) 41 | self.assertIsInstance(test_ui.dynamic_functions, list) 42 | 43 | 44 | class TestUI(UI): 45 | def __init__(self): 46 | super(UI, self).__init__() 47 | self.module_functions = None 48 | self.dynamic_functions = None 49 | self.on_result = threading.Event() 50 | 51 | def on_sample_result(self, module_functions, dynamic_functions): 52 | self.module_functions = module_functions 53 | self.dynamic_functions = dynamic_functions 54 | self.on_result.set() 55 | 56 | 57 | if __name__ == "__main__": 58 | unittest.main() 59 | -------------------------------------------------------------------------------- /apps/tracer/src/App.css: -------------------------------------------------------------------------------- 1 | .app-content { 2 | flex: 1; 3 | } 4 | 5 | .top-area { 6 | display: flex; 7 | overflow: hidden; 8 | } 9 | 10 | .app-splitter { 11 | border-top: 5px groove #ef6456; 12 | } 13 | 14 | .bottom-area { 15 | display: flex; 16 | } 17 | 18 | .navigation-area { 19 | display: flex; 20 | flex-direction: column; 21 | border-right: 1px #ccc solid; 22 | } 23 | 24 | .handler-list { 25 | flex: 1; 26 | overflow: scroll; 27 | } 28 | 29 | .target-actions { 30 | padding: 5px; 31 | } 32 | 33 | .editor-area { 34 | display: flex; 35 | flex: 1; 36 | flex-direction: column; 37 | white-space: nowrap; 38 | } 39 | 40 | .editor-area section { 41 | flex: 1; 42 | } 43 | 44 | .editor-area .bp5-button:focus { 45 | outline: 0; 46 | } 47 | 48 | .editor-area section.editor-toolbar { 49 | display: flex; 50 | justify-content: space-between; 51 | flex: 0; 52 | } 53 | 54 | .editor-toolbar .bp5-switch { 55 | padding-top: 10px; 56 | } 57 | 58 | .handler-muted input:checked ~ .bp5-control-indicator { 59 | background: #cd4246 !important; 60 | } 61 | 62 | .monaco-editor { 63 | position: absolute !important; 64 | } 65 | 66 | .bottom-tabs { 67 | display: flex; 68 | flex-direction: column; 69 | flex: 1; 70 | } 71 | 72 | .bottom-tabs .bp5-tab-list { 73 | padding-left: 10px; 74 | } 75 | 76 | .bottom-tabs .bp5-tab:focus { 77 | outline: 0; 78 | } 79 | 80 | .bottom-tab-panel { 81 | display: flex; 82 | margin-top: 0; 83 | flex: 1; 84 | overflow: hidden; 85 | font-family: monospace; 86 | background-color: #1e1e1e; 87 | color: #e4e4e4; 88 | } 89 | 90 | .event-view { 91 | flex: 1; 92 | } 93 | 94 | .disassembly-view { 95 | flex: 1; 96 | } 97 | 98 | .memory-view { 99 | flex: 1; 100 | } 101 | -------------------------------------------------------------------------------- /frida_tools/cli_formatting.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Union 2 | 3 | from colorama import Fore, Style 4 | 5 | THEME_COLOR = "#ef6456" 6 | 7 | STYLE_FILE = Fore.CYAN + Style.BRIGHT 8 | STYLE_LOCATION = Fore.LIGHTYELLOW_EX 9 | STYLE_ERROR = Fore.RED + Style.BRIGHT 10 | STYLE_WARNING = Fore.YELLOW + Style.BRIGHT 11 | STYLE_CODE = Fore.WHITE + Style.DIM 12 | STYLE_RESET_ALL = Style.RESET_ALL 13 | 14 | CATEGORY_STYLE = { 15 | "warning": STYLE_WARNING, 16 | "error": STYLE_ERROR, 17 | } 18 | 19 | 20 | def format_error(error: BaseException) -> str: 21 | return STYLE_ERROR + str(error) + Style.RESET_ALL 22 | 23 | 24 | def format_compiling(script_path: str, cwd: str) -> str: 25 | name = format_filename(script_path, cwd) 26 | return f"{STYLE_RESET_ALL}Compiling {STYLE_FILE}{name}{STYLE_RESET_ALL}..." 27 | 28 | 29 | def format_compiled( 30 | script_path: str, cwd: str, time_started: Union[int, float], time_finished: Union[int, float] 31 | ) -> str: 32 | name = format_filename(script_path, cwd) 33 | elapsed = int((time_finished - time_started) * 1000.0) 34 | return f"{STYLE_RESET_ALL}Compiled {STYLE_FILE}{name}{STYLE_RESET_ALL}{STYLE_CODE} ({elapsed} ms){STYLE_RESET_ALL}" 35 | 36 | 37 | def format_diagnostic(diag: Dict[str, Any], cwd: str) -> str: 38 | category = diag["category"] 39 | code = diag["code"] 40 | text = diag["text"] 41 | 42 | file = diag.get("file", None) 43 | if file is not None: 44 | filename = format_filename(file["path"], cwd) 45 | line = file["line"] + 1 46 | character = file["character"] + 1 47 | 48 | path_segment = f"{STYLE_FILE}{filename}{STYLE_RESET_ALL}" 49 | line_segment = f"{STYLE_LOCATION}{line}{STYLE_RESET_ALL}" 50 | character_segment = f"{STYLE_LOCATION}{character}{STYLE_RESET_ALL}" 51 | 52 | prefix = f"{path_segment}:{line_segment}:{character_segment} - " 53 | else: 54 | prefix = "" 55 | 56 | category_style = CATEGORY_STYLE.get(category, STYLE_RESET_ALL) 57 | 58 | return f"{prefix}{category_style}{category}{STYLE_RESET_ALL} {STYLE_CODE}TS{code}{STYLE_RESET_ALL}: {text}" 59 | 60 | 61 | def format_filename(path: str, cwd: str) -> str: 62 | if path.startswith(cwd): 63 | return path[len(cwd) + 1 :] 64 | return path 65 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frida_tools/rm.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import codecs 3 | import os 4 | import sys 5 | from typing import Any, List 6 | 7 | from colorama import Fore, Style 8 | 9 | from frida_tools.application import ConsoleApplication 10 | 11 | 12 | def main() -> None: 13 | app = RmApplication() 14 | app.run() 15 | 16 | 17 | class RmApplication(ConsoleApplication): 18 | def _add_options(self, parser: argparse.ArgumentParser) -> None: 19 | parser.add_argument("files", help="files to remove", nargs="+") 20 | parser.add_argument("-f", "--force", help="ignore nonexistent files", action="store_true") 21 | parser.add_argument( 22 | "-r", "--recursive", help="remove directories and their contents recursively", action="store_true" 23 | ) 24 | 25 | def _usage(self) -> str: 26 | return "%(prog)s [options] FILE..." 27 | 28 | def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None: 29 | self._paths = options.files 30 | self._flags = [] 31 | if options.force: 32 | self._flags.append("force") 33 | if options.recursive: 34 | self._flags.append("recursive") 35 | 36 | def _needs_target(self) -> bool: 37 | return False 38 | 39 | def _start(self) -> None: 40 | try: 41 | self._attach(self._pick_worker_pid()) 42 | 43 | data_dir = os.path.dirname(__file__) 44 | with codecs.open(os.path.join(data_dir, "fs_agent.js"), "r", "utf-8") as f: 45 | source = f.read() 46 | 47 | def on_message(message: Any, data: Any) -> None: 48 | self._reactor.schedule(lambda: self._on_message(message, data)) 49 | 50 | assert self._session is not None 51 | script = self._session.create_script(name="pull", source=source) 52 | script.on("message", on_message) 53 | self._on_script_created(script) 54 | script.load() 55 | 56 | errors = script.exports_sync.rm(self._paths, self._flags) 57 | 58 | for message in errors: 59 | self._print(Fore.RED + Style.BRIGHT + message + Style.RESET_ALL, file=sys.stderr) 60 | 61 | status = 0 if len(errors) == 0 else 1 62 | self._exit(status) 63 | except Exception as e: 64 | self._update_status(str(e)) 65 | self._exit(1) 66 | return 67 | 68 | def _on_message(self, message: Any, data: Any) -> None: 69 | print(message) 70 | 71 | 72 | if __name__ == "__main__": 73 | try: 74 | main() 75 | except KeyboardInterrupt: 76 | pass 77 | -------------------------------------------------------------------------------- /frida_tools/model.py: -------------------------------------------------------------------------------- 1 | class Module: 2 | def __init__(self, name: str, base_address: int, size: int, path: str) -> None: 3 | self.name = name 4 | self.base_address = base_address 5 | self.size = size 6 | self.path = path 7 | 8 | def __repr__(self) -> str: 9 | return 'Module(name="%s", base_address=0x%x, size=%d, path="%s")' % ( 10 | self.name, 11 | self.base_address, 12 | self.size, 13 | self.path, 14 | ) 15 | 16 | def __hash__(self) -> int: 17 | return self.base_address.__hash__() 18 | 19 | def __eq__(self, other: object) -> bool: 20 | return isinstance(other, Module) and self.base_address == other.base_address 21 | 22 | def __ne__(self, other: object) -> bool: 23 | return not (isinstance(other, Module) and self.base_address == other.base_address) 24 | 25 | 26 | class Function: 27 | def __init__(self, name: str, absolute_address: int) -> None: 28 | self.name = name 29 | self.absolute_address = absolute_address 30 | 31 | def __str__(self) -> str: 32 | return self.name 33 | 34 | def __repr__(self) -> str: 35 | return 'Function(name="%s", absolute_address=0x%x)' % (self.name, self.absolute_address) 36 | 37 | def __hash__(self) -> int: 38 | return self.absolute_address.__hash__() 39 | 40 | def __eq__(self, other: object) -> bool: 41 | return isinstance(other, Function) and self.absolute_address == other.absolute_address 42 | 43 | def __ne__(self, other: object) -> bool: 44 | return not (isinstance(other, Function) and self.absolute_address == other.absolute_address) 45 | 46 | 47 | class ModuleFunction(Function): 48 | def __init__(self, module: Module, name: str, relative_address: int, exported: bool) -> None: 49 | super().__init__(name, module.base_address + relative_address) 50 | self.module = module 51 | self.relative_address = relative_address 52 | self.exported = exported 53 | 54 | def __repr__(self) -> str: 55 | return 'ModuleFunction(module="%s", name="%s", relative_address=0x%x)' % ( 56 | self.module.name, 57 | self.name, 58 | self.relative_address, 59 | ) 60 | 61 | 62 | class ObjCMethod(Function): 63 | def __init__(self, mtype: str, cls: str, method: str, address: int) -> None: 64 | self.mtype = mtype 65 | self.cls = cls 66 | self.method = method 67 | self.address = address 68 | super().__init__(self.display_name(), address) 69 | 70 | def display_name(self) -> str: 71 | return "{mtype}[{cls} {method}]".format(mtype=self.mtype, cls=self.cls, method=self.method) 72 | 73 | def __repr__(self) -> str: 74 | return 'ObjCMethod(mtype="%s", cls="%s", method="%s", address=0x%x)' % ( 75 | self.mtype, 76 | self.cls, 77 | self.method, 78 | self.address, 79 | ) 80 | -------------------------------------------------------------------------------- /frida_tools/join.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from typing import Any, List, MutableMapping 3 | 4 | 5 | def main() -> None: 6 | from frida_tools.application import ConsoleApplication, await_ctrl_c 7 | 8 | class JoinApplication(ConsoleApplication): 9 | def __init__(self) -> None: 10 | ConsoleApplication.__init__(self, await_ctrl_c) 11 | self._parsed_options: MutableMapping[str, Any] = {} 12 | 13 | def _usage(self) -> str: 14 | return "%(prog)s [options] target portal-location [portal-certificate] [portal-token]" 15 | 16 | def _add_options(self, parser: argparse.ArgumentParser) -> None: 17 | parser.add_argument( 18 | "--portal-location", help="join portal at LOCATION", metavar="LOCATION", dest="portal_location" 19 | ) 20 | parser.add_argument( 21 | "--portal-certificate", 22 | help="speak TLS with portal, expecting CERTIFICATE", 23 | metavar="CERTIFICATE", 24 | dest="portal_certificate", 25 | ) 26 | parser.add_argument( 27 | "--portal-token", help="authenticate with portal using TOKEN", metavar="TOKEN", dest="portal_token" 28 | ) 29 | parser.add_argument( 30 | "--portal-acl-allow", 31 | help="limit portal access to control channels with TAG", 32 | metavar="TAG", 33 | action="append", 34 | dest="portal_acl", 35 | ) 36 | 37 | def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None: 38 | location = args[0] if len(args) >= 1 else options.portal_location 39 | certificate = args[1] if len(args) >= 2 else options.portal_certificate 40 | token = args[2] if len(args) >= 3 else options.portal_token 41 | acl = options.portal_acl 42 | 43 | if location is None: 44 | parser.error("portal location must be specified") 45 | 46 | if certificate is not None: 47 | self._parsed_options["certificate"] = certificate 48 | if token is not None: 49 | self._parsed_options["token"] = token 50 | if acl is not None: 51 | self._parsed_options["acl"] = acl 52 | 53 | self._location = location 54 | 55 | def _needs_target(self) -> bool: 56 | return True 57 | 58 | def _start(self) -> None: 59 | self._update_status("Joining portal...") 60 | try: 61 | assert self._session is not None 62 | self._session.join_portal(self._location, **self._parsed_options) 63 | except Exception as e: 64 | self._update_status("Unable to join: " + str(e)) 65 | self._exit(1) 66 | return 67 | self._update_status("Joined!") 68 | self._exit(0) 69 | 70 | def _stop(self) -> None: 71 | pass 72 | 73 | app = JoinApplication() 74 | app.run() 75 | 76 | 77 | if __name__ == "__main__": 78 | try: 79 | main() 80 | except KeyboardInterrupt: 81 | pass 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Frida CLI tools 2 | 3 | CLI tools for [Frida](https://frida.re). 4 | 5 | ### Installing Fish completions 6 | 7 | Currently there is no mechanism to install Fish completions through the setup.py 8 | script so if you want to have completions in Fish you will have to install it 9 | manually. Unless you've changed your XDG_CONFIG_HOME location, you should just 10 | copy the completion file into `~/.config/fish/completions` like so: 11 | 12 | cp completions/frida.fish ~/.config/fish/completions 13 | 14 | ### frida-itrace file format 15 | 16 | File starts with a 4-byte magic: "ITRC" 17 | https://github.com/frida/frida-tools/blob/1ea077fdb49440e5807cf25fae41e389e3d2bd4a/frida_tools/itracer.py#L365-L366 18 | 19 | Then, following that, there are two different types of records, MESSAGE and 20 | CHUNK. Each record starts with a big-endian uint32 specifying the type of 21 | record, where 1 means MESSAGE, 2 means CHUNK. 22 | 23 | #### MESSAGE 24 | 25 | - `length`: uint32 (big-endian) 26 | - `message`: JSON, UTF-8 encoded 27 | - `data_size`: uint32 (big-endian) 28 | - `data_values`: uint8[data_size] 29 | 30 | Generated [here](https://github.com/frida/frida-tools/blob/1ea077fdb49440e5807cf25fae41e389e3d2bd4a/frida_tools/itracer.py#L451-L458). 31 | 32 | There are three different kinds of MESSAGEs: 33 | 34 | - ["itrace:start"](https://github.com/frida/frida-tools/blob/1ea077fdb49440e5807cf25fae41e389e3d2bd4a/agents/itracer/agent.ts#L68-L76): 35 | Signals that the trace is starting, providing the initial register values. 36 | Contains register names and sizes in the JSON portion, and register values in 37 | the data portion. 38 | Generated [here](https://github.com/frida/frida-itrace/blob/ad7780bde9e518e325d7aaf848e9a29e1a53b7d2/lib/backend.ts#L341-L359). 39 | - "itrace:end": Signals that the endpoint was reached, when specifying a range 40 | with an end address included. 41 | - "itrace:compile": Signals that a basic block was discovered, providing the 42 | schema of future CHUNKs pertaining to it. 43 | Generated [here](https://github.com/frida/frida-itrace/blob/ad7780bde9e518e325d7aaf848e9a29e1a53b7d2/lib/backend.ts#L277-L323) 44 | and by the [code](https://github.com/frida/frida-itrace/blob/ad7780bde9e518e325d7aaf848e9a29e1a53b7d2/lib/backend.ts#L398-L401) 45 | above it that computes the "writes" array. 46 | 47 | The "writes" array contains tuples (arrays) that look like this: 48 | 49 | (block_offset, cpu_ctx_offset) 50 | 51 | Where `block_offset` is how many bytes into the basic block the write happens, 52 | and `cpu_ctx_offset` is the index into the registers declared by 53 | "itrace:start". 54 | 55 | #### CHUNK 56 | 57 | - `size`: uint32 (big-endian) 58 | - `data`: uint8[size] 59 | 60 | Generated [here](https://github.com/frida/frida-tools/blob/1ea077fdb49440e5807cf25fae41e389e3d2bd4a/frida_tools/itracer.py#L464-L465). 61 | 62 | The CHUNK records combine to a stream of raw register values at different parts 63 | of the given basic block. Each record looks like this: 64 | 65 | - `block_start_address`: uint64 (target-endian, i.e. little-endian on arm64) 66 | - `link_register_value`: uint64 (target-endian) 67 | - `block_register_values`: uint64[n], where n depends on the specific basic 68 | block. (See above docs on "itrace:compile" and its "writes" array.) 69 | -------------------------------------------------------------------------------- /frida_tools/reactor.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import threading 3 | import time 4 | from typing import Callable, Deque, Optional, Tuple, Union 5 | 6 | import frida 7 | 8 | 9 | class Reactor: 10 | """ 11 | Run the given function until return in the main thread (or the thread of 12 | the run method) and in a background thread receive and run additional tasks. 13 | """ 14 | 15 | def __init__( 16 | self, run_until_return: Callable[["Reactor"], None], on_stop: Optional[Callable[[], None]] = None 17 | ) -> None: 18 | self._running = False 19 | self._run_until_return = run_until_return 20 | self._on_stop = on_stop 21 | self._pending: Deque[Tuple[Callable[[], None], Union[int, float]]] = collections.deque([]) 22 | self._lock = threading.Lock() 23 | self._cond = threading.Condition(self._lock) 24 | 25 | self.io_cancellable = frida.Cancellable() 26 | 27 | self.ui_cancellable = frida.Cancellable() 28 | self._ui_cancellable_fd = self.ui_cancellable.get_pollfd() 29 | 30 | def __del__(self) -> None: 31 | self._ui_cancellable_fd.release() 32 | 33 | def is_running(self) -> bool: 34 | with self._lock: 35 | return self._running 36 | 37 | def run(self) -> None: 38 | with self._lock: 39 | self._running = True 40 | 41 | worker = threading.Thread(target=self._run) 42 | worker.start() 43 | 44 | self._run_until_return(self) 45 | 46 | self.stop() 47 | worker.join() 48 | 49 | def _run(self) -> None: 50 | running = True 51 | while running: 52 | now = time.time() 53 | work = None 54 | timeout = None 55 | previous_pending_length = -1 56 | with self._lock: 57 | for item in self._pending: 58 | (f, when) = item 59 | if now >= when: 60 | work = f 61 | self._pending.remove(item) 62 | break 63 | if len(self._pending) > 0: 64 | timeout = max([min(map(lambda item: item[1], self._pending)) - now, 0]) 65 | previous_pending_length = len(self._pending) 66 | 67 | if work is not None: 68 | with self.io_cancellable: 69 | try: 70 | work() 71 | except frida.OperationCancelledError: 72 | pass 73 | 74 | with self._lock: 75 | if self._running and len(self._pending) == previous_pending_length: 76 | self._cond.wait(timeout) 77 | running = self._running 78 | 79 | if self._on_stop is not None: 80 | self._on_stop() 81 | 82 | self.ui_cancellable.cancel() 83 | 84 | def stop(self) -> None: 85 | self.schedule(self._stop) 86 | 87 | def _stop(self) -> None: 88 | with self._lock: 89 | self._running = False 90 | 91 | def schedule(self, f: Callable[[], None], delay: Optional[Union[int, float]] = None) -> None: 92 | """ 93 | append a function to the tasks queue of the reactor, optionally with a 94 | delay in seconds 95 | """ 96 | 97 | now = time.time() 98 | if delay is not None: 99 | when = now + delay 100 | else: 101 | when = now 102 | with self._lock: 103 | self._pending.append((f, when)) 104 | self._cond.notify() 105 | 106 | def cancel_io(self) -> None: 107 | self.io_cancellable.cancel() 108 | -------------------------------------------------------------------------------- /apps/tracer/src/MemoryView.tsx: -------------------------------------------------------------------------------- 1 | import "./MemoryView.css"; 2 | import { Button, ControlGroup, InputGroup, SegmentedControl, Spinner } from "@blueprintjs/core"; 3 | import { useR2 } from "@frida/react-use-r2"; 4 | import { useCallback, useEffect, useRef, useState } from "react"; 5 | 6 | export interface MemoryViewProps { 7 | address?: bigint; 8 | onSelectAddress: SelectAddressHandler; 9 | } 10 | 11 | export type SelectAddressHandler = (address: bigint) => void; 12 | 13 | export default function MemoryView({ address, onSelectAddress }: MemoryViewProps) { 14 | const addressInputRef = useRef(null); 15 | const [format, setFormat] = useState("x"); 16 | const [data, setData] = useState(""); 17 | const [isLoading, setIsLoading] = useState(false); 18 | const { executeR2Command } = useR2(); 19 | 20 | useEffect(() => { 21 | if (address === undefined) { 22 | return; 23 | } 24 | 25 | let ignore = false; 26 | setIsLoading(true); 27 | 28 | async function start() { 29 | const data = await executeR2Command(`${format} @ 0x${address!.toString(16)}`); 30 | if (ignore) { 31 | return; 32 | } 33 | 34 | setData(data); 35 | setIsLoading(false); 36 | } 37 | 38 | start(); 39 | 40 | return () => { 41 | ignore = true; 42 | }; 43 | }, [format, address, executeR2Command]); 44 | 45 | useEffect(() => { 46 | if (address === undefined) { 47 | return; 48 | } 49 | 50 | addressInputRef.current!.value = `0x${address.toString(16)}`; 51 | }, [address]); 52 | 53 | const adjustAddress = useCallback((delta: number) => { 54 | let newAddress: bigint; 55 | try { 56 | newAddress = BigInt(addressInputRef.current!.value) + BigInt(delta); 57 | } catch (e) { 58 | return; 59 | } 60 | onSelectAddress(newAddress); 61 | }, [onSelectAddress]); 62 | 63 | if (isLoading) { 64 | return ( 65 | 66 | ); 67 | } 68 | 69 | return ( 70 |
71 |
72 | 73 | 74 | { 77 | if (e.key === "Enter") { 78 | e.preventDefault(); 79 | adjustAddress(0); 80 | } 81 | }} 82 | placeholder="Memory address…" 83 | /> 84 | 85 | 86 | 109 |
110 |
111 |
112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /apps/tracer/src/HandlerList.tsx: -------------------------------------------------------------------------------- 1 | import "./HandlerList.css"; 2 | import { Handler, ScopeId, HandlerId } from "./model.js"; 3 | import { Tree, TreeNodeInfo } from "@blueprintjs/core"; 4 | import { useEffect, useRef, useState } from "react"; 5 | 6 | export interface HandlerListProps { 7 | handlers: Handler[]; 8 | selectedScope: ScopeId; 9 | onScopeSelect: ScopeEventHandler; 10 | selectedHandler: HandlerId | null; 11 | onHandlerSelect: HandlerEventHandler; 12 | } 13 | 14 | export type ScopeEventHandler = (id: ScopeId) => void; 15 | export type HandlerEventHandler = (id: HandlerId) => void; 16 | 17 | export default function HandlerList({ handlers, selectedScope, onScopeSelect, selectedHandler, onHandlerSelect }: HandlerListProps) { 18 | const treeRef = useRef(null); 19 | const [mouseInsideNode, setMouseInsideNode] = useState(false); 20 | 21 | const scopes = handlers.reduce((result, { scope }) => result.add(scope), new Set()); 22 | const handlerNodes: TreeNodeInfo[] = Array.from(scopes).map(scope => { 23 | const isExpanded = scope === selectedScope; 24 | return { 25 | id: scope, 26 | label: labelFromScope(scope), 27 | isExpanded, 28 | icon: isExpanded ? "folder-open" : "folder-close", 29 | childNodes: handlers 30 | .filter(h => h.scope === scope) 31 | .map(({ id, display_name, config }) => { 32 | return { 33 | id, 34 | label: display_name, 35 | isSelected: id === selectedHandler, 36 | icon: "code-block", 37 | className: config.muted ? "handler-node-muted" : "", 38 | }; 39 | }), 40 | }; 41 | }); 42 | 43 | function handleNodeClick(node: TreeNodeInfo) { 44 | if (typeof node.id === "string") { 45 | onScopeSelect((selectedScope !== node.id) ? node.id : ""); 46 | } else { 47 | onHandlerSelect(node.id as HandlerId); 48 | } 49 | } 50 | 51 | function handleNodeExpand(node: TreeNodeInfo) { 52 | onScopeSelect(node.id as ScopeId); 53 | } 54 | 55 | function handleNodeCollapse() { 56 | onScopeSelect(""); 57 | } 58 | 59 | useEffect(() => { 60 | if (selectedHandler === null || mouseInsideNode) { 61 | return; 62 | } 63 | 64 | const tree = treeRef.current; 65 | if (tree === null) { 66 | return; 67 | } 68 | 69 | const scopeElement = tree.getNodeContentElement(selectedScope); 70 | if (scopeElement === undefined) { 71 | return; 72 | } 73 | 74 | scopeElement.addEventListener("transitionend", scrollIntoView); 75 | 76 | requestAnimationFrame(scrollIntoView); 77 | 78 | function scrollIntoView() { 79 | tree!.getNodeContentElement(selectedHandler!)?.scrollIntoView({ block: "center" }); 80 | } 81 | 82 | return () => { 83 | scopeElement.removeEventListener("transitionend", scrollIntoView); 84 | }; 85 | }, [treeRef, selectedScope, selectedHandler, mouseInsideNode]); 86 | 87 | return ( 88 | setMouseInsideNode(true)} 93 | onNodeMouseLeave={() => setMouseInsideNode(false)} 94 | onNodeClick={handleNodeClick} 95 | onNodeExpand={handleNodeExpand} 96 | onNodeCollapse={handleNodeCollapse} 97 | /> 98 | ); 99 | } 100 | 101 | function labelFromScope(scope: string) { 102 | let dirsepIndex = scope.lastIndexOf("/"); 103 | if (dirsepIndex === -1) { 104 | dirsepIndex = scope.lastIndexOf("\\"); 105 | } 106 | if (dirsepIndex === -1) { 107 | return scope; 108 | } 109 | return scope.substring(dirsepIndex + 1); 110 | } 111 | -------------------------------------------------------------------------------- /agents/repl/agent.ts: -------------------------------------------------------------------------------- 1 | class REPL { 2 | #quickCommands = new Map(); 3 | 4 | registerQuickCommand(name: string, handler: QuickCommandHandler) { 5 | this.#quickCommands.set(name, handler); 6 | } 7 | 8 | unregisterQuickCommand(name: string) { 9 | this.#quickCommands.delete(name); 10 | } 11 | 12 | _invokeQuickCommand(tokens: string[]): any { 13 | const name = tokens[0]; 14 | const handler = this.#quickCommands.get(name); 15 | if (handler !== undefined) { 16 | const { minArity, onInvoke } = handler; 17 | if (tokens.length - 1 < minArity) { 18 | throw Error(`${name} needs at least ${minArity} arg${(minArity === 1) ? "" : "s"}`); 19 | } 20 | return onInvoke(...tokens.slice(1)); 21 | } else { 22 | throw Error(`Unknown command ${name}`); 23 | } 24 | } 25 | } 26 | 27 | const repl = new REPL(); 28 | 29 | globalThis.REPL = repl; 30 | globalThis.cm = null; 31 | globalThis.cs = {}; 32 | 33 | registerLazyBridgeGetter("ObjC"); 34 | registerLazyBridgeGetter("Swift"); 35 | registerLazyBridgeGetter("Java"); 36 | 37 | function registerLazyBridgeGetter(name: string) { 38 | Object.defineProperty(globalThis, name, { 39 | enumerable: true, 40 | configurable: true, 41 | get() { 42 | return lazyLoadBridge(name); 43 | } 44 | }); 45 | } 46 | 47 | function lazyLoadBridge(name: string): unknown { 48 | send({ type: "frida:load-bridge", name }); 49 | let bridge: unknown; 50 | recv("frida:bridge-loaded", message => { 51 | bridge = Script.evaluate(`/frida/bridges/${message.filename}`, 52 | "(function () { " + [ 53 | message.source, 54 | `Object.defineProperty(globalThis, '${name}', { value: bridge });`, 55 | `return bridge;` 56 | ].join("\n") + " })();"); 57 | }).wait(); 58 | return bridge; 59 | } 60 | 61 | declare global { 62 | var REPL: REPL; 63 | var cm: CModule | null; 64 | var cs: { 65 | [name: string]: NativePointerValue; 66 | }; 67 | } 68 | 69 | interface QuickCommandHandler { 70 | minArity: number; 71 | onInvoke: (...args: string[]) => any; 72 | } 73 | 74 | const rpcExports: RpcExports = { 75 | fridaEvaluateExpression(expression: string) { 76 | return evaluate(() => globalThis.eval(expression)); 77 | }, 78 | fridaEvaluateQuickCommand(tokens: string[]) { 79 | return evaluate(() => repl._invokeQuickCommand(tokens)); 80 | }, 81 | fridaLoadCmodule(code: string | null, toolchain: CModuleToolchain) { 82 | const cs = globalThis.cs; 83 | 84 | if (cs._frida_log === undefined) 85 | cs._frida_log = new NativeCallback(onLog, "void", ["pointer"]); 86 | 87 | let codeToLoad: string | ArrayBuffer | null = code; 88 | if (code === null) { 89 | recv("frida:cmodule-payload", (message, data) => { 90 | codeToLoad = data; 91 | }); 92 | } 93 | 94 | globalThis.cm = new CModule(codeToLoad!, cs, { toolchain }); 95 | }, 96 | }; 97 | 98 | function evaluate(func: () => any) { 99 | try { 100 | const result = func(); 101 | if (result instanceof ArrayBuffer) { 102 | return result; 103 | } else { 104 | const type = (result === null) ? "null" : typeof result; 105 | return [type, result]; 106 | } 107 | } catch (exception) { 108 | const e = exception as Error; 109 | return ["error", { 110 | name: e.name, 111 | message: e.message, 112 | stack: e.stack 113 | }]; 114 | } 115 | } 116 | 117 | Object.defineProperty(rpc, "exports", { 118 | get() { 119 | return rpcExports; 120 | }, 121 | set(value) { 122 | for (const [k, v] of Object.entries(value)) { 123 | rpcExports[k] = v as AnyFunction; 124 | } 125 | } 126 | }); 127 | 128 | function onLog(messagePtr: NativePointer) { 129 | const message = messagePtr.readUtf8String(); 130 | console.log(message); 131 | } 132 | -------------------------------------------------------------------------------- /frida_tools/ls.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import codecs 3 | import os 4 | from datetime import datetime, timezone 5 | from operator import itemgetter 6 | from typing import Any, List 7 | 8 | from colorama import Fore, Style 9 | 10 | from frida_tools.application import ConsoleApplication 11 | 12 | STYLE_DIR = Fore.BLUE + Style.BRIGHT 13 | STYLE_EXECUTABLE = Fore.GREEN + Style.BRIGHT 14 | STYLE_LINK = Fore.CYAN + Style.BRIGHT 15 | STYLE_ERROR = Fore.RED + Style.BRIGHT 16 | 17 | 18 | def main() -> None: 19 | app = LsApplication() 20 | app.run() 21 | 22 | 23 | class LsApplication(ConsoleApplication): 24 | def _add_options(self, parser: argparse.ArgumentParser) -> None: 25 | parser.add_argument("files", help="files to list information about", nargs="*") 26 | 27 | def _usage(self) -> str: 28 | return "%(prog)s [options] [FILE]..." 29 | 30 | def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None: 31 | self._files = options.files 32 | 33 | def _needs_target(self) -> bool: 34 | return False 35 | 36 | def _start(self) -> None: 37 | try: 38 | self._attach(self._pick_worker_pid()) 39 | 40 | data_dir = os.path.dirname(__file__) 41 | with codecs.open(os.path.join(data_dir, "fs_agent.js"), "r", "utf-8") as f: 42 | source = f.read() 43 | 44 | def on_message(message: Any, data: Any) -> None: 45 | print(message) 46 | 47 | assert self._session is not None 48 | script = self._session.create_script(name="ls", source=source) 49 | script.on("message", on_message) 50 | self._on_script_created(script) 51 | script.load() 52 | 53 | groups = script.exports_sync.ls(self._files) 54 | except Exception as e: 55 | self._update_status(f"Failed to retrieve listing: {e}") 56 | self._exit(1) 57 | return 58 | 59 | exit_status = 0 60 | for i, group in enumerate(sorted(groups, key=lambda g: g["path"])): 61 | path = group["path"] 62 | if path != "" and len(groups) > 1: 63 | if i > 0: 64 | self._print("") 65 | self._print(path + ":") 66 | 67 | for path, message in group["errors"]: 68 | self._print(STYLE_ERROR + message + Style.RESET_ALL) 69 | exit_status = 2 70 | 71 | rows = [] 72 | for name, target, type, access, nlink, owner, group, size, raw_mtime in group["entries"]: 73 | mtime = datetime.fromtimestamp(raw_mtime / 1000.0, tz=timezone.utc) 74 | rows.append((type + access, str(nlink), owner, group, str(size), mtime.strftime("%c"), name, target)) 75 | 76 | if len(rows) == 0: 77 | break 78 | 79 | widths = [] 80 | for column_index in range(len(rows[0]) - 2): 81 | width = max(map(lambda row: len(row[column_index]), rows)) 82 | widths.append(width) 83 | 84 | adjustments = [ 85 | "", 86 | ">", 87 | "<", 88 | "<", 89 | ">", 90 | "<", 91 | ] 92 | col_formats = [] 93 | for i, width in enumerate(widths): 94 | adj = adjustments[i] 95 | if adj != "": 96 | fmt = "{:" + adj + str(width) + "}" 97 | else: 98 | fmt = "{}" 99 | col_formats.append(fmt) 100 | row_description = " ".join(col_formats) 101 | 102 | for row in sorted(rows, key=itemgetter(6)): 103 | meta_fields = row_description.format(*row[:-2]) 104 | 105 | name, target = row[6:8] 106 | ftype_and_perms = row[0] 107 | ftype = ftype_and_perms[0] 108 | fperms = ftype_and_perms[1:] 109 | name = format_name(name, ftype, fperms, target) 110 | 111 | self._print(meta_fields + " " + name) 112 | 113 | self._exit(exit_status) 114 | 115 | 116 | def format_name(name: str, ftype: str, fperms: str, target) -> str: 117 | if ftype == "l": 118 | target_path, target_details = target 119 | if target_details is not None: 120 | target_type, target_perms = target_details 121 | target_summary = format_name(target_path, target_type, target_perms, None) 122 | else: 123 | target_summary = STYLE_ERROR + target_path + Style.RESET_ALL 124 | return STYLE_LINK + name + Style.RESET_ALL + " -> " + target_summary 125 | 126 | if ftype == "d": 127 | return STYLE_DIR + name + Style.RESET_ALL 128 | 129 | if "x" in fperms: 130 | return STYLE_EXECUTABLE + name + Style.RESET_ALL 131 | 132 | return name 133 | 134 | 135 | if __name__ == "__main__": 136 | try: 137 | main() 138 | except KeyboardInterrupt: 139 | pass 140 | -------------------------------------------------------------------------------- /agents/itracer/agent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TraceBuffer, 3 | TraceBufferReader, 4 | TraceSession, 5 | TraceStrategy, 6 | } from "frida-itrace"; 7 | 8 | type RawTraceStrategy = RawTraceThreadStrategy | RawTraceRangeStrategy; 9 | type RawTraceThreadStrategy = ["thread", ["id", number] | ["index", number]]; 10 | type RawTraceRangeStrategy = ["range", [CodeLocation, CodeLocation | null]] 11 | 12 | type CodeLocation = 13 | | ["address", string] 14 | | ["module-export", [string, string]] 15 | | ["module-offset", [string, number]] 16 | | ["symbol", string] 17 | ; 18 | 19 | const BUFFER_READER_POLL_INTERVAL_MSEC = 10; 20 | 21 | class Agent { 22 | session: TraceSession | null = null; 23 | buffer: TraceBuffer | null = null; 24 | reader: TraceBufferReader | null = null; 25 | drainTimer: NodeJS.Timeout | null = null; 26 | 27 | createBuffer(): string { 28 | this.buffer = TraceBuffer.create(); 29 | return this.buffer.location; 30 | } 31 | 32 | openBuffer(location: string) { 33 | this.buffer = TraceBuffer.open(location); 34 | } 35 | 36 | launchBufferReader() { 37 | this.reader = new TraceBufferReader(this.buffer!); 38 | this.drainTimer = setInterval(this.#drainBuffer, BUFFER_READER_POLL_INTERVAL_MSEC); 39 | } 40 | 41 | stopBufferReader() { 42 | clearInterval(this.drainTimer!); 43 | this.drainTimer = null; 44 | 45 | this.#drainBuffer(); 46 | 47 | this.reader = null; 48 | } 49 | 50 | #drainBuffer = () => { 51 | const chunk = this.reader!.read(); 52 | if (chunk.byteLength === 0) { 53 | return; 54 | } 55 | send({ type: "itrace:chunk" }, chunk); 56 | 57 | const lost = this.reader!.lost; 58 | if (lost !== 0) { 59 | send({ type: "itrace:lost", payload: { lost } }); 60 | } 61 | }; 62 | 63 | launchTraceSession(rawStrategy: RawTraceStrategy) { 64 | const strategy = parseTraceStrategy(rawStrategy); 65 | const session = new TraceSession(strategy, this.buffer!); 66 | this.session = session; 67 | 68 | session.events.on("start", (regSpecs, regValues) => { 69 | send({ type: "itrace:start", payload: regSpecs }, regValues); 70 | }); 71 | session.events.on("end", () => { 72 | send({ type: "itrace:end" }); 73 | }); 74 | session.events.on("compile", block => { 75 | send({ type: "itrace:compile", payload: block }); 76 | }); 77 | session.events.on("panic", message => { 78 | console.error(message); 79 | }); 80 | 81 | session.open(); 82 | } 83 | 84 | queryProgramName() { 85 | return Process.enumerateModules()[0].name; 86 | } 87 | 88 | listThreads() { 89 | return Process.enumerateThreads(); 90 | } 91 | } 92 | 93 | function parseTraceStrategy(rawStrategy: RawTraceStrategy): TraceStrategy { 94 | const [kind, params] = rawStrategy; 95 | switch (kind) { 96 | case "thread": { 97 | let thread: ThreadDetails; 98 | const threads = Process.enumerateThreads(); 99 | switch (params[0]) { 100 | case "id": { 101 | const desiredId = params[1]; 102 | const th = threads.find(t => t.id === desiredId); 103 | if (th === undefined) { 104 | throw new Error("invalid thread ID"); 105 | } 106 | thread = th; 107 | break; 108 | } 109 | case "index": { 110 | thread = threads[params[1]]; 111 | if (thread === undefined) { 112 | throw new Error("invalid thread index"); 113 | } 114 | break; 115 | } 116 | } 117 | return { 118 | type: "thread", 119 | threadId: thread.id 120 | }; 121 | } 122 | case "range": { 123 | return { 124 | type: "range", 125 | start: parseCodeLocation(params[0]), 126 | end: parseCodeLocation(params[1]) 127 | }; 128 | } 129 | } 130 | } 131 | 132 | function parseCodeLocation(location: CodeLocation | null): NativePointer { 133 | if (location === null) { 134 | return NULL; 135 | } 136 | 137 | const [kind, params] = location; 138 | switch (kind) { 139 | case "address": { 140 | const address = ptr(params); 141 | try { 142 | address.readVolatile(1); 143 | } catch (e) { 144 | throw new Error(`invalid address: ${address}`); 145 | } 146 | return address; 147 | } 148 | case "module-export": 149 | return Process.getModuleByName(params[0]).getExportByName(params[1]); 150 | case "module-offset": 151 | return Process.getModuleByName(params[0]).base.add(params[1]); 152 | case "symbol": { 153 | const name = params; 154 | const { address } = DebugSymbol.fromName(name); 155 | if (!address.isNull()) { 156 | return address; 157 | } 158 | return Module.getGlobalExportByName(name); 159 | } 160 | } 161 | } 162 | 163 | const agent = new Agent(); 164 | 165 | const agentMethodNames = Object.getOwnPropertyNames(Object.getPrototypeOf(agent)) 166 | .filter(name => name !== "constructor"); 167 | for (const name of agentMethodNames) { 168 | rpc.exports[name] = (agent as any)[name].bind(agent); 169 | } 170 | -------------------------------------------------------------------------------- /apps/tracer/src/AddTargetsDialog.tsx: -------------------------------------------------------------------------------- 1 | import "./AddTargetsDialog.css"; 2 | import { TraceSpecScope, StagedItem, StagedItemId } from "./model.js"; 3 | import { 4 | Button, 5 | ButtonGroup, 6 | Card, 7 | CardList, 8 | Dialog, 9 | DialogBody, 10 | DialogFooter, 11 | FormGroup, 12 | InputGroup, 13 | Menu, 14 | MenuItem, 15 | Popover 16 | } from "@blueprintjs/core"; 17 | import { useRef, useState } from "react"; 18 | import { useDebouncedCallback } from "use-debounce"; 19 | 20 | export interface AddTargetsProps { 21 | isOpen: boolean; 22 | stagedItems: StagedItem[]; 23 | onClose: CloseEventHandler; 24 | onQuery: QueryEventHandler; 25 | onCommit: CommitEventHandler; 26 | } 27 | 28 | export type CloseEventHandler = () => void; 29 | export type QueryEventHandler = (scope: TraceSpecScope, query: string) => void; 30 | export type CommitEventHandler = (id: StagedItemId | null) => void; 31 | 32 | export default function AddTargetsDialog({ isOpen, stagedItems, onClose, onQuery, onCommit }: AddTargetsProps) { 33 | const inputRef = useRef(null); 34 | const onQueryDebounced = useDebouncedCallback(onQuery, 250); 35 | const [scope, setScope] = useState(TraceSpecScope.Function); 36 | 37 | const kindMenu = ( 38 | 41 | { 42 | Object.keys(TraceSpecScope) 43 | .map(s => ( 44 | setScope((TraceSpecScope as any)[s])} 48 | /> 49 | )) 50 | } 51 | 52 | } 53 | placement="bottom-end" 54 | > 55 | 58 | 59 | ); 60 | 61 | const actions = ( 62 | 63 | 64 | 65 | 66 | ); 67 | 68 | const candidates = (stagedItems.length !== 0) ? ( 69 | 70 | 71 | {stagedItems.map(([id, scope, member]) => { 72 | return ( 73 | 74 | {scope}!{member} 75 | 123 | {(spawnedProgram !== null) ? : null} 124 | 125 | 126 |
127 | {connectionError} 128 |
129 | 130 | 137 | 144 | 157 | 158 |
159 | setHandlerMuted(e.target.checked)} 164 | > 165 | Muted 166 | 167 | setCaptureBacktraces(e.target.checked)} 171 | > 172 | Capture Backtraces 173 | 174 |
175 |
176 | 182 |
183 | 184 | 185 | 186 | setSelectedTabId(tabId as any)} animate={false}> 187 | 188 | 189 | 190 | 191 | 192 | 193 | 200 | 201 |
202 | 203 | 204 | ); 205 | } 206 | -------------------------------------------------------------------------------- /frida_tools/pm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import annotations 3 | 4 | import argparse 5 | import html 6 | import itertools 7 | import json 8 | import os 9 | import re 10 | import sys 11 | import textwrap 12 | import threading 13 | import time 14 | from typing import List, Optional 15 | 16 | import frida 17 | from colorama import Style 18 | from prompt_toolkit.formatted_text import HTML 19 | from prompt_toolkit.shortcuts import print_formatted_text 20 | 21 | from frida_tools.application import ConsoleApplication 22 | from frida_tools.cli_formatting import THEME_COLOR 23 | 24 | VERSION_COLOR = "#9e9e9e" 25 | 26 | THEME_ATTR = f'fg="{THEME_COLOR}"' 27 | VERSION_ATTR = f'fg="{VERSION_COLOR}"' 28 | 29 | ANSI_PATTERN = re.compile(r"\x1b\[[0-9;]*m") 30 | 31 | SPINNER_DELAY = 0.25 32 | 33 | 34 | def main() -> None: 35 | PackageManagerApplication().run() 36 | 37 | 38 | class PackageManagerApplication(ConsoleApplication): 39 | def _usage(self) -> str: 40 | return "%(prog)s [options] [...]" 41 | 42 | def _add_options(self, parser: argparse.ArgumentParser) -> None: 43 | default_registry = frida.PackageManager().registry 44 | parser.add_argument( 45 | "--registry", 46 | metavar="HOST", 47 | default=None, 48 | help=f"package registry to use (default: {default_registry})", 49 | ) 50 | 51 | sub = parser.add_subparsers(dest="command", metavar="", required=True) 52 | 53 | search_p = sub.add_parser("search", help="search for packages") 54 | search_p.add_argument("query", nargs="?", default="", help="search string, e.g. 'trace'") 55 | search_p.add_argument("--offset", type=int, metavar="N", help="result offset") 56 | search_p.add_argument("--limit", type=int, metavar="N", help="max results") 57 | search_p.add_argument( 58 | "--json", 59 | action="store_true", 60 | help="emit raw JSON instead of a table", 61 | ) 62 | 63 | install_p = sub.add_parser("install", help="install one or more packages") 64 | install_p.add_argument( 65 | "specs", 66 | nargs="*", 67 | metavar="SPEC", 68 | help="package spec, e.g. 'frida-objc-bridge@^8.0.5' or 'frida-il2cpp-bridge'", 69 | ) 70 | install_p.add_argument( 71 | "--project-root", 72 | default=os.getcwd(), 73 | metavar="DIR", 74 | help="directory that will receive node_modules (default: CWD)", 75 | ) 76 | role_group = install_p.add_mutually_exclusive_group() 77 | role_group.add_argument( 78 | "-P", 79 | "--save-prod", 80 | action="store_const", 81 | const="runtime", 82 | dest="role", 83 | help="save as production dependencies (default)", 84 | ) 85 | role_group.add_argument( 86 | "-D", 87 | "--save-dev", 88 | action="store_const", 89 | const="development", 90 | dest="role", 91 | help="save as development dependencies", 92 | ) 93 | role_group.add_argument( 94 | "--save-optional", action="store_const", const="optional", dest="role", help="save as optional dependencies" 95 | ) 96 | install_p.add_argument( 97 | "--omit", 98 | help="dependency types to skip", 99 | choices=["dev", "optional", "peer"], 100 | dest="omits", 101 | action="append", 102 | ) 103 | install_p.add_argument("--quiet", action="store_true", help="suppress the progress bar") 104 | 105 | def _initialize( 106 | self, 107 | parser: argparse.ArgumentParser, 108 | options: argparse.Namespace, 109 | args: List[str], 110 | ) -> None: 111 | self._opts = options 112 | 113 | pm = frida.PackageManager() 114 | if options.registry is not None: 115 | pm.registry = options.registry 116 | self._pm = pm 117 | 118 | def _needs_device(self) -> bool: 119 | return False 120 | 121 | def _start(self) -> None: 122 | try: 123 | if self._opts.command == "search": 124 | self._cmd_search() 125 | elif self._opts.command == "install": 126 | self._cmd_install() 127 | self._exit(0) 128 | except Exception as e: 129 | self._log("error", str(e)) 130 | self._exit(1) 131 | 132 | def _cmd_search(self) -> None: 133 | interactive = self._have_terminal and not self._plain_terminal 134 | show_spinner = (not self._opts.json) and interactive 135 | 136 | stop_event = None 137 | spinner_thread = None 138 | if show_spinner: 139 | stop_event, spinner_thread = start_spinner(THEME_COLOR) 140 | 141 | try: 142 | res = self._pm.search( 143 | self._opts.query, 144 | offset=self._opts.offset, 145 | limit=self._opts.limit, 146 | ) 147 | finally: 148 | if stop_event is not None: 149 | stop_event.set() 150 | if spinner_thread is not None: 151 | spinner_thread.join() 152 | 153 | if self._opts.json: 154 | print( 155 | json.dumps( 156 | { 157 | "packages": [ 158 | { 159 | "name": p.name, 160 | "version": p.version, 161 | "description": p.description, 162 | "url": p.url, 163 | } 164 | for p in res.packages 165 | ], 166 | "total": res.total, 167 | }, 168 | indent=2, 169 | sort_keys=True, 170 | ) 171 | ) 172 | return 173 | 174 | use_color = interactive 175 | col_w = 80 176 | 177 | for pkg in res.packages: 178 | header = ( 179 | (f"" f"") 180 | if use_color 181 | else f"{pkg.name}@{pkg.version}" 182 | ) 183 | 184 | desc = pkg.description or "" 185 | raw_len = len(pkg.name) + 1 + len(pkg.version) 186 | gap = " " * max(1, 32 - raw_len) 187 | wrapped = textwrap.wrap(desc, width=col_w - 32) 188 | 189 | first_line = f"{header}{gap}{esc(wrapped[0]) if wrapped else ''}" 190 | if use_color: 191 | print_formatted_text(HTML(first_line)) 192 | else: 193 | print(first_line) 194 | 195 | for w in wrapped[1:]: 196 | print(" " * 32 + w) 197 | 198 | url_chunk = f"" if use_color else pkg.url 199 | if use_color: 200 | print_formatted_text(HTML(" " * 32 + url_chunk)) 201 | else: 202 | print(" " * 32 + pkg.url) 203 | print() 204 | 205 | shown = len(res.packages) 206 | offset = self._opts.offset or 0 207 | earlier = offset 208 | later = max(res.total - (offset + shown), 0) 209 | 210 | if earlier or later: 211 | parts = [] 212 | if earlier: 213 | parts.append(f"{earlier} earlier") 214 | if later: 215 | parts.append(f"{later} more") 216 | print("… " + " and ".join(parts) + ". Use --limit and --offset to navigate through results.") 217 | 218 | def _cmd_install(self) -> None: 219 | pm = self._pm 220 | normalized_omits = self._normalize_omits(self._opts.omits) 221 | 222 | interactive = self._have_terminal and not self._plain_terminal 223 | 224 | if self._opts.quiet or not interactive: 225 | result = pm.install( 226 | project_root=self._opts.project_root, 227 | role=self._opts.role, 228 | specs=self._opts.specs, 229 | omits=normalized_omits, 230 | ) 231 | else: 232 | BAR_LEN = 30 233 | FG = ansi_fg(THEME_COLOR) 234 | RESET = Style.RESET_ALL 235 | start_time = time.time() 236 | 237 | bar_visible = False 238 | longest_vis = 0 239 | last_snapshot = None 240 | lock = threading.Lock() 241 | done = threading.Event() 242 | 243 | def render(phase: str, fraction: float, details: Optional[str]) -> None: 244 | nonlocal bar_visible, longest_vis, last_snapshot 245 | 246 | if details is not None: 247 | return 248 | 249 | with lock: 250 | last_snapshot = (phase, fraction, details) 251 | 252 | if not bar_visible and time.time() - start_time < SPINNER_DELAY: 253 | return 254 | bar_visible = True 255 | 256 | pct = int(fraction * 100) 257 | fill = int(fraction * BAR_LEN) 258 | bar = "█" * fill + " " * (BAR_LEN - fill) 259 | msg = phase.replace("-", " ") 260 | 261 | line = f"\r{FG}[{bar}]{RESET} {pct:3d}% {msg}" 262 | vis = len(ANSI_PATTERN.sub("", line)) - 1 263 | 264 | pad = " " * max(0, longest_vis - vis) 265 | longest_vis = max(longest_vis, vis) 266 | 267 | sys.stderr.write(line + pad) 268 | sys.stderr.flush() 269 | 270 | if phase == "complete": 271 | done.set() 272 | 273 | def watchdog() -> None: 274 | time.sleep(SPINNER_DELAY) 275 | with lock: 276 | snap = None if bar_visible else last_snapshot 277 | if snap is not None: 278 | render(*snap) 279 | 280 | pm.on("install-progress", render) 281 | threading.Thread(target=watchdog, daemon=True).start() 282 | 283 | try: 284 | result = pm.install( 285 | project_root=self._opts.project_root, 286 | role=self._opts.role, 287 | specs=self._opts.specs, 288 | omits=normalized_omits, 289 | ) 290 | finally: 291 | pm.off("install-progress", render) 292 | done.wait(0.05) 293 | if bar_visible: 294 | sys.stderr.write("\r" + " " * 80 + "\r") 295 | sys.stderr.flush() 296 | 297 | if self._opts.quiet: 298 | return 299 | 300 | if result.packages: 301 | for pkg in result.packages: 302 | print(f"✓ {pkg.name}@{pkg.version}") 303 | n = len(result.packages) 304 | package_or_packages = plural(n, "package") 305 | print(f"\n{n} {package_or_packages} installed into {os.path.abspath(self._opts.project_root)}") 306 | else: 307 | print("✔ up to date") 308 | 309 | def _normalize_omits(self, omits: Optional[List[str]]) -> Optional[List[str]]: 310 | if not omits: 311 | return omits 312 | normalized = [] 313 | for omit in omits: 314 | if omit == "dev": 315 | normalized.append("development") 316 | else: 317 | normalized.append(omit) 318 | return normalized 319 | 320 | 321 | def plural(n: int, word: str) -> str: 322 | return word if n == 1 else f"{word}s" 323 | 324 | 325 | def start_spinner(theme_hex: str) -> tuple[threading.Event, threading.Thread]: 326 | stop = threading.Event() 327 | frames = itertools.cycle("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏") 328 | colour = ansi_fg(theme_hex) 329 | reset = Style.RESET_ALL 330 | 331 | def run() -> None: 332 | while not stop.is_set(): 333 | frame = next(frames) 334 | sys.stdout.write(f"\r{colour}{frame}{reset} Searching…") 335 | sys.stdout.flush() 336 | time.sleep(0.1) 337 | sys.stdout.write("\r" + " " * 40 + "\r") 338 | sys.stdout.flush() 339 | 340 | t = threading.Thread(target=run, daemon=True) 341 | t.start() 342 | return stop, t 343 | 344 | 345 | def esc(text: str) -> str: 346 | return html.escape(text, quote=False) 347 | 348 | 349 | def ansi_fg(hex_color: str) -> str: 350 | r, g, b = (int(hex_color[i : i + 2], 16) for i in (1, 3, 5)) 351 | return f"\x1b[38;2;{r};{g};{b}m" 352 | 353 | 354 | if __name__ == "__main__": 355 | try: 356 | main() 357 | except KeyboardInterrupt: 358 | pass 359 | -------------------------------------------------------------------------------- /frida_tools/ps.py: -------------------------------------------------------------------------------- 1 | def main() -> None: 2 | import argparse 3 | import functools 4 | import json 5 | import math 6 | import platform 7 | import sys 8 | from base64 import b64encode 9 | from typing import List, Tuple, Union 10 | 11 | try: 12 | import termios 13 | import tty 14 | except: 15 | pass 16 | 17 | import frida._frida as _frida 18 | 19 | from frida_tools.application import ConsoleApplication 20 | 21 | class PSApplication(ConsoleApplication): 22 | def _add_options(self, parser: argparse.ArgumentParser) -> None: 23 | parser.add_argument( 24 | "-a", 25 | "--applications", 26 | help="list only applications", 27 | action="store_true", 28 | dest="list_only_applications", 29 | default=False, 30 | ) 31 | parser.add_argument( 32 | "-i", 33 | "--installed", 34 | help="include all installed applications", 35 | action="store_true", 36 | dest="include_all_applications", 37 | default=False, 38 | ) 39 | parser.add_argument( 40 | "-j", 41 | "--json", 42 | help="output results as JSON", 43 | action="store_const", 44 | dest="output_format", 45 | const="json", 46 | default="text", 47 | ) 48 | parser.add_argument( 49 | "-e", 50 | "--exclude-icons", 51 | help="exclude icons in output", 52 | action="store_true", 53 | dest="exclude_icons", 54 | ) 55 | 56 | def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None: 57 | if options.include_all_applications and not options.list_only_applications: 58 | parser.error("-i cannot be used without -a") 59 | self._list_only_applications = options.list_only_applications 60 | self._include_all_applications = options.include_all_applications 61 | self._output_format = options.output_format 62 | self._exclude_icons = options.exclude_icons 63 | self._terminal_type, self._icon_size = self._detect_terminal() 64 | 65 | def _usage(self) -> str: 66 | return "%(prog)s [options]" 67 | 68 | def _start(self) -> None: 69 | if self._list_only_applications: 70 | self._list_applications() 71 | else: 72 | self._list_processes() 73 | 74 | def _list_processes(self) -> None: 75 | if not self._exclude_icons and self._output_format == "text" and self._terminal_type == "iterm2": 76 | scope = "full" 77 | else: 78 | scope = "minimal" 79 | 80 | try: 81 | assert self._device is not None 82 | processes = self._device.enumerate_processes(scope=scope) 83 | except Exception as e: 84 | self._update_status(f"Failed to enumerate processes: {e}") 85 | self._exit(1) 86 | return 87 | 88 | if self._output_format == "text": 89 | if len(processes) > 0: 90 | pid_column_width = max(map(lambda p: len(str(p.pid)), processes)) 91 | icon_width = max(map(compute_icon_width, processes)) 92 | name_column_width = icon_width + max(map(lambda p: len(p.name), processes)) 93 | 94 | header_format = "%" + str(pid_column_width) + "s %s" 95 | self._print(header_format % ("PID", "Name")) 96 | self._print(f"{pid_column_width * '-'} {name_column_width * '-'}") 97 | 98 | line_format = "%" + str(pid_column_width) + "d %s" 99 | name_format = "%-" + str(name_column_width - icon_width) + "s" 100 | 101 | for process in sorted(processes, key=functools.cmp_to_key(compare_processes)): 102 | if icon_width != 0: 103 | icons = process.parameters.get("icons", None) 104 | if icons is not None: 105 | icon = self._render_icon(icons[0]) 106 | else: 107 | icon = " " 108 | name = icon + " " + name_format % process.name 109 | else: 110 | name = name_format % process.name 111 | 112 | self._print(line_format % (process.pid, name)) 113 | else: 114 | self._log("error", "No running processes.") 115 | elif self._output_format == "json": 116 | result = [] 117 | for process in sorted(processes, key=functools.cmp_to_key(compare_processes)): 118 | result.append({"pid": process.pid, "name": process.name}) 119 | self._print(json.dumps(result, sort_keys=False, indent=2)) 120 | 121 | self._exit(0) 122 | 123 | def _list_applications(self) -> None: 124 | if not self._exclude_icons and self._output_format == "text" and self._terminal_type == "iterm2": 125 | scope = "full" 126 | else: 127 | scope = "minimal" 128 | 129 | try: 130 | assert self._device is not None 131 | applications = self._device.enumerate_applications(scope=scope) 132 | except Exception as e: 133 | self._update_status(f"Failed to enumerate applications: {e}") 134 | self._exit(1) 135 | return 136 | 137 | if not self._include_all_applications: 138 | applications = list(filter(lambda app: app.pid != 0, applications)) 139 | 140 | if self._output_format == "text": 141 | if len(applications) > 0: 142 | pid_column_width = max(map(lambda app: len(str(app.pid)), applications)) 143 | icon_width = max(map(compute_icon_width, applications)) 144 | name_column_width = icon_width + max(map(lambda app: len(app.name), applications)) 145 | identifier_column_width = max(map(lambda app: len(app.identifier), applications)) 146 | 147 | header_format = ( 148 | "%" 149 | + str(pid_column_width) 150 | + "s " 151 | + "%-" 152 | + str(name_column_width) 153 | + "s " 154 | + "%-" 155 | + str(identifier_column_width) 156 | + "s" 157 | ) 158 | self._print(header_format % ("PID", "Name", "Identifier")) 159 | self._print(f"{pid_column_width * '-'} {name_column_width * '-'} {identifier_column_width * '-'}") 160 | 161 | line_format = "%" + str(pid_column_width) + "s %s %-" + str(identifier_column_width) + "s" 162 | name_format = "%-" + str(name_column_width - icon_width) + "s" 163 | 164 | for app in sorted(applications, key=functools.cmp_to_key(compare_applications)): 165 | if icon_width != 0: 166 | icons = app.parameters.get("icons", None) 167 | if icons is not None: 168 | icon = self._render_icon(icons[0]) 169 | else: 170 | icon = " " 171 | name = icon + " " + name_format % app.name 172 | else: 173 | name = name_format % app.name 174 | 175 | if app.pid == 0: 176 | self._print(line_format % ("-", name, app.identifier)) 177 | else: 178 | self._print(line_format % (app.pid, name, app.identifier)) 179 | 180 | elif self._include_all_applications: 181 | self._log("error", "No installed applications.") 182 | else: 183 | self._log("error", "No running applications.") 184 | elif self._output_format == "json": 185 | result = [] 186 | if len(applications) > 0: 187 | for app in sorted(applications, key=functools.cmp_to_key(compare_applications)): 188 | result.append({"pid": (app.pid or None), "name": app.name, "identifier": app.identifier}) 189 | self._print(json.dumps(result, sort_keys=False, indent=2)) 190 | 191 | self._exit(0) 192 | 193 | def _render_icon(self, icon) -> str: 194 | return "\033]1337;File=inline=1;width={}px;height={}px;:{}\007".format( 195 | self._icon_size, self._icon_size, b64encode(icon["image"]).decode("ascii") 196 | ) 197 | 198 | def _detect_terminal(self) -> Tuple[str, int]: 199 | icon_size = 0 200 | 201 | if not self._have_terminal or self._plain_terminal or platform.system() != "Darwin": 202 | return ("simple", icon_size) 203 | 204 | fd = sys.stdin.fileno() 205 | old_attributes = termios.tcgetattr(fd) 206 | try: 207 | tty.setraw(fd) 208 | new_attributes = termios.tcgetattr(fd) 209 | new_attributes[3] = new_attributes[3] & ~termios.ICANON & ~termios.ECHO 210 | termios.tcsetattr(fd, termios.TCSANOW, new_attributes) 211 | 212 | sys.stdout.write("\033[1337n") 213 | sys.stdout.write("\033[5n") 214 | sys.stdout.flush() 215 | 216 | response = self._read_terminal_response("n") 217 | if response not in ("0", "3"): 218 | self._read_terminal_response("n") 219 | 220 | if response.startswith("ITERM2 "): 221 | version_tokens = response.split(" ", 1)[1].split(".", 2) 222 | if len(version_tokens) >= 2 and int(version_tokens[0]) >= 3: 223 | sys.stdout.write("\033[14t") 224 | sys.stdout.flush() 225 | height_in_pixels = int(self._read_terminal_response("t").split(";")[1]) 226 | 227 | sys.stdout.write("\033[18t") 228 | sys.stdout.flush() 229 | height_in_cells = int(self._read_terminal_response("t").split(";")[1]) 230 | 231 | icon_size = math.ceil((height_in_pixels / height_in_cells) * 1.77) 232 | 233 | return ("iterm2", icon_size) 234 | 235 | return ("simple", icon_size) 236 | finally: 237 | termios.tcsetattr(fd, termios.TCSANOW, old_attributes) 238 | 239 | def _read_terminal_response(self, terminator: str) -> str: 240 | sys.stdin.read(1) 241 | sys.stdin.read(1) 242 | result = "" 243 | while True: 244 | ch = sys.stdin.read(1) 245 | if ch == terminator: 246 | break 247 | result += ch 248 | return result 249 | 250 | def compare_applications(a: _frida.Application, b: _frida.Application) -> int: 251 | a_is_running = a.pid != 0 252 | b_is_running = b.pid != 0 253 | if a_is_running == b_is_running: 254 | if a.name > b.name: 255 | return 1 256 | elif a.name < b.name: 257 | return -1 258 | else: 259 | return 0 260 | elif a_is_running: 261 | return -1 262 | else: 263 | return 1 264 | 265 | def compare_processes(a: _frida.Process, b: _frida.Process) -> int: 266 | a_has_icon = "icons" in a.parameters 267 | b_has_icon = "icons" in b.parameters 268 | if a_has_icon == b_has_icon: 269 | if a.name > b.name: 270 | return 1 271 | elif a.name < b.name: 272 | return -1 273 | else: 274 | return 0 275 | elif a_has_icon: 276 | return -1 277 | else: 278 | return 1 279 | 280 | def compute_icon_width(item: Union[_frida.Application, _frida.Process]) -> int: 281 | for icon in item.parameters.get("icons", []): 282 | if icon["format"] == "png": 283 | return 4 284 | return 0 285 | 286 | app = PSApplication() 287 | app.run() 288 | 289 | 290 | if __name__ == "__main__": 291 | try: 292 | main() 293 | except KeyboardInterrupt: 294 | pass 295 | -------------------------------------------------------------------------------- /apps/tracer/src/EventView.tsx: -------------------------------------------------------------------------------- 1 | import "./EventView.css"; 2 | import { DisassemblyTarget, Event, HandlerId } from "./model.js"; 3 | import { Button, Card } from "@blueprintjs/core"; 4 | import Ansi from "@curvenote/ansi-to-react"; 5 | import prettyMilliseconds from "pretty-ms"; 6 | import { ReactElement, useCallback, useEffect, useRef, useState } from "react"; 7 | import AutoSizer from "react-virtualized-auto-sizer"; 8 | import { VariableSizeList } from "react-window"; 9 | 10 | export interface EventViewProps { 11 | events: Event[]; 12 | selectedIndex: number | null; 13 | onActivate: EventActionHandler; 14 | onDeactivate: EventActionHandler; 15 | onDisassemble: DisassembleHandler; 16 | onSymbolicate: SymbolicateHandler; 17 | } 18 | 19 | export type EventActionHandler = (handlerId: HandlerId, eventIndex: number) => void; 20 | export type DisassembleHandler = (target: DisassemblyTarget) => void; 21 | export type SymbolicateHandler = (addresses: bigint[]) => Promise; 22 | 23 | const NON_BLOCKING_SPACE = "\u00A0"; 24 | const INDENT = NON_BLOCKING_SPACE.repeat(3) + "|" + NON_BLOCKING_SPACE; 25 | 26 | export default function EventView({ 27 | events, 28 | selectedIndex = null, 29 | onActivate, 30 | onDeactivate, 31 | onDisassemble, 32 | onSymbolicate, 33 | }: EventViewProps) { 34 | const listRef = useRef(null); 35 | const listOuterRef = useRef(null); 36 | const [items, setItems] = useState([]); 37 | const [selectedCallerSymbol, setSelectedCallerSymbol] = useState(""); 38 | const [selectedBacktraceSymbols, setSelectedBacktraceSymbols] = useState(null); 39 | const autoscroll = useRef({ enabled: true, pending: [] }); 40 | 41 | useEffect(() => { 42 | let lastTimestamp: number | null = null; 43 | let lastTid: number | null = null; 44 | const newItems = events.reduce((result, event, i) => { 45 | const ev = event.slice() as Event; 46 | const [_targetId, timestamp, threadId, _depth, _caller, _backtrace, _message, style] = ev; 47 | 48 | if (lastTimestamp !== null) { 49 | ev[1] = timestamp - lastTimestamp; 50 | } else { 51 | ev[1] = 0; 52 | } 53 | lastTimestamp = timestamp; 54 | 55 | if (threadId !== lastTid) { 56 | result.push([i, threadId, style]); 57 | lastTid = threadId; 58 | } 59 | 60 | result.push([i, ev]); 61 | return result; 62 | }, [[] as SpacerItem] as Item[]); 63 | setItems(newItems); 64 | 65 | let ignore = false; 66 | 67 | if (autoscroll.current.enabled) { 68 | autoscroll.current.pending.push(() => { 69 | if (!ignore) { 70 | listRef.current?.scrollToItem(newItems.length - 1, "end"); 71 | } 72 | }); 73 | } 74 | 75 | return () => { 76 | ignore = true; 77 | }; 78 | }, [events]); 79 | 80 | const itemSize = useCallback((i: number) => { 81 | const item = items[i]; 82 | 83 | if (item.length === 0) { 84 | return 5; 85 | } 86 | 87 | if (item.length === 3) { 88 | return 15.43; 89 | } 90 | 91 | const [eventIndex, event] = item; 92 | const [_targetId, _timestamp, _threadId, _depth, _caller, backtrace, message, _style] = event; 93 | const numLines = message.split("\n").length; 94 | let size = 30; 95 | if (numLines > 1) { 96 | size += (numLines - 1) * 14; 97 | size -= 7; 98 | } 99 | if (eventIndex === selectedIndex) { 100 | size += 150; 101 | if (backtrace !== null) { 102 | size += (backtrace.length - 1) * 34; 103 | } 104 | } 105 | return size; 106 | }, [items, selectedIndex]); 107 | 108 | useEffect(() => { 109 | setSelectedCallerSymbol(null); 110 | setSelectedBacktraceSymbols(null); 111 | 112 | const list = listRef.current; 113 | if (list !== null) { 114 | list.resetAfterIndex(0, true); 115 | if (selectedIndex !== null) { 116 | const itemIndex = items.findIndex(([i,]) => i === selectedIndex); 117 | list.scrollToItem(itemIndex); 118 | } 119 | } 120 | }, [selectedIndex]); 121 | 122 | useEffect(() => { 123 | if (selectedIndex === null) { 124 | return; 125 | } 126 | 127 | const [_targetId, _timestamp, _threadId, _depth, caller, backtrace, _message, _style] = events[selectedIndex]; 128 | let ignore = false; 129 | 130 | async function symbolicate() { 131 | if (caller !== null && backtrace === null) { 132 | const [symbol] = await onSymbolicate([BigInt(caller)]); 133 | if (!ignore) { 134 | setSelectedCallerSymbol(symbol); 135 | } 136 | } 137 | 138 | if (backtrace !== null) { 139 | const symbols = await onSymbolicate(backtrace.map(BigInt)); 140 | if (!ignore) { 141 | setSelectedBacktraceSymbols(symbols); 142 | } 143 | } 144 | } 145 | 146 | symbolicate(); 147 | 148 | return () => { 149 | ignore = true; 150 | }; 151 | }, [events, selectedIndex, onSymbolicate]); 152 | 153 | const handleDisassemblyRequest = useCallback((rawAddress: string) => { 154 | onDisassemble({ type: "instruction", address: BigInt(rawAddress) }); 155 | }, [onDisassemble]); 156 | 157 | return ( 158 | 159 | {({ width, height }) => ( 160 | 161 | ref={listRef} 162 | outerRef={listOuterRef} 163 | className="event-list" 164 | width={width} 165 | height={height} 166 | itemCount={items.length} 167 | itemSize={itemSize} 168 | itemData={items} 169 | onItemsRendered={() => { 170 | let work: AutoscrollWork | undefined; 171 | while ((work = autoscroll.current.pending.shift()) !== undefined) { 172 | work(); 173 | } 174 | }} 175 | onScroll={props => { 176 | if (!props.scrollUpdateWasRequested) { 177 | const container = listOuterRef.current!; 178 | autoscroll.current.enabled = container.scrollTop >= (container.scrollHeight - container.offsetHeight - 20); 179 | } 180 | }} 181 | > 182 | {({ data, index: itemIndex, style }) => { 183 | const item = data[itemIndex]; 184 | 185 | if (item.length === 0) { 186 | return ( 187 |
188 | ); 189 | } 190 | 191 | if (item.length === 3) { 192 | const [, threadId, textStyle] = item; 193 | const colorClass = "ansi-" + textStyle.join("-"); 194 | return ( 195 |
199 | /* TID 0x{threadId.toString(16)} */ 200 |
201 | ); 202 | } 203 | 204 | const [eventIndex, event] = item; 205 | const [targetId, timestamp, threadId, depth, caller, backtrace, message, textStyle] = event; 206 | 207 | const isSelected = eventIndex === selectedIndex; 208 | let selectedEventDetails: ReactElement | undefined; 209 | if (isSelected) { 210 | selectedEventDetails = ( 211 | 212 | 213 | 214 | 215 | 216 | 217 | 219 | 220 | {(caller !== null && backtrace === null) ? ( 221 | 222 | 223 | 226 | 227 | ) : null 228 | } 229 | {(backtrace !== null) ? ( 230 | 231 | 232 | 238 | 239 | ) : null 240 | } 241 | 242 |
Thread ID0x{threadId.toString(16)} 218 |
Caller 224 | 225 |
Backtrace 233 | {backtrace.map((address, i) => 234 | )} 237 |
243 | 244 |
245 | ); 246 | } 247 | 248 | const eventClasses = ["event-item"]; 249 | if (isSelected) { 250 | eventClasses.push("event-selected"); 251 | } 252 | 253 | const timestampStr = (timestamp !== 0) ? `+${prettyMilliseconds(timestamp)}` : ""; 254 | 255 | const colorClass = "ansi-" + textStyle.join("-"); 256 | 257 | return ( 258 |
262 |
263 | {timestampStr} 264 | {INDENT.repeat(depth)} 265 | 273 |
274 | {isSelected ? selectedEventDetails : null} 275 |
276 | ); 277 | }} 278 | 279 | )} 280 | 281 | ); 282 | } 283 | 284 | type Item = EventItem | ThreadIdMarkerItem | SpacerItem; 285 | type EventItem = [index: number, event: Event]; 286 | type ThreadIdMarkerItem = [index: number, threadId: number, style: string[]]; 287 | type SpacerItem = []; 288 | 289 | interface AutoscrollState { 290 | enabled: boolean; 291 | pending: AutoscrollWork[]; 292 | } 293 | 294 | type AutoscrollWork = () => void; 295 | --------------------------------------------------------------------------------