frida-server 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- frida_server/__init__.py +73 -0
- frida_server/__main__.py +10 -0
- frida_server/checksums.py +71 -0
- frida_server/cli.py +101 -0
- frida_server/console.py +54 -0
- frida_server/deployer.py +210 -0
- frida_server/device.py +296 -0
- frida_server/downloader.py +112 -0
- frida_server/exceptions.py +34 -0
- frida_server/host.py +164 -0
- frida_server/platform_tools.py +107 -0
- frida_server/py.typed +0 -0
- frida_server-1.0.0.dist-info/METADATA +102 -0
- frida_server-1.0.0.dist-info/RECORD +18 -0
- frida_server-1.0.0.dist-info/WHEEL +5 -0
- frida_server-1.0.0.dist-info/entry_points.txt +2 -0
- frida_server-1.0.0.dist-info/licenses/LICENSE +674 -0
- frida_server-1.0.0.dist-info/top_level.txt +1 -0
frida_server/__init__.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""frida-server — zero-config Frida server deployment for Android.
|
|
2
|
+
|
|
3
|
+
Detects a connected Android device, matches the ``frida-server`` binary to the
|
|
4
|
+
Frida version installed on your PC, downloads it, pushes it, and launches it —
|
|
5
|
+
all in one command.
|
|
6
|
+
|
|
7
|
+
Typical use is via the ``frida-server`` command-line tool. The object model can
|
|
8
|
+
also be driven programmatically::
|
|
9
|
+
|
|
10
|
+
from frida_server import Deployer
|
|
11
|
+
|
|
12
|
+
Deployer().run() # interactive: pick a device and deploy
|
|
13
|
+
|
|
14
|
+
or, for the common case, the module-level convenience wrapper::
|
|
15
|
+
|
|
16
|
+
from frida_server import setup
|
|
17
|
+
|
|
18
|
+
setup(serial="emulator-5554", update=False)
|
|
19
|
+
|
|
20
|
+
See :class:`frida_server.deployer.Deployer` for the orchestration entry point.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from .exceptions import (
|
|
26
|
+
AdbError,
|
|
27
|
+
DeviceError,
|
|
28
|
+
DownloadError,
|
|
29
|
+
FridaServerError,
|
|
30
|
+
RootAccessError,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"__version__",
|
|
35
|
+
"setup",
|
|
36
|
+
"Deployer",
|
|
37
|
+
"FridaServerError",
|
|
38
|
+
"AdbError",
|
|
39
|
+
"DeviceError",
|
|
40
|
+
"DownloadError",
|
|
41
|
+
"RootAccessError",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
__version__ = "1.0.0"
|
|
45
|
+
__author__ = "S. SHAJON"
|
|
46
|
+
__license__ = "GPL-3.0-or-later"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def setup(
|
|
50
|
+
*,
|
|
51
|
+
serial: str | None = None,
|
|
52
|
+
update: bool = True,
|
|
53
|
+
port: int = 27042,
|
|
54
|
+
force: bool = False,
|
|
55
|
+
) -> bool:
|
|
56
|
+
"""Convenience wrapper around :meth:`frida_server.deployer.Deployer.run`.
|
|
57
|
+
|
|
58
|
+
Deploys and starts ``frida-server`` on a connected Android device, returning
|
|
59
|
+
``True`` if the server ends up running. See :meth:`Deployer.run` for the
|
|
60
|
+
full argument reference.
|
|
61
|
+
"""
|
|
62
|
+
from .deployer import Deployer
|
|
63
|
+
|
|
64
|
+
return Deployer().run(serial=serial, update=update, port=port, force=force)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def __getattr__(name: str):
|
|
68
|
+
# Lazily expose Deployer so ``import frida_server`` stays cheap.
|
|
69
|
+
if name == "Deployer":
|
|
70
|
+
from .deployer import Deployer
|
|
71
|
+
|
|
72
|
+
return Deployer
|
|
73
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
frida_server/__main__.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Local cache of known-good ``frida-server`` SHA-256 digests.
|
|
2
|
+
|
|
3
|
+
Because several devices of different architectures may be used from the same
|
|
4
|
+
machine, the cache is **grouped by architecture** — each arch keeps its own map
|
|
5
|
+
of Frida version → digest::
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
"android-arm64": { "17.17.0": "55ef78c3…" },
|
|
9
|
+
"android-x86_64": { "17.17.0": "b34a33bd…" }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
When a binary is downloaded and pushed, its SHA-256 is recorded under that
|
|
13
|
+
device's arch. On later runs the on-device binary's digest is compared against
|
|
14
|
+
the entry for its arch to detect a corrupt, tampered, or foreign server and
|
|
15
|
+
trigger a re-deploy — going beyond a plain version-string check.
|
|
16
|
+
|
|
17
|
+
The store lives at ``~/.frida-server/checksums.json`` and degrades gracefully:
|
|
18
|
+
a missing, corrupt, or legacy-format file is treated as empty.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
import json
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ChecksumStore:
|
|
29
|
+
"""A JSON-backed map of ``arch`` → (``version`` → SHA-256 hex digest)."""
|
|
30
|
+
|
|
31
|
+
_PATH = Path.home() / ".frida-server" / "checksums.json"
|
|
32
|
+
|
|
33
|
+
def __init__(self, path: Path | None = None) -> None:
|
|
34
|
+
self._path = path or self._PATH
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def file_sha256(path: str | Path) -> str:
|
|
38
|
+
"""Return the SHA-256 hex digest of a local file."""
|
|
39
|
+
digest = hashlib.sha256()
|
|
40
|
+
with open(path, "rb") as fh:
|
|
41
|
+
for block in iter(lambda: fh.read(1 << 20), b""):
|
|
42
|
+
digest.update(block)
|
|
43
|
+
return digest.hexdigest()
|
|
44
|
+
|
|
45
|
+
def _load(self) -> dict[str, dict[str, str]]:
|
|
46
|
+
try:
|
|
47
|
+
with open(self._path, encoding="utf-8") as fh:
|
|
48
|
+
data = json.load(fh)
|
|
49
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
50
|
+
return {}
|
|
51
|
+
# Keep only well-formed ``arch -> {version: digest}`` buckets; anything
|
|
52
|
+
# else (e.g. a legacy flat layout) is ignored and re-established on use.
|
|
53
|
+
return {
|
|
54
|
+
arch: bucket
|
|
55
|
+
for arch, bucket in data.items()
|
|
56
|
+
if isinstance(bucket, dict)
|
|
57
|
+
} if isinstance(data, dict) else {}
|
|
58
|
+
|
|
59
|
+
def get(self, version: str, arch: str) -> str | None:
|
|
60
|
+
"""Return the cached digest for ``version`` on ``arch``, or ``None``."""
|
|
61
|
+
return self._load().get(arch, {}).get(version)
|
|
62
|
+
|
|
63
|
+
def set(self, version: str, arch: str, digest: str) -> None:
|
|
64
|
+
"""Record ``digest`` for ``version`` on ``arch``, creating the store if needed."""
|
|
65
|
+
data = self._load()
|
|
66
|
+
data.setdefault(arch, {})[version] = digest
|
|
67
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
tmp = self._path.with_suffix(".json.tmp")
|
|
69
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
70
|
+
json.dump(data, fh, indent=2, sort_keys=True)
|
|
71
|
+
tmp.replace(self._path) # atomic on the same filesystem
|
frida_server/cli.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Command-line interface for ``frida-server``.
|
|
2
|
+
|
|
3
|
+
:class:`Cli` is a thin wrapper around :class:`~frida_server.deployer.Deployer`:
|
|
4
|
+
parse arguments, translate :class:`~frida_server.exceptions.FridaServerError`
|
|
5
|
+
into a clean exit code, and keep ``Ctrl-C`` quiet.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .console import Console
|
|
15
|
+
from .deployer import Deployer
|
|
16
|
+
from .exceptions import FridaServerError
|
|
17
|
+
|
|
18
|
+
_DESCRIPTION = "Deploy and start frida-server on a connected Android device."
|
|
19
|
+
_EPILOG = (
|
|
20
|
+
"examples:\n"
|
|
21
|
+
" frida-server # pick a device and deploy\n"
|
|
22
|
+
" frida-server -s emulator-5554 # pick a device and deploy\n"
|
|
23
|
+
" frida-server --no-update --port 27043 # pick a device and deploy\n"
|
|
24
|
+
" frida-server --force # re-download even if versions match\n"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Cli:
|
|
29
|
+
"""Parses arguments and drives a :class:`Deployer` run."""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self._console = Console()
|
|
33
|
+
|
|
34
|
+
def build_parser(self) -> argparse.ArgumentParser:
|
|
35
|
+
"""Construct the argument parser (kept separate for testing)."""
|
|
36
|
+
parser = argparse.ArgumentParser(
|
|
37
|
+
prog="frida-server",
|
|
38
|
+
description=_DESCRIPTION,
|
|
39
|
+
epilog=_EPILOG,
|
|
40
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"-s",
|
|
44
|
+
"--serial",
|
|
45
|
+
metavar="SERIAL",
|
|
46
|
+
help="target a specific device serial (default: prompt when several)",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"-p",
|
|
50
|
+
"--port",
|
|
51
|
+
type=int,
|
|
52
|
+
default=27042,
|
|
53
|
+
metavar="PORT",
|
|
54
|
+
help="TCP port to forward after start (default: 27042)",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--no-update",
|
|
58
|
+
dest="update",
|
|
59
|
+
action="store_false",
|
|
60
|
+
help="skip the host-side Frida update check",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--force",
|
|
64
|
+
action="store_true",
|
|
65
|
+
help="re-download and re-push even if a matching server is present",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"-V",
|
|
69
|
+
"--version",
|
|
70
|
+
action="version",
|
|
71
|
+
version=f"%(prog)s {__version__}",
|
|
72
|
+
)
|
|
73
|
+
return parser
|
|
74
|
+
|
|
75
|
+
def run(self, argv: list[str] | None = None) -> int:
|
|
76
|
+
"""Entry point. Returns a process exit code."""
|
|
77
|
+
args = self.build_parser().parse_args(argv)
|
|
78
|
+
try:
|
|
79
|
+
ok = Deployer(self._console).run(
|
|
80
|
+
serial=args.serial,
|
|
81
|
+
update=args.update,
|
|
82
|
+
port=args.port,
|
|
83
|
+
force=args.force,
|
|
84
|
+
)
|
|
85
|
+
return 0 if ok else 1
|
|
86
|
+
except FridaServerError as exc:
|
|
87
|
+
self._console.error(str(exc))
|
|
88
|
+
return 1
|
|
89
|
+
except KeyboardInterrupt:
|
|
90
|
+
print()
|
|
91
|
+
self._console.info("Interrupted.")
|
|
92
|
+
return 130
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main(argv: list[str] | None = None) -> int:
|
|
96
|
+
"""Console-script entry point: run the CLI and return an exit code."""
|
|
97
|
+
return Cli().run(argv)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__": # pragma: no cover
|
|
101
|
+
sys.exit(main())
|
frida_server/console.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Console output.
|
|
2
|
+
|
|
3
|
+
A single :class:`Console` object owns all terminal output for a run, keeping the
|
|
4
|
+
``[*] / [+] / [!] / [-] / [?]`` status style in one place. ANSI colour
|
|
5
|
+
auto-disables when stdout is not a TTY (or when ``NO_COLOR`` is set), so piped
|
|
6
|
+
output stays clean.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Console:
|
|
16
|
+
"""Renders status lines with consistent markers and optional colour."""
|
|
17
|
+
|
|
18
|
+
_COLORS = {
|
|
19
|
+
"info": "36", # cyan
|
|
20
|
+
"success": "32", # green
|
|
21
|
+
"warn": "33", # yellow
|
|
22
|
+
"error": "31", # red
|
|
23
|
+
"prompt": "35", # magenta
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def __init__(self, *, color: bool | None = None) -> None:
|
|
27
|
+
if color is None:
|
|
28
|
+
color = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
|
|
29
|
+
self._color = color
|
|
30
|
+
|
|
31
|
+
def _paint(self, kind: str, text: str) -> str:
|
|
32
|
+
if not self._color:
|
|
33
|
+
return text
|
|
34
|
+
return f"\033[{self._COLORS[kind]}m{text}\033[0m"
|
|
35
|
+
|
|
36
|
+
def info(self, message: str) -> None:
|
|
37
|
+
"""Neutral progress line, e.g. ``[*] Doing a thing``."""
|
|
38
|
+
print(f"{self._paint('info', '[*]')} {message}")
|
|
39
|
+
|
|
40
|
+
def success(self, message: str) -> None:
|
|
41
|
+
"""Positive outcome, e.g. ``[+] Done``."""
|
|
42
|
+
print(f"{self._paint('success', '[+]')} {message}")
|
|
43
|
+
|
|
44
|
+
def warn(self, message: str) -> None:
|
|
45
|
+
"""Non-fatal warning, e.g. ``[!] Heads up``."""
|
|
46
|
+
print(f"{self._paint('warn', '[!]')} {message}")
|
|
47
|
+
|
|
48
|
+
def error(self, message: str) -> None:
|
|
49
|
+
"""Failure line, written to stderr, e.g. ``[-] Broke``."""
|
|
50
|
+
print(f"{self._paint('error', '[-]')} {message}", file=sys.stderr)
|
|
51
|
+
|
|
52
|
+
def prompt(self, message: str) -> str:
|
|
53
|
+
"""Ask the user a question and return the stripped answer."""
|
|
54
|
+
return input(f"{self._paint('prompt', '[?]')} {message}").strip()
|
frida_server/deployer.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""High-level orchestration: the full deploy-and-run flow.
|
|
2
|
+
|
|
3
|
+
:class:`Deployer` ties the layers together — resolve the host Frida version,
|
|
4
|
+
pick a device, compare against what is already on the device, download only when
|
|
5
|
+
needed, then push and launch. It is the single entry point the CLI and library
|
|
6
|
+
users share.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
|
|
14
|
+
from .checksums import ChecksumStore
|
|
15
|
+
from .console import Console
|
|
16
|
+
from .device import Device, DeviceManager
|
|
17
|
+
from .downloader import Downloader
|
|
18
|
+
from .exceptions import DeviceError
|
|
19
|
+
from .host import FridaHost
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Deployer:
|
|
23
|
+
"""Deploys and starts ``frida-server`` on a connected Android device."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, console: Console | None = None) -> None:
|
|
26
|
+
self._console = console or Console()
|
|
27
|
+
self._host = FridaHost(self._console)
|
|
28
|
+
self._manager = DeviceManager(self._console)
|
|
29
|
+
self._checksums = ChecksumStore()
|
|
30
|
+
|
|
31
|
+
def run(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
serial: str | None = None,
|
|
35
|
+
update: bool = True,
|
|
36
|
+
port: int = 27042,
|
|
37
|
+
force: bool = False,
|
|
38
|
+
) -> bool:
|
|
39
|
+
"""Deploy and start ``frida-server`` on a connected Android device.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
serial: target a specific device serial; when ``None`` and several
|
|
43
|
+
are connected, the user is prompted to choose.
|
|
44
|
+
update: when ``True``, offer to update the host Frida install first.
|
|
45
|
+
port: TCP port to forward from PC to device after a successful start.
|
|
46
|
+
force: re-download and re-push even if a matching server exists.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
``True`` if ``frida-server`` ends up running on the device.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
FridaServerError: (or a subclass) on any unrecoverable failure.
|
|
53
|
+
"""
|
|
54
|
+
console = self._console
|
|
55
|
+
console.info("Starting Frida server setup...")
|
|
56
|
+
|
|
57
|
+
# 1. Host Frida version — the server binary must match it exactly.
|
|
58
|
+
version = self._host.maybe_update(interactive=update)
|
|
59
|
+
|
|
60
|
+
# 2. Device discovery / selection.
|
|
61
|
+
self._manager.ensure_adb()
|
|
62
|
+
serials = self._manager.serials()
|
|
63
|
+
if not serials:
|
|
64
|
+
raise DeviceError(
|
|
65
|
+
"No devices connected. Connect an Android device with USB debugging on."
|
|
66
|
+
)
|
|
67
|
+
if serial is not None:
|
|
68
|
+
device = self._manager.get(serial)
|
|
69
|
+
else:
|
|
70
|
+
console.info(f"Found {len(serials)} device(s): {', '.join(serials)}")
|
|
71
|
+
device = self._select_device(self._manager.devices())
|
|
72
|
+
|
|
73
|
+
console.info(f"Selected device: {device.describe()}")
|
|
74
|
+
console.info(f"Using Frida version: {version} (matched to host install).")
|
|
75
|
+
|
|
76
|
+
# 3. Root access up front — every later privileged step depends on it.
|
|
77
|
+
device.ensure_root()
|
|
78
|
+
|
|
79
|
+
# 4. Decide whether the on-device binary can be reused: the version must
|
|
80
|
+
# match AND its SHA-256 must match the digest we cached when we last
|
|
81
|
+
# deployed this version/arch. A mismatch (corrupt/tampered/foreign
|
|
82
|
+
# binary) forces a fresh deploy.
|
|
83
|
+
if not force and self._can_reuse(device, version):
|
|
84
|
+
console.info(f"frida-server v{version} already present — reusing it.")
|
|
85
|
+
return self._finish(device, port, ask_restart=True)
|
|
86
|
+
|
|
87
|
+
# 5. Download + push the matching binary, recording its checksum, then start.
|
|
88
|
+
with tempfile.TemporaryDirectory(prefix="frida-server-") as workdir:
|
|
89
|
+
binary = Downloader(console).download(version, device.arch, workdir)
|
|
90
|
+
digest = ChecksumStore.file_sha256(binary)
|
|
91
|
+
device.push_server(str(binary))
|
|
92
|
+
self._verify_pushed(device, digest)
|
|
93
|
+
self._checksums.set(version, device.arch, digest)
|
|
94
|
+
|
|
95
|
+
return self._finish(device, port)
|
|
96
|
+
|
|
97
|
+
def _can_reuse(self, device: Device, version: str) -> bool:
|
|
98
|
+
"""Return True if the on-device server matches by version and checksum."""
|
|
99
|
+
console = self._console
|
|
100
|
+
existing = device.installed_server_version()
|
|
101
|
+
if existing is None:
|
|
102
|
+
console.info("frida-server not found on device.")
|
|
103
|
+
return False
|
|
104
|
+
if existing != version:
|
|
105
|
+
console.info(
|
|
106
|
+
f"On-device v{existing} differs from host v{version} — replacing."
|
|
107
|
+
)
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
expected = self._checksums.get(version, device.arch)
|
|
111
|
+
if expected is None:
|
|
112
|
+
# No cached digest means we can't prove the on-device binary is
|
|
113
|
+
# genuine — re-deploy a fresh verified copy and establish the cache.
|
|
114
|
+
console.info(
|
|
115
|
+
"No cached checksum for this version — re-deploying to verify."
|
|
116
|
+
)
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
actual = device.server_sha256()
|
|
120
|
+
if actual is None:
|
|
121
|
+
console.warn("Could not read on-device checksum — reusing by version.")
|
|
122
|
+
return True
|
|
123
|
+
if actual != expected:
|
|
124
|
+
console.warn(
|
|
125
|
+
f"On-device checksum mismatch (sha256:{actual[:12]}… vs "
|
|
126
|
+
f"{expected[:12]}…) — re-deploying."
|
|
127
|
+
)
|
|
128
|
+
return False
|
|
129
|
+
console.success(f"Checksum verified (sha256:{actual[:12]}…).")
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
def _confirm_restart(self, pid: str) -> bool:
|
|
133
|
+
"""Ask whether to restart an already-running server. Defaults to no."""
|
|
134
|
+
try:
|
|
135
|
+
answer = self._console.prompt(
|
|
136
|
+
f"frida-server is already running (PID {pid}). Restart? (y/N): "
|
|
137
|
+
).lower()
|
|
138
|
+
except (KeyboardInterrupt, EOFError):
|
|
139
|
+
print()
|
|
140
|
+
return False
|
|
141
|
+
return answer == "y"
|
|
142
|
+
|
|
143
|
+
def _verify_pushed(self, device: Device, digest: str) -> None:
|
|
144
|
+
"""Confirm the freshly pushed binary hashes to the expected digest."""
|
|
145
|
+
actual = device.server_sha256()
|
|
146
|
+
if actual is None:
|
|
147
|
+
return # sha256sum unavailable — cannot verify, but push reported OK
|
|
148
|
+
if actual == digest:
|
|
149
|
+
self._console.success(f"Pushed binary verified (sha256:{digest[:12]}…).")
|
|
150
|
+
else:
|
|
151
|
+
self._console.warn(
|
|
152
|
+
f"Pushed binary checksum mismatch (sha256:{actual[:12]}… vs "
|
|
153
|
+
f"{digest[:12]}…) — the transfer may be corrupt."
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _select_device(self, devices: list[Device]) -> Device:
|
|
157
|
+
"""Return the chosen device, prompting the user when several are present."""
|
|
158
|
+
if len(devices) == 1:
|
|
159
|
+
return devices[0]
|
|
160
|
+
|
|
161
|
+
self._console.info("Multiple devices detected. Select one:\n")
|
|
162
|
+
for index, device in enumerate(devices, 1):
|
|
163
|
+
print(f" {index}. {device.describe()}")
|
|
164
|
+
print()
|
|
165
|
+
|
|
166
|
+
while True:
|
|
167
|
+
try:
|
|
168
|
+
choice = self._console.prompt(f"Select device (1-{len(devices)}): ")
|
|
169
|
+
idx = int(choice) - 1
|
|
170
|
+
if 0 <= idx < len(devices):
|
|
171
|
+
return devices[idx]
|
|
172
|
+
self._console.error(f"Enter a number between 1 and {len(devices)}.")
|
|
173
|
+
except ValueError:
|
|
174
|
+
self._console.error("Please enter a valid number.")
|
|
175
|
+
except (KeyboardInterrupt, EOFError):
|
|
176
|
+
self._console.info("Cancelled.")
|
|
177
|
+
sys.exit(0)
|
|
178
|
+
|
|
179
|
+
def _finish(self, device: Device, port: int, *, ask_restart: bool = False) -> bool:
|
|
180
|
+
"""Start the server (or keep a running one), forward the port, and report.
|
|
181
|
+
|
|
182
|
+
When ``ask_restart`` is set and a ``frida-server`` is already running, the
|
|
183
|
+
user is asked whether to restart it. The default answer is *no* (an empty
|
|
184
|
+
line, EOF, or Ctrl-C all mean "keep the running server"), so the existing
|
|
185
|
+
process is left untouched and only the port forward is (re)applied.
|
|
186
|
+
"""
|
|
187
|
+
console = self._console
|
|
188
|
+
|
|
189
|
+
if ask_restart:
|
|
190
|
+
running = device.running_pids()
|
|
191
|
+
if running and not self._confirm_restart(running[0]):
|
|
192
|
+
console.info("Keeping the running frida-server.")
|
|
193
|
+
device.forward_port(port)
|
|
194
|
+
console.success("Frida server is ready! You can now attach with Frida.")
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
if device.start_server():
|
|
198
|
+
device.forward_port(port)
|
|
199
|
+
console.success("Frida server is ready! You can now attach with Frida.")
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
console.error("frida-server may not have started correctly.")
|
|
203
|
+
log = device.read_server_log()
|
|
204
|
+
if log:
|
|
205
|
+
console.error(f"Server output:\n{log}")
|
|
206
|
+
console.error(
|
|
207
|
+
f"Try manually: adb -s {device.serial} shell "
|
|
208
|
+
"/data/local/tmp/frida-server (to see the error output)"
|
|
209
|
+
)
|
|
210
|
+
return False
|