USB drives are useful for transferring files on macOS systems, providing a quick and simple way to share documents, photos, and more between devices. They’re plug-and-play, allowing users to easily access and transfer data. However, it’s essential to be cautious about security. USB drives can carry malware, posing a risk to your macOS systems. Organizations should proactively implement real-time tracking and analysis of USB drive activities in their macOS systems to effectively address potential security threats.
Wazuh is a robust solution for enhancing macOS security by monitoring USB drives. With its capabilities, Wazuh can track USB drive activities, detect anomalies, and generate real-time alerts. By offering detailed insights into USB-related events, Wazuh empowers organizations to establish strict security policies, respond promptly to potential threats, and maintain the integrity of their macOS systems.
Previously, we demonstrated monitoring USB drives in Linux using Wazuh and monitoring USB drives in Windows using Wazuh. In this blog post, we explore how Wazuh boosts your protection against threats from USB drives on macOS systems.
Infrastructure
- A pre-built, ready-to-use Wazuh OVA 4.7.2. Follow this virtual machine (OVA) guide to download and set up the Wazuh virtual machine.
- A macOS Sonoma 14.3 endpoint with Wazuh agent 4.7.2 installed and enrolled to the Wazuh server. Follow this installing Wazuh agents on macOS endpoints guide to perform the installation.
Configuration
We configure the macOS endpoint using a custom Swift
script to extract information about the plugged-in USB drives from the system. The script then correlates this information with the Linux USB ID repository file usb.ids
and logs the data to a file. The Wazuh agent forwards these logs to the Wazuh server for analysis. Finally, we configure the Wazuh server to generate security alerts based on the USB drive log data.
macOS endpoint
Perform the steps below to configure the monitored macOS endpoint to generate USB drive information and log the data in JSON format into a file.
Configuring the Swift script
1. Run the following command to install Xcode
command line tools, which include the Swift
compiler:
# xcode-select --install
2. Verify that the Swift
compiler is installed correctly:
# swiftc --version
Output:
Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5) Target: x86_64-apple-darwin23.3.0
3. Create a usb_monitor.log
file in the /var/log
directory to log USB events:
# touch /var/log/usb_monitor.log
4. Set the right permission to usb_monitor.log
file:
# chmod 640 /var/log/usb_monitor.log
5. Download the usb.ids
file and save it in the /Library/Ossec/etc
directory:
# curl http://www.linux-usb.org/usb.ids -o /Library/Ossec/etc/usb.ids
6. Create a USBMonitor.swift
file in the /Library/Ossec/etc
directory:
# touch /Library/Ossec/etc/USBMonitor.swift
This script uses the I/O Kit framework in macOS systems to capture USB events and extract device details. It also parses the usb.ids
file to establish a mapping of vendor and product IDs to their corresponding names and appends this information as JSON to the usb_monitor.log
file.
7. Add the following content to the newly created USBMonitor.swift
file:
import Foundation import IOKit import IOKit.usb struct USBEvent: Codable { var eventType: String var timestamp: String var deviceInfo: [String: String] init(eventType: String, deviceInfo: [String: String]) { self.eventType = eventType self.deviceInfo = deviceInfo self.timestamp = USBEvent.currentLocalTimestamp() } private static func currentLocalTimestamp() -> String { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" // Updated format formatter.timeZone = TimeZone.current return formatter.string(from: Date()) } } var usbVendorProductMap = [String: [String: String]]() func parseUSBIDs(from filePath: String) { do { let content = try String(contentsOfFile: filePath, encoding: .utf8) var currentVendorID: String? for line in content.split(whereSeparator: \.isNewline) { let trimmedLine = line.trimmingCharacters(in: .whitespaces) if trimmedLine.isEmpty || trimmedLine.hasPrefix("#") { continue } if line.first!.isNumber { let components = trimmedLine.split(separator: " ", maxSplits: 1) if components.count == 2 { currentVendorID = String(components[0]) let vendorName = String(components[1]) usbVendorProductMap[currentVendorID!] = ["": vendorName] // Vendor name } } else if line.first == "\t", let vendorID = currentVendorID { let components = trimmedLine.split(separator: " ", maxSplits: 1) if components.count == 2 { let productID = String(components[0]) let productName = String(components[1]) usbVendorProductMap[vendorID]?[productID] = productName } } } } catch { print("Failed to read or parse USB IDs file: \(error)") } } func logUSBEvent(event: USBEvent, to filePath: String) { let encoder = JSONEncoder() if let jsonData = try? encoder.encode(event), let jsonString = String(data: jsonData, encoding: .utf8) { do { try jsonString.appendLineToURL(fileURL: URL(fileURLWithPath: filePath)) } catch { print("Failed to write to log file: \(error)") } } } extension String { func appendLineToURL(fileURL: URL) throws { try (self + "\n").appendToFile(fileURL: fileURL) } func appendToFile(fileURL: URL) throws { let data = self.data(using: .utf8)! try data.append(fileURL: fileURL) } } extension Data { func append(fileURL: URL) throws { if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) { defer { fileHandle.closeFile() } fileHandle.seekToEndOfFile() fileHandle.write(self) } else { try write(to: fileURL, options: .atomic) } } } func extractDeviceInfo(device: io_object_t) -> [String: String] { var deviceInfo = [String: String]() let vendorIDKey = kUSBVendorID as String let productIDKey = kUSBProductID as String let serialNumberKey = kUSBSerialNumberString as String if let vendorIDRef = IORegistryEntrySearchCFProperty(device, kIOServicePlane, vendorIDKey as CFString, kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively | kIORegistryIterateParents)), CFGetTypeID(vendorIDRef) == CFNumberGetTypeID() { var vendorID: Int = 0 CFNumberGetValue((vendorIDRef as! CFNumber), CFNumberType.intType, &vendorID) let hexVendorID = String(format: "%04x", vendorID) deviceInfo[vendorIDKey] = hexVendorID if let vendorName = usbVendorProductMap[hexVendorID]?[""] { deviceInfo["vendorName"] = vendorName } } if let productIDRef = IORegistryEntrySearchCFProperty(device, kIOServicePlane, productIDKey as CFString, kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively | kIORegistryIterateParents)), CFGetTypeID(productIDRef) == CFNumberGetTypeID() { var productID: Int = 0 CFNumberGetValue((productIDRef as! CFNumber), CFNumberType.intType, &productID) let hexProductID = String(format: "%04x", productID) deviceInfo[productIDKey] = hexProductID if let vendorID = deviceInfo[vendorIDKey], let productName = usbVendorProductMap[vendorID]?[hexProductID] { deviceInfo["productName"] = productName } } if let serialNumberRef = IORegistryEntrySearchCFProperty(device, kIOServicePlane, serialNumberKey as CFString, kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively | kIORegistryIterateParents)) as? String { deviceInfo[serialNumberKey] = serialNumberRef } // Debug: Print the mapping results including Serial Number print("Mapped Vendor ID: \(deviceInfo[vendorIDKey] ?? "Not Found")") print("Mapped Product ID: \(deviceInfo[productIDKey] ?? "Not Found")") print("Mapped Serial Number: \(deviceInfo[serialNumberKey] ?? "Not Found")") print("Mapped Vendor Name: \(deviceInfo["vendorName"] ?? "Not Found")") print("Mapped Product Name: \(deviceInfo["productName"] ?? "Not Found")") return deviceInfo } func usbDeviceCallback(context: UnsafeMutableRawPointer?, iterator: io_iterator_t, eventType: String) { var device: io_object_t repeat { device = IOIteratorNext(iterator) if device != 0 { let deviceInfo = extractDeviceInfo(device: device) let event = USBEvent( eventType: eventType, deviceInfo: deviceInfo ) logUSBEvent(event: event, to: "/var/log/usb_monitor.log") IOObjectRelease(device) } } while device != 0 } let notifyPort = IONotificationPortCreate(kIOMainPortDefault) let runLoopSource = IONotificationPortGetRunLoopSource(notifyPort).takeRetainedValue() CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, CFRunLoopMode.defaultMode) let scriptPath = URL(fileURLWithPath: #file).deletingLastPathComponent().path parseUSBIDs(from: "/Library/Ossec/etc/usb.ids") var deviceAddedIter = io_iterator_t() var deviceRemovedIter = io_iterator_t() let matchingDict = IOServiceMatching(kIOUSBDeviceClassName) as NSMutableDictionary IOServiceAddMatchingNotification(notifyPort, kIOFirstMatchNotification, matchingDict, { (context, iterator) in usbDeviceCallback(context: context, iterator: iterator, eventType: "USBConnected") }, nil, &deviceAddedIter) usbDeviceCallback(context: nil, iterator: deviceAddedIter, eventType: "USBConnected") IOServiceAddMatchingNotification(notifyPort, kIOTerminatedNotification, matchingDict, { (context, iterator) in usbDeviceCallback(context: context, iterator: iterator, eventType: "USBDisconnected") }, nil, &deviceRemovedIter) usbDeviceCallback(context: nil, iterator: deviceRemovedIter, eventType: "USBDisconnected") CFRunLoopRun()
Where:
/var/log/usb_monitor.log
is the log file where the USB drive log data is stored./Library/Ossec/etc/usb.ids
is the text file containing a list of assigned Vendor and Product IDs for USB devices. It serves as a reference for operating systems and software to properly identify and interact with USB devices based on their unique identifiers.
Note
For macOS 11 and earlier versions, replace this line: let notifyPort = IONotificationPortCreate(kIOMainPortDefault)
with let notifyPort = IONotificationPortCreate(kIOMasterPortDefault)
in the above Swift script.
8. Compile the USBMonitor.swift
script and make it executable:
# swiftc /Library/Ossec/etc/USBMonitor.swift -o /Library/Ossec/etc/USBMonitor
9. Test the executable to see if it runs without any errors:
# /Library/Ossec/etc/USBMonitor
Note
Press Ctrl + C
keys on your keyboard to abort the script execution.
You may encounter the following error due to an encoding issue of usb.ids
file.
Failed to read or parse USB IDs file: Error Domain=NSCocoaErrorDomain Code=261 "The file “usb.ids” couldn’t be opened using text encoding Unicode (UTF-8)." UserInfo={NSFilePath=/Library/Ossec/etc/usb.ids, NSStringEncoding=4}
Run the following command to resolve the encoding issue within the usb.ids
file:
# iconv -f iso-8859-1 -t utf-8 /Library/Ossec/etc/usb.ids > usb-utf8.ids && mv usb-utf8.ids /Library/Ossec/etc/usb.ids
After resolving the encoding issue, repeat step 9 to ensure the executable runs without any errors.
Forwarding USB logs to the Wazuh server
We configure the Wazuh agent installed on the monitored macOS endpoint to read the usb_monitor.log
file and forward the log data to the Wazuh server.
Perform the following steps to proceed with the configuration process.
1. Add the following configuration within the <ossec_config>
block of the Wazuh agent /Library/Ossec/etc/ossec.conf
file:
<localfile> <log_format>json</log_format> <location>/var/log/usb_monitor.log</location> </localfile>
2. Restart the Wazuh agent for the changes to take effect:
# /Library/Ossec/bin/wazuh-control restart
Setting macOS startup script
In this section, we automate the USBMonitor
script execution by creating a Property List (.plist
) and loading it to the LaunchDaemons. LaunchDaemons are responsible for starting and stopping system-wide services, such as background processes at boot time.
Perform the steps below to configure the macOS startup script.
1. Create a new file com.user.usbmonitor.plist
in the /Library/LaunchDaemons
directory:
# touch /Library/LaunchDaemons/com.user.usbmonitor.plist
2. Add the following content to the /Library/LaunchDaemons/com.user.usbmonitor.plist
file:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>com.user.usbmonitor</string> <key>ProgramArguments</key> <array> <string>/Library/Ossec/etc/USBMonitor</string> </array> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <true/> </dict> </plist>
3. Load the com.user.usbmonitor.plist
file to execute USBMonitor
during boot-up:
# launchctl load /Library/LaunchDaemons/com.user.usbmonitor.plist
4. Reboot the monitored macOS endpoint:
# shutdown -r now
Wazuh server
We now configure the Wazuh server to generate security alerts based on the USB drive logs received from the Wazuh agent. Since the received log data is in JSON format, Wazuh decodes them using an out-of-the-box JSON decoder. We only need to write custom rules to generate security alerts.
Perform the steps below to proceed with the Wazuh server configuration.
1. Create a custom rule file macos_usb_rules.xml
in the /var/ossec/etc/rules
directory:
# touch /var/ossec/etc/rules/macos_usb_rules.xml
2. Add the following detection rules to the macos_usb_rules.xml file:
<!-- rules for USB drives in macOS --> <group name="macOS,usb-detect,"> <!-- rule for connected USB drives --> <rule id="111060" level="7"> <decoded_as>json</decoded_as> <field name="eventType">^USBConnected$</field> <description>macOS: USB drive $(deviceInfo.productName) was connected.</description> </rule> <!-- rule for disconnected USB drives --> <rule id="111062" level="7"> <decoded_as>json</decoded_as> <field name="eventType">^USBDisconnected$</field> <description>macOS: USB drive $(deviceInfo.productName) was disconnected.</description> </rule> </group>
Where:
- Rule ID
111060
triggers a security alert when a new USB drive is connected to a macOS endpoint. - Rule ID
111062
triggers a security alert when a USB drive is disconnected from a macOS endpoint.
3. Restart the Wazuh manager to apply the changes:
# systemctl restart wazuh-manager
Testing the configuration
To test the configuration, connect a USB drive to the monitored macOS endpoint and disconnect it afterward.
Detection result
Once a USB drive is connected to the monitored macOS endpoint, we see a security alert with rule ID 111060
generated on the Wazuh dashboard. Similarly, upon unplugging the USB drive from the macOS endpoint, we see a security alert with rule ID 111062
generated on the Wazuh dashboard.
Filtering authorized and unauthorized USB drives
This section shows how to create a CDB (constant database) list in the Wazuh server for detecting unauthorized USB drives connected to the monitored macOS endpoint. The CDB list contains the serial numbers of all authorized USB drives, which we later use to enhance the custom rules created earlier in this blog post.
Perform the following steps to proceed with the configuration on your Wazuh server.
1. Create a CDB list, usb-drives
in the /var/ossec/etc/
directory:
# touch /var/ossec/etc/lists/usb-drives
2. Add the serial numbers of the authorized USB drives followed by a colon (:
) to the CDB list:
234567890126:
Note
To extract serial numbers, you should consider the deviceInfo.kUSBSerialNumberString
field in the USB drive log data.
3. Add the configuration <list>etc/lists/usb-drives</list>
to the <ruleset>
block of the /var/ossec/etc/ossec.conf
file:
<ruleset> <!-- Default ruleset --> <decoder_dir>ruleset/decoders</decoder_dir> <rule_dir>ruleset/rules</rule_dir> <rule_exclude>0215-policy_rules.xml</rule_exclude> <list>etc/lists/audit-keys</list> <list>etc/lists/amazon/aws-eventnames</list> <list>etc/lists/security-eventchannel</list> <!-- User-defined ruleset --> <decoder_dir>etc/decoders</decoder_dir> <rule_dir>etc/rules</rule_dir> <list>etc/lists/usb-drives</list> </ruleset>
4. Clear the previously added custom rules in /var/ossec/etc/rules/macos_usb_rules.xml
and add the following content to the same file:
<!-- rules for USB drives in macOS --> <group name="macOS,usb-detect,"> <!-- rule for connected USB drives --> <rule id="111060" level="5"> <decoded_as>json</decoded_as> <field name="eventType">^USBConnected$</field> <description>macOS: USB drive $(deviceInfo.productName) was connected.</description> </rule> <!-- rule for connected unauthorized USB drives --> <rule id="111061" level="8"> <if_sid>111060</if_sid> <list field="deviceInfo.kUSBSerialNumberString" lookup="not_match_key">etc/lists/usb-drives</list> <description>macOS: Unauthorized USB drive $(deviceInfo.productName) was connected.</description> </rule> <!-- rule for disconnected USB drives --> <rule id="111062" level="5"> <decoded_as>json</decoded_as> <field name="eventType">^USBDisconnected$</field> <description>macOS: USB drive $(deviceInfo.productName) was disconnected.</description> </rule> <!-- rule for disconnected unauthorized USB drives --> <rule id="111063" level="8"> <if_sid>111062</if_sid> <list field="deviceInfo.kUSBSerialNumberString" lookup="not_match_key">etc/lists/usb-drives</list> <description>macOS: Unauthorized USB drive $(deviceInfo.productName) was disconnected.</description> </rule> </group>
Where:
111061
triggers security alerts when the serial numbers of the plugged-in USB drives don’t match the authorized USB drive serial numbers on the CDB list.111063
triggers security alerts when unauthorized USB drives are disconnected from the monitored macOS endpoint.
5. Restart the Wazuh manager for the changes to take effect:
# systemctl restart wazuh-manager
Testing the configuration
To test the configuration, connect a USB drive to the monitored macOS endpoint and disconnect it afterward. Ensure that the serial number of the connected USB drive is not present in the earlier created CDB list.
Detection result
Once the unauthorized USB drive is connected to the monitored macOS endpoint, we should see a security alert with rule ID 111061
generated on the Wazuh dashboard. Similarly, when we unplug the USB drive from the macOS endpoint, we should see a security alert with rule ID 111063
generated on the Wazuh dashboard.
Conclusion
Safeguarding sensitive data on macOS systems is an essential aspect of maintaining a secure computing environment. Wazuh deployment for monitoring USB drives adds an extra layer of defense against potential threats and unauthorized access. In this blog post, we discovered how Wazuh excels in the vigilant monitoring of USB devices across monitored macOS endpoints and contributes to a resilient and efficient security framework.
Wazuh stands as a robust, open source Security Information and Event Management (SIEM) and Extended Detection and Response (XDR) solution. Explore our community for ongoing support and the latest news.
Reference