windows内核编程基础-0

0. 准备

  • 驱动程序:针对某硬件,连接硬件与OS;
  • 内核程序:针对某功能,作为内核的插件。

sys文件对ntkrnl.exe,就像dll对OD.exe(等支持插件的程序)。

驱动由3类:

  1. NT:原始模型;
  2. WDM,基于NT,2000年之后;
  3. WDF,封装WDM。

0.0 关于API

Windows的API分为用户层API和内核层API。在用户层中,无法直接调用内核层API,实际上,绝大部分的用户层的API在最底层都会通过ntdll.dll切换到内核层,间接调用内核层的API。

在内核层编程中,不能调用用户层的API(如CreateFile)。在内核层中会提供内核版本的创建文件,创建进程等等API。

0.1 命名前缀

windows系统内核将不同的API分成了不同的组件,每个组件的API名称等会带有一个组件的名称:

函数前缀所属的组件或函数说明
Cc缓存管理器
Cm配置管理器(即注册表)
Dbg/Kd调试支持函数
Ex执行体函数
FsRtl文件系统驱动程序
Fstub文件系统引导接口函数
HalHAL提供的接口函数
IoI/O管理器
Ke内核函数
Lpc本地过程调用(LPC)函数
Mm内存管理器
Ntwindwos系统服务
Ob对象管理器
Perf日志记录函数
Po电源管理器
Pp即插即用管理器
Ps进程/线程
RawRAW文件系统函数
Rtl内存运行时库函数
Se安全函数
Vf驱动程序检验器函数
WmiWindows管理器规范

0.2 返回值

内核API通常返回NTSTATUS类型的宏,STATUS开头,如STATUS_SUCCESS, STATUS_PENDING(等待,常见于异步操作)

0.3 函数所占内存属性

指定函数所占内存属性:

#pragma alloc_text(类型, 函数)

类型:

  • INIT,调用完即可释放;
  • PAGE,位于分页内存;
  • NONE_PAGE,位于非分页内存。

0.4 数据类型

数据类型长度基本型类型名称
UINT88 bitunsigned char无符号字符
UCHAR8 bitunsigned char无符号字符
PUCHAR32 bitunsigned char*无符号字符指针
UINT1616 bitunsigned short无符号短整形
USHORT16 bitunsigned short无符号短整形
PUSHORT32 bitunsigned short*无符号短整形指针
UINT3232 bitunsigned int无符号整形
UINT32 bitunsigned int无符号整形
ULONG32 bitunsigned long无符号长整形
PUNIT32 bitunsigned int*无符号整形指针
UINT6464 bitunsigned __int64无符号64位整形
ULONG6464 bitunsigned __int64无符号64位整形
PULONG6432 bitunsigned __int64*无符号64位整形指针

字符串

内核中统一采用UNICODE_STRING结构体来存取字符串,这样做是为了更安全。

typedef struct _UNICODE_STRING {
    USHORT Length;  // 字符串的长度,单位是字节数
    USHORT MaximumLength;   // 最大字节数
#ifdef MIDL_PASS
    [size_is(MaximumLength / 2), length_is((Length) / 2) ] USHORT * Buffer;
#else // MIDL_PASS
    _Field_size_bytes_part_opt_(MaximumLength, Length) PWCH   Buffer;
#endif // MIDL_PASS
}   UNICODE_STRING;

操作这个结构体的函数:

函数名功能
RtlInitUnicodeString初始化字符串 ,注意,此函数不会分配空间.
RtlFreeUnicodeString销毁字符串
RtlCopyUnicodeString拷贝字符串
RtlAppendUnicodeStringToString追加字符串
RtlCmpareUnicodeString比较字符串
RtlUnicodeStringToInteger字符串转数字
RtlIntegerToUnicodeString数字转字符串
RtlAppendUnicodeStringToString将UNICODE字符串结构转换为ANSI
Kdprint输出调试信息

字符串初始化可以使用宏RTL_CONSTANT_STRING

UNICODE_STRING str = RTL_CONSTANT_STRING(L"hello");

示例:

#include <ntddk.h>

void OnUnLoad(DRIVER_OBJECT* driver) {
    driver;
}

NTSTATUS DriverEntry(
    DRIVER_OBJECT* _this,
    UNICODE_STRING* regPath)
{
    regPath;
    NTSTATUS status = STATUS_SUCCESS;
    _this->DriverUnload = OnUnLoad;

    DbgBreakPoint();

    // 字符串操作操作
    UNICODE_STRING path = { 0 };
    // 初始化的操作,只是将常量字符串保存到指针中.
    // 所以path不能被修改.
    RtlInitUnicodeString(&path, L"c:\\123\\456.txt");

    // 追加字符串(修改常量缓冲区,是错误,会蓝屏)
    __try {
        RtlAppendUnicodeToString(&path, L"\\*");
    }
    __except ( EXCEPTION_EXECUTE_HANDLER ) {
        KdPrint(("产生异常:%08X" , GetExceptionCode()));
    }

    // 给字符串分配可写的缓冲区
    WCHAR buff[100];
    path.Buffer = buff;
    path.Length = 0;
    path.MaximumLength = sizeof(buff);

    // 或者申请堆空间( 在内核中,栈空间非常小 )
    WCHAR* pBuff = (WCHAR*)ExAllocatePool(PagedPool, 1024);
    path.Buffer = pBuff;
    path.Length = 0;
    path.MaximumLength = 1024;
    RtlAppendUnicodeToString(&path, L"Hello ");
    RtlAppendUnicodeToString(&path, L"world");

    // 注意这里的输出方法
    KdPrint(("path=%wZ\n", &path));

    // 初始化方法
    UNICODE_STRING str1 = RTL_CONSTANT_STRING(L"WORLD");
	KdPrint((" %wZ\n", &str1));

    // 释放内存
    ExFreePool(pBuff);
    return status;
}

内存操作

ExAllocatePool, 内存分配函数.

ExFreePool, 内存释放函数.

内核链表

在内核中,有很多链表:进程内核对象链表,线程内核对象链表,驱动对象链表,模块链表…

Windows内核中,链表都是使用如下结构的双向链表:

typedef struct _LIST_ENTRY {
    struct _LIST_ENTRY *Flink; // 指向下一个节点,如果没有下一个,则指向链表的头结点(
    struct _LIST_ENTRY *Blink; // 指向上一个节点.(双向链表)
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

操作链表:

  1. InitializeListHead 初始化链表
  2. InsertHeadList 将节点插入到链表头部
  3. InsertTailList 将节点插入到链表尾部
  4. RemoveTailList 删除该节点前一个节点

1. 一个简单的驱动

驱动程序一般使用C语言项目,也可以使用C++项目,但是,C++由缺点:

  • 名称粉碎机制,在定义函数的时候,需要加入extern "C"
  • 效率低。

基本的3点:

  1. #include <ntddk.h>
  2. DriverEntry()入口函数;
  3. 驱动卸载函数。

注意:

  • 系统版本要匹配:Driver Setting - target OS versionplatform
  • 大多时候要release编译

其次,每一个警告都要修复。

#include <ntddk.h>

VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
	DbgPrint("Driver unload\n");
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pRegistryString)
{
	DbgPrint(("First Driver\n"));
	pDriver->DriverUnload = DriverUnload;
	return STATUS_SUCCESS;
}

1.0 加载/卸载驱动


void CDriverLoaderDlg::OnBnClickedBtnload()
{
	// 1. 打开服务管理器
	m_hServiceManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
	// 2. 创建服务
	m_hService = CreateService(m_hServiceManager, 
		L"ServiceName",
		L"ServceDisplayName",
		SERVICE_ALL_ACCESS,
		SERVICE_KERNEL_DRIVER,	//Service Type: Driver
		SERVICE_DEMAND_START,
		SERVICE_ERROR_IGNORE,
		m_strPath,
		NULL,
		NULL,
		NULL,
		NULL,
		NULL);
	// 2.1 若服务存在,直接打开
	if (ERROR_SERVICE_EXISTS == GetLastError())
	{
		m_hService = OpenService(m_hServiceManager, L"ServiceName", SERVICE_ALL_ACCESS);
	}
	// 2.2 判断是否成功
	if (!m_hService)
	{
		CString cs;
		cs.Format(_T("%d"), GetLastError());
		MessageBox(L"CreateService() Failed", cs);
		CloseServiceHandle(m_hServiceManager);
		return;
	}

	// 3. 查询服务状态,若服务暂停,则启动服务
	SERVICE_STATUS status;
	QueryServiceStatus(m_hService, &status);
	if (SERVICE_STOPPED == status.dwCurrentState)
	{
		StartService(m_hService, NULL, NULL);
		Sleep(1000);

		// check again
		QueryServiceStatus(m_hService, &status);
		if (status.dwCurrentState != SERVICE_RUNNING)
		{
			MessageBoxA(0, 0, 0, 0);
			CloseServiceHandle(m_hServiceManager);
			CloseServiceHandle(m_hService);
		}
		else
		{
			m_strStatus = L"Running";
			UpdateData(FALSE);

		}
	}
	
	m_hDev = CreateFileW(
		L"\\\\.\\mysymbol",
		GENERIC_READ | GENERIC_WRITE,
		FILE_SHARE_READ,
		NULL,
		OPEN_EXISTING,
		FILE_ATTRIBUTE_NORMAL,
		NULL);
	if (INVALID_HANDLE_VALUE == m_hDev)
	{
		LPVOID lpMsgBuf;
		FormatMessage(
			FORMAT_MESSAGE_ALLOCATE_BUFFER |
			FORMAT_MESSAGE_FROM_SYSTEM |
			FORMAT_MESSAGE_IGNORE_INSERTS,
			NULL,
			GetLastError(),
			0, // Default language
			(LPTSTR)&lpMsgBuf,
			0,
			NULL
		);

		MessageBoxW((LPCTSTR)lpMsgBuf, L"Error", MB_OK | MB_ICONINFORMATION);
		// Free the buffer.
		LocalFree(lpMsgBuf);

	}
}


void CDriverLoaderDlg::OnBnClickedBtnunload()
{
	// 若服务不是停止状态,则停止
	SERVICE_STATUS status;

	CloseHandle(m_hDev);

	QueryServiceStatus(m_hService, &status);
	if (SERVICE_STOPPED != status.dwCurrentState)
	{
		ControlService(m_hService, SERVICE_CONTROL_STOP, &status);
		while(QueryServiceStatus(m_hService, &status))
		{
			Sleep(status.dwWaitHint);
			if (SERVICE_STOPPED == status.dwCurrentState)
			{
				m_strStatus = L"Stopped";
				UpdateData(FALSE);
				break;
			}
			else
			{
				m_strStatus = L"Can't stop";
				UpdateData(FALSE);

			}
			
			
		}
	}
	
	// 删除服务(卸载驱动)
	if (!DeleteService(m_hService))
	{
		CString cs;
		cs.Format(_T("%d"), GetLastError());
		MessageBox(L"DeleteService Failed", cs);
		//m_State = L"卸载失败";
	}
	else
	{
		Sleep(1000);
		m_strStatus = L"DeleteService Successfully";
		UpdateData(FALSE);
	}

	CloseServiceHandle(m_hService);
	CloseServiceHandle(m_hServiceManager);

}

1.1 调试驱动

其实就是内核调试。

KdPrint(())输出调试信息。需要debug模式,release模式不起作用。

注意双括号。

_asm int 3;DbgBreakPoint(),windbg会自动断下,有源码信息的话会自动源码调试。

1.2 蓝屏处理

*(int*)0 = 0,windbg断下后马上执行!analyze -v,会出现c0000005,奔溃的代码等很多信息。

2. MDL

Memory Descriptor List.

这是一个未公开的结构,相关API可以将内存重新映射,并且指定内存属性。没有它的话,就要考虑IRQL等很多问题。

typedef struct _MDL {
    struct _MDL *Next;  //用于挂入到一个队列中,如插入到驱动程序的IRP的MDL队列中。
    CSHORT Size;    //指定这个MDL所占的空间大小=MDL结构体的大小+sizeof(PFN_NUMBER)*映射需要的页面数。
    CSHORT MdlFlags;    //指明MDL的映射方式
    struct _EPROCESS *Process;  //指明此MDL属于哪个进程。
    PVOID MappedSystemVa;   //所描述的内存如果有映射到系统空间并锁定。那么这个成员指定了MDL在系统空间
    PVOID StartVa;  //所描述的内存映射后的虚拟地址的开始页面地址,这个地址总是页面对齐的地址
    ULONG ByteCount;    //此MDL所描述的内存块有多少个字节
    ULONG ByteOffset;   //MDL映射的虚拟地址的首地址在StartVa页面中的偏移值。
} MDL, *PMDL;

一个连续的虚拟内存地址范围可能是由多个分布(spread over)在不相邻的物理页所组成的。系统使用MDL(内存描述符表)结构体来表明虚拟内存缓冲区的物理页面布局。我们应该避免直接访问MDL。我们可以使用MS Windows提供的宏,他们提供了对这个结构体基本 的访问。

  • MmGetMdlVirtualAddress 获取缓冲区的虚拟内存地址
  • MmGetMdlByteCount 获取缓冲区的大小(字节数)
  • MmGetMdlByteOffset 获取缓冲区开端的物理页的大小(字节数)
  • MmGetMdlPfnArray 获取记录物理页码的一个数组指针。

MDL API

分配/取消分配MDL:

  • IoCreateMDL(老函数)
  • IoAllocateMdl
  • IoFreeMdl

把缓冲区转制成MDL:MmInitializeMdl

但是以上两种方式都不能初始化物理页码数组。


APIDesc
MmBuildMdlForNonPagedPool()建立MDL,初始化物理页码数组。
MmProbeAndLockPages()锁定分页内存
MmUnlockPages()取消锁定
MmMapLockedPages()将内存映射到内核空间或用户空间
MmMapLockedPagesSpecifyCache()获取VA
MmUnmapLockedPages()取消锁定

对于在非分页池中分配的缓冲区,可以用MmBuidlMdlForNonpagedPool ())来初始化页码数组。

对于可分页的内存,虚拟内存和物理内存之间的联系是暂时的,所以MDL的页码数组只在特定的环境和 时间段有效,因为很可能其他的程序对它们进行重新分配,为了使其他的程序无法对他们进行修改和 重新分配(在我们释放之前),我们就需要把这段内存锁定,防止其他程序修改。

MmProbeAndLockPages()可以实现这个功能,这个函数同时还为当前的布局初始化了页码数组。当我们用MmUnlockPages())来释放被锁定的内存时,页码数组也会随之无效。


MmGetSystemAddressForMdlSafe()返回非分页内核空间VA 。

假如MDL指定的是映射一块内核级别的虚拟地址空间,那么我们要用MmGetSystemAddressForMdlSafe(),这样我们就能防止映射目标是来自用户模式的空间,而来自用户模式空间的物理页只能在用户模式上下文环境中使用,并且随时可能被清空。用函数进行申明后,就可以防止以上情况发生了。


还可以通过ExAllocatePool()创建一个MDL,通过MmInitializeMdl()将它初始化为MDL。此时的内存必须是非分页(nonpageable)的。释放用ExFreePool()

MDL小结

MDL就是描述一块虚拟内存的结构体,里面有个成员记录了多个页码,这些页码即处于各个不同物理地址的物理块的页号。

所以要对一块受系统保护的区域进行写操作的话,可以这样来修改它的保护属性:

  1. IoAllocateMdl()创建一个MDL,显然里面的物理页号数组没有初始化
  2. MmBuildMdlForNonPagedPool()初始化页码数组,使之成为实际有效的MDL
  3. MmProbeAndLockPages()进行锁定,并且重新赋值新的保护属性为可读
  4. MmMapLockedPagesSpecifyCache()获得我们所映射后的实际内存区域的虚拟地址

用MDL描述内存页后,如果分配了:

  • 非页面缓冲池内存,则用MmBuildMdlForNonPagedPool()
  • 页面缓冲池内存,则用MmProbeAndLockPages()

将锁定页面映射进用户的地址空间:UserVirtualAddress = MmMapLockedPages(MDL, UserMode);


示例1:

const wchar_t* pStr = L"123456789abcdefg0"; // 常量字符串,不可修改.
// 创建一个内存描述符列表
PMDL mdl = IoAllocateMdl(pStr, 17/*字节数*/, 0, 0, 0);
// 为内存描述符列表建立虚拟内存分页
MmBuildMdlForNonPagedPool(&mdl);
// 将虚拟内存加载到物理内存, 修改内存描述符分页属性为可写,并返回虚拟内存分页地址
wchar_t* p = (wchar_t*)MmMapLockedPagesSpecifyCache(mdl, KernelMode, MmWriteCombined, 0, 
p[1] = 'A'; // 本来不可修改的内存现在可以修改了.
// 取消锁定和映射
MmUnmapLockedPages(p, mdl);
// 释放内存描述符列表
IoFreeMdl(mdl);

示例2:

PMDL pMdl;
if (pMdl = MmCreateMdl(
    NULL,
    pBuf,
    nLen))
{
    MmBuildMdlForNonPagedPool(pMdl);
    pMdl->MdlFlags |= MDL_MAPPED_TO_SYSTEM_VA;
    pVirtualAddr = MmMapLockedPages(pMdl, KernelMode);
    /*
        Do Something
    */
    MmUnlockPages(pMdl);
    IoFreeMdl(pMdl);
}

可以分析wrk NtWriteFile来分析MDL。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值