AndroidFridaManager 1.9.0__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.
@@ -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: 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
@@ -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
- command = f"""adb shell "su -c 'sh -c \"{cmd}\"'" """
317
+ shell_cmd = f"""su -c 'sh -c "{cmd}"'"""
104
318
  else:
105
- command = f"""adb shell "su 0 sh -c \\"{cmd}\\"\" """
319
+ shell_cmd = f"""su 0 sh -c "{cmd}" """
106
320
 
107
321
  try:
108
- process = subprocess.Popen(command, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
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 "Address already in use" in stderr.decode():
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: {stderr.decode()}")
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
- The test is done by trying multiple methods to detect the frida-server process.
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
- # 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
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.run_adb_command_as_root("/system/bin/killall frida-server")
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 number: string
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 number: string
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 number: string
457
+ :type path: string
218
458
  :param version: The version. By default the latest version will be used.
219
- :type number: string
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 run_adb_command_as_root(self,command):
325
- if self.adb_check_root() == False:
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
- output = subprocess.run(['adb', 'shell','su -c '+command], capture_output=True, text=True)
586
+ adb_cmd = self._build_adb_command(['shell', f'su -c {command}'])
331
587
  else:
332
- output = subprocess.run(['adb', 'shell','su 0 '+command], capture_output=True, text=True)
588
+ adb_cmd = self._build_adb_command(['shell', f'su 0 {command}'])
333
589
 
334
- return output
590
+ return subprocess.run(adb_cmd, capture_output=True, text=True)
335
591
 
336
592
 
337
- def _adb_push_file(self,file,dst):
338
- output = subprocess.run(['adb', 'push',file,dst], capture_output=True, text=True)
339
- return output
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
- output = self.run_adb_command_as_root("ls "+path)
361
- if len(output.stderr) > 1:
362
- return False
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
- return True
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 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
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
- return bool(subprocess.run(['adb', 'shell','su 0 id -u'], capture_output=True, text=True).stdout)
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
- output = self.run_adb_command_as_root("rm "+path)
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
-
@@ -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.1"
@@ -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.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
- ![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.1-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=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=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,,