AndroidFridaManager 1.8.9__py3-none-any.whl → 1.9.1__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 +417 -85
- AndroidFridaManager/about.py +1 -1
- AndroidFridaManager/job_manager.py +108 -35
- {androidfridamanager-1.8.9.dist-info → androidfridamanager-1.9.1.dist-info}/METADATA +2 -2
- androidfridamanager-1.9.1.dist-info/RECORD +11 -0
- androidfridamanager-1.8.9.dist-info/RECORD +0 -11
- {androidfridamanager-1.8.9.dist-info → androidfridamanager-1.9.1.dist-info}/WHEEL +0 -0
- {androidfridamanager-1.8.9.dist-info → androidfridamanager-1.9.1.dist-info}/entry_points.txt +0 -0
- {androidfridamanager-1.8.9.dist-info → androidfridamanager-1.9.1.dist-info}/licenses/LICENSE +0 -0
- {androidfridamanager-1.8.9.dist-info → androidfridamanager-1.9.1.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,23 +15,26 @@ 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
|
|
@@ -42,19 +45,42 @@ class FridaManager():
|
|
|
42
45
|
self._setup_logging()
|
|
43
46
|
self.logger = logging.getLogger(__name__)
|
|
44
47
|
|
|
48
|
+
# Multi-device support
|
|
49
|
+
self._device_serial: Optional[str] = None
|
|
50
|
+
self._is_rooted: Optional[bool] = None # Cached root status
|
|
51
|
+
self._multiple_devices: bool = False
|
|
52
|
+
|
|
45
53
|
# Check if ADB is available
|
|
46
54
|
self._check_adb_availability()
|
|
47
55
|
|
|
56
|
+
# Handle device selection
|
|
57
|
+
if device_serial:
|
|
58
|
+
self._device_serial = device_serial
|
|
59
|
+
self._validate_device(device_serial)
|
|
60
|
+
else:
|
|
61
|
+
self._auto_select_device()
|
|
62
|
+
|
|
48
63
|
if self.is_remote:
|
|
49
64
|
frida.get_device_manager().add_remote_device(self.device_socket)
|
|
50
65
|
|
|
66
|
+
@property
|
|
67
|
+
def device_serial(self) -> Optional[str]:
|
|
68
|
+
"""Get the current target device serial."""
|
|
69
|
+
return self._device_serial
|
|
70
|
+
|
|
71
|
+
@device_serial.setter
|
|
72
|
+
def device_serial(self, serial: str) -> None:
|
|
73
|
+
"""Set the target device serial."""
|
|
74
|
+
self._validate_device(serial)
|
|
75
|
+
self._device_serial = serial
|
|
76
|
+
self._is_rooted = None # Reset cached root status
|
|
51
77
|
|
|
52
78
|
def _setup_logging(self):
|
|
53
79
|
"""
|
|
54
|
-
Setup logging for the current instance of FridaManager
|
|
80
|
+
Setup logging for the current instance of FridaManager
|
|
55
81
|
"""
|
|
56
82
|
logger = logging.getLogger()
|
|
57
|
-
|
|
83
|
+
|
|
58
84
|
# Check if the logger already has handlers (i.e., if another project has set it up)
|
|
59
85
|
if not logger.handlers:
|
|
60
86
|
logger.setLevel(logging.INFO)
|
|
@@ -87,37 +113,232 @@ class FridaManager():
|
|
|
87
113
|
self.logger.info(" - Make sure 'adb' command is accessible from your terminal")
|
|
88
114
|
sys.exit(1)
|
|
89
115
|
|
|
116
|
+
# ==================== Multi-Device Support ====================
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def get_connected_devices(cls) -> List[Dict[str, str]]:
|
|
120
|
+
"""
|
|
121
|
+
Get list of all connected Android devices via ADB.
|
|
122
|
+
|
|
123
|
+
:return: List of device dictionaries with 'serial', 'state', 'type', and 'model' keys
|
|
124
|
+
:rtype: List[Dict[str, str]]
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
result = subprocess.run(
|
|
128
|
+
['adb', 'devices', '-l'],
|
|
129
|
+
capture_output=True,
|
|
130
|
+
text=True,
|
|
131
|
+
timeout=10
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
devices = []
|
|
135
|
+
for line in result.stdout.strip().split('\n')[1:]: # Skip header
|
|
136
|
+
if not line.strip():
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
parts = line.split()
|
|
140
|
+
if len(parts) < 2:
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
serial = parts[0]
|
|
144
|
+
state = parts[1]
|
|
145
|
+
|
|
146
|
+
# Parse additional info
|
|
147
|
+
model = ""
|
|
148
|
+
product = ""
|
|
149
|
+
for part in parts[2:]:
|
|
150
|
+
if part.startswith("model:"):
|
|
151
|
+
model = part.split(":", 1)[1]
|
|
152
|
+
elif part.startswith("product:"):
|
|
153
|
+
product = part.split(":", 1)[1]
|
|
154
|
+
|
|
155
|
+
# Determine device type
|
|
156
|
+
device_type = "emulator" if serial.startswith("emulator-") else "physical"
|
|
157
|
+
|
|
158
|
+
devices.append({
|
|
159
|
+
'serial': serial,
|
|
160
|
+
'state': state,
|
|
161
|
+
'type': device_type,
|
|
162
|
+
'model': model or product or serial,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return devices
|
|
166
|
+
|
|
167
|
+
except subprocess.TimeoutExpired:
|
|
168
|
+
return []
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logging.getLogger(__name__).debug(f"Error getting devices: {e}")
|
|
171
|
+
return []
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def get_frida_devices(cls) -> List[Dict[str, str]]:
|
|
175
|
+
"""
|
|
176
|
+
Get list of devices visible to Frida.
|
|
177
|
+
|
|
178
|
+
:return: List of device dictionaries with 'id', 'name', 'type' keys
|
|
179
|
+
:rtype: List[Dict[str, str]]
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
devices = []
|
|
183
|
+
for device in frida.enumerate_devices():
|
|
184
|
+
if device.type in ('usb', 'remote'):
|
|
185
|
+
devices.append({
|
|
186
|
+
'id': device.id,
|
|
187
|
+
'name': device.name,
|
|
188
|
+
'type': device.type,
|
|
189
|
+
})
|
|
190
|
+
return devices
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logging.getLogger(__name__).debug(f"Error enumerating Frida devices: {e}")
|
|
193
|
+
return []
|
|
194
|
+
|
|
195
|
+
def _validate_device(self, serial: str) -> bool:
|
|
196
|
+
"""
|
|
197
|
+
Validate that a device with the given serial is connected.
|
|
198
|
+
|
|
199
|
+
:param serial: Device serial to validate
|
|
200
|
+
:return: True if valid, raises RuntimeError otherwise
|
|
201
|
+
"""
|
|
202
|
+
devices = self.get_connected_devices()
|
|
203
|
+
device_serials = [d['serial'] for d in devices]
|
|
204
|
+
|
|
205
|
+
if serial not in device_serials:
|
|
206
|
+
available = ", ".join(device_serials) if device_serials else "none"
|
|
207
|
+
raise RuntimeError(
|
|
208
|
+
f"Device '{serial}' not found. Available devices: {available}"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Check device state
|
|
212
|
+
device = next(d for d in devices if d['serial'] == serial)
|
|
213
|
+
if device['state'] != 'device':
|
|
214
|
+
raise RuntimeError(
|
|
215
|
+
f"Device '{serial}' is not ready (state: {device['state']})"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
def _auto_select_device(self) -> None:
|
|
221
|
+
"""
|
|
222
|
+
Automatically select a device when multiple are connected.
|
|
223
|
+
|
|
224
|
+
Priority:
|
|
225
|
+
1. If only one device, use it
|
|
226
|
+
2. If multiple devices, prefer emulators (they have root by default)
|
|
227
|
+
3. If multiple emulators, use the first one
|
|
228
|
+
"""
|
|
229
|
+
devices = self.get_connected_devices()
|
|
230
|
+
ready_devices = [d for d in devices if d['state'] == 'device']
|
|
231
|
+
|
|
232
|
+
if not ready_devices:
|
|
233
|
+
self.logger.warning("No Android devices connected")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
if len(ready_devices) == 1:
|
|
237
|
+
self._device_serial = ready_devices[0]['serial']
|
|
238
|
+
self._multiple_devices = False
|
|
239
|
+
if self.verbose:
|
|
240
|
+
self.logger.info(f"[*] Using device: {self._device_serial}")
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
# Multiple devices - prefer emulators
|
|
244
|
+
self._multiple_devices = True
|
|
245
|
+
emulators = [d for d in ready_devices if d['type'] == 'emulator']
|
|
246
|
+
physical = [d for d in ready_devices if d['type'] == 'physical']
|
|
247
|
+
|
|
248
|
+
if emulators:
|
|
249
|
+
self._device_serial = emulators[0]['serial']
|
|
250
|
+
self.logger.info(
|
|
251
|
+
f"[*] Multiple devices connected. Auto-selected emulator: {self._device_serial}"
|
|
252
|
+
)
|
|
253
|
+
if physical:
|
|
254
|
+
self.logger.info(
|
|
255
|
+
f"[*] Physical device(s) also connected: {', '.join(d['serial'] for d in physical)}"
|
|
256
|
+
)
|
|
257
|
+
elif physical:
|
|
258
|
+
# Only physical devices - select first but warn
|
|
259
|
+
self._device_serial = physical[0]['serial']
|
|
260
|
+
self.logger.warning(
|
|
261
|
+
f"[*] Multiple physical devices connected. Selected: {self._device_serial}. "
|
|
262
|
+
"Note: Physical devices require root for full functionality."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def _build_adb_command(self, args: List[str]) -> List[str]:
|
|
266
|
+
"""
|
|
267
|
+
Build ADB command with device targeting if needed.
|
|
268
|
+
|
|
269
|
+
:param args: ADB command arguments (without 'adb' prefix)
|
|
270
|
+
:return: Complete command list including device targeting
|
|
271
|
+
"""
|
|
272
|
+
cmd = ['adb']
|
|
273
|
+
if self._device_serial and self._multiple_devices:
|
|
274
|
+
cmd.extend(['-s', self._device_serial])
|
|
275
|
+
cmd.extend(args)
|
|
276
|
+
return cmd
|
|
277
|
+
|
|
278
|
+
def get_frida_device(self):
|
|
279
|
+
"""
|
|
280
|
+
Get the Frida device object for the current target device.
|
|
281
|
+
|
|
282
|
+
:return: Frida Device object
|
|
283
|
+
:raises RuntimeError: If device cannot be found
|
|
284
|
+
"""
|
|
285
|
+
if self.is_remote:
|
|
286
|
+
return frida.get_device_manager().add_remote_device(self.device_socket)
|
|
287
|
+
|
|
288
|
+
if self._device_serial:
|
|
289
|
+
try:
|
|
290
|
+
# Try to get device by ID (matches ADB serial for USB devices)
|
|
291
|
+
return frida.get_device(self._device_serial)
|
|
292
|
+
except frida.InvalidArgumentError:
|
|
293
|
+
# Fallback: enumerate and find by ID
|
|
294
|
+
for device in frida.enumerate_devices():
|
|
295
|
+
if device.id == self._device_serial:
|
|
296
|
+
return device
|
|
297
|
+
raise RuntimeError(f"Frida device '{self._device_serial}' not found")
|
|
298
|
+
else:
|
|
299
|
+
# Fallback to get_usb_device for single device scenario
|
|
300
|
+
return frida.get_usb_device()
|
|
301
|
+
|
|
302
|
+
# ==================== Frida Server Management ====================
|
|
303
|
+
|
|
90
304
|
def run_frida_server(self, frida_server_path="/data/local/tmp/"):
|
|
91
305
|
# Check if frida-server is already running
|
|
92
306
|
if self.is_frida_server_running():
|
|
93
307
|
if self.verbose:
|
|
94
308
|
self.logger.info("[*] frida-server is already running, skipping start")
|
|
95
309
|
return True
|
|
96
|
-
|
|
310
|
+
|
|
97
311
|
if frida_server_path is self.run_frida_server.__defaults__[0]:
|
|
98
312
|
cmd = self.frida_install_dst + "frida-server &"
|
|
99
313
|
else:
|
|
100
314
|
cmd = frida_server_path + "frida-server &"
|
|
101
315
|
|
|
102
316
|
if self.is_magisk_mode:
|
|
103
|
-
|
|
317
|
+
shell_cmd = f"""su -c 'sh -c "{cmd}"'"""
|
|
104
318
|
else:
|
|
105
|
-
|
|
319
|
+
shell_cmd = f"""su 0 sh -c "{cmd}" """
|
|
106
320
|
|
|
107
321
|
try:
|
|
108
|
-
|
|
322
|
+
adb_cmd = self._build_adb_command(['shell', shell_cmd])
|
|
323
|
+
process = subprocess.Popen(
|
|
324
|
+
adb_cmd,
|
|
325
|
+
stdout=subprocess.DEVNULL,
|
|
326
|
+
stderr=subprocess.DEVNULL,
|
|
327
|
+
start_new_session=True
|
|
328
|
+
)
|
|
109
329
|
# Give it a moment to start and potentially fail
|
|
110
330
|
import time
|
|
111
331
|
time.sleep(1)
|
|
112
|
-
|
|
332
|
+
|
|
113
333
|
# Check if process failed immediately
|
|
114
334
|
if process.poll() is not None:
|
|
115
335
|
stdout, stderr = process.communicate()
|
|
116
|
-
if
|
|
336
|
+
stderr_text = stderr.decode() if isinstance(stderr, bytes) else str(stderr or "")
|
|
337
|
+
if "Address already in use" in stderr_text:
|
|
117
338
|
self.logger.info("[*] frida-server is already running on the device")
|
|
118
339
|
return True
|
|
119
340
|
else:
|
|
120
|
-
self.logger.error(f"Failed to start frida-server: {
|
|
341
|
+
self.logger.error(f"Failed to start frida-server: {stderr_text}")
|
|
121
342
|
return False
|
|
122
343
|
else:
|
|
123
344
|
# Process is still running (background), which is expected for frida-server
|
|
@@ -129,40 +350,59 @@ class FridaManager():
|
|
|
129
350
|
else:
|
|
130
351
|
self.logger.error("frida-server does not seem to be running after start command")
|
|
131
352
|
return False
|
|
132
|
-
|
|
353
|
+
|
|
133
354
|
except Exception as e:
|
|
134
355
|
self.logger.error(f"Error starting frida-server: {e}")
|
|
135
356
|
return False
|
|
136
357
|
|
|
137
358
|
|
|
138
|
-
def is_frida_server_running(self):
|
|
359
|
+
def is_frida_server_running(self) -> bool:
|
|
139
360
|
"""
|
|
140
|
-
Checks if on the connected device a frida server is running.
|
|
141
|
-
|
|
361
|
+
Checks if on the connected device a frida server is running.
|
|
362
|
+
|
|
363
|
+
This method first tries non-root commands, then falls back to root commands
|
|
364
|
+
if available. Safe to call on non-rooted devices (returns False).
|
|
142
365
|
|
|
143
366
|
:return: True if a frida-server is running otherwise False.
|
|
144
367
|
:rtype: bool
|
|
145
368
|
"""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
369
|
+
try:
|
|
370
|
+
# Method 1: Try pidof without root (works on some devices)
|
|
371
|
+
result = self._run_adb_shell_command("pidof frida-server")
|
|
372
|
+
if result.stdout.strip():
|
|
373
|
+
try:
|
|
374
|
+
int(result.stdout.strip().split()[0]) # Validate it's a number
|
|
375
|
+
return True
|
|
376
|
+
except (ValueError, IndexError):
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
# Method 2: Try ps -A without root
|
|
380
|
+
result = self._run_adb_shell_command("ps -A 2>/dev/null | grep frida-server | grep -v grep")
|
|
381
|
+
if result.stdout.strip():
|
|
382
|
+
return True
|
|
383
|
+
|
|
384
|
+
# Method 3: Try with root if available
|
|
385
|
+
if self.is_device_rooted():
|
|
386
|
+
result = self.run_adb_command_as_root("pidof frida-server")
|
|
387
|
+
if result.stdout.strip():
|
|
388
|
+
return True
|
|
389
|
+
|
|
390
|
+
result = self.run_adb_command_as_root("ps | grep frida-server | grep -v grep")
|
|
391
|
+
if result.stdout.strip():
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
except Exception as e:
|
|
397
|
+
self.logger.debug(f"Error checking frida-server status: {e}")
|
|
398
|
+
return False
|
|
162
399
|
|
|
163
400
|
|
|
164
401
|
def stop_frida_server(self):
|
|
165
|
-
self.
|
|
402
|
+
if self.is_device_rooted():
|
|
403
|
+
self.run_adb_command_as_root("/system/bin/killall frida-server")
|
|
404
|
+
else:
|
|
405
|
+
self.logger.warning("Cannot stop frida-server: device not rooted")
|
|
166
406
|
|
|
167
407
|
|
|
168
408
|
def remove_frida_server(self, frida_server_path="/data/local/tmp/"):
|
|
@@ -170,22 +410,22 @@ class FridaManager():
|
|
|
170
410
|
cmd = self.frida_install_dst + "frida-server"
|
|
171
411
|
else:
|
|
172
412
|
cmd = frida_server_path + "frida-server"
|
|
173
|
-
|
|
413
|
+
|
|
174
414
|
self.stop_frida_server()
|
|
175
415
|
self._adb_remove_file_if_exist(cmd)
|
|
176
416
|
|
|
177
417
|
|
|
178
418
|
def install_frida_server(self, dst_dir="/data/local/tmp/", version="latest"):
|
|
179
419
|
"""
|
|
180
|
-
Install the frida server binary on the Android device.
|
|
420
|
+
Install the frida server binary on the Android device.
|
|
181
421
|
This includes downloading the frida-server, decompress it and pushing it to the Android device.
|
|
182
422
|
By default it is pushed into the /data/local/tmp/ directory.
|
|
183
423
|
Further the binary will be set to executable in order to run it.
|
|
184
424
|
|
|
185
|
-
:param dst_dir: The destination folder where the frida-server binary should be installed (pushed).
|
|
186
|
-
:type
|
|
425
|
+
:param dst_dir: The destination folder where the frida-server binary should be installed (pushed).
|
|
426
|
+
:type dst_dir: string
|
|
187
427
|
:param version: The version. By default the latest version will be used.
|
|
188
|
-
:type
|
|
428
|
+
:type version: string
|
|
189
429
|
|
|
190
430
|
"""
|
|
191
431
|
if dst_dir is self.install_frida_server.__defaults__[0]:
|
|
@@ -214,9 +454,9 @@ class FridaManager():
|
|
|
214
454
|
If you want to download a specific version you have to provide it trough the version parameter.
|
|
215
455
|
|
|
216
456
|
:param path: The path where the compressed frida-server should be downloded.
|
|
217
|
-
:type
|
|
457
|
+
:type path: string
|
|
218
458
|
:param version: The version. By default the latest version will be used.
|
|
219
|
-
:type
|
|
459
|
+
:type version: string
|
|
220
460
|
|
|
221
461
|
:return: The location of the downloaded frida server in its compressed form.
|
|
222
462
|
:rtype: string
|
|
@@ -278,13 +518,13 @@ class FridaManager():
|
|
|
278
518
|
|
|
279
519
|
if version == "latest":
|
|
280
520
|
url = "https://api.github.com/repos/frida/frida/releases/"+version
|
|
281
|
-
|
|
521
|
+
|
|
282
522
|
try:
|
|
283
523
|
res = requests.get(url)
|
|
284
524
|
except requests.exceptions.RequestException as e:
|
|
285
525
|
self.logger.error(f"Error making request to {url}: {e}")
|
|
286
526
|
raise RuntimeError(f"Failed to fetch Frida release information: {e}")
|
|
287
|
-
|
|
527
|
+
|
|
288
528
|
with warnings.catch_warnings():
|
|
289
529
|
warnings.simplefilter("ignore", SyntaxWarning)
|
|
290
530
|
try:
|
|
@@ -315,83 +555,176 @@ class FridaManager():
|
|
|
315
555
|
self.logger.info(f"[*] making frida-server executable: {final_cmd}")
|
|
316
556
|
|
|
317
557
|
self.run_adb_command_as_root(f"chmod +x {cmd}")
|
|
318
|
-
|
|
319
558
|
|
|
320
559
|
|
|
321
|
-
### some functions to work with adb ###
|
|
322
560
|
|
|
561
|
+
### some functions to work with adb ###
|
|
323
562
|
|
|
324
|
-
def
|
|
325
|
-
|
|
563
|
+
def _run_adb_shell_command(self, command: str) -> subprocess.CompletedProcess:
|
|
564
|
+
"""
|
|
565
|
+
Run an ADB shell command (without root) on the target device.
|
|
566
|
+
|
|
567
|
+
:param command: Shell command to run
|
|
568
|
+
:return: subprocess.CompletedProcess with stdout/stderr
|
|
569
|
+
"""
|
|
570
|
+
adb_cmd = self._build_adb_command(['shell', command])
|
|
571
|
+
return subprocess.run(adb_cmd, capture_output=True, text=True)
|
|
572
|
+
|
|
573
|
+
def run_adb_command_as_root(self, command: str) -> subprocess.CompletedProcess:
|
|
574
|
+
"""
|
|
575
|
+
Run an ADB command as root on the target device.
|
|
576
|
+
|
|
577
|
+
:param command: Command to run as root
|
|
578
|
+
:return: subprocess.CompletedProcess with stdout/stderr
|
|
579
|
+
:raises RuntimeError: If device is not rooted
|
|
580
|
+
"""
|
|
581
|
+
if not self.is_device_rooted():
|
|
326
582
|
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.")
|
|
327
583
|
raise RuntimeError("Device not rooted or su binary not accessible")
|
|
328
584
|
|
|
329
585
|
if self.is_magisk_mode:
|
|
330
|
-
|
|
586
|
+
adb_cmd = self._build_adb_command(['shell', f'su -c {command}'])
|
|
331
587
|
else:
|
|
332
|
-
|
|
588
|
+
adb_cmd = self._build_adb_command(['shell', f'su 0 {command}'])
|
|
333
589
|
|
|
334
|
-
return
|
|
590
|
+
return subprocess.run(adb_cmd, capture_output=True, text=True)
|
|
335
591
|
|
|
336
592
|
|
|
337
|
-
def _adb_push_file(self,file,dst):
|
|
338
|
-
|
|
339
|
-
|
|
593
|
+
def _adb_push_file(self, file: str, dst: str) -> subprocess.CompletedProcess:
|
|
594
|
+
"""Push a file to the device."""
|
|
595
|
+
adb_cmd = self._build_adb_command(['push', file, dst])
|
|
596
|
+
return subprocess.run(adb_cmd, capture_output=True, text=True)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _adb_pull_file(self, src_file: str, dst: str) -> subprocess.CompletedProcess:
|
|
600
|
+
"""Pull a file from the device."""
|
|
601
|
+
adb_cmd = self._build_adb_command(['pull', src_file, dst])
|
|
602
|
+
return subprocess.run(adb_cmd, capture_output=True, text=True)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _get_android_device_arch(self) -> str:
|
|
606
|
+
"""Get the architecture of the target Android device."""
|
|
607
|
+
try:
|
|
608
|
+
device = self.get_frida_device()
|
|
609
|
+
return device.query_system_parameters()['arch']
|
|
610
|
+
except Exception as e:
|
|
611
|
+
self.logger.warning(f"Failed to get arch via Frida, falling back to ADB: {e}")
|
|
612
|
+
# Fallback to ADB
|
|
613
|
+
result = self._run_adb_shell_command("getprop ro.product.cpu.abi")
|
|
614
|
+
abi = result.stdout.strip()
|
|
615
|
+
# Map ABI to Frida arch
|
|
616
|
+
if 'arm64' in abi or 'aarch64' in abi:
|
|
617
|
+
return 'arm64'
|
|
618
|
+
elif 'armeabi' in abi or 'arm' in abi:
|
|
619
|
+
return 'arm'
|
|
620
|
+
elif 'x86_64' in abi:
|
|
621
|
+
return 'x64'
|
|
622
|
+
elif 'x86' in abi:
|
|
623
|
+
return 'ia32'
|
|
624
|
+
return 'arm64' # Default
|
|
340
625
|
|
|
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
626
|
|
|
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
627
|
def _adb_make_binary_executable(self, path):
|
|
356
628
|
output = self.run_adb_command_as_root("chmod +x "+path)
|
|
357
629
|
|
|
358
630
|
|
|
359
|
-
def _adb_does_file_exist(self,path):
|
|
360
|
-
|
|
361
|
-
if
|
|
362
|
-
|
|
631
|
+
def _adb_does_file_exist(self, path: str) -> bool:
|
|
632
|
+
"""Check if a file exists on the device."""
|
|
633
|
+
if self.is_device_rooted():
|
|
634
|
+
output = self.run_adb_command_as_root("ls " + path)
|
|
635
|
+
return len(output.stderr) <= 1
|
|
363
636
|
else:
|
|
364
|
-
|
|
365
|
-
|
|
637
|
+
output = self._run_adb_shell_command(f"ls {path} 2>/dev/null")
|
|
638
|
+
return len(output.stderr) <= 1 and output.stdout.strip()
|
|
366
639
|
|
|
367
640
|
|
|
368
|
-
def
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
641
|
+
def is_device_rooted(self) -> bool:
|
|
642
|
+
"""
|
|
643
|
+
Check if the target device has root access.
|
|
644
|
+
Caches result after first check.
|
|
645
|
+
|
|
646
|
+
:return: True if device is rooted, False otherwise
|
|
647
|
+
"""
|
|
648
|
+
if self._is_rooted is not None:
|
|
649
|
+
return self._is_rooted
|
|
372
650
|
|
|
373
|
-
|
|
651
|
+
self._is_rooted = self.adb_check_root()
|
|
652
|
+
return self._is_rooted
|
|
653
|
+
|
|
654
|
+
def adb_check_root(self) -> bool:
|
|
655
|
+
"""
|
|
656
|
+
Check if the device has root access via su binary.
|
|
657
|
+
|
|
658
|
+
:return: True if root is available, False otherwise
|
|
659
|
+
"""
|
|
660
|
+
try:
|
|
661
|
+
# Try Magisk-style su
|
|
662
|
+
result = subprocess.run(
|
|
663
|
+
self._build_adb_command(['shell', 'su -v']),
|
|
664
|
+
capture_output=True,
|
|
665
|
+
text=True,
|
|
666
|
+
timeout=5
|
|
667
|
+
)
|
|
668
|
+
if result.stdout.strip():
|
|
669
|
+
self.is_magisk_mode = True
|
|
670
|
+
return True
|
|
671
|
+
|
|
672
|
+
# Try traditional su
|
|
673
|
+
result = subprocess.run(
|
|
674
|
+
self._build_adb_command(['shell', 'su 0 id -u']),
|
|
675
|
+
capture_output=True,
|
|
676
|
+
text=True,
|
|
677
|
+
timeout=5
|
|
678
|
+
)
|
|
679
|
+
if result.stdout.strip() == "0":
|
|
680
|
+
return True
|
|
681
|
+
|
|
682
|
+
return False
|
|
683
|
+
|
|
684
|
+
except subprocess.TimeoutExpired:
|
|
685
|
+
self.logger.debug("Root check timed out")
|
|
686
|
+
return False
|
|
687
|
+
except Exception as e:
|
|
688
|
+
self.logger.debug(f"Root check failed: {e}")
|
|
689
|
+
return False
|
|
374
690
|
|
|
375
691
|
|
|
376
692
|
def _adb_remove_file_if_exist(self, path="/data/local/tmp/frida-server"):
|
|
377
693
|
if self._adb_does_file_exist(path):
|
|
378
|
-
|
|
694
|
+
if self.is_device_rooted():
|
|
695
|
+
self.run_adb_command_as_root("rm " + path)
|
|
696
|
+
else:
|
|
697
|
+
self._run_adb_shell_command(f"rm {path}")
|
|
379
698
|
|
|
380
699
|
|
|
381
700
|
def main():
|
|
382
701
|
if len(sys.argv) > 1:
|
|
383
702
|
parser = argparse.ArgumentParser(description='FridaManager initialization parameters.')
|
|
384
|
-
|
|
703
|
+
|
|
385
704
|
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
705
|
parser.add_argument('--socket', type=str, default="", help='Socket to use for the connection. Expected in the format <ip:port>.')
|
|
387
706
|
parser.add_argument('--verbose', required=False, action="store_const", const=True, default=False, help='Enable verbose output. Default is False.')
|
|
388
707
|
parser.add_argument('--frida_install_dst', type=str, default="/data/local/tmp/", help='Frida installation destination. Default is "/data/local/tmp/".')
|
|
389
708
|
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.')
|
|
709
|
+
parser.add_argument('-d', '--device', type=str, default=None, help='Target device serial (e.g., emulator-5554). Auto-selects if not specified.')
|
|
710
|
+
parser.add_argument('-l', '--list-devices', required=False, action="store_const", const=True, default=False, help='List all connected devices and exit.')
|
|
390
711
|
|
|
391
712
|
args = parser.parse_args()
|
|
392
713
|
|
|
714
|
+
# List devices mode
|
|
715
|
+
if args.list_devices:
|
|
716
|
+
devices = FridaManager.get_connected_devices()
|
|
717
|
+
if not devices:
|
|
718
|
+
print("No devices connected")
|
|
719
|
+
else:
|
|
720
|
+
print(f"{'Serial':<20} {'Type':<10} {'State':<12} {'Model'}")
|
|
721
|
+
print("-" * 60)
|
|
722
|
+
for d in devices:
|
|
723
|
+
print(f"{d['serial']:<20} {d['type']:<10} {d['state']:<12} {d['model']}")
|
|
724
|
+
sys.exit(0)
|
|
725
|
+
|
|
393
726
|
if args.is_running:
|
|
394
|
-
afm_obj = FridaManager()
|
|
727
|
+
afm_obj = FridaManager(device_serial=args.device)
|
|
395
728
|
if afm_obj.is_frida_server_running():
|
|
396
729
|
afm_obj.logger.info("[*] frida-server is running on Android device")
|
|
397
730
|
else:
|
|
@@ -401,7 +734,7 @@ def main():
|
|
|
401
734
|
|
|
402
735
|
|
|
403
736
|
|
|
404
|
-
afm_obj = FridaManager(args.is_remote, args.socket, args.verbose, args.frida_install_dst)
|
|
737
|
+
afm_obj = FridaManager(args.is_remote, args.socket, args.verbose, args.frida_install_dst, device_serial=args.device)
|
|
405
738
|
else:
|
|
406
739
|
afm_obj = FridaManager()
|
|
407
740
|
|
|
@@ -416,4 +749,3 @@ def main():
|
|
|
416
749
|
|
|
417
750
|
if __name__ == "__main__":
|
|
418
751
|
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.
|
|
@@ -64,8 +114,8 @@ class JobManager(object):
|
|
|
64
114
|
self.logger.info("[*] Program closed. Stopping active jobs...")
|
|
65
115
|
self.stop_jobs()
|
|
66
116
|
|
|
67
|
-
|
|
68
|
-
|
|
117
|
+
print("\n[*] Have a nice day!")
|
|
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.
|
|
3
|
+
Version: 1.9.1
|
|
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=kdA79LW0idbJnch6HzXt_J115HysG9zVZS_WZQnIutk,29143
|
|
2
|
+
AndroidFridaManager/__init__.py,sha256=T6AKtrGSLQ9M5bJoWDQcsRTJbSEbksdgrx3AAAdozRI,171
|
|
3
|
+
AndroidFridaManager/about.py,sha256=DNJPRvDT-qbeIc-pXeyCemp2PRRwpIdaG2Du3iDNigk,98
|
|
4
|
+
AndroidFridaManager/job.py,sha256=1NNcfCjkyUtwUkMXSgT4uswA8UStHo3jxbeJwJoWhc8,3352
|
|
5
|
+
AndroidFridaManager/job_manager.py,sha256=S3biHhYrk-DUUfrHA-g8vbOqwgl4FnWELrUjMxsFyG8,15983
|
|
6
|
+
androidfridamanager-1.9.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
7
|
+
androidfridamanager-1.9.1.dist-info/METADATA,sha256=rvJB9NrUBVxqx7_j6cQyfE1jHtyx4wyqvA3PEgglOfE,5141
|
|
8
|
+
androidfridamanager-1.9.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
androidfridamanager-1.9.1.dist-info/entry_points.txt,sha256=GmNngu2fDNCxUcquFRegBa7GWknPKG1jsM4lvWeyKnY,64
|
|
10
|
+
androidfridamanager-1.9.1.dist-info/top_level.txt,sha256=oH2lVMSRlghmt-_tVrOEUqvY462P9hd5Ktgp5-1qF3o,20
|
|
11
|
+
androidfridamanager-1.9.1.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=7dIvGq_wA61VMrus2Q1sdiF21VEyucKtWneueIWAHk0,98
|
|
4
|
-
AndroidFridaManager/job.py,sha256=1NNcfCjkyUtwUkMXSgT4uswA8UStHo3jxbeJwJoWhc8,3352
|
|
5
|
-
AndroidFridaManager/job_manager.py,sha256=E4lNe4E9mvLcWdYCn2z-Rh4_b9fA_Fzv3LOqWE_bsvQ,13264
|
|
6
|
-
androidfridamanager-1.8.9.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
7
|
-
androidfridamanager-1.8.9.dist-info/METADATA,sha256=dyHEulGO3aZuES87zsTZklOMLtLbpzN3vbydLCSQnTc,5141
|
|
8
|
-
androidfridamanager-1.8.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
-
androidfridamanager-1.8.9.dist-info/entry_points.txt,sha256=GmNngu2fDNCxUcquFRegBa7GWknPKG1jsM4lvWeyKnY,64
|
|
10
|
-
androidfridamanager-1.8.9.dist-info/top_level.txt,sha256=oH2lVMSRlghmt-_tVrOEUqvY462P9hd5Ktgp5-1qF3o,20
|
|
11
|
-
androidfridamanager-1.8.9.dist-info/RECORD,,
|
|
File without changes
|
{androidfridamanager-1.8.9.dist-info → androidfridamanager-1.9.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{androidfridamanager-1.8.9.dist-info → androidfridamanager-1.9.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|