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.
@@ -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: The number to multiply.
28
- :type number: bool
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 number: string
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 number: bool
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 number: bool
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
- cmd = self.frida_install_dst + "frida-server &"
321
+ frida_bin = self.frida_install_dst + "frida-server"
99
322
  else:
100
- cmd = frida_server_path + "frida-server &"
101
-
102
- if self.is_magisk_mode:
103
- command = f"""adb shell "su -c 'sh -c \"{cmd}\"'" """
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
- command = f"""adb shell "su 0 sh -c \\"{cmd}\\"\" """
333
+ # Traditional su mode
334
+ shell_cmd = f"""su 0 sh -c "{frida_bin} &" """
106
335
 
107
336
  try:
108
- process = subprocess.Popen(command, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
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 "Address already in use" in stderr.decode():
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: {stderr.decode()}")
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
- The test is done by trying multiple methods to detect the frida-server process.
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
- # Try pidof first (most reliable if available)
147
- result = self.run_adb_command_as_root("pidof frida-server")
148
- if result.stdout.strip():
149
- return True
150
-
151
- # Fallback to ps grep if pidof doesn't work
152
- result = self.run_adb_command_as_root("ps | grep frida-server | grep -v grep")
153
- if result.stdout.strip():
154
- return True
155
-
156
- # Try alternative ps command format
157
- result = self.run_adb_command_as_root("ps -A | grep frida-server | grep -v grep")
158
- if result.stdout.strip():
159
- return True
160
-
161
- return False
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.run_adb_command_as_root("/system/bin/killall frida-server")
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 number: string
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 number: string
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 number: string
472
+ :type path: string
218
473
  :param version: The version. By default the latest version will be used.
219
- :type number: string
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
- def run_adb_command_as_root(self,command):
325
- if self.adb_check_root() == False:
326
- 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.")
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.is_magisk_mode:
330
- output = subprocess.run(['adb', 'shell','su -c '+command], capture_output=True, text=True)
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
- output = subprocess.run(['adb', 'shell','su 0 '+command], capture_output=True, text=True)
612
+ # Traditional su mode
613
+ adb_cmd = self._build_adb_command(['shell', f'su 0 {command}'])
333
614
 
334
- return output
615
+ return subprocess.run(adb_cmd, capture_output=True, text=True)
335
616
 
336
617
 
337
- def _adb_push_file(self,file,dst):
338
- output = subprocess.run(['adb', 'push',file,dst], capture_output=True, text=True)
339
- return output
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
- output = self.run_adb_command_as_root("ls "+path)
361
- if len(output.stderr) > 1:
362
- return False
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
- return True
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 adb_check_root(self):
369
- if bool(subprocess.run(['adb', 'shell','su -v'], capture_output=True, text=True).stdout):
370
- self.is_magisk_mode = True
371
- return True
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
- return bool(subprocess.run(['adb', 'shell','su 0 id -u'], capture_output=True, text=True).stdout)
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
- output = self.run_adb_command_as_root("rm "+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}")
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
-
@@ -2,4 +2,4 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  __author__ = "Daniel Baier"
5
- __version__ = "1.9.0"
5
+ __version__ = "1.9.2"
@@ -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. This method will also
20
- register an atexit(), ensuring that cleanup operations
21
- are performed on jobs when this class is GC'd.
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 = ['adb', 'shell', 'am', 'start', '-n', f'{package_name}/{main_activity}']
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 = ['adb', 'shell', 'monkey', '-p', package_name, '-c', 'android.intent.category.LAUNCHER', '1']
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 = ['adb', 'shell', 'pidof', package_name]
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
- subprocess.run(["adb", "shell", "am", "force-stop", app_package])
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
- subprocess.run(["adb", "shell", "am", "force-stop", app_package])
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
- subprocess.run(["adb", "shell", "kill", str(pid)])
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
- # we can also use the IP address ot the target machine instead of using USB - e.g. when we have multpile AVDs
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.0
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
- ![version](https://img.shields.io/badge/version-1.9.0-blue) [![PyPI version](https://badge.fury.io/py/AndroidFridaManager.svg)](https://badge.fury.io/py/AndroidFridaManager) [![Publish status](https://github.com/fkie-cad/friTap/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/fkie-cad/AndroidFridaManager/actions/workflows/publish-to-pypi.yml)
36
+ ![version](https://img.shields.io/badge/version-1.9.2-blue) [![PyPI version](https://badge.fury.io/py/AndroidFridaManager.svg)](https://badge.fury.io/py/AndroidFridaManager) [![Publish status](https://github.com/fkie-cad/friTap/actions/workflows/publish.yml/badge.svg?branch=main)](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,,