Windows Kernel Programming 笔记 1~5 内核开发入门
1 windows内部概况
描述一些Windows内部工作中最重要、最基本的概念,部分概念将在后面的章节做更详细的研究
1.1 进程
进程不运行(Processes dont't run - processes manage),线程才执行代码
进程拥有以下内容:
- 一个可执行程序(PE文件),包括代码和数据
- 私有的虚拟内存空间
- 主令牌(primary token),是一个对象,存储进程默认安全上下文
- 对象(事件、信号、文件)句柄表
- 一个或多个线程(没有线程的用户态进程一般情况下会被内核销毁)
1.2 虚拟内存
每个进程拥有自己的虚拟、私有、线性地址空间
(该地址空间初始时几乎为空,然后pe、ntdll.dll开始被影射,接着是其他子系统dll)
32位进程默认地址空间2GB,设置pe中的LARGEADDRESSAWARE
标志可以增加到3GB(32位系统)或4GB(64位系统)
64位进程默认地址空间128TB(win8之前是8TB)
虚拟内存被映射到物理内存(RAM)或临时驻留在文件中(如page file)
如果不在物理内存,则触发page fault异常,并或取数据到物理内存中
页(page)是内存管理的单位,默认大小为4KB
页状态
虚拟内存中的页处于三种状态之一
- Free:未分配
- Committed:已分配,通常映射到RAM或文件(例如page file)
- Reserved:未分配,对cpu而言与Free相似,自动分配将不会使用该页
一个例子是线程栈(thread stack)
系统内存
系统空间与进程无关
系统空间就是内核
1.3 线程
实际执行代码的是线程
线程拥有的最重要的内容:
- 当前访问模式(用户或内核)
- 执行上下文
- 一个或两个栈(stack)
- Thread Local Storage(TLS)
- 基本优先级和当前(动态)优先级
- 处理器关联信息
线程最常处于的状态:
- Running:在逻辑处理器运行中
- Ready:等待运行(所有处理器在忙或不可用)
- Waiting:等待某个事件,事件触发就变成Ready
括号中的数字是状态号:
Running(2) => Waiting(5) => Deferred Ready(7), Ready(1) => Running(2)
1.3.1 线程栈
线程至少有一个位于内核空间的栈(32位系统12KB,64位系统24KB)
用户态的线程还有一个位于所属进程空间的栈(默认上限1MB)
线程Running
或Ready
时,内核栈驻留在RAM
栈初始时会尽可能少提交页(最少一页),剩下的页设置为Reserved
,而最后一个Committed
的页的下一页设置为PAGE_GUARD
1.4 系统调用(又名系统服务)
原标题:System Services (a.k.a. System Calls)
R3代码通过系统调用完成一些只能在R0下完成的功能,如分配内存、打开文件、创建线程等
大致流程是:
调用subsystem dll(如kernel32.dll)中的文档化api(如CreateFile)
进入NTDLL中的 Native Api(如NtCreateFile)
进入内核中的系统服务分发函数
进入Native Api对应的内核中的函数
Native Api将调用号存入eax然后进入r0的系统服务分发函数,eax实际是SSDT(System Service Dispatch Table)的下标
1.5 通用系统架构
1.6 句柄和对象
对象被引用计数,当计数为0时才会被释放
句柄是进程的对象表的索引
注意:返回值为句柄的函数,大多数失败时返回0
。有些返回INVALID_HANDLE_VALUE (-1)
,比如CreateFile
句柄值是4的倍数,0不是有效句柄值
1.6.1 对象名
某些类型的对象可以有名称,可用于通过合适的 Open 函数按名称打开对象。
用户模式调用 Create 函数按名称创建对象,如果存在,则仅打开现有对象。
winObj中显示的名称有时不是对象的真实名称:
- 进程和线程显示ID
- 文件对象显示文件名(或设备名),因为共享的原因,无法通过文件名获得文件对象句柄
- (注册表)键对象与注册表的路径一起显示,原因同文件对象
- 目录对象显示路径,目录不是文件系统对象,而是对象管理器目录,可通过Sysinternals WinObj查看
- 令牌对象名称与存储在令牌中的用户名一起显示
1.6.2 访问现有对象
Process Explorer 的句柄视图中的访问列显示用于打开或创建句柄的访问掩码
Process Explorer中显示的引用数(References)不是实际引用数(outstanding references)
[windbg]中用!trueref
获取实际引用数(actual reference)
2 内核开发入门
本章主要是关于准备内核开发所需的环境,包括开发和调试的工具以及环境配置
以及启动和运行内核驱动的知识
然后写一个可以加载和卸载的驱动
驱动开发准备工作
首先按 2.1安装工具 完成安装,然后为驱动开发配置虚拟机(未包括内核调试的配置)
安装无签名驱动
如果驱动没有签名,安装驱动需要以该模式启动系统
1 | bcdedit / set testsigning on
|
显示内核调试信息
在HKLM\SYSTEM\CurrentControlSet\Control\Session Manager
添加一个名为Debug Print Filter
的键
在键中添加一个DWORD
,名为DEFAULT
,值为8
虚拟机文件共享
实际操作时,安装在虚拟机中(避免本机崩溃),需要共享项目的文件给虚拟机
共享本机的 vs解决方案文件夹 给虚拟机,名称为MyDriver
虚拟机中的debug输出路径为:\\vmware-host\Shared Folders\MyDriver\x64\Debug
驱动调试工具
安装完WDK后,把C:\Program Files (x86)\Windows Kits\10\Tools\x64
这个目录复制到虚拟机中,这个是x64下的驱动开发调试工具,比如用于查看内存池的poolmon
2.1 安装工具
需要vs2019、windows 10 sdk(vs2019中安装)、windows 10 driver kit(WDK)
以及 Sysinternals,该工具包含debug view、process monitor等一系列有用的工具
在实际编译中发现,新版本的vs驱动项目默认开启缓解Spectre 漏洞
可以在c/c++、代码生成中关闭该项,或在vs installer中安装对应工具
2.2 创建一个驱动工程
vs2019中选择创建一个Empty WDM Driver
,创建完成后有个inf
后缀的文件,暂时不需要,删除掉
2.3 DriverEntry 和 Unload Routines
DriverEntry 是驱动的默认入口点
系统线程以IRQL_PASSIVE_LEVEL
(0)调用 DriverEntry
DriverEntry函数原型:
1 2 3 | extern "C"
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath);
|
一个简单的驱动(sample.cpp):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | void SampleUnload(_In_ PDRIVER_OBJECT DriverObject) {
UNREFERENCED_PARAMETER(DriverObject);
}
extern "C"
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject - >DriverUnload = SampleUnload;
return STATUS_SUCCESS;
}
|
2.4 安装驱动
安装驱动和安装用户态服务相似,需要调用Create Service API或使用工具
sc.exe(系统自带)是著名工具之一
安装驱动需要管理员权限
创建服务项:
1 | sc create sample type = kernel binPath = "\\vmware-host\Shared Folders\MyDriver\x64\Debug\sample.sys"
|
随后就能在注册表(regedit.exe)的HKLM\System\CurrentControlSet\Services\Sample
中看到该项
注册表项位置:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Sample
假设binPath= c:\
,注册表项ImagePath= \??\c:\
假设binPaht= "\\vmware-hots\"
,注册表项ImagePath= \??\UNC\vmware-hots\
加载驱动(启动服务):
在process explorer中,选择System进程,查看dll窗口,拉到最下面就能看到sample.sys
卸载驱动(停止服务):
2.5 简单跟踪(S1)
KdPrint 宏
是DbgPrint API
的包装
通过在每个函数开头加入KdPrint(("Debug messgae"));
可以观察函数调用的发生
使用DebugView,选择capture Kernel可以看到内核调试信息
2.6 练习:显示系统信息(E1)
创建一个驱动用于显示系统版本信息,使用RtlGetVersion
code:
1 2 3 4 5 6 7 8 9 10 | / / Get Version
RTL_OSVERSIONINFOW versionInfo = { 0 , };
versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOW);
RtlGetVersion(&versionInfo);
/ / Print
DbgPrint( "[E1] Major:%d\n[E1] Minor:%d\n[E1] Build:%d" ,
versionInfo.dwMajorVersion,
versionInfo.dwMinorVersion,
versionInfo.dwBuildNumber);
|
shell:
1 2 3 | sc create E1_OSVersion type = kernel binPath = "\\vmware-host\Shared Folders\MyDriver\x64\Debug\E1_OSVersion.sys"
sc start E1_OSVersion
sc stop E1_OSVersion
|
3 内核编程基础
研究一些内核的API、结构和定义,以及一些驱动程序中的机制
3.1 通用内核编程指南
用户模式和内核模式调试的重要区别
|
用户模式 |
内核模式 |
未处理异常 |
进程崩溃 |
系统崩溃 |
终止 |
当进程终止,所有内存和资源都会被自动释放 |
当驱动卸载,如果没有手动释放,会造成泄露直到重启 |
返回值 |
API错误有时候会忽略 |
应该不忽略任何错误 |
IRQL |
总是 PASSIVE_LEVEL (0) |
可能为更高 |
错误代码 |
通常只会影响本进程 |
影响整个系统 |
测试和调试 |
通常在开发机器上调试 |
需要双机调试 |
库(Lib) |
可以使用C/C++库(如STL、boost) |
大多数标准库无法使用 |
异常处理 |
可以使用C++异常或SEH |
只能使用SEH |
C++支持 |
完全的C++支持 |
不支持C++ runtime |
3.1.1 未处理异常
未处理异常会导致蓝屏,原因是防止继续执行代码、对系统造成不可逆转的伤害
内核代码不应该跳过任何细节或错误检查
3.1.2 终止
如果驱动程序卸载时仍保留分配的内存或打开的内核句柄,这些资源不会自动释放,只会在下次系统启动时释放
原因是驱动程序可以分配一些缓冲区,然后将其传递给另一个与之合作的驱动程序
3.1.3 函数返回值
忽略内核API的返回值很危险,应该总是检查返回值
3.1.4 IRQL
中断请求级(Interrupt Request Level, IRQL)通常为0
用户模式下始终为0,内核模式下大部分时间为0
3.1.5 C++使用
没有C++ runtime
一些不支持的C++特性:
- 不支持
new
和delete
,这正常是在用户模式堆分配的
- 不会调用具有非默认构造函数的全局变量
- 避免在构造函数中使用代码,创建一些要显式调用的Init函数
- 仅将指针分配为全局变量,动态创建实例
- 不支持C++异常处理(
try
、catch
、throw
)
- 不可使用标准C++库,如
std::vector<>
、std::wstring
等
一些支持的C++特性:
nullptr
关键字
auto
关键字
- 模板将在有意义时使用
- 重载new 和delete 运算符
- 构造函数和析构函数,尤其是用于构建 RAII 类型
3.1.6 测试和调试
内核调试需要双机调试,一台作为调试者、另一台作为被调试者运行驱动程序
3.2 Debug vs. Release 生成
内核术语是 Checked(Debug)和 Free(Release)
Debug意味着可以使用DBG符号
3.3 内核API
内核API常用前缀的意义:
- Ex:一般执行函数
- Ke:一般内核函数
- Mm:内存管理
- Rtl:一般运行时库
- FsRtl:文件系统运行时库
- Flt:文件系统迷你过滤库
- Ob:对象管理
- Io:I/O管理
- Se:安全
- Ps:进程结构
- Po:电源管理
- Wmi:Windows管理工具
- Zw:native API 包装
- Hal:硬件抽象层
- Cm:配置管理器(注册表)
Nt前缀的内核函数对应NtDll.Dll的函数,会根据 KTHREAD 结构的标记(调用者是否来自内核)对参数进行检查
Zw前缀的内核函数先将调用者模式设为KernelMode(0)
,然后调用Nt前缀的内核函数
3.4 函数和错误代码
可以在ntstatus.h
中找到NTSTATUS
值的定义
大多数代码并不关心错误具体是什么,仅测试最高位即可,可以使用NT_SUCCESS
宏
当返回到用户层时,会由STATUS_xxx
转成ERROR_yyy
,用户模式通过GetLastError可以得到这些错误
通常遇到错误时,会返回相同的 NTSTATUS 到调用函数
3.5 字符串
内核使用UNICODE_STRING
1 2 3 4 5 6 7 | typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWCH Buffer ;
} UNICODE_STRING;
typedef UNICODE_STRING * PUNICODE_STRING;
typedef const UNICODE_STRING * PCUNICODE_STRING;
|
Length
是字符串的字节数(不包括\x00\x00结束符)
MaximumLength
是不需要重新分配内存的情况下、字符串字节数上限
需要注意的是,UNICODE_STRING并不总是有\x00\x00结尾
一些常用的字符串操作函数:
- RtlInitUnicodeString
- RtlCopyUnicodeString
- RtlCompareUnicodeString
- RtlEqualUnicodeString
- RtlAppendUnicodeStringToString
- RtlAppendUnicodeToString
3.6 动态内存分配(S2)
内核提供两种通用内存池(general memory pools)给驱动使用:
- 页池(Paged pool):可能会被换出(paged out)的内存池
- 非页池(Non Paged Pool):一直在RAM中的内存池
枚举类型POOL_TYPE
表示池类型,只有三种是可以用于驱动的:
PagedPool
、NonPagedPool
、NonPagedPoolNx
(non-page pool没有可执行权限)
常用内存池函数:
- ExAllocatePool(已过时,将被下面的函数取代)
- ExAllocatePoolWithTag
- ExAllocatePoolWithQuotaTag
- ExFreePool
tag是4字节的值
可以在PoolMon(WDK的Windows Kits中)中观察到有tag的内存池(tag以大端序字符串显示)
给ustring分配页池内存:
code:
1 2 3 4 5 6 7 8 9 10 | UNICODE_STRING strA;
int length;
/ / allocate
strA. Buffer = (WCHAR * )ExAllocatePoolWithTag(PagedPool,
length, 'dcba' );
if (strA. Buffer = = nullptr) {
KdPrint(( "Failed to allocate memory\n" ));
return STATUS_INSUFFICIENT_RESOURCES;
}
strA.MaximumLength = length;
|
shell:
1 2 3 | sc create S2_DynMemAlloc type = kernel binPath = "\\vmware-host\Shared Folders\MyDriver\x64\Debug\S2_DynMemAlloc.sys"
sc start S2_DynMemAlloc
sc stop S2_DynMemAlloc
|
3.7 链表
内核使用循环双向链表:
1 2 3 4 | typedef struct _LIST_ENTRY {
struct _LIST_ENTRY * Flink;
struct _LIST_ENTRY * Blink;
} LIST_ENTRY, * PLIST_ENTRY;
|
CONTAINING_RECORD
宏执行适当的偏移计算并转换为实际数据类型
CONTAINING_RECORD(pvoid, type, entry_member_name)
1 2 3 4 5 6 7 8 9 | struct MyDataItem {
/ / some data members
LIST_ENTRY Link;
/ / more data members
};
MyDataItem * GetItem(LIST_ENTRY * pEntry) {
return CONTAINING_RECORD(pEntry, MyDataItem, Link);
}
|
常用链表函数(时间复杂度都是常数):
- InitializeListHead
- InsertHeadList
- InsertTailList
- IsListEmpty
- RemoveHeadList
- RemoveTailList
- RemoveEntryList
- ExInterlockedInsertHeadList
- ExInterlockedInsertTailList
- ExInterlockedRemoveHeadList
后三个关于自旋锁,在第6章详细讨论
3.8 驱动对象(The Driver Object)
常用major function代码:
- IRP_MJ_CREATE (0)
- IRP_MJ_CLOSE (2)
- IRP_MJ_READ (3)
- IRP_MJ_WRITE (4)
- IRP_MJ_DEVICE_CONTROL (14)
- IRP_MJ_INTERNAL_DEVICE_CONTROL (15)
- IRP_MJ_PNP (31)
- IRP_MJ_POWER (22)
MajorFunction
数组由内核初始化指向内核内部例程IopInvalidDeviceRequest
,该例程直接返回失败,表示不支持该操作
3.9 设备对象(Device Objects)
驱动通过设备与r3代码通信,驱动应该至少创建一个设备对象并为其命名
CreateFile可以打开设备,第一个参数为设备对象名称
打开文件或设备的句柄会创建内核结构 FILE_OBJECT 的实例,这是个半文档化的结构。
更准确的说,CreteFile接受一个symbolic link
(符号链接)
对象管理器中名为??
的目录下的符号链接都可被用户模式代码通过CreateFile或Createfile2调用
可以通过WinObj查看(WinObj中目录名为Global??
)
使用符号链接的CreateFile的文件名(第一个参数),必须加上前缀\\.\
(c++中是"\\\\.\\"
)
如果创建了多个设备对象,将形成一个单向链表,添加设备时是头插法,所以第一个创建的设备在链表的最后
4 驱动从头到尾(Driver from Start to Finish)(S3)
将完成一个完整的驱动及客户端程序,利用驱动完成只能在内核模式下完成的功能(设置任意级别的线程优先级)
4.1 绪论
线程优先级 = 进程优先级 + 相对线程优先级
用户模式下,设置进程优先级可以用SetPriorityClass
,共有6个级别
设置相对线程优先级可以用SetThreadPriority
,共有7个级别
下面是线程优先级合法值的表(通过windows api设置),据别的书说是个未文档化的东西,windows不建议开发时考虑线程优先级,该表的值随windows版本变化可能发生改变
进程优先级 |
-Sat |
-2 |
-1 |
0 |
+1 |
+2 |
+sat |
Idle(low) |
1 |
|
|
4 |
|
|
15 |
Below Normal |
1 |
|
|
6 |
|
|
15 |
Normal |
1 |
|
|
8 |
|
|
15 |
Above Normal |
1 |
|
|
10 |
|
|
15 |
High |
1 |
|
|
13 |
|
|
15 |
Real-time |
16 |
|
|
24 |
|
|
31 |
进程优先级枚举:级别+_PRIORITY_CLASS
线程优先级枚举:THREAD_PRIORITY_
+级别
4.2 驱动初始化
大多数驱动需要在DriverEntry中做如下操作:
- 设置Unload例程
- 设置驱动支持的调度例程
- 创建一个设备对象
- 创建一个指向设备对象的符号链接
所有驱动必须支持IRP_MJ_CREATE
和IRP_MJ_CLOSE
,不然无法打开一个驱动的设备的句柄,通常这两个调度例程是相同的
调度例程的函数原型:NTSTATUS Function(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp)
4.2.1 将信息传给驱动
用户模式客户端可用的三个基础函数:WriteFile
、ReadFile
、DeviceIoControl
4.2.2 客户端/驱动程序通信协议
必须使用CTL_CODE
宏来构建控制代码
1 2 3 | ((DeviceType) << 16 ) | ((Access) << 14 ) | ((Function) << 2 ) | (Method) \
)
|
- DeviceType:设备类型标识,
FILE_DEVICE_xxx
,第三方应以0x8000开头
- Function:指示特定操作的升序数字,第三方应该以0x800开头
- Method:指示客户端提供的输入和输出缓冲区如何传递给驱动程序(将在第6章详细讨论)
- Access:指示对驱动来说这个操作是什么?
示例:
1 2 3 | MY_DEVICE, 0x800 , METHOD_NEITHER, FILE_ANY_ACCESS)
|
4.2.3 创建一个设备对象
创建设备名:
在创建一个设备对象前,需要先创建一个UNICODE_STRING
存储内部设备名称
下面是两种初始化方式:
1 2 3 4 5 6 | / / plan A
UNICODE_STRING devName = RTL_CONSTANT_STRING(L "\\Device\\YourName" );
/ / plan B
UNICODE_STRING devName;
RtlInitUnicodeString(&devName, L "\\Device\\YourName" );
|
设备名称需要在设备对象管理器目录下
(RtlInitUnicodeString函数内部字符串的长度,RTL_CONSTANT_STRING宏在编译时计算长度)
创建设备对象:
创建设备对象需要调用IoCreateDevice
1 2 3 4 5 6 7 8 | NTSTATUS IoCreateDevice(
_In_ PDRIVER_OBJECT DriverObject,
_In_ ULONG DeviceExtensionSize,
_In_opt_ PUNICODE_STRING DeviceName,
_In_ DEVICE_TYPE DeviceType,
_In_ ULONG DeviceCharacteristics,
_In_ BOOLEAN Exclusive,
_Outptr_ PDEVICE_OBJECT * DeviceObject);
|
创建设备完整示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | UNICODE_STRING devName = RTL_CONSTANT_STRING(L "\\DEVICE\\devName" );
PDEVICE_OBJECT devObj;
status = IoCreateDevice(
DriverObject, / / our driver object
0 , / / no need for extra bytes
&devName, / / the device name
FILE_DEVICE_UNKNOWN, / / device type
0 , / / characteristics flags
FALSE, / / not exclusive
&devObj / / the resulting pointer
);
if (status < 0 ) {
KdPrint(( "[] Failed to create device object (0x%08X)\n" , status));
return status;
}
|
创建符号链接:
需要创建一个指向设备的符号链接,供r3调用
同样需要先创建一个字符串作为符号链接对象名称
1 2 3 4 5 6 | UNICODE_STRING symLink = RTL_CONSTANT_STRING(L "\\??\\symLinkName" );
status = IoCreateSymbolicLink(&symLink, &devName);
if (status < 0 ) {
KdPrint(( "[] Failed to create symbolic link (0x%08X)\n" , status));
return status;
}
|
注意:资源释放
上面创建的字符串会自动释放(好像在函数的栈中)?但对象不会,需要(在unload例程中)手动删除
1 2 3 4 5 6 7 | void Unload(_In_ PDRIVER_OBJECT DriverObject) {
/ / delete symbolic link
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L "\\??\\symLinkName" );
IoDeleteSymbolicLink(&symLink);
/ / delete device object
IoDeleteDevice(DriverObject - >DeviceObject);
}
|
4.3 客户端代码
将用CTL_CODE
构造的控制代码放到一个头文件中,供驱动代码和用户模式客户端代码同时使用
通过符号链接获驱动的设备的句柄
1 2 3 4 5 6 7 8 9 10 11 | {
HANDLE hDevice = CreateFile(L "\\\\.\\symLinkName" , GENERIC_WRITE,
FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0 , nullptr);
if (hDevice = = INVALID_HANDLE_VALUE)
return Error( "Failed to open device" );
}
int Error(const char * msg) {
printf( "%s (error=%d)\n" , msg, GetLastError());
return 1 ;
}
|
4.4 Create和Close调度例程
该例程什么都不用做,直接返回成功即可
1 2 3 4 5 6 7 8 | NTSTATUS PriorityBoosterCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
Irp - >IoStatus.Status = STATUS_SUCCESS;
Irp - >IoStatus.Information = 0 ;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
|
IRP是半文档化结构,通常来自运行中的管理器:I/O Manager, Plug & Play Manager or Power Manager
对驱动程序的每个请求总是包装在 IRP 中
IRP中有一个或多个IO_STACK_LOCATION
结构
为了完成IRP,需要调用IoCompleteRequest
,这个函数做很多东西,基本上理解为将IRP传播回创建者(通常是I/O管理器),然后由管理器通知客户端操作完成
4.5 DeviceIoControl调度例程
调用IoGetCurrentIrpStackLocation
获取当前设备对应的IO_STACK_LOCATION
IO_STACK_LOCATION
中有控制代码、输入输出buffer指针等
调度例程运行在调用该例程的用户模式进程的上下文中
1 2 3 | DWORD threadId;
PETHREAD Thread;
status = PsLookupThreadByThreadId(ULongToHandle(threadId), &Thread);
|
使用ULongToHandle
(这实际上只是个casting)将pid转换成HANDLE
线程和进程存在一个全局私有内核句柄表,句柄的“值”实际上就是ID
(HANDLE在64位系统是64位,线程ID始终是32位)
4.6 安装和测试
1 2 3 4 | sc create S3_PriorityBooster type = kernel binPath = "\\vmware-host\Shared Folders\MyDriver\x64\Debug\S3_PriorityBooster.sys"
sc start S3_PriorityBooster
sc stop S3_PriorityBooster
sc delete S3_PriorityBooster
|
start后可以在WinObj中的Driver
目录下看到驱动、GLOBAL??
目录下看到符号链接
可以在Process Explorer中查看进程的pid以及其线程的动态优先级
5 调试
关于使用WinDbg进行调试
5.1 windows的调试工具
四个调试器:
- Cdb 和 Ntsd 是用户模式调试器,可以附加到进程上,是命令行界面,没有什么大的区别
- Kd 是内核调试器,提供命令行界面,可以附加到本地内核或其他机器
- WinDbg 是有图形化界面的调试器,可以调试用户和内核模式
WinDbg Preview是WinDbg的“最新版”,解决了一些WinDbg上的bug
这些调试器都是基于DbgEng.Dll
5.2 WinDbg简介
虽然有GUI,实际上还是命令行,所有UI操作都会转成命令,显示在命令行窗口上
WinDbg支持三种类型的命令:
- 标准命令(Intrinsic):内置在调试器中,在被调试的目标上运行
- 元命令(Meta):以
.
开头,作用于调试器(debugging process)本身,而不是直接作用于被调试目标
- 拓展命令:以
!
开头,提供调试器大部分功能,都在拓展DLL中实现
教程:用户模式调试基础
符号信息:
设置符号的方法1:.symfix
设置符号的方法2:设置环境变量
_NT_SYMBOL_PATH
=SRV*c:\Symbols*http://msdl.microsoft.com/download/symbols
lm
:显示进程加载的模块,以及各模块是否加载了符号
.reload /f modulename.dll
:强制加载模块的符号
!sym noisy
:记录符号加载尝试的详细信息
线程:
~
:显示调试进程中所有线程的信息
线程信息前的.
表示当前线程,#
表示触发中断的线程
输入提示冒号右边的数字是当前线程的索引
1 2 | 0 Id : 874c . 18068 Suspend: 1 Teb: 00000001 ` 2229d000 Unfrozen
[下标] Id : [PID].[TID] Suspend: [挂起计数] Teb: [TEB地址] [是否冻结]
|
~ns
:切换到索引为n的线程
可以组合命令~nk
,这样可以在不切换线程的情况下,在别的线程执行操作(这里是显示别的线程的调用堆栈)
k
:当前线程的调用堆栈(stack trace)
!teb
:查看TEB的部分信息,默认当前线程的
进制转换:
16转10:
1 2 | 0 : 000 > ? 874c
Evaluate expression: 34636 = 0000874c
|
10转16:
1 2 | 0 : 000 > ? 0n34636
Evaluate expression: 34636 = 0000874c
|
数据或结构的显示:
dt [type]
:显示数据结构的定义(如显示_TEB:dt ntdll!_teb
)
dt [type] [addr]
:显示数据结构的数据(如显示某个_TEB:dt ntdll!_teb 000000`012229d000
)
r [reg]
:读取寄存器(如读取rcx:r rcx
)
d{a|b|c|d|D|f|p|q|u|w|W}
:以指定类型显示指定地址的数据
a:ascii字符
b,w,d,q:字节
u:unicode
f:单精浮点
D:双精浮点
u
:显示反汇编,默认8句汇编指令
!error [error_code]
:显示错误信息
断点和运行:
bp [symbol]
:设置断点(如CreateFile:bp kernel32!createfilew
)
bl
:显示当前设置的断点
bd
:禁用断点,禁用所有断点:bd *
bc
:删除断点
g
(F5):运行直到断点
p
(F10):步过
t
(F11):步进
5.3 内核调试(本地)
本地内核调试
修改启动项:bcdedit /debug on
本地内核调试教程
!process 0 0
:显示所有进程的基本信息
1 2 3 4 5 6 7 8 | lkd> !process 0 0
* * * * NT ACTIVE PROCESS DUMP * * * *
PROCESS ffff8d0e682a73c0
SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 001ad002 ObjectTable: ffffe20712204b80 HandleCount: 9542.
Image: System
(truncated)
|
- PROCESS旁边的地址:EPROCESS的地址
- SessionId:进程所处的对话
- Cid:pid
- Peb:PEB地址(在用户模式地址空间)
- ParentCid:父进程pid
- DirBase:进程主页目录的物理地址(x32是PDPT基址、x64是PML4基址)
- ObjectTable:指向进程的私有句柄表的指针
- HandleCount:进程中的句柄数
- Image:可执行文件名称,或与可执行文件无关的特殊进程名称
!process
指令后第一个数字是筛选特定进程,0表示所有进程;第二个数字是细节掩码,0表示最少细节;第三个参数是筛选可执行文件
.process /p [EPROCESS]
:切换到指定进程
peb在用户模式地址空间中,查看peb需要先设置正确的用户模式进程环境
不切换的做法:.process /p ffff8d0e849df080; !peb e8a8c9c000
调用堆栈中,nt前缀表示内核
.reload /user
:加载用户模式符号
其余常用/有趣的内核模式调试指令:
!pcr
:显示指定为附加索引的处理器的进程控制区域 (PCR)(如果未指定索引,则默认显示处理器 0)
!vm
:显示系统和进程的内存统计信息
!running
:显示有关在系统上所有处理器上运行的线程的信息
5.4 完全内核调试(双机)
完全内核调试需要”双机“
最好的连接方式是通过网络,这需要主机和被调试目标系统版最少为Win8
另外一种方法是COM串口,大多数虚拟机支持虚拟串口而不需要真实(物理的)串口线
详细配置方式略过
配置目标机器
1 2 | bcdedit / debug on
bcdedit / dbgsettings serial debugport: 1 baudrate: 115200
|
配置主机
调试器需要设置调试端口映射和命名管道,与虚拟机上的相同
输入提示kd左边的数字是引起中断的处理器的索引
5.5 内核驱动调试教程
可以设置未来断点(在运行程序前设置断点)
如设置驱动prioritybooster的入口点:bu prioritybooster!driverentry
可以设置只在指定进程上中断:bp /p [EPROCESS] [symbol]
如:bp /p ffffdd06042e4080 prioritybooster!priorityboosterdevicecontrol
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法