# ETHREAD

概述

  1. 每个 Windows 线程在 0 环都有一个对应的 ETHREAD 结构体
  2. 该结构体包含了线程的所有重要信息

kd> dt _ETHREAD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
ntdll!_ETHREAD
+0x000 Tcb : _KTHREAD
+0x1c0 CreateTime : _LARGE_INTEGER
+0x1c0 NestedFaultCount : Pos 0, 2 Bits
+0x1c0 ApcNeeded : Pos 2, 1 Bit
+0x1c8 ExitTime : _LARGE_INTEGER
+0x1c8 LpcReplyChain : _LIST_ENTRY
+0x1c8 KeyedWaitChain : _LIST_ENTRY
+0x1d0 ExitStatus : Int4B
+0x1d0 OfsChain : Ptr32 Void
+0x1d4 PostBlockList : _LIST_ENTRY
+0x1dc TerminationPort : Ptr32 _TERMINATION_PORT
+0x1dc ReaperLink : Ptr32 _ETHREAD
+0x1dc KeyedWaitValue : Ptr32 Void
+0x1e0 ActiveTimerListLock : Uint4B
+0x1e4 ActiveTimerListHead : _LIST_ENTRY
+0x1ec Cid : _CLIENT_ID // 线程标识信息
// kd> dt _CLIENT_ID
// ntdll!_CLIENT_ID
// +0x000 UniqueProcess : Ptr32 Void // 所属进程 PID
// +0x004 UniqueThread : Ptr32 Void // 当前线程 TID
+0x1f4 LpcReplySemaphore : _KSEMAPHORE
+0x1f4 KeyedWaitSemaphore : _KSEMAPHORE
+0x208 LpcReplyMessage : Ptr32 Void
+0x208 LpcWaitingOnPort : Ptr32 Void
+0x20c ImpersonationInfo : Ptr32 _PS_IMPERSONATION_INFORMATION
+0x210 IrpList : _LIST_ENTRY
+0x218 TopLevelIrp : Uint4B
+0x21c DeviceToVerify : Ptr32 _DEVICE_OBJECT
+0x220 ThreadsProcess : Ptr32 _EPROCESS // 指向线程所属进程(创建者)
+0x224 StartAddress : Ptr32 Void
+0x228 Win32StartAddress : Ptr32 Void
+0x228 LpcReceivedMessageId : Uint4B
+0x22c ThreadListEntry : _LIST_ENTRY // 双向链表,串联同一进程的所有线程
+0x234 RundownProtect : _EX_RUNDOWN_REF
+0x238 ThreadLock : _EX_PUSH_LOCK
+0x23c LpcReplyMessageId : Uint4B
+0x240 ReadClusterSize : Uint4B
+0x244 GrantedAccess : Uint4B
+0x248 CrossThreadFlags : Uint4B
+0x248 Terminated : Pos 0, 1 Bit
+0x248 DeadThread : Pos 1, 1 Bit
+0x248 HideFromDebugger : Pos 2, 1 Bit
+0x248 ActiveImpersonationInfo : Pos 3, 1 Bit
+0x248 SystemThread : Pos 4, 1 Bit
+0x248 HardErrorsAreDisabled : Pos 5, 1 Bit
+0x248 BreakOnTermination : Pos 6, 1 Bit
+0x248 SkipCreationMsg : Pos 7, 1 Bit
+0x248 SkipTerminationMsg : Pos 8, 1 Bit
+0x24c SameThreadPassiveFlags : Uint4B
+0x24c ActiveExWorker : Pos 0, 1 Bit
+0x24c ExWorkerCanWaitUser : Pos 1, 1 Bit
+0x24c MemoryMaker : Pos 2, 1 Bit
+0x250 SameThreadApcFlags : Uint4B
+0x250 LpcReceivedMsgIdValid : Pos 0, 1 Bit
+0x250 LpcExitThreadCalled : Pos 1, 1 Bit
+0x250 AddressSpaceOwner : Pos 2, 1 Bit
+0x254 ForwardClusterOnly : UChar
+0x255 DisablePageFaultClustering : UChar

注意EPROCESS 中有两条线程链表 —— +0x050 ThreadListHead (位于 KPROCESS 中)和 +0x190 ThreadListHead (位于 EPROCESS 中)。它们分别对应 KTHREAD+0x1b0ETHREAD+0x22c 处的 ThreadListEntry

# +0x000 Tcb : _KTHREAD

_KTHREAD 包含了线程的调度状态、上下文信息、优先级等内核管理所需的关键数据。

kd> dt _KTHREAD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
ntdll!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER // 可等待对象,可作为 WaitForSingleObject 等函数的参数
+0x010 MutantListHead : _LIST_ENTRY
+0x018 InitialStack : Ptr32 Void // 线程内核栈栈底(高地址端)
+0x01c StackLimit : Ptr32 Void // 线程内核栈界限(低地址端)
// 说明:每个线程在 0 环都有属于自己的内核堆栈
// TSS 中存储的 ESP0 始终指向当前正在运行的线程的内核栈
+0x020 Teb : Ptr32 Void // TEB(Thread Environment Block,线程环境块)
// 大小为 4KB,位于 3 环
// 3 环:FS:[0] -> TEB
// 0 环:FS:[0] -> KPCR
+0x024 TlsArray : Ptr32 Void
+0x028 KernelStack : Ptr32 Void // 线程内核栈当前栈顶(ESP)
+0x02c DebugActive : UChar // 若值为 -1,不能使用调试寄存器 Dr0~Dr7
+0x02d State : UChar // 线程状态(就绪 / 等待 / 运行)
+0x02e Alerted : [2] UChar
+0x030 Iopl : UChar
+0x031 NpxState : UChar
+0x032 Saturation : Char
+0x033 Priority : Char
+0x034 ApcState : _KAPC_STATE // APC 相关
+0x04c ContextSwitches : Uint4B
+0x050 IdleSwapBlock : UChar
+0x051 Spare0 : [3] UChar
+0x054 WaitStatus : Int4B
+0x058 WaitIrql : UChar
+0x059 WaitMode : Char
+0x05a WaitNext : UChar
+0x05b WaitReason : UChar
+0x05c WaitBlockList : Ptr32 _KWAIT_BLOCK
+0x060 WaitListEntry : _LIST_ENTRY // 等待链表 / 调度链表节点(详见后续文章)
+0x060 SwapListEntry : _SINGLE_LIST_ENTRY
+0x068 WaitTime : Uint4B
+0x06c BasePriority : Char // 初始值来自 KPROCESS.BasePriority
// 可通过 KeSetBasePriorityThread 重新设定
+0x06d DecrementCount : UChar
+0x06e PriorityDecrement : Char
+0x06f Quantum : Char
+0x070 WaitBlock : [4] _KWAIT_BLOCK // 等待对象(WaitForSingleObject 使用)
+0x0d0 LegoData : Ptr32 Void
+0x0d4 KernelApcDisable : Uint4B
+0x0d8 UserAffinity : Uint4B
+0x0dc SystemAffinityActive : UChar
+0x0dd PowerState : UChar
+0x0de NpxIrql : UChar
+0x0df InitialNode : UChar
+0x0e0 ServiceTable : Ptr32 Void // 指向系统服务表基址
+0x0e4 Queue : Ptr32 _KQUEUE
+0x0e8 ApcQueueLock : Uint4B // APC 相关
+0x0f0 Timer : _KTIMER
+0x118 QueueListEntry : _LIST_ENTRY
+0x120 SoftAffinity : Uint4B
+0x124 Affinity : Uint4B
+0x128 Preempted : UChar
+0x129 ProcessReadyQueue : UChar
+0x12a KernelStackResident : UChar
+0x12b NextProcessor : UChar
+0x12c CallbackStack : Ptr32 Void
+0x130 Win32Thread : Ptr32 Void
+0x134 TrapFrame : Ptr32 _KTRAP_FRAME // 进 0 环时保存的寄存器现场
+0x138 ApcStatePointer : [2] Ptr32 _KAPC_STATE // APC 相关
+0x140 PreviousMode : Char // 先前模式:某些内核函数据此判断调用来源是 3 环还是 0 环
+0x141 EnableStackSwap : UChar
+0x142 LargeStack : UChar
+0x143 ResourceIndex : UChar
+0x144 KernelTime : Uint4B
+0x148 UserTime : Uint4B
+0x14c SavedApcState : _KAPC_STATE // APC 相关
+0x164 Alertable : UChar
+0x165 ApcStateIndex : UChar
+0x166 ApcQueueable : UChar
+0x167 AutoAlignment : UChar
+0x168 StackBase : Ptr32 Void
+0x16c SuspendApc : _KAPC
+0x19c SuspendSemaphore : _KSEMAPHORE
+0x1b0 ThreadListEntry : _LIST_ENTRY // 双向链表,串联同一进程的所有线程
+0x1b8 FreezeCount : Char
+0x1b9 SuspendCount : Char
+0x1ba IdealProcessor : UChar
+0x1bb DisableBoost : UChar

# 练习:使用 WinDbg 断开进程的某个线程

思考

  1. 断链后,在调试器(如 OllyDbg)中还能不能找到被断开的线程?
  2. 断链后,进程运行过程中会不会出现异常?

# 实验步骤

一、 编译以下代码,程序包含主线程和子线程,两者分别每 5 秒打印一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <windows.h>

DWORD WINAPI child_thread_func(LPVOID lpParam) {
while (1) {
printf("Child Thread\n");
Sleep(5000);
}
return 0;
}

int main() {
HANDLE child_thread;

child_thread = CreateThread(
NULL, // 默认安全属性
0, // 默认堆栈大小
child_thread_func, // 线程函数
NULL, // 传递给线程函数的参数
0, // 默认创建标志
NULL // 不需要线程 ID
);

if (child_thread == NULL) {
printf("Failed to create thread! Error: %d\n", GetLastError());
return 1;
}

while (1) {
printf("Main Thread\n");
Sleep(5000);
}

CloseHandle(child_thread);
return 0;
}

二、 使用 OllyDbg 运行程序,查看线程列表,可以看到与任务管理器中显示的线程数相同。

运行效果:

三、 在 WinDbg 中使用 !process 0 0 找到该程序的进程。

四、 查看进程结构体。由于 KPROCESSEPROCESS 的第一个成员,两者使用同一个地址。

1
2
3
4
5
6
7
8
kd> dt _EPROCESS 85eaa4b0
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
...
+0x174 ImageFileName : [16] "test.exe"
...
+0x190 ThreadListHead : _LIST_ENTRY [ 0x8604c404 - 0x85ec724c ]
...
1
2
3
4
5
kd> dt _KPROCESS 85eaa4b0
nt!_KPROCESS
...
+0x050 ThreadListHead : _LIST_ENTRY [ 0x8604c388 - 0x85ec71d0 ]
...

五、 逐一检查线程节点,定位子线程。

通过 KPROCESS.ThreadListHead 遍历(节点位于 KTHREAD+0x1b0 ):

1
2
3
4
kd> dt _CLIENT_ID 0x8604c388-1b0+1ec
nt!_CLIENT_ID
+0x000 UniqueProcess : 0x000001a4 Void
+0x004 UniqueThread : 0x00000700 Void // 主线程
1
2
3
4
kd> dt _CLIENT_ID 0x85ec71d0-1b0+1ec
nt!_CLIENT_ID
+0x000 UniqueProcess : 0x000001a4 Void
+0x004 UniqueThread : 0x000006d8 Void // 子线程

通过 EPROCESS.ThreadListHead 遍历(节点位于 ETHREAD+0x22c ),同样可以定位到 TID 0x700 (主线程)和 0x6d8 (子线程)。

六、 将子线程从两条链表中断链。

1
2
3
4
5
6
7
// 断开 KPROCESS.ThreadListHead 链表中的子线程节点
kd> ed (0x85eaa500+4) 0x8604c388
kd> ed 0x8604c388 0x85eaa500

// 断开 EPROCESS.ThreadListHead 链表中的子线程节点
kd> ed (0x85eaa640+4) 0x8604c404
kd> ed 0x8604c404 0x85eaa640

七、 恢复系统运行,可以发现任务管理器中线程数由 2 变为 1,但 OllyDbg 中仍然显示两个线程。

八、 查看程序运行情况,子线程仍在正常打印。

# 实验结论

  1. 任务管理器通过 ThreadListHead 链表判断线程数量,断链后线程 "消失"
  2. 系统调度线程并不依赖 ThreadListHead ,因此断链不影响线程的实际运行
  3. OllyDbg 如果在断链前已经附加到进程,它会保留之前获取的线程信息,仍能看到两个线程

补充说明

如果对线程进行断链,然后使用 OllyDbg 通过附加的方式调试,此时只能观察到一个线程。此外,使用 OllyDbg 暂停程序时,由于子线程不在链表中,不会被暂停,子线程将继续运行。


# KPCR(Processor Control Region,CPU 控制区)

  1. 当线程进入 0 环时, FS:[0] 指向 KPCR(3 环时指向 TEB)
  2. 每个 CPU 都有一个 KPCR 结构体(一个核对应一个 KPCR
  3. KPCR 中存储了 CPU 运行所需的重要数据:GDT、IDT 以及线程相关信息

dt _KPCR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
nt!_KPCR
+0x000 NtTib : _NT_TIB // 线程信息块(详见下文)
+0x01c SelfPcr : Ptr32 _KPCR // 指向自身,方便寻址
+0x020 Prcb : Ptr32 _KPRCB // 指向扩展结构体 KPRCB(即 PrcbData)
+0x024 Irql : UChar
+0x028 IRR : Uint4B
+0x02c IrrActive : Uint4B
+0x030 IDR : Uint4B
+0x034 KdVersionBlock : Ptr32 Void
+0x038 IDT : Ptr32 _KIDTENTRY // IDT 表基址
+0x03c GDT : Ptr32 _KGDTENTRY // GDT 表基址
+0x040 TSS : Ptr32 _KTSS // TSS 指针,每个 CPU 有一个 TSS
+0x044 MajorVersion : Uint2B
+0x046 MinorVersion : Uint2B
+0x048 SetMember : Uint4B
+0x04c StallScaleFactor : Uint4B
+0x050 DebugActive : UChar
+0x051 Number : UChar // CPU 编号(0、1、2...)
+0x052 Spare0 : UChar
+0x053 SecondLevelCacheAssociativity : UChar
+0x054 VdmAlert : Uint4B
+0x058 KernelReserved : [14] Uint4B
+0x090 SecondLevelCacheSize : Uint4B
+0x094 HalReserved : [16] Uint4B
+0x0d4 InterruptMode : Uint4B
+0x0d8 Spare1 : UChar
+0x0dc KernelReserved2 : [17] Uint4B
+0x120 PrcbData : _KPRCB // 扩展结构体

# +0x000 NtTib : _NT_TIB

_NT_TIB (NT Thread Information Block)用于存储线程的异常处理链、TLS 和用户模式栈信息。

1
2
3
4
5
6
7
8
9
10
11
12
nt!_NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
// 异常链表:
// 3 环中存储所有异常处理函数
// 0 环中同样有自己的异常处理函数链表
+0x004 StackBase : Ptr32 Void // 当前线程栈基址
+0x008 StackLimit : Ptr32 Void // 当前线程栈界限
+0x00c SubSystemTib : Ptr32 Void
+0x010 FiberData : Ptr32 Void
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32 Void
+0x018 Self : Ptr32 _NT_TIB // 指向自身,方便定位

# +0x120 PrcbData : _KPRCB

_KPRCB (Kernel Processor Control Block,内核处理器控制块)用于管理每个处理器核心的状态和上下文,包含调度、中断、性能计数等信息。

dt _KPRCB(仅列出关键字段)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
nt!_KPRCB
+0x000 MinorVersion : Uint2B
+0x002 MajorVersion : Uint2B
+0x004 CurrentThread : Ptr32 _KTHREAD // 当前 CPU 正在执行的线程
+0x008 NextThread : Ptr32 _KTHREAD // 即将切换到的线程
+0x00c IdleThread : Ptr32 _KTHREAD // 无线程需要调度时执行的空闲线程
+0x010 Number : Char
+0x014 SetMember : Uint4B
+0x018 CpuType : Char
+0x01c ProcessorState : _KPROCESSOR_STATE
+0x4a0 NpxThread : Ptr32 _KTHREAD
+0x4a4 InterruptCount : Uint4B
+0x4a8 KernelTime : Uint4B
+0x4ac UserTime : Uint4B
+0x4fc KeContextSwitches : Uint4B // 上下文切换计数
+0x860 DpcListHead : _LIST_ENTRY
+0x868 DpcStack : Ptr32 Void
+0x88c QuantumEnd : Uint4B // 标记 CPU 时间片是否到期
...

其中 CurrentThreadNextThreadIdleThread 三个字段是线程调度的核心:

  • CurrentThread :当前正在此 CPU 上运行的线程
  • NextThread :已被选定、即将切换到的线程(备用线程)
  • IdleThread :没有就绪线程可调度时,CPU 执行的空闲线程