Вступ
USB-накопичувачі корисні для передачі файлів у системах macOS, забезпечуючи швидкий і простий спосіб обміну документами, фотографіями та іншим між пристроями. Вони підключаються та працюють, що дозволяє користувачам легко отримувати доступ і передавати дані. Однак важливо бути обережним щодо безпеки. USB-накопичувачі можуть містити зловмисне програмне забезпечення, створюючи ризик для систем macOS. Організації повинні завчасно впроваджувати відстеження та аналіз USB-накопичувачів у режимі реального часу у своїх системах macOS, щоб ефективно протистояти потенційним загрозам безпеці.
Wazuh – це надійне рішення для підвищення безпеки macOS шляхом моніторингу USB-накопичувачів. Завдяки своїм можливостям Wazuh може відстежувати дії USB-накопичувача, виявляти аномалії та генерувати сповіщення в режимі реального часу. Пропонуючи детальну інформацію про події, пов’язані з USB, Wazuh дає змогу організаціям встановлювати суворі політики безпеки, оперативно реагувати на потенційні загрози та підтримувати цілісність систем macOS.
Раніше ми вже розбирали моніторинг USB-накопичувачів на Linux та Windows за допомогою Wazuh. В цій статті розберемо захист від загроз з USB-накопичувачів у системах macOS.
Інфраструктура
Налаштована і готова до використання остання версія віртуальної машини Wazuh OVA, що включає основні компоненти рішення (сервер, indexer, дешборд).
Встановлена і зареєстрована на сервері Wazuh кінцева точка macOS Sonoma 14.3 із агентом Wazuh.
Конфігурація
Необхідно налаштувати кінцеву точку macOS за допомогою кастомного скрипту Swift, щоб отримати інформацію про підключені USB-накопичувачі із системи. Потім скрипт співвідносить цю інформацію з файлом usb.ids репозиторію USB ID Linux і записує дані у файл. Агент Wazuh пересилає ці журнали на сервер Wazuh для аналізу. Нарешті, відбувається налаштування сервера Wazuh для створення сповіщень безпеки на основі даних журналу USB-накопичувача.
macOS
Нижче розписані необхідні кроки, щоб налаштувати відстежувану кінцеву точку macOS для генерування інформації про USB-накопичувач і реєстрації даних у форматі JSON у файл.
Налаштування скрипту Swift
1. Виконайте таку команду, щоб установити інструменти командного рядка Xcode, які включають компілятор Swift:
# xcode-select --install
2. Переконайтеся, що компілятор Swift встановлено правильно:
# swiftc --version
Отримуємо:
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. Створіть файл usb_monitor.log у каталозі /var/log для реєстрації USB подій:
# touch /var/log/usb_monitor.log
4. Встановіть правильний дозвіл для файлу usb_monitor.log:
# chmod 640 /var/log/usb_monitor.log
5. Завантажте файл usb.ids і збережіть його в каталозі /Library/Ossec/etc:
# curl http://www.linux-usb.org/usb.ids -o /Library/Ossec/etc/usb.ids
6. Створіть файл USBMonitor.swift у каталозі /Library/Ossec/etc:
# touch /Library/Ossec/etc/USBMonitor.swift
Цей скрипт використовує структуру I/O Kit у системах macOS для захоплення USB подій і вилучення деталей пристрою. Він також аналізує файл usb.ids, щоб встановити зіставлення ідентифікаторів постачальника та продукту з їхніми відповідними іменами та додає цю інформацію як JSON до файлу usb_monitor.log.
7. Додайте такий вміст до щойно створеного файлу USBMonitor.swift:
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()
Де:
/var/log/usb_monitor.log– це файл журналу, у якому зберігаються дані журналу USB-накопичувача./Library/Ossec/etc/usb.ids– це текстовий файл, який містить список призначених ідентифікаторів постачальників і продуктів для пристроїв USB. Він служить довідником для операційних систем і програмного забезпечення для належної ідентифікації USB-пристроїв і взаємодії з ними на основі їх унікальних ідентифікаторів.
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry’s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
Abc Company
CEO
Примітка:
Для macOS 11 і попередніх версій замініть цей рядок: let notifyPort = IONotificationPortCreate(kIOMainPortDefault) на let notifyPort = IONotificationPortCreate(kIOMasterPortDefault) у наведеному вище скрипті Swift.
8. Скомпілюйте скрипт USBMonitor.swift і зробіть його виконуваним:
# swiftc /Library/Ossec/etc/USBMonitor.swift -o /Library/Ossec/etc/USBMonitor
9. Перевірте виконуваний файл, щоб побачити, чи він працює без будь-яких помилок:
# /Library/Ossec/etc/USBMonitor
Примітка:
Натисніть клавіші Ctrl + C на клавіатурі, щоб перервати виконання скрипту.
Ви можете зіткнутися з такою помилкою через проблему кодування файлу usb.ids.
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}
Виконайте таку команду, щоб розв’язати проблему кодування у файлі usb.ids:
# 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
Після розв’язання проблеми з кодуванням повторіть крок 9, щоб переконатися, що виконуваний файл працює без помилок.
Пересилання USB журналів на сервер Wazuh
Налаштовуйте агент Wazuh, встановлений на контрольованій кінцевій точці macOS, для читання файлу usb_monitor.log і пересилання даних журналу на сервер Wazuh.
Виконайте наступні кроки, щоб продовжити процес налаштування.
1. Додайте таку конфігурацію в блок <ossec_config> файлу Wazuh agent /Library/Ossec/etc/ossec.conf:
<localfile>
<log_format>json</log_format>
<location>/var/log/usb_monitor.log</location>
</localfile>
2. Перезапустіть агент Wazuh, щоб зміни набули чинності:
# /Library/Ossec/bin/wazuh-control restart
Налаштування скрипту запуску macOS
Автоматизуйте виконання скрипту USBMonitor, створивши список властивостей (.plist) і завантаживши його в LaunchDaemons. LaunchDaemons відповідають за запуск і зупинку системних служб, таких як фонові процеси під час завантаження.
Виконайте наведені нижче дії, щоб налаштувати скрипт запуску macOS.
1. Створіть новий файл com.user.usbmonitor.plist у каталозі /Library/LaunchDaemons:
# touch /Library/LaunchDaemons/com.user.usbmonitor.plist
2. Додайте наступний вміст до файлу /Library/LaunchDaemons/com.user.usbmonitor.plist:
<?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. Завантажте файл com.user.usbmonitor.plist, щоб запустити USBMonitor під час завантаження:
# launchctl load /Library/LaunchDaemons/com.user.usbmonitor.plist
4. Перезавантажте контрольовану кінцеву точку macOS:
# shutdown -r now
Сервер Wazuh
Тепер потрібно налаштувати сервер Wazuh для створення сповіщень безпеки на основі журналів USB-накопичувача, отриманих від агента Wazuh. Оскільки отримані дані журналу мають формат JSON, Wazuh декодує їх за допомогою готового декодера JSON. Тут потрібно лише написати спеціальні правила для генерування сповіщень безпеки.
Виконайте наведені нижче дії, щоб продовжити налаштування сервера Wazuh.
1. Створіть спеціальний файл правил macos_usb_rules.xml у каталозі /var/ossec/etc/rules:
# touch /var/ossec/etc/rules/macos_usb_rules.xml
2. Додайте такі правила виявлення до файлу macos_usb_rules.xml:
<!-- 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>
Де:
- Ідентифікатор правила
111060запускає сповіщення безпеки, коли новий USB-накопичувач підключається до кінцевої точки macOS. - Ідентифікатор правила
111062запускає сповіщення безпеки, коли USB-накопичувач від’єднується від кінцевої точки macOS.
3. Перезапустіть менеджер Wazuh, щоб застосувати зміни:
# systemctl restart wazuh-manager
Тестування конфігурації
Щоб перевірити конфігурацію, підключіть USB-накопичувач до контрольованої кінцевої точки macOS, а потім від’єднайте його.
Коли USB-накопичувач підключено до контрольованої кінцевої точки macOS, ми бачимо сповіщення системи безпеки з ідентифікатором правила 111060, згенероване на інформаційній панелі Wazuh. Подібним чином, після від’єднання USB-накопичувача від кінцевої точки macOS, ми бачимо сповіщення безпеки з ідентифікатором правила 111062, згенероване на інформаційній панелі Wazuh.


Фільтрація авторизованих і неавторизованих USB-накопичувачів
У цьому розділі показано, як створити список CDB (постійна база даних) на сервері Wazuh для виявлення неавторизованих USB-накопичувачів, підключених до контрольованої кінцевої точки macOS. Список CDB містить серійні номери всіх авторизованих USB-накопичувачів, які ми пізніше використовуємо для вдосконалення спеціальних правил, створених раніше в цій публікації блогу.
Виконайте наведені нижче дії, щоб продовжити налаштування на вашому сервері Wazuh.
1. Створіть список CDB, usb-drives у каталозі /var/ossec/etc/:
# touch /var/ossec/etc/lists/usb-drives
2. Додайте серійні номери авторизованих USB-накопичувачів із двокрапкою (:) до списку CDB:
234567890126:
Примітка:
Щоб отримати серійні номери, слід розглянути поле deviceInfo.kUSBSerialNumberString у даних журналу USB-накопичувача.
3. Додайте конфігурацію etc/lists/usb-drives до блоку файлу /var/ossec/etc/ossec.conf:
<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. Очистіть раніше додані спеціальні правила в /var/ossec/etc/rules/macos_usb_rules.xml і додайте такий вміст до того самого файлу:
<!-- 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>
Де:
111061запускає сповіщення безпеки, коли серійні номери підключених USB-накопичувачів не збігаються з авторизованими серійними номерами USB-накопичувачів у списку CDB.111063запускає сповіщення безпеки, коли неавторизовані USB-накопичувачі від’єднуються від контрольованої кінцевої точки macOS.
5. Перезапустіть менеджер Wazuh, щоб зміни набули чинності:
# systemctl restart wazuh-manager
Тестування конфігурації
Щоб перевірити конфігурацію, підключіть USB-накопичувач до контрольованої кінцевої точки macOS, а потім від’єднайте його. Переконайтеся, що серійний номер підключеного USB-накопичувача відсутній у раніше створеному списку CDB.
Щойно неавторизований USB-накопичувач буде підключено до контрольованої кінцевої точки macOS, ми побачимо сповіщення системи безпеки з ідентифікатором правила 111061, згенероване на інформаційній панелі Wazuh. Подібним чином, коли ми від’єднуємо USB-накопичувач від кінцевої точки macOS, ми маємо побачити сповіщення безпеки з ідентифікатором правила 111063, згенероване на інформаційній панелі Wazuh.









