BadPI - A bad USB device


Figure 1 : BadPi

Inspired by the Hak5 rubber ducky LINK and bad-usb devices made from Arduinos LINK decided to build one for fun from a Pi zero W. These devices emulate human-input-devices (hid) such as keyboards, mice and joysticks. They can also emulate network device i.e. ethernet-over-USB, allowing you to create SSH bridges using the Wifi interface. There are Pi images out there for such devices, but being a 'trusting' soul wanted to build one from source. Also, in addition to this functionality also wanted to add some input / output interfaces on the BadPi, to allow you to set its operating mode and functions once plugged into a target machine. This does increase its size, as shown in figure 1, no longer has the appearance of a USB pen-drive, more of an external USB hard disk, or MP3 player. Below is an outline of the work so far, some success, but not finished yet, still work to be done.

Table of Contents

Basic Hardware / Software
UART, RNDIS & ECM
USB disk
Ducky scripts
Wifi bridge
Wifi sniffer
Ethernet sniffer

Basic Hardware / Software

The first challenge was to get the USB hid emulation work. Googling around found this web page LINK, initially no joy. Further reading found that the kernel had been updated since this tutorial was written, so the methods described do not work on the latest versions of Raspian. Solution, roll back to a previous version LINK, to cut a long story short, downloaded a lot of older versions and found that 2017-02-16-raspbian-jessie worked ok for me (do confess lost count, but it was around that version). To check you have the right kernel version as me run uname:

uname -a
Linux raspberrypi 4.4.50+ #970 Mon Feb 20 19:12:50 GMT 2017 armv6l GNU/Linux

As shown, for me this returns version 4.4.50. At its core my implementation follows this tutorial, a few modifications as i wanted to be able to select what 'gadgets' were enabled on boot using dip switches. Basic install is as follows. Add to the end of /boot/config.txt:

dtoverlay=dwc2

Next add to the end of /etc/modules, loads the required modules at boot, also enables the I2C interface that is used to control the display:

i2c-dev
dwc2
libcomposite

Note, small problem i found that when these modules are enabled i had problems trying to get the local keyboard to work, not surprising if the Pi is emulating a keyboard. Not a big issue as normally running the Pi headless and SSHing. Therefore, make sure you have setup the Wifi interface before you reboot, otherwise you won't be able to access the Pi. Later, can also open a terminal over serial and Ethernet-over-USB i.e. a peer-to-peer connection.

To allow the Pi to be powered and communicate over a normal USB cable, you need to solder on an old USB cable. Cut and strip a USB cable with a male USB A connector (shown in figure 1), this will expose 5 cables:

Solder these wires to the corresponding coloured pads shown in figure 2, the trick here is to tin these wires first, then trim back the wire to a short stub, this helps prevent the insulation shrinking further when you solder them to the pad i.e. helps ensure the wire does not bridge a track on the pcb. Heat shrink or tape the screen cable so that it does not touch the pcb.


Figure 2 : Pi USB solder pads

Rather than running a script from rc.local as suggested in the original tutorial i set a sudo cron job to run at boot, at the command line type: sudo crontab -e then enter:

@reboot date >> /home/pi/bin/error
@reboot date >> /home/pi/bin/log
@reboot /home/pi/bin/go.sh 1>>/home/pi/bin/log 2>>/home/pi/bin/error

The script go.sh is shown below. This script changes directory to ~/bin so that any files produced go into this directory, it then reads the dip switches connected to GPIO lines, shown in figure 3, writing their state to the file mode. The DIP switches do not need any external circuitry, using internal pull-ups. As they are only read on boot switch bounce is also not an issue.

#!/bin/sh
#go.sh - start script
cd /home/pi/bin
sudo /usr/bin/python /home/pi/bin/dipSwitches.py
sudo /home/pi/bin/startBoot.sh
sleep 0.5 
sudo /home/pi/bin/startBadUsb.sh 
sleep 0.5 
sudo /usr/bin/python /home/pi/bin/displayButtons.py

The code for dipSwitches.py is below, reads GPIO lines and sets associated bit in variable value before writting this data to the file mode.

#dipSwitches.py
import RPi.GPIO as GPIO
import time

GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)

# Inputs: 18, 23, 24, 25

GPIO.setup(18, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(24, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(25, GPIO.IN, pull_up_down=GPIO.PUD_UP)

value = 0
if (GPIO.input(18)==1):
        value = value | 8

if (GPIO.input(23)==1):
        value = value | 4
if (GPIO.input(24)==1):
        value = value | 2
if (GPIO.input(25)==1):
        value = value | 1

print value

f = open("mode", "w")
f.write(str(value))
f.close()



Figure 3 : DIP switches

The script startBoot.sh is then executed, not really needed, this writes to the display status/debug information, allowing the user to see what mode the Pi is booting into, useful for a headless box.

#!/bin/sh
# startBoot.sh - display debug/status info

if test -e mode
then
        mode=`cat mode`
        if test $mode -eq 1
        then
                #keyboard + serial      
                echo badUsbMode1
                /usr/bin/python /home/pi/bin/bootMessage.py 1 
        else
                if test $mode -eq 2
                then
                        #keyboard + serial + disk
                        echo badUsbMode2
                        /usr/bin/python /home/pi/bin/bootMessage.py 2 
                else
                        if test $mode -eq 3
                        then
                                #keyboard + serial + disk + ethernet
                                echo badUsbMode3
                                /usr/bin/python /home/pi/bin/bootMessage.py 3 
                        else
                                echo baseRaspberryPi    
                        fi
                fi
        fi
fi 

The display shown in figure 4 is a OLED 128x64 display, controlled using bootMessage.py below. This is based on the Adafruit libs LINK, just works out of the box, as always Adafruit has very good software and support. Note, the display is powered from the +3V & 0V supply from the GPIO header. SDA and SCL are also just wired to the header, no external pull-up resistors used.

# bootMessage.py

import Adafruit_SSD1306
import time

from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw

import sys 


def menu():
    global payloads
    print sys.argv[0], sys.argv[1]

    # Draw a black filled box to clear the image.
    draw.rectangle((0,0,width,height), outline=0, fill=0)
    
    if(sys.argv[1] == '0'):
        size = 9
        offset = 1 
        draw.text((left, top),                 "Booting ...",  font=font, fill=255)
        draw.text((left, top+(size*1)+offset), "  PiZero", font=font, fill=255)

    elif(sys.argv[1] == '1'):
        size = 9
        offset = 1 
        draw.text((left, top),                 "Booting ...",  font=font, fill=255)
        draw.text((left, top+(size*1)+offset), "  Keyboard", font=font, fill=255)
        draw.text((left, top+(size*2)+offset), "  Serial", font=font, fill=255)
    
    elif(sys.argv[1] == '2'):
        size = 9
        offset = 1 
        draw.text((left, top),                 "Booting ...",  font=font, fill=255)
        draw.text((left, top+(size*1)+offset), "  Keyboard", font=font, fill=255)
        draw.text((left, top+(size*2)+offset), "  Serial", font=font, fill=255)
        draw.text((left, top+(size*3)+offset), "  Disk", font=font, fill=255)

    elif(sys.argv[1] == '3'):
        size = 9
        offset = 1 
        draw.text((left, top),                 "Booting ...",  font=font, fill=255)
        draw.text((left, top+(size*1)+offset), "  Keyboard", font=font, fill=255)
        draw.text((left, top+(size*2)+offset), "  Serial", font=font, fill=255)
        draw.text((left, top+(size*3)+offset), "  Disk", font=font, fill=255)
        draw.text((left, top+(size*4)+offset), "  Ethernet", font=font, fill=255)

    else:
        size = 9
        offset = 1 
        draw.text((left, top),                 "Booting ...",  font=font, fill=255)
        draw.text((left, top+(size*1)+offset), "  Default", font=font, fill=255)


    # Display image.
    disp.image(image)
    disp.display()

RST = None     # on the PiOLED this pin isnt used
disp = Adafruit_SSD1306.SSD1306_128_64(rst=RST, i2c_address=0x3c)
disp.begin()

# Clear display.
disp.clear()
disp.display()

width = disp.width
height = disp.height

padding = 1 
top = padding
bottom = height-padding
left = 1
right = 126

# Create blank image for drawing.
# Make sure to create image with mode '1' for 1-bit color.
image = Image.new('1', (width, height))

# Get drawing object to draw on image.
draw = ImageDraw.Draw(image)

# Load font.
font = ImageFont.load_default()
#font = ImageFont.truetype('Minecraft.ttf', 12)
#font = ImageFont.truetype('visitor1.ttf', 9)
#font = ImageFont.truetype('apple.ttf', 7)

menu()

time.sleep(0.5)



Figure 4 : Display

USB gadgets are started using startBoot.sh, shown below.

#!/bin/sh
# startBoot.sh

if test -e mode
then
	mode=`cat mode`
	if test $mode -eq 1
	then
		#keyboard + serial	
		echo badUsbMode1
		/home/pi/bin/init-1.sh
	else
		if test $mode -eq 2
		then
			#keyboard + serial + disk
			echo badUsbMode2
                        /home/pi/bin/init-2.sh
		else
			if test $mode -eq 3
			then
				#keyboard + serial + disk + ethernet
				echo badUsbMode3
                                /home/pi/bin/init-3.sh
			else
				echo baseRaspberryPi 	
			fi
		fi
	fi
fi 

The case is a modified version of one on Thingiverse LINK, .stl is available here: LINK. Need to modify top/bottom edges such that it clips together, at present just gone for glue. Also need to add a hole for the DIP switch, cut using a knife/file. Gadget configuration is copied from the original tutorial LINK, this works fine, except that Ethernet over USB, doesn't work under Windows, as Windows does not support this type of device as standard (discussed in next section).


Figure 5 : Case

#!/bin/bash
#init-3.sh

cd /sys/kernel/config/usb_gadget/
mkdir -p g1
cd g1
echo 0x1d6b > idVendor  # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget

#echo 0x04b3 > idVendor  # RNDIS id codes 
#echo 0x4010 > idProduct # 

echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB2

mkdir -p strings/0x409
echo "deadbeef01234567890" > strings/0x409/serialnumber
echo "example.com" > strings/0x409/manufacturer
echo "Generic USB Keyboard" > strings/0x409/product

N="usb0"
mkdir -p functions/hid.$N
echo 1 > functions/hid.usb0/protocol
echo 1 > functions/hid.usb0/subclass
echo 8 > functions/hid.usb0/report_length
echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > functions/hid.usb0/report_desc

C=1
mkdir -p configs/c.$C/strings/0x409
echo "Config $C: ECM network" > configs/c.$C/strings/0x409/configuration 
echo 250 > configs/c.$C/MaxPower 
ln -s functions/hid.$N configs/c.$C/

FILE=/home/pi/Disk/usbDisk.img
mkdir -p ${FILE/img/d}
mount -o loop,ro, -t vfat $FILE ${FILE/img/d} # FOR IMAGE CREATED WITH DD
mkdir -p functions/mass_storage.usb0
echo 1 > functions/mass_storage.usb0/stall
echo 0 > functions/mass_storage.usb0/lun.0/cdrom
echo 0 > functions/mass_storage.usb0/lun.0/ro
echo 0 > functions/mass_storage.usb0/lun.0/nofua
echo $FILE > functions/mass_storage.usb0/lun.0/file
ln -s functions/mass_storage.usb0 configs/c.1/

mkdir -p functions/acm.usb0
ln -s functions/acm.usb0 configs/c.1/

mkdir -p functions/ecm.usb0
HOST="48:6f:73:74:50:43" # "HostPC"
SELF="42:61:64:55:53:42" # "BadUSB"
echo $HOST > functions/ecm.usb0/host_addr
echo $SELF > functions/ecm.usb0/dev_addr
ln -s functions/ecm.usb0 configs/c.1/

ls /sys/class/udc > UDC

Figure 6 : USB gadget config script

Finally the boot script starts the user interface controlled by displayButtons.py allowing the user to select items on the small display, controlled using the three push buttons mounted on the side of the box i.e. up, down and select. The three side push buttons control the display cursor, as these are push buttons added a some hardware switch de-bounce, as shown in figure 7, simple CR filter. Resistor vales where just what i had to hand, perhaps would change to 10K and 100.




Figure 7 : push_button


Figure 8 : push_button

The menu screen allows the user to select different functions, at present just got around to doing Ducky scripts and a graceful shutdown, but will add other options later.

#displayButtons.py

import RPi.GPIO as GPIO
import Adafruit_SSD1306
import time

from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw

import subprocess
import os

GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)

#Pins 17=bottom, 22=middle, 27=top

GPIO.setup(17, GPIO.IN)
GPIO.setup(27, GPIO.IN)
GPIO.setup(22, GPIO.IN)

def bottom_click(pin):
    global cursorX, cursorY, selection, screen, payloads
    print "bottom button clicked"
    if(screen == 0):
        # DUCKY
        if(selection == 1):
            payloads = os.listdir("/home/pi/Ducky/Payloads")
            screen = 1
            selection = 1 
            cursorX = 0     
            cursorY = 14
            menu()
        # STATS
        if(selection == 2):
            screen = 2
            cursorX = 0     
            cursorY = 14 + cursorS*5
            menu()
        # NU 
        if(selection == 3):
            screen = 0
            menu()
        # NU
        if(selection == 4):
            screen = 4
            menu()
        # NU
        if(selection == 5):
            screen = 5
            menu()
        # SHUTDOWN
        if(selection == 6):
            screen = 6
            cursorX = 0     
            cursorY = 14 + cursorS*5
            menu()

    # DUCKY
    elif(screen == 1):
        if(selection == 6):
            screen = 0
            selection = 1 
            cursorX = 0     
            cursorY = 14
            menu()
	else:
            duckyString = "sudo /home/pi/Ducky/go.sh " + payloads[selection-1]
            print duckyString
            os.system(duckyString) 

    # STATS
    elif(screen == 2):
        screen = 0
        selection = 1 
        cursorX = 0     
        cursorY = 14 
        menu()
 
def middle_click(pin):
    global cursorY, cursorS, selection
    print "middle button clicked"
    if(screen == 0 or screen == 1):
        if(selection < 6):
            selection = selection + 1
            cursorY = cursorY + cursorS 
            menu()

def top_click(pin):
    global cursorY, cursorS, selection
    print "top button clicked"
    if(screen == 0 or screen == 1):
        if(selection > 1):
            selection = selection - 1    
            cursorY = cursorY - cursorS 
            menu()

def menu():
    global payloads

    # Draw a black filled box to clear the image.
    draw.rectangle((0,0,width,height), outline=0, fill=0)
    
    if(screen == 0):
        # MAIN MENU
        size = 9
        offset = 0 
        draw.text((left, top),                 "Bad USB Menu",  font=font, fill=255)
        draw.text((left, top+(size*1)+offset), "   Ducky", font=font, fill=255)
        draw.text((left, top+(size*2)+offset), "   Stats",  font=font, fill=255)
        draw.text((left, top+(size*3)+offset), "        ",  font=font, fill=255)
        draw.text((left, top+(size*6)+offset), "   Shutdown",  font=font, fill=255)

        draw.rectangle((cursorX,cursorY,cursorX+cursorW,cursorY+cursorH), outline=255, fill=1)
        draw.line((0,11,74,11), fill=255)

    elif(screen == 1):
        # DUCKY
        size = 9
        offset = 0 
        draw.text((left, top),                 "Ducky Scripts",  font=font, fill=255)
        draw.text((left, top+(size*1)+offset), "   " + str(payloads[0]), font=font, fill=255)
        draw.text((left, top+(size*2)+offset), "   " + str(payloads[1]), font=font, fill=255)
        draw.text((left, top+(size*3)+offset), "   " + str(payloads[2]), font=font, fill=255)
        draw.text((left, top+(size*4)+offset), "   " + str(payloads[3]), font=font, fill=255)
        draw.text((left, top+(size*6)+offset), "   Back",  font=font, fill=255)

        draw.rectangle((cursorX,cursorY,cursorX+cursorW,cursorY+cursorH), outline=255, fill=1)
        draw.line((0,11,79,11), fill=255)

    elif(screen == 2):
        # SYSTEM STATS
        stats()

    elif(screen == 3):
        # NU
        print ""
    
    elif(screen == 4):
        # NU
        print ""

    elif(screen == 5):
        # NU
        print ""

    elif(screen == 6):
        # SHUTDOWN
        size = 9
        offset = 0 
        draw.text((left, top), "Shutdown ...",  font=font, fill=255)
        disp.image(image)
        disp.display()
        time.sleep(0.5)
        os.system("shutdown -h now")

    else:
        print "Menu error"

    # Display image.
    disp.image(image)
    disp.display()

def stats():
    # Draw a black filled box to clear the image.
    draw.rectangle((0,0,width,height), outline=0, fill=0)
    
    cmd = "hostname -I | cut -d\' \' -f1"
    IP = subprocess.check_output(cmd, shell = True )
    cmd = "top -bn1 | grep load | awk '{printf \"CPU Load: %.2f\", $(NF-2)}'"
    CPU = subprocess.check_output(cmd, shell = True )
    cmd = "free -m | awk 'NR==2{printf \"Mem: %s/%sMB %.2f%%\", $3,$2,$3*100/$2 }'"
    MemUsage = subprocess.check_output(cmd, shell = True )
    cmd = "df -h | awk '$NF==\"/\"{printf \"Disk: %d/%dGB %s\", $3,$2,$5}'"
    Disk = subprocess.check_output(cmd, shell = True )

    size = 9 
    draw.text((left, top),          "IP: " + str(IP),  font=font, fill=255)
    draw.text((left, top+(size*1)), str(CPU), font=font, fill=255)
    draw.text((left, top+(size*2)), str(MemUsage),  font=font, fill=255)
    draw.text((left, top+(size*3)), str(Disk),  font=font, fill=255)
    draw.text((left, top+(size*6)), "  Back",  font=font, fill=255)
    
    draw.rectangle((cursorX,cursorY,cursorX+cursorW,cursorY+cursorH), outline=255, fill=1)

    # Display image.
    disp.image(image)
    disp.display()


GPIO.add_event_detect(17, GPIO.FALLING, callback=bottom_click, bouncetime=500)
GPIO.add_event_detect(22, GPIO.FALLING, callback=middle_click, bouncetime=500)
GPIO.add_event_detect(27, GPIO.FALLING, callback=top_click,    bouncetime=500)

RST = None     # on the PiOLED this pin isnt used
disp = Adafruit_SSD1306.SSD1306_128_64(rst=RST, i2c_address=0x3c)
disp.begin()

# Clear display.
disp.clear()
disp.display()

width = disp.width
height = disp.height

cursorX = 0     
cursorY = 14
cursorW = 10
cursorH = 5
cursorS = 9

screen = 0
selection = 1
payloads = []

padding = 1 
top = padding
bottom = height-padding
left = 1
right = 126

# Create blank image for drawing.
# Make sure to create image with mode '1' for 1-bit color.
image = Image.new('1', (width, height))

# Get drawing object to draw on image.
draw = ImageDraw.Draw(image)

# Load font.
font = ImageFont.load_default()
#font = ImageFont.truetype('Minecraft.ttf', 12)
#font = ImageFont.truetype('visitor1.ttf', 9)
#font = ImageFont.truetype('apple.ttf', 7)

menu()

while True:
    time.sleep(500)

UART, RNDIS & ECM

To connect to the Pi remotely you can use either the Serial or Ethernet ports. Being able to SSH into the Pi using a serial link is a must have, very useful, allows you to get the Wifi interface working without having to connect a monitor and a keyboard (as noted, keyboard may no longer work with these mods). Also it more reliable than Ethernet-over-USB when moving between Windows and Linux (also had joys of NetWork managers automatically changing network interface settings). To allow you to log into the Pi over a serial interface at the command line enter raspi-config, then select:

The second option increases the memory available to the processor as we will not be running GPU intensive programs. Using the USB serial port configured in figure 6, you can now log into the Pi using a serial terminal. On the host machine you should see a new terminal device or comm port. Under Linux i see it as /dev/ttyACM0(name may vary), at the command prompt type ls /dev/tty* to identify the new device. Under Windows you can find out the comm port number by running Device Manager and looking under Ports(COM & LPT). To connect to the Pi under Linux, type:

screen /dev/ttyACM0 115200

You may need to press ENTER to display the log-in text, but otherwise good to go. For more info on the screen command see LINK, the key command to remember is CTL+A k, if you wish to stop screen running. Under Linux the ECM Ethernet-over-USB module works out of the box, just need to decide on the MAC addresses i.e. for HOST and Pi network interfaces, i went with the tutorial values. If you run ifconfig on the PC the network USB device appears for me as eth2, to assign an IP address enter:

sudo ifconfig eth2 192.168.0.1

Using the serial terminal do the same on the Pi, if you run ifconfig on the network USB device appears for me as usb0, i assigned it the IP address 192.168.0.2. To connect to the Pi simply type on the PC command line (and enter password when prompted):

ssh pi@192.168.0.2

Problem number 1, the Linux network manager resets the IP address. To stop this had to kill the default network manager on the PC. Also have troubles when you are working under Windows as by default it does not recognise ECM devices LINK:

"The main industry protocols are (in chronological order): Remote NDIS (RNDIS, a Microsoft vendor protocol), Ethernet Control Model (ECM), Ethernet Emulation Model (EEM), and Network Control Model (NCM). The latter three are part of the larger Communications Device Class (CDC) group of protocols of the USB Implementers Forum (USB-IF)."

To fix this issue found these solutions, but have not had a chance to try them out yet

USB disk

This one worked as tutorial, but a slightly quicker method of creating a 2GB disk image:

fallocate -l 2048M usbDisk.img
mkfs.fat usbDisk.img 

Renamed disk to "DUCKY" using the standard Windows file manager. Stored on this disk are some useful tools : LINK.

Ducky scripts

The configuration script shown in figure 6 create a Human Interface Device (hid) keyboard (/dev/hidg0). To send characters across the USB cable to the PC 'all' you need to do is sent the correct control sequences to this device. To do this i found a nice piece of C code: LINK, compile this up to produce the executable hid-gadget-test, you can then just pass/pipe characters to this program, sending them to the PC:

sudo chmod -R 777 /dev/hidg0 
echo 'a' | hid-gadget-test /dev/hidg0 keyboard

Using this we can now create and use Ducky scripts LINK. To convert Ducky Scripts into a suitable format/sequence of characters for hid-gadget-test i modified this python code LINK. My coding i would describe as functional :), the important thing is it works.

Update: 5 Sep 2017, this project paused, last used as in an open day demo to illustrate the dangers of plugging a USB device of unknown origins into your computer :). Thing i would like to play with are:

Wifi bridge

Wifi sniffer

Ethernet sniffer

Creative Commons Licence

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

Contact details: email - mike.freeman@york.ac.uk, telephone - 01904 32(5473)

Back