04/11/2019
Blog technique
Windows filter communication ports
Adrien Chevalier
Filter communication ports are a communication mechanism between usermode processes and filesystem filter drivers. When accessible by unprivileged users, it may offer privilege escalation vulnerabilities. We did not understand why we could not find opened handles in client process, and as we did not find documentation on how it is implemented in Windows, we took a quick look and wrote this blog post :). Keep in mind that it is a quickly written blog post, several asumptions may be wrong. If you find such mistakes, just let us know :).
Filter communication ports are handled by the fltlib.dll
module. The FilterConnectCommunicationPort
function will create the port handle. It opens a new handle on the \Global??\FltMgrMsg
symbolic link, which points over the FileSystemFiltersFltMgrMsg
device object. Actually, the NtCreateFile
syscall will also be provided with one or two extended attributes.
The following struct represents NTFS Extended Attributes (the attribute’s value follows directly EaName
, after EaValueLength
bytes):
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_file_full_ea_information
typedef struct _FILE_FULL_EA_INFORMATION {
ULONG NextEntryOffset;
UCHAR Flags;
UCHAR EaNameLength;
USHORT EaValueLength;
CHAR EaName[1];
} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;
In this case, the first attribute will be:
- Flag: 0x700 (undocumented) ;
- EaName: FLTPORT ;
- EaValue: filter communication port name (
PortName
parameter), as aUNICODE_STRING
pointer.
A third extended attribute may be present, and will contain the Context
data, which is provided to the FilterConnectCommunicationPort
, and which will be sent to the driver.
When a driver creates the filter communication port using the FltCreateCommunicationPort
function, a new object will be created with:
- The provided object attributes, and thus the provided port name. The
OBJ_KERNEL_HANDLE
attribute must be set. - The « KernelMode » AccessMode.
- The « KernelMode » ProbeMode.
This newly created object is a FLT_MSG_CONNECTION
(undocumented, we chose a new name 🙂 ), which follows this scheme:
typedef struct _FLT_MSG_OBJECT_CALLBACKS {
PFLT_FILTER Filter;
ULONGLONG ServerPortCookie;
PFLT_CONNECT_NOTIFY ConnectNotifyCallback;
PFLT_DISCONNECT_NOTIFY DisconnectNotifyCallback;
PFLT_MESSAGE_NOTIFY MessageNotifyCallback;
}FLT_MSG_OBJECT_CALLBACKS, *PFLT_MSG_OBJECT_CALLBACKS;
typedef struct _FLT_MSG_CONNECTION {
LIST_ENTRY List;
PFLT_CONNECT_NOTIFY ConnectNotifyCallback;
PFLT_DISCONNECT_NOTIFY DisconnectNotifyCallback;
PFLT_MESSAGE_NOTIFY MessageNotifyCallback;
PFLT_FILTER Filter;
PFLT_MSG_OBJECT_CALLBACKS Callbacks;
ULONG undefined;
ULONG ConnectionCount;
ULONG MaximumConnections;
}FLT_MSG_CONNECTION, *PFLT_MSG_CONNECTION;
The FLT_MSG_CONNECTION
is also added to the Filter
object (FLT_FILTER
), in its ConnectionList item.
Now, when the IRP_MJ_CREATE
reaches the FltMgr.sys driver for \.FltMgrMsg
, a custom IRP handler is called for this particular device object, and a specific function handles this IRP. Before that, ntoskrnl will parse the EaBuffer
NtCreateFile
parameter, and will copy its contents into an newly allocated pool buffer (IoEa
tag) and will check this buffer using the IoCheckEaBufferValidity(PVOID EaBuffer, ULONG EaLength, PULONG DwEaAttributesCount)
. This functions only checks if there is no overflow with the offsets and sizes.
We did not search where the extended attributes checks are performed by the kernel, we only know it is correctly checked (we tried providing bad structs and pointers, without any problem).
This routine will verify that a « FLTPORT » extended attribute is present, and will retrieve its EaValue (the communication port name). This name will be passed to ObReferenceObject
(which seems to perform the access check), with an AccessMode
set to ExGetPreviousMode()
. The MaximumConnections
is checked, and a new unnamed kernel object will be created. Then, the ConnectNotifyCallback
routine of the port object will be called. If this call is successful, the FsContext2
item of the referenced FileObject
(FILE_OBJECT
struct) will also be created, and will have several of his members initiliazed, and will point over these objects (filter object, etc.). This struct has not been fully analyzed, and will not be detailed here.
When sending a filter message using the FilterSendMessage
function from the userland, an IOCTL is sent to the opened device object. In this case, the 0x8801B
CTL code will be used (DISK_FILE_SYSTEM
, METHOD_NEITHER
, FILE_WRITE_ACCESS
, Function 6). For FltGetMessage
, the 0x8401F
CTL code will be used. They are sent with the NtDeviceIoControlFile
function, but several other ones may be sent through NtFsControlFile
(the control codes begin with 0x90000
).
The FltMsg.sys
driver will handle these IOCTLs by first checking the provided userland buffer pointers with ProbeForRead
and ProbeForWrite
calls, but will NOT copy these buffers into newly allocated areas, and that’s why https://docs.microsoft.com/fr-fr/windows-hardware/drivers/ddi/fltkernel/nf-fltkernel-fltcreatecommunicationport states « Note that OutputBuffer is a pointer to a raw, unlocked user-mode buffer. This pointer is valid only in the context of the user-mode process and must only be accessed from within a try/except block ». On FltSendMessage
IOCTLs, the callback function pointer is retrieved using the referenced file object (FILE_OBJECT
) and its FsContext2
member. The original FLT_MSG_CONNECTION
structure pointer may be found at *(*(FsContext2 + 0x8) + 0x10)
To summarize, filter communication ports relay on METHOD_NEITHER
IOCTLs. It seems that it is actually not possible to retrieve, from userland, which process opens which filter communication port, as the only handles which may be found are the FltMgrMsg
device object, and an unamed one. If you have any insight on how it could be possible to list them from userland, let us know!
Like METHOD_NEITHER
IOCTLs, supplied message buffers must be probed and locked (or copied in kernel buffers) by the message notify routine: unlike these IOCTLs, attackers cannot provide kernel addresses (a ProbeForRead/Write is made by default by FltMgr
) but TOCTOU attacks may also be present. And like any other IOCTL handling, the notify routine should never store userland pointers in these buffers (or, in this case, handle them with the same security considerations).