Blocking Data Exfiltration to USB Drives with CBFS Filter


USB drives remain one of the simplest ways for data to leave a system that is otherwise well protected on the network side. A user (or a piece of malware acting on a user's session) only needs write access to a mounted volume to copy sensitive files off of it, and no firewall or DLP proxy sees that traffic because it never touches the network. This article describes how to use CBFilter to detect removable volumes as they are mounted and restrict write access to them so that only explicitly authorized processes can place data on them.

This article builds directly on Removable Devices and Per-Volume Rules, which covers the mechanics of tracking volume attachment and detachment with CBFilter. Read that article first if you are not already familiar with BeforeFilterAttachToVolume, AfterFilterAttachToVolume, and the FireVolumeEvents property; this article assumes that background and focuses on the additional step of identifying which of those volumes are removable USB drives and enforcing a write policy on them.

Identifying Removable Volumes

Before any volume events will fire, enable them with the FireVolumeEvents property:

filter.FireVolumeEvents = Constants.FS_MOUNT_CONTROL;

CBFilter's volume events tell you that a volume was mounted, and give you its name, but they do not tell you what kind of device it is. Neither VolumeName nor VolumeNameNT distinguishes a USB flash drive from a fixed internal disk or a mapped network share presented as a local volume. That determination has to be made with a separate call to the Windows API once you have a usable path to the volume.

The most direct option is GetDriveTypeW, which accepts a root path (such as "E:\\") and returns DRIVE_REMOVABLE for removable media. The complication is timing: at BeforeFilterAttachToVolume, a drive letter is not guaranteed to be assigned to the volume yet, and the Removable Devices and Per-Volume Rules article already notes that VolumeName "is not always guaranteed" to be populated. In practice, AfterFilterAttachToVolume is the more reliable place to make this check, since the drive letter assignment has normally completed by the time the filter has already attached. If VolumeName is still empty at that point, treat the volume conservatively (that is, apply the same restrictions you would apply to a removable drive) rather than assuming it is safe, since a volume you cannot positively classify is not one you can rule out as removable.

``` void AfterFilterAttachToVolume(object sender, CBFilterAfterFilterAttachToVolumeEventArgs e) { bool isRemovable = false; if (!string.IsNullOrEmpty(e.VolumeName)) { uint driveType = GetDriveTypeW(e.VolumeName + "\"); isRemovable = (driveType == DRIVE_REMOVABLE); } else { // No drive letter available yet; treat the volume as removable until proven otherwise. isRemovable = true; }

if (isRemovable) { AddUsbWriteProtectionRule(e.VolumeNameNT); } } ```

GetDriveTypeW will also report USB hard drives and card readers as DRIVE_REMOVABLE, along with anything else Windows classifies the same way (some external SSDs enumerate as DRIVE_FIXED instead, depending on how the enclosure reports itself). If your policy needs to distinguish more finely between device types, such as separating flash drives from external hard drives, you will need to go a step further and query the underlying storage device's bus type with DeviceIoControl and IOCTL_STORAGE_QUERY_PROPERTY, which is outside the scope of this article.

Restricting Writes to the Volume

Once a volume has been classified as removable, add a filter rule scoped to that volume using AddFilterRule, matching every file on it with a rule based on VolumeNameNT (never VolumeName, for the deadlock reasons already covered in Removable Devices and Per-Volume Rules):

filter.AddFilterRule(e.VolumeNameNT + "\\*.*", Constants.ACCESS_NONE, Constants.FS_CE_BEFORE_CREATE | Constants.FS_CE_BEFORE_OPEN, Constants.FS_NE_NONE);

This rule does not deny access on its own; it only tells CBFilter to fire BeforeCreateFile and BeforeOpenFile for anything on the volume, so that the event handlers can make a per-process decision. BeforeCreateFile fires only when a file is created with a create-new disposition. Copying or saving a file onto the drive usually opens the destination with an overwrite-if disposition, which fires BeforeOpenFile instead, as does opening an existing file there for writing. Both need to be handled to cover both cases.

Make the decision when the file is opened rather than in BeforeWriteFile. By default, BeforeWriteFile reports only the system's delayed (noncached) writes, which reach the drive after the application has already been told that its write succeeded, so denying them does not stop the file from being created.

In each handler, check the identity of the requesting process with GetOriginatorProcessName against an application-defined allow list, and deny the request if the process is not on it. In BeforeOpenFile, deny only opens that ask for write access, so that reading files on the drive keeps working:

``` void BeforeCreateFile(object sender, CBFilterBeforeCreateFileEventArgs e) { string procName = filter.GetOriginatorProcessName(); if (!IsAuthorized(procName)) { e.ResultCode = 5; // ERROR_ACCESS_DENIED e.ProcessRequest = false; } }

void BeforeOpenFile(object sender, CBFilterBeforeOpenFileEventArgs e) { bool wantsWrite = (e.DesiredAccess & (Constants.DESIRED_ACCESS_FILE_WRITE_DATA | Constants.DESIRED_ACCESS_FILE_APPEND_DATA)) != 0; if (!wantsWrite) return;

string procName = filter.GetOriginatorProcessName(); if (!IsAuthorized(procName)) { e.ResultCode = 5; // ERROR_ACCESS_DENIED e.ProcessRequest = false; } } ```

With both handlers in place, an unauthorized process that tries to copy a file onto the drive gets "Access is denied", and no file is created there.

GetOriginatorProcessName returns the name of the process, not a verified identity. A process can be renamed or replaced, so if you need to defend against a deliberately spoofed process name rather than just a general policy against casual copying, pair this check with GetOriginatorToken (covered in Restricting File Access by User or Group with CBFS Filter) or with code-signature verification on the originator's executable path, rather than relying on the name alone.

Remember to remove the rule when the drive is detached, in an AfterFilterDetachFromVolume handler, since the same VolumeNameNT value may be reassigned to a different physical device the next time a drive is inserted.

Handling Read Access Separately

This article focuses on write access, since that is what an exfiltration policy typically needs to restrict. If your policy also needs to prevent authorized processes from reading sensitive files back off of internal storage in order to stage them onto the USB drive in the first place, that is a distinct control: it means restricting reads on the source location, not writes on the destination volume, and it should be implemented as its own rule against the protected directory using FS_CE_BEFORE_READ, following the same pattern used in Access and Default Rules in CBFS Filter.

Getting Started with CBFilter

You can find an evaluation version of the SDK for your platform and programming language in the Download Center. After downloading and installing the SDK, you will receive a library, sample code, and comprehensive documentation on your system. The documentation includes a "Getting Started" section with programming instructions. Additionally, free technical support is available during the evaluation phase.

We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@callback.com.