Reactos 第 5 章 进程与线程 — 5.3 系统调用 NtCreateProcess()

第 5 章 进程与线程 --- 5.3 系统调用 NtCreateProcess()

5.3.0 框架图

复制代码
┌──────────────────────────────────────────────────────────────────────────────────┐
│                        NtCreateProcess 完整调用链                              │
│                                                                                  │
│   用户态                                                                          │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │  kernel32!CreateProcessW                                              │       │
│   │     │                                                               │       │
│   │     ├─► CreateFile (打开可执行文件)                                   │       │
│   │     ├─► NtCreateSection (创建 Section 对象)                          │       │
│   │     ├─► NtCreateProcess (创建进程)                                   │       │
│   │     ├─► NtCreateThread (创建初始线程)                                │       │
│   │     ├─► NtResumeThread (启动线程)                                   │       │
│   │     └─► 通知 CSRSS/Win32k                                           │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                    │                                           │
│                                    ▼ syscall                                    │
│   内核态                                                                          │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │  ntdll!NtCreateProcess                                               │       │
│   │     │                                                               │       │
│   │     ▼                                                               │       │
│   │  ntoskrnl!NtCreateProcess                                            │       │
│   │     │                                                               │       │
│   │     └─► NtCreateProcessEx (扩展版本)                                 │       │
│   │           │                                                         │       │
│   │           └─► PspCreateProcess (核心创建逻辑)                        │       │
│   │                 │                                                   │       │
│   │                 ├─► 步骤 1: 参数校验                                  │       │
│   │                 ├─► 步骤 2: 解析父进程句柄                            │       │
│   │                 ├─► 步骤 3: 解析 SectionHandle                        │       │
│   │                 ├─► 步骤 4: 解析 DebugPort/ExceptionPort             │       │
│   │                 ├─► 步骤 5: 创建 EPROCESS 对象                        │       │
│   │                 ├─► 步骤 6: 初始化 EPROCESS 字段                      │       │
│   │                 ├─► 步骤 7: 创建地址空间                              │       │
│   │                 ├─► 步骤 8: 初始化 KPROCESS                           │       │
│   │                 ├─► 步骤 9: 复制安全令牌                              │       │
│   │                 ├─► 步骤 10: 初始化对象管理器                         │       │
│   │                 ├─► 步骤 11: 初始化用户态地址空间                      │       │
│   │                 ├─► 步骤 12: 分配 PID                                 │       │
│   │                 ├─► 步骤 13: 插入 PsActiveProcessHead                 │       │
│   │                 ├─► 步骤 14: 计算 Quantum/Priority                    │       │
│   │                 ├─► 步骤 15: 创建 PEB                                 │       │
│   │                 ├─► 步骤 16: 插入对象目录                             │       │
│   │                 ├─► 步骤 17: 访问检查                                 │       │
│   │                 └─► 步骤 18: 返回进程句柄                             │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
└──────────────────────────────────────────────────────────────────────────────────┘

5.3.0.1 设计意图

核心问题

如何在内核态创建一个完整的进程对象?需要经过哪些步骤?每个步骤的目的是什么?

设计哲学 :「先建壳,再建芯;先建芯,再激活

想象一下,创建一个进程就像建造一座新房子

  • 建壳(创建 EPROCESS 对象框架):先搭建房子的框架结构------地基、墙壁、屋顶。这就像创建 EPROCESS 对象,分配内存空间,建立基本的数据结构。

  • 建芯(填充 EPROCESS 的各个字段):然后安装房子的核心设施------水电管道、门窗、电气系统。这就像初始化 EPROCESS 的各个字段:地址空间(水电)、安全令牌(门禁)、句柄表(窗户)、PEB(装修图纸)。

  • 激活(插入全局链表和对象目录):最后办理入住手续------登记房产、发放钥匙、通知邻居。这就像将进程插入全局链表(PsActiveProcessHead)、分配 PID(房产证号)、创建句柄(钥匙)、通知子系统(邻居)。

本节定位

本节深入分析 NtCreateProcess 系统调用的完整实现,特别是核心函数 PspCreateProcess。读完本节后,读者应当能够:

  • 理解进程创建的完整流程(18 个步骤)
  • 掌握每个步骤的目的和实现细节
  • 理解错误处理和回滚机制(事务性创建)
  • 理解与其他子系统的交互(内存管理器、对象管理器、安全子系统)

5.3.1 NtCreateProcess 函数签名

NtCreateProcess:进程创建的"总指挥"

NtCreateProcess 是创建进程的核心系统调用。如果把创建进程比作申请开办一家新公司 ,那么 NtCreateProcess 就是向政府部门提交的公司注册申请表

c 复制代码
NTSTATUS NTAPI NtCreateProcess(
    OUT PHANDLE ProcessHandle,
    IN ACCESS_MASK DesiredAccess,
    IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
    IN HANDLE ParentProcess,
    IN BOOLEAN InheritObjectTable,
    IN HANDLE SectionHandle OPTIONAL,
    IN HANDLE DebugPort OPTIONAL,
    IN HANDLE ExceptionPort OPTIONAL);

参数详解

参数 类型 说明 比喻
ProcessHandle OUT PHANDLE 返回创建的进程句柄 公司营业执照(拿到后可以运营公司)
DesiredAccess IN ACCESS_MASK 请求的访问权限 申请的权限范围(能做什么)
ObjectAttributes IN POBJECT_ATTRIBUTES 对象属性(名称等) 公司名称、地址等信息
ParentProcess IN HANDLE 父进程句柄 母公司(子公司继承母公司属性)
InheritObjectTable IN BOOLEAN 是否继承父进程句柄表 是否继承母公司的业务关系
SectionHandle IN HANDLE 可执行文件的 Section 句柄 公司章程(定义公司运营规则)
DebugPort IN HANDLE 调试端口(可选) 监管部门(监督公司运营)
ExceptionPort IN HANDLE 异常端口(可选) 紧急联络渠道(处理突发事件)

返回值

返回值 说明 比喻
STATUS_SUCCESS 创建成功 申请通过,公司成立
STATUS_INVALID_HANDLE 无效句柄 提供的母公司信息无效
STATUS_ACCESS_DENIED 访问被拒绝 权限不足,申请被驳回
STATUS_INSUFFICIENT_RESOURCES 资源不足 办公场地不足,无法注册

参数之间的关系

  • ParentProcess 与 InheritObjectTable:如果 InheritObjectTable 为 TRUE,新进程会继承父进程的可继承句柄。就像子公司可以继承母公司的部分业务关系。

  • SectionHandle 与进程类型:如果 SectionHandle 为 NULL,创建的是系统进程(没有可执行文件);如果非 NULL,创建的是用户进程(有可执行文件)。就像有章程的公司是正规公司,没有章程的是特殊机构。

  • DebugPort 与 ExceptionPort:这两个参数是可选的,用于调试和异常处理。就像公司可以选择是否接受监管部门监督。


5.3.2 NtCreateProcessEx(新接口)

NtCreateProcessEx:升级版的"公司注册系统"

随着 Windows 系统的发展,进程创建的需求越来越复杂。NtCreateProcessEx 就像是升级版的公司注册系统,提供了更多的选项和更清晰的参数结构。

c 复制代码
NTSTATUS NTAPI NtCreateProcessEx(
    OUT PHANDLE ProcessHandle,
    IN ACCESS_MASK DesiredAccess,
    IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
    IN HANDLE ParentProcess,
    IN BOOLEAN InheritObjectTable,
    IN HANDLE SectionHandle OPTIONAL,
    IN HANDLE DebugPort OPTIONAL,
    IN HANDLE ExceptionPort OPTIONAL,
    IN BOOLEAN InheritDebugPort,
    IN PVOID Reserved);

新增参数

参数 说明
InheritDebugPort 是否继承父进程的调试端口(新增)
Reserved 预留参数,供未来扩展使用

标志位(PROCESS_CREATE_FLAGS_*)

这些标志位就像是公司注册时的附加选项

标志 说明 比喻
PROCESS_CREATE_FLAGS_BREAKAWAY 脱离 Job 子公司脱离母公司集团
PROCESS_CREATE_FLAGS_NO_DEBUG_INHERIT 不继承调试端口 不接受母公司的监管
PROCESS_CREATE_FLAGS_INHERIT_HANDLES 继承句柄 继承母公司的业务关系
PROCESS_CREATE_FLAGS_LARGE_PAGES 使用大页面 申请大型办公场地
PROCESS_CREATE_FLAGS_PROTECTED_PROCESS 受保护进程 特殊保护的公司(如银行)

与 NtCreateProcess 的关系

NtCreateProcess 是 NtCreateProcessEx 的简化包装,就像快速注册通道完整注册流程的关系:

c 复制代码
NTSTATUS NTAPI NtCreateProcess(...) {
    return NtCreateProcessEx(ProcessHandle,
                            DesiredAccess,
                            ObjectAttributes,
                            ParentProcess,
                            InheritObjectTable,
                            SectionHandle,
                            DebugPort,
                            ExceptionPort,
                            FALSE,  // InheritDebugPort(默认不继承)
                            NULL);  // Reserved(预留)
}

这种设计保证了向后兼容性:旧代码可以继续使用 NtCreateProcess,新代码可以使用 NtCreateProcessEx 获得更多控制选项。


5.3.3 PspCreateProcess 深入分析

PspCreateProcess:进程创建的"施工队长"

PspCreateProcess 是进程创建的核心实现函数。如果把创建进程比作建造房子 ,那么 PspCreateProcess 就是负责具体施工的施工队长,他需要按照图纸一步步完成施工。

整个施工过程分为 18 个步骤,每个步骤都有明确的目的和依赖关系。如果任何一个步骤失败,都需要回滚之前已完成的工作,就像施工失败需要拆除已建的部分。

步骤详解

复制代码
┌──────────────────────────────────────────────────────────────────────────────────┐
│                        PspCreateProcess 执行流程                              │
│                                                                                  │
│   步骤 1: 参数校验                                                               │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ if (Flags & ~PROCESS_CREATE_FLAGS_LEGAL_MASK)                       │       │
│   │     return STATUS_INVALID_PARAMETER;                                │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【施工前检查】就像施工前检查图纸是否合规。如果参数中有非法标志位,       │
│   立即返回错误,不开始任何施工工作。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 2: 解析父进程句柄                                                         │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ Status = ObReferenceObjectByHandle(ParentProcess,                   │       │
│   │                                    PROCESS_CREATE_PROCESS,          │       │
│   │                                    PsProcessType,                   │       │
│   │                                    &Parent);                        │       │
│   │ if (!NT_SUCCESS(Status)) return Status;                             │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【确认母公司】就像确认母公司是否存在、是否有权设立子公司。       │
│   通过 ObReferenceObjectByHandle 获取父进程对象指针,       │
│   同时增加引用计数防止父进程被提前销毁。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 3: 解析 SectionHandle                                                     │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ if (SectionHandle != NULL) {                                        │       │
│   │     Status = ObReferenceObjectByHandle(SectionHandle,               │       │
│   │                                        SECTION_MAP_EXECUTE,        │       │
│   │                                        NULL,                        │       │
│   │                                        &SectionObject);             │       │
│   │ }                                                                  │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【获取公司章程】如果提供了 SectionHandle,获取可执行文件的       │
│   Section 对象。这个 Section 包含了程序的代码和数据,       │
│   就像公司章程定义了公司的运营规则。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 4: 解析 DebugPort/ExceptionPort                                          │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ if (DebugPort != NULL) {                                           │       │
│   │     Status = DbgkReferenceObjectByPointer(DebugPort,               │       │
│   │                                           &DebugObject);            │       │
│   │ }                                                                  │       │
│   │ if (ExceptionPort != NULL) {                                       │       │
│   │     Status = ObReferenceObjectByHandle(ExceptionPort,              │       │
│   │                                         PORT_ALL_ACCESS,           │       │
│   │                                         LpcPortObjectType,         │       │
│   │                                         &ExceptionPortObject);     │       │
│   │ }                                                                  │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【设置监管渠道】如果提供了调试端口或异常端口,       │
│   获取对应的对象引用。调试端口用于调试器监控进程,       │
│   异常端口用于接收进程的异常通知。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 5: 创建 EPROCESS 对象                                                     │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ Status = ObCreateObject(KernelMode,                                │       │
│   │                         PsProcessType,                             │       │
│   │                         ObjectAttributes,                          │       │
│   │                         KernelMode,                                │       │
│   │                         NULL,                                      │       │
│   │                         sizeof(EPROCESS),                          │       │
│   │                         0,                                         │       │
│   │                         0,                                         │       │
│   │                         &Process);                                 │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【搭建框架】调用 ObCreateObject 创建 EPROCESS 对象。       │
│   这就像搭建房子的框架结构,分配内存空间,       │
│   添加 OBJECT_HEADER(对象管理器的头部信息)。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 6: 初始化 EPROCESS 字段                                                   │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ RtlZeroMemory(Process, sizeof(EPROCESS));                          │       │
│   │ ExInitializeRundownProtection(&Process->RundownProtect);           │       │
│   │ InitializeListHead(&Process->ThreadListHead);                       │       │
│   │ Process->InheritedFromUniqueProcessId = Parent->UniqueProcessId;   │       │
│   │ // ... 其他字段初始化                                               │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【清空并初始化】将 EPROCESS 结构清零,       │
│   然后初始化关键字段:       │
│   - RundownProtect:防止进程被提前销毁的保护机制       │
│   - ThreadListHead:线程链表头(还没线程,先准备空链表)       │
│   - InheritedFromUniqueProcessId:记录父进程 PID       │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 7: 创建地址空间                                                           │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ if (SectionObject != NULL) {                                       │       │
│   │     Status = MmCreateProcessAddressSpace(Process);                  │       │
│   │ } else {                                                           │       │
│   │     Status = MmInitializeHandBuiltProcess(Process);                 │       │
│   │ }                                                                  │       │
│   └─────────────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【铺设地基】创建进程的虚拟地址空间。       │
│   - 有 Section:调用 MmCreateProcessAddressSpace 创建完整地址空间       │
│   - 无 Section:调用 MmInitializeHandBuiltProcess 创建空地址空间       │
│   这就像铺设房子的地基,后续所有建筑都建在这个地基上。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 8: 初始化 KPROCESS                                                        │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ KeInitializeProcess(&Process->Pcb,                                 │       │
│   │                     Parent->Pcb.Affinity,                          │       │
│   │                     Parent->Pcb.BasePriority);                     │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【安装调度系统】初始化 KPROCESS(EPROCESS 的第一个字段)。       │
│   设置:       │
│   - Affinity:CPU 亲和性(继承父进程)       │
│   - BasePriority:基础优先级(继承父进程)       │
│   - DirectoryTableBase:页目录基址(从地址空间获取)       │
│   这就像安装房子的电力系统,调度器会直接使用 KPROCESS。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 9: 复制安全令牌                                                           │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ Status = PspInitializeProcessSecurity(Process,                     │       │
│   │                                       Parent);                     │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【安装门禁系统】复制父进程的安全令牌到新进程。       │
│   令牌决定了进程能访问哪些系统资源,       │
│   就像门禁卡决定了员工能进入哪些区域。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 10: 初始化对象管理器                                                        │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ Status = ObInitProcess(Process,                                    │       │
│   │                        InheritObjectTable,                         │       │
│   │                        Parent->ObjectTable);                       │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【建立窗户系统】初始化进程的句柄表。       │
│   - 如果 InheritObjectTable 为 TRUE:继承父进程的可继承句柄       │
│   - 如果 FALSE:创建空的句柄表       │
│   句柄就像窗户,进程通过句柄看到和操作外部资源。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 11: 初始化用户态地址空间                                                     │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ Status = MmInitializeProcessAddressSpace(Process,                  │       │
│   │                                          SectionObject);           │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【装修内部空间】初始化用户态地址空间的具体内容。       │
│   映射可执行文件、ntdll.dll 等到地址空间,       │
│   就像装修房子的内部空间,摆放家具和设备。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 12: 分配 PID                                                              │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ Process->UniqueProcessId = ExCreateHandle(PspCidTable,             │       │
│   │                                           &CidEntry);              │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【发放房产证】通过 ExCreateHandle 在 PspCidTable 中分配 PID。       │
│   PID 是进程的唯一标识符,就像房产证号唯一标识一栋房子。       │
│   PspCidTable 是全局句柄表,存储所有进程和线程对象。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 13: 插入 PsActiveProcessHead                                              │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ ExAcquireResourceExclusiveLite(&PspActiveProcessMutex, TRUE);      │       │
│   │ InsertTailList(&PsActiveProcessHead,                               │       │
│   │                &Process->ActiveProcessLinks);                      │       │
│   │ ExReleaseResourceLite(&PspActiveProcessMutex);                    │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【登记到房产名录】将进程插入全局活动进程链表 PsActiveProcessHead。       │
│   使用 PspActiveProcessMutex 保护并发访问,       │
│   就像将新房子登记到城市的房产名录中。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 14: 计算 Quantum/Priority                                                  │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ PspComputeQuantumAndPriority(Process);                            │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【设置运营参数】计算进程的时间片(Quantum)和优先级(Priority)。       │
│   这些参数决定了进程在调度时的行为,       │
│   就像设置公司的运营参数(营业时间、服务级别)。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 15: 创建 PEB                                                              │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ Status = MmCreatePeb(Process,                                     │       │
│   │                      InitialPeb);                                  │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【制作装修图纸】在用户态地址空间中创建 PEB。       │
│   PEB 存储进程的用户态信息(模块列表、堆、参数等),       │
│   就像装修图纸记录了房子的内部布局和设施。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 16: 插入对象目录                                                           │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ Status = ObInsertObject((PVOID)Process,                            │       │
│   │                         NULL,                                      │       │
│   │                         DesiredAccess,                              │       │
│   │                         0,                                         │       │
│   │                         NULL,                                      │       │
│   │                         ProcessHandle);                             │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【发放钥匙】将进程对象插入对象管理器的对象目录,       │
│   并创建进程句柄返回给调用者。       │
│   这就像房子建好后发放钥匙,业主可以用钥匙进入房子。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 17: 访问检查                                                               │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ Status = SeAccessCheck(Process->Token,                             │       │
│   │                        &PsProcessType->GenericMapping,             │       │
│   │                        DesiredAccess,                              │       │
│   │                        NULL);                                      │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【权限复核】检查调用者请求的访问权限是否合法。       │
│   使用进程的安全令牌进行访问检查,       │
│   确保调用者有权访问新创建的进程。                                      │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 18: 返回进程句柄                                                           │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ return STATUS_SUCCESS;                                             │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   【交付完成】所有步骤成功完成,返回 STATUS_SUCCESS。       │
│   进程句柄已经通过 ProcessHandle 参数返回给调用者,       │
│   调用者可以用这个句柄操作新进程。                                      │
└──────────────────────────────────────────────────────────────────────────────────┘

5.3.4 关键设计决策的深度分析

5.3.4.1 父进程句柄必须指定的原因

为什么必须指定父进程句柄?

想象一下,创建一个新进程就像成立一家子公司。在现实世界中,子公司必须有一个母公司,它从母公司继承资产、业务关系和管理模式。同样,Windows 要求每个新进程都必须有一个父进程。

三个核心原因

  1. 继承关系:子进程需要继承父进程的某些属性(令牌、配额、设备映射)

    • 就像子公司继承母公司的品牌授权、经营许可和业务资质
    • 具体继承的内容包括:
      • 安全令牌(Token):决定了进程能访问哪些资源,就像员工卡决定了能进入哪些区域
      • 配额(Quota):限制了进程能使用的资源量,就像公司预算限制
      • 设备映射(Device Map):DOS 设备名称到 NT 设备名称的映射,就像公司内部使用的缩写代号
  2. 权限检查:需要验证调用者对父进程有 PROCESS_CREATE_PROCESS 权限

    • 就像成立子公司需要母公司董事会的批准
    • 这个权限检查防止恶意进程随意创建子进程
    • 例如,一个低权限的进程不能以高权限进程为父进程创建子进程
  3. 安全模型:确保创建者对父进程有足够权限

    • Windows 的安全模型基于"谁创建谁负责"的原则
    • 父进程对子进程有特殊的控制权(如可以终止子进程)
    • 这建立了清晰的进程层次关系,便于系统管理和资源追踪

代码实现细节

c 复制代码
// 在 PspCreateProcess 中,父进程句柄的解析是第二步
Status = ObReferenceObjectByHandle(
    ParentProcess,                    // 父进程句柄
    PROCESS_CREATE_PROCESS,           // 需要的权限
    PsProcessType,                    // 对象类型
    PreviousMode,                     // 调用者模式
    (PVOID*)&Parent,                  // 输出:父进程对象指针
    NULL
);

if (!NT_SUCCESS(Status)) {
    // 如果父进程不存在或权限不足,立即返回错误
    return Status;
}

为什么不能传 NULL?

有些人可能会问:"为什么不能传 NULL 表示创建一个'根进程'?"答案是:

  • Windows 系统中只有一个真正的"根进程"------System 进程(PID 4),它在系统启动时由内核创建
  • 所有用户进程都必须有父进程,形成一棵进程树
  • 这种设计使得系统可以追踪进程的来源和关系,便于调试和安全管理
  • 如果允许 NULL,会破坏这种清晰的层次结构

5.3.4.2 SectionHandle 与进程类型的关系

SectionHandle 决定进程类型

SectionHandle 就像是公司的章程文件。有章程的公司是正规的、有明确业务的公司;没有章程的公司是特殊机构,执行特殊任务。

SectionHandle 进程类型 说明 比喻
NULL 系统进程 没有可执行文件,通常是系统服务 特殊机构(如情报局),没有公开的章程
非 NULL 用户进程 有可执行文件,正常的应用程序 正规公司,有明确的章程和业务范围

深入理解 Section 对象

Section 对象是内存管理器提供的核心对象,它代表了一个内存区域与文件的映射关系。对于进程创建,Section 对象有以下重要作用:

  1. 可执行文件的内存映射

    • 当你运行 notepad.exe 时,系统首先打开这个文件
    • 然后创建一个 Section 对象,将文件映射到内存
    • 这个 Section 对象包含了程序的代码、数据、资源等
  2. 进程地址空间的蓝图

    • Section 对象决定了进程地址空间的初始布局
    • 它指定了代码段、数据段的位置和大小
    • 它还包含了程序的入口点地址
  3. 写时复制(Copy-on-Write)优化

    • 多个进程可以共享同一个 Section 对象
    • 当某个进程需要修改内存时,才复制相应的页面
    • 这大大节省了内存,例如多个记事本进程共享同一份代码

系统进程的创建

当 SectionHandle 为 NULL 时,创建的是系统进程。这种进程有以下特点:

c 复制代码
// 系统进程的创建示例(如 System 进程、smss.exe)
Status = NtCreateProcess(
    &ProcessHandle,
    PROCESS_ALL_ACCESS,
    NULL,                    // 无对象名称
    ParentProcess,
    FALSE,                   // 不继承句柄
    NULL,                    // 无 Section ------ 这是关键!
    NULL,                    // 无调试端口
    NULL                     // 无异常端口
);

系统进程的典型用途:

  • System 进程(PID 4):内核的"进程容器",内核线程在这里运行
  • smss.exe:会话管理器,负责创建用户会话
  • csrss.exe:Windows 子系统服务器进程
  • 服务进程:某些系统服务可能以空进程方式创建,稍后再加载代码

SectionHandle 的生命周期

复制代码
┌──────────────────────────────────────────────────────────────────────────────────┐
│                        SectionHandle 的生命周期                                │
│                                                                                  │
│   1. 打开可执行文件                                                              │
│      CreateFile("notepad.exe", GENERIC_READ | GENERIC_EXECUTE, ...)            │
│                                    │                                           │
│                                    ▼                                           │
│   2. 创建 Section 对象                                                           │
│      NtCreateSection(&SectionHandle, SECTION_ALL_ACCESS, ..., FileHandle)      │
│                                    │                                           │
│                                    ▼                                           │
│   3. 创建进程                                                                    │
│      NtCreateProcess(&ProcessHandle, ..., SectionHandle, ...)                  │
│                                    │                                           │
│                                    ▼                                           │
│   4. 进程使用 Section                                                            │
│      - Section 被映射到进程地址空间                                            │
│      - 进程的代码、数据来自 Section                                            │
│      - Section 引用计数增加                                                    │
│                                    │                                           │
│                                    ▼                                           │
│   5. 关闭句柄                                                                    │
│      NtClose(SectionHandle);  // 进程仍持有 Section 的引用                     │
│      NtClose(FileHandle);                                                      │
│                                                                                  │
│   注意:即使关闭 SectionHandle,Section 对象仍然存在,                          │
│   因为进程持有它的引用。直到进程终止,Section 才会被释放。                      │
└──────────────────────────────────────────────────────────────────────────────────┘

5.3.4.3 PID 分配机制

PID 实际上是句柄

PID(进程标识符)看起来像是一个简单的整数,但实际上它是全局句柄表中的一个句柄值。这个设计非常巧妙,让我们深入理解它。

c 复制代码
Process->UniqueProcessId = ExCreateHandle(PspCidTable, &CidEntry);

PspCidTable 的本质

PspCidTable 是一个系统全局的句柄表,专门用于存储进程和线程对象:

  • 名称含义:CID = Client ID,客户端标识符
  • 存储内容:所有进程对象(EPROCESS)和线程对象(ETHREAD)
  • 句柄值:PID 和 TID 都是这个表中的句柄
  • 访问方式:通过 PID 可以直接找到对应的 EPROCESS 对象

为什么用句柄表而不是简单的计数器?

  1. 安全性:句柄表有访问控制,不是任何人都能通过 PID 获取进程对象
  2. 快速查找:句柄表是高效的哈希表,查找时间复杂度接近 O(1)
  3. 对象生命周期管理:句柄表管理对象的引用计数
  4. 统一接口:进程和线程使用相同的机制,代码简洁

PID 分配的具体过程

复制代码
┌──────────────────────────────────────────────────────────────────────────────────┐
│                        PID 分配流程                                            │
│                                                                                  │
│   步骤 1: 准备句柄表项                                                           │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ CidEntry.Object = Process;  // 指向 EPROCESS 对象                   │       │
│   │ CidEntry.GrantedAccess = PROCESS_ALL_ACCESS;                       │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 2: 在句柄表中分配一个槽位                                                 │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ PID = ExCreateHandle(PspCidTable, &CidEntry);                      │       │
│   │                                                                     │       │
│   │ // ExCreateHandle 内部:                                            │       │
│   │ // 1. 在句柄表中找一个空闲槽位                                       │       │
│   │ // 2. 将 CidEntry 的内容写入该槽位                                   │       │
│   │ // 3. 返回槽位的索引作为句柄值(即 PID)                             │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                    │                                           │
│                                    ▼                                           │
│   步骤 3: 将 PID 存储到 EPROCESS                                                │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ Process->UniqueProcessId = PID;                                    │       │
│   │                                                                     │       │
│   │ // 现在,可以通过 PID 找到这个进程对象                              │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   PID 的特性:                                                                  │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 1. 唯一性:同一时刻,系统中没有两个进程有相同的 PID                  │       │
│   │ 2. 可复用:进程终止后,PID 可以被新进程复用                          │       │
│   │ 3. 顺序性:PID 通常递增分配,但可能跳过某些值                       │       │
│   │ 4. 特殊值:PID=0 表示空闲进程(Idle),PID=4 表示 System 进程       │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
└──────────────────────────────────────────────────────────────────────────────────┘

PID 复用的安全性考虑

当进程终止后,它的 PID 可以被新进程复用。这带来一个潜在问题:如果某个进程记住了另一个进程的 PID,然后那个进程终止了,新进程可能恰好分配到相同的 PID,导致误操作。

Windows 通过以下机制缓解这个问题:

  1. 句柄验证:通过 PID 获取进程对象时,会验证对象类型
  2. 延迟复用:PID 不会立即复用,而是等待一段时间
  3. 引用计数:即使进程终止,如果还有代码持有进程对象的引用,PID 也不会被复用

通过 PID 获取进程对象

c 复制代码
// PsLookupProcessByProcessId:通过 PID 获取 EPROCESS
NTSTATUS PsLookupProcessByProcessId(
    IN HANDLE ProcessId,           // PID(实际上是句柄值)
    OUT PEPROCESS *Process         // 输出:EPROCESS 指针
) {
    // 在 PspCidTable 中查找
    Status = ObReferenceObjectByHandle(
        ProcessId,                 // PID 作为句柄
        PROCESS_ALL_ACCESS,
        PsProcessType,
        KernelMode,
        (PVOID*)Process,
        NULL
    );
    return Status;
}

5.3.4.4 地址空间创建的时序

为什么先创建地址空间再初始化 KPROCESS?

这是一个关键的时序依赖问题。要理解它,我们需要深入了解 KPROCESS 的结构和地址空间的作用。

KPROCESS 的关键字段

c 复制代码
typedef struct _KPROCESS {
    // ... 其他字段 ...
    ULONG_PTR DirectoryTableBase;  // 页目录基址 ------ 关键字段!
    // ... 其他字段 ...
} KPROCESS;

DirectoryTableBase 是页目录的物理地址,它是虚拟地址空间的核心:

  • 每个进程有独立的页目录
  • 页目录包含了进程所有虚拟地址到物理地址的映射
  • CPU 的 CR3 寄存器存储当前进程的 DirectoryTableBase
  • 切换进程时,CR3 也随之切换

创建顺序的必要性

复制代码
┌──────────────────────────────────────────────────────────────────────────────────┐
│                        地址空间创建时序                                        │
│                                                                                  │
│   错误的顺序(假设):                                                           │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 1. KeInitializeProcess(&Process->Pcb, ...)                         │       │
│   │    问题:DirectoryTableBase 从哪里来?                              │       │
│   │                                                                     │       │
│   │ 2. MmCreateProcessAddressSpace(Process)                            │       │
│   │    创建页目录,获取 DirectoryTableBase                              │       │
│   │    但 KPROCESS 已经初始化了!需要重新设置 DirectoryTableBase        │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   正确的顺序:                                                                   │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 1. MmCreateProcessAddressSpace(Process)                            │       │
│   │    创建页目录结构                                                   │       │
│   │    分配物理页作为页目录                                             │       │
│   │    设置 Process->Pcb.DirectoryTableBase = 页目录物理地址           │       │
│   │                                                                     │       │
│   │ 2. KeInitializeProcess(&Process->Pcb, ...)                         │       │
│   │    读取 Process->Pcb.DirectoryTableBase(已设置)                  │       │
│   │    初始化其他 KPROCESS 字段                                        │       │
│   │    设置调度相关的参数                                               │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   MmCreateProcessAddressSpace 的内部工作:                                     │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ NTSTATUS MmCreateProcessAddressSpace(PEPROCESS Process)             │       │
│   │ {                                                                   │       │
│   │     // 1. 分配页目录物理页                                          │       │
│   │     PageDirectory = MmAllocPage();                                 │       │
│   │                                                                     │       │
│   │     // 2. 初始化页目录                                              │       │
│   │     // 复制内核地址空间映射(所有进程共享内核空间)                  │       │
│   │     RtlCopyMemory(PageDirectory, KernelPageDirectory, ...);        │       │
│   │                                                                     │       │
│   │     // 3. 设置 DirectoryTableBase                                  │       │
│   │     Process->Pcb.DirectoryTableBase = MmGetPhysicalAddress(        │       │
│   │         PageDirectory                                              │       │
│   │     );                                                              │       │
│   │                                                                     │       │
│   │     // 4. 初始化用户态地址空间区域                                  │       │
│   │     // (如 PEB 区域、共享用户数据区域等)                          │       │
│   │                                                                     │       │
│   │     return STATUS_SUCCESS;                                         │       │
│   │ }                                                                   │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
└──────────────────────────────────────────────────────────────────────────────────┘

地址空间创建的详细步骤

  1. 分配页目录

    • 页目录是一页(4KB)物理内存
    • 包含 1024 个页目录项(PDE),每个 PDE 管理 4MB 虚拟地址空间
    • 总共管理 4GB 虚拟地址空间(32 位系统)
  2. 复制内核映射

    • 所有进程共享内核地址空间(高 2GB)
    • 新进程的页目录复制内核的页目录项
    • 这样所有进程看到的内核空间是一样的
  3. 设置用户态空间

    • 用户态地址空间(低 2GB)初始为空
    • 后续会映射可执行文件、DLL、堆、栈等
  4. 创建特殊区域

    • 共享用户数据区(KUSER_SHARED_DATA)
    • PEB 区域(预留空间,稍后创建 PEB)
    • TEB 区域(预留空间,线程创建时分配)

5.3.4.5 错误处理与回滚

事务性创建机制

进程创建是一个复杂的多步骤操作,就像建造房子一样,任何一个步骤失败都需要回滚之前已完成的工作。Windows 采用事务性创建机制,确保失败时不会留下半成品。

c 复制代码
// CleanupWithRef: 释放已分配的 EPROCESS 和引用
VOID CleanupWithRef(PEPROCESS Process, PEPROCESS Parent, ...) {
    if (Process) {
        ObDereferenceObject(Process);
    }
    if (Parent) {
        ObDereferenceObject(Parent);
    }
    // ... 释放其他引用
}

// Cleanup: 只释放父进程引用
VOID Cleanup(PEPROCESS Parent, ...) {
    if (Parent) {
        ObDereferenceObject(Parent);
    }
    // ... 释放其他引用
}

回滚顺序

  1. 如果 EPROCESS 创建失败 → 释放父进程引用
  2. 如果地址空间创建失败 → 释放 EPROCESS 和父进程引用
  3. 如果后续步骤失败 → 释放已分配的资源

为什么回滚如此重要?

想象一下,如果进程创建失败后不回滚:

  1. 内存泄漏:EPROCESS 对象占用的内存永远不会释放
  2. 句柄泄漏:父进程、Section 等对象的引用计数不会减少
  3. 系统不稳定:半成品进程可能导致系统状态不一致
  4. 安全风险:未完全初始化的进程可能被恶意利用

5.3.5 错误处理与回滚

错误处理:施工失败的"拆除方案"

进程创建是一个复杂的多步骤操作,就像建造房子一样,任何一个步骤失败都需要回滚之前已完成的工作。Windows 采用事务性创建机制,确保失败时不会留下半成品。

想象一下,如果在铺设地基(步骤 7)时发现土壤不适合建筑,那么之前搭建的框架(步骤 5-6)都需要拆除。同样,如果进程创建失败,需要释放已分配的资源。

复制代码
┌──────────────────────────────────────────────────────────────────────────────────┐
│                        PspCreateProcess 错误处理                            │
│                                                                                  │
│   错误码列表:                                                                    │
│                                                                                  │
│   错误码                          说明                     比喻               │
│   ────────────────────────────  ────────────────────────  ──────────────────  │
│   STATUS_INVALID_HANDLE          无效句柄                 母公司不存在        │
│   STATUS_ACCESS_DENIED           访问被拒绝               权限不足            │
│   STATUS_INSUFFICIENT_RESOURCES  资源不足                 办公场地不足        │
│   STATUS_INVALID_PARAMETER       无效参数                 申请表填写错误      │
│   STATUS_PROCESS_IS_TERMINATING  进程正在终止             母公司正在倒闭      │
│   STATUS_FILE_CORRUPT_ERROR      文件损坏                 公司章程损坏        │
│   STATUS_IMAGE_FILE_CORRUPT      镜像文件损坏             公司章程格式错误    │
│   STATUS_NOT_SUPPORTED           不支持                   不允许此类公司      │
│                                                                                  │
│   回滚路径:                                                                     │
│                                                                                  │
│   步骤 1-4 失败(施工前准备阶段):                                               │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ Cleanup(Parent, SectionObject, DebugObject, ExceptionPortObject);  │       │
│   │ return Status;                                                    │       │
│   │                                                                     │       │
│   │ 【还没开始施工】只是获取了各种引用,释放这些引用即可。               │       │
│   │ 就像施工前检查发现图纸有问题,取消施工计划。                        │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   步骤 5 失败(EPROCESS 创建失败):                                              │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ Cleanup(Parent, SectionObject, DebugObject, ExceptionPortObject);  │       │
│   │ return Status;                                                    │       │
│   │                                                                     │       │
│   │ 【框架搭建失败】ObCreateObject 失败,没有创建 EPROCESS。            │       │
│   │ 只需要释放之前获取的引用。                                         │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   步骤 6-15 失败(施工进行中):                                                  │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ CleanupWithRef(Process, Parent, SectionObject, DebugObject,        │       │
│   │               ExceptionPortObject);                               │       │
│   │ return Status;                                                    │       │
│   │                                                                     │       │
│   │ 【施工中途失败】EPROCESS 已创建,需要释放 EPROCESS 和其他引用。     │       │
│   │ ObDereferenceObject(Process) 会触发 EPROCESS 的销毁流程。          │       │
│   │ 就像房子建了一半发现地基不稳,需要拆除已建部分。                    │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   步骤 16-18 失败(即将完工):                                                   │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ // 进程已经创建,需要特殊处理                                        │       │
│   │ NtTerminateProcess(ProcessHandle, Status);                         │       │
│   │ return Status;                                                    │       │
│   │                                                                     │       │
│   │ 【完工前失败】进程对象已插入对象目录,其他进程可能已经能看到它。    │       │
│   │ 不能简单释放,需要调用 NtTerminateProcess 正常终止进程。            │       │
│   │ 就像房子建好后发现验收不合格,需要正式拆除而不是半途废弃。          │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   回滚的重要性:                                                                 │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 1. 防止资源泄漏:如果不回滚,已分配的内存、句柄等会一直占用        │       │
│   │ 2. 保证系统稳定:半成品进程可能导致系统状态不一致                  │       │
│   │ 3. 安全考虑:未完全初始化的进程可能被恶意利用                      │       │
│   │ 4. 正确的错误处理:调用者能收到明确的错误码                        │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
└──────────────────────────────────────────────────────────────────────────────────┘

5.3.6 概念解释

5.3.6.1 ParentProcess / InheritObjectTable

ParentProcess:父进程句柄

ParentProcess 是创建新进程时必须指定的父进程句柄。就像每个子公司都有一个母公司,每个新进程都有一个父进程。

父进程的作用

  1. 属性继承:新进程从父进程继承以下属性:

    • 安全令牌(Token):决定进程的权限级别
    • 配额限制(Quota):限制进程能使用的资源量
    • 设备映射(Device Map):DOS 设备名称映射
    • 优先级(BasePriority):进程的基础调度优先级
    • CPU 亲和性(Affinity):进程可以在哪些 CPU 上运行
  2. 进程树结构:所有进程形成一棵树,根节点是 System 进程(PID 4):

    复制代码
    System (PID 4)
        │
        ├── smss.exe (会话管理器)
        │       │
        │       ├── csrss.exe (子系统服务器)
        │       └── winlogon.exe (登录管理器)
        │               │
        │               ├── services.exe (服务控制管理器)
        │               │       │
        │               │       ├── svchost.exe (服务宿主进程)
        │               │       └── ... (其他服务进程)
        │               │
        │               └── lsass.exe (本地安全认证服务)
        │
        └── ... (其他系统进程)
  3. 权限检查:调用者必须对父进程有 PROCESS_CREATE_PROCESS 权限

InheritObjectTable:是否继承父进程句柄表

InheritObjectTable 是一个布尔值,决定新进程是否继承父进程的可继承句柄。就像子公司是否继承母公司的业务关系

句柄继承的详细机制

复制代码
┌──────────────────────────────────────────────────────────────────────────────────┐
│                        句柄继承机制                                            │
│                                                                                  │
│   父进程的句柄表:                                                               │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 句柄值    对象类型        是否可继承    说明                           │       │
│   │ 0x0004   File            TRUE         打开的配置文件(可继承)        │       │
│   │ 0x0008   File            FALSE        打开的临时文件(不可继承)      │       │
│   │ 0x000C   Event           TRUE         同步事件(可继承)              │       │
│   │ 0x0010   Mutex           FALSE        互斥锁(不可继承)              │       │
│   │ 0x0014   Process         TRUE         子进程句柄(可继承)            │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   如果 InheritObjectTable = TRUE:                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 新进程的句柄表会复制父进程的可继承句柄:                              │       │
│   │                                                                     │       │
│   │ 句柄值    对象类型        说明                                       │       │
│   │ 0x0004   File            继承的配置文件句柄                          │       │
│   │ 0x000C   Event           继承的同步事件句柄                          │       │
│   │ 0x0014   Process         继承的子进程句柄                            │       │
│   │                                                                     │       │
│   │ 注意:不可继承的句柄(0x0008, 0x0010)不会被复制                    │       │
│   │ 句柄值可能不同,但指向相同的对象                                    │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   如果 InheritObjectTable = FALSE:                                            │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 新进程的句柄表为空,不继承任何句柄                                  │       │
│   │ 新进程需要自己打开需要的资源                                        │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   句柄继承的应用场景:                                                          │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 1. 管道通信:父进程创建管道,子进程继承管道句柄进行通信              │       │
│   │ 2. 文件共享:父进程打开文件,子进程继承文件句柄共享访问              │       │
│   │ 3. 同步机制:父进程创建事件/互斥锁,子进程继承用于同步              │       │
│   │ 4. 进程控制:父进程可以创建子进程并保留其句柄用于控制              │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
└──────────────────────────────────────────────────────────────────────────────────┘

如何设置句柄为可继承?

在创建句柄时,通过 OBJECT_ATTRIBUTES 结构设置 OBJ_INHERIT 标志:

c 复制代码
OBJECT_ATTRIBUTES ObjAttr;
InitializeObjectAttributes(&ObjAttr,
                           NULL,                // 无名称
                           OBJ_INHERIT,         // 可继承!
                           NULL,                // 无根目录
                           NULL);               // 无安全描述符

// 创建一个可继承的事件句柄
NtCreateEvent(&EventHandle,
              EVENT_ALL_ACCESS,
              &ObjAttr,
              NotificationEvent,
              FALSE);

5.3.6.2 SectionHandle / SECTION_MAP_EXECUTE

SectionHandle:可执行文件的 Section 对象句柄

SectionHandle 是一个指向 Section 对象的句柄,这个 Section 对象代表了可执行文件在内存中的映射。就像公司的章程文件,定义了公司的运营规则。

Section 对象的本质

Section 对象是内存管理器提供的核心对象类型,它有以下用途:

  1. 文件映射(File Mapping):将文件内容映射到进程的地址空间
  2. 共享内存:多个进程可以共享同一个 Section 对象
  3. 可执行文件加载:将 .exe/.dll 文件映射为可执行的内存区域

SECTION_MAP_EXECUTE 权限

SECTION_MAP_EXECUTE 是请求的访问权限之一,表示希望将 Section 映射为可执行的内存区域。

权限 说明 用途
SECTION_MAP_READ 可读映射 映射为只读内存,如数据文件
SECTION_MAP_WRITE 可写映射 映射为可写内存,如共享内存
SECTION_MAP_EXECUTE 可执行映射 映射为可执行内存,如 .exe/.dll
SECTION_ALL_ACCESS 所有权限 完全控制

Section 对象的创建过程

c 复制代码
// 1. 打开可执行文件
HANDLE FileHandle;
Status = NtCreateFile(&FileHandle,
                      GENERIC_READ | GENERIC_EXECUTE | SYNCHRONIZE,
                      &ObjAttr,
                      &IoStatus,
                      NULL,                   // 分配大小
                      FILE_ATTRIBUTE_NORMAL,
                      FILE_SHARE_READ,
                      FILE_OPEN,
                      FILE_SYNCHRONOUS_IO_NONALERT,
                      NULL,                   // EaBuffer
                      0);                     // EaLength

// 2. 创建 Section 对象
HANDLE SectionHandle;
Status = NtCreateSection(&SectionHandle,
                         SECTION_ALL_ACCESS,
                         NULL,                 // 对象属性
                         NULL,                 // 最大大小(由文件决定)
                         PAGE_EXECUTE_READ,    // 页面保护
                         SEC_IMAGE,            // 可执行镜像
                         FileHandle);

// 3. 关闭文件句柄(Section 已持有文件引用)
NtClose(FileHandle);

SEC_IMAGE 标志的重要性

SEC_IMAGE 标志告诉内存管理器这是一个可执行文件镜像,需要特殊处理:

  1. PE 文件解析:解析 PE 头,确定代码段、数据段的位置
  2. 节区映射:将 PE 文件的各个节区映射到正确的虚拟地址
  3. 入口点确定:记录程序的入口点地址
  4. 重定位处理:如果需要,处理地址重定位

5.3.6.3 DebugPort / DbgkDebugObjectType

DebugPort:调试对象句柄

DebugPort 是一个可选参数,用于进程调试。就像监管部门,可以监控进程的运行状态。

调试端口的作用

当进程设置了 DebugPort,调试器可以接收以下事件:

事件类型 说明 触发时机
CREATE_PROCESS_DEBUG_EVENT 进程创建事件 进程启动时
CREATE_THREAD_DEBUG_EVENT 线程创建事件 新线程创建时
LOAD_DLL_DEBUG_EVENT DLL 加载事件 加载新 DLL 时
UNLOAD_DLL_DEBUG_EVENT DLL 卸载事件 卸载 DLL 时
EXCEPTION_DEBUG_EVENT 异常事件 发生异常时
EXIT_PROCESS_DEBUG_EVENT 进程退出事件 进程终止时
EXIT_THREAD_DEBUG_EVENT 线程退出事件 线程终止时
OUTPUT_DEBUG_STRING_EVENT 调试输出事件 OutputDebugString 调用时

DbgkDebugObjectType:调试对象类型

DbgkDebugObjectType 是调试对象的类型定义,用于对象管理器的类型检查。

c 复制代码
// 调试对象的创建
HANDLE DebugObjectHandle;
Status = DbgCreateDebugObject(&DebugObjectHandle,
                              DEBUG_ALL_ACCESS,
                              NULL);

// 将调试对象附加到进程
Status = NtCreateProcess(&ProcessHandle,
                         PROCESS_ALL_ACCESS,
                         NULL,
                         ParentProcess,
                         FALSE,
                         SectionHandle,
                         DebugObjectHandle,    // 调试端口
                         NULL);

调试器的工作流程

复制代码
┌──────────────────────────────────────────────────────────────────────────────────┐
│                        调试器工作流程                                          │
│                                                                                  │
│   1. 创建调试对象                                                                │
│      DbgCreateDebugObject(&DebugObject)                                        │
│                                    │                                           │
│                                    ▼                                           │
│   2. 创建被调试进程                                                              │
│      NtCreateProcess(..., DebugObject, ...)                                    │
│                                    │                                           │
│                                    ▼                                           │
│   3. 等待调试事件                                                                │
│      WaitForDebugEvent(&DebugEvent, INFINITE)                                  │
│                                    │                                           │
│                                    ▼                                           │
│   4. 处理调试事件                                                                │
│      switch (DebugEvent.dwDebugEventCode) {                                    │
│          case CREATE_PROCESS_DEBUG_EVENT:                                      │
│              // 处理进程创建                                                    │
│          case EXCEPTION_DEBUG_EVENT:                                           │
│              // 处理异常(如断点)                                              │
│          case LOAD_DLL_DEBUG_EVENT:                                            │
│              // 处理 DLL 加载                                                  │
│          ...                                                                   │
│      }                                                                          │
│                                    │                                           │
│                                    ▼                                           │
│   5. 继续被调试进程                                                              │
│      ContinueDebugEvent(DebugEvent.dwProcessId,                               │
│                         DebugEvent.dwThreadId,                                │
│                         DBG_CONTINUE)                                          │
│                                    │                                           │
│                                    ▼                                           │
│   6. 循环等待下一个事件                                                          │
│      goto 步骤 3                                                               │
│                                                                                  │
│   调试事件的处理示例(断点):                                                  │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ case EXCEPTION_DEBUG_EVENT:                                         │       │
│   │     if (DebugEvent.u.Exception.ExceptionRecord.ExceptionCode       │       │
│   │         == EXCEPTION_BREAKPOINT) {                                  │       │
│   │         // 这是断点异常                                              │       │
│   │         // 调试器可以:                                              │       │
│   │         // 1. 显示当前变量值                                         │       │
│   │         // 2. 显示调用栈                                             │       │
│   │         // 3. 让用户选择继续或单步执行                               │       │
│   │     }                                                               │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
└──────────────────────────────────────────────────────────────────────────────────┘

5.3.6.4 PspCidTable / PspActiveProcessMutex

PspCidTable:全局 PID/TID 句柄表

PspCidTable 是系统全局的句柄表,专门用于存储进程和线程对象。就像城市的户籍登记系统,记录所有居民(进程和线程)的信息。

PspCidTable 的特点

  1. 全局唯一:整个系统只有一个 PspCidTable
  2. 存储对象:所有 EPROCESS 和 ETHREAD 对象
  3. 句柄值:PID 和 TID 都是这个表中的句柄值
  4. 快速查找:通过 PID/TID 可以快速找到对应的对象

PspCidTable 的结构

复制代码
┌──────────────────────────────────────────────────────────────────────────────────┐
│                        PspCidTable 结构                                        │
│                                                                                  │
│   PspCidTable 是一个动态扩展的句柄表:                                          │
│                                                                                  │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 句柄值 (PID/TID)    对象指针        对象类型                         │       │
│   │ 4                   EPROCESS       System 进程                      │       │
│   │ 8                   EPROCESS       smss.exe 进程                    │       │
│   │ 12                  ETHREAD        System 进程的线程                │       │
│   │ 16                  EPROCESS       csrss.exe 进程                   │       │
│   │ 20                  ETHREAD        csrss.exe 的线程 1               │       │
│   │ 24                  ETHREAD        csrss.exe 的线程 2               │       │
│   │ ...                 ...            ...                              │       │
│   │ 1234                EPROCESS       用户启动的 notepad.exe           │       │
│   │ 1238                ETHREAD        notepad.exe 的主线程             │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   通过 PID 查找进程对象:                                                       │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ PsLookupProcessByProcessId(PID, &Process)                           │       │
│   │                                                                     │       │
│   │ // 内部实现:                                                        │       │
│   │ // 1. 将 PID 作为句柄值                                             │       │
│   │ // 2. 在 PspCidTable 中查找                                         │       │
│   │ // 3. 验证对象类型是 PsProcessType                                  │       │
│   │ // 4. 返回 EPROCESS 指针                                            │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   通过 TID 查找线程对象:                                                       │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ PsLookupThreadByThreadId(TID, &Thread)                              │       │
│   │                                                                     │       │
│   │ // 内部实现类似,验证对象类型是 PsThreadType                         │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
└──────────────────────────────────────────────────────────────────────────────────┘

PspActiveProcessMutex:保护全局进程链表的互斥锁

PspActiveProcessMutex 是一个互斥锁,用于保护全局进程链表 PsActiveProcessHead 的并发访问。就像房产登记处的排队系统,防止多人同时登记造成混乱。

为什么需要互斥锁?

在多线程环境中,多个线程可能同时创建或终止进程,如果不加保护:

  1. 链表损坏:并发插入/删除可能导致链表结构损坏
  2. 数据丢失:一个线程的修改可能被另一个线程覆盖
  3. 系统崩溃:链表损坏可能导致遍历时崩溃

互斥锁的使用

c 复制代码
// 插入进程到全局链表
ExAcquireResourceExclusiveLite(&PspActiveProcessMutex, TRUE);
InsertTailList(&PsActiveProcessHead, &Process->ActiveProcessLinks);
ExReleaseResourceLite(&PspActiveProcessMutex);

// 从全局链表移除进程
ExAcquireResourceExclusiveLite(&PspActiveProcessMutex, TRUE);
RemoveEntryList(&Process->ActiveProcessLinks);
ExReleaseResourceLite(&PspActiveProcessMutex);

PsActiveProcessHead 的作用

PsActiveProcessHead 是全局活动进程链表的头部,所有活动进程都链接在这个链表上:

  • 遍历所有进程:系统可以通过这个链表遍历所有活动进程
  • 进程管理:任务管理器、性能监控等工具使用这个链表获取进程信息
  • 资源统计:系统统计进程数量、资源使用等
c 复制代码
// 遍历所有活动进程
PLIST_ENTRY Current = PsActiveProcessHead.Flink;
while (Current != &PsActiveProcessHead) {
    PEPROCESS Process = CONTAINING_RECORD(Current, EPROCESS, ActiveProcessLinks);
    // 处理每个进程
    printf("PID: %d, Name: %s\n", Process->UniqueProcessId, Process->ImageFileName);
    Current = Current->Flink;
}

5.3.7 为什么要这样设计

本节深入探讨 NtCreateProcess 设计背后的原因,每个问题都涉及重要的设计决策。理解这些设计原因,有助于我们更好地理解 Windows 进程管理的精髓。

5.3.7.1 为什么 NtCreateProcess 不直接创建进程,而是先创建地址空间?

问题背景

有些人可能会认为,创建进程应该是一个"一步到位"的操作------直接创建完整的进程对象。但实际上,NtCreateProcess 分为多个步骤,其中地址空间的创建是最早的关键步骤之一。

答案:地址空间是进程的核心资源,必须先创建。

详细解释

  1. 进程的本质是地址空间隔离

    • 进程的核心特征是拥有独立的虚拟地址空间
    • 不同进程的虚拟地址空间相互隔离,一个进程不能直接访问另一个进程的内存
    • 地址空间是进程存在的"物理基础",就像房子的地基是房子存在的基础
  2. 地址空间创建需要分配关键资源

    • 页目录(Page Directory):存储虚拟地址到物理地址的映射关系
    • 页表(Page Tables):更细粒度的地址映射
    • 物理内存:页目录和页表本身需要物理内存存储
    • 这些资源必须在初始化进程结构之前准备好
  3. KPROCESS 需要 DirectoryTableBase

    • KPROCESS 是 EPROCESS 的第一个字段,包含调度器需要的信息
    • DirectoryTableBase(页目录基址)是 KPROCESS 的关键字段
    • CPU 的 CR3 寄存器存储当前进程的 DirectoryTableBase
    • 进程切换时,调度器会将新进程的 DirectoryTableBase 加载到 CR3
    • 如果地址空间还没创建,DirectoryTableBase 就不存在,无法初始化 KPROCESS

比喻理解

想象一下建造房子:

  • 地址空间 = 地基:地基是房子的基础,必须先铺设
  • KPROCESS = 房屋结构:房屋结构建在地基之上,地基没准备好,结构无法搭建
  • EPROCESS = 房屋档案:房屋档案记录了房子的信息,但档案本身不决定房子能否存在

如果先创建 EPROCESS(房屋档案)再创建地址空间(地基),就像先写房屋档案再铺设地基------档案里的信息(如地基位置)还没有实际数据,只能填空值或等待后续更新,这会导致设计复杂化。

代码证据

c 复制代码
// PspCreateProcess 中的顺序(步骤 7-8)
// 步骤 7:创建地址空间
Status = MmCreateProcessAddressSpace(Process);
if (!NT_SUCCESS(Status)) {
    CleanupWithRef(Process, Parent, ...);
    return Status;
}

// 步骤 8:初始化 KPROCESS
// 此时 Process->Pcb.DirectoryTableBase 已经被 MmCreateProcessAddressSpace 设置
KeInitializeProcess(&Process->Pcb,
                    Parent->Pcb.Affinity,
                    Parent->Pcb.BasePriority);

5.3.7.2 为什么必须指定父进程句柄(不能 NULL)?

问题背景

在 Unix/Linux 系统中,fork() 系统调用总是以调用进程为父进程。但在 Windows 中,NtCreateProcess 要求显式指定父进程句柄,而且不能为 NULL。为什么这样设计?

答案:继承和权限检查的需要。

详细解释

  1. 属性继承的需要

    • Windows 进程需要从父进程继承多种属性:
      • 安全令牌(Token):决定了进程的权限级别和访问能力
      • 配额限制(Quota):限制进程能使用的 CPU 时间、内存等
      • 设备映射(Device Map):DOS 设备名称到 NT 设备名称的映射
      • 优先级和亲和性:调度相关的参数
    • 如果允许 NULL,新进程就没有这些属性的来源,需要额外设计"默认属性"机制
  2. 权限检查的需要

    • 创建进程需要验证调用者有足够的权限
    • 具体来说,调用者对父进程必须有 PROCESS_CREATE_PROCESS 权限
    • 这个权限检查防止恶意进程随意创建子进程
    • 例如,一个低权限进程不能以高权限进程为父进程创建子进程(否则会提升权限)
  3. 进程树结构的维护

    • Windows 系统中的所有进程形成一棵树
    • 树的根是 System 进程(PID 4),由内核在启动时创建
    • 每个进程都有唯一的父进程,形成清晰的层次关系
    • 这种结构便于:
      • 追踪进程来源:调试时可以查看进程的创建链
      • 资源归属:确定进程的资源来源和责任归属
      • 权限传递:理解权限如何从父进程传递到子进程
  4. 安全模型的要求

    • Windows 安全模型基于"谁创建谁负责"的原则
    • 父进程对子进程有特殊的控制权:
      • 可以终止子进程
      • 可以读取子进程的内存
      • 可以调试子进程
    • 如果允许 NULL,会破坏这种责任关系

比喻理解

想象一下公司注册:

  • 父进程 = 母公司:新公司(子公司)必须有母公司
  • 属性继承 = 业务继承:子公司继承母公司的资质、许可、品牌
  • 权限检查 = 董事会批准:成立子公司需要母公司董事会批准
  • 进程树 = 企业集团:所有公司形成企业集团结构

如果允许 NULL(无母公司),就像允许成立"无来源公司"------这样的公司没有资质继承,没有责任归属,没有监管主体,会破坏整个企业体系的管理结构。

Unix/Linux 的对比

Unix/Linux 的 fork() 系统调用隐式以调用进程为父进程:

c 复制代码
// Unix/Linux
pid_t child_pid = fork();  // 父进程隐式为调用进程

Windows 的 NtCreateProcess 显式指定父进程:

c 复制代码
// Windows
NtCreateProcess(&ProcessHandle,
                PROCESS_ALL_ACCESS,
                NULL,
                NtCurrentProcess(),  // 显式指定父进程为当前进程
                FALSE,
                SectionHandle,
                NULL,
                NULL);

两种设计各有优劣:

  • Unix/Linux:简单,但灵活性较低
  • Windows:灵活(可以以任意进程为父进程),但需要显式指定

5.3.7.3 为什么 SectionHandle 决定是真实进程还是空壳?

问题背景

NtCreateProcess 的 SectionHandle 参数可以为 NULL 或非 NULL。为什么这个参数决定了进程是"真实进程"还是"空壳进程"?

答案:Section 包含可执行文件信息,决定了进程的"身份"。

详细解释

  1. Section 对象的本质

    • Section 对象是内存管理器提供的对象,代表文件与内存的映射
    • 对于进程创建,Section 对象包含可执行文件(.exe)的内容:
      • 代码段:程序的机器指令
      • 数据段:程序的静态数据
      • 资源:图标、字符串、对话框模板等
      • PE 头:程序的组织结构信息
    • Section 对象就像程序的"灵魂",决定了进程运行什么代码
  2. 有 SectionHandle = 真实进程

    • 进程有明确的可执行文件
    • 进程地址空间会映射可执行文件的内容
    • 进程有明确的入口点(程序的 main 或 WinMain)
    • 进程有明确的名称(来自可执行文件的文件名)
    • 例如:notepad.exeexplorer.execmd.exe
  3. 无 SectionHandle = 空壳进程(系统进程)

    • 进程没有可执行文件
    • 进程地址空间初始为空(只有内核映射)
    • 进程没有明确的入口点
    • 进程的代码来自其他来源:
      • 内核线程:System 进程中的线程运行内核代码
      • 动态加载:某些进程稍后通过其他机制加载代码
    • 例如:System 进程(PID 4)、某些服务进程

比喻理解

想象一下公司注册:

  • SectionHandle = 公司章程:章程定义了公司的业务范围、运营规则
  • 有章程 = 正规公司:有明确的业务,如"某某科技有限公司"
  • 无章程 = 特殊机构:如情报局、特勤处,没有公开的业务范围

空壳进程就像特殊机构------它们存在,但不像普通公司有明确的"营业执照"(可执行文件)。它们的"业务"(代码)来自特殊渠道(内核代码或动态加载)。

代码示例

c 复制代码
// 创建真实进程(有 Section)
HANDLE SectionHandle;
NtCreateSection(&SectionHandle, SECTION_ALL_ACCESS, NULL, NULL,
                PAGE_EXECUTE_READ, SEC_IMAGE, FileHandle);

NtCreateProcess(&ProcessHandle, PROCESS_ALL_ACCESS, NULL,
                NtCurrentProcess(), FALSE,
                SectionHandle,  // 有 Section!
                NULL, NULL);

// 创建空壳进程(无 Section)
NtCreateProcess(&ProcessHandle, PROCESS_ALL_ACCESS, NULL,
                NtCurrentProcess(), FALSE,
                NULL,  // 无 Section!
                NULL, NULL);

空壳进程的应用

空壳进程虽然看起来"空",但有重要的用途:

  1. System 进程(PID 4)

    • 内核线程的"容器"
    • 内核代码不在任何用户态进程中运行
    • System 进程提供了内核线程的进程上下文
  2. 服务进程

    • 某些服务可能以空壳方式创建
    • 稍后通过 ServiceMain 函数加载服务代码
  3. 进程注入

    • 恶意软件可能创建空壳进程
    • 然后注入代码到进程中运行
    • 这是安全研究的重要话题

5.3.7.4 为什么 PspCidTable 是系统全局的?

问题背景

PspCidTable 是存储 PID/TID 的全局句柄表。为什么这个表必须是全局的,而不是每个进程有自己的 PID 表?

答案:全局唯一性和跨进程访问的需要。

详细解释

  1. PID/TID 的全局唯一性

    • PID(进程标识符)和 TID(线程标识符)必须在系统范围内唯一
    • 如果每个进程有自己的 PID 表,不同进程可能看到相同的 PID 值指向不同的进程
    • 全局表确保:PID=1234 在任何进程中都指向同一个进程对象
  2. 跨进程访问的需要

    • 进程经常需要访问其他进程:
      • 调试器:调试器进程需要访问被调试进程
      • 任务管理器:需要列出和操作所有进程
      • 进程间通信:需要通过 PID 打开其他进程的句柄
    • 如果 PID 表不是全局的,跨进程访问会非常复杂
  3. API 设计的简洁性

    • OpenProcess(pid) 可以直接通过 PID 打开进程句柄
    • 如果 PID 不是全局唯一,需要额外的参数指定"哪个进程的 PID"
    • 全局表使得 API 设计简洁明了
  4. 调试和监控的需要

    • 系统管理员需要查看系统中所有进程
    • 调试器需要通过 PID/TID 定位进程和线程
    • 性能监控工具需要统计所有进程的资源使用
    • 全局表提供了统一的访问入口

比喻理解

想象一下身份证系统:

  • PspCidTable = 全国身份证数据库:存储所有人的身份证信息
  • PID = 身份证号:每个人的唯一标识
  • 全局唯一 = 全国唯一:身份证号在全国范围内唯一
  • 跨进程访问 = 跨地区查询:任何地方都可以通过身份证号查到同一个人

如果每个地区有自己的身份证数据库(每个进程有自己的 PID 表),身份证号可能在不同地区指向不同的人,跨地区查询会非常混乱。

PspCidTable 的实现

c 复制代码
// PspCidTable 的全局定义
PHANDLE_TABLE PspCidTable = NULL;

// 初始化(系统启动时)
PspCidTable = ExCreateHandleTable(NULL);

// 分配 PID
HANDLE_TABLE_ENTRY CidEntry;
CidEntry.Object = Process;
CidEntry.GrantedAccess = PROCESS_ALL_ACCESS;
Process->UniqueProcessId = ExCreateHandle(PspCidTable, &CidEntry);

// 通过 PID 查找进程
NTSTATUS PsLookupProcessByProcessId(HANDLE ProcessId, PEPROCESS *Process) {
    return ObReferenceObjectByHandle(ProcessId, PROCESS_ALL_ACCESS,
                                     PsProcessType, KernelMode,
                                     (PVOID*)Process, NULL);
}

5.3.7.5 为什么先 MmCreateProcessAddressSpace 再 KeInitializeProcess?

问题背景

在 PspCreateProcess 的步骤 7 和步骤 8 中,先调用 MmCreateProcessAddressSpace 创建地址空间,再调用 KeInitializeProcess 初始化 KPROCESS。为什么这个顺序不能颠倒?

答案:KPROCESS 需要 DirectoryTableBase,而它来自地址空间。

详细解释

  1. KPROCESS 的关键字段

    c 复制代码
    typedef struct _KPROCESS {
        DISPATCHER_HEADER Header;
        LIST_ENTRY ProfileListHead;
        ULONG_PTR DirectoryTableBase;  // 页目录基址 ------ 关键!
        // ... 其他字段 ...
    } KPROCESS;
    • DirectoryTableBase 是页目录的物理地址
    • 它是虚拟地址空间的核心数据
  2. 地址空间创建提供 DirectoryTableBase

    c 复制代码
    NTSTATUS MmCreateProcessAddressSpace(PEPROCESS Process) {
        // 分配页目录
        PageDirectory = MmAllocPage();
    
        // 设置 DirectoryTableBase
        Process->Pcb.DirectoryTableBase = MmGetPhysicalAddress(PageDirectory);
    
        return STATUS_SUCCESS;
    }
  3. KeInitializeProcess 使用 DirectoryTableBase

    c 复制代码
    VOID KeInitializeProcess(PKPROCESS Process, KAFFINITY Affinity, KPRIORITY BasePriority) {
        // 使用 Process->DirectoryTableBase(已由 MmCreateProcessAddressSpace 设置)
        // 初始化调度相关字段
        Process->Affinity = Affinity;
        Process->BasePriority = BasePriority;
        // ...
    }
  4. 如果顺序颠倒会怎样

    • 如果先调用 KeInitializeProcess,DirectoryTableBase 还不存在
    • KeInitializeProcess 需要设置 DirectoryTableBase,但不知道应该设置什么值
    • 后续调用 MmCreateProcessAddressSpace 时,需要重新设置 DirectoryTableBase
    • 这会导致代码复杂化,可能出现不一致的状态

比喻理解

想象一下盖房子:

  • 地址空间 = 地基:地基决定了房子的位置和基础结构
  • KPROCESS = 房屋框架:房屋框架建在地基之上
  • DirectoryTableBase = 地基坐标:房屋框架需要知道地基的位置才能正确搭建

如果先搭建框架再铺设地基,框架不知道应该建在哪里,只能随意放置。后续铺设地基时,需要移动框架到正确位置,这会导致结构不稳定。

代码顺序

c 复制代码
// 正确的顺序(PspCreateProcess 中)
// 步骤 7:创建地址空间(设置 DirectoryTableBase)
Status = MmCreateProcessAddressSpace(Process);

// 步骤 8:初始化 KPROCESS(使用已设置的 DirectoryTableBase)
KeInitializeProcess(&Process->Pcb,
                    Parent->Pcb.Affinity,
                    Parent->Pcb.BasePriority);

5.3.7.6 为什么需要 PspInitializeProcessSecurity 复制令牌?

问题背景

进程创建时,需要调用 PspInitializeProcessSecurity 复制父进程的安全令牌。为什么每个进程需要独立的令牌,而不是直接共享父进程的令牌?

答案:安全隔离和独立权限控制的需要。

详细解释

  1. 令牌的本质

    • 安全令牌(Token)是进程的"身份证"
    • 包含进程的用户身份、组身份、权限列表
    • 决定了进程能访问哪些资源、能执行哪些操作
  2. 独立令牌的必要性

    • 权限修改 :子进程可能需要不同的权限
      • 例如,父进程是管理员权限,但子进程应该以普通用户权限运行
      • 如果共享令牌,修改子进程权限会影响父进程
    • 安全隔离 :子进程的权限变化不应影响父进程
      • 如果子进程被攻击者利用,攻击者获得的权限不应扩展到父进程
    • 生命周期独立 :父进程终止后,子进程应该继续运行
      • 如果共享令牌,父进程终止可能导致令牌失效,子进程无法继续运行
  3. 令牌复制的实现

    c 复制代码
    NTSTATUS PspInitializeProcessSecurity(PEPROCESS Process, PEPROCESS Parent) {
        // 复制父进程的令牌
        Status = SeCopyToken(&Process->Token,
                             Parent->Token,
                             FALSE);  // 不继承特权
    
        // 如果复制失败,使用默认令牌
        if (!NT_SUCCESS(Status)) {
            Process->Token = SeGetDefaultToken();
        }
    
        return Status;
    }
  4. 令牌继承与修改

    • 子进程默认继承父进程的令牌
    • 但可以通过 CreateProcessAsUser 等函数指定不同的令牌
    • 子进程运行后,也可以通过 AdjustTokenPrivileges 等函数修改自己的权限
    • 这些修改只影响子进程,不影响父进程

比喻理解

想象一下员工卡系统:

  • 令牌 = 员工卡:员工卡决定了员工能进入哪些区域
  • 父进程 = 高级经理:高级经理有高级员工卡,能进入所有区域
  • 子进程 = 新员工:新员工应该有独立的员工卡
  • 复制令牌 = 发放新卡:新员工获得新卡,权限可以与经理相同或不同

如果新员工直接使用经理的员工卡(共享令牌),会有问题:

  • 新员工丢失卡片,经理也无法进入
  • 新员工权限被修改,经理的权限也跟着变化
  • 新员工离职后,经理的卡片可能失效

独立令牌确保每个进程有独立的"员工卡",互不影响。

5.3.7.7 PEB 创建的时机为什么在 KeInitializeProcess 之后?

问题背景

PEB(进程环境块)在用户态地址空间中创建,这个操作发生在 KeInitializeProcess 之后。为什么 PEB 不能更早创建?

答案:PEB 需要进程的地址空间,而地址空间需要先创建和初始化。

详细解释

  1. PEB 的位置

    • PEB 位于用户态地址空间(低 2GB)
    • 具体地址通常是 0x7FFDF000(32 位系统)
    • PEB 是用户态数据结构,用户态代码可以直接访问
  2. PEB 创建的前提条件

    • 地址空间存在:PEB 需要映射到地址空间中
    • 地址空间初始化:地址空间需要有基本的映射(如共享用户数据区)
    • KPROCESS 初始化:调度器需要知道进程的地址空间信息
  3. PEB 创建的时机

    • PspCreateProcess 的步骤 15 创建 PEB
    • 此时:
      • 地址空间已创建(步骤 7)
      • KPROCESS 已初始化(步骤 8)
      • 安全令牌已设置(步骤 9)
      • 句柄表已初始化(步骤 10)
      • 用户态地址空间已初始化(步骤 11)
      • PID 已分配(步骤 12)
      • 进程已插入全局链表(步骤 13)
  4. PEB 创建的内容

    c 复制代码
    NTSTATUS MmCreatePeb(PEPROCESS Process, PPEB InitialPeb) {
        // 在用户态地址空间中分配 PEB
        PebAddress = 0x7FFDF000;
    
        // 映射 PEB 页面
        Status = MmMapViewInProcessSpace(Process, PebAddress, sizeof(PEB));
    
        // 初始化 PEB 内容
        Peb->ImageBaseAddress = Process->SectionBaseAddress;
        Peb->ProcessParameters = InitialPeb;
        // ...
    
        return Status;
    }

比喻理解

想象一下装修房子:

  • 地址空间 = 房子:房子需要先建成
  • KPROCESS = 房屋结构:房屋结构需要先搭建
  • PEB = 装修图纸:装修图纸放在房子里

装修图纸(PEB)不能在房子建成之前放置,因为还没有地方放。必须先建房子(地址空间),搭建结构(KPROCESS),然后才能放置装修图纸(PEB)。

5.3.7.8 为什么需要 PspActiveProcessMutex 保护插入?

问题背景

将进程插入全局链表 PsActiveProcessHead 时,需要使用 PspActiveProcessMutex 互斥锁保护。为什么需要这个保护?

答案:线程安全,防止并发访问导致链表损坏。

详细解释

  1. 多线程环境

    • Windows 是多线程操作系统
    • 多个线程可能同时创建进程
    • 多个线程可能同时终止进程
    • 如果不加保护,并发操作可能导致链表损坏
  2. 链表操作的风险

    • 插入操作:InsertTailList 修改链表指针
    • 删除操作:RemoveEntryList 修改链表指针
    • 遍历操作:遍历链表时,如果链表被修改,可能访问无效节点
  3. 竞态条件示例

    复制代码
    线程 A:正在插入进程 P1
        Current = PsActiveProcessHead.Flink;
        PsActiveProcessHead.Flink = &P1->ActiveProcessLinks;
        P1->ActiveProcessLinks.Flink = Current;
        // 还没完成,Current->Blink 还没更新
    
    线程 B:同时插入进程 P2
        Current = PsActiveProcessHead.Flink;  // 拿到的是 P1(部分插入)
        PsActiveProcessHead.Flink = &P2->ActiveProcessLinks;
        P2->ActiveProcessLinks.Flink = Current;
        // P1 的插入被中断,链表结构损坏
  4. 互斥锁的作用

    c 复制代码
    // 插入进程(加锁)
    ExAcquireResourceExclusiveLite(&PspActiveProcessMutex, TRUE);
    InsertTailList(&PsActiveProcessHead, &Process->ActiveProcessLinks);
    ExReleaseResourceLite(&PspActiveProcessMutex);
    
    // 遍历进程(加锁)
    ExAcquireResourceSharedLite(&PspActiveProcessMutex, TRUE);
    PLIST_ENTRY Current = PsActiveProcessHead.Flink;
    while (Current != &PsActiveProcessHead) {
        PEPROCESS Process = CONTAINING_RECORD(Current, EPROCESS, ActiveProcessLinks);
        // 处理进程
        Current = Current->Flink;
    }
    ExReleaseResourceLite(&PspActiveProcessMutex);

比喻理解

想象一下排队系统:

  • PsActiveProcessHead = 排队队伍:所有人排队等待服务
  • 插入进程 = 新人加入队伍:新人需要找到队伍末尾加入
  • PspActiveProcessMutex = 排队号码机:只有拿到号码的人才能操作队伍

如果没有排队号码机(互斥锁),多人可能同时尝试加入队伍,导致队伍混乱:

  • 两人都认为自己是队伍末尾
  • 两人都插入,导致队伍分叉
  • 后面的人不知道应该跟谁

互斥锁确保同一时间只有一个人操作队伍,保证队伍结构正确。

5.3.7.9 进程对象插入对象目录的时机为什么不在最开始?

问题背景

进程对象通过 ObInsertObject 插入对象目录,这个操作发生在步骤 16,接近创建流程的末尾。为什么不一开始就插入对象目录?

答案:确保进程完全初始化后再暴露给其他进程。

详细解释

  1. 对象目录的作用

    • 对象目录是对象管理器提供的命名空间
    • 进程可以有自己的名称(如 "\Sessions\1\Processes\1234")
    • 其他进程可以通过名称打开进程句柄
  2. 过早插入的风险

    • 如果进程在步骤 5(创建 EPROCESS)后就插入对象目录
    • 其他进程可能立即打开这个进程的句柄
    • 但此时进程还没完全初始化:
      • 地址空间可能还没创建
      • 安全令牌可能还没设置
      • PEB 可能还没创建
    • 其他进程看到的进程状态是不完整的,可能导致错误
  3. 正确时机的保证

    • 步骤 16 插入对象目录时,进程已经:
      • 地址空间已创建并初始化
      • KPROCESS 已初始化
      • 安全令牌已设置
      • 句柄表已初始化
      • PID 已分配
      • 进程已插入全局链表
      • PEB 已创建
    • 此时进程状态完整,其他进程可以安全地访问
  4. 对象可见性的控制

    c 复制代码
    // 步骤 16:插入对象目录
    Status = ObInsertObject((PVOID)Process,
                            NULL,
                            DesiredAccess,
                            0,
                            NULL,
                            ProcessHandle);
    
    // ObInsertObject 内部:
    // 1. 将进程对象插入对象目录(如果有名称)
    // 2. 创建进程句柄
    // 3. 返回句柄给调用者

比喻理解

想象一下开店:

  • 进程创建 = 店铺装修:装修需要多个步骤
  • 插入对象目录 = 正式开业:开业后顾客可以进入
  • 过早插入 = 装修期间开业:顾客进入时店铺还没准备好

如果店铺在装修期间就开业(过早插入对象目录),顾客进入时可能看到:

  • 地面还没铺设
  • 货架还没安装
  • 商品还没上架

顾客可能因为店铺状态不完整而感到困惑或遇到问题。正确的做法是装修完成后才开业,确保顾客看到的是完整的店铺。

5.3.7.10 为什么需要单独的 NtCreateProcessEx?

问题背景

Windows 有两个创建进程的系统调用:NtCreateProcess 和 NtCreateProcessEx。为什么需要两个版本?

答案:兼容性和功能扩展的需要。

详细解释

  1. 历史背景

    • NtCreateProcess 是早期版本的系统调用(Windows NT 3.1/3.5/4.0)
    • NtCreateProcessEx 是扩展版本(Windows XP/2003)
    • 新版本增加了更多参数和功能
  2. 向后兼容性

    • 旧代码使用 NtCreateProcess
    • 如果直接修改 NtCreateProcess 的参数,会破坏旧代码
    • 保持 NtCreateProcess 的接口不变,通过 NtCreateProcessEx 提供新功能
  3. NtCreateProcessEx 的新功能

    • InheritDebugPort 参数:是否继承父进程的调试端口
    • Reserved 参数:预留参数,供未来扩展
    • 更多标志位:PROCESS_CREATE_FLAGS_* 系列标志
  4. 包装实现

    c 复制代码
    NTSTATUS NtCreateProcess(OUT PHANDLE ProcessHandle,
                            IN ACCESS_MASK DesiredAccess,
                            IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
                            IN HANDLE ParentProcess,
                            IN BOOLEAN InheritObjectTable,
                            IN HANDLE SectionHandle OPTIONAL,
                            IN HANDLE DebugPort OPTIONAL,
                            IN HANDLE ExceptionPort OPTIONAL) {
        // NtCreateProcess 只是 NtCreateProcessEx 的包装
        return NtCreateProcessEx(ProcessHandle,
                                DesiredAccess,
                                ObjectAttributes,
                                ParentProcess,
                                InheritObjectTable,
                                SectionHandle,
                                DebugPort,
                                ExceptionPort,
                                FALSE,  // InheritDebugPort(默认不继承)
                                NULL);  // Reserved(预留)
    }
  5. 标志位扩展

    c 复制代码
    // NtCreateProcessEx 支持的标志位
    #define PROCESS_CREATE_FLAGS_BREAKAWAY          0x01  // 脱离 Job
    #define PROCESS_CREATE_FLAGS_NO_DEBUG_INHERIT   0x02  // 不继承调试端口
    #define PROCESS_CREATE_FLAGS_INHERIT_HANDLES    0x04  // 继承句柄
    #define PROCESS_CREATE_FLAGS_LARGE_PAGES        0x08  // 使用大页面
    #define PROCESS_CREATE_FLAGS_PROTECTED_PROCESS  0x10  // 受保护进程

比喻理解

想象一下 API 版本:

  • NtCreateProcess = V1.0 API:早期版本,功能有限
  • NtCreateProcessEx = V2.0 API:新版本,功能扩展
  • 包装实现 = 兼容层:V1.0 API 内部调用 V2.0 API

软件开发中常见的模式:

  • 保持旧 API 不变,确保旧代码继续工作
  • 提供新 API,提供更多功能
  • 旧 API 包装新 API,减少代码重复

这种设计模式在 Windows 中很常见:

  • NtCreateFile / NtCreateFileEx
  • NtOpenProcess / NtOpenProcessEx
  • 等等

5.3.8 增强子节 1:NtCreateProcess 与 NtCreateProcessEx 的差异

API 演进的历史背景

Windows 系统调用 API 的演进是一个有趣的话题。NtCreateProcess 和 NtCreateProcessEx 的差异展示了 Windows 如何在保持向后兼容性的同时扩展功能。

版本演进时间线

复制代码
┌──────────────────────────────────────────────────────────────────────────────────┐
│                        NtCreateProcess API 演进时间线                          │
│                                                                                  │
│   Windows NT 3.1 (1993)                                                         │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ NtCreateProcess 首次引入                                            │       │
│   │ - 8 个参数                                                          │       │
│   │ - 基本的进程创建功能                                                │       │
│   │ - 标志位编码在参数低位                                              │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   Windows 2000 (1999)                                                           │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ NtCreateProcess 继续使用                                            │       │
│   │ - 标志位编码方式暴露出扩展性问题                                    │       │
│   │ - 需要更多控制选项                                                  │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   Windows XP/2003 (2001-2003)                                                   │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ NtCreateProcessEx 引入                                              │       │
│   │ - 10 个参数(新增 InheritDebugPort 和 Reserved)                    │       │
│   │ - 清晰的参数结构                                                    │       │
│   │ - 更好的扩展性                                                      │       │
│   │ - NtCreateProcess 包装 NtCreateProcessEx                            │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   Windows Vista+ (2006+)                                                        │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ NtCreateProcessEx2 等更多版本                                       │       │
│   │ - 支持更多进程类型(如受保护进程)                                  │       │
│   │ - 更复杂的安全模型                                                  │       │
│   │ - 更多标志位和选项                                                  │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
└──────────────────────────────────────────────────────────────────────────────────┘

旧接口的标志位编码问题

复制代码
┌──────────────────────────────────────────────────────────────────────────────────┐
│                        NtCreateProcess vs NtCreateProcessEx                 │
│                                                                                  │
│   历史背景:                                                                     │
│                                                                                  │
│   Windows 2000: NtCreateProcess (原始版本)                                     │
│   Windows XP/2003: NtCreateProcessEx (扩展版本)                               │
│   Windows Vista+: NtCreateProcessEx2 (更多扩展)                              │
│                                                                                  │
│   NtCreateProcess 的旧标志位编码:                                               │
│                                                                                  │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 旧接口使用 SectionHandle 和 DebugPort 的低位来编码标志:              │       │
│   │                                                                     │       │
│   │   SectionHandle = (PVOID)((ULONG_PTR)RealSectionHandle | Flags);    │       │
│   │   DebugPort = (PVOID)((ULONG_PTR)RealDebugPort | InheritDebugPort); │       │
│   │                                                                     │       │
│   │   问题: 标志位空间有限,扩展性差                                       │       │
│   │                                                                     │       │
│   │   【比喻】就像在电话号码后面加备注信息:                              │       │
│   │   电话号码 = 12345678 | 0x01(表示紧急联系人)                       │       │
│   │   问题:电话号码可能被误读,备注信息空间有限                          │       │
│   │   更好的做法:电话号码和备注分开存储                                  │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   NtCreateProcessEx 的新接口:                                                   │
│                                                                                  │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 新增独立参数:                                                        │       │
│   │   - InheritDebugPort: BOOLEAN                                       │       │
│   │   - Reserved: PVOID (预留,供未来扩展)                               │       │
│   │                                                                     │       │
│   │   优点: 清晰的参数,更好的扩展性                                      │       │
│   │                                                                     │       │
│   │   【比喻】就像把电话号码和备注分开存储:                              │       │
│   │   电话号码:12345678                                                │       │
│   │   备注:紧急联系人                                                  │       │
│   │   优点:清晰明了,可以添加更多备注字段                                │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   兼容性保证:                                                                  │
│                                                                                  │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ NtCreateProcess 内部调用 NtCreateProcessEx:                         │       │
│   │                                                                     │       │
│   │   NTSTATUS NtCreateProcess(...) {                                   │       │
│   │       return NtCreateProcessEx(..., FALSE, NULL);                   │       │
│   │   }                                                                 │       │
│   │                                                                     │       │
│   │   这样旧代码可以继续使用 NtCreateProcess                             │       │
│   │                                                                     │       │
│   │   【比喻】就像旧版 API 是新版 API 的"简化版":                      │       │
│   │   - 旧版:快速通道,默认选项                                        │       │
│   │   - 新版:完整通道,自定义选项                                      │       │
│   │   - 旧版内部调用新版,传入默认参数                                  │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
└──────────────────────────────────────────────────────────────────────────────────┘

标志位的详细说明

标志位 说明 应用场景
PROCESS_CREATE_FLAGS_BREAKAWAY 0x01 脱离 Job 对象 进程不想受 Job 限制时使用
PROCESS_CREATE_FLAGS_NO_DEBUG_INHERIT 0x02 不继承调试端口 子进程不应该被调试器监控
PROCESS_CREATE_FLAGS_INHERIT_HANDLES 0x04 继承句柄 子进程需要访问父进程的资源
PROCESS_CREATE_FLAGS_LARGE_PAGES 0x08 使用大页面 需要优化内存性能的进程
PROCESS_CREATE_FLAGS_PROTECTED_PROCESS 0x10 受保护进程 防止被其他进程干扰(如 DRM)

受保护进程的特殊性

Windows 8.1 引入了受保护进程(Protected Process)概念:

  • 目的:防止恶意进程注入代码、读取内存、终止进程
  • 应用:DRM 系统、安全软件、系统关键进程
  • 限制:即使是管理员权限,也无法完全控制受保护进程
  • 实现:通过 PROCESS_CREATE_FLAGS_PROTECTED_PROCESS 标志创建

API 设计的最佳实践

从 NtCreateProcess 到 NtCreateProcessEx 的演进,展示了 API 设计的最佳实践:

  1. 向后兼容性:旧 API 继续工作,新 API 提供新功能
  2. 清晰的参数:避免在参数中编码额外信息
  3. 预留扩展空间:Reserved 参数为未来预留
  4. 包装模式:旧 API 包装新 API,减少代码重复

5.3.9 增强子节 2:CreateProcess (kernel32) 的完整流程

CreateProcess:用户态的"一站式服务窗口"

CreateProcess 是 kernel32.dll 提供的用户态 API,它封装了整个进程创建流程。如果把 NtCreateProcess 比作向政府部门提交的公司注册申请 ,那么 CreateProcess 就像是一站式服务窗口,它会帮你完成所有准备工作,包括:

  1. 准备申请材料(解析参数、验证文件)

  2. 提交申请(调用 NtCreateProcess)

  3. 招聘员工(创建初始线程)

  4. 通知相关部门(通知 CSRSS/Win32k)

  5. 启动运营(启动线程执行)

    ┌──────────────────────────────────────────────────────────────────────────────────┐
    │ CreateProcess (kernel32) 完整流程 │
    │ │
    │ ┌─────────────────────────────────────────────────────────────────────┐ │
    │ │ 1. 参数解析和验证 │ │
    │ │ - 解析命令行参数 │ │
    │ │ - 检查可执行文件路径 │ │
    │ │ - 验证安全策略(Badapp/WinSafer) │ │
    │ │ │ │
    │ │ 【准备申请材料】就像准备公司注册申请表, │ │
    │ │ 检查公司名称是否合法、经营范围是否合规、 │ │
    │ │ 是否有安全限制(如某些行业需要特殊许可)。 │ │
    │ └─────────────────────────────────────────────────────────────────────┘ │
    │ │ │
    │ ▼ │
    │ ┌─────────────────────────────────────────────────────────────────────┐ │
    │ │ 2. 打开可执行文件 │ │
    │ │ - CreateFile(ImageName, GENERIC_READ | GENERIC_EXECUTE, ...) │ │
    │ │ - 获取文件句柄 │ │
    │ │ │ │
    │ │ 【获取公司章程】打开可执行文件,获取文件句柄。 │ │
    │ │ 这个文件包含了程序的代码和数据, │ │
    │ │ 就像公司章程定义了公司的运营规则。 │ │
    │ └─────────────────────────────────────────────────────────────────────┘ │
    │ │ │
    │ ▼ │
    │ ┌─────────────────────────────────────────────────────────────────────┐ │
    │ │ 3. 创建 Section 对象 │ │
    │ │ - NtCreateSection(&SectionHandle, │ │
    │ │ SECTION_ALL_ACCESS, │ │
    │ │ NULL, │ │
    │ │ &SectionSize, │ │
    │ │ PAGE_EXECUTE_READ, │ │
    │ │ SEC_IMAGE, │ │
    │ │ FileHandle); │ │
    │ │ │ │
    │ │ 【制作章程副本】将可执行文件映射为 Section 对象。 │ │
    │ │ Section 是内存管理器的对象,代表文件在内存中的映射。 │ │
    │ │ SEC_IMAGE 标志表示这是可执行文件镜像。 │ │
    │ └─────────────────────────────────────────────────────────────────────┘ │
    │ │ │
    │ ▼ │
    │ ┌─────────────────────────────────────────────────────────────────────┐ │
    │ │ 4. 创建进程 │ │
    │ │ - NtCreateProcess(&ProcessHandle, │ │
    │ │ PROCESS_ALL_ACCESS, │ │
    │ │ NULL, │ │
    │ │ NtCurrentProcess(), │ │
    │ │ InheritHandles, │ │
    │ │ SectionHandle, │ │
    │ │ NULL, │ │
    │ │ NULL); │ │
    │ │ │ │
    │ │ 【提交注册申请】调用内核系统调用创建进程。 │ │
    │ │ 这一步完成了进程对象的创建(见 5.3.3 的 18 个步骤)。 │ │
    │ │ 此时进程已经存在,但还没有线程运行。 │ │
    │ └─────────────────────────────────────────────────────────────────────┘ │
    │ │ │
    │ ▼ │
    │ ┌─────────────────────────────────────────────────────────────────────┐ │
    │ │ 5. 创建初始线程 │ │
    │ │ - NtCreateThread(&ThreadHandle, │ │
    │ │ THREAD_ALL_ACCESS, │ │
    │ │ NULL, │ │
    │ │ ProcessHandle, │ │
    │ │ &ClientId, │ │
    │ │ &ThreadContext, │ │
    │ │ NULL, │ │
    │ │ FALSE); │ │
    │ │ - 设置线程入口为 BaseThreadStart │ │
    │ │ │ │
    │ │ 【招聘首位员工】创建进程的第一个线程。 │ │
    │ │ 线程入口设置为 BaseThreadStart(kernel32 的内部函数), │ │
    │ │ 这个函数会调用 LdrInitializeThunk 初始化进程, │ │
    │ │ 然后跳转到程序的真正入口点(main 或 WinMain)。 │ │
    │ └─────────────────────────────────────────────────────────────────────┘ │
    │ │ │
    │ ▼ │
    │ ┌─────────────────────────────────────────────────────────────────────┐ │
    │ │ 6. 通知 CSRSS │ │
    │ │ - CsrClientCallServer(CsrCreateProcess, ...) │ │
    │ │ - 注册进程到 CSRSS 进程列表 │ │
    │ │ │ │
    │ │ 【通知行业协会】CSRSS(Client/Server Runtime Subsystem) │ │
    │ │ 是 Windows 子系统的核心进程,负责管理所有用户进程。 │ │
    │ │ 新进程需要向 CSRSS 注册,就像新公司需要向行业协会登记。 │ │
    │ └─────────────────────────────────────────────────────────────────────┘ │
    │ │ │
    │ ▼ │
    │ ┌─────────────────────────────────────────────────────────────────────┐ │
    │ │ 7. 通知 Win32k │ │
    │ │ - 如果是 GUI 进程,通知 win32k.sys │ │
    │ │ - 初始化 GDI/USER 资源 │ │
    │ │ │ │
    │ │ 【通知监管部门】如果进程会使用图形界面(GUI), │ │
    │ │ 需要通知 win32k.sys(窗口管理器)初始化图形资源。 │ │
    │ │ 就像某些公司需要通知特定监管部门(如餐饮公司通知卫生部门)。 │ │
    │ └─────────────────────────────────────────────────────────────────────┘ │
    │ │ │
    │ ▼ │
    │ ┌─────────────────────────────────────────────────────────────────────┐ │
    │ │ 8. 启动线程 │ │
    │ │ - NtResumeThread(ThreadHandle, NULL) │ │
    │ │ - 线程开始执行 BaseThreadStart → LdrInitializeThunk │ │
    │ │ - 最终进入用户态入口点 │ │
    │ │ │ │
    │ │ 【正式开业】调用 NtResumeThread 启动线程。 │ │
    │ │ 线程开始执行: │ │
    │ │ 1. BaseThreadStart(kernel32 入口) │ │
    │ │ 2. LdrInitializeThunk(加载器初始化,加载 DLL) │ │
    │ │ 3. 程序入口点(main/WinMain) │ │
    │ │ 就像公司正式开业,员工开始工作。 │ │
    │ └─────────────────────────────────────────────────────────────────────┘ │
    │ │
    │ 线程启动后的执行流程: │
    │ ┌─────────────────────────────────────────────────────────────────────┐ │
    │ │ │ │
    │ │ BaseThreadStart │ │
    │ │ │ │ │
    │ │ ▼ │ │
    │ │ LdrInitializeThunk │ │
    │ │ │ 加载所有依赖的 DLL │ │
    │ │ │ 调用每个 DLL 的 DllMain │ │
    │ │ │ 初始化进程堆 │ │
    │ │ ▼ │ │
    │ │ 程序入口点 (main 或 WinMain) │ │
    │ │ │ │ │
    │ │ ▼ │ │
    │ │ 用户程序开始执行 │ │
    │ │ │ │
    │ └─────────────────────────────────────────────────────────────────────┘ │
    └──────────────────────────────────────────────────────────────────────────────────┘


5.3.10 小结

本章总结

本章深入分析了 NtCreateProcess 系统调用的完整实现,从用户态 API 到内核态核心函数,涵盖了进程创建的每一个细节。通过本章的学习,读者应该对 Windows 进程创建机制有了全面的理解。

5.3.10.1 关键知识点

核心概念回顾

主题 关键点 详细说明
NtCreateProcess 创建进程的系统调用 用户态通过 ntdll.dll 调用,进入内核态执行实际创建操作
NtCreateProcessEx 扩展版本,支持更多选项 新增 InheritDebugPort 和 Reserved 参数,提供更好的扩展性
PspCreateProcess 核心创建函数,18 个步骤 内核态的核心实现,完成所有进程创建的实际工作
地址空间创建 MmCreateProcessAddressSpace 创建进程的虚拟地址空间,分配页目录和页表
PID 分配 ExCreateHandle(PspCidTable) 在全局句柄表中分配唯一的进程标识符
PEB 创建 MmCreatePeb 在用户态地址空间中创建进程环境块
错误处理 Cleanup/CleanupWithRef 回滚 事务性创建机制,失败时回滚所有已分配资源

18 步骤流程回顾

复制代码
┌──────────────────────────────────────────────────────────────────────────────────┐
│                        PspCreateProcess 18 步骤总结                            │
│                                                                                  │
│   阶段 1: 准备阶段(步骤 1-4)                                                   │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 步骤 1: 参数校验 ------ 检查标志位是否合法                              │       │
│   │ 步骤 2: 解析父进程句柄 ------ 获取父进程对象                            │       │
│   │ 步骤 3: 解析 SectionHandle ------ 获取可执行文件映射                    │       │
│   │ 步骤 4: 解析 DebugPort/ExceptionPort ------ 设置调试和异常端口          │       │
│   │                                                                     │       │
│   │ 【比喻】施工前准备:检查图纸、确认母公司、获取章程、设置监管        │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   阶段 2: 创建阶段(步骤 5-11)                                                  │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 步骤 5: 创建 EPROCESS 对象 ------ 分配进程结构                          │       │
│   │ 步骤 6: 初始化 EPROCESS 字段 ------ 设置基本属性                        │       │
│   │ 步骤 7: 创建地址空间 ------ 分配页目录                                  │       │
│   │ 步骤 8: 初始化 KPROCESS ------ 设置调度参数                            │       │
│   │ 步骤 9: 复制安全令牌 ------ 继承父进程权限                              │       │
│   │ 步骤 10: 初始化对象管理器 ------ 创建句柄表                             │       │
│   │ 步骤 11: 初始化用户态地址空间 ------ 映射可执行文件                     │       │
│   │                                                                     │       │
│   │ 【比喻】施工进行:搭建框架、铺设地基、安装系统、装修内部            │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   阶段 3: 注册阶段(步骤 12-15)                                                 │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 步骤 12: 分配 PID ------ 在全局句柄表中分配唯一标识                     │       │
│   │ 步骤 13: 插入 PsActiveProcessHead ------ 加入全局进程链表               │       │
│   │ 步骤 14: 计算 Quantum/Priority ------ 设置调度参数                     │       │
│   │ 步骤 15: 创建 PEB ------ 在用户态创建进程环境块                         │       │
│   │                                                                     │       │
│   │ 【比喻】登记注册:发放房产证、登记名录、设置参数、制作图纸          │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   阶段 4: 完成阶段(步骤 16-18)                                                 │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 步骤 16: 插入对象目录 ------ 创建进程句柄                               │       │
│   │ 步骤 17: 访问检查 ------ 验证调用者权限                                 │       │
│   │ 步骤 18: 返回进程句柄 ------ 完成创建                                   │       │
│   │                                                                     │       │
│   │ 【比喻】交付完成:发放钥匙、权限复核、交付使用                      │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
└──────────────────────────────────────────────────────────────────────────────────┘

5.3.10.2 设计原则

Windows 进程创建的设计哲学

  1. 分层设计:用户态 API → 系统调用 → 内部函数

    • 比喻:就像公司的管理层级:员工(用户态)→ 经理(系统调用)→ 总裁(内核态)
    • 每层有明确的职责,上层依赖下层,下层不知道上层的存在
    • 这种设计使得系统模块化,便于维护和扩展
  2. 事务性创建:失败时回滚所有已分配资源

    • 比喻:就像建造房子,如果中途失败,需要拆除已建部分
    • 确保系统不会留下半成品进程,避免资源泄漏和状态不一致
    • 每个步骤都有对应的回滚操作,保证原子性
  3. 权限检查:多个检查点验证权限

    • 比喻:就像公司注册需要多个部门审批
    • 步骤 2:检查对父进程的权限
    • 步骤 17:检查对新进程的请求权限
    • 多层检查确保安全性,防止权限提升攻击
  4. 模块化:各个子系统独立负责自己的部分

    • 比喻:就像建造房子涉及多个专业团队
    • 内存管理器负责地址空间
    • 对象管理器负责对象创建和句柄
    • 安全子系统负责令牌和权限
    • 进程管理器负责协调所有子系统

5.3.10.3 常见陷阱

开发中需要注意的问题

  1. 忘记继承句柄:需要显式设置 InheritObjectTable

    • 问题:子进程无法访问父进程的资源
    • 解决:如果需要继承,设置 InheritObjectTable = TRUE
    • 注意:只有标记为 OBJ_INHERIT 的句柄才会被继承
  2. 无效的 SectionHandle:必须有正确的可执行权限

    • 问题:创建进程失败,返回 STATUS_ACCESS_DENIED
    • 解决:创建 Section 时请求 SECTION_MAP_EXECUTE 权限
    • 注意:Section 必须用 SEC_IMAGE 标志创建
  3. 父进程权限不足:需要 PROCESS_CREATE_PROCESS 权限

    • 问题:无法以指定进程为父进程创建子进程
    • 解决:打开父进程时请求 PROCESS_CREATE_PROCESS 权限
    • 注意:低权限进程不能以高权限进程为父进程
  4. 资源泄漏:创建失败时需要正确释放资源

    • 问题:创建失败后,已分配的资源没有释放
    • 解决:使用 Cleanup/CleanupWithRef 函数正确回滚
    • 注意:Windows 内核已经处理了这个问题,但用户态代码需要注意

调试技巧

  • 使用 Process Monitor 工具监控进程创建过程
  • 检查返回的错误码,根据错误码定位问题
  • 使用调试器跟踪 NtCreateProcess 的执行
  • 查看进程的 PEB 和 TEB 内容,验证初始化是否正确

5.3.10.4 后续学习路径

下一步学习建议

  1. 线程创建:NtCreateThread 系统调用

    • 进程创建后需要创建线程才能执行代码
    • 理解线程的创建流程和初始化过程
    • 学习 TEB(线程环境块)的结构和作用
  2. 进程终止:NtTerminateProcess 系统调用

    • 进程如何正常终止
    • 资源清理和回滚机制
    • 进程终止通知(CSRSS、父进程)
  3. 线程调度:第 6 章

    • 理解调度器如何选择线程运行
    • 优先级、时间片、CPU 亲和性
    • 调度算法和调度策略
  4. 进程间通信

    • 管道、共享内存、消息队列
    • LPC(本地过程调用)
    • RPC(远程过程调用)
  5. 进程安全

    • 安全令牌的详细结构
    • ACL(访问控制列表)
    • 特权和权限管理

推荐阅读源码

  • ntoskrnl/ps/process.c(file:///d:/reactos/ntoskrnl/ps/process.c):进程创建的核心实现
  • dll/kernel32/client/proc.c(file:///d:/reactos/dll/kernel32/client/proc.c):CreateProcess 的用户态实现
  • sdk/include/ndk/pstypes.h(file:///d:/reactos/sdk/include/ndk/pstypes.h):EPROCESS/ETHREAD 结构定义
  • sdk/include/ndk/peb_teb.h(file:///d:/reactos/sdk/include/ndk/peb_teb.h):PEB/TEB 结构定义

实践建议

  1. 使用 WinDbg 调试器跟踪进程创建过程
  2. 编写简单的程序调用 CreateProcess,观察进程创建
  3. 使用 Process Explorer 查看进程的详细信息
  4. 阅读 ReactOS 源码,理解具体实现细节

源码位置ntoskrnl/ps/process.c(file:///d:/reactos/ntoskrnl/ps/process.c)、dll/kernel32/client/proc.c(file:///d:/reactos/dll/kernel32/client/proc.c)

相关推荐
ch.ju1 小时前
Java程序设计(第3版)第四章——继承的调用
java·开发语言
W_LuYi1851 小时前
Tauri + Rust + Vue 3 打造极速轻量桌面应用
java·开发语言·vue.js·rust
少司府1 小时前
C++进阶:红黑树
开发语言·数据结构·c++·b树·二叉树·红黑树
feng_you_ying_li1 小时前
Linux之线程同步:条件变量和两种生产消费模型
linux·运维·服务器
特种加菲猫2 小时前
哈希表的实现
开发语言·c++
C+-C资深大佬2 小时前
Python 新手学习指南
开发语言·python
小张小张爱学习2 小时前
Java基础面试题
java·开发语言
Drone_xjw2 小时前
Qt国际化多语言配置详解-入门到精通
开发语言·qt·命令模式
杨了个杨89822 小时前
HAproxy+Keepalive的简介及安装
运维·服务器