├── 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 |
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 |
77 | );
78 | })}
79 |
80 |
81 | ) : null;
82 |
83 | return (
84 |
100 | );
101 | }
102 |
103 | function labelForTraceSpecScope(scope: TraceSpecScope) {
104 | switch (scope) {
105 | case TraceSpecScope.Function:
106 | return "Function";
107 | case TraceSpecScope.RelativeFunction:
108 | return "Relative Function";
109 | case TraceSpecScope.AbsoluteInstruction:
110 | return "Instruction";
111 | case TraceSpecScope.Imports:
112 | return "All Module Imports";
113 | case TraceSpecScope.Module:
114 | return "All Module Exports";
115 | case TraceSpecScope.ObjcMethod:
116 | return "Objective-C Method";
117 | case TraceSpecScope.SwiftFunc:
118 | return "Swift Function";
119 | case TraceSpecScope.JavaMethod:
120 | return "Java Method";
121 | case TraceSpecScope.DebugSymbol:
122 | return "Debug Symbol";
123 | }
124 | }
125 |
126 | function placeholderForTraceTargetSpecScope(scope: TraceSpecScope) {
127 | switch (scope) {
128 | case TraceSpecScope.Function:
129 | return "[Module!]Function";
130 | case TraceSpecScope.RelativeFunction:
131 | return "Module!Offset";
132 | case TraceSpecScope.AbsoluteInstruction:
133 | return "0x1234";
134 | case TraceSpecScope.Imports:
135 | case TraceSpecScope.Module:
136 | return "Module";
137 | case TraceSpecScope.ObjcMethod:
138 | return "-[*Auth foo:bar:], +[Foo foo*], or *[Bar baz]";
139 | case TraceSpecScope.SwiftFunc:
140 | return "*SomeModule*!SomeClassPrefix*.*secret*()";
141 | case TraceSpecScope.JavaMethod:
142 | return "Class!Method";
143 | case TraceSpecScope.DebugSymbol:
144 | return "Function";
145 | }
146 | }
--------------------------------------------------------------------------------
/apps/tracer/src/HandlerEditor.tsx:
--------------------------------------------------------------------------------
1 | import { HandlerId } from "./model.js";
2 | import Editor from "@monaco-editor/react";
3 | import type monaco from "monaco-editor";
4 | import { useEffect, useState } from "react";
5 |
6 | export interface HandlerEditorProps {
7 | handlerId: HandlerId | null;
8 | handlerCode: string;
9 | onChange: CodeEventHandler;
10 | onSave: CodeEventHandler;
11 | }
12 |
13 | export type CodeEventHandler = (code: string) => void;
14 |
15 | const USE_META_KEY = navigator.platform.indexOf("Mac") === 0 || navigator.platform === "iPhone";
16 |
17 | export default function HandlerEditor({ handlerId, handlerCode, onChange, onSave }: HandlerEditorProps) {
18 | const [editor, setEditor] = useState(null);
19 | const [monaco, setMonaco] = useState(null);
20 |
21 | const editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = {
22 | automaticLayout: true,
23 | readOnly: handlerId === null,
24 | readOnlyMessage: { value: "Cannot edit without a handler selected" },
25 | };
26 |
27 | function handleEditorDidMount(editor: monaco.editor.IStandaloneCodeEditor, monaco: any) {
28 | setEditor(editor);
29 | setMonaco(monaco);
30 | }
31 |
32 | useEffect(() => {
33 | if (monaco === null) {
34 | return;
35 | }
36 |
37 | const callback = editor!.onKeyDown(e => {
38 | if ((USE_META_KEY ? e.metaKey : e.ctrlKey) && e.keyCode === monaco.KeyCode.KeyS) {
39 | onSave(editor!.getValue());
40 | e.preventDefault();
41 | }
42 | });
43 |
44 | return () => {
45 | callback.dispose();
46 | };
47 | }, [onSave, editor, monaco]);
48 |
49 | return (
50 | onChange(editor!.getValue())}
61 | />
62 | );
63 | }
64 |
65 | async function handleEditorWillMount(monaco: any) {
66 | const typingsResponse = await fetch("https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/master/types/frida-gum/index.d.ts");
67 | const typingsContent = await typingsResponse.text();
68 | monaco.languages.typescript.typescriptDefaults.addExtraLib(typingsContent + `
69 | declare function defineHandler(handler: TraceHandler): TraceHandler;
70 |
71 | type TraceHandler = FunctionTraceHandler | InstructionTraceHandler;
72 |
73 | interface FunctionTraceHandler {
74 | /**
75 | * Called synchronously when about to call the traced function.
76 | *
77 | * @this {InvocationContext} - Object with useful properties, where you may also add properties
78 | * of your own for use in onLeave.
79 | * @param {function} log - Call this function with a string to be presented to the user.
80 | * @param {array} args - Function arguments represented as an array of NativePointer objects.
81 | * For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8.
82 | * It is also possible to modify arguments by assigning a NativePointer object to an element of this array.
83 | * @param {object} state - Object allowing you to keep state across handlers.
84 | * Only one JavaScript function will execute at a time, so do not worry about race-conditions.
85 | * However, do not use this to store function arguments across onEnter/onLeave, but instead
86 | * use "this" which is an object for keeping state local to an invocation.
87 | */
88 | onEnter?(this: InvocationContext, log: TraceLogFunction, args: InvocationArguments, state: TraceScriptState): void;
89 |
90 | /**
91 | * Called synchronously when about to return from the traced function.
92 | *
93 | * See onEnter for details.
94 | *
95 | * @this {InvocationContext} - Object with useful properties, including any extra properties
96 | * added by your onEnter code.
97 | * @param {function} log - Call this function with a string to be presented to the user.
98 | * @param {NativePointer} retval - Return value represented as a NativePointer object.
99 | * @param {object} state - Object allowing you to keep state across handlers.
100 | */
101 | onLeave?(this: InvocationContext, log: TraceLogFunction, retval: InvocationReturnValue, state: TraceScriptState): void;
102 | }
103 |
104 | /**
105 | * Called synchronously when about to execute the traced instruction.
106 | *
107 | * @this {InvocationContext} - Object with useful properties.
108 | * @param {function} log - Call this function with a string to be presented to the user.
109 | * @param {array} args - When the traced instruction is the first instruction of a function,
110 | * use this parameter to access its arguments represented as an array of NativePointer objects.
111 | * For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8.
112 | * It is also possible to modify arguments by assigning a NativePointer object to an element of this array.
113 | * @param {object} state - Object allowing you to keep state across handlers.
114 | * Only one JavaScript function will execute at a time, so do not worry about race-conditions.
115 | */
116 | type InstructionTraceHandler = (this: InvocationContext, log: TraceLogFunction, args: InvocationArguments, state: TraceScriptState) => void;
117 |
118 | type TraceLogFunction = (...args: any[]) => void;
119 |
120 | interface TraceScriptState {
121 | [x: string]: any;
122 | }
123 | `, "");
124 | }
125 |
--------------------------------------------------------------------------------
/frida_tools/lsd.py:
--------------------------------------------------------------------------------
1 | def main() -> None:
2 | import functools
3 | import threading
4 |
5 | import frida
6 | from prompt_toolkit.application import Application
7 | from prompt_toolkit.key_binding import KeyBindings
8 | from prompt_toolkit.layout.containers import HSplit, VSplit
9 | from prompt_toolkit.layout.layout import Layout
10 | from prompt_toolkit.widgets import Label
11 |
12 | from frida_tools.application import ConsoleApplication
13 | from frida_tools.reactor import Reactor
14 |
15 | class LSDApplication(ConsoleApplication):
16 | def __init__(self) -> None:
17 | super().__init__(self._process_input, self._on_stop)
18 | self._ui_app = None
19 | self._pending_labels = set()
20 | self._spinner_frames = ["v", "<", "^", ">"]
21 | self._spinner_offset = 0
22 | self._lock = threading.Lock()
23 |
24 | def _usage(self) -> str:
25 | return "%(prog)s [options]"
26 |
27 | def _needs_device(self) -> bool:
28 | return False
29 |
30 | def _process_input(self, reactor: Reactor) -> None:
31 | try:
32 | devices = frida.enumerate_devices()
33 | except Exception as e:
34 | self._update_status(f"Failed to enumerate devices: {e}")
35 | self._exit(1)
36 | return
37 |
38 | bindings = KeyBindings()
39 |
40 | @bindings.add("")
41 | def _(event):
42 | self._reactor.io_cancellable.cancel()
43 |
44 | self._ui_app = Application(key_bindings=bindings, full_screen=False)
45 |
46 | id_rows = []
47 | type_rows = []
48 | name_rows = []
49 | os_rows = []
50 | for device in sorted(devices, key=functools.cmp_to_key(compare_devices)):
51 | id_rows.append(Label(device.id, dont_extend_width=True))
52 | type_rows.append(Label(device.type, dont_extend_width=True))
53 | name_rows.append(Label(device.name, dont_extend_width=True))
54 | os_label = Label("", dont_extend_width=True)
55 | os_rows.append(os_label)
56 |
57 | with self._lock:
58 | self._pending_labels.add(os_label)
59 | worker = threading.Thread(target=self._fetch_parameters, args=(device, os_label))
60 | worker.start()
61 |
62 | status_label = Label(" ")
63 | body = HSplit(
64 | [
65 | VSplit(
66 | [
67 | HSplit([Label("Id", dont_extend_width=True), HSplit(id_rows)], padding_char="-", padding=1),
68 | HSplit(
69 | [Label("Type", dont_extend_width=True), HSplit(type_rows)], padding_char="-", padding=1
70 | ),
71 | HSplit(
72 | [Label("Name", dont_extend_width=True), HSplit(name_rows)], padding_char="-", padding=1
73 | ),
74 | HSplit([Label("OS", dont_extend_width=True), HSplit(os_rows)], padding_char="-", padding=1),
75 | ],
76 | padding=2,
77 | ),
78 | status_label,
79 | ]
80 | )
81 |
82 | self._ui_app.layout = Layout(body, focused_element=status_label)
83 |
84 | self._reactor.schedule(self._update_progress)
85 | self._ui_app.run()
86 | self._ui_app._redraw()
87 |
88 | def _on_stop(self):
89 | if self._ui_app is not None:
90 | self._ui_app.exit()
91 |
92 | def _update_progress(self):
93 | with self._lock:
94 | if not self._pending_labels:
95 | self._exit(0)
96 | return
97 |
98 | glyph = self._spinner_frames[self._spinner_offset % len(self._spinner_frames)]
99 | self._spinner_offset += 1
100 | for label in self._pending_labels:
101 | label.text = glyph
102 | self._ui_app.invalidate()
103 |
104 | self._reactor.schedule(self._update_progress, delay=0.1)
105 |
106 | def _fetch_parameters(self, device, os_label):
107 | try:
108 | with self._reactor.io_cancellable:
109 | params = device.query_system_parameters()
110 | os = params["os"]
111 | version = os.get("version")
112 | if version is not None:
113 | text = os["name"] + " " + version
114 | else:
115 | text = os["name"]
116 | except:
117 | text = ""
118 |
119 | with self._lock:
120 | os_label.text = text
121 | self._pending_labels.remove(os_label)
122 |
123 | self._ui_app.invalidate()
124 |
125 | def compare_devices(a: frida.core.Device, b: frida.core.Device) -> int:
126 | a_score = score(a)
127 | b_score = score(b)
128 | if a_score == b_score:
129 | if a.name is None or b.name is None:
130 | return 0
131 | if a.name > b.name:
132 | return 1
133 | elif a.name < b.name:
134 | return -1
135 | else:
136 | return 0
137 | else:
138 | if a_score > b_score:
139 | return -1
140 | elif a_score < b_score:
141 | return 1
142 | else:
143 | return 0
144 |
145 | def score(device: frida.core.Device) -> int:
146 | type = device.type
147 | if type == "local":
148 | return 3
149 | elif type == "usb":
150 | return 2
151 | else:
152 | return 1
153 |
154 | app = LSDApplication()
155 | app.run()
156 |
157 |
158 | if __name__ == "__main__":
159 | try:
160 | main()
161 | except KeyboardInterrupt:
162 | pass
163 |
--------------------------------------------------------------------------------
/frida_tools/compiler.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import os
3 | import sys
4 | from timeit import default_timer as timer
5 | from typing import Any, Dict, List, Optional
6 |
7 | import frida
8 |
9 | from frida_tools.application import ConsoleApplication, await_ctrl_c
10 | from frida_tools.cli_formatting import format_compiled, format_compiling, format_diagnostic, format_error
11 |
12 |
13 | def main() -> None:
14 | app = CompilerApplication()
15 | app.run()
16 |
17 |
18 | class CompilerApplication(ConsoleApplication):
19 | def __init__(self) -> None:
20 | super().__init__(await_ctrl_c)
21 |
22 | def _usage(self) -> str:
23 | return "%(prog)s [options] "
24 |
25 | def _add_options(self, parser: argparse.ArgumentParser) -> None:
26 | parser.add_argument("module", help="TypeScript/JavaScript module to compile")
27 | parser.add_argument("-o", "--output", help="write output to ")
28 | parser.add_argument("-w", "--watch", help="watch for changes and recompile", action="store_true")
29 | parser.add_argument("-S", "--no-source-maps", help="omit source-maps", action="store_true")
30 | parser.add_argument("-c", "--compress", help="minify code", action="store_true")
31 | parser.add_argument("-v", "--verbose", help="be verbose", action="store_true")
32 | parser.add_argument(
33 | "-F",
34 | "--output-format",
35 | help="desired output format",
36 | choices=["unescaped", "hex-bytes", "c-string"],
37 | default="unescaped",
38 | )
39 | parser.add_argument(
40 | "-B", "--bundle-format", help="desired bundle format", choices=["esm", "iife"], default="esm"
41 | )
42 | parser.add_argument(
43 | "-T", "--type-check", help="desired type-checking mode", choices=["full", "none"], default="full"
44 | )
45 | parser.add_argument(
46 | "-P",
47 | "--platform",
48 | help="JavaScript runtime platform",
49 | choices=["gum", "browser", "neutral"],
50 | default="gum",
51 | )
52 | parser.add_argument(
53 | "-E",
54 | "--external",
55 | metavar="MODULE",
56 | action="append",
57 | default=[],
58 | help="mark MODULE as external (may be specified multiple times)",
59 | )
60 |
61 | def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None:
62 | self._module = os.path.abspath(options.module)
63 | self._output = options.output
64 | self._mode = "watch" if options.watch else "build"
65 | self._verbose = self._mode == "watch" or options.verbose
66 | self._compiler_options = {
67 | "project_root": os.getcwd(),
68 | "output_format": options.output_format,
69 | "bundle_format": options.bundle_format,
70 | "type_check": options.type_check,
71 | "source_maps": "omitted" if options.no_source_maps else "included",
72 | "compression": "terser" if options.compress else "none",
73 | "platform": options.platform,
74 | "externals": options.external,
75 | }
76 |
77 | compiler = frida.Compiler()
78 | self._compiler = compiler
79 |
80 | def on_compiler_finished() -> None:
81 | self._reactor.schedule(lambda: self._on_compiler_finished())
82 |
83 | def on_compiler_output(bundle: str) -> None:
84 | self._reactor.schedule(lambda: self._on_compiler_output(bundle))
85 |
86 | def on_compiler_diagnostics(diagnostics: List[Dict[str, Any]]) -> None:
87 | self._reactor.schedule(lambda: self._on_compiler_diagnostics(diagnostics))
88 |
89 | compiler.on("starting", self._on_compiler_starting)
90 | compiler.on("finished", on_compiler_finished)
91 | compiler.on("output", on_compiler_output)
92 | compiler.on("diagnostics", on_compiler_diagnostics)
93 |
94 | self._compilation_started: Optional[float] = None
95 |
96 | def _needs_device(self) -> bool:
97 | return False
98 |
99 | def _start(self) -> None:
100 | try:
101 | if self._mode == "build":
102 | self._compiler.build(self._module, **self._compiler_options)
103 | self._exit(0)
104 | else:
105 | self._compiler.watch(self._module, **self._compiler_options)
106 | except Exception as e:
107 | error = e
108 | self._reactor.schedule(lambda: self._on_fatal_error(error))
109 |
110 | def _on_fatal_error(self, error: Exception) -> None:
111 | self._print(format_error(error))
112 | self._exit(1)
113 |
114 | def _on_compiler_starting(self) -> None:
115 | self._compilation_started = timer()
116 | if self._verbose:
117 | self._reactor.schedule(lambda: self._print_compiler_starting())
118 |
119 | def _print_compiler_starting(self) -> None:
120 | if self._mode == "watch":
121 | sys.stdout.write("\x1bc")
122 | self._print(format_compiling(self._module, os.getcwd()))
123 |
124 | def _on_compiler_finished(self) -> None:
125 | if self._verbose:
126 | time_finished = timer()
127 | assert self._compilation_started is not None
128 | self._print(format_compiled(self._module, os.getcwd(), self._compilation_started, time_finished))
129 |
130 | def _on_compiler_output(self, bundle: str) -> None:
131 | if self._output is not None:
132 | try:
133 | with open(self._output, "w", encoding="utf-8", newline="\n") as f:
134 | f.write(bundle)
135 | except Exception as e:
136 | self._on_fatal_error(e)
137 | else:
138 | sys.stdout.write(bundle)
139 |
140 | def _on_compiler_diagnostics(self, diagnostics: List[Dict[str, Any]]) -> None:
141 | cwd = os.getcwd()
142 | for diag in diagnostics:
143 | self._print(format_diagnostic(diag, cwd))
144 |
145 |
146 | if __name__ == "__main__":
147 | try:
148 | main()
149 | except KeyboardInterrupt:
150 | pass
151 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import sys
4 | from pathlib import Path
5 | from typing import Iterator, List
6 |
7 | from setuptools import setup
8 |
9 | SOURCE_ROOT = Path(__file__).resolve().parent
10 |
11 | pkg_info = SOURCE_ROOT / "PKG-INFO"
12 | in_source_package = pkg_info.exists()
13 |
14 |
15 | def main():
16 | setup(
17 | name="frida-tools",
18 | version=detect_version(),
19 | description="Frida CLI tools",
20 | long_description="CLI tools for [Frida](https://frida.re).",
21 | long_description_content_type="text/markdown",
22 | author="Frida Developers",
23 | author_email="oleavr@frida.re",
24 | url="https://frida.re",
25 | install_requires=[
26 | "colorama >= 0.2.7, < 1.0.0",
27 | "frida >= 17.5.0, < 18.0.0",
28 | "prompt-toolkit >= 2.0.0, < 4.0.0",
29 | "pygments >= 2.0.2, < 3.0.0",
30 | "websockets >= 13.0.0, < 14.0.0",
31 | ],
32 | license="wxWindows Library Licence, Version 3.1",
33 | zip_safe=False,
34 | keywords="frida debugger dynamic instrumentation inject javascript windows macos linux ios iphone ipad android qnx",
35 | classifiers=[
36 | "Development Status :: 5 - Production/Stable",
37 | "Environment :: Console",
38 | "Environment :: MacOS X",
39 | "Environment :: Win32 (MS Windows)",
40 | "Intended Audience :: Developers",
41 | "Intended Audience :: Science/Research",
42 | "License :: OSI Approved",
43 | "Natural Language :: English",
44 | "Operating System :: MacOS :: MacOS X",
45 | "Operating System :: Microsoft :: Windows",
46 | "Operating System :: POSIX :: Linux",
47 | "Programming Language :: Python :: 3",
48 | "Programming Language :: Python :: 3.7",
49 | "Programming Language :: Python :: 3.8",
50 | "Programming Language :: Python :: 3.9",
51 | "Programming Language :: Python :: 3.10",
52 | "Programming Language :: JavaScript",
53 | "Topic :: Software Development :: Debuggers",
54 | "Topic :: Software Development :: Libraries :: Python Modules",
55 | ],
56 | packages=["frida_tools"],
57 | package_data={
58 | "frida_tools": fetch_built_assets(),
59 | },
60 | entry_points={
61 | "console_scripts": [
62 | "frida = frida_tools.repl:main",
63 | "frida-ls-devices = frida_tools.lsd:main",
64 | "frida-ps = frida_tools.ps:main",
65 | "frida-kill = frida_tools.kill:main",
66 | "frida-ls = frida_tools.ls:main",
67 | "frida-rm = frida_tools.rm:main",
68 | "frida-pull = frida_tools.pull:main",
69 | "frida-push = frida_tools.push:main",
70 | "frida-discover = frida_tools.discoverer:main",
71 | "frida-trace = frida_tools.tracer:main",
72 | "frida-itrace = frida_tools.itracer:main",
73 | "frida-join = frida_tools.join:main",
74 | "frida-create = frida_tools.creator:main",
75 | "frida-compile = frida_tools.compiler:main",
76 | "frida-pm = frida_tools.pm:main",
77 | "frida-apk = frida_tools.apk:main",
78 | ]
79 | },
80 | )
81 |
82 |
83 | def detect_version() -> str:
84 | if in_source_package:
85 | version_line = [
86 | line for line in pkg_info.read_text(encoding="utf-8").split("\n") if line.startswith("Version: ")
87 | ][0].strip()
88 | version = version_line[9:]
89 | else:
90 | releng_location = next(enumerate_releng_locations(), None)
91 | if releng_location is not None:
92 | sys.path.insert(0, str(releng_location.parent))
93 | from releng.frida_version import detect
94 |
95 | version = detect(SOURCE_ROOT).name.replace("-dev.", ".dev")
96 | else:
97 | version = "0.0.0"
98 | return version
99 |
100 |
101 | def fetch_built_assets() -> List[str]:
102 | assets = []
103 |
104 | if in_source_package:
105 | pkgdir = SOURCE_ROOT / "frida_tools"
106 | assets += [f.name for f in pkgdir.glob("*_agent.js")]
107 | assets += [f.relative_to(pkgdir).as_posix() for f in (pkgdir / "bridges").glob("*.js")]
108 | assets += [f.name for f in pkgdir.glob("*.zip")]
109 | else:
110 | agents_builddir = SOURCE_ROOT / "build" / "agents"
111 | if agents_builddir.exists():
112 | for child in agents_builddir.iterdir():
113 | if child.is_dir():
114 | for f in child.glob("*_agent.js"):
115 | shutil.copy(f, SOURCE_ROOT / "frida_tools")
116 | assets.append(f.name)
117 |
118 | bridges_builddir = SOURCE_ROOT / "build" / "bridges"
119 | if bridges_builddir.exists():
120 | bridges_dir = SOURCE_ROOT / "frida_tools" / "bridges"
121 | bridges_dir.mkdir(exist_ok=True)
122 | for f in bridges_builddir.glob("*.js"):
123 | shutil.copy(f, bridges_dir)
124 | assets.append((Path("bridges") / f.name).as_posix())
125 |
126 | apps_builddir = SOURCE_ROOT / "build" / "apps"
127 | if apps_builddir.exists():
128 | for child in apps_builddir.iterdir():
129 | if child.is_dir():
130 | for f in child.glob("*.zip"):
131 | shutil.copy(f, SOURCE_ROOT / "frida_tools")
132 | assets.append(f.name)
133 |
134 | return assets
135 |
136 |
137 | def enumerate_releng_locations() -> Iterator[Path]:
138 | val = os.environ.get("MESON_SOURCE_ROOT")
139 | if val is not None:
140 | parent_releng = Path(val) / "releng"
141 | if releng_location_exists(parent_releng):
142 | yield parent_releng
143 |
144 | local_releng = SOURCE_ROOT / "releng"
145 | if releng_location_exists(local_releng):
146 | yield local_releng
147 |
148 |
149 | def releng_location_exists(location: Path) -> bool:
150 | return (location / "frida_version.py").exists()
151 |
152 |
153 | if __name__ == "__main__":
154 | main()
155 |
--------------------------------------------------------------------------------
/frida_tools/stream_controller.py:
--------------------------------------------------------------------------------
1 | import threading
2 | from typing import Any, AnyStr, BinaryIO, Callable, Mapping, Optional
3 |
4 |
5 | class StreamController:
6 | def __init__(
7 | self,
8 | post: Callable[[Any, Optional[AnyStr]], None],
9 | on_incoming_stream_request: Optional[Callable[[Any, Any], BinaryIO]] = None,
10 | on_incoming_stream_closed=None,
11 | on_stats_updated=None,
12 | ) -> None:
13 | self.streams_opened = 0
14 | self.bytes_received = 0
15 | self.bytes_sent = 0
16 |
17 | self._handlers = {".create": self._on_create, ".finish": self._on_finish, ".write": self._on_write}
18 |
19 | self._post = post
20 | self._on_incoming_stream_request = on_incoming_stream_request
21 | self._on_incoming_stream_closed = on_incoming_stream_closed
22 | self._on_stats_updated = on_stats_updated
23 |
24 | self._sources = {}
25 | self._next_endpoint_id = 1
26 |
27 | self._requests = {}
28 | self._next_request_id = 1
29 |
30 | def dispose(self) -> None:
31 | error = DisposedException("disposed")
32 | for request in self._requests.values():
33 | request[2] = error
34 | for event in [request[0] for request in self._requests.values()]:
35 | event.set()
36 |
37 | def open(self, label, details={}) -> "Sink":
38 | eid = self._next_endpoint_id
39 | self._next_endpoint_id += 1
40 |
41 | endpoint = {"id": eid, "label": label, "details": details}
42 |
43 | sink = Sink(self, endpoint)
44 |
45 | self.streams_opened += 1
46 | self._notify_stats_updated()
47 |
48 | return sink
49 |
50 | def receive(self, stanza: Mapping[str, Any], data: Any) -> None:
51 | sid = stanza["id"]
52 | name = stanza["name"]
53 | payload = stanza.get("payload", None)
54 |
55 | stype = name[0]
56 | if stype == ".":
57 | self._on_request(sid, name, payload, data)
58 | elif stype == "+":
59 | self._on_notification(sid, name, payload)
60 | else:
61 | raise ValueError("unknown stanza: " + name)
62 |
63 | def _on_create(self, payload: Mapping[str, Any], data: Any) -> None:
64 | endpoint = payload["endpoint"]
65 | eid = endpoint["id"]
66 | label = endpoint["label"]
67 | details = endpoint["details"]
68 |
69 | if self._on_incoming_stream_request is None:
70 | raise ValueError("incoming streams not allowed")
71 | source = self._on_incoming_stream_request(label, details)
72 |
73 | self._sources[eid] = (source, label, details)
74 |
75 | self.streams_opened += 1
76 | self._notify_stats_updated()
77 |
78 | def _on_finish(self, payload: Mapping[str, Any], data: Any) -> None:
79 | eid = payload["endpoint"]["id"]
80 |
81 | entry = self._sources.pop(eid, None)
82 | if entry is None:
83 | raise ValueError("invalid endpoint ID")
84 | source, label, details = entry
85 |
86 | source.close()
87 |
88 | if self._on_incoming_stream_closed is not None:
89 | self._on_incoming_stream_closed(label, details)
90 |
91 | def _on_write(self, payload: Mapping[str, Any], data: Any) -> None:
92 | entry = self._sources.get(payload["endpoint"]["id"], None)
93 | if entry is None:
94 | raise ValueError("invalid endpoint ID")
95 | source, *_ = entry
96 |
97 | source.write(data)
98 |
99 | self.bytes_received += len(data)
100 | self._notify_stats_updated()
101 |
102 | def _request(self, name: str, payload: Mapping[Any, Any], data: Optional[AnyStr] = None):
103 | rid = self._next_request_id
104 | self._next_request_id += 1
105 |
106 | completed = threading.Event()
107 | request = [completed, None, None]
108 | self._requests[rid] = request
109 |
110 | self._post({"id": rid, "name": name, "payload": payload}, data)
111 |
112 | completed.wait()
113 |
114 | error = request[2]
115 | if error is not None:
116 | raise error
117 |
118 | return request[1]
119 |
120 | def _on_request(self, sid, name: str, payload: Mapping[str, Any], data: Any) -> None:
121 | handler = self._handlers.get(name, None)
122 | if handler is None:
123 | raise ValueError("invalid request: " + name)
124 |
125 | try:
126 | result = handler(payload, data)
127 | except Exception as e:
128 | self._reject(sid, e)
129 | return
130 |
131 | self._resolve(sid, result)
132 |
133 | def _resolve(self, sid, value) -> None:
134 | self._post({"id": sid, "name": "+result", "payload": value})
135 |
136 | def _reject(self, sid, error) -> None:
137 | self._post({"id": sid, "name": "+error", "payload": {"message": str(error)}})
138 |
139 | def _on_notification(self, sid, name: str, payload) -> None:
140 | request = self._requests.pop(sid, None)
141 | if request is None:
142 | raise ValueError("invalid request ID")
143 |
144 | if name == "+result":
145 | request[1] = payload
146 | elif name == "+error":
147 | request[2] = StreamException(payload["message"])
148 | else:
149 | raise ValueError("unknown notification: " + name)
150 | completed, *_ = request
151 | completed.set()
152 |
153 | def _notify_stats_updated(self) -> None:
154 | if self._on_stats_updated is not None:
155 | self._on_stats_updated()
156 |
157 |
158 | class Sink:
159 | def __init__(self, controller: StreamController, endpoint) -> None:
160 | self._controller = controller
161 | self._endpoint = endpoint
162 |
163 | controller._request(".create", {"endpoint": endpoint})
164 |
165 | def close(self) -> None:
166 | self._controller._request(".finish", {"endpoint": self._endpoint})
167 |
168 | def write(self, chunk) -> None:
169 | ctrl = self._controller
170 |
171 | ctrl._request(".write", {"endpoint": self._endpoint}, chunk)
172 |
173 | ctrl.bytes_sent += len(chunk)
174 | ctrl._notify_stats_updated()
175 |
176 |
177 | class DisposedException(Exception):
178 | pass
179 |
180 |
181 | class StreamException(Exception):
182 | pass
183 |
--------------------------------------------------------------------------------
/frida_tools/_repl_magic.py:
--------------------------------------------------------------------------------
1 | import abc
2 | import codecs
3 | import json
4 | import os
5 | from typing import TYPE_CHECKING, Optional, Sequence
6 |
7 | if TYPE_CHECKING:
8 | import frida_tools.repl
9 |
10 |
11 | class Magic(abc.ABC):
12 | @property
13 | def description(self) -> str:
14 | return "no description"
15 |
16 | @abc.abstractproperty
17 | def required_args_count(self) -> int:
18 | pass
19 |
20 | def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> Optional[bool]:
21 | pass
22 |
23 |
24 | class Resume(Magic):
25 | @property
26 | def description(self) -> str:
27 | return "resume execution of the spawned process"
28 |
29 | @property
30 | def required_args_count(self) -> int:
31 | return 0
32 |
33 | def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
34 | repl._reactor.schedule(lambda: repl._resume())
35 |
36 |
37 | class Load(Magic):
38 | @property
39 | def description(self) -> str:
40 | return "Load an additional script and reload the current REPL state"
41 |
42 | @property
43 | def required_args_count(self) -> int:
44 | return 1
45 |
46 | def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
47 | try:
48 | proceed = repl._get_confirmation(
49 | "Are you sure you want to load a new script and discard all current state?"
50 | )
51 | if not proceed:
52 | repl._print("Discarding load command")
53 | return
54 |
55 | repl._user_scripts.append(args[0])
56 | repl._perform_on_reactor_thread(lambda: repl._load_script())
57 | except Exception as e:
58 | repl._print(f"Failed to load script: {e}")
59 |
60 |
61 | class Reload(Magic):
62 | @property
63 | def description(self) -> str:
64 | return "reload (i.e. rerun) the script that was given as an argument to the REPL"
65 |
66 | @property
67 | def required_args_count(self) -> int:
68 | return 0
69 |
70 | def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> bool:
71 | try:
72 | repl._perform_on_reactor_thread(lambda: repl._load_script())
73 | return True
74 | except Exception as e:
75 | repl._print(f"Failed to load script: {e}")
76 | return False
77 |
78 |
79 | class Unload(Magic):
80 | @property
81 | def required_args_count(self) -> int:
82 | return 0
83 |
84 | def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
85 | repl._unload_script()
86 |
87 |
88 | class Autoperform(Magic):
89 | @property
90 | def description(self) -> str:
91 | return (
92 | "receive on/off as first and only argument, when switched on will wrap any REPL code with Java.performNow()"
93 | )
94 |
95 | @property
96 | def required_args_count(self) -> int:
97 | return 1
98 |
99 | def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
100 | repl._autoperform_command(args[0])
101 |
102 |
103 | class Autoreload(Magic):
104 | _VALID_ARGUMENTS = ("on", "off")
105 |
106 | @property
107 | def description(self) -> str:
108 | return "disable or enable auto reloading of script files"
109 |
110 | @property
111 | def required_args_count(self) -> int:
112 | return 1
113 |
114 | def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
115 | if args[0] not in self._VALID_ARGUMENTS:
116 | raise ValueError("Autoreload command only receive on or off as an argument")
117 |
118 | required_state = args[0] == "on"
119 | if required_state == repl._autoreload:
120 | repl._print("Autoreloading is already in the desired state")
121 | return
122 |
123 | if required_state:
124 | repl._monitor_all()
125 | else:
126 | repl._demonitor_all()
127 | repl._autoreload = required_state
128 |
129 |
130 | class Exec(Magic):
131 | @property
132 | def description(self) -> str:
133 | return "execute the given file path in the context of the currently loaded scripts"
134 |
135 | @property
136 | def required_args_count(self) -> int:
137 | return 1
138 |
139 | def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
140 | if not os.path.exists(args[0]):
141 | repl._print("Can't read the given file because it does not exist")
142 | return
143 |
144 | try:
145 | with codecs.open(args[0], "rb", "utf-8") as f:
146 | if not repl._exec_and_print(repl._evaluate_expression, f.read()):
147 | repl._errors += 1
148 | except PermissionError:
149 | repl._print("Can't read the given file because of a permission error")
150 |
151 |
152 | class Time(Magic):
153 | @property
154 | def description(self) -> str:
155 | return "measure the execution time of the given expression and print it to the screen"
156 |
157 | @property
158 | def required_args_count(self) -> int:
159 | return -2
160 |
161 | def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
162 | repl._exec_and_print(
163 | repl._evaluate_expression,
164 | """
165 | (() => {{
166 | const _startTime = Date.now();
167 | const _result = eval({expression});
168 | const _endTime = Date.now();
169 | console.log('Time: ' + (_endTime - _startTime) + ' ms.');
170 | return _result;
171 | }})();""".format(
172 | expression=json.dumps(" ".join(args))
173 | ),
174 | )
175 |
176 |
177 | class Help(Magic):
178 | @property
179 | def description(self) -> str:
180 | return "print a list of available REPL commands"
181 |
182 | @property
183 | def required_args_count(self) -> int:
184 | return 0
185 |
186 | def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
187 | repl._print("Available commands: ")
188 | for name, command in repl._magic_command_args.items():
189 | if command.required_args_count >= 0:
190 | required_args = f"({command.required_args_count})"
191 | else:
192 | required_args = f"({abs(command.required_args_count) - 1}+)"
193 |
194 | repl._print(f" %{name}{required_args} - {command.description}")
195 |
196 | repl._print("")
197 | repl._print("For help with Frida scripting API, check out https://frida.re/docs/")
198 | repl._print("")
199 |
--------------------------------------------------------------------------------
/tests/test_arguments.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from frida_tools.application import ConsoleApplication
4 | from frida_tools.kill import KillApplication
5 |
6 |
7 | class DummyConsoleApplication(ConsoleApplication):
8 | def _usage(self):
9 | return "no usage"
10 |
11 |
12 | class DeviceParsingTestCase(unittest.TestCase):
13 | def test_short_device_id(self):
14 | test_cases = [("short device id", "123", ["-D", "123"]), ("long device id", "abc", ["--device", "abc"])]
15 | for message, result, args in test_cases:
16 | with self.subTest(message, args=args):
17 | app = DummyConsoleApplication(args=args)
18 | self.assertEqual(result, app._device_id)
19 |
20 | def test_device_id_missing(self):
21 | test_cases = [("short device", ["-D"]), ("long device", ["--device"])]
22 | for message, args in test_cases:
23 | with self.subTest(message, args=args):
24 | with self.assertRaises(SystemExit):
25 | DummyConsoleApplication(args=args)
26 |
27 | def test_device_type(self):
28 | test_cases = [
29 | ("short usb", "usb", ["-U"]),
30 | ("long usb", "usb", ["--usb"]),
31 | ("short remote", "remote", ["-R"]),
32 | ("long remote", "remote", ["--remote"]),
33 | ]
34 | for message, result, args in test_cases:
35 | with self.subTest(message, args=args):
36 | app = DummyConsoleApplication(args=args)
37 | self.assertEqual(app._device_type, result)
38 |
39 | def test_remote_host(self):
40 | test_cases = [
41 | ("short host", "127.0.0.1", ["-H", "127.0.0.1"]),
42 | ("long host", "192.168.1.1:1234", ["--host", "192.168.1.1:1234"]),
43 | ]
44 |
45 | for message, result, args in test_cases:
46 | with self.subTest(message, args=args):
47 | app = DummyConsoleApplication(args=args)
48 | self.assertEqual(app._host, result)
49 |
50 | def test_missing_remote_host(self):
51 | test_cases = [("short host", ["-H"]), ("long host", ["--host"])]
52 | for message, args in test_cases:
53 | with self.subTest(message, args=args):
54 | with self.assertRaises(SystemExit):
55 | DummyConsoleApplication(args=args)
56 |
57 | def test_certificate(self):
58 | path = "/path/to/file"
59 | args = ["--certificate", path]
60 | app = DummyConsoleApplication(args=args)
61 | self.assertEqual(path, app._certificate)
62 |
63 | def test_missing_certificate(self):
64 | args = ["--certificate"]
65 | with self.assertRaises(SystemExit):
66 | DummyConsoleApplication(args=args)
67 |
68 | def test_origin(self):
69 | origin = "null"
70 | args = ["--origin", origin]
71 | app = DummyConsoleApplication(args=args)
72 | self.assertEqual(origin, app._origin)
73 |
74 | def test_missing_origin(self):
75 | args = ["--origin"]
76 | with self.assertRaises(SystemExit):
77 | DummyConsoleApplication(args=args)
78 |
79 | def test_token(self):
80 | token = "ABCDEF"
81 | args = ["--token", token]
82 | app = DummyConsoleApplication(args=args)
83 | self.assertEqual(token, app._token)
84 |
85 | def test_missing_token(self):
86 | args = ["--token"]
87 | with self.assertRaises(SystemExit):
88 | DummyConsoleApplication(args=args)
89 |
90 | def test_keepalive_interval(self):
91 | interval = 123
92 | args = ["--keepalive-interval", str(interval)]
93 | app = DummyConsoleApplication(args=args)
94 | self.assertEqual(interval, app._keepalive_interval)
95 |
96 | def test_missing_keepalive_interval(self):
97 | args = ["--keepalive-interval"]
98 | with self.assertRaises(SystemExit):
99 | DummyConsoleApplication(args=args)
100 |
101 | def test_non_decimal_keepalive_interval(self):
102 | args = ["--keepalive-interval", "abc"]
103 | with self.assertRaises(SystemExit):
104 | DummyConsoleApplication(args=args)
105 |
106 | def test_default_session_transport(self):
107 | app = DummyConsoleApplication(args=[])
108 | self.assertEqual("multiplexed", app._session_transport)
109 |
110 | def test_p2p_session_transport(self):
111 | app = DummyConsoleApplication(args=["--p2p"])
112 | self.assertEqual("p2p", app._session_transport)
113 |
114 | def test_stun_server(self):
115 | stun_server = "192.168.1.1"
116 | args = ["--stun-server", stun_server]
117 | app = DummyConsoleApplication(args=args)
118 | self.assertEqual(stun_server, app._stun_server)
119 |
120 | def test_missing_stun_server(self):
121 | args = ["--stun-server"]
122 | with self.assertRaises(SystemExit):
123 | DummyConsoleApplication(args=args)
124 |
125 | def test_single_relay(self):
126 | address = "127.0.0.1"
127 | username = "admin"
128 | password = "password"
129 | kind = "turn-udp"
130 |
131 | serialized = ",".join((address, username, password, kind))
132 | args = ["--relay", serialized]
133 | app = DummyConsoleApplication(args=args)
134 |
135 | self.assertEqual(len(app._relays), 1)
136 | self.assertEqual(app._relays[0].address, address)
137 | self.assertEqual(app._relays[0].username, username)
138 | self.assertEqual(app._relays[0].password, password)
139 | self.assertEqual(app._relays[0].kind, kind)
140 |
141 | def test_multiple_relay(self):
142 | relays = [("127.0.0.1", "admin", "password", "turn-udp"), ("192.168.1.1", "user", "user", "turn-tls")]
143 | args = []
144 | for relay in relays:
145 | args.append("--relay")
146 | args.append(",".join(relay))
147 |
148 | app = DummyConsoleApplication(args=args)
149 |
150 | self.assertEqual(len(app._relays), len(relays))
151 | for i in range(len(relays)):
152 | self.assertEqual(app._relays[i].address, relays[i][0])
153 | self.assertEqual(app._relays[i].username, relays[i][1])
154 | self.assertEqual(app._relays[i].password, relays[i][2])
155 | self.assertEqual(app._relays[i].kind, relays[i][3])
156 |
157 | def test_multiple_device_types(self):
158 | combinations = [("host and device id", ["--host", "127.0.0.1", "-D", "ABCDEF"])]
159 |
160 | for message, args in combinations:
161 | with self.subTest(message, args=args):
162 | with self.assertRaises(SystemExit):
163 | DummyConsoleApplication(args=args)
164 |
165 |
166 | class KillParsingTestCase(unittest.TestCase):
167 | def test_no_arguments(self):
168 | with self.assertRaises(SystemExit):
169 | KillApplication(args=[])
170 |
171 | def test_passing_pid(self):
172 | kill_app = KillApplication(args=["2"])
173 | self.assertEqual(kill_app._process, 2)
174 |
175 | def test_passing_process_name(self):
176 | kill_app = KillApplication(args=["python"])
177 | self.assertEqual(kill_app._process, "python")
178 |
179 | def test_passing_file(self):
180 | with self.assertRaises(SystemExit):
181 | KillApplication(args=["./file"])
182 |
--------------------------------------------------------------------------------
/frida_tools/creator.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import codecs
3 | import os
4 | import platform
5 | from typing import Dict, List, Tuple
6 |
7 | import frida
8 |
9 | from frida_tools.application import ConsoleApplication
10 |
11 |
12 | def main() -> None:
13 | app = CreatorApplication()
14 | app.run()
15 |
16 |
17 | class CreatorApplication(ConsoleApplication):
18 | def _usage(self) -> str:
19 | return "%(prog)s [options] -t agent|cmodule"
20 |
21 | def _add_options(self, parser: argparse.ArgumentParser) -> None:
22 | default_project_name = os.path.basename(os.getcwd())
23 | parser.add_argument(
24 | "-n", "--project-name", help="project name", dest="project_name", default=default_project_name
25 | )
26 | parser.add_argument("-o", "--output-directory", help="output directory", dest="outdir", default=".")
27 | parser.add_argument("-t", "--template", help="template file: cmodule|agent", dest="template", default=None)
28 |
29 | def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None:
30 | parsed_args = parser.parse_args()
31 | if not parsed_args.template:
32 | parser.error("template must be specified")
33 | impl = getattr(self, "_generate_" + parsed_args.template, None)
34 | if impl is None:
35 | parser.error("unknown template type")
36 | self._generate = impl
37 |
38 | self._project_name = options.project_name
39 | self._outdir = options.outdir
40 |
41 | def _needs_device(self) -> bool:
42 | return False
43 |
44 | def _start(self) -> None:
45 | (assets, message) = self._generate()
46 |
47 | outdir = self._outdir
48 | for name, data in assets.items():
49 | asset_path = os.path.join(outdir, name)
50 |
51 | asset_dir = os.path.dirname(asset_path)
52 | try:
53 | os.makedirs(asset_dir)
54 | except:
55 | pass
56 |
57 | with codecs.open(asset_path, "wb", "utf-8") as f:
58 | f.write(data)
59 |
60 | self._print("Created", asset_path)
61 |
62 | self._print("\n" + message)
63 |
64 | self._exit(0)
65 |
66 | def _generate_agent(self) -> Tuple[Dict[str, str], str]:
67 | assets = {}
68 |
69 | assets[
70 | "package.json"
71 | ] = f"""{{
72 | "name": "{self._project_name}-agent",
73 | "version": "1.0.0",
74 | "description": "Frida agent written in TypeScript",
75 | "private": true,
76 | "main": "agent/index.ts",
77 | "type": "module",
78 | "scripts": {{
79 | "prepare": "npm run build",
80 | "build": "frida-compile agent/index.ts -o _agent.js -c",
81 | "watch": "frida-compile agent/index.ts -o _agent.js -w"
82 | }},
83 | "devDependencies": {{
84 | "@types/frida-gum": "^19.0.0",
85 | "@types/node": "^18.14.0"
86 | }}
87 | }}
88 | """
89 |
90 | assets[
91 | "tsconfig.json"
92 | ] = """\
93 | {
94 | "compilerOptions": {
95 | "target": "ES2022",
96 | "lib": ["ES2022"],
97 | "module": "Node16",
98 | "strict": true,
99 | "noEmit": true
100 | },
101 | "include": ["agent/**/*.ts"]
102 | }
103 |
104 | """
105 |
106 | assets[
107 | "agent/index.ts"
108 | ] = """\
109 | import { log } from "./logger.js";
110 |
111 | const header = Memory.alloc(16);
112 | header
113 | .writeU32(0xdeadbeef).add(4)
114 | .writeU32(0xd00ff00d).add(4)
115 | .writeU64(uint64("0x1122334455667788"));
116 | log(hexdump(header.readByteArray(16) as ArrayBuffer, { ansi: true }));
117 |
118 | Process.getModuleByName("libSystem.B.dylib")
119 | .enumerateExports()
120 | .slice(0, 16)
121 | .forEach((exp, index) => {
122 | log(`export ${index}: ${exp.name}`);
123 | });
124 |
125 | Interceptor.attach(Module.findGlobalExportByName("open")!, {
126 | onEnter(args) {
127 | const path = args[0].readUtf8String();
128 | log(`open() path="${path}"`);
129 | }
130 | });
131 | """
132 |
133 | assets[
134 | "agent/logger.ts"
135 | ] = """\
136 | export function log(message: string): void {
137 | console.log(message);
138 | }
139 | """
140 |
141 | assets[".gitignore"] = "/node_modules/\n_agent.js\n"
142 |
143 | message = """\
144 | Run `npm install` to bootstrap, then:
145 | - Keep one terminal running: npm run watch
146 | - Inject agent using the REPL: frida Calculator -l _agent.js
147 | - Edit agent/*.ts - REPL will live-reload on save
148 |
149 | Tip: Use an editor like Visual Studio Code for code completion, inline docs,
150 | instant type-checking feedback, refactoring tools, etc.
151 | """
152 |
153 | return (assets, message)
154 |
155 | def _generate_cmodule(self) -> Tuple[Dict[str, str], str]:
156 | assets = {}
157 |
158 | assets[
159 | "meson.build"
160 | ] = f"""\
161 | project('{self._project_name}', 'c',
162 | default_options: 'buildtype=release',
163 | )
164 |
165 | shared_module('{self._project_name}', '{self._project_name}.c',
166 | name_prefix: '',
167 | include_directories: include_directories('include'),
168 | )
169 | """
170 |
171 | assets[
172 | self._project_name + ".c"
173 | ] = """\
174 | #include
175 |
176 | static void frida_log (const char * format, ...);
177 | extern void _frida_log (const gchar * message);
178 |
179 | void
180 | init (void)
181 | {
182 | frida_log ("init()");
183 | }
184 |
185 | void
186 | finalize (void)
187 | {
188 | frida_log ("finalize()");
189 | }
190 |
191 | void
192 | on_enter (GumInvocationContext * ic)
193 | {
194 | gpointer arg0;
195 |
196 | arg0 = gum_invocation_context_get_nth_argument (ic, 0);
197 |
198 | frida_log ("on_enter() arg0=%p", arg0);
199 | }
200 |
201 | void
202 | on_leave (GumInvocationContext * ic)
203 | {
204 | gpointer retval;
205 |
206 | retval = gum_invocation_context_get_return_value (ic);
207 |
208 | frida_log ("on_leave() retval=%p", retval);
209 | }
210 |
211 | static void
212 | frida_log (const char * format,
213 | ...)
214 | {
215 | gchar * message;
216 | va_list args;
217 |
218 | va_start (args, format);
219 | message = g_strdup_vprintf (format, args);
220 | va_end (args);
221 |
222 | _frida_log (message);
223 |
224 | g_free (message);
225 | }
226 | """
227 |
228 | assets[".gitignore"] = "/build/\n"
229 |
230 | session = frida.attach(0)
231 | script = session.create_script("rpc.exports.getBuiltins = () => CModule.builtins;")
232 | self._on_script_created(script)
233 | script.load()
234 | builtins = script.exports_sync.get_builtins()
235 | script.unload()
236 | session.detach()
237 |
238 | for name, data in builtins["headers"].items():
239 | assets["include/" + name] = data
240 |
241 | system = platform.system()
242 | if system == "Windows":
243 | module_extension = "dll"
244 | elif system == "Darwin":
245 | module_extension = "dylib"
246 | else:
247 | module_extension = "so"
248 |
249 | cmodule_path = os.path.join(self._outdir, "build", self._project_name + "." + module_extension)
250 |
251 | message = f"""\
252 | Run `meson build && ninja -C build` to build, then:
253 | - Inject CModule using the REPL: frida Calculator -C {cmodule_path}
254 | - Edit *.c, and build incrementally through `ninja -C build`
255 | - REPL will live-reload whenever {cmodule_path} changes on disk
256 | """
257 |
258 | return (assets, message)
259 |
260 |
261 | if __name__ == "__main__":
262 | try:
263 | main()
264 | except KeyboardInterrupt:
265 | pass
266 |
--------------------------------------------------------------------------------
/frida_tools/push.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import codecs
3 | import os
4 | import sys
5 | import time
6 | from threading import Event, Thread
7 | from typing import AnyStr, List, MutableMapping, Optional
8 |
9 | import frida
10 | from colorama import Fore, Style
11 |
12 | from frida_tools.application import ConsoleApplication
13 | from frida_tools.stream_controller import DisposedException, StreamController
14 | from frida_tools.units import bytes_to_megabytes
15 |
16 |
17 | def main() -> None:
18 | app = PushApplication()
19 | app.run()
20 |
21 |
22 | class PushApplication(ConsoleApplication):
23 | def _add_options(self, parser: argparse.ArgumentParser) -> None:
24 | parser.add_argument("files", help="local files to push", nargs="+")
25 |
26 | def _usage(self) -> str:
27 | return "%(prog)s [options] LOCAL... REMOTE"
28 |
29 | def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None:
30 | paths = options.files
31 | if len(paths) == 1:
32 | raise ValueError("missing remote path")
33 | self._local_paths = paths[:-1]
34 | self._remote_path = paths[-1]
35 |
36 | self._script: Optional[frida.core.Script] = None
37 | self._stream_controller: Optional[StreamController] = None
38 | self._total_bytes = 0
39 | self._time_started: Optional[float] = None
40 | self._completed = Event()
41 | self._transfers: MutableMapping[str, bool] = {}
42 |
43 | def _needs_target(self) -> bool:
44 | return False
45 |
46 | def _start(self) -> None:
47 | try:
48 | self._attach(self._pick_worker_pid())
49 |
50 | data_dir = os.path.dirname(__file__)
51 | with codecs.open(os.path.join(data_dir, "fs_agent.js"), "r", "utf-8") as f:
52 | source = f.read()
53 |
54 | def on_message(message, data) -> None:
55 | self._reactor.schedule(lambda: self._on_message(message, data))
56 |
57 | assert self._session is not None
58 | script = self._session.create_script(name="push", source=source)
59 | self._script = script
60 | script.on("message", on_message)
61 | self._on_script_created(script)
62 | script.load()
63 |
64 | self._stream_controller = StreamController(
65 | self._post_stream_stanza, on_stats_updated=self._on_stream_stats_updated
66 | )
67 |
68 | worker = Thread(target=self._perform_push)
69 | worker.start()
70 | except Exception as e:
71 | self._update_status(f"Failed to push: {e}")
72 | self._exit(1)
73 | return
74 |
75 | def _stop(self) -> None:
76 | for path in self._local_paths:
77 | if path not in self._transfers:
78 | self._complete_transfer(path, success=False)
79 |
80 | if self._stream_controller is not None:
81 | self._stream_controller.dispose()
82 |
83 | def _perform_push(self) -> None:
84 | for path in self._local_paths:
85 | try:
86 | self._total_bytes += os.path.getsize(path)
87 | except:
88 | pass
89 | self._time_started = time.time()
90 |
91 | for i, path in enumerate(self._local_paths):
92 | filename = os.path.basename(path)
93 |
94 | try:
95 | with open(path, "rb") as f:
96 | assert self._stream_controller is not None
97 | sink = self._stream_controller.open(str(i), {"filename": filename, "target": self._remote_path})
98 | while True:
99 | chunk = f.read(4 * 1024 * 1024)
100 | if len(chunk) == 0:
101 | break
102 | sink.write(chunk)
103 | sink.close()
104 | except DisposedException:
105 | break
106 | except Exception as e:
107 | self._print_error(str(e))
108 | self._complete_transfer(path, success=False)
109 |
110 | self._completed.wait()
111 |
112 | self._reactor.schedule(lambda: self._on_push_finished())
113 |
114 | def _on_push_finished(self) -> None:
115 | successes = self._transfers.values()
116 |
117 | if any(successes):
118 | self._render_summary_ui()
119 |
120 | status = 0 if all(successes) else 1
121 | self._exit(status)
122 |
123 | def _render_progress_ui(self) -> None:
124 | if self._completed.is_set():
125 | return
126 | assert self._stream_controller is not None
127 | megabytes_sent = bytes_to_megabytes(self._stream_controller.bytes_sent)
128 | total_megabytes = bytes_to_megabytes(self._total_bytes)
129 | if total_megabytes != 0 and megabytes_sent <= total_megabytes:
130 | self._update_status(f"Pushed {megabytes_sent:.1f} out of {total_megabytes:.1f} MB")
131 | else:
132 | self._update_status(f"Pushed {megabytes_sent:.1f} MB")
133 |
134 | def _render_summary_ui(self) -> None:
135 | assert self._time_started is not None
136 | duration = time.time() - self._time_started
137 |
138 | if len(self._local_paths) == 1:
139 | prefix = f"{self._local_paths[0]}: "
140 | else:
141 | prefix = ""
142 |
143 | files_transferred = sum(map(int, self._transfers.values()))
144 |
145 | assert self._stream_controller is not None
146 | bytes_sent = self._stream_controller.bytes_sent
147 | megabytes_per_second = bytes_to_megabytes(bytes_sent) / duration
148 |
149 | self._update_status(
150 | "{}{} file{} pushed. {:.1f} MB/s ({} bytes in {:.3f}s)".format(
151 | prefix,
152 | files_transferred,
153 | "s" if files_transferred != 1 else "",
154 | megabytes_per_second,
155 | bytes_sent,
156 | duration,
157 | )
158 | )
159 |
160 | def _on_message(self, message, data) -> None:
161 | handled = False
162 |
163 | if message["type"] == "send":
164 | payload = message["payload"]
165 | ptype = payload["type"]
166 | if ptype == "stream":
167 | stanza = payload["payload"]
168 | self._stream_controller.receive(stanza, data)
169 | handled = True
170 | elif ptype == "push:io-success":
171 | index = payload["index"]
172 | self._on_io_success(self._local_paths[index])
173 | handled = True
174 | elif ptype == "push:io-error":
175 | index = payload["index"]
176 | self._on_io_error(self._local_paths[index], payload["error"])
177 | handled = True
178 |
179 | if not handled:
180 | self._print(message)
181 |
182 | def _on_io_success(self, local_path: str) -> None:
183 | self._complete_transfer(local_path, success=True)
184 |
185 | def _on_io_error(self, local_path: str, error) -> None:
186 | self._print_error(f"{local_path}: {error}")
187 | self._complete_transfer(local_path, success=False)
188 |
189 | def _complete_transfer(self, local_path: str, success: bool) -> None:
190 | self._transfers[local_path] = success
191 | if len(self._transfers) == len(self._local_paths):
192 | self._completed.set()
193 |
194 | def _post_stream_stanza(self, stanza, data: Optional[AnyStr] = None) -> None:
195 | self._script.post({"type": "stream", "payload": stanza}, data=data)
196 |
197 | def _on_stream_stats_updated(self) -> None:
198 | self._render_progress_ui()
199 |
200 | def _print_error(self, message: str) -> None:
201 | self._print(Fore.RED + Style.BRIGHT + message + Style.RESET_ALL, file=sys.stderr)
202 |
203 |
204 | if __name__ == "__main__":
205 | try:
206 | main()
207 | except KeyboardInterrupt:
208 | pass
209 |
--------------------------------------------------------------------------------
/frida_tools/pull.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import codecs
3 | import os
4 | import sys
5 | import time
6 | import typing
7 | from threading import Thread
8 | from typing import Any, AnyStr, List, Mapping, Optional
9 |
10 | import frida
11 | from colorama import Fore, Style
12 |
13 | from frida_tools.application import ConsoleApplication
14 | from frida_tools.stream_controller import StreamController
15 | from frida_tools.units import bytes_to_megabytes
16 |
17 |
18 | def main() -> None:
19 | app = PullApplication()
20 | app.run()
21 |
22 |
23 | class PullApplication(ConsoleApplication):
24 | def _add_options(self, parser: argparse.ArgumentParser) -> None:
25 | parser.add_argument("files", help="remote files to pull", nargs="+")
26 |
27 | def _usage(self) -> str:
28 | return "%(prog)s [options] REMOTE... LOCAL"
29 |
30 | def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None:
31 | paths = options.files
32 | if len(paths) == 1:
33 | self._remote_paths = paths
34 | self._local_paths = [os.path.join(os.getcwd(), basename_of_unknown_path(paths[0]))]
35 | elif len(paths) == 2:
36 | remote, local = paths
37 | self._remote_paths = [remote]
38 | if os.path.isdir(local):
39 | self._local_paths = [os.path.join(local, basename_of_unknown_path(remote))]
40 | else:
41 | self._local_paths = [local]
42 | else:
43 | self._remote_paths = paths[:-1]
44 | local_dir = paths[-1]
45 | local_filenames = map(basename_of_unknown_path, self._remote_paths)
46 | self._local_paths = [os.path.join(local_dir, filename) for filename in local_filenames]
47 |
48 | self._script: Optional[frida.core.Script] = None
49 | self._stream_controller: Optional[StreamController] = None
50 | self._total_bytes = 0
51 | self._time_started: Optional[float] = None
52 | self._failed_paths = []
53 |
54 | def _needs_target(self) -> bool:
55 | return False
56 |
57 | def _start(self) -> None:
58 | try:
59 | self._attach(self._pick_worker_pid())
60 |
61 | data_dir = os.path.dirname(__file__)
62 | with codecs.open(os.path.join(data_dir, "fs_agent.js"), "r", "utf-8") as f:
63 | source = f.read()
64 |
65 | def on_message(message: Mapping[Any, Any], data: Any) -> None:
66 | self._reactor.schedule(lambda: self._on_message(message, data))
67 |
68 | assert self._session is not None
69 | script = self._session.create_script(name="pull", source=source)
70 | self._script = script
71 | script.on("message", on_message)
72 | self._on_script_created(script)
73 | script.load()
74 |
75 | self._stream_controller = StreamController(
76 | self._post_stream_stanza,
77 | self._on_incoming_stream_request,
78 | on_stats_updated=self._on_stream_stats_updated,
79 | )
80 |
81 | worker = Thread(target=self._perform_pull)
82 | worker.start()
83 | except Exception as e:
84 | self._update_status(f"Failed to pull: {e}")
85 | self._exit(1)
86 | return
87 |
88 | def _stop(self) -> None:
89 | if self._stream_controller is not None:
90 | self._stream_controller.dispose()
91 |
92 | def _perform_pull(self) -> None:
93 | error = None
94 | try:
95 | assert self._script is not None
96 | self._script.exports_sync.pull(self._remote_paths)
97 | except Exception as e:
98 | error = e
99 |
100 | self._reactor.schedule(lambda: self._on_pull_finished(error))
101 |
102 | def _on_pull_finished(self, error: Optional[Exception]) -> None:
103 | for path, state in self._failed_paths:
104 | if state == "partial":
105 | try:
106 | os.unlink(path)
107 | except:
108 | pass
109 |
110 | if error is None:
111 | self._render_summary_ui()
112 | else:
113 | self._print_error(str(error))
114 |
115 | success = len(self._failed_paths) == 0 and error is None
116 | status = 0 if success else 1
117 | self._exit(status)
118 |
119 | def _render_progress_ui(self) -> None:
120 | assert self._stream_controller is not None
121 | megabytes_received = bytes_to_megabytes(self._stream_controller.bytes_received)
122 | total_megabytes = bytes_to_megabytes(self._total_bytes)
123 | if total_megabytes != 0 and megabytes_received <= total_megabytes:
124 | self._update_status(f"Pulled {megabytes_received:.1f} out of {total_megabytes:.1f} MB")
125 | else:
126 | self._update_status(f"Pulled {megabytes_received:.1f} MB")
127 |
128 | def _render_summary_ui(self) -> None:
129 | assert self._time_started is not None
130 | duration = time.time() - self._time_started
131 |
132 | if len(self._remote_paths) == 1:
133 | prefix = f"{self._remote_paths[0]}: "
134 | else:
135 | prefix = ""
136 |
137 | assert self._stream_controller is not None
138 | sc = self._stream_controller
139 | bytes_received = sc.bytes_received
140 | megabytes_per_second = bytes_to_megabytes(bytes_received) / duration
141 |
142 | self._update_status(
143 | "{}{} file{} pulled. {:.1f} MB/s ({} bytes in {:.3f}s)".format(
144 | prefix,
145 | sc.streams_opened,
146 | "s" if sc.streams_opened != 1 else "",
147 | megabytes_per_second,
148 | bytes_received,
149 | duration,
150 | )
151 | )
152 |
153 | def _on_message(self, message: Mapping[Any, Any], data: Any) -> None:
154 | handled = False
155 |
156 | if message["type"] == "send":
157 | payload = message["payload"]
158 | ptype = payload["type"]
159 | if ptype == "stream":
160 | stanza = payload["payload"]
161 | assert self._stream_controller is not None
162 | self._stream_controller.receive(stanza, data)
163 | handled = True
164 | elif ptype == "pull:status":
165 | self._total_bytes = payload["total"]
166 | self._time_started = time.time()
167 | self._render_progress_ui()
168 | handled = True
169 | elif ptype == "pull:io-error":
170 | index = payload["index"]
171 | self._on_io_error(self._remote_paths[index], self._local_paths[index], payload["error"])
172 | handled = True
173 |
174 | if not handled:
175 | self._print(message)
176 |
177 | def _on_io_error(self, remote_path, local_path, error) -> None:
178 | self._print_error(f"{remote_path}: {error}")
179 | self._failed_paths.append((local_path, "partial"))
180 |
181 | def _post_stream_stanza(self, stanza, data: Optional[AnyStr] = None) -> None:
182 | self._script.post({"type": "stream", "payload": stanza}, data=data)
183 |
184 | def _on_incoming_stream_request(self, label: str, details) -> typing.BinaryIO:
185 | local_path = self._local_paths[int(label)]
186 | try:
187 | return open(local_path, "wb")
188 | except Exception as e:
189 | self._print_error(str(e))
190 | self._failed_paths.append((local_path, "unopened"))
191 | raise
192 |
193 | def _on_stream_stats_updated(self) -> None:
194 | self._render_progress_ui()
195 |
196 | def _print_error(self, message: str) -> None:
197 | self._print(Fore.RED + Style.BRIGHT + message + Style.RESET_ALL, file=sys.stderr)
198 |
199 |
200 | def basename_of_unknown_path(path: str) -> str:
201 | return path.replace("\\", "/").rsplit("/", 1)[-1]
202 |
203 |
204 | if __name__ == "__main__":
205 | try:
206 | main()
207 | except KeyboardInterrupt:
208 | pass
209 |
--------------------------------------------------------------------------------
/frida_tools/discoverer.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import threading
3 | from typing import List, Mapping, Optional, Tuple
4 |
5 | import frida
6 |
7 | from frida_tools.application import ConsoleApplication, await_enter
8 | from frida_tools.model import Function, Module, ModuleFunction
9 | from frida_tools.reactor import Reactor
10 |
11 |
12 | class UI:
13 | def on_sample_start(self, total: int) -> None:
14 | pass
15 |
16 | def on_sample_result(
17 | self,
18 | module_functions: Mapping[Module, List[Tuple[ModuleFunction, int]]],
19 | dynamic_functions: List[Tuple[ModuleFunction, int]],
20 | ) -> None:
21 | pass
22 |
23 | def _on_script_created(self, script: frida.core.Script) -> None:
24 | pass
25 |
26 |
27 | class Discoverer:
28 | def __init__(self, reactor: Reactor) -> None:
29 | self._reactor = reactor
30 | self._ui = None
31 | self._script: Optional[frida.core.Script] = None
32 |
33 | def dispose(self) -> None:
34 | if self._script is not None:
35 | try:
36 | self._script.unload()
37 | except:
38 | pass
39 | self._script = None
40 |
41 | def start(self, session: frida.core.Session, runtime: str, ui: UI) -> None:
42 | def on_message(message, data) -> None:
43 | print(message, data)
44 |
45 | self._ui = ui
46 |
47 | script = session.create_script(name="discoverer", source=self._create_discover_script(), runtime=runtime)
48 | self._script = script
49 | self._ui._on_script_created(script)
50 | script.on("message", on_message)
51 | script.load()
52 |
53 | params = script.exports_sync.start()
54 | ui.on_sample_start(params["total"])
55 |
56 | def stop(self) -> None:
57 | result = self._script.exports_sync.stop()
58 |
59 | modules = {
60 | int(module_id): Module(m["name"], int(m["base"], 16), m["size"], m["path"])
61 | for module_id, m in result["modules"].items()
62 | }
63 |
64 | module_functions = {}
65 | dynamic_functions = []
66 | for module_id, name, visibility, raw_address, count in result["targets"]:
67 | address = int(raw_address, 16)
68 |
69 | if module_id != 0:
70 | module = modules[module_id]
71 | exported = visibility == "e"
72 | function = ModuleFunction(module, name, address - module.base_address, exported)
73 |
74 | functions = module_functions.get(module, [])
75 | if len(functions) == 0:
76 | module_functions[module] = functions
77 | functions.append((function, count))
78 | else:
79 | function = Function(name, address)
80 |
81 | dynamic_functions.append((function, count))
82 |
83 | self._ui.on_sample_result(module_functions, dynamic_functions)
84 |
85 | def _create_discover_script(self) -> str:
86 | return """\
87 | const threadIds = new Set();
88 | const result = new Map();
89 |
90 | rpc.exports = {
91 | start: function () {
92 | for (const { id: threadId } of Process.enumerateThreads()) {
93 | threadIds.add(threadId);
94 | Stalker.follow(threadId, {
95 | events: { call: true },
96 | onCallSummary(summary) {
97 | for (const [address, count] of Object.entries(summary)) {
98 | result.set(address, (result.get(address) ?? 0) + count);
99 | }
100 | }
101 | });
102 | }
103 |
104 | return {
105 | total: threadIds.size
106 | };
107 | },
108 | stop: function () {
109 | for (const threadId of threadIds.values()) {
110 | Stalker.unfollow(threadId);
111 | }
112 | threadIds.clear();
113 |
114 | const targets = [];
115 | const modules = {};
116 |
117 | const moduleMap = new ModuleMap();
118 | const allModules = moduleMap.values().reduce((m, module) => m.set(module.path, module), new Map());
119 | const moduleDetails = new Map();
120 | let nextModuleId = 1;
121 |
122 | for (const [address, count] of result.entries()) {
123 | let moduleId = 0;
124 | let name;
125 | let visibility = 'i';
126 | const addressPtr = ptr(address);
127 |
128 | const path = moduleMap.findPath(addressPtr);
129 | if (path !== null) {
130 | const module = allModules.get(path);
131 |
132 | let details = moduleDetails.get(path);
133 | if (details !== undefined) {
134 | moduleId = details.id;
135 | } else {
136 | moduleId = nextModuleId++;
137 |
138 | details = {
139 | id: moduleId,
140 | exports: module.enumerateExports().reduce((m, e) => m.set(e.address.toString(), e.name), new Map())
141 | };
142 | moduleDetails.set(path, details);
143 |
144 | modules[moduleId] = module;
145 | }
146 |
147 | const exportName = details.exports.get(address);
148 | if (exportName !== undefined) {
149 | name = exportName;
150 | visibility = 'e';
151 | } else {
152 | name = 'sub_' + addressPtr.sub(module.base).toString(16);
153 | }
154 | } else {
155 | name = 'dsub_' + addressPtr.toString(16);
156 | }
157 |
158 | targets.push([moduleId, name, visibility, address, count]);
159 | }
160 |
161 | result.clear();
162 |
163 | return {
164 | targets,
165 | modules
166 | };
167 | }
168 | };
169 | """
170 |
171 |
172 | class DiscovererApplication(ConsoleApplication, UI):
173 | _discoverer: Optional[Discoverer]
174 |
175 | def __init__(self) -> None:
176 | self._results_received = threading.Event()
177 | ConsoleApplication.__init__(self, self._await_keys)
178 |
179 | def _await_keys(self, reactor: Reactor) -> None:
180 | await_enter(reactor)
181 | reactor.schedule(lambda: self._discoverer.stop())
182 | while reactor.is_running() and not self._results_received.is_set():
183 | self._results_received.wait(0.5)
184 |
185 | def _usage(self) -> str:
186 | return "%(prog)s [options] target"
187 |
188 | def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None:
189 | self._discoverer = None
190 |
191 | def _needs_target(self) -> bool:
192 | return True
193 |
194 | def _start(self) -> None:
195 | self._update_status("Injecting script...")
196 | self._discoverer = Discoverer(self._reactor)
197 | self._discoverer.start(self._session, self._runtime, self)
198 |
199 | def _stop(self) -> None:
200 | self._print("Stopping...")
201 | assert self._discoverer is not None
202 | self._discoverer.dispose()
203 | self._discoverer = None
204 |
205 | def on_sample_start(self, total: int) -> None:
206 | self._update_status(f"Tracing {total} threads. Press ENTER to stop.")
207 | self._resume()
208 |
209 | def on_sample_result(
210 | self,
211 | module_functions: Mapping[Module, List[Tuple[ModuleFunction, int]]],
212 | dynamic_functions: List[Tuple[ModuleFunction, int]],
213 | ) -> None:
214 | for module, functions in module_functions.items():
215 | self._print(module.name)
216 | self._print("\t%-10s\t%s" % ("Calls", "Function"))
217 | for function, count in sorted(functions, key=lambda item: item[1], reverse=True):
218 | self._print("\t%-10d\t%s" % (count, function))
219 | self._print("")
220 |
221 | if len(dynamic_functions) > 0:
222 | self._print("Dynamic functions:")
223 | self._print("\t%-10s\t%s" % ("Calls", "Function"))
224 | for function, count in sorted(dynamic_functions, key=lambda item: item[1], reverse=True):
225 | self._print("\t%-10d\t%s" % (count, function))
226 |
227 | self._results_received.set()
228 |
229 |
230 | def main() -> None:
231 | app = DiscovererApplication()
232 | app.run()
233 |
234 |
235 | if __name__ == "__main__":
236 | try:
237 | main()
238 | except KeyboardInterrupt:
239 | pass
240 |
--------------------------------------------------------------------------------
/apps/tracer/src/App.tsx:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 | import AddTargetsDialog from "./AddTargetsDialog.tsx";
3 | import DisassemblyView from "./DisassemblyView.tsx";
4 | import EventView from "./EventView.tsx";
5 | import HandlerEditor from "./HandlerEditor.tsx";
6 | import HandlerList from "./HandlerList.tsx";
7 | import MemoryView from "./MemoryView.tsx";
8 | import { useModel } from "./model.js";
9 | import {
10 | BlueprintProvider,
11 | Callout,
12 | Button,
13 | ButtonGroup,
14 | Switch,
15 | Tabs,
16 | Tab,
17 | } from "@blueprintjs/core";
18 | import { Resplit } from "react-resplit";
19 |
20 | export default function App() {
21 | const {
22 | lostConnection,
23 |
24 | spawnedProgram,
25 | respawn,
26 |
27 | handlers,
28 | selectedScope,
29 | selectScope,
30 | selectedHandler,
31 | setSelectedHandlerId,
32 | handlerCode,
33 | draftedCode,
34 | setDraftedCode,
35 | deployCode,
36 | handlerMuted,
37 | setHandlerMuted,
38 | captureBacktraces,
39 | setCaptureBacktraces,
40 |
41 | selectedTabId,
42 | setSelectedTabId,
43 |
44 | events,
45 | latestMatchingEventIndex,
46 | selectedEventIndex,
47 | setSelectedEventIndex,
48 |
49 | disassemblyTarget,
50 | disassemble,
51 |
52 | memoryLocation,
53 | showMemoryLocation,
54 |
55 | addingTargets,
56 | startAddingTargets,
57 | finishAddingTargets,
58 | stageItems,
59 | stagedItems,
60 | commitItems,
61 |
62 | addInstructionHook,
63 |
64 | symbolicate,
65 | } = useModel();
66 |
67 | const connectionError = lostConnection
68 | ?
72 | : null;
73 |
74 | const eventView = (
75 | {
79 | setSelectedHandlerId(handlerId);
80 | setSelectedEventIndex(eventIndex);
81 | }}
82 | onDeactivate={() => {
83 | setSelectedEventIndex(null);
84 | }}
85 | onDisassemble={disassemble}
86 | onSymbolicate={symbolicate}
87 | />
88 | );
89 |
90 | const disassemblyView = (
91 |
100 | );
101 |
102 | const memoryView = (
103 |
107 | );
108 |
109 | return (
110 | <>
111 |
112 |
113 |
114 |
121 |
122 |
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 | | Thread ID |
216 | 0x{threadId.toString(16)} |
217 |
218 | |
219 |
220 | {(caller !== null && backtrace === null) ? (
221 |
222 | | Caller |
223 |
224 |
225 | |
226 |
227 | ) : null
228 | }
229 | {(backtrace !== null) ? (
230 |
231 | | Backtrace |
232 |
233 | {backtrace.map((address, i) =>
234 | )}
237 | |
238 |
239 | ) : null
240 | }
241 |
242 |
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 |
--------------------------------------------------------------------------------