Protecting from the Dark World

Recently a driver was put on my radar and it had pretty relaxed permissions on it, which isn’t necessarily bad but if interesting functions or privileged instructions are exposed to a low privileged user, more fun shall, and most definitely will, be had. I know of a couple ways to implement Discretionary Access Control Lists, or DACL’s, or other checks to prevent this scenario from happening. This may be second hand nature for actual kernel developers, but for those dabbling and getting into it or unsure or whatever the reason is…you might learn a thing or two! Also, there are several ways to achieve this, but this is how I would do mine.

My complete version of code is here and here if you want to get straight to the coding!

Beware, I am no professional kernel developer. I just read the docs, die of boredom, and science the shit out of things

Implementing DACL(s)

Let’s take a look at a pretty basic driver so it can make a little more sense.

#include <ntddk.h>

extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING)
{
    NTSTATUS Status = STATUS_UNSUCCESSFUL;
    PDEVICE_OBJECT DeviceObject = NULL;

    Status = IoCreateDevice( ...Params..., &DeviceObject);
    if (!NT_SUCCESS(Status))
    {
        if (DeviceObject)
        {
            IoDeleteDevice(DeviceObject);
        }
        return Status;
    }
    return Status;
}

IoCreateDevice is used to create a device object for the driver to use. This is pretty vanilla so when you compile this, the permissions for it will be pretty relaxed. Low level users will be allowed to interact with the kernel as you can see in the picture.

There isn’t really an issue here, but it depends on how the driver is programmed. We can ask ourselves questions like what functions are accessible from user-land? Can user-land input influence anything? Is it possible for memory corruption or logic bugs that can be exploited? Are privileged instructions like RDMSR and/or WRMSR available to the user?

For a real world example, Intel had a driver called iqvw64e.sys that exposed the following APIs: MmMapIoSpace, MmUnmapIoSpace, and my personal favorite…MmGetPhysicalAddress! The issue here is since these are exposed to the low level user, a user can map arbitrary memory and read what they’re not supposed to and escalate their privileges which can lead to more amazing horrible things. MmGetPhysicalAddress only makes it easy because virtual address to physical address translation is being done for you. Not having to brute force? Pssh, I think yes!

The possibilities are endless!

We have to implement DACLs to prevent this from even happening in the first place! Unfortunately it isn’t very straight forward and took me a little bit of time and doc reading to find out how to actually do this.

There are a number of API’s that can be used and going into each and every one of them will just be kind of…gross.

The first API I use is RtlCreateAcl. Pretty basic, nothing crazy just creates an ACL. Now here is where the heavy lifting begins. Let’s break down RtlAddAccessAllowedAce. The parameters it takes in are:

NTSYSAPI NTSTATUS RtlAddAccessAllowedAce(
  PACL        Acl,
  ULONG       AceRevision,
  ACCESS_MASK AccessMask,
  PSID        Sid
);

ACL is the buffer that is allocated that holds the PACL structure that will get populated with the ACL’s we are about to make. Now the AccessMask is where the permissions get assigned. So you can specify READ or WRITE, DELETE, FILE_ALL_ACCESS, etc. Lastly, the Security Identifier, or Sid, is for the user that will have this access. Now the caveat is each group/user/whatever will need to have their own entry. The following code is what I would do if I just wanted LocalSystem and Administrators to access the device object:

// Create an ACE to allow Local System
Status = RtlAddAccessAllowedAce(TempPaclAcl, ACL_REVISION, FILE_ALL_ACCESS, SeExports->SeLocalSystemSid);
if (!NT_SUCCESS(Status))
{
    // Error handling
}

// Create an ACE to allow Administrators
Status = RtlAddAccessAllowedAce(TempPaclAcl, ACL_REVISION, FILE_ALL_ACCESS, SeExports->SeAliasAdminsSid);
if (!NT_SUCCESS(Status))
{
    // Error handling
}

That is the most important part for the ACL. Afterwards, the security descriptor needs to get created and set with RtlCreateSecurityDescriptor and RtlSetDaclSecurityDescriptor, then validated by RtlValidSecurityDescriptor.

// Create the security descriptor
Status = RtlCreateSecurityDescriptor(TempPaclAcl2, ACL_REVISION1);
if (!NT_SUCCESS(Status))
{
    // Error handling
}

// Set the security descriptor
Status = RtlSetDaclSecurityDescriptor(TempPaclAcl2, TRUE, TempPaclAcl, FALSE);
if (!NT_SUCCESS(Status))
{
    // Error Handling
}

// Check to see if it was set and verify it was
if (!RtlValidSecurityDescriptor(TempPaclAcl2))
{
    // Error Handling
}

If all is well on that aspect, we should be set! All that is left now is to enforce the DACL and there should be a change of permissions to the driver’s device object. We can’t just write to this so we use ZwSetSecurityObject to set the device object’s security state. This particular function does need a handle to the device object, so ObOpenObjectByPointer is used to obtain a handle to it. This function takes in seven parameters but none are really needed other than the device object and the handle that will get populated.

HANDLE hFile = NULL;

Status = ObOpenObjectByPointer(DeviceObject, NULL, NULL, NULL, NULL, NULL, &hFile);
if (!NT_SUCCESS(Status))
{
    // Error Handling
}

Now we can enforce the DACL!

Status = ZwSetSecurityObject(hFile, DACL_SECURITY_INFORMATION, TempPaclAcl2);
if (!NT_SUCCESS(Status))
{
    // Error Handling
}
ZwClose(hFile);

Now if we recompile the driver and view it again, we are greeted with a device object that can only be interacted with LocalSystem or Administrators. Super sick.

To summarize: I used RtlCreateAcl to begin constructing the ACL then used RtlAddAccessAllowedAce to only allow access from Administrators and LocalSystem. I then used RtlCreateSecurityDescriptor, RtlSetDaclSecurityDescriptor, and RtlValidSecurityDescriptor to create the descriptor and verify it. Lastly, I used ZwSetSecurityObject to enforce the newly create ACLs. There’s a little work that needs to be done for this to happen but I think it is worth it in the end to properly allow access to it 🙂

You can find the full implementation at the bottom of this page or view it here on Github.

Token Checking

Is there ever a situation where you just CAN’T do that? I’m not quite sure either but in the event you do find yourself in a situation where you can’t do the above, checking the token would be another way to enforce some protection to the dark side.

The first thing that is done is check to see if the request is coming from user-land and if so, a series of checks then begin to take place.

NTSTATUS DriverCreate(PDEVICE_OBJECT, PIRP Irp)
{
    NTSTATUS Status = STATUS_UNSUCCESSFUL;

    if (Irp->RequestorMode == UserMode)
    {
        // do something
    }
}

The goal here is to yank the token from the process, where you can find in the IO_SECURITY_CONTEXT structure. You can do that by:

pFileObject = StackLocation->FileObject; 
if (!pFileObject || !pFileObject->FileName.Length)
{
    // Grab the Security Context that way we can have access to the token
    pSecurityContext = StackLocation->Parameters.Create.SecurityContext;
    if (pSecurityContext != NULL && pSecurityContext->AccessState != NULL)
    {
        // do something
    }
}

Sick. Let’s have a looksy at the SECURITY_SUBJECT_CONTEXT struct:

typedef struct _SECURITY_SUBJECT_CONTEXT {
  PACCESS_TOKEN                ClientToken;
  SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;
  PACCESS_TOKEN                PrimaryToken;
  PVOID                        ProcessAuditId;
} SECURITY_SUBJECT_CONTEXT, *PSECURITY_SUBJECT_CONTEXT;

I haven’t seen a ClientToken before which is pretty neat, but we definitely want to get access to the primary token. Here is where SeQueryInformationToken comes to play. SeQueryInformationToken takes in three parameters:

NTSTATUS SeQueryInformationToken(
  PACCESS_TOKEN           Token,
  TOKEN_INFORMATION_CLASS TokenInformationClass,
  PVOID                   *TokenInformation
);

TOKEN_INFORMATION_CLASS has lots of values that you can check against the token such as TokenType, TokenIsAppContainer, etc. Some pretty interesting ones but I am interested only in TokenIntegrityLevel because as stated in the docs:

The buffer receives a TOKEN_MANDATORY_LABEL structure that specifies the token's integrity level. This value is valid starting with Windows Vista. For SeQueryInformationToken the output is the actual integrity level (DWORD).

This sounds exactly what I am looking for. The game plan now is to store the information of the current token we have into our token information pointer and then see the Relative Identifier, or RID, to see the integrity level. Here is the code to visualize it:

PVOID pTokenInformation = NULL;

pAccessToken = pSecurityContext->AccessState->SubjectSecurityContext.PrimaryToken;
if (pAccessToken != NULL)
{
    Status = SeQueryInformationToken(pAccessToken, TokenIntegrityLevel, &pTokenInformation);
    if (!NT_SUCCESS(Status))
    {
        // error handling
    }
    if (PtrToUlong(pTokenInformation) < SECURITY_MANDATORY_HIGH_RID)
    {
        Status = ACCESS_DENIED;
    }
}

Anything that is below SECURITY_MANDATORY_HIGH_RID will get denied. These will be the users like low level users, etc. These values are defined in ntifs.h:

#define SECURITY_MANDATORY_LABEL_AUTHORITY          {0,0,0,0,0,16}
#define SECURITY_MANDATORY_UNTRUSTED_RID            (0x00000000L)
#define SECURITY_MANDATORY_LOW_RID                  (0x00001000L)
#define SECURITY_MANDATORY_MEDIUM_RID               (0x00002000L)
#define SECURITY_MANDATORY_MEDIUM_PLUS_RID          (SECURITY_MANDATORY_MEDIUM_RID + 0x100)
#define SECURITY_MANDATORY_HIGH_RID                 (0x00003000L)
#define SECURITY_MANDATORY_SYSTEM_RID               (0x00004000L)
#define SECURITY_MANDATORY_PROTECTED_PROCESS_RID    (0x00005000L)

So what I’m doing is checking if my token is under 0x3000 and if it is, I deny access. Pretty neat, huh?

All that is left to do now is process the Irp and all is well! This check will now prevent a regular user from interacting with the kernel. Possible vulnerability…MI-MI-MI-MI-MITIGATED!

To summarize: Every call made to the kernel’s dispatch table will always hit IRP_MJ_CREATE first before it gets to where it needs to so by implementing a token check in IRP_MJ_CREATE, we check everything BEFORE and grant/deny based on the token’s integrity value. This, too, will help minimize the possibility of a low level user to escalate their privileges if privileged instructions or interesting functions are exposed.

That is all!

NTSTATUS DriverCreate(PDEVICE_OBJECT, PIRP Irp)
{
    NTSTATUS Status = STATUS_UNSUCCESSFUL;
    PFILE_OBJECT pFileObject = NULL;
    PVOID pTokenInformation = NULL;

    PACCESS_TOKEN pAccessToken = NULL;
    PIO_SECURITY_CONTEXT pSecurityContext = NULL;

    PIO_STACK_LOCATION StackLocation = IoGetCurrentIrpStackLocation(Irp);

    if (Irp->RequestorMode == UserMode)
    {
        pFileObject = StackLocation->FileObject;

        if (!pFileObject || !pFileObject->FileName.Length)
        {
            // Grab the Security Context that way we can have access to the token
            pSecurityContext = StackLocation->Parameters.Create.SecurityContext;
            if (pSecurityContext != NULL && pSecurityContext->AccessState != NULL)
            {
                // Get the primary token to check the integrity of it
                pAccessToken = pSecurityContext->AccessState->SubjectSecurityContext.PrimaryToken;
                if (pAccessToken != NULL)
                {
                    Status = SeQueryInformationToken(pAccessToken, TokenIntegrityLevel, &pTokenInformation);
                    if (!NT_SUCCESS(Status))
                    {
#ifndef _DEBUG
                        DbgPrint("[%s::%d] Failed with status: 0x%08x\n", __FUNCTION__, __LINE__, Status);
#endif
                        goto ExitFailure;
                    }

                    // Deny access if it's not being ran by an admin or higher
                    if (PtrToUlong(pTokenInformation) < SECURITY_MANDATORY_HIGH_RID)
                    {
                        Status = STATUS_ACCESS_DENIED;
                    }
                    pTokenInformation = NULL;
                }
            }
        }
        else
        {
            Status = STATUS_ACCESS_DENIED;
        }
    }

ExitFailure:
    Irp->IoStatus.Information = 0;
    Irp->IoStatus.Status = Status;

    IoCompleteRequest(Irp, IO_NO_INCREMENT);

#ifndef _DEBUG
    DbgPrint("[%s::%d] Completed Successfully.\n", __FUNCTION__, __LINE__);
#endif
    return Status;
}
NTSTATUS DriverCreate(PDEVICE_OBJECT, PIRP Irp)
{
    NTSTATUS Status = STATUS_UNSUCCESSFUL;
    PFILE_OBJECT pFileObject = NULL;
    PVOID pTokenInformation = NULL;

    PACCESS_TOKEN pAccessToken = NULL;
    PIO_SECURITY_CONTEXT pSecurityContext = NULL;

    PIO_STACK_LOCATION StackLocation = IoGetCurrentIrpStackLocation(Irp);

    if (Irp->RequestorMode == UserMode)
    {
        pFileObject = StackLocation->FileObject;

        if (!pFileObject || !pFileObject->FileName.Length)
        {
            // Grab the Security Context that way we can have access to the token
            pSecurityContext = StackLocation->Parameters.Create.SecurityContext;
            if (pSecurityContext != NULL && pSecurityContext->AccessState != NULL)
            {
                // Get the primary token to check the integrity of it
                pAccessToken = pSecurityContext->AccessState->SubjectSecurityContext.PrimaryToken;
                if (pAccessToken != NULL)
                {
                    Status = SeQueryInformationToken(pAccessToken, TokenIntegrityLevel, &pTokenInformation);
                    if (!NT_SUCCESS(Status))
                    {
#ifndef _DEBUG
                        DbgPrint("[%s::%d] Failed with status: 0x%08x\n", __FUNCTION__, __LINE__, Status);
#endif
                        goto ExitFailure;
                    }

                    // Deny access if it's not being ran by an admin or higher
                    if (PtrToUlong(pTokenInformation) < SECURITY_MANDATORY_HIGH_RID)
                    {
                        Status = STATUS_ACCESS_DENIED;
                    }
                    pTokenInformation = NULL;
                }
            }
        }
        else
        {
            Status = STATUS_ACCESS_DENIED;
        }
    }

ExitFailure:
    Irp->IoStatus.Information = 0;
    Irp->IoStatus.Status = Status;

    IoCompleteRequest(Irp, IO_NO_INCREMENT);

#ifndef _DEBUG
    DbgPrint("[%s::%d] Completed Successfully.\n", __FUNCTION__, __LINE__);
#endif
    return Status;
}

 

Leave a Reply

Your email address will not be published. Required fields are marked *