AndroidFridaManager 1.9.0__tar.gz → 1.9.2__tar.gz
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.
- androidfridamanager-1.9.2/AndroidFridaManager/FridaManager.py +804 -0
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/AndroidFridaManager/about.py +1 -1
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/AndroidFridaManager/job_manager.py +107 -34
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/AndroidFridaManager.egg-info/PKG-INFO +2 -2
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/PKG-INFO +2 -2
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/README.md +1 -1
- androidfridamanager-1.9.0/AndroidFridaManager/FridaManager.py +0 -419
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/AndroidFridaManager/__init__.py +0 -0
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/AndroidFridaManager/job.py +0 -0
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/AndroidFridaManager.egg-info/SOURCES.txt +0 -0
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/AndroidFridaManager.egg-info/dependency_links.txt +0 -0
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/AndroidFridaManager.egg-info/entry_points.txt +0 -0
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/AndroidFridaManager.egg-info/requires.txt +0 -0
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/AndroidFridaManager.egg-info/top_level.txt +0 -0
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/LICENSE +0 -0
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/MANIFEST.in +0 -0
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/pyproject.toml +0 -0
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/requirements.txt +0 -0
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/setup.cfg +0 -0
- {androidfridamanager-1.9.0 → androidfridamanager-1.9.2}/setup.py +0 -0
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import frida
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import logging
|
|
8
|
+
from colorlog import ColoredFormatter
|
|
9
|
+
import subprocess
|
|
10
|
+
import requests
|
|
11
|
+
import lzma
|
|
12
|
+
import re
|
|
13
|
+
import warnings
|
|
14
|
+
from shutil import copyfile
|
|
15
|
+
import tempfile
|
|
16
|
+
import argparse
|
|
17
|
+
import shutil
|
|
18
|
+
from typing import Optional, List, Dict, Tuple
|
|
19
|
+
|
|
20
|
+
# some parts are taken from ttps://github.com/Mind0xP/Frida-Python-Binding/
|
|
21
|
+
|
|
22
|
+
class FridaManager():
|
|
23
|
+
|
|
24
|
+
def __init__(self, is_remote=False, socket="", verbose=False, frida_install_dst="/data/local/tmp/", device_serial: Optional[str] = None):
|
|
25
|
+
"""
|
|
26
|
+
Constructor of the current FridaManager instance
|
|
27
|
+
|
|
28
|
+
:param is_remote: Whether to use remote Frida connection.
|
|
29
|
+
:type is_remote: bool
|
|
30
|
+
:param socket: The socket to connect to the remote device. The remote device needs to be set by <ip:port>. By default this string will be empty in order to indicate that FridaManger is working with the first connected USB device.
|
|
31
|
+
:type socket: string
|
|
32
|
+
:param verbose: Set the output to verbose, so that the logging information gets printed. By default set to False.
|
|
33
|
+
:type verbose: bool
|
|
34
|
+
:param frida_install_dst: The path where the frida server should be installed. By default it will be installed to /data/local/tmp/.
|
|
35
|
+
:type frida_install_dst: string
|
|
36
|
+
:param device_serial: Specific device serial to target (e.g., 'emulator-5554'). If None, auto-selects device (prefers emulators when multiple devices connected).
|
|
37
|
+
:type device_serial: Optional[str]
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
self.is_remote = is_remote
|
|
41
|
+
self.device_socket = socket
|
|
42
|
+
self.verbose = verbose
|
|
43
|
+
self.is_magisk_mode = False
|
|
44
|
+
self.is_adb_root_mode = False # LineageOS adb root mode (adbd runs as root)
|
|
45
|
+
self.frida_install_dst = frida_install_dst
|
|
46
|
+
self._setup_logging()
|
|
47
|
+
self.logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
# Multi-device support
|
|
50
|
+
self._device_serial: Optional[str] = None
|
|
51
|
+
self._is_rooted: Optional[bool] = None # Cached root status
|
|
52
|
+
self._multiple_devices: bool = False
|
|
53
|
+
|
|
54
|
+
# Check if ADB is available
|
|
55
|
+
self._check_adb_availability()
|
|
56
|
+
|
|
57
|
+
# Handle device selection
|
|
58
|
+
if device_serial:
|
|
59
|
+
self._device_serial = device_serial
|
|
60
|
+
self._validate_device(device_serial)
|
|
61
|
+
else:
|
|
62
|
+
self._auto_select_device()
|
|
63
|
+
|
|
64
|
+
if self.is_remote:
|
|
65
|
+
frida.get_device_manager().add_remote_device(self.device_socket)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def device_serial(self) -> Optional[str]:
|
|
69
|
+
"""Get the current target device serial."""
|
|
70
|
+
return self._device_serial
|
|
71
|
+
|
|
72
|
+
@device_serial.setter
|
|
73
|
+
def device_serial(self, serial: str) -> None:
|
|
74
|
+
"""Set the target device serial."""
|
|
75
|
+
self._validate_device(serial)
|
|
76
|
+
self._device_serial = serial
|
|
77
|
+
# Reset cached root status and mode flags for new device
|
|
78
|
+
self._is_rooted = None
|
|
79
|
+
self.is_magisk_mode = False
|
|
80
|
+
self.is_adb_root_mode = False
|
|
81
|
+
|
|
82
|
+
def _setup_logging(self):
|
|
83
|
+
"""
|
|
84
|
+
Setup logging for the current instance of FridaManager
|
|
85
|
+
"""
|
|
86
|
+
logger = logging.getLogger()
|
|
87
|
+
|
|
88
|
+
# Check if the logger already has handlers (i.e., if another project has set it up)
|
|
89
|
+
if not logger.handlers:
|
|
90
|
+
logger.setLevel(logging.INFO)
|
|
91
|
+
color_formatter = ColoredFormatter(
|
|
92
|
+
"%(log_color)s[%(asctime)s] [%(levelname)-4s]%(reset)s - %(message)s",
|
|
93
|
+
datefmt='%d-%m-%y %H:%M:%S',
|
|
94
|
+
reset=True,
|
|
95
|
+
log_colors={
|
|
96
|
+
'DEBUG': 'cyan',
|
|
97
|
+
'INFO': 'green',
|
|
98
|
+
'WARNING': 'bold_yellow',
|
|
99
|
+
'ERROR': 'bold_red',
|
|
100
|
+
'CRITICAL': 'bold_red',
|
|
101
|
+
},
|
|
102
|
+
secondary_log_colors={},
|
|
103
|
+
style='%')
|
|
104
|
+
logging_handler = logging.StreamHandler()
|
|
105
|
+
logging_handler.setFormatter(color_formatter)
|
|
106
|
+
logger.addHandler(logging_handler)
|
|
107
|
+
|
|
108
|
+
def _check_adb_availability(self):
|
|
109
|
+
"""
|
|
110
|
+
Check if ADB is available in the system PATH
|
|
111
|
+
"""
|
|
112
|
+
if not shutil.which("adb"):
|
|
113
|
+
self.logger.info("Error: ADB (Android Debug Bridge) is not found in your system PATH.")
|
|
114
|
+
self.logger.info("Please install Android SDK platform-tools and add it to your PATH:")
|
|
115
|
+
self.logger.info(" - Download from: https://developer.android.com/studio/releases/platform-tools")
|
|
116
|
+
self.logger.info(" - Or install via package manager (e.g., 'brew install android-platform-tools' on macOS)")
|
|
117
|
+
self.logger.info(" - Make sure 'adb' command is accessible from your terminal")
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
|
|
120
|
+
# ==================== Multi-Device Support ====================
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def get_connected_devices(cls) -> List[Dict[str, str]]:
|
|
124
|
+
"""
|
|
125
|
+
Get list of all connected Android devices via ADB.
|
|
126
|
+
|
|
127
|
+
:return: List of device dictionaries with 'serial', 'state', 'type', and 'model' keys
|
|
128
|
+
:rtype: List[Dict[str, str]]
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
result = subprocess.run(
|
|
132
|
+
['adb', 'devices', '-l'],
|
|
133
|
+
capture_output=True,
|
|
134
|
+
text=True,
|
|
135
|
+
timeout=10
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
devices = []
|
|
139
|
+
for line in result.stdout.strip().split('\n')[1:]: # Skip header
|
|
140
|
+
if not line.strip():
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
parts = line.split()
|
|
144
|
+
if len(parts) < 2:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
serial = parts[0]
|
|
148
|
+
state = parts[1]
|
|
149
|
+
|
|
150
|
+
# Parse additional info
|
|
151
|
+
model = ""
|
|
152
|
+
product = ""
|
|
153
|
+
for part in parts[2:]:
|
|
154
|
+
if part.startswith("model:"):
|
|
155
|
+
model = part.split(":", 1)[1]
|
|
156
|
+
elif part.startswith("product:"):
|
|
157
|
+
product = part.split(":", 1)[1]
|
|
158
|
+
|
|
159
|
+
# Determine device type
|
|
160
|
+
device_type = "emulator" if serial.startswith("emulator-") else "physical"
|
|
161
|
+
|
|
162
|
+
devices.append({
|
|
163
|
+
'serial': serial,
|
|
164
|
+
'state': state,
|
|
165
|
+
'type': device_type,
|
|
166
|
+
'model': model or product or serial,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
return devices
|
|
170
|
+
|
|
171
|
+
except subprocess.TimeoutExpired:
|
|
172
|
+
return []
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logging.getLogger(__name__).debug(f"Error getting devices: {e}")
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def get_frida_devices(cls) -> List[Dict[str, str]]:
|
|
179
|
+
"""
|
|
180
|
+
Get list of devices visible to Frida.
|
|
181
|
+
|
|
182
|
+
:return: List of device dictionaries with 'id', 'name', 'type' keys
|
|
183
|
+
:rtype: List[Dict[str, str]]
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
devices = []
|
|
187
|
+
for device in frida.enumerate_devices():
|
|
188
|
+
if device.type in ('usb', 'remote'):
|
|
189
|
+
devices.append({
|
|
190
|
+
'id': device.id,
|
|
191
|
+
'name': device.name,
|
|
192
|
+
'type': device.type,
|
|
193
|
+
})
|
|
194
|
+
return devices
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logging.getLogger(__name__).debug(f"Error enumerating Frida devices: {e}")
|
|
197
|
+
return []
|
|
198
|
+
|
|
199
|
+
def _validate_device(self, serial: str) -> bool:
|
|
200
|
+
"""
|
|
201
|
+
Validate that a device with the given serial is connected.
|
|
202
|
+
|
|
203
|
+
:param serial: Device serial to validate
|
|
204
|
+
:return: True if valid, raises RuntimeError otherwise
|
|
205
|
+
"""
|
|
206
|
+
devices = self.get_connected_devices()
|
|
207
|
+
device_serials = [d['serial'] for d in devices]
|
|
208
|
+
|
|
209
|
+
if serial not in device_serials:
|
|
210
|
+
available = ", ".join(device_serials) if device_serials else "none"
|
|
211
|
+
raise RuntimeError(
|
|
212
|
+
f"Device '{serial}' not found. Available devices: {available}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Check device state
|
|
216
|
+
device = next(d for d in devices if d['serial'] == serial)
|
|
217
|
+
if device['state'] != 'device':
|
|
218
|
+
raise RuntimeError(
|
|
219
|
+
f"Device '{serial}' is not ready (state: {device['state']})"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
def _auto_select_device(self) -> None:
|
|
225
|
+
"""
|
|
226
|
+
Automatically select a device when multiple are connected.
|
|
227
|
+
|
|
228
|
+
Priority:
|
|
229
|
+
1. If only one device, use it
|
|
230
|
+
2. If multiple devices, prefer emulators (they have root by default)
|
|
231
|
+
3. If multiple emulators, use the first one
|
|
232
|
+
"""
|
|
233
|
+
devices = self.get_connected_devices()
|
|
234
|
+
ready_devices = [d for d in devices if d['state'] == 'device']
|
|
235
|
+
|
|
236
|
+
if not ready_devices:
|
|
237
|
+
self.logger.warning("No Android devices connected")
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
if len(ready_devices) == 1:
|
|
241
|
+
self._device_serial = ready_devices[0]['serial']
|
|
242
|
+
self._multiple_devices = False
|
|
243
|
+
if self.verbose:
|
|
244
|
+
self.logger.info(f"[*] Using device: {self._device_serial}")
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
# Multiple devices - prefer emulators
|
|
248
|
+
self._multiple_devices = True
|
|
249
|
+
emulators = [d for d in ready_devices if d['type'] == 'emulator']
|
|
250
|
+
physical = [d for d in ready_devices if d['type'] == 'physical']
|
|
251
|
+
|
|
252
|
+
if emulators:
|
|
253
|
+
self._device_serial = emulators[0]['serial']
|
|
254
|
+
self.logger.info(
|
|
255
|
+
f"[*] Multiple devices connected. Auto-selected emulator: {self._device_serial}"
|
|
256
|
+
)
|
|
257
|
+
if physical:
|
|
258
|
+
self.logger.info(
|
|
259
|
+
f"[*] Physical device(s) also connected: {', '.join(d['serial'] for d in physical)}"
|
|
260
|
+
)
|
|
261
|
+
elif physical:
|
|
262
|
+
# Only physical devices - select first but warn
|
|
263
|
+
self._device_serial = physical[0]['serial']
|
|
264
|
+
self.logger.warning(
|
|
265
|
+
f"[*] Multiple physical devices connected. Selected: {self._device_serial}. "
|
|
266
|
+
"Note: Physical devices require root for full functionality."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _build_adb_command(self, args: List[str]) -> List[str]:
|
|
270
|
+
"""
|
|
271
|
+
Build ADB command with device targeting if needed.
|
|
272
|
+
|
|
273
|
+
:param args: ADB command arguments (without 'adb' prefix)
|
|
274
|
+
:return: Complete command list including device targeting
|
|
275
|
+
"""
|
|
276
|
+
cmd = ['adb']
|
|
277
|
+
if self._device_serial and self._multiple_devices:
|
|
278
|
+
cmd.extend(['-s', self._device_serial])
|
|
279
|
+
cmd.extend(args)
|
|
280
|
+
return cmd
|
|
281
|
+
|
|
282
|
+
def get_frida_device(self):
|
|
283
|
+
"""
|
|
284
|
+
Get the Frida device object for the current target device.
|
|
285
|
+
|
|
286
|
+
:return: Frida Device object
|
|
287
|
+
:raises RuntimeError: If device cannot be found
|
|
288
|
+
"""
|
|
289
|
+
if self.is_remote:
|
|
290
|
+
return frida.get_device_manager().add_remote_device(self.device_socket)
|
|
291
|
+
|
|
292
|
+
if self._device_serial:
|
|
293
|
+
try:
|
|
294
|
+
# Try to get device by ID (matches ADB serial for USB devices)
|
|
295
|
+
return frida.get_device(self._device_serial)
|
|
296
|
+
except frida.InvalidArgumentError:
|
|
297
|
+
# Fallback: enumerate and find by ID
|
|
298
|
+
for device in frida.enumerate_devices():
|
|
299
|
+
if device.id == self._device_serial:
|
|
300
|
+
return device
|
|
301
|
+
raise RuntimeError(f"Frida device '{self._device_serial}' not found")
|
|
302
|
+
else:
|
|
303
|
+
# Fallback to get_usb_device for single device scenario
|
|
304
|
+
return frida.get_usb_device()
|
|
305
|
+
|
|
306
|
+
# ==================== Frida Server Management ====================
|
|
307
|
+
|
|
308
|
+
def run_frida_server(self, frida_server_path="/data/local/tmp/"):
|
|
309
|
+
# Check if frida-server is already running
|
|
310
|
+
if self.is_frida_server_running():
|
|
311
|
+
if self.verbose:
|
|
312
|
+
self.logger.info("[*] frida-server is already running, skipping start")
|
|
313
|
+
return True
|
|
314
|
+
|
|
315
|
+
# Ensure root access is available
|
|
316
|
+
if not self.is_device_rooted():
|
|
317
|
+
self.logger.error("Cannot start frida-server: device is not rooted")
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
if frida_server_path is self.run_frida_server.__defaults__[0]:
|
|
321
|
+
frida_bin = self.frida_install_dst + "frida-server"
|
|
322
|
+
else:
|
|
323
|
+
frida_bin = frida_server_path + "frida-server"
|
|
324
|
+
|
|
325
|
+
# Build the command based on root mode
|
|
326
|
+
if self.is_adb_root_mode:
|
|
327
|
+
# ADB root mode (LineageOS): run directly, adbd is already root
|
|
328
|
+
shell_cmd = f"{frida_bin} &"
|
|
329
|
+
elif self.is_magisk_mode:
|
|
330
|
+
# Magisk mode
|
|
331
|
+
shell_cmd = f"""su -c 'sh -c "{frida_bin} &"'"""
|
|
332
|
+
else:
|
|
333
|
+
# Traditional su mode
|
|
334
|
+
shell_cmd = f"""su 0 sh -c "{frida_bin} &" """
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
adb_cmd = self._build_adb_command(['shell', shell_cmd])
|
|
338
|
+
process = subprocess.Popen(
|
|
339
|
+
adb_cmd,
|
|
340
|
+
stdout=subprocess.DEVNULL,
|
|
341
|
+
stderr=subprocess.DEVNULL,
|
|
342
|
+
start_new_session=True
|
|
343
|
+
)
|
|
344
|
+
# Give it a moment to start and potentially fail
|
|
345
|
+
import time
|
|
346
|
+
time.sleep(1)
|
|
347
|
+
|
|
348
|
+
# Check if process failed immediately
|
|
349
|
+
if process.poll() is not None:
|
|
350
|
+
stdout, stderr = process.communicate()
|
|
351
|
+
stderr_text = stderr.decode() if isinstance(stderr, bytes) else str(stderr or "")
|
|
352
|
+
if "Address already in use" in stderr_text:
|
|
353
|
+
self.logger.info("[*] frida-server is already running on the device")
|
|
354
|
+
return True
|
|
355
|
+
else:
|
|
356
|
+
self.logger.error(f"Failed to start frida-server: {stderr_text}")
|
|
357
|
+
return False
|
|
358
|
+
else:
|
|
359
|
+
# Process is still running (background), which is expected for frida-server
|
|
360
|
+
if self.verbose:
|
|
361
|
+
self.logger.info("[*] frida-server started successfully in background")
|
|
362
|
+
|
|
363
|
+
if self.is_frida_server_running():
|
|
364
|
+
return True
|
|
365
|
+
else:
|
|
366
|
+
self.logger.error("frida-server does not seem to be running after start command")
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
except Exception as e:
|
|
370
|
+
self.logger.error(f"Error starting frida-server: {e}")
|
|
371
|
+
return False
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def is_frida_server_running(self) -> bool:
|
|
375
|
+
"""
|
|
376
|
+
Checks if on the connected device a frida server is running.
|
|
377
|
+
|
|
378
|
+
This method first tries non-root commands, then falls back to root commands
|
|
379
|
+
if available. Safe to call on non-rooted devices (returns False).
|
|
380
|
+
|
|
381
|
+
:return: True if a frida-server is running otherwise False.
|
|
382
|
+
:rtype: bool
|
|
383
|
+
"""
|
|
384
|
+
try:
|
|
385
|
+
# Method 1: Try pidof without root (works on some devices)
|
|
386
|
+
result = self._run_adb_shell_command("pidof frida-server")
|
|
387
|
+
if result.stdout.strip():
|
|
388
|
+
try:
|
|
389
|
+
int(result.stdout.strip().split()[0]) # Validate it's a number
|
|
390
|
+
return True
|
|
391
|
+
except (ValueError, IndexError):
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
# Method 2: Try ps -A without root
|
|
395
|
+
result = self._run_adb_shell_command("ps -A 2>/dev/null | grep frida-server | grep -v grep")
|
|
396
|
+
if result.stdout.strip():
|
|
397
|
+
return True
|
|
398
|
+
|
|
399
|
+
# Method 3: Try with root if available
|
|
400
|
+
if self.is_device_rooted():
|
|
401
|
+
result = self.run_adb_command_as_root("pidof frida-server")
|
|
402
|
+
if result.stdout.strip():
|
|
403
|
+
return True
|
|
404
|
+
|
|
405
|
+
result = self.run_adb_command_as_root("ps | grep frida-server | grep -v grep")
|
|
406
|
+
if result.stdout.strip():
|
|
407
|
+
return True
|
|
408
|
+
|
|
409
|
+
return False
|
|
410
|
+
|
|
411
|
+
except Exception as e:
|
|
412
|
+
self.logger.debug(f"Error checking frida-server status: {e}")
|
|
413
|
+
return False
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def stop_frida_server(self):
|
|
417
|
+
if self.is_device_rooted():
|
|
418
|
+
self.run_adb_command_as_root("/system/bin/killall frida-server")
|
|
419
|
+
else:
|
|
420
|
+
self.logger.warning("Cannot stop frida-server: device not rooted")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def remove_frida_server(self, frida_server_path="/data/local/tmp/"):
|
|
424
|
+
if frida_server_path is self.remove_frida_server.__defaults__[0]:
|
|
425
|
+
cmd = self.frida_install_dst + "frida-server"
|
|
426
|
+
else:
|
|
427
|
+
cmd = frida_server_path + "frida-server"
|
|
428
|
+
|
|
429
|
+
self.stop_frida_server()
|
|
430
|
+
self._adb_remove_file_if_exist(cmd)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def install_frida_server(self, dst_dir="/data/local/tmp/", version="latest"):
|
|
434
|
+
"""
|
|
435
|
+
Install the frida server binary on the Android device.
|
|
436
|
+
This includes downloading the frida-server, decompress it and pushing it to the Android device.
|
|
437
|
+
By default it is pushed into the /data/local/tmp/ directory.
|
|
438
|
+
Further the binary will be set to executable in order to run it.
|
|
439
|
+
|
|
440
|
+
:param dst_dir: The destination folder where the frida-server binary should be installed (pushed).
|
|
441
|
+
:type dst_dir: string
|
|
442
|
+
:param version: The version. By default the latest version will be used.
|
|
443
|
+
:type version: string
|
|
444
|
+
|
|
445
|
+
"""
|
|
446
|
+
if dst_dir is self.install_frida_server.__defaults__[0]:
|
|
447
|
+
frida_dir = self.frida_install_dst
|
|
448
|
+
else:
|
|
449
|
+
frida_dir = dst_dir
|
|
450
|
+
|
|
451
|
+
with tempfile.TemporaryDirectory() as dir:
|
|
452
|
+
if self.verbose:
|
|
453
|
+
self.logger.info(f"[*] downloading frida-server to {dir}")
|
|
454
|
+
file_path = self.download_frida_server(dir,version)
|
|
455
|
+
tmp_frida_server = self.extract_frida_server_comp(file_path)
|
|
456
|
+
# ensure's that we always overwrite the current installation with our recent downloaded version
|
|
457
|
+
self._adb_remove_file_if_exist(frida_dir + "frida-server")
|
|
458
|
+
if self.verbose:
|
|
459
|
+
self.logger.info(f"[*] pushing frida-server to {frida_dir}")
|
|
460
|
+
self._adb_push_file(tmp_frida_server,frida_dir)
|
|
461
|
+
self.make_frida_server_executable()
|
|
462
|
+
return True
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# by default the latest frida-server version will be downloaded
|
|
466
|
+
def download_frida_server(self, path, version="latest"):
|
|
467
|
+
"""
|
|
468
|
+
Downloads a frida server. By default the latest version is used.
|
|
469
|
+
If you want to download a specific version you have to provide it trough the version parameter.
|
|
470
|
+
|
|
471
|
+
:param path: The path where the compressed frida-server should be downloded.
|
|
472
|
+
:type path: string
|
|
473
|
+
:param version: The version. By default the latest version will be used.
|
|
474
|
+
:type version: string
|
|
475
|
+
|
|
476
|
+
:return: The location of the downloaded frida server in its compressed form.
|
|
477
|
+
:rtype: string
|
|
478
|
+
"""
|
|
479
|
+
url = self.get_frida_server_for_android_url(version)
|
|
480
|
+
with open(path+"/frida-server","wb") as fsb:
|
|
481
|
+
res = requests.get(url)
|
|
482
|
+
fsb.write(res.content)
|
|
483
|
+
if self.verbose:
|
|
484
|
+
self.logger.info(f"[*] writing frida-server to {path}")
|
|
485
|
+
|
|
486
|
+
return path+"/frida-server"
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def extract_frida_server_comp(self, file_path):
|
|
491
|
+
if self.verbose:
|
|
492
|
+
self.logger.info(f"[*] extracting {file_path} ...")
|
|
493
|
+
# create a subdir for the specified filename
|
|
494
|
+
frida_server_dir = file_path[:-3]
|
|
495
|
+
os.makedirs(frida_server_dir)
|
|
496
|
+
with lzma.open(file_path, 'rb') as f:
|
|
497
|
+
decompressed_file = f.read()
|
|
498
|
+
with open(frida_server_dir+'/frida-server', 'wb') as f:
|
|
499
|
+
f.write(decompressed_file)
|
|
500
|
+
|
|
501
|
+
# del compressed file
|
|
502
|
+
os.remove(file_path)
|
|
503
|
+
return frida_server_dir+"/frida-server"
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def get_frida_server_for_android_url(self, version):
|
|
507
|
+
arch = self._get_android_device_arch()
|
|
508
|
+
|
|
509
|
+
if self.verbose:
|
|
510
|
+
self.logger.info(f"[*] Android architecture: {arch}")
|
|
511
|
+
arch_str = "x86"
|
|
512
|
+
|
|
513
|
+
if arch == "arm64":
|
|
514
|
+
arch_str = "arm64"
|
|
515
|
+
elif arch == "arm":
|
|
516
|
+
arch_str = "arm"
|
|
517
|
+
elif arch == "ia32":
|
|
518
|
+
arch_str = "x86"
|
|
519
|
+
elif arch == "x64":
|
|
520
|
+
arch_str = "x86_64"
|
|
521
|
+
else:
|
|
522
|
+
arch_str = "x86"
|
|
523
|
+
|
|
524
|
+
if self.verbose:
|
|
525
|
+
self.logger.info(f"[*] Android architecture string: {arch_str}")
|
|
526
|
+
|
|
527
|
+
download_url = self._get_frida_server_donwload_url(arch_str,version)
|
|
528
|
+
return download_url
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _get_frida_server_donwload_url(self, arch, version):
|
|
532
|
+
frida_download_prefix = "https://github.com/frida/frida/releases"
|
|
533
|
+
|
|
534
|
+
if version == "latest":
|
|
535
|
+
url = "https://api.github.com/repos/frida/frida/releases/"+version
|
|
536
|
+
|
|
537
|
+
try:
|
|
538
|
+
res = requests.get(url)
|
|
539
|
+
except requests.exceptions.RequestException as e:
|
|
540
|
+
self.logger.error(f"Error making request to {url}: {e}")
|
|
541
|
+
raise RuntimeError(f"Failed to fetch Frida release information: {e}")
|
|
542
|
+
|
|
543
|
+
with warnings.catch_warnings():
|
|
544
|
+
warnings.simplefilter("ignore", SyntaxWarning)
|
|
545
|
+
try:
|
|
546
|
+
frida_server_path = re.findall(r'\/download\/\d+\.\d+\.\d+\/frida\-server\-\d+\.\d+\.\d+\-android\-'+arch+'\.xz',res.text)
|
|
547
|
+
except SyntaxWarning:
|
|
548
|
+
frida_server_path = re.findall(r'/download/\d+\.\d+\.\d+/frida-server-\d+\.\d+\.\d+-android-' + arch + r'\.xz', res.text)
|
|
549
|
+
|
|
550
|
+
final_url = frida_download_prefix + frida_server_path[0]
|
|
551
|
+
|
|
552
|
+
else:
|
|
553
|
+
final_url = "https://github.com/frida/frida/releases/download/"+ version +"/frida-server-"+version+"-android-"+arch+".xz"
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
if self.verbose:
|
|
557
|
+
self.logger.info(f"[*] frida-server download url: {final_url}")
|
|
558
|
+
|
|
559
|
+
return final_url
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def make_frida_server_executable(self, frida_server_path="/data/local/tmp/"):
|
|
563
|
+
if frida_server_path is self.make_frida_server_executable.__defaults__[0]:
|
|
564
|
+
cmd = self.frida_install_dst + "frida-server"
|
|
565
|
+
else:
|
|
566
|
+
cmd = frida_server_path + "frida-server"
|
|
567
|
+
|
|
568
|
+
final_cmd = "chmod +x "+cmd
|
|
569
|
+
if self.verbose:
|
|
570
|
+
self.logger.info(f"[*] making frida-server executable: {final_cmd}")
|
|
571
|
+
|
|
572
|
+
self.run_adb_command_as_root(f"chmod +x {cmd}")
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
### some functions to work with adb ###
|
|
577
|
+
|
|
578
|
+
def _run_adb_shell_command(self, command: str) -> subprocess.CompletedProcess:
|
|
579
|
+
"""
|
|
580
|
+
Run an ADB shell command (without root) on the target device.
|
|
581
|
+
|
|
582
|
+
:param command: Shell command to run
|
|
583
|
+
:return: subprocess.CompletedProcess with stdout/stderr
|
|
584
|
+
"""
|
|
585
|
+
adb_cmd = self._build_adb_command(['shell', command])
|
|
586
|
+
return subprocess.run(adb_cmd, capture_output=True, text=True)
|
|
587
|
+
|
|
588
|
+
def run_adb_command_as_root(self, command: str) -> subprocess.CompletedProcess:
|
|
589
|
+
"""
|
|
590
|
+
Run an ADB command as root on the target device.
|
|
591
|
+
|
|
592
|
+
Supports three root modes:
|
|
593
|
+
1. ADB root mode (LineageOS): Commands run directly (adbd is root)
|
|
594
|
+
2. Magisk mode: Uses 'su -c command'
|
|
595
|
+
3. Traditional su: Uses 'su 0 command'
|
|
596
|
+
|
|
597
|
+
:param command: Command to run as root
|
|
598
|
+
:return: subprocess.CompletedProcess with stdout/stderr
|
|
599
|
+
:raises RuntimeError: If device is not rooted
|
|
600
|
+
"""
|
|
601
|
+
if not self.is_device_rooted():
|
|
602
|
+
self.logger.error("Device is not rooted. Please root it before using FridaAndroidManager and ensure that you are able to run commands with the su-binary or enable ADB root mode.")
|
|
603
|
+
raise RuntimeError("Device not rooted or su binary not accessible")
|
|
604
|
+
|
|
605
|
+
if self.is_adb_root_mode:
|
|
606
|
+
# ADB root mode (LineageOS): adbd runs as root, no su needed
|
|
607
|
+
adb_cmd = self._build_adb_command(['shell', command])
|
|
608
|
+
elif self.is_magisk_mode:
|
|
609
|
+
# Magisk mode: use su -c
|
|
610
|
+
adb_cmd = self._build_adb_command(['shell', f'su -c {command}'])
|
|
611
|
+
else:
|
|
612
|
+
# Traditional su mode
|
|
613
|
+
adb_cmd = self._build_adb_command(['shell', f'su 0 {command}'])
|
|
614
|
+
|
|
615
|
+
return subprocess.run(adb_cmd, capture_output=True, text=True)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _adb_push_file(self, file: str, dst: str) -> subprocess.CompletedProcess:
|
|
619
|
+
"""Push a file to the device."""
|
|
620
|
+
adb_cmd = self._build_adb_command(['push', file, dst])
|
|
621
|
+
return subprocess.run(adb_cmd, capture_output=True, text=True)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _adb_pull_file(self, src_file: str, dst: str) -> subprocess.CompletedProcess:
|
|
625
|
+
"""Pull a file from the device."""
|
|
626
|
+
adb_cmd = self._build_adb_command(['pull', src_file, dst])
|
|
627
|
+
return subprocess.run(adb_cmd, capture_output=True, text=True)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _get_android_device_arch(self) -> str:
|
|
631
|
+
"""Get the architecture of the target Android device."""
|
|
632
|
+
try:
|
|
633
|
+
device = self.get_frida_device()
|
|
634
|
+
return device.query_system_parameters()['arch']
|
|
635
|
+
except Exception as e:
|
|
636
|
+
self.logger.warning(f"Failed to get arch via Frida, falling back to ADB: {e}")
|
|
637
|
+
# Fallback to ADB
|
|
638
|
+
result = self._run_adb_shell_command("getprop ro.product.cpu.abi")
|
|
639
|
+
abi = result.stdout.strip()
|
|
640
|
+
# Map ABI to Frida arch
|
|
641
|
+
if 'arm64' in abi or 'aarch64' in abi:
|
|
642
|
+
return 'arm64'
|
|
643
|
+
elif 'armeabi' in abi or 'arm' in abi:
|
|
644
|
+
return 'arm'
|
|
645
|
+
elif 'x86_64' in abi:
|
|
646
|
+
return 'x64'
|
|
647
|
+
elif 'x86' in abi:
|
|
648
|
+
return 'ia32'
|
|
649
|
+
return 'arm64' # Default
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _adb_make_binary_executable(self, path):
|
|
653
|
+
output = self.run_adb_command_as_root("chmod +x "+path)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _adb_does_file_exist(self, path: str) -> bool:
|
|
657
|
+
"""Check if a file exists on the device."""
|
|
658
|
+
if self.is_device_rooted():
|
|
659
|
+
output = self.run_adb_command_as_root("ls " + path)
|
|
660
|
+
return len(output.stderr) <= 1
|
|
661
|
+
else:
|
|
662
|
+
output = self._run_adb_shell_command(f"ls {path} 2>/dev/null")
|
|
663
|
+
return len(output.stderr) <= 1 and output.stdout.strip()
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def is_device_rooted(self) -> bool:
|
|
667
|
+
"""
|
|
668
|
+
Check if the target device has root access.
|
|
669
|
+
Caches result after first check.
|
|
670
|
+
|
|
671
|
+
:return: True if device is rooted, False otherwise
|
|
672
|
+
"""
|
|
673
|
+
if self._is_rooted is not None:
|
|
674
|
+
return self._is_rooted
|
|
675
|
+
|
|
676
|
+
self._is_rooted = self.adb_check_root()
|
|
677
|
+
return self._is_rooted
|
|
678
|
+
|
|
679
|
+
def adb_check_root(self) -> bool:
|
|
680
|
+
"""
|
|
681
|
+
Check if the device has root access.
|
|
682
|
+
|
|
683
|
+
Supports three root modes:
|
|
684
|
+
1. ADB root mode (LineageOS): adbd runs as root, no su needed
|
|
685
|
+
2. Magisk mode: su -c command
|
|
686
|
+
3. Traditional su: su 0 command
|
|
687
|
+
|
|
688
|
+
:return: True if root is available, False otherwise
|
|
689
|
+
"""
|
|
690
|
+
try:
|
|
691
|
+
# First, check for ADB root mode (LineageOS with "adb root" enabled)
|
|
692
|
+
# In this mode, adbd runs as root and all shell commands are root
|
|
693
|
+
result = subprocess.run(
|
|
694
|
+
self._build_adb_command(['shell', 'id', '-u']),
|
|
695
|
+
capture_output=True,
|
|
696
|
+
text=True,
|
|
697
|
+
timeout=5
|
|
698
|
+
)
|
|
699
|
+
if result.stdout.strip() == "0":
|
|
700
|
+
# Shell is already running as root (adb root mode)
|
|
701
|
+
self.is_adb_root_mode = True
|
|
702
|
+
self.is_magisk_mode = False
|
|
703
|
+
if self.verbose:
|
|
704
|
+
self.logger.info("[*] Detected ADB root mode (adbd running as root)")
|
|
705
|
+
return True
|
|
706
|
+
|
|
707
|
+
# Try Magisk-style su
|
|
708
|
+
result = subprocess.run(
|
|
709
|
+
self._build_adb_command(['shell', 'su -v']),
|
|
710
|
+
capture_output=True,
|
|
711
|
+
text=True,
|
|
712
|
+
timeout=5
|
|
713
|
+
)
|
|
714
|
+
if result.stdout.strip():
|
|
715
|
+
self.is_magisk_mode = True
|
|
716
|
+
self.is_adb_root_mode = False
|
|
717
|
+
if self.verbose:
|
|
718
|
+
self.logger.info("[*] Detected Magisk root mode")
|
|
719
|
+
return True
|
|
720
|
+
|
|
721
|
+
# Try traditional su
|
|
722
|
+
result = subprocess.run(
|
|
723
|
+
self._build_adb_command(['shell', 'su 0 id -u']),
|
|
724
|
+
capture_output=True,
|
|
725
|
+
text=True,
|
|
726
|
+
timeout=5
|
|
727
|
+
)
|
|
728
|
+
if result.stdout.strip() == "0":
|
|
729
|
+
self.is_magisk_mode = False
|
|
730
|
+
self.is_adb_root_mode = False
|
|
731
|
+
if self.verbose:
|
|
732
|
+
self.logger.info("[*] Detected traditional su root mode")
|
|
733
|
+
return True
|
|
734
|
+
|
|
735
|
+
return False
|
|
736
|
+
|
|
737
|
+
except subprocess.TimeoutExpired:
|
|
738
|
+
self.logger.debug("Root check timed out")
|
|
739
|
+
return False
|
|
740
|
+
except Exception as e:
|
|
741
|
+
self.logger.debug(f"Root check failed: {e}")
|
|
742
|
+
return False
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _adb_remove_file_if_exist(self, path="/data/local/tmp/frida-server"):
|
|
746
|
+
if self._adb_does_file_exist(path):
|
|
747
|
+
if self.is_device_rooted():
|
|
748
|
+
self.run_adb_command_as_root("rm " + path)
|
|
749
|
+
else:
|
|
750
|
+
self._run_adb_shell_command(f"rm {path}")
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def main():
|
|
754
|
+
if len(sys.argv) > 1:
|
|
755
|
+
parser = argparse.ArgumentParser(description='FridaManager initialization parameters.')
|
|
756
|
+
|
|
757
|
+
parser.add_argument('--is_remote', type=lambda x: (str(x).lower() == 'true'), default=False, help='Whether to use Frida in remote mode. Default is False.')
|
|
758
|
+
parser.add_argument('--socket', type=str, default="", help='Socket to use for the connection. Expected in the format <ip:port>.')
|
|
759
|
+
parser.add_argument('--verbose', required=False, action="store_const", const=True, default=False, help='Enable verbose output. Default is False.')
|
|
760
|
+
parser.add_argument('--frida_install_dst', type=str, default="/data/local/tmp/", help='Frida installation destination. Default is "/data/local/tmp/".')
|
|
761
|
+
parser.add_argument('-r','--is_running', required=False, action="store_const", const=True, default=False, help='Checks only if frida-server is running on the Android device or not.')
|
|
762
|
+
parser.add_argument('-d', '--device', type=str, default=None, help='Target device serial (e.g., emulator-5554). Auto-selects if not specified.')
|
|
763
|
+
parser.add_argument('-l', '--list-devices', required=False, action="store_const", const=True, default=False, help='List all connected devices and exit.')
|
|
764
|
+
|
|
765
|
+
args = parser.parse_args()
|
|
766
|
+
|
|
767
|
+
# List devices mode
|
|
768
|
+
if args.list_devices:
|
|
769
|
+
devices = FridaManager.get_connected_devices()
|
|
770
|
+
if not devices:
|
|
771
|
+
print("No devices connected")
|
|
772
|
+
else:
|
|
773
|
+
print(f"{'Serial':<20} {'Type':<10} {'State':<12} {'Model'}")
|
|
774
|
+
print("-" * 60)
|
|
775
|
+
for d in devices:
|
|
776
|
+
print(f"{d['serial']:<20} {d['type']:<10} {d['state']:<12} {d['model']}")
|
|
777
|
+
sys.exit(0)
|
|
778
|
+
|
|
779
|
+
if args.is_running:
|
|
780
|
+
afm_obj = FridaManager(device_serial=args.device)
|
|
781
|
+
if afm_obj.is_frida_server_running():
|
|
782
|
+
afm_obj.logger.info("[*] frida-server is running on Android device")
|
|
783
|
+
else:
|
|
784
|
+
afm_obj.logger.info("[*] frida-server is not running on Android device")
|
|
785
|
+
|
|
786
|
+
sys.exit()
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
afm_obj = FridaManager(args.is_remote, args.socket, args.verbose, args.frida_install_dst, device_serial=args.device)
|
|
791
|
+
else:
|
|
792
|
+
afm_obj = FridaManager()
|
|
793
|
+
|
|
794
|
+
afm_obj.install_frida_server()
|
|
795
|
+
afm_obj.run_frida_server()
|
|
796
|
+
result = afm_obj.is_frida_server_running()
|
|
797
|
+
if result:
|
|
798
|
+
afm_obj.logger.info("[*] succesfull installed and launched latest frida-server version on Android device")
|
|
799
|
+
else:
|
|
800
|
+
afm_obj.logger.error("[-] unable to run frida-server on Android device")
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
if __name__ == "__main__":
|
|
804
|
+
main()
|