正常通信流程:
R3-> 符号链接 -> 设备对象 -> 驱动对象 -> 驱动功能
驱动通信实质上是设备通信
设备是挂在驱动上的 DeviceObject 上面的
正常 IO 通信
R0:
// 创建设备名称
UNICODE_STRING Devicename;
RtlInitUnicodeString(&Devicename,L"\\Device\\MyDevice");
// 创建设备
IoCreateDevice(
pDriver, // 当前设备所属的驱动对象
0,
&Devicename, // 设备对象的名称
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&pDeviceObj // 设备对象指针
);
R3:
CreateFileW(SYMBOL_LINK_NAME,GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
DeviceIoControl(g_Device,KillAPP,&pEprocess,sizeof(DWORD),&outBuffer,sizeof(DWORD),NULL,NULL))
隐蔽通信原理与常见种类
正常的设备通信会被各种 ark 或其他工具遍历到
winobj:https://learn.microsoft.com/zh-cn/sysinternals/downloads/winobj
devicetree.exe
其次通过无模块加载技术加载的驱动没有驱动对象,也无法创建正常的 io 通信。所以我们另寻通信方式。
任何能从 3 环主动发起 0 环能接收到的 API 或方法都能作为通信方法,如果对通信的实时性没要求也可去掉“主动”一词
常见隐蔽通信方式
1. 劫持 io 通信
故名意思,去劫持系统白名单驱动的通信。
在导入表有查看是否有建立 IO 通信的函数
发现没有 IRP_MJ_DEVICE_CONTROL 我们可以给他增加一个来作为我们的通信
2.minifilter 端口
FltCreateCommunicationPort
FltCloseCommunicationPort
通过回调函数的 InputBuffer 接受用户态的消息并通过 OutputBuffer 回复
用户态通过 FilterConnectCommunicationPort 和 FilterSendMessage 通信
3. 共享内存
3 环申请一块内存,0 环使用 mdl 映射
3. 注册表
3 环和内核都有可以读写注册表的函数,故而可以作为通信
if (!WriteRegistryDword(HKEY_LOCAL_MACHINE, L"", L"oPid", GetCurrentProcessId())) {return false;}
if (!WriteRegistryQword(HKEY_LOCAL_MACHINE, L"", L"oAddr", reinterpret_cast<DWORD_PTR>(req))) {
InitializeObjectAttributes(&ObjectAttributes, &RegPath, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);
Status = ZwOpenKey(&KeyHandle, KEY_READ, &ObjectAttributes);
if (!NT_SUCCESS(Status)) {return Status;}
KEY_VALUE_PARTIAL_INFORMATION KeyValueInfo;
Status = ZwQueryValueKey(KeyHandle, &AddrValueName, KeyValuePartialInformation, &KeyValueInfo, sizeof(KeyValueInfo) + sizeof(DWORD), &ResultLength);
if (NT_SUCCESS(Status)) {if (KeyValueInfo.Type == REG_QWORD && KeyValueInfo.DataLength == sizeof(uintptr_t)) {RtlCopyMemory(&Buffer, KeyValueInfo.Data, sizeof(uintptr_t));
oAddr = Buffer;
}
else {Status = STATUS_INVALID_PARAMETER;}
}
ZwClose(KeyHandle);
4. 文件
同上 3 和 0 都有操作文件的函数
5. 套接字
使用 socket 进行通信
6.data ptr
.data 是目前恶意程序最主流的通信方式,因为太多了,很难全部监控,如果找到的.data 足够隐蔽,那么很难短时间内检测到。
有很多内核函数中是函数指针的调用方式,而这些函数指针存在.data 区,或者.rdata 区,我们通过交互指针的方法,让函数执行到我们模块的函数中。
_guard_dispatch_icall_fptr是 Windows 系统中与控制流防护 (CFG) 相关的关键函数指针,主要用于验证间接函数调用的合法性, 我们可以搜索“_guard_dispatch_icall_fptr”来枚举可利用的指针。
ntoskrnl.exe 被 PG 和各大安全软件监控较为严重,我们可以去其他模块中寻找 data ptr(写个脚本让 ida 去跑)
File->Script file…
像这种 NtUser 开头的函数一般都可从 3 环调用到,并且可以发现具有可利用指针
利用 InterlockedExchangePointer 函数交换指针
const PVOID win32k = system::get_kernel_modulebase(("win32k.sys"), &nSize);
if (win32k) {nt_qword = system::search_kernel((uintptr_t)win32k, NT_QWORD_SIG, NT_QWORD_MASK);
const uintptr_t nt_qword_deref = (uintptr_t)nt_qword + 7 + *(int*)((unsigned char*)nt_qword + 3);
*(void**)&oNtOldFun = InterlockedExchangePointer((void**)nt_qword_deref, (void*)NtFun);
}
我们只需要把一个参数当作结构体指针使用,并约定通信码,3 环调用 API 填入指定参数即可完成通信,其他应用不知晓通信码,即可正确调用原 API。
(LoadLibraryA)(("user32.dll"));
(LoadLibraryA)(("win32u.dll"));
const HMODULE win32u = (GetModuleHandleA)(("win32u.dll"));
if (!win32u)
{return 0;}
*(void**)&NtUserFun= GetProcAddress(win32u, ("函数名"));
检测方法
内存与文件做对比,检测指针有效性,栈回溯地址是否在合法模块等等
python 脚本








