首页
社区
课程
招聘
[原创]Windows Kernel Programming 笔记 1~5 内核开发入门
2022-1-18 17:24 31720

[原创]Windows Kernel Programming 笔记 1~5 内核开发入门

2022-1-18 17:24
31720

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)

 

线程RunningReady时,内核栈驻留在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
#include <ntddk.h>
 
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\

 

加载驱动(启动服务):

1
sc start sample

在process explorer中,选择System进程,查看dll窗口,拉到最下面就能看到sample.sys

 

卸载驱动(停止服务):

1
sc stop sample

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++特性:

  • 不支持newdelete,这正常是在用户模式堆分配的
  • 不会调用具有非默认构造函数的全局变量
    • 避免在构造函数中使用代码,创建一些要显式调用的Init函数
    • 仅将指针分配为全局变量,动态创建实例
  • 不支持C++异常处理(trycatchthrow
  • 不可使用标准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表示池类型,只有三种是可以用于驱动的:
PagedPoolNonPagedPoolNonPagedPoolNx
(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_CREATEIRP_MJ_CLOSE,不然无法打开一个驱动的设备的句柄,通常这两个调度例程是相同的

 

调度例程的函数原型:NTSTATUS Function(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp)

4.2.1 将信息传给驱动

用户模式客户端可用的三个基础函数:WriteFileReadFileDeviceIoControl

4.2.2 客户端/驱动程序通信协议

必须使用CTL_CODE宏来构建控制代码

1
2
3
#define CTL_CODE( DeviceType, Function, Method, Access ) (                 \
    ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)
  • DeviceType:设备类型标识,FILE_DEVICE_xxx,第三方应以0x8000开头
  • Function:指示特定操作的升序数字,第三方应该以0x800开头
  • Method:指示客户端提供的输入和输出缓冲区如何传递给驱动程序(将在第6章详细讨论)
  • Access:指示对驱动来说这个操作是什么?

示例:

1
2
3
#define MY_DEVICE 0x800
#define IOCTL_MY_OP CTL_CODE(\
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


[培训]《安卓高级研修班(网课)》月薪三万计划

收藏
点赞4
打赏
分享
最新回复 (10)
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
棒冰2022 2022-1-19 11:13
2
0
不错,学习了
雪    币: 160
活跃值: (19)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wx_小叶_726 2022-1-26 16:17
3
0
大佬,我们这里安全公司缺人,要不要考虑一下?
雪    币: 120
活跃值: (29)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mir3777 2022-1-26 18:15
4
0
终于看到另一个也读这本书的人了
雪    币: 1
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Code-X 2022-2-2 01:09
5
1
国内Windows已经世风日下,国企在搞国产化,微软在往云上走。感觉Windows慢慢也没搞头了。
雪    币: 5916
活跃值: (661)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
鸿渐之翼 2022-2-2 09:38
6
0
师傅我想问下学习Windows内核需要看哪些书籍
雪    币: 5916
活跃值: (661)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
鸿渐之翼 2022-2-2 09:38
7
0
Code-X 国内Windows已经世风日下,国企在搞国产化,微软在往云上走。感觉Windows慢慢也没搞头了。
哈哈哈
雪    币: 2662
活跃值: (3495)
能力值: ( LV5,RANK:63 )
在线值:
发帖
回帖
粉丝
wx_御史神风 2022-2-12 14:53
8
0
鸿渐之翼 师傅我想问下学习Windows内核需要看哪些书籍
我也是学内核没多久,Windows Kernel Programming这本书作为入门挺不错的,再深入点我也不是很了解了,打好基础就去多关注一些论坛上的新文章吧
雪    币: 144
活跃值: (1223)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
林动11 2022-2-16 04:53
9
0
wx_御史神风 我也是学内核没多久,Windows Kernel Programming这本书作为入门挺不错的,再深入点我也不是很了解了,打好基础就去多关注一些论坛上的新文章吧
谭文的<windows内核编程> 也ok.我也在读这个
雪    币: 33
活跃值: (244)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
fishleong 2022-2-16 09:20
10
1
这本书3年前就看过,里面课后题项目都做了.入门很好,就是很多东西没讲, 网络这块的就一点没有,现在win内核开发的岗位非常少,招人的要求都很高,光这本估计还不够入职门槛.这书现在有第2版,会有网络相关的内容,可以关注一下
雪    币: 1641
活跃值: (3601)
能力值: (RANK:15 )
在线值:
发帖
回帖
粉丝
はつゆき 2022-2-16 09:30
11
0
fishleong 这本书3年前就看过,里面课后题项目都做了.入门很好,就是很多东西没讲, 网络这块的就一点没有,现在win内核开发的岗位非常少,招人的要求都很高,光这本估计还不够入职门槛.这书现在有第2版,会有网络相关 ...
岗位并不少,要求也并不高,只是很多人觉得参加个培训,看本书,就可以拿高于其他计算机方向的薪资。
他们不屑于去工资低的地方,他们觉得他们选择windows内核开发,就理所应该拿钱多。
游客
登录 | 注册 方可回帖
返回