文章目录
0. 准备
- 驱动程序:针对某硬件,连接硬件与OS;
- 内核程序:针对某功能,作为内核的插件。
sys文件对ntkrnl.exe,就像dll对OD.exe(等支持插件的程序)。
驱动由3类:
- NT:原始模型;
- WDM,基于NT,2000年之后;
- 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 | 文件系统引导接口函数 |
Hal | HAL提供的接口函数 |
Io | I/O管理器 |
Ke | 内核函数 |
Lpc | 本地过程调用(LPC)函数 |
Mm | 内存管理器 |
Nt | windwos系统服务 |
Ob | 对象管理器 |
Perf | 日志记录函数 |
Po | 电源管理器 |
Pp | 即插即用管理器 |
Ps | 进程/线程 |
Raw | RAW文件系统函数 |
Rtl | 内存运行时库函数 |
Se | 安全函数 |
Vf | 驱动程序检验器函数 |
Wmi | Windows管理器规范 |
0.2 返回值
内核API通常返回NTSTATUS
类型的宏,STATUS
开头,如STATUS_SUCCESS, STATUS_PENDING(等待,常见于异步操作)
0.3 函数所占内存属性
指定函数所占内存属性:
#pragma alloc_text(类型, 函数)
类型:
INIT
,调用完即可释放;PAGE
,位于分页内存;NONE_PAGE
,位于非分页内存。
0.4 数据类型
数据类型 | 长度 | 基本型 | 类型名称 |
---|---|---|---|
UINT8 | 8 bit | unsigned char | 无符号字符 |
UCHAR | 8 bit | unsigned char | 无符号字符 |
PUCHAR | 32 bit | unsigned char* | 无符号字符指针 |
UINT16 | 16 bit | unsigned short | 无符号短整形 |
USHORT | 16 bit | unsigned short | 无符号短整形 |
PUSHORT | 32 bit | unsigned short* | 无符号短整形指针 |
UINT32 | 32 bit | unsigned int | 无符号整形 |
UINT | 32 bit | unsigned int | 无符号整形 |
ULONG | 32 bit | unsigned long | 无符号长整形 |
PUNIT | 32 bit | unsigned int* | 无符号整形指针 |
UINT64 | 64 bit | unsigned __int64 | 无符号64位整形 |
ULONG64 | 64 bit | unsigned __int64 | 无符号64位整形 |
PULONG64 | 32 bit | unsigned __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;
操作链表:
InitializeListHead
初始化链表InsertHeadList
将节点插入到链表头部InsertTailList
将节点插入到链表尾部RemoveTailList
删除该节点前一个节点
1. 一个简单的驱动
驱动程序一般使用C语言项目,也可以使用C++项目,但是,C++由缺点:
- 名称粉碎机制,在定义函数的时候,需要加入
extern "C"
; - 效率低。
基本的3点:
#include <ntddk.h>
;DriverEntry()
入口函数;- 驱动卸载函数。
注意:
- 系统版本要匹配:
Driver Setting - target OS version
和platform
。 - 大多时候要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
但是以上两种方式都不能初始化物理页码数组。
API | Desc |
---|---|
MmBuildMdlForNonPagedPool() | 建立MDL,初始化物理页码数组。 |
MmProbeAndLockPages() | 锁定分页内存 |
MmUnlockPages() | 取消锁定 |
MmMapLockedPages() | 将内存映射到内核空间或用户空间 |
MmMapLockedPagesSpecifyCache() | 获取VA |
MmUnmapLockedPages() | 取消锁定 |
对于在非分页池中分配的缓冲区,可以用MmBuidlMdlForNonpagedPool ()
)来初始化页码数组。
对于可分页的内存,虚拟内存和物理内存之间的联系是暂时的,所以MDL的页码数组只在特定的环境和 时间段有效,因为很可能其他的程序对它们进行重新分配,为了使其他的程序无法对他们进行修改和 重新分配(在我们释放之前),我们就需要把这段内存锁定,防止其他程序修改。
MmProbeAndLockPages()
可以实现这个功能,这个函数同时还为当前的布局初始化了页码数组。当我们用MmUnlockPages()
)来释放被锁定的内存时,页码数组也会随之无效。
MmGetSystemAddressForMdlSafe()
返回非分页内核空间VA 。
假如MDL指定的是映射一块内核级别的虚拟地址空间,那么我们要用MmGetSystemAddressForMdlSafe()
,这样我们就能防止映射目标是来自用户模式的空间,而来自用户模式空间的物理页只能在用户模式上下文环境中使用,并且随时可能被清空。用函数进行申明后,就可以防止以上情况发生了。
还可以通过ExAllocatePool()
创建一个MDL,通过MmInitializeMdl()
将它初始化为MDL。此时的内存必须是非分页(nonpageable)的。释放用ExFreePool()
。
MDL小结
MDL就是描述一块虚拟内存的结构体,里面有个成员记录了多个页码,这些页码即处于各个不同物理地址的物理块的页号。
所以要对一块受系统保护的区域进行写操作的话,可以这样来修改它的保护属性:
IoAllocateMdl()
创建一个MDL,显然里面的物理页号数组没有初始化MmBuildMdlForNonPagedPool()
初始化页码数组,使之成为实际有效的MDLMmProbeAndLockPages()
进行锁定,并且重新赋值新的保护属性为可读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。