描述一些Windows内部工作中最重要、最基本的概念,部分概念将在后面的章节做更详细的研究
进程不运行(Processes dont't run - processes manage),线程才执行代码
进程拥有以下内容:
每个进程拥有自己的虚拟、私有、线性地址空间
(该地址空间初始时几乎为空,然后pe、ntdll.dll开始被影射,接着是其他子系统dll)
32位进程默认地址空间2GB,设置pe中的LARGEADDRESSAWARE
标志可以增加到3GB(32位系统)或4GB(64位系统)
64位进程默认地址空间128TB(win8之前是8TB)
虚拟内存被映射到物理内存(RAM)或临时驻留在文件中(如page file)
如果不在物理内存,则触发page fault异常,并或取数据到物理内存中
页(page)是内存管理的单位,默认大小为4KB
虚拟内存中的页处于三种状态之一
系统空间与进程无关
系统空间就是内核
实际执行代码的是线程
线程拥有的最重要的内容:
线程最常处于的状态:
括号中的数字是状态号:
Running(2) => Waiting(5) => Deferred Ready(7), Ready(1) => Running(2)
线程至少有一个位于内核空间的栈(32位系统12KB,64位系统24KB)
用户态的线程还有一个位于所属进程空间的栈(默认上限1MB)
线程Running
或Ready
时,内核栈驻留在RAM
栈初始时会尽可能少提交页(最少一页),剩下的页设置为Reserved
,而最后一个Committed
的页的下一页设置为PAGE_GUARD
原标题: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)的下标
对象被引用计数,当计数为0时才会被释放
句柄是进程的对象表的索引
注意:返回值为句柄的函数,大多数失败时返回0
。有些返回INVALID_HANDLE_VALUE (-1)
,比如CreateFile
句柄值是4的倍数,0不是有效句柄值
某些类型的对象可以有名称,可用于通过合适的 Open 函数按名称打开对象。
用户模式调用 Create 函数按名称创建对象,如果存在,则仅打开现有对象。
winObj中显示的名称有时不是对象的真实名称:
Process Explorer 的句柄视图中的访问列显示用于打开或创建句柄的访问掩码
Process Explorer中显示的引用数(References)不是实际引用数(outstanding references)
[windbg]中用!trueref
获取实际引用数(actual reference)
本章主要是关于准备内核开发所需的环境,包括开发和调试的工具以及环境配置
以及启动和运行内核驱动的知识
然后写一个可以加载和卸载的驱动
首先按 2.1安装工具 完成安装,然后为驱动开发配置虚拟机(未包括内核调试的配置)
安装无签名驱动
如果驱动没有签名,安装驱动需要以该模式启动系统
显示内核调试信息
在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
需要vs2019、windows 10 sdk(vs2019中安装)、windows 10 driver kit(WDK)
以及 Sysinternals,该工具包含debug view、process monitor等一系列有用的工具
在实际编译中发现,新版本的vs驱动项目默认开启缓解Spectre 漏洞
可以在c/c++、代码生成中关闭该项,或在vs installer中安装对应工具
vs2019中选择创建一个Empty WDM Driver
,创建完成后有个inf
后缀的文件,暂时不需要,删除掉
DriverEntry 是驱动的默认入口点
系统线程以IRQL_PASSIVE_LEVEL
(0)调用 DriverEntry
DriverEntry函数原型:
一个简单的驱动(sample.cpp):
安装驱动和安装用户态服务相似,需要调用Create Service API或使用工具
sc.exe(系统自带)是著名工具之一
安装驱动需要管理员权限
创建服务项:
随后就能在注册表(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
卸载驱动(停止服务):
KdPrint 宏
是DbgPrint API
的包装
通过在每个函数开头加入KdPrint(("Debug messgae"));
可以观察函数调用的发生
使用DebugView,选择capture Kernel可以看到内核调试信息
创建一个驱动用于显示系统版本信息,使用RtlGetVersion
code:
shell:
研究一些内核的API、结构和定义,以及一些驱动程序中的机制
用户模式和内核模式调试的重要区别
未处理异常会导致蓝屏,原因是防止继续执行代码、对系统造成不可逆转的伤害
内核代码不应该跳过任何细节或错误检查
如果驱动程序卸载时仍保留分配的内存或打开的内核句柄,这些资源不会自动释放,只会在下次系统启动时释放
原因是驱动程序可以分配一些缓冲区,然后将其传递给另一个与之合作的驱动程序
忽略内核API的返回值很危险,应该总是检查返回值
中断请求级(Interrupt Request Level, IRQL)通常为0
用户模式下始终为0,内核模式下大部分时间为0
没有C++ runtime
一些不支持的C++特性:
一些支持的C++特性:
内核调试需要双机调试,一台作为调试者、另一台作为被调试者运行驱动程序
内核术语是 Checked(Debug)和 Free(Release)
Debug意味着可以使用DBG符号
内核API常用前缀的意义:
Nt前缀的内核函数对应NtDll.Dll的函数,会根据 KTHREAD 结构的标记(调用者是否来自内核)对参数进行检查
Zw前缀的内核函数先将调用者模式设为KernelMode(0)
,然后调用Nt前缀的内核函数
可以在ntstatus.h
中找到NTSTATUS
值的定义
大多数代码并不关心错误具体是什么,仅测试最高位即可,可以使用NT_SUCCESS
宏
当返回到用户层时,会由STATUS_xxx
转成ERROR_yyy
,用户模式通过GetLastError可以得到这些错误
通常遇到错误时,会返回相同的 NTSTATUS 到调用函数
内核使用UNICODE_STRING
Length
是字符串的字节数(不包括\x00\x00结束符)
MaximumLength
是不需要重新分配内存的情况下、字符串字节数上限
需要注意的是,UNICODE_STRING并不总是有\x00\x00结尾
一些常用的字符串操作函数:
内核提供两种通用内存池(general memory pools)给驱动使用:
枚举类型POOL_TYPE
表示池类型,只有三种是可以用于驱动的:
PagedPool
、NonPagedPool
、NonPagedPoolNx
(non-page pool没有可执行权限)
常用内存池函数:
tag是4字节的值
可以在PoolMon(WDK的Windows Kits中)中观察到有tag的内存池(tag以大端序字符串显示)
给ustring分配页池内存:
code:
shell:
内核使用循环双向链表:
CONTAINING_RECORD
宏执行适当的偏移计算并转换为实际数据类型
CONTAINING_RECORD(pvoid, type, entry_member_name)
常用链表函数(时间复杂度都是常数):
后三个关于自旋锁,在第6章详细讨论
常用major function代码:
MajorFunction
数组由内核初始化指向内核内部例程IopInvalidDeviceRequest
,该例程直接返回失败,表示不支持该操作
驱动通过设备与r3代码通信,驱动应该至少创建一个设备对象并为其命名
CreateFile可以打开设备,第一个参数为设备对象名称
打开文件或设备的句柄会创建内核结构 FILE_OBJECT 的实例,这是个半文档化的结构。
更准确的说,CreteFile接受一个symbolic link
(符号链接)
对象管理器中名为??
的目录下的符号链接都可被用户模式代码通过CreateFile或Createfile2调用
可以通过WinObj查看(WinObj中目录名为Global??
)
使用符号链接的CreateFile的文件名(第一个参数),必须加上前缀\\.\
(c++中是"\\\\.\\"
)
如果创建了多个设备对象,将形成一个单向链表,添加设备时是头插法,所以第一个创建的设备在链表的最后
将完成一个完整的驱动及客户端程序,利用驱动完成只能在内核模式下完成的功能(设置任意级别的线程优先级)
线程优先级 = 进程优先级 + 相对线程优先级
用户模式下,设置进程优先级可以用SetPriorityClass
,共有6个级别
设置相对线程优先级可以用SetThreadPriority
,共有7个级别
下面是线程优先级合法值的表(通过windows api设置),据别的书说是个未文档化的东西,windows不建议开发时考虑线程优先级,该表的值随windows版本变化可能发生改变
进程优先级枚举:级别+_PRIORITY_CLASS
线程优先级枚举:THREAD_PRIORITY_
+级别
大多数驱动需要在DriverEntry中做如下操作:
所有驱动必须支持IRP_MJ_CREATE
和IRP_MJ_CLOSE
,不然无法打开一个驱动的设备的句柄,通常这两个调度例程是相同的
调度例程的函数原型:NTSTATUS Function(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp)
用户模式客户端可用的三个基础函数:WriteFile
、ReadFile
、DeviceIoControl
必须使用CTL_CODE
宏来构建控制代码
示例:
创建设备名:
在创建一个设备对象前,需要先创建一个UNICODE_STRING
存储内部设备名称
下面是两种初始化方式:
设备名称需要在设备对象管理器目录下
(RtlInitUnicodeString函数内部字符串的长度,RTL_CONSTANT_STRING宏在编译时计算长度)
创建设备对象:
创建设备对象需要调用IoCreateDevice
创建设备完整示例:
创建符号链接:
需要创建一个指向设备的符号链接,供r3调用
同样需要先创建一个字符串作为符号链接对象名称
注意:资源释放
上面创建的字符串会自动释放(好像在函数的栈中)?但对象不会,需要(在unload例程中)手动删除
将用CTL_CODE
构造的控制代码放到一个头文件中,供驱动代码和用户模式客户端代码同时使用
通过符号链接获驱动的设备的句柄
该例程什么都不用做,直接返回成功即可
IRP是半文档化结构,通常来自运行中的管理器:I/O Manager, Plug & Play Manager or Power Manager
对驱动程序的每个请求总是包装在 IRP 中
IRP中有一个或多个IO_STACK_LOCATION
结构
为了完成IRP,需要调用IoCompleteRequest
,这个函数做很多东西,基本上理解为将IRP传播回创建者(通常是I/O管理器),然后由管理器通知客户端操作完成
调用IoGetCurrentIrpStackLocation
获取当前设备对应的IO_STACK_LOCATION
IO_STACK_LOCATION
中有控制代码、输入输出buffer指针等
调度例程运行在调用该例程的用户模式进程的上下文中
使用ULongToHandle
(这实际上只是个casting)将pid转换成HANDLE
线程和进程存在一个全局私有内核句柄表,句柄的“值”实际上就是ID
(HANDLE在64位系统是64位,线程ID始终是32位)
start后可以在WinObj中的Driver
目录下看到驱动、GLOBAL??
目录下看到符号链接
可以在Process Explorer中查看进程的pid以及其线程的动态优先级
关于使用WinDbg进行调试
四个调试器:
WinDbg Preview是WinDbg的“最新版”,解决了一些WinDbg上的bug
这些调试器都是基于DbgEng.Dll
虽然有GUI,实际上还是命令行,所有UI操作都会转成命令,显示在命令行窗口上
WinDbg支持三种类型的命令:
符号信息:
设置符号的方法1:.symfix
设置符号的方法2:设置环境变量
_NT_SYMBOL_PATH
=SRV*c:\Symbols*http://msdl.microsoft.com/download/symbols
lm
:显示进程加载的模块,以及各模块是否加载了符号
.reload /f modulename.dll
:强制加载模块的符号
!sym noisy
:记录符号加载尝试的详细信息
线程:
~
:显示调试进程中所有线程的信息
线程信息前的.
表示当前线程,#
表示触发中断的线程
输入提示冒号右边的数字是当前线程的索引
~ns
:切换到索引为n的线程
可以组合命令~nk
,这样可以在不切换线程的情况下,在别的线程执行操作(这里是显示别的线程的调用堆栈)
k
:当前线程的调用堆栈(stack trace)
!teb
:查看TEB的部分信息,默认当前线程的
进制转换:
16转10:
10转16:
数据或结构的显示:
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):步进
修改启动项:bcdedit /debug on
!process 0 0
:显示所有进程的基本信息
!process
指令后第一个数字是筛选特定进程,0表示所有进程;第二个数字是细节掩码,0表示最少细节;第三个参数是筛选可执行文件
.process /p [EPROCESS]
:切换到指定进程
peb在用户模式地址空间中,查看peb需要先设置正确的用户模式进程环境
不切换的做法:.process /p ffff8d0e849df080; !peb e8a8c9c000
调用堆栈中,nt前缀表示内核
.reload /user
:加载用户模式符号
其余常用/有趣的内核模式调试指令:
完全内核调试需要”双机“
最好的连接方式是通过网络,这需要主机和被调试目标系统版最少为Win8
另外一种方法是COM串口,大多数虚拟机支持虚拟串口而不需要真实(物理的)串口线
详细配置方式略过
调试器需要设置调试端口映射和命名管道,与虚拟机上的相同
输入提示kd左边的数字是引起中断的处理器的索引
可以设置未来断点(在运行程序前设置断点)
如设置驱动prioritybooster的入口点:bu prioritybooster!driverentry
可以设置只在指定进程上中断:bp /p [EPROCESS] [symbol]
如:bp /p ffffdd06042e4080 prioritybooster!priorityboosterdevicecontrol
bcdedit
/
set
testsigning on
bcdedit
/
set
testsigning on
extern
"C"
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath);
extern
"C"
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath);
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;
}
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;
}
sc create sample
type
=
kernel binPath
=
"\\vmware-host\Shared Folders\MyDriver\x64\Debug\sample.sys"
sc create sample
type
=
kernel binPath
=
"\\vmware-host\Shared Folders\MyDriver\x64\Debug\sample.sys"
sc start sample
sc stop sample
/
/
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);
/
/
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);
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
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
|
用户模式 |
内核模式 |
未处理异常 |
进程崩溃 |
系统崩溃 |
终止 |
当进程终止,所有内存和资源都会被自动释放 |
当驱动卸载,如果没有手动释放,会造成泄露直到重启 |
返回值 |
API错误有时候会忽略 |
应该不忽略任何错误 |
IRQL |
总是 PASSIVE_LEVEL (0) |
可能为更高 |
错误代码 |
通常只会影响本进程 |
影响整个系统 |
测试和调试 |
通常在开发机器上调试 |
需要双机调试 |
库(Lib) |
可以使用C/C++库(如STL、boost) |
大多数标准库无法使用 |
异常处理 |
可以使用C++异常或SEH |
只能使用SEH |
C++支持 |
完全的C++支持 |
不支持C++ runtime |
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWCH
Buffer
;
} UNICODE_STRING;
typedef UNICODE_STRING
*
PUNICODE_STRING;
typedef const UNICODE_STRING
*
PCUNICODE_STRING;
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWCH
Buffer
;
} UNICODE_STRING;
typedef UNICODE_STRING
*
PUNICODE_STRING;
typedef const UNICODE_STRING
*
PCUNICODE_STRING;
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;
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;
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
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
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY
*
Flink;
struct _LIST_ENTRY
*
Blink;
} LIST_ENTRY,
*
PLIST_ENTRY;
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY
*
Flink;
struct _LIST_ENTRY
*
Blink;
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)