AndroidFridaManager 1.9.0__tar.gz → 1.9.2__tar.gz

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