Files
KeyDive/keydive/adb.py
T

288 lines
11 KiB
Python
Raw Normal View History

2024-10-26 15:22:13 +02:00
import logging
import re
import shutil
import subprocess
2025-01-19 14:13:07 +01:00
from pathlib import Path
2024-10-26 15:22:13 +02:00
import frida
import requests
2024-10-27 19:38:42 +01:00
from frida.core import Device
2024-10-26 15:22:13 +02:00
# Suppress urllib3 warnings
2025-01-19 14:13:07 +01:00
logging.getLogger("urllib3.connectionpool").setLevel(logging.ERROR)
2024-10-26 15:22:13 +02:00
def shell(prompt: list) -> subprocess.CompletedProcess:
"""
2025-01-19 14:13:07 +01:00
Executes a shell command and returns the result.
2024-10-26 15:22:13 +02:00
2025-01-19 14:13:07 +01:00
Parameters:
prompt (list): The command to execute as a list of strings.
2024-10-26 15:22:13 +02:00
Returns:
2025-01-19 14:13:07 +01:00
subprocess.CompletedProcess: The result containing return code, stdout, and stderr.
2024-10-26 15:22:13 +02:00
"""
2025-01-19 14:13:07 +01:00
prompt = list(map(str, prompt)) # Ensure all command parts are strings
# logging.getLogger("Shell").debug(" ".join(prompt))
return subprocess.run(prompt, capture_output=True) # Run the command and capture output
2024-10-26 15:22:13 +02:00
class ADB:
"""
Class for managing interactions with the Android device via ADB.
"""
def __init__(self, device: str = None, timeout: int = 5):
"""
2025-01-19 14:13:07 +01:00
Initializes ADB connection to the device.
2024-10-26 15:22:13 +02:00
2025-01-19 14:13:07 +01:00
Parameters:
device (str, optional): Device ID to connect to, defaults to the first USB device.
timeout (int, optional): Timeout for connection in seconds. Defaults to 5.
2024-10-26 15:22:13 +02:00
Raises:
EnvironmentError: If ADB is not found in the system path.
2025-01-19 14:13:07 +01:00
Exception: If connection to the device fails.
2024-10-26 15:22:13 +02:00
"""
self.logger = logging.getLogger(self.__class__.__name__)
# Ensure ADB is available
2025-01-19 14:13:07 +01:00
if not shutil.which("adb"):
2024-10-26 15:22:13 +02:00
raise EnvironmentError(
2025-01-19 14:13:07 +01:00
"ADB is not recognized as an environment variable. "
"Ensure ADB is installed and refer to the documentation: "
"https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#adb-android-debug-bridge"
2024-10-26 15:22:13 +02:00
)
# Start the ADB server if not already running
sp = shell(['adb', 'start-server'])
if sp.returncode != 0:
2025-01-19 14:13:07 +01:00
self.logger.warning("ADB server startup failed (Error: %s)", sp.stdout.decode("utf-8").strip())
2024-10-26 15:22:13 +02:00
2025-01-19 14:13:07 +01:00
# Connect to device (or default to the first USB device)
2024-10-26 15:22:13 +02:00
try:
self.device: Device = frida.get_device(id=device, timeout=timeout) if device else frida.get_usb_device(timeout=timeout)
2025-01-19 14:13:07 +01:00
self.logger.info("Connected to device: %s (%s)", self.device.name, self.device.id)
2024-10-26 15:22:13 +02:00
except Exception as e:
2025-01-19 14:13:07 +01:00
self.logger.error("Failed to connect to device: %s", e)
2024-10-26 15:22:13 +02:00
raise e
self.prompt = ['adb', '-s', self.device.id, 'shell']
2025-01-19 14:13:07 +01:00
# Retrieve and log device properties
2024-10-26 15:22:13 +02:00
properties = self.device_properties()
if properties:
2025-01-19 14:13:07 +01:00
self.logger.info("SDK API: %s", properties.get("ro.build.version.sdk", "Unknown"))
self.logger.info("ABI CPU: %s", properties.get("ro.product.cpu.abi", "Unknown"))
2024-10-26 15:22:13 +02:00
else:
2025-01-19 14:13:07 +01:00
self.logger.warning("No device properties retrieved")
2024-10-26 15:22:13 +02:00
def device_properties(self) -> dict:
"""
2025-01-19 14:13:07 +01:00
Retrieves system properties from the device.
2024-10-26 15:22:13 +02:00
Returns:
2025-01-19 14:13:07 +01:00
dict: A dictionary mapping property keys to their corresponding values.
2024-10-26 15:22:13 +02:00
"""
# https://source.android.com/docs/core/architecture/configuration/add-system-properties?#shell-commands
properties = {}
2025-01-19 14:13:07 +01:00
# Execute the shell command to retrieve device properties
2024-10-26 15:22:13 +02:00
sp = shell([*self.prompt, 'getprop'])
if sp.returncode != 0:
2025-01-19 14:13:07 +01:00
self.logger.error("Failed to retrieve device properties (Error: %s)", sp.stdout.decode("utf-8").strip())
2024-10-26 15:22:13 +02:00
return properties
2025-01-19 14:13:07 +01:00
# Parse the output and cast values accordingly
for line in sp.stdout.decode("utf-8").splitlines():
match = re.match(r"\[(.*?)\]: \[(.*?)\]", line)
2024-10-26 15:22:13 +02:00
if match:
key, value = match.groups()
2025-01-19 14:13:07 +01:00
# Cast numeric and boolean values where appropriate
2024-10-26 15:22:13 +02:00
if value.isdigit():
value = int(value)
2025-01-19 14:13:07 +01:00
elif value.lower() in ("true", "false"):
value = value.lower() == "true"
2024-10-26 15:22:13 +02:00
properties[key] = value
return properties
def list_applications(self, user: bool = True, system: bool = False) -> dict:
"""
2025-01-19 14:13:07 +01:00
Lists installed applications on the device, with optional filters for user/system apps.
2024-10-26 15:22:13 +02:00
2025-01-19 14:13:07 +01:00
Parameters:
user (bool, optional): Include user-installed apps. Defaults to True.
system (bool, optional): Include system apps. Defaults to False.
2024-10-26 15:22:13 +02:00
Returns:
2025-01-19 14:13:07 +01:00
dict: A dictionary of application packages and their file paths.
2024-10-26 15:22:13 +02:00
"""
applications = {}
2025-01-19 14:13:07 +01:00
# Validate input; return empty dict if no filter is set
2024-10-26 15:22:13 +02:00
if not user and not system:
return applications
2025-01-19 14:13:07 +01:00
# Set the appropriate shell command based on user/system filters
2024-10-26 15:22:13 +02:00
prompt = [*self.prompt, 'pm', 'list', 'packages', '-f']
if user and not system:
2025-01-19 14:13:07 +01:00
prompt.append("-3")
2024-10-26 15:22:13 +02:00
elif not user and system:
2025-01-19 14:13:07 +01:00
prompt.append("-s")
2024-10-26 15:22:13 +02:00
2025-01-19 14:13:07 +01:00
# Execute the shell command to list applications
2024-10-26 15:22:13 +02:00
sp = shell(prompt)
if sp.returncode != 0:
2025-01-19 14:13:07 +01:00
self.logger.error("Failed to retrieve app list (Error: %s)", sp.stdout.decode("utf-8").strip())
2024-10-26 15:22:13 +02:00
return applications
2025-01-19 14:13:07 +01:00
# Parse and add applications to the dictionary
for line in sp.stdout.decode("utf-8").splitlines():
2024-11-01 19:27:11 +01:00
try:
2025-01-19 14:13:07 +01:00
path, package = line.strip().split(":", 1)[1].rsplit("=", 1)
2024-11-01 19:27:11 +01:00
applications[package] = path
except Exception as e:
pass
2024-10-26 15:22:13 +02:00
return applications
def start_application(self, package: str) -> bool:
"""
Starts an application by its package name.
2025-01-19 14:13:07 +01:00
Parameters:
package (str): The package name of the application.
2024-10-26 15:22:13 +02:00
Returns:
2025-01-19 14:13:07 +01:00
bool: True if the app was started successfully, False otherwise.
2024-10-26 15:22:13 +02:00
"""
2025-01-19 14:13:07 +01:00
# Get package information using dumpsys
2024-10-26 15:22:13 +02:00
sp = shell([*self.prompt, 'dumpsys', 'package', package])
2025-01-19 14:13:07 +01:00
lines = sp.stdout.decode("utf-8").splitlines()
2024-10-26 15:22:13 +02:00
2024-11-01 19:27:11 +01:00
# Remove empty lines to ensure backwards compatibility
lines = [l.strip() for l in lines if l.strip()]
2025-01-19 14:13:07 +01:00
# Look for MAIN activity to identify entry point
2024-10-26 15:22:13 +02:00
for i, line in enumerate(lines):
2025-01-19 14:13:07 +01:00
if "android.intent.action.MAIN" in line:
match = re.search(fr"({package}/[^ ]+)", lines[i + 1])
2024-10-26 15:22:13 +02:00
if match:
2025-01-19 14:13:07 +01:00
# Start the application by its main activity
2024-10-26 15:22:13 +02:00
main_activity = match.group()
sp = shell([*self.prompt, 'am', 'start', '-n', main_activity])
if sp.returncode == 0:
return True
2025-01-19 14:13:07 +01:00
self.logger.error("Failed to start app %s (Error: %s)", package, sp.stdout.decode("utf-8").strip())
2024-10-26 15:22:13 +02:00
break
2025-01-19 14:13:07 +01:00
self.logger.error("Package %s not found or no MAIN intent", package)
2024-10-26 15:22:13 +02:00
return False
def enumerate_processes(self) -> dict:
"""
2025-01-19 14:13:07 +01:00
Lists running processes and maps process names to their PIDs.
2024-10-26 15:22:13 +02:00
Returns:
2025-01-19 14:13:07 +01:00
dict: Dictionary of process names and corresponding PIDs.
2024-10-26 15:22:13 +02:00
"""
# https://github.com/frida/frida/issues/1225#issuecomment-604181822
processes = {}
2025-01-19 14:13:07 +01:00
# Attempt to get the list of processes using the 'ps -A' command
2024-10-26 15:22:13 +02:00
prompt = [*self.prompt, 'ps']
sp = shell([*prompt, '-A'])
2025-01-19 14:13:07 +01:00
lines = sp.stdout.decode("utf-8").splitlines()
2024-11-01 18:08:46 +01:00
2025-01-19 14:13:07 +01:00
# If the output has less than 10 lines, retry with a simpler 'ps' command
2024-10-26 15:22:13 +02:00
if len(lines) < 10:
sp = shell(prompt)
if sp.returncode != 0:
2025-01-19 14:13:07 +01:00
self.logger.error("Failed to execute ps command (Error: %s)", sp.stdout.decode("utf-8").strip())
2024-10-26 15:22:13 +02:00
return processes
2025-01-19 14:13:07 +01:00
lines = sp.stdout.decode("utf-8").splitlines()
2024-10-26 15:22:13 +02:00
# Iterate through lines starting from the second line (skipping header)
for line in lines[1:]:
try:
parts = line.split() # USER,PID,PPID,VSZ,RSS,WCHAN,ADDR,S,NAME
pid = int(parts[1]) # Extract PID
2025-01-19 14:13:07 +01:00
name = " ".join(parts[8:]).strip() # Extract process name
2024-10-26 15:22:13 +02:00
2025-01-19 14:13:07 +01:00
# Handle cases where process name might be in brackets (e.g., kernel threads)
name = name if name.startswith("[") else Path(name).name
2024-10-26 15:22:13 +02:00
processes[name] = pid
except Exception as e:
pass
return processes
def install_application(self, path: Path = None, url: str = None) -> bool:
"""
2025-01-19 14:13:07 +01:00
Installs an application on the device either from a local file or by downloading from a URL.
2024-10-26 15:22:13 +02:00
2025-01-19 14:13:07 +01:00
Parameters:
2024-10-26 15:22:13 +02:00
path (Path, optional): The local file path of the APK to install. Defaults to None.
url (str, optional): The URL to download the APK from. Defaults to None.
Returns:
bool: True if the installation was successful, False otherwise.
"""
2025-01-19 14:13:07 +01:00
# Prepare the shell command for installation
2024-10-26 15:22:13 +02:00
prompt = [*self.prompt[:-1], 'install']
2025-01-19 14:13:07 +01:00
# Install from a local file path if a valid path is provided
2024-10-26 15:22:13 +02:00
if path and path.is_file():
2025-01-19 14:13:07 +01:00
sp = shell([*prompt, path]) # Run the installation command with the local file path
2024-10-26 15:22:13 +02:00
if sp.returncode == 0:
return True
2025-01-19 14:13:07 +01:00
self.logger.error("Installation failed for local path: %s (Error: %s)", path, sp.stdout.decode("utf-8").strip())
2024-10-26 15:22:13 +02:00
2025-01-19 14:13:07 +01:00
# If URL is provided, attempt to download the APK and install it
2024-10-26 15:22:13 +02:00
status = False
if url:
2025-01-19 14:13:07 +01:00
file = Path("tmp.apk") # Temporary file to store the downloaded APK
2024-10-26 15:22:13 +02:00
try:
2025-01-19 14:13:07 +01:00
# Download the APK from the provided URL
r = requests.get(url, headers={"Accept": "*/*", "User-Agent": "KeyDive/ADB"})
2024-10-26 15:22:13 +02:00
r.raise_for_status()
2025-01-19 14:13:07 +01:00
# Save the downloaded APK to a temporary file
2024-10-26 15:22:13 +02:00
file.write_bytes(r.content)
2025-01-19 14:13:07 +01:00
# Attempt installation from the downloaded APK
2024-10-26 15:22:13 +02:00
status = self.install_application(path=file)
except Exception as e:
2025-01-19 14:13:07 +01:00
self.logger.error("Failed to download application from URL: %s (Error: %s)", url, e)
file.unlink(missing_ok=True) # Clean up the temporary file, even if there was an error
2024-10-26 15:22:13 +02:00
return status
def open_url(self, url: str) -> bool:
"""
2025-01-19 14:13:07 +01:00
Opens a specified URL on the device.
2024-10-26 15:22:13 +02:00
2025-01-19 14:13:07 +01:00
Parameters:
url (str): The URL to be opened on the device.
2024-10-26 15:22:13 +02:00
Returns:
2025-01-19 14:13:07 +01:00
bool: True if the URL was successfully opened, False otherwise.
2024-10-26 15:22:13 +02:00
"""
2025-01-19 14:13:07 +01:00
# Execute the shell command to open the URL using the Android 'am' (Activity Manager) command.
2024-10-26 15:22:13 +02:00
sp = shell([*self.prompt, 'am', 'start', '-a', 'android.intent.action.VIEW', '-d', url])
2025-01-19 14:13:07 +01:00
# Check the result of the command execution and log if there is an error
2024-10-26 15:22:13 +02:00
if sp.returncode != 0:
2025-01-19 14:13:07 +01:00
self.logger.error("URL open failed for: %s (Return: %s)", url, sp.stdout.decode("utf-8").strip())
2024-10-26 15:22:13 +02:00
return False
return True
2025-01-19 14:13:07 +01:00
__all__ = ("ADB",)