AndroidFridaManager 1.9.0__py3-none-any.whl → 1.9.2__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.
- AndroidFridaManager/FridaManager.py +476 -91
- AndroidFridaManager/about.py +1 -1
- AndroidFridaManager/job_manager.py +107 -34
- {androidfridamanager-1.9.0.dist-info → androidfridamanager-1.9.2.dist-info}/METADATA +2 -2
- androidfridamanager-1.9.2.dist-info/RECORD +11 -0
- androidfridamanager-1.9.0.dist-info/RECORD +0 -11
- {androidfridamanager-1.9.0.dist-info → androidfridamanager-1.9.2.dist-info}/WHEEL +0 -0
- {androidfridamanager-1.9.0.dist-info → androidfridamanager-1.9.2.dist-info}/entry_points.txt +0 -0
- {androidfridamanager-1.9.0.dist-info → androidfridamanager-1.9.2.dist-info}/licenses/LICENSE +0 -0
- {androidfridamanager-1.9.0.dist-info → androidfridamanager-1.9.2.dist-info}/top_level.txt +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import frida
|
|
5
5
|
import os
|
|
6
|
-
import sys
|
|
6
|
+
import sys
|
|
7
7
|
import logging
|
|
8
8
|
from colorlog import ColoredFormatter
|
|
9
9
|
import subprocess
|
|
@@ -15,46 +15,76 @@ from shutil import copyfile
|
|
|
15
15
|
import tempfile
|
|
16
16
|
import argparse
|
|
17
17
|
import shutil
|
|
18
|
+
from typing import Optional, List, Dict, Tuple
|
|
18
19
|
|
|
19
20
|
# some parts are taken from ttps://github.com/Mind0xP/Frida-Python-Binding/
|
|
20
21
|
|
|
21
22
|
class FridaManager():
|
|
22
23
|
|
|
23
|
-
def __init__(self, is_remote=False, socket="", verbose=False, frida_install_dst="/data/local/tmp/"):
|
|
24
|
+
def __init__(self, is_remote=False, socket="", verbose=False, frida_install_dst="/data/local/tmp/", device_serial: Optional[str] = None):
|
|
24
25
|
"""
|
|
25
26
|
Constructor of the current FridaManager instance
|
|
26
27
|
|
|
27
|
-
:param is_remote:
|
|
28
|
-
:type
|
|
28
|
+
:param is_remote: Whether to use remote Frida connection.
|
|
29
|
+
:type is_remote: bool
|
|
29
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.
|
|
30
|
-
:type
|
|
31
|
+
:type socket: string
|
|
31
32
|
:param verbose: Set the output to verbose, so that the logging information gets printed. By default set to False.
|
|
32
|
-
:type
|
|
33
|
+
:type verbose: bool
|
|
33
34
|
:param frida_install_dst: The path where the frida server should be installed. By default it will be installed to /data/local/tmp/.
|
|
34
|
-
:type
|
|
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]
|
|
35
38
|
|
|
36
39
|
"""
|
|
37
40
|
self.is_remote = is_remote
|
|
38
41
|
self.device_socket = socket
|
|
39
42
|
self.verbose = verbose
|
|
40
43
|
self.is_magisk_mode = False
|
|
44
|
+
self.is_adb_root_mode = False # LineageOS adb root mode (adbd runs as root)
|
|
41
45
|
self.frida_install_dst = frida_install_dst
|
|
42
46
|
self._setup_logging()
|
|
43
47
|
self.logger = logging.getLogger(__name__)
|
|
44
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
|
+
|
|
45
54
|
# Check if ADB is available
|
|
46
55
|
self._check_adb_availability()
|
|
47
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
|
+
|
|
48
64
|
if self.is_remote:
|
|
49
65
|
frida.get_device_manager().add_remote_device(self.device_socket)
|
|
50
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
|
|
51
81
|
|
|
52
82
|
def _setup_logging(self):
|
|
53
83
|
"""
|
|
54
|
-
Setup logging for the current instance of FridaManager
|
|
84
|
+
Setup logging for the current instance of FridaManager
|
|
55
85
|
"""
|
|
56
86
|
logger = logging.getLogger()
|
|
57
|
-
|
|
87
|
+
|
|
58
88
|
# Check if the logger already has handlers (i.e., if another project has set it up)
|
|
59
89
|
if not logger.handlers:
|
|
60
90
|
logger.setLevel(logging.INFO)
|
|
@@ -87,37 +117,243 @@ class FridaManager():
|
|
|
87
117
|
self.logger.info(" - Make sure 'adb' command is accessible from your terminal")
|
|
88
118
|
sys.exit(1)
|
|
89
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
|
+
|
|
90
308
|
def run_frida_server(self, frida_server_path="/data/local/tmp/"):
|
|
91
309
|
# Check if frida-server is already running
|
|
92
310
|
if self.is_frida_server_running():
|
|
93
311
|
if self.verbose:
|
|
94
312
|
self.logger.info("[*] frida-server is already running, skipping start")
|
|
95
313
|
return True
|
|
96
|
-
|
|
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
|
+
|
|
97
320
|
if frida_server_path is self.run_frida_server.__defaults__[0]:
|
|
98
|
-
|
|
321
|
+
frida_bin = self.frida_install_dst + "frida-server"
|
|
99
322
|
else:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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} &"'"""
|
|
104
332
|
else:
|
|
105
|
-
|
|
333
|
+
# Traditional su mode
|
|
334
|
+
shell_cmd = f"""su 0 sh -c "{frida_bin} &" """
|
|
106
335
|
|
|
107
336
|
try:
|
|
108
|
-
|
|
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
|
+
)
|
|
109
344
|
# Give it a moment to start and potentially fail
|
|
110
345
|
import time
|
|
111
346
|
time.sleep(1)
|
|
112
|
-
|
|
347
|
+
|
|
113
348
|
# Check if process failed immediately
|
|
114
349
|
if process.poll() is not None:
|
|
115
350
|
stdout, stderr = process.communicate()
|
|
116
|
-
if
|
|
351
|
+
stderr_text = stderr.decode() if isinstance(stderr, bytes) else str(stderr or "")
|
|
352
|
+
if "Address already in use" in stderr_text:
|
|
117
353
|
self.logger.info("[*] frida-server is already running on the device")
|
|
118
354
|
return True
|
|
119
355
|
else:
|
|
120
|
-
self.logger.error(f"Failed to start frida-server: {
|
|
356
|
+
self.logger.error(f"Failed to start frida-server: {stderr_text}")
|
|
121
357
|
return False
|
|
122
358
|
else:
|
|
123
359
|
# Process is still running (background), which is expected for frida-server
|
|
@@ -129,40 +365,59 @@ class FridaManager():
|
|
|
129
365
|
else:
|
|
130
366
|
self.logger.error("frida-server does not seem to be running after start command")
|
|
131
367
|
return False
|
|
132
|
-
|
|
368
|
+
|
|
133
369
|
except Exception as e:
|
|
134
370
|
self.logger.error(f"Error starting frida-server: {e}")
|
|
135
371
|
return False
|
|
136
372
|
|
|
137
373
|
|
|
138
|
-
def is_frida_server_running(self):
|
|
374
|
+
def is_frida_server_running(self) -> bool:
|
|
139
375
|
"""
|
|
140
|
-
Checks if on the connected device a frida server is running.
|
|
141
|
-
|
|
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).
|
|
142
380
|
|
|
143
381
|
:return: True if a frida-server is running otherwise False.
|
|
144
382
|
:rtype: bool
|
|
145
383
|
"""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
162
414
|
|
|
163
415
|
|
|
164
416
|
def stop_frida_server(self):
|
|
165
|
-
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")
|
|
166
421
|
|
|
167
422
|
|
|
168
423
|
def remove_frida_server(self, frida_server_path="/data/local/tmp/"):
|
|
@@ -170,22 +425,22 @@ class FridaManager():
|
|
|
170
425
|
cmd = self.frida_install_dst + "frida-server"
|
|
171
426
|
else:
|
|
172
427
|
cmd = frida_server_path + "frida-server"
|
|
173
|
-
|
|
428
|
+
|
|
174
429
|
self.stop_frida_server()
|
|
175
430
|
self._adb_remove_file_if_exist(cmd)
|
|
176
431
|
|
|
177
432
|
|
|
178
433
|
def install_frida_server(self, dst_dir="/data/local/tmp/", version="latest"):
|
|
179
434
|
"""
|
|
180
|
-
Install the frida server binary on the Android device.
|
|
435
|
+
Install the frida server binary on the Android device.
|
|
181
436
|
This includes downloading the frida-server, decompress it and pushing it to the Android device.
|
|
182
437
|
By default it is pushed into the /data/local/tmp/ directory.
|
|
183
438
|
Further the binary will be set to executable in order to run it.
|
|
184
439
|
|
|
185
|
-
:param dst_dir: The destination folder where the frida-server binary should be installed (pushed).
|
|
186
|
-
:type
|
|
440
|
+
:param dst_dir: The destination folder where the frida-server binary should be installed (pushed).
|
|
441
|
+
:type dst_dir: string
|
|
187
442
|
:param version: The version. By default the latest version will be used.
|
|
188
|
-
:type
|
|
443
|
+
:type version: string
|
|
189
444
|
|
|
190
445
|
"""
|
|
191
446
|
if dst_dir is self.install_frida_server.__defaults__[0]:
|
|
@@ -214,9 +469,9 @@ class FridaManager():
|
|
|
214
469
|
If you want to download a specific version you have to provide it trough the version parameter.
|
|
215
470
|
|
|
216
471
|
:param path: The path where the compressed frida-server should be downloded.
|
|
217
|
-
:type
|
|
472
|
+
:type path: string
|
|
218
473
|
:param version: The version. By default the latest version will be used.
|
|
219
|
-
:type
|
|
474
|
+
:type version: string
|
|
220
475
|
|
|
221
476
|
:return: The location of the downloaded frida server in its compressed form.
|
|
222
477
|
:rtype: string
|
|
@@ -278,13 +533,13 @@ class FridaManager():
|
|
|
278
533
|
|
|
279
534
|
if version == "latest":
|
|
280
535
|
url = "https://api.github.com/repos/frida/frida/releases/"+version
|
|
281
|
-
|
|
536
|
+
|
|
282
537
|
try:
|
|
283
538
|
res = requests.get(url)
|
|
284
539
|
except requests.exceptions.RequestException as e:
|
|
285
540
|
self.logger.error(f"Error making request to {url}: {e}")
|
|
286
541
|
raise RuntimeError(f"Failed to fetch Frida release information: {e}")
|
|
287
|
-
|
|
542
|
+
|
|
288
543
|
with warnings.catch_warnings():
|
|
289
544
|
warnings.simplefilter("ignore", SyntaxWarning)
|
|
290
545
|
try:
|
|
@@ -315,83 +570,214 @@ class FridaManager():
|
|
|
315
570
|
self.logger.info(f"[*] making frida-server executable: {final_cmd}")
|
|
316
571
|
|
|
317
572
|
self.run_adb_command_as_root(f"chmod +x {cmd}")
|
|
318
|
-
|
|
319
573
|
|
|
320
574
|
|
|
321
|
-
### some functions to work with adb ###
|
|
322
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'
|
|
323
596
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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.")
|
|
327
603
|
raise RuntimeError("Device not rooted or su binary not accessible")
|
|
328
604
|
|
|
329
|
-
if self.
|
|
330
|
-
|
|
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}'])
|
|
331
611
|
else:
|
|
332
|
-
|
|
612
|
+
# Traditional su mode
|
|
613
|
+
adb_cmd = self._build_adb_command(['shell', f'su 0 {command}'])
|
|
333
614
|
|
|
334
|
-
return
|
|
615
|
+
return subprocess.run(adb_cmd, capture_output=True, text=True)
|
|
335
616
|
|
|
336
617
|
|
|
337
|
-
def _adb_push_file(self,file,dst):
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
340
650
|
|
|
341
|
-
|
|
342
|
-
def _adb_pull_file(self,src_file,dst):
|
|
343
|
-
output = subprocess.run(['adb', 'pull',src_file,dst], capture_output=True, text=True)
|
|
344
|
-
return output
|
|
345
|
-
|
|
346
651
|
|
|
347
|
-
def _get_android_device_arch(self):
|
|
348
|
-
if self.is_remote:
|
|
349
|
-
frida_usb_json_data = frida.get_remote_device().query_system_parameters()
|
|
350
|
-
else:
|
|
351
|
-
frida_usb_json_data = frida.get_usb_device().query_system_parameters()
|
|
352
|
-
return frida_usb_json_data['arch']
|
|
353
|
-
|
|
354
|
-
|
|
355
652
|
def _adb_make_binary_executable(self, path):
|
|
356
653
|
output = self.run_adb_command_as_root("chmod +x "+path)
|
|
357
654
|
|
|
358
655
|
|
|
359
|
-
def _adb_does_file_exist(self,path):
|
|
360
|
-
|
|
361
|
-
if
|
|
362
|
-
|
|
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
|
|
363
661
|
else:
|
|
364
|
-
|
|
365
|
-
|
|
662
|
+
output = self._run_adb_shell_command(f"ls {path} 2>/dev/null")
|
|
663
|
+
return len(output.stderr) <= 1 and output.stdout.strip()
|
|
366
664
|
|
|
367
665
|
|
|
368
|
-
def
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
372
675
|
|
|
373
|
-
|
|
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
|
|
374
743
|
|
|
375
744
|
|
|
376
745
|
def _adb_remove_file_if_exist(self, path="/data/local/tmp/frida-server"):
|
|
377
746
|
if self._adb_does_file_exist(path):
|
|
378
|
-
|
|
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}")
|
|
379
751
|
|
|
380
752
|
|
|
381
753
|
def main():
|
|
382
754
|
if len(sys.argv) > 1:
|
|
383
755
|
parser = argparse.ArgumentParser(description='FridaManager initialization parameters.')
|
|
384
|
-
|
|
756
|
+
|
|
385
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.')
|
|
386
758
|
parser.add_argument('--socket', type=str, default="", help='Socket to use for the connection. Expected in the format <ip:port>.')
|
|
387
759
|
parser.add_argument('--verbose', required=False, action="store_const", const=True, default=False, help='Enable verbose output. Default is False.')
|
|
388
760
|
parser.add_argument('--frida_install_dst', type=str, default="/data/local/tmp/", help='Frida installation destination. Default is "/data/local/tmp/".')
|
|
389
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.')
|
|
390
764
|
|
|
391
765
|
args = parser.parse_args()
|
|
392
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
|
+
|
|
393
779
|
if args.is_running:
|
|
394
|
-
afm_obj = FridaManager()
|
|
780
|
+
afm_obj = FridaManager(device_serial=args.device)
|
|
395
781
|
if afm_obj.is_frida_server_running():
|
|
396
782
|
afm_obj.logger.info("[*] frida-server is running on Android device")
|
|
397
783
|
else:
|
|
@@ -401,7 +787,7 @@ def main():
|
|
|
401
787
|
|
|
402
788
|
|
|
403
789
|
|
|
404
|
-
afm_obj = FridaManager(args.is_remote, args.socket, args.verbose, args.frida_install_dst)
|
|
790
|
+
afm_obj = FridaManager(args.is_remote, args.socket, args.verbose, args.frida_install_dst, device_serial=args.device)
|
|
405
791
|
else:
|
|
406
792
|
afm_obj = FridaManager()
|
|
407
793
|
|
|
@@ -416,4 +802,3 @@ def main():
|
|
|
416
802
|
|
|
417
803
|
if __name__ == "__main__":
|
|
418
804
|
main()
|
|
419
|
-
|
AndroidFridaManager/about.py
CHANGED
|
@@ -4,21 +4,24 @@
|
|
|
4
4
|
import atexit
|
|
5
5
|
import subprocess
|
|
6
6
|
import frida
|
|
7
|
-
from typing import Optional, Dict, Union
|
|
7
|
+
from typing import Optional, Dict, Union, List
|
|
8
8
|
from .job import Job, FridaBasedException
|
|
9
9
|
import time
|
|
10
10
|
import re
|
|
11
11
|
import logging
|
|
12
12
|
|
|
13
13
|
class JobManager(object):
|
|
14
|
-
""" A class representing the current Job manager. """
|
|
14
|
+
""" A class representing the current Job manager with multi-device support. """
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def __init__(self,host="", enable_spawn_gating=False) -> None:
|
|
17
|
+
def __init__(self, host="", enable_spawn_gating=False, device_serial: Optional[str] = None) -> None:
|
|
18
18
|
"""
|
|
19
|
-
Init a new job manager
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
Init a new job manager with optional device targeting.
|
|
20
|
+
|
|
21
|
+
:param host: Remote host for Frida connection (ip:port format)
|
|
22
|
+
:param enable_spawn_gating: Enable spawn gating for child process tracking
|
|
23
|
+
:param device_serial: Specific device serial to target (e.g., 'emulator-5554').
|
|
24
|
+
If None, uses default device selection.
|
|
22
25
|
"""
|
|
23
26
|
|
|
24
27
|
self.jobs = {}
|
|
@@ -34,6 +37,11 @@ class JobManager(object):
|
|
|
34
37
|
self.init_last_job = False
|
|
35
38
|
self.logger = logging.getLogger(__name__)
|
|
36
39
|
self._ensure_logging_setup()
|
|
40
|
+
|
|
41
|
+
# Multi-device support
|
|
42
|
+
self._device_serial = device_serial
|
|
43
|
+
self._multiple_devices = self._check_multiple_devices()
|
|
44
|
+
|
|
37
45
|
atexit.register(self.cleanup)
|
|
38
46
|
|
|
39
47
|
def _ensure_logging_setup(self):
|
|
@@ -46,11 +54,53 @@ class JobManager(object):
|
|
|
46
54
|
# Set up basic logging if no handlers exist
|
|
47
55
|
root_logger.setLevel(logging.INFO)
|
|
48
56
|
handler = logging.StreamHandler()
|
|
49
|
-
formatter = logging.Formatter('[%(asctime)s] [%(levelname)-4s] - %(message)s',
|
|
57
|
+
formatter = logging.Formatter('[%(asctime)s] [%(levelname)-4s] - %(message)s',
|
|
50
58
|
datefmt='%d-%m-%y %H:%M:%S')
|
|
51
59
|
handler.setFormatter(formatter)
|
|
52
60
|
root_logger.addHandler(handler)
|
|
53
61
|
|
|
62
|
+
def _check_multiple_devices(self) -> bool:
|
|
63
|
+
"""Check if multiple devices are connected."""
|
|
64
|
+
try:
|
|
65
|
+
result = subprocess.run(
|
|
66
|
+
['adb', 'devices'],
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
timeout=5
|
|
70
|
+
)
|
|
71
|
+
# Count device lines (skip header)
|
|
72
|
+
device_count = sum(
|
|
73
|
+
1 for line in result.stdout.strip().split('\n')[1:]
|
|
74
|
+
if line.strip() and '\tdevice' in line
|
|
75
|
+
)
|
|
76
|
+
return device_count > 1
|
|
77
|
+
except Exception:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def _build_adb_command(self, args: List[str]) -> List[str]:
|
|
81
|
+
"""
|
|
82
|
+
Build ADB command with device targeting if needed.
|
|
83
|
+
|
|
84
|
+
:param args: ADB command arguments (without 'adb' prefix)
|
|
85
|
+
:return: Complete command list including device targeting
|
|
86
|
+
"""
|
|
87
|
+
cmd = ['adb']
|
|
88
|
+
if self._device_serial and self._multiple_devices:
|
|
89
|
+
cmd.extend(['-s', self._device_serial])
|
|
90
|
+
cmd.extend(args)
|
|
91
|
+
return cmd
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def device_serial(self) -> Optional[str]:
|
|
95
|
+
"""Get the current target device serial."""
|
|
96
|
+
return self._device_serial
|
|
97
|
+
|
|
98
|
+
@device_serial.setter
|
|
99
|
+
def device_serial(self, serial: str) -> None:
|
|
100
|
+
"""Set the target device serial."""
|
|
101
|
+
self._device_serial = serial
|
|
102
|
+
self._multiple_devices = self._check_multiple_devices()
|
|
103
|
+
|
|
54
104
|
def cleanup(self) -> None:
|
|
55
105
|
"""
|
|
56
106
|
Clean up all of the job in the job manager.
|
|
@@ -65,7 +115,7 @@ class JobManager(object):
|
|
|
65
115
|
self.stop_jobs()
|
|
66
116
|
|
|
67
117
|
print("\n[*] Have a nice day!")
|
|
68
|
-
|
|
118
|
+
|
|
69
119
|
|
|
70
120
|
def job_list(self):
|
|
71
121
|
return list(self.jobs.keys())
|
|
@@ -103,8 +153,8 @@ class JobManager(object):
|
|
|
103
153
|
if main_activity:
|
|
104
154
|
self.package_name = package_name
|
|
105
155
|
# Prepare the base command for starting the app with main activity
|
|
106
|
-
cmd = ['
|
|
107
|
-
|
|
156
|
+
cmd = self._build_adb_command(['shell', 'am', 'start', '-n', f'{package_name}/{main_activity}'])
|
|
157
|
+
|
|
108
158
|
# Add extras if provided
|
|
109
159
|
if extras:
|
|
110
160
|
for key, value in extras.items():
|
|
@@ -114,11 +164,11 @@ class JobManager(object):
|
|
|
114
164
|
cmd.extend(['--es', key, value])
|
|
115
165
|
else:
|
|
116
166
|
# Command to start the app using monkey if no main activity is provided
|
|
117
|
-
cmd = ['
|
|
118
|
-
|
|
167
|
+
cmd = self._build_adb_command(['shell', 'monkey', '-p', package_name, '-c', 'android.intent.category.LAUNCHER', '1'])
|
|
168
|
+
|
|
119
169
|
# Run the command and capture the output
|
|
120
170
|
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
121
|
-
|
|
171
|
+
|
|
122
172
|
# Extract the PID from the output
|
|
123
173
|
pid = None
|
|
124
174
|
if 'ThisTime' in result.stdout:
|
|
@@ -128,7 +178,7 @@ class JobManager(object):
|
|
|
128
178
|
pid = int(pid_match.group(1))
|
|
129
179
|
elif 'Events injected' in result.stdout:
|
|
130
180
|
# `monkey` command does not provide PID directly, need to get it separately
|
|
131
|
-
pid_cmd = ['
|
|
181
|
+
pid_cmd = self._build_adb_command(['shell', 'pidof', package_name])
|
|
132
182
|
pid_result = subprocess.run(pid_cmd, capture_output=True, text=True, check=True)
|
|
133
183
|
if pid_result.stdout:
|
|
134
184
|
pid = int(pid_result.stdout.split()[0])
|
|
@@ -156,14 +206,13 @@ class JobManager(object):
|
|
|
156
206
|
self.logger.info(f"[*] attaching to app: {target_process}")
|
|
157
207
|
self.process_session = self.device.attach(int(target_process) if target_process.isnumeric() else target_process)
|
|
158
208
|
|
|
159
|
-
|
|
160
209
|
|
|
161
210
|
|
|
162
211
|
def setup_frida_session(self, target_process, custom_hooking_handler_name, should_spawn=True,foreground=False):
|
|
163
212
|
self.first_instrumenation_script = custom_hooking_handler_name
|
|
164
213
|
self.device = self.setup_frida_handler(self.host, self.enable_spawn_gating)
|
|
165
214
|
|
|
166
|
-
try:
|
|
215
|
+
try:
|
|
167
216
|
if should_spawn:
|
|
168
217
|
self.pid = self.spawn(target_process)
|
|
169
218
|
else:
|
|
@@ -172,8 +221,8 @@ class JobManager(object):
|
|
|
172
221
|
raise FridaBasedException(f"TimeOutError: {te}")
|
|
173
222
|
except frida.ProcessNotFoundError as pe:
|
|
174
223
|
raise FridaBasedException(f"ProcessNotFoundError: {pe}")
|
|
175
|
-
|
|
176
|
-
|
|
224
|
+
|
|
225
|
+
|
|
177
226
|
def init_job(self,frida_script_name, custom_hooking_handler_name):
|
|
178
227
|
try:
|
|
179
228
|
if self.process_session:
|
|
@@ -211,7 +260,7 @@ class JobManager(object):
|
|
|
211
260
|
except Exception as fe:
|
|
212
261
|
raise FridaBasedException(f"Frida-Error: {fe}")
|
|
213
262
|
|
|
214
|
-
|
|
263
|
+
|
|
215
264
|
def start_job(self,frida_script_name, custom_hooking_handler_name):
|
|
216
265
|
try:
|
|
217
266
|
if self.process_session:
|
|
@@ -226,12 +275,12 @@ class JobManager(object):
|
|
|
226
275
|
if self.pid != -1:
|
|
227
276
|
self.device.resume(self.pid)
|
|
228
277
|
time.sleep(1) # without it Java.perform silently fails
|
|
229
|
-
|
|
278
|
+
|
|
230
279
|
return job
|
|
231
280
|
|
|
232
281
|
else:
|
|
233
282
|
self.logger.error("[-] no frida session. Aborting...")
|
|
234
|
-
|
|
283
|
+
|
|
235
284
|
|
|
236
285
|
except frida.TransportError as fe:
|
|
237
286
|
raise FridaBasedException(f"Problems while attaching to frida-server: {fe}")
|
|
@@ -245,7 +294,7 @@ class JobManager(object):
|
|
|
245
294
|
self.stop_app_with_last_job(job,self.package_name)
|
|
246
295
|
pass
|
|
247
296
|
|
|
248
|
-
|
|
297
|
+
|
|
249
298
|
def stop_jobs(self):
|
|
250
299
|
jobs_to_stop = [job_id for job_id, job in self.jobs.items() if job.state == "running"]
|
|
251
300
|
for job_id in jobs_to_stop:
|
|
@@ -255,8 +304,8 @@ class JobManager(object):
|
|
|
255
304
|
except frida.InvalidOperationError:
|
|
256
305
|
self.logger.error('[job manager] Job: {0} - An error occurred stopping job. Device may '
|
|
257
306
|
'no longer be available.'.format(job_id))
|
|
258
|
-
|
|
259
|
-
|
|
307
|
+
|
|
308
|
+
|
|
260
309
|
def stop_job_with_id(self,job_id):
|
|
261
310
|
if job_id in self.jobs:
|
|
262
311
|
job = self.jobs[job_id]
|
|
@@ -282,13 +331,14 @@ class JobManager(object):
|
|
|
282
331
|
|
|
283
332
|
|
|
284
333
|
def stop_app(self, app_package):
|
|
285
|
-
|
|
334
|
+
cmd = self._build_adb_command(["shell", "am", "force-stop", app_package])
|
|
335
|
+
subprocess.run(cmd)
|
|
286
336
|
|
|
287
337
|
|
|
288
338
|
def stop_app_with_last_job(self, last_job, app_package):
|
|
289
339
|
last_job.close_job()
|
|
290
340
|
self.stop_app(app_package)
|
|
291
|
-
|
|
341
|
+
|
|
292
342
|
|
|
293
343
|
|
|
294
344
|
def stop_app_with_closing_frida(self, app_package):
|
|
@@ -296,21 +346,44 @@ class JobManager(object):
|
|
|
296
346
|
for job_id in jobs_to_stop:
|
|
297
347
|
self.logger.info(f"[*] trying to close job: {job_id}")
|
|
298
348
|
self.stop_job_with_id(job_id)
|
|
299
|
-
|
|
349
|
+
|
|
300
350
|
self.detach_from_app()
|
|
301
|
-
|
|
351
|
+
cmd = self._build_adb_command(["shell", "am", "force-stop", app_package])
|
|
352
|
+
subprocess.run(cmd)
|
|
302
353
|
|
|
303
354
|
|
|
304
355
|
def kill_app(self, pid):
|
|
305
|
-
|
|
356
|
+
cmd = self._build_adb_command(["shell", "kill", str(pid)])
|
|
357
|
+
subprocess.run(cmd)
|
|
306
358
|
|
|
307
|
-
|
|
308
|
-
def setup_frida_handler(self,host="", enable_spawn_gating=False):
|
|
359
|
+
|
|
360
|
+
def setup_frida_handler(self, host="", enable_spawn_gating=False):
|
|
361
|
+
"""
|
|
362
|
+
Setup the Frida device handler with multi-device support.
|
|
363
|
+
|
|
364
|
+
:param host: Remote host for Frida connection
|
|
365
|
+
:param enable_spawn_gating: Enable spawn gating
|
|
366
|
+
:return: Frida Device object
|
|
367
|
+
"""
|
|
309
368
|
try:
|
|
310
369
|
if len(host) > 4:
|
|
311
|
-
#
|
|
370
|
+
# Remote device connection
|
|
312
371
|
device = frida.get_device_manager().add_remote_device(host)
|
|
372
|
+
elif self._device_serial:
|
|
373
|
+
# Multi-device: Get specific device by serial
|
|
374
|
+
try:
|
|
375
|
+
device = frida.get_device(self._device_serial)
|
|
376
|
+
except frida.InvalidArgumentError:
|
|
377
|
+
# Fallback: enumerate and find by ID
|
|
378
|
+
device = None
|
|
379
|
+
for d in frida.enumerate_devices():
|
|
380
|
+
if d.id == self._device_serial:
|
|
381
|
+
device = d
|
|
382
|
+
break
|
|
383
|
+
if device is None:
|
|
384
|
+
raise FridaBasedException(f"Frida device '{self._device_serial}' not found")
|
|
313
385
|
else:
|
|
386
|
+
# Single device: Use USB device
|
|
314
387
|
device = frida.get_usb_device()
|
|
315
388
|
|
|
316
389
|
# to handle forks
|
|
@@ -320,7 +393,7 @@ class JobManager(object):
|
|
|
320
393
|
self.first_instrumenation_script(device.attach(child.pid))
|
|
321
394
|
device.resume(child.pid)
|
|
322
395
|
|
|
323
|
-
# if the target process is starting another process
|
|
396
|
+
# if the target process is starting another process
|
|
324
397
|
def on_spawn_added(spawn):
|
|
325
398
|
self.logger.info(f"Process spawned with pid {spawn.pid}. Name: {spawn.identifier}")
|
|
326
399
|
if callable(self.first_instrumenation_script):
|
|
@@ -331,7 +404,7 @@ class JobManager(object):
|
|
|
331
404
|
if enable_spawn_gating:
|
|
332
405
|
device.enable_spawn_gating()
|
|
333
406
|
device.on("spawn_added", on_spawn_added)
|
|
334
|
-
|
|
407
|
+
|
|
335
408
|
return device
|
|
336
409
|
|
|
337
410
|
except frida.InvalidArgumentError:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AndroidFridaManager
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.2
|
|
4
4
|
Summary: A python API in order to install and run the frida-server on an Android device.
|
|
5
5
|
Home-page: https://github.com/fkie-cad/AndroidFridaManager
|
|
6
6
|
Author: Daniel Baier
|
|
@@ -33,7 +33,7 @@ Dynamic: requires-dist
|
|
|
33
33
|
Dynamic: requires-python
|
|
34
34
|
Dynamic: summary
|
|
35
35
|
|
|
36
|
-
 [](https://badge.fury.io/py/AndroidFridaManager) [](https://github.com/fkie-cad/AndroidFridaManager/actions/workflows/publish-to-pypi.yml)
|
|
37
37
|
|
|
38
38
|
# AndroidFridaManager
|
|
39
39
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
AndroidFridaManager/FridaManager.py,sha256=QonEFHpI5YTWWmHPrHGiUNuF-V0V_5qUC1GQ1yMx_Gg,31522
|
|
2
|
+
AndroidFridaManager/__init__.py,sha256=T6AKtrGSLQ9M5bJoWDQcsRTJbSEbksdgrx3AAAdozRI,171
|
|
3
|
+
AndroidFridaManager/about.py,sha256=eT4HT7KJGOEnLNu9eam06-A_1n_ZVYAexp02TRRRW6o,98
|
|
4
|
+
AndroidFridaManager/job.py,sha256=1NNcfCjkyUtwUkMXSgT4uswA8UStHo3jxbeJwJoWhc8,3352
|
|
5
|
+
AndroidFridaManager/job_manager.py,sha256=S3biHhYrk-DUUfrHA-g8vbOqwgl4FnWELrUjMxsFyG8,15983
|
|
6
|
+
androidfridamanager-1.9.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
7
|
+
androidfridamanager-1.9.2.dist-info/METADATA,sha256=P8k6Nsb_aqnsh_cqzFixy2HiIpbs5tQ5LxmkzCfQvFQ,5141
|
|
8
|
+
androidfridamanager-1.9.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
androidfridamanager-1.9.2.dist-info/entry_points.txt,sha256=GmNngu2fDNCxUcquFRegBa7GWknPKG1jsM4lvWeyKnY,64
|
|
10
|
+
androidfridamanager-1.9.2.dist-info/top_level.txt,sha256=oH2lVMSRlghmt-_tVrOEUqvY462P9hd5Ktgp5-1qF3o,20
|
|
11
|
+
androidfridamanager-1.9.2.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
AndroidFridaManager/FridaManager.py,sha256=h4B1TxYZyFU2TbQhbFXblEIAmG8lehiqDJmSeM1VrIA,16515
|
|
2
|
-
AndroidFridaManager/__init__.py,sha256=T6AKtrGSLQ9M5bJoWDQcsRTJbSEbksdgrx3AAAdozRI,171
|
|
3
|
-
AndroidFridaManager/about.py,sha256=AnyNW3Vf1B29iyCca75YikQoOslkmhci3b9IbRrWFrM,98
|
|
4
|
-
AndroidFridaManager/job.py,sha256=1NNcfCjkyUtwUkMXSgT4uswA8UStHo3jxbeJwJoWhc8,3352
|
|
5
|
-
AndroidFridaManager/job_manager.py,sha256=HUilXVNBEqdgY4LTlerK9XZpb0w7UgQBCq_bxpXwVVI,13253
|
|
6
|
-
androidfridamanager-1.9.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
7
|
-
androidfridamanager-1.9.0.dist-info/METADATA,sha256=p4wEduWuT8dzPBG6rtjnDfpNbTBjLDuqgLd390elqAM,5141
|
|
8
|
-
androidfridamanager-1.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
-
androidfridamanager-1.9.0.dist-info/entry_points.txt,sha256=GmNngu2fDNCxUcquFRegBa7GWknPKG1jsM4lvWeyKnY,64
|
|
10
|
-
androidfridamanager-1.9.0.dist-info/top_level.txt,sha256=oH2lVMSRlghmt-_tVrOEUqvY462P9hd5Ktgp5-1qF3o,20
|
|
11
|
-
androidfridamanager-1.9.0.dist-info/RECORD,,
|
|
File without changes
|
{androidfridamanager-1.9.0.dist-info → androidfridamanager-1.9.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{androidfridamanager-1.9.0.dist-info → androidfridamanager-1.9.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|