进程与线程

windows进程与线程的底层实现

更多信息参见《Windows内核原理与实现》第三章。

特别强调:

  • 笔记中的内容,与《内核情景分析》《Windows内核原理与实现》均有不同。
  • ReactOS是开源免费的Windows NT系列(含NT4.0/2000/XP/2003)克隆操作系统
  • WRK 是微软针对教育和学术界开放的 Windows 内核的部分源码
  • 而笔记是基于Windows XP SP2/SP3 二进制文件.
  • 微软并不开源,很多内核成员的作用需要自己去分析.

整个系统结构:

image-20210909142442752

image-20210909142343552

执行体只维护属性,内核只根据属性做事,内核层比执行体更接近底层

  • Ps开头的基本是执行体函数
  • KE,Ki开头函数开头的函数都是内核函数(一般KE是导出函数,Ki是不导出函数)
  • Nt开头的函数还是ntdll.dll那层

Windows内核重要变量

进程结构体

进程结构体_EPROCESS

每个windows进程在0环都有一个对应的结构体:_EPROCESS这个结构体包含了进程所有重要的信息。(PEB是三环关于进程信息的另一个结构体)

_EPROCESS结构体中的成员微软并没有公布作用。(靠逆向分析出来的)

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
kd> dt _eprocess
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS//_KPROCESS结构体
+0x06c ProcessLock : _EX_PUSH_LOCK
+0x070 CreateTime : _LARGE_INTEGER//进程的创建时间
+0x078 ExitTime : _LARGE_INTEGER//进程的退出时间
+0x080 RundownProtect : _EX_RUNDOWN_REF
+0x084 UniqueProcessId : Ptr32 Void//进程的编号,任务管理器中的PID
+0x088 ActiveProcessLinks : _LIST_ENTRY//双向链表,所有的活动进程都链接在一起构成的链表,全局变量PsActiveProcessHead指向该链表的头
+0x090 QuotaUsage : [3] Uint4B//物理页相关的统计信息
+0x09c QuotaPeak : [3] Uint4B//物理页相关的统计信息
+0x0a8 CommitCharge : Uint4B//虚拟内存相关的统计信息
+0x0ac PeakVirtualSize : Uint4B//虚拟内存相关的统计信息
+0x0b0 VirtualSize : Uint4B//虚拟内存相关的统计信息
+0x0b4 SessionProcessLinks : _LIST_ENTRY
+0x0bc DebugPort : Ptr32 Void//调试相关,是被调试进程与调试进程的通行的桥梁,与反调试的DebugPort清零相关。
+0x0c0 ExceptionPort : Ptr32 Void//调试相关
+0x0c4 ObjectTable : Ptr32 _HANDLE_TABLE//句柄表,反调试也会用到这个句柄表,遍历系统内所有其他进程句柄表查是否存在自己的句柄
+0x0c8 Token : _EX_FAST_REF//安全相关的属性
+0x0cc WorkingSetLock : _FAST_MUTEX
+0x0ec WorkingSetPage : Uint4B
+0x0f0 AddressCreationLock : _FAST_MUTEX
+0x110 HyperSpaceLock : Uint4B
+0x114 ForkInProgress : Ptr32 _ETHREAD
+0x118 HardwareTrigger : Uint4B
+0x11c VadRoot : Ptr32 Void//非常重要,指向了一棵平衡二叉树,标识0~2G哪些虚拟地址分配了,与模块隐藏有很大关系
+0x120 VadHint : Ptr32 Void
+0x124 CloneRoot : Ptr32 Void//已废弃
+0x128 NumberOfPrivatePages : Uint4B
+0x12c NumberOfLockedPages : Uint4B
+0x130 Win32Process : Ptr32 Void
+0x134 Job : Ptr32 _EJOB
+0x138 SectionObject : Ptr32 Void
+0x13c SectionBaseAddress : Ptr32 Void
+0x140 QuotaBlock : Ptr32 _EPROCESS_QUOTA_BLOCK
+0x144 WorkingSetWatch : Ptr32 _PAGEFAULT_HISTORY
+0x148 Win32WindowStation : Ptr32 Void
+0x14c InheritedFromUniqueProcessId : Ptr32 Void//父进程的pid
+0x150 LdtInformation : Ptr32 Void
+0x154 VadFreeHint : Ptr32 Void
+0x158 VdmObjects : Ptr32 Void
+0x15c DeviceMap : Ptr32 Void
+0x160 PhysicalVadList : _LIST_ENTRY
+0x168 PageDirectoryPte : _HARDWARE_PTE_X86
+0x168 Filler : Uint8B
+0x170 Session : Ptr32 Void
+0x174 ImageFileName : [16] UChar//当前进程的名字
+0x184 JobLinks : _LIST_ENTRY
+0x18c LockedPagesList : Ptr32 Void
+0x190 ThreadListHead : _LIST_ENTRY//8字节,一个进程下的所有线程组成的双向链表
+0x198 SecurityPort : Ptr32 Void
+0x19c PaeTop : Ptr32 Void
+0x1a0 ActiveThreads : Uint4B//当前进程中活动线程的数量
+0x1a4 GrantedAccess : Uint4B
+0x1a8 DefaultHardErrorProcessing : Uint4B
+0x1ac LastThreadExitStatus : Int4B
+0x1b0 Peb : Ptr32 _PEB//PEB结构体,三环存进程信息的结构体
+0x1b4 PrefetchTrace : _EX_FAST_REF
+0x1b8 ReadOperationCount : _LARGE_INTEGER//调用Read文件的次数
+0x1c0 WriteOperationCount : _LARGE_INTEGER//调用Write文件的次数
+0x1c8 OtherOperationCount : _LARGE_INTEGER//调用其他IO函数的次数
+0x1d0 ReadTransferCount : _LARGE_INTEGER
+0x1d8 WriteTransferCount : _LARGE_INTEGER
+0x1e0 OtherTransferCount : _LARGE_INTEGER
+0x1e8 CommitChargeLimit : Uint4B
+0x1ec CommitChargePeak : Uint4B
+0x1f0 AweInfo : Ptr32 Void
+0x1f4 SeAuditProcessCreationInfo : _SE_AUDIT_PROCESS_CREATION_INFO//存着进程全路径名称的信息,结构内部如下注释
/*
kd> dt _SE_AUDIT_PROCESS_CREATION_INFO(内嵌结构)
nt!_SE_AUDIT_PROCESS_CREATION_INFO
+0x000 ImageFileName : Ptr32 _OBJECT_NAME_INFORMATION
kd> dt _OBJECT_NAME_INFORMATION(指针)
nt!_OBJECT_NAME_INFORMATION
+0x000 Name : _UNICODE_STRING//全路径名称
*/
+0x1f8 Vm : _MMSUPPORT
+0x238 LastFaultCount : Uint4B
+0x23c ModifiedPageCount : Uint4B
+0x240 NumberOfVads : Uint4B
+0x244 JobStatus : Uint4B
+0x248 Flags : Uint4B//死不死活不活都是他
+0x248 CreateReported : Pos 0, 1 Bit
+0x248 NoDebugInherit : Pos 1, 1 Bit
+0x248 ProcessExiting : Pos 2, 1 Bit
+0x248 ProcessDelete : Pos 3, 1 Bit
+0x248 Wow64SplitPages : Pos 4, 1 Bit
+0x248 VmDeleted : Pos 5, 1 Bit
+0x248 OutswapEnabled : Pos 6, 1 Bit
+0x248 Outswapped : Pos 7, 1 Bit
+0x248 ForkFailed : Pos 8, 1 Bit
+0x248 HasPhysicalVad : Pos 9, 1 Bit
+0x248 AddressSpaceInitialized : Pos 10, 2 Bits
+0x248 SetTimerResolution : Pos 12, 1 Bit
+0x248 BreakOnTermination : Pos 13, 1 Bit
+0x248 SessionCreationUnderway : Pos 14, 1 Bit
+0x248 WriteWatch : Pos 15, 1 Bit
+0x248 ProcessInSession : Pos 16, 1 Bit
+0x248 OverrideAddressSpace : Pos 17, 1 Bit
+0x248 HasAddressSpace : Pos 18, 1 Bit
+0x248 LaunchPrefetched : Pos 19, 1 Bit
+0x248 InjectInpageErrors : Pos 20, 1 Bit
+0x248 VmTopDown : Pos 21, 1 Bit
+0x248 Unused3 : Pos 22, 1 Bit
+0x248 Unused4 : Pos 23, 1 Bit
+0x248 VdmAllowed : Pos 24, 1 Bit
+0x248 Unused : Pos 25, 5 Bits
+0x248 Unused1 : Pos 30, 1 Bit
+0x248 Unused2 : Pos 31, 1 Bit
+0x24c ExitStatus : Int4B//进程死了之后,在内核还会存在一段时间,该成员可获取退出状态
+0x250 NextPageColor : Uint2B
+0x252 SubSystemMinorVersion : UChar//子系统版本
+0x253 SubSystemMajorVersion : UChar//子系统版本
+0x252 SubSystemVersion : Uint2B//子系统版本
+0x254 PriorityClass : UChar
+0x255 WorkingSetAcquiredUnsafe : UChar
+0x258 Cookie : Uint4B

_EPROCESS结构体定义

全局变量PsActiveProcessHead指向活动进程链表头中的_EPROCESS的0x88偏移位置

PsActiveProcessHead也是两个值,下一个和前一个,两个四字节地址。

PsActiveProcessHead未导出

image-20210906162843550

任务管理器查的就是这个链表。

PEB

三环进程结构体

三环时,FS:[30]指向这个PEB结构

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
typedef struct _PEB {               // Size: 0x1D8
000h UCHAR InheritedAddressSpace;
001h UCHAR ReadImageFileExecOptions;
//下面参数标识当前进程是否处于调试状态,Kernel32.dll中的IsDebuggerPresent() API就是用来获取该处的值的(是,则返回1;否,则返回0)。【破解之法】只要借助OllyDbg调试器的编辑功能,将PEB.BeingDebugged的值修改为0(FALSE)即可。
002h UCHAR BeingDebugged; //Debug运行标志(可用于反调试技术),只要该进程被调试就会被置1
003h UCHAR SpareBool;
004h HANDLE Mutant;
008h HINSTANCE ImageBaseAddress; //程序加载的基地址
00Ch struct _PEB_LDR_DATA *Ldr //Ptr32 指向_PEB_LDR_DATA结构体,记录了程序包含哪些模块(可用于反调试技术)
010h struct _RTL_USER_PROCESS_PARAMETERS *ProcessParameters;
014h ULONG SubSystemData;
018h HANDLE DefaultHeap; //(可用于反调试技术)
01Ch KSPIN_LOCK FastPebLock;
020h ULONG FastPebLockRoutine;
024h ULONG FastPebUnlockRoutine;
028h ULONG EnvironmentUpdateCount;
02Ch ULONG KernelCallbackTable;
030h LARGE_INTEGER SystemReserved;
038h struct _PEB_FREE_BLOCK *FreeList
03Ch ULONG TlsExpansionCounter;
040h ULONG TlsBitmap;
044h LARGE_INTEGER TlsBitmapBits;
04Ch ULONG ReadOnlySharedMemoryBase;
050h ULONG ReadOnlySharedMemoryHeap;
054h ULONG ReadOnlyStaticServerData;
058h ULONG AnsiCodePageData;
05Ch ULONG OemCodePageData;
060h ULONG UnicodeCaseTableData;
064h ULONG NumberOfProcessors;
068h LARGE_INTEGER NtGlobalFlag; // Address of a local copy(可用于反调试技术)
070h LARGE_INTEGER CriticalSectionTimeout;
078h ULONG HeapSegmentReserve;
07Ch ULONG HeapSegmentCommit;
080h ULONG HeapDeCommitTotalFreeThreshold;
084h ULONG HeapDeCommitFreeBlockThreshold;
088h ULONG NumberOfHeaps;
08Ch ULONG MaximumNumberOfHeaps;
090h ULONG ProcessHeaps;
094h ULONG GdiSharedHandleTable;
098h ULONG ProcessStarterHelper;
09Ch ULONG GdiDCAttributeList;
0A0h KSPIN_LOCK LoaderLock;
0A4h ULONG OSMajorVersion;
0A8h ULONG OSMinorVersion;
0ACh USHORT OSBuildNumber;
0AEh USHORT OSCSDVersion;
0B0h ULONG OSPlatformId;
0B4h ULONG ImageSubsystem;
0B8h ULONG ImageSubsystemMajorVersion;
0BCh ULONG ImageSubsystemMinorVersion;
0C0h ULONG ImageProcessAffinityMask;
0C4h ULONG GdiHandleBuffer[0x22];
14Ch ULONG PostProcessInitRoutine;
150h ULONG TlsExpansionBitmap;
154h UCHAR TlsExpansionBitmapBits[0x80];
1D4h ULONG SessionId;
} PEB, *PPEB;

PEB更多信息参见[[windows开发#模块隐藏|windows开发的模块隐藏章节]]

_KRPOCESS结构体

这个就是常说的进程PCB结构体

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
kd> dt _KPROCESS
ntdll!_KPROCESS
+0x000 Header : _DISPATCHER_HEADER//只要0环对象是以这个开头的,就是“可等待”对象,比如Mutex互斥体,Event事件等(WaitForSingleObject)
/*kd> dt _DISPATCHER_HEADER
nt!_DISPATCHER_HEADER
+0x000 Type : UChar
+0x001 Absolute : UChar
+0x002 Size : UChar
+0x003 Inserted : UChar
+0x004 SignalState : Int4B
+0x008 WaitListHead : _LIST_ENTRY//双向链表
*/
+0x010 ProfileListHead : _LIST_ENTRY//性能分析相关,把自己的进程插入这个链表系统就可以帮你监视性能了(一般自动帮你插了),性能监视器中可以查看
+0x018 DirectoryTableBase : [2] Uint4B//页目录表基址(最重要),第0个就是最终添加到CR3里的值,第1个是如果超过4GB物理内存的CR3
+0x020 LdtDescriptor : _KGDTENTRY//历史遗留,16位Windows 段选择子不够 每个进程都有一个LDT表
+0x028 Int21Descriptor : _KIDTENTRY//DOS下用的中断
+0x030 IopmOffset : Uint2B
+0x032 Iopl : UChar
+0x033 Unused : UChar
+0x034 ActiveProcessors : Uint4B
+0x038 KernelTime : Uint4B//统计信息,记录了一个进程在用户模式下所花的时间
+0x03c UserTime : Uint4B//统计信息,记录了一个进程在内核模式下所花的时间
+0x040 ReadyListHead : _LIST_ENTRY//当前进程所有就绪状态的线程的双向链表
+0x048 SwapListEntry : _SINGLE_LIST_ENTRY//当前进程被交换到硬盘上的链表
+0x04c VdmTrapcHandler : Ptr32 Void
+0x050 ThreadListHead : _LIST_ENTRY//8字节,一个进程下的所有线程组成的双向链表
+0x058 ProcessLock : Uint4B//进程锁,拿这个值调API,防止你修改的时候和操作系统同时改冲突。
+0x05c Affinity : Uint4B//规定进程里面的所有线程偏好哪些CPU核心上跑(偏好)
//如果值为1,那么这个进程的所有线程在0号CPU跑(00000001)
//如果值为3,那么这个进程的所有线程只能在0,1号CPU跑(00000011)
//如果值为4,那么这个进程的所有线程只能在2号CPU跑(00000100)
//如果值为5,那么这个进程的所有线程只能在0,2号CPU跑(00000101)
//所以32位系统最多32核,64位系统最多64核
+0x060 StackCount : Uint2B//记录了当前进程中有多少个线程的栈位于内存中
+0x062 BasePriority : Char//基础优先级或最低优先级,该进程中的所有线程最起码的优先级,创建线程的最起始的优先级都是这个
+0x063 ThreadQuantum : Char//进程下线程默认的时间片
+0x064 AutoAlignment : UChar//自动对齐(已废弃)
+0x065 State : UChar//描述硬盘交换的状态,0表示未换(即在内存中),还有正在换中和已经换到硬盘中了等情况
+0x066 ThreadSeed : UChar//规定进程里面的所有线程偏好在哪个CPU核心上跑
+0x067 DisableBoost : UChar
+0x068 PowerState : UChar
+0x069 DisableQuantum : UChar
+0x06a IdealNode : UChar
+0x06b Flags : _KEXECUTE_OPTIONS
+0x06b ExecuteOptions : UChar

实验

1、用OD附加记事本,然后查看记事本进程的PEB的BeingDebugged成员的值(注意:OD不要使用插件)

OD附加前:image-20210909171530578

OD附加后:image-20210909171108678

2、用OD附加记事本,下断点,然后在windbg中清空EPROCESS中的DebugPort中的值,然后单步调试,观察结果.

修改前:image-20210909165418505

修改后:image-20210909165445414

单步调试触发:

image-20210909165516800

单步调试直接触发异常,OD卡死

进程断链实现任务管理器隐藏

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
//目标进程断链函数
BOOLEAN CutProcessInLink(char* name)
{
ULONG curProcess;
__asm
{
mov eax, dword ptr fs : [0x124] ;
mov ecx, [eax + 0x44];
mov curProcess, ecx;
}
PLIST_ENTRY plistProcess = (PLIST_ENTRY)(curProcess + 0x88);
BOOLEAN isFlags = FALSE;
while (plistProcess->Flink != (PLIST_ENTRY)(curProcess + 0x88))
{
ULONG nextProcess = ((ULONG)(plistProcess)) - 0x88;
plistProcess = plistProcess->Flink;
//KdPrint(("%s\n", (PCHAR)(nextProcess + 0x174)));
if (strcmp(name, (PCHAR)(nextProcess + 0x174)) == 0)
{
curProcess = nextProcess;//找到目标进程
//执行断链操作
PLIST_ENTRY curProcessLink = (PLIST_ENTRY)(curProcess + 0x88);
curProcessLink->Blink->Flink = curProcessLink->Flink;
curProcessLink->Flink->Blink = curProcessLink->Blink;
isFlags = TRUE;
break;

}
}
if (!isFlags)
{
return FALSE;
}
return TRUE;
}

断链前:image-20210909173755007

锻炼后:image-20210909173913107

但是这里还是能看到:

image-20210909173241836

线程结构体

image-20210831191949142

线程结构体_ETHREAD

每个windows线程在0环都有一个对应的结构体:_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
kd> dt _ETHREAD
nt!_ETHREAD
+0x000 Tcb : _KTHREAD//_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//8字节,两个内容,分别是线程所属进程id和线程id
+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//指向创建自己的进程的_EPROCESS结构体
+0x224 StartAddress : Ptr32 Void//线程起始函数地址
+0x228 Win32StartAddress : Ptr32 Void
+0x228 LpcReceivedMessageId : Uint4B
+0x22c ThreadListEntry : _LIST_ENTRY//双向链表 一个进程所有的线程 都挂在一个链表中 挂的就是这个位置,一共有两个这样的链表,8字节
+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

_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
81
82
83
84
85
86
87
kd> dt _KTHREAD
nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER//“可等待对象”
+0x010 MutantListHead : _LIST_ENTRY//互斥体链表
+0x018 InitialStack : Ptr32 Void//当前堆栈顶起始
+0x01c StackLimit : Ptr32 Void//当前堆栈边界
+0x020 Teb : Ptr32 Void//TEB,Thread Environment Block,线程环境块。大小4KB,位于用户地址空间。三环时FS:[0]指向TEB。
+0x024 TlsArray : Ptr32 Void
+0x028 KernelStack : Ptr32 Void//0环堆栈顶,哪个线程正在跑就把该零环堆栈的地址放到TSS中的esp0中。
+0x02c DebugActive : UChar//调试激活,如果值为-1,不能使用调试寄存器:Dr0~Dr7,硬件断点相关
+0x02d State : UChar//线程状态:就绪,等待还是运行
+0x02e Alerted : [2] UChar//正常情况下,用户模式的APC是不会打断用户态程序的执行流的。除非,线程是Alertable——可唤醒的。
+0x030 Iopl : UChar
+0x031 NpxState : UChar
+0x032 Saturation : Char
+0x033 Priority : Char//线程的优先级?
+0x034 ApcState : _KAPC_STATE//APC相关
/*
kd> dt _KAPC_STATE
ntdll!_KAPC_STATE
+0x000 ApcListHead : [2] _LIST_ENTRY//两个APC队列
+0x010 Process : Ptr32 _KPROCESS//0x44偏移位置是该线程所在的进程的_EPROCESS结构
+0x014 KernelApcInProgress : UChar
+0x015 KernelApcPending : UChar
+0x016 UserApcPending : UChar


*/
+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//当线程处于等待状态的时候指向等待链表(8字节)
+0x060 SwapListEntry : _SINGLE_LIST_ENTRY//等待线程的内存交换到硬盘的信息记录在该链表(8字节)
+0x068 WaitTime : Uint4B
+0x06c BasePriority : Char//优先级,其初始值是所属进程的BasePriority值(KPROCESS->BasePriority),以后可以通过KeSetBasePriorityThread()函数重新设定
+0x06d DecrementCount : UChar
+0x06e PriorityDecrement : Char
+0x06f Quantum : Char//当前线程剩余的CPU时间片
+0x070 WaitBlock : [4] _KWAIT_BLOCK//等待哪个对象
+0x0d0 LegoData : Ptr32 Void
+0x0d4 KernelApcDisable : Uint4B//内核apc是否可执行
+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//先前模式,某些内核函数会判断程序是0环调用还是3环调用。0代表调用来自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//表示是否可以向线程的APC队列中插入APC
+0x167 AutoAlignment : UChar
+0x168 StackBase : Ptr32 Void
+0x16c SuspendApc : _KAPC
+0x19c SuspendSemaphore : _KSEMAPHORE
+0x1b0 ThreadListEntry : _LIST_ENTRY//双向链表,一个进程所有的线程都挂在一个链表中,挂的就是这个位置,一共有两个这样的链表,8字节
+0x1b8 FreezeCount : Char
+0x1b9 SuspendCount : Char
+0x1ba IdealProcessor : UChar
+0x1bb DisableBoost : UChar

0x1b0:ThreadListEntry,一个进程所有的线程都挂在一个链表中,一共有两个这样的链表

image-20210907153725611

  • 线程:上面的圈在_KTHREAD中,下面的圈在_ETHREAD中
  • 进程:上面的圈在_KPROCESS中,下面的圈在_EPROCESS中

思考

将线程链中的某个线程断了,如果线程断了还是否可以执行,是不是在od就看不到该线程了?

线程和进程实际上是无法真正隐藏的

逆PspCreateProcess大致思路(暂时无法自己逆向,基础铺垫不够)

1
2
3
4
5
6
7
8
9
10
11
12
//3环
CreateProcessA
->CreateProcessInternalA
->CreateProcessInternalW
->NtCreateProcessEx
->BasePushProcessParameters
->NtCreateThread
->NtResumeThread
->执行代码OEP
//0环
ZwCreateProcess/NtCreateProcess
->PspCreateProcess

三环线程结构体_TEB

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
kd> dt _TEB
nt!_TEB
+0x000 NtTib : _NT_TIB//_NT_TIB结构体,_KPCR结构体的第一个成员也是_NT_TIB结构体
+0x01c EnvironmentPointer : Ptr32 Void
+0x020 ClientId : _CLIENT_ID
+0x028 ActiveRpcHandle : Ptr32 Void
+0x02c ThreadLocalStoragePointer : Ptr32 Void
+0x030 ProcessEnvironmentBlock : Ptr32 _PEB
+0x034 LastErrorValue : Uint4B
+0x038 CountOfOwnedCriticalSections : Uint4B
+0x03c CsrClientThread : Ptr32 Void
+0x040 Win32ThreadInfo : Ptr32 Void
+0x044 User32Reserved : [26] Uint4B
+0x0ac UserReserved : [5] Uint4B
+0x0c0 WOW32Reserved : Ptr32 Void
+0x0c4 CurrentLocale : Uint4B
+0x0c8 FpSoftwareStatusRegister : Uint4B
+0x0cc SystemReserved1 : [54] Ptr32 Void
+0x1a4 ExceptionCode : Int4B
+0x1a8 ActivationContextStack : _ACTIVATION_CONTEXT_STACK
+0x1bc SpareBytes1 : [24] UChar
+0x1d4 GdiTebBatch : _GDI_TEB_BATCH
+0x6b4 RealClientId : _CLIENT_ID
+0x6bc GdiCachedProcessHandle : Ptr32 Void
+0x6c0 GdiClientPID : Uint4B
+0x6c4 GdiClientTID : Uint4B
+0x6c8 GdiThreadLocalInfo : Ptr32 Void
+0x6cc Win32ClientInfo : [62] Uint4B
+0x7c4 glDispatchTable : [233] Ptr32 Void
+0xb68 glReserved1 : [29] Uint4B
+0xbdc glReserved2 : Ptr32 Void
+0xbe0 glSectionInfo : Ptr32 Void
+0xbe4 glSection : Ptr32 Void
+0xbe8 glTable : Ptr32 Void
+0xbec glCurrentRC : Ptr32 Void
+0xbf0 glContext : Ptr32 Void
+0xbf4 LastStatusValue : Uint4B
+0xbf8 StaticUnicodeString : _UNICODE_STRING
+0xc00 StaticUnicodeBuffer : [261] Uint2B
+0xe0c DeallocationStack : Ptr32 Void
+0xe10 TlsSlots : [64] Ptr32 Void
+0xf10 TlsLinks : _LIST_ENTRY
+0xf18 Vdm : Ptr32 Void
+0xf1c ReservedForNtRpc : Ptr32 Void
+0xf20 DbgSsReserved : [2] Ptr32 Void
+0xf28 HardErrorsAreDisabled : Uint4B
+0xf2c Instrumentation : [16] Ptr32 Void
+0xf6c WinSockData : Ptr32 Void
+0xf70 GdiBatchCount : Uint4B
+0xf74 InDbgPrint : UChar
+0xf75 FreeStackOnTermination : UChar
+0xf76 HasFiberData : UChar
+0xf77 IdealProcessor : UChar
+0xf78 Spare3 : Uint4B
+0xf7c ReservedForPerf : Ptr32 Void
+0xf80 ReservedForOle : Ptr32 Void
+0xf84 WaitingOnLoaderLock : Uint4B
+0xf88 Wx86Thread : _Wx86ThreadState
+0xf94 TlsExpansionSlots : Ptr32 Ptr32 Void
+0xf98 ImpersonationLocale : Uint4B
+0xf9c IsImpersonating : Uint4B
+0xfa0 NlsCache : Ptr32 Void
+0xfa4 pShimData : Ptr32 Void
+0xfa8 HeapVirtualAffinity : Uint4B
+0xfac CurrentTransactionHandle : Ptr32 Void
+0xfb0 ActiveFrame : Ptr32 _TEB_ACTIVE_FRAME
+0xfb4 SafeThunkCall : UChar
+0xfb5 BooleanSpare : [3] UChar

CPU结构体

image-20210831191911557

_KPCR结构体

KPCR:CPU控制区(Processor Control Region)

CPU在内核中的结构体

_KPCR介绍

  1. 当线程进入0环时,FS:[0]指向KPCR
  2. 每个CPU核心都有一个KPCR结构体
  3. KPCR中存储了CPU本身要用的一些重要数据:GDT,IDT以及线程相关的一些信息。
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
kd> dt _KPCR
nt!_KPCR
+0x000 NtTib : _NT_TIB//_NT_TIB结构体,_TEB结构体第一项成员也是_NT_TIB结构体
+0x01c SelfPcr : Ptr32 _KPCR//指向当前_KPCR结构体的指针
+0x020 Prcb : Ptr32 _KPRCB//_KPRCB结构体指针(其实就是存了_KPCR中0x120偏移的_KPRCB结构体首地址)
+0x024 Irql : UChar
+0x028 IRR : Uint4B
+0x02c IrrActive : Uint4B
+0x030 IDR : Uint4B
+0x034 KdVersionBlock : Ptr32 Void
+0x038 IDT : Ptr32 _KIDTENTRY//IDT表基址,每个CPU核一个IDT
+0x03c GDT : Ptr32 _KGDTENTRY//GDT表基址,每个CPU核一个GDT
+0x040 TSS : Ptr32 _KTSS//_KTSS结构体指针,指向TSS,每个CPU核一个TSS
+0x044 MajorVersion : Uint2B
+0x046 MinorVersion : Uint2B
+0x048 SetMember : Uint4B
+0x04c StallScaleFactor : Uint4B
+0x050 DebugActive : UChar//调试激活,0表示
+0x051 Number : UChar//当前CPU的编号,从0开始
+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//_KPRCB结构体,拓展结构体

_NT_TIB结构体

1
2
3
4
5
6
7
8
9
10
nt!_NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD//当前线程异常链表,KPCR存的是R0的,TEB存的是R3的
+0x004 StackBase : Ptr32 Void//栈底,KPCR存的是R0的(逆向发现存的是【KTHREAD的InitialStack的值-0x210后的值】),TEB存的是R3的
//这个0x210是_FX_SAVE_AREA结构,就是浮点寄存器,再上面部分就是_Ktrap_frame结构体
+0x008 StackLimit : Ptr32 Void//栈的边界,KPCR存的是R0的,TEB存的是R3的
+0x00c SubSystemTib : Ptr32 Void
+0x010 FiberData : Ptr32 Void
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32 Void
+0x018 Self : Ptr32 _NT_TIB//指向自己(也就是指向KPCR结构或TEB结构首地址),这样设计的目的是为了查找方便

_KPRCB结构体

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
81
82
83
84
85
86
87
88
89
90
91
92
93
kd> dt _KPRCB
nt!_KPRCB
+0x000 MinorVersion : Uint2B
+0x002 MajorVersion : Uint2B
+0x004 CurrentThread : Ptr32 _KTHREAD//当前CPU正在跑的线程,或者空闲线程函数地址?
+0x008 NextThread : Ptr32 _KTHREAD//要切换的时候,即将切换的下一个线程
+0x00c IdleThread : Ptr32 _KTHREAD//空闲线程,如果没有需要切换的就绪线程,要切换的空闲线程
+0x010 Number : Char//当前CPU的编号,从0开始
+0x011 Reserved : Char
+0x012 BuildType : Uint2B
+0x014 SetMember : Uint4B
+0x018 CpuType : Char
+0x019 CpuID : Char
+0x01a CpuStep : Uint2B
+0x01c ProcessorState : _KPROCESSOR_STATE//里面有上下文信息
+0x33c KernelReserved : [16] Uint4B
+0x37c HalReserved : [16] Uint4B
+0x3bc PrcbPad0 : [92] UChar
+0x418 LockQueue : [16] _KSPIN_LOCK_QUEUE
+0x498 PrcbPad1 : [8] UChar
+0x4a0 NpxThread : Ptr32 _KTHREAD
+0x4a4 InterruptCount : Uint4B//中断次数
+0x4a8 KernelTime : Uint4B
+0x4ac UserTime : Uint4B
+0x4b0 DpcTime : Uint4B
+0x4b4 DebugDpcTime : Uint4B
+0x4b8 InterruptTime : Uint4B
+0x4bc AdjustDpcThreshold : Uint4B
+0x4c0 PageColor : Uint4B
+0x4c4 SkipTick : Uint4B
+0x4c8 MultiThreadSetBusy : UChar
+0x4c9 Spare2 : [3] UChar
+0x4cc ParentNode : Ptr32 _KNODE
+0x4d0 MultiThreadProcessorSet : Uint4B//是否多核处理器
+0x4d4 MultiThreadSetMaster : Ptr32 _KPRCB//指向别的核的_KPRCB结构
+0x4d8 ThreadStartCount : [2] Uint4B
+0x4e0 CcFastReadNoWait : Uint4B
+0x4e4 CcFastReadWait : Uint4B
+0x4e8 CcFastReadNotPossible : Uint4B
+0x4ec CcCopyReadNoWait : Uint4B
+0x4f0 CcCopyReadWait : Uint4B
+0x4f4 CcCopyReadNoWaitMiss : Uint4B
+0x4f8 KeAlignmentFixupCount : Uint4B
+0x4fc KeContextSwitches : Uint4B
+0x500 KeDcacheFlushCount : Uint4B
+0x504 KeExceptionDispatchCount : Uint4B
+0x508 KeFirstLevelTbFills : Uint4B
+0x50c KeFloatingEmulationCount : Uint4B
+0x510 KeIcacheFlushCount : Uint4B
+0x514 KeSecondLevelTbFills : Uint4B
+0x518 KeSystemCalls : Uint4B//系统调用次数(3环进0环次数)
+0x51c SpareCounter0 : [1] Uint4B
+0x520 PPLookasideList : [16] _PP_LOOKASIDE_LIST
+0x5a0 PPNPagedLookasideList : [32] _PP_LOOKASIDE_LIST
+0x6a0 PPPagedLookasideList : [32] _PP_LOOKASIDE_LIST
+0x7a0 PacketBarrier : Uint4B
+0x7a4 ReverseStall : Uint4B
+0x7a8 IpiFrame : Ptr32 Void
+0x7ac PrcbPad2 : [52] UChar
+0x7e0 CurrentPacket : [3] Ptr32 Void
+0x7ec TargetSet : Uint4B
+0x7f0 WorkerRoutine : Ptr32 void
+0x7f4 IpiFrozen : Uint4B
+0x7f8 PrcbPad3 : [40] UChar
+0x820 RequestSummary : Uint4B
+0x824 SignalDone : Ptr32 _KPRCB
+0x828 PrcbPad4 : [56] UChar
+0x860 DpcListHead : _LIST_ENTRY//DPC链表
+0x868 DpcStack : Ptr32 Void
+0x86c DpcCount : Uint4B
+0x870 DpcQueueDepth : Uint4B
+0x874 DpcRoutineActive : Uint4B
+0x878 DpcInterruptRequested : Uint4B
+0x87c DpcLastCount : Uint4B
+0x880 DpcRequestRate : Uint4B
+0x884 MaximumDpcQueueDepth : Uint4B
+0x888 MinimumDpcRate : Uint4B
+0x88c QuantumEnd : Uint4B//标志当前在运行的CPU时间片是否用完,没有用完是0,用完就是个非零的值
+0x890 PrcbPad5 : [16] UChar
+0x8a0 DpcLock : Uint4B
+0x8a4 PrcbPad6 : [28] UChar
+0x8c0 CallDpc : _KDPC
+0x8e0 ChainedInterruptList : Ptr32 Void
+0x8e4 LookasideIrpFloat : Int4B
+0x8e8 SpareFields0 : [6] Uint4B
+0x900 VendorString : [13] UChar//处理器品牌名
+0x90d InitialApicId : UChar
+0x90e LogicalProcessorsPerPhysicalProcessor : UChar
+0x910 MHz : Uint4B
+0x914 FeatureBits : Uint4B
+0x918 UpdateSignature : _LARGE_INTEGER
+0x920 NpxSaveArea : _FX_SAVE_AREA
+0xb30 PowerState : _PROCESSOR_POWER_STATE

KiProcessorBlock全局变量(未导出)

PKPRCB KiProcessorBlock[MAXIMUM_PROCESSORS];

  • [定 义] wrk\wrk-v1.2\base\ntos\ke\kernldat.c

  • [初始化] wrk\wrk-v1.2\base\ntos\ex\obinit.c [KiSystemStartup ()]

  • [引 用]

    • KiSwapThread() 线程调度
    • KiSetAffinityThread() 设置线程亲和性
  • [描 述]

    系统核心数据结构,与线程调度密切相关。所有KPRCB的指针数组,往前拨偏移可以得到对应KPCR。MAXIMUM_PROCESSORS定义为32。而实际数组的元素个数由上面KeNumberProcessors决定,每个处理器对应一个KPRCB结构体。

指定进程名找进程结构首地址

通过cpu结构,线程结构

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
ULONG findProcess(char* name)
{
ULONG curProcess;
__asm
{
mov eax, dword ptr fs : [0x124] ;//KPCR中找当前线程结构体地址
mov ecx, [eax + 0x44];//获取线程结构体中存储的线程所属的进程结构体地址
mov curProcess, ecx;//即获取到当前进程结构体地址
}//上面三行作用等于API函数:PsGetCurrentProcess()

PLIST_ENTRY plistProcess = (PLIST_ENTRY)(curProcess + 0x88);//活动进程双向链表
BOOLEAN isFlags = FALSE;
//遍历活动进程双向链表,通过进程名字找对应进程
while (plistProcess->Flink != (PLIST_ENTRY)(curProcess + 0x88))
{
ULONG nextProcess = ((ULONG)(plistProcess)) - 0x88;
plistProcess = plistProcess->Flink;
//KdPrint(("%s\n", (PCHAR)(nextProcess + 0x174)));
if (strcmp(name, (PCHAR)(nextProcess + 0x174)) == 0)
{
curProcess = nextProcess;
isFlags = TRUE;
break;
}
}
if (!isFlags)
{
curProcess = 0;
}
return curProcess;
}

对象头结构体

每个内核对象都有对象头结构体。

windbg查看一个进程下的线程,用如下命令:

1
!process 要找的_EPROCESS结构体首地址

image-20210912105201611

_OBJECT_HEADER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kd> dt _OBJECT_HEADER
nt!_OBJECT_HEADER
+0x000 PointerCount : Int4B
+0x004 HandleCount : Int4B
+0x004 NextToFree : Ptr32 Void
+0x008 Type : Ptr32 _OBJECT_TYPE//_OBJECT_TYPE结构体
+0x00c NameInfoOffset : UChar
+0x00d HandleInfoOffset : UChar
+0x00e QuotaInfoOffset : UChar
+0x00f Flags : UChar
+0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : Ptr32 Void
+0x014 SecurityDescriptor : Ptr32 Void
+0x018 Body : _QUAD//内嵌真正的对象结构

_OBJECT_TYPE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kd> dt _OBJECT_TYPE
ntdll!_OBJECT_TYPE
+0x000 Mutex : _ERESOURCE
+0x038 TypeList : _LIST_ENTRY
+0x040 Name : _UNICODE_STRING//对象类型名,如Process,Thread
+0x048 DefaultObject : Ptr32 Void
+0x04c Index : Uint4B
+0x050 TotalNumberOfObjects : Uint4B
+0x054 TotalNumberOfHandles : Uint4B
+0x058 HighWaterNumberOfObjects : Uint4B
+0x05c HighWaterNumberOfHandles : Uint4B
+0x060 TypeInfo : _OBJECT_TYPE_INITIALIZER//内含对象的回调函数
+0x0ac Key : Uint4B
+0x0b0 ObjectLocks : [4] _ERESOURCE

实例如下

进程对象头:

image-20210912105028347

线程对象头:

image-20210912105300055

等待链表_调度链表

进程结构体EPROCESS(0x50和0x190)是2个链表,里面圈着当前进程所有的线程。

对进程断链,程序可以正常运行,原因是CPU执行与调度是基于线程的,进程断链只是影响一些遍历系统进程的API,并不会影响程序执行。

对线程断链也是一样的,断链后再Windbg或OD中无法看到被断掉的线程,但并不影响执行(仍然在跑)

CPU到哪里去找线程?

等待链表

1
dd KiWaitListHead

KiWaitListHead储存着等待线程双向链表的全局变量,存了两个指针,分别指向的是前一个和后一个_ETHREAD结构体中的_KTHREAD结构体0x60偏移的WaitListEntry成员

线程调用了Sleep()或WaitForSingleObject()等函数时,就挂到这个链表中。

线程有三种状态

  1. 就绪
  2. 等待
  3. 运行

正在运行中的线程存储在KPCR中,就绪和等待的线程全在另外的33个链表中。一个等待链表,32个就绪链表:

这些链表都使用了_KTHREAD(0x60)这个位置,也就是说,线程在某一时刻,只能属于其中一个圈

调度链表(就绪链表)

既然有32个就绪链表,就要有32个链表头。

1
kd>dd KiDispatcherReadyListHead L70

KiDispatcherReadyListHead储存着32个就绪线程双向链表的全局变量,32个8字节的数组,每个链表占8字节。

这个地址存储了32个链表头,分别对应32个优先级的调度链表,地址越高,优先级越高。如果FLink 等于 BLink 等于地址,说明此时链表为空;如果FLink等于BLink不等于地址,说明此时链表中只有一个线程。比如现在我 dd 打印,这时操作系统挂起,所有线程都处于等待状态,全部调度链表都是空的(因为windbg调试的时候,所有线程都会被挂起)

1
2
3
4
kd> dd KiDispatcherReadyListHead
8055bc20 8055bc20 8055bc20 8055bc28 8055bc28
8055bc30 8055bc30 8055bc30 8055bc38 8055bc38
...

image-20210913143736703

全局变量KiReadySummary,32位,如果哪个存在就绪链表,其该位被置1。其从低位到高位就绪链表优先级依次升高,因此需要找从左边数第一个为1的位数,该位作为位数从KiDispatchReadListHead数组中寻找。

1827556-20200422144013785-765135328

调度链表讲解

版本差异

  • XP只有32个调度链表,整个系统(包括多核)只有32个
  • 64位系统有64个调度链表,整个系统(包括多核)只有64个

服务器版本又有不同

KiWaitListHead整个系统只有一个,但KiDispatcherReadyListHead这个数组有几个CPU就有几组。

总结

  • 正在运行的线程在KPCR中
  • 准备运行的线程在32个调度链表中(0~31级),KiDispatcherReadyListHead是个存储了这32个链表头的数组。
  • 等待状态的线程存储在等待链表中,KiWaitListHead存储链表头。
  • 这些圈都挂一个相同的位置:_KTHREAD(0x60)

线程切换

逆向线程主动切换

Windows的线程切换比较复杂,

为了更好的学习,先了解一份代码:逆向ThreadSwitch

image-20210910191936188

  1. 四个参数, ebx:_KPCR esi:新线程 _KTHREAD edi:旧线程 _KTHREAD ecx:WaitIrql

  2. 换esp的瞬间就是线程切换的真正本质

  3. 不一定切换CR3,仅在跨进程线程切换时才切换CR3

  4. TSS中存储的一定是当前线程的,因为每次线程切换,都会更新TSS中的对应值

  5. 线程切换时FS虽然不变,但是fs的段选择子对应的段描述符的baseAddress会被修改为当前线程对应的TEB内存

  6. push的方式将老线程的异常链表(_KPCR.ExceptionList)存到老线程堆栈,改变esp后(即修改线程后),通过pop的方式取出新线程堆栈中的异常链表存到_KPCR.ExceptionList中

  7. 空闲线程的_KTHREAD,当KiFindReadyThread找KPCR中的nextThread为空,并且也找不到就绪链表中有线程的时候,就把线程交换给空闲线程。

    image-20210917174204289

    如图可知,IdleThread的线程开始入口并非放在_ETHREAD中,而是另有地方存

    ​ 如何找到IdleThread跑的函数:_KPCR._KPRCB.IdleThread.KernelStack,我们知道交换到IdleThread是经过SwapContext线程切换过去的。SwapContext将线程的KernelStack放入esp后的堆栈操作有这些,pop ecx(取出异常链表地址),popf(取出eflags寄存器),retn(取出要执行的地址跳转)。如图:

    image-20210917175257328

    windbg当前断下的就是IdleThread线程。

    image-20210917174430574image-20210917174644671

    image-20210917174728430

    因此可知:IdleThread线程入口是KiIdleLoop函数

  8. KiFindReadyThread,先找KPCR中nextThread是否有值,若为空去找调度线程链表找到最高优先级的就绪线程

  9. 很多细节没考究


image-20210911111900026

1
2
3
4
。。。(上图函数)
->KiSwapThread
->KiSwapContext
->SwapContext

KiSwapThread

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
.text:004050BF ; _DWORD __cdecl KiSwapThread()//无参
.text:004050BF @KiSwapThread@0 proc near ; CODE XREF: KeDelayExecutionThread(x,x,x):loc_405017↑p
.text:004050BF ; KeWaitForSingleObject(x,x,x,x,x):loc_40513E↓p ...
.text:004050BF
.text:004050BF ; FUNCTION CHUNK AT .text:0040EA85 SIZE 00000015 BYTES
.text:004050BF ; FUNCTION CHUNK AT .text:004109AF SIZE 00000009 BYTES
.text:004050BF
.text:004050BF mov edi, edi
.text:004050C1 push esi
.text:004050C2 push edi
.text:004050C3 db 3Eh
.text:004050C3 mov eax, ds:0FFDFF020h//eax取_KPRCB结构体首地址
.text:004050C9 mov esi, eax//esi为_KPRCB结构体首地址
.text:004050CB mov eax, [esi+8]//eax取下一个线程结构体地址
.text:004050CE test eax, eax//判断eax是否为0
.text:004050D0 mov edi, [esi+4]//edi取当前CPU正在跑的线程结构体地址
.text:004050D3 jnz loc_4109AF//下一个线程结构体不为0则跳转
.text:004050D9 push ebx
.text:004050DA movsx ebx, byte ptr [esi+10h]//ebx取kpcr的CPU核心编号
.text:004050DE xor edx, edx
.text:004050E0 mov ecx, ebx
.text:004050E2 call @KiFindReadyThread@8 ; KiFindReadyThread(x,x)//没有下一个线程结构体的情况下调该函数寻找准备好的线程
.text:004050E7 test eax, eax//判断是否找到
.text:004050E9 jz loc_40EA85//未找到则跳转
.text:004050EF
.text:004050EF loc_4050EF: ; CODE XREF: KiSwapThread()+99D6↓j
.text:004050EF pop ebx
.text:004050F0
.text:004050F0 loc_4050F0: ; CODE XREF: KiSwapThread()+B8F4↓j//开始线程切换
.text:004050F0 mov ecx, eax//ecx取下一个线程或空闲结构体地址
.text:004050F2 call @KiSwapContext@4 ; KiSwapContext(x)//开始切换线程上下文
.text:004050F7 test al, al
.text:004050F9 mov cl, [edi+58h] ; NewIrql
.text:004050FC mov edi, [edi+54h]
.text:004050FF mov esi, ds:__imp_@KfLowerIrql@4 ; KfLowerIrql(x)
.text:00405105 jnz loc_415ADB
.text:0040510B
.text:0040510B loc_40510B: ; CODE XREF: IopCompleteRequest(x,x,x,x,x)+24A↓j
.text:0040510B call esi ; KfLowerIrql(x) ; KfLowerIrql(x)
.text:0040510D mov eax, edi
.text:0040510F pop edi
.text:00405110 pop esi
.text:00405111 retn
.text:00405111 @KiSwapThread@0 endp

.text:0040EA85 ; START OF FUNCTION CHUNK FOR @KiSwapThread@0
.text:0040EA85
.text:0040EA85 loc_40EA85: ; CODE XREF: KiSwapThread()+2A↑j//没取到下一个就绪线程的话,走此处
.text:0040EA85 mov eax, [esi+0Ch]//eax取KPCR中的IdleThread,即返回值取闲置线程
.text:0040EA88 xor edx, edx//edx清0
.text:0040EA8A inc edx//edx=1
.text:0040EA8B mov ecx, ebx//ecx取kpcr的CPU核心编号=0
.text:0040EA8D shl edx, cl//edx左移0,edx=1
.text:0040EA8F or ds:_KiIdleSummary, edx//_KiIdleSummary第0位置1
.text:0040EA95 jmp loc_4050EF
.text:0040EA95 ; END OF FUNCTION CHUNK FOR @KiSwapThread@0

.text:004109AF ; START OF FUNCTION CHUNK FOR @KiSwapThread@0
.text:004109AF
.text:004109AF loc_4109AF: ; CODE XREF: KiSwapThread()+14↑j//下一个线程结构体已准备就绪的情况下:
.text:004109AF and dword ptr [esi+8], 0//_KPRCB结构体中下一个线程结构体位置清空
.text:004109B3 jmp loc_4050F0
.text:004109B3 ; END OF FUNCTION CHUNK FOR @KiSwapThread@0

KiFindReadyThread

image-20210911143508984

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
//两个参数,分别是ecx为kpcr的CPU核心编号(单核模式下这个参数是没有用的),edx为0,表示要求的最低优先级,此处是函数开始位置
.text:00405052 ; __fastcall KiFindReadyThread(x, x)
.text:00405052 @KiFindReadyThread@8 proc near ; CODE XREF: KiSwapThread()+23↓p
.text:00405052 ; KiAdjustQuantumThread(x)+10C4↓p ...
.text:00405052
.text:00405052 ; FUNCTION CHUNK AT .text:00401EEC SIZE 00000011 BYTES
.text:00405052 ; FUNCTION CHUNK AT .text:0040503F SIZE 0000000E BYTES
.text:00405052
.text:00405052 xor eax, eax//eax清零
.text:00405054 inc eax//eax=1
.text:00405055 mov ecx, edx//ecx清零
.text:00405057 shl eax, cl//eax左移0位
.text:00405059 push 10h
.text:0040505B pop ecx//ecx=10
.text:0040505C dec eax//eax=0
.text:0040505D not eax//eax=0xFFFFFFFF
.text:0040505F and eax, ds:_KiReadySummary//_KiReadySummary为3500,eax=_KiReadySummary
.text:00405065 mov edx, eax//edx=_KiReadySummary
.text:00405067 shr edx, 10h//edx=_KiReadySummary>>0x10=0
.text:0040506A jnz short loc_405070//目前情况不跳转,但其实取决于_KiReadySummary
.text:0040506C xor ecx, ecx//ecx=0
.text:0040506E mov edx, eax//edx=0
.text:00405070
.text:00405070 loc_405070: ; CODE XREF: KiFindReadyThread(x,x)+18↑j
.text:00405070 test edx, 0FFFFFF00h
.text:00405076 jz short loc_40507B//目前情况不跳转,取决于ReadySummary
.text:00405078 add ecx, 8//目前情况ecx=8
.text:0040507B
.text:0040507B loc_40507B: ; CODE XREF: KiFindReadyThread(x,x)+24↑j
.text:0040507B mov edx, eax//edx=_KiReadySummary=0x3500
.text:0040507D shr edx, cl//edx=_KiReadySummary>>8=0x35
.text:0040507F push esi
.text:00405080 push 1Fh
.text:00405082 movsx edx, ds:_KiFindFirstSetLeft[edx]//此代码ida有问题,实际上取的是byte而不是dword,前面针对edx的种种操作都是为了确定该下标,动态调试0x35下标的是0x05,edx=0x05
.text:00405089 add edx, ecx//edx=5+8=0xD
.text:0040508B pop ecx//ECX=0x1F
.text:0040508C sub ecx, edx//ecx=0x1F-0xD=0x12
.text:0040508E shl eax, cl//_KiReadySummar0x3500左移0x12位,eax=0xD4000000
.text:00405090 lea esi, _KiDispatcherReadyListHead[edx*8]//对应优先级的调度链表中取链表首地址
.text:00405097 test eax, eax
.text:00405099 jz short loc_40503F//eax是0就跳
.text:0040509B
.text:0040509B loc_40509B: ; CODE XREF: KiFindReadyThread(x,x)-7↑j
.text:0040509B test eax, eax
.text:0040509D jge short loc_405043//eax大于等于0就跳,跳过去也是处理完再跳回来
.text:0040509F mov eax, [esi]//eax取得线调度链表中的第一个线程结构体的0x60偏移
.text:004050A1 mov ecx, [eax]//ecx取调度链表中线程结构体等待链表中的第一个
.text:004050A3 sub eax, 60h//eax减了60后是线程结构体首地址
.text:004050A6 push edi
.text:004050A7 mov edi, [eax+64h]//edi取调度链表中线程结构体等待链表中的下一个
.text:004050AA mov [edi], ecx
.text:004050AC mov [ecx+4], edi
.text:004050AF cmp [esi], esi//当前链表是否为空
.text:004050B1 pop edi
.text:004050B2 jz loc_401EEC
.text:004050B8
.text:004050B8 loc_4050B8: ; CODE XREF: KiFindReadyThread(x,x)-11↑j
.text:004050B8 pop esi
.text:004050B9 retn
.text:004050B9 @KiFindReadyThread@8 endp

分析感觉不是很准确

网上对KiFindReadyThread更详尽的分析参考

该函数做了三件事:

  1. 解析KiReadySummary,找到从左起第一个为1的位数;
  2. 用该位获取从KiDispatchReadListHead中的第一个_KTHREAD线程,将其从链表中摘除;
  3. 如果摘除后该链表为空,则找到相应的KiReadySummary位将其置0。

KiSwapContext

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
//KiSwapContext只有一个参数,通过ecx传参,[[0FFDFF020h]+8]:_KPRCB中+8偏移的NextThread,即将切换的下一个线程结构体_KTHREAD
.text:00404828 ; __fastcall KiSwapContext(x)
.text:00404828 @KiSwapContext@4 proc near ; CODE XREF: KiSwapThread()+33↓p
.text:00404828
.text:00404828 var_10 = dword ptr -10h
.text:00404828 var_C = dword ptr -0Ch
.text:00404828 var_8 = dword ptr -8
.text:00404828 var_4 = dword ptr -4
.text:00404828
.text:00404828 sub esp, 10h//升栈
//保存当前线程寄存器的现场
.text:0040482B mov [esp+10h+var_4], ebx//保存原线程寄存器ebx
.text:0040482F mov [esp+10h+var_8], esi//保存原线程寄存器esi
.text:00404833 mov [esp+10h+var_C], edi//保存原线程寄存器edi
.text:00404837 mov [esp+10h+var_10], ebp//保存原线程寄存器ebp
.text:0040483A mov ebx, ds:0FFDFF01Ch//ebx取KPCR结构体首地址
.text:00404840 mov esi, ecx//esi取即将切换的下一个线程结构体_KTHREAD地址
.text:00404842 mov edi, [ebx+124h]//edi取当前CPU正在跑的线程结构体_KTHREAD地址
.text:00404848 mov [ebx+124h], esi//将【即将切换的下一个线程结构体_KTHREAD地址】放入【当前CPU正在跑的线程结构体_KTHREAD地址位置】
.text:0040484E mov cl, [edi+58h]//cl取WaitIrql
.text:00404851 call SwapContext
.text:00404856 mov ebp, [esp+10h+var_10]//取到新线程的寄存器ebp
.text:00404859 mov edi, [esp+10h+var_C]//取到新线程的寄存器edi
.text:0040485D mov esi, [esp+10h+var_8]//取到新线程的寄存器esi
.text:00404861 mov ebx, [esp+10h+var_4]//取到新线程的寄存器ebx
.text:00404865 add esp, 10h//平栈
.text:00404868 retn
.text:00404868 @KiSwapContext@4 endp

image-20210912143605531

SwapContext

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
//四个参数 ebx: _KPCR   esi: 新线程 _KTHREAD  edi: 旧线程 _KTHREAD  ecx:旧的WaitIrql
.text:00404924 SwapContext proc near ; CODE XREF: KiUnlockDispatcherDatabase(x)+72↑p
.text:00404924 ; KiSwapContext(x)+29↑p ...
.text:00404924 or cl, cl
.text:00404926 mov byte ptr es:[esi+2Dh], 2//设置即将切换的下一个线程结构体的状态State为2 状态如下: 1 就绪; 2 运行; 5 等待
.text:0040492B pushf
.text:0040492C
.text:0040492C loc_40492C: ; CODE XREF: KiIdleLoop()+5A↓j
.text:0040492C mov ecx, [ebx]//ecx取当前线程异常链表
.text:0040492E cmp dword ptr [ebx+994h], 0//比较DpcRoutineActive位是否为0
.text:00404935 push ecx//当前线程异常链表压栈
.text:00404936 jnz loc_404A70//若DpcRoutineActive位不为0则跳转
.text:0040493C cmp ds:_PPerfGlobalGroupMask, 0//比较_PPerfGlobalGroupMask全局变量是否为0
.text:00404943 jnz loc_404A47//_PPerfGlobalGroupMask全局变量不为0则跳转,跳转处理后依然跳回loc_404949

.text:00404949
.text:00404949 loc_404949: ; CODE XREF: SwapContext+12B↓j
.text:00404949 ; SwapContext+13C↓j ...
.text:00404949 mov ebp, cr0//ebp为cr0
.text:0040494C mov edx, ebp//edx取cr0
.text:0040494E mov cl, [esi+2Ch]//cl取即将切换的下一个线程的DebugActive
.text:00404951 mov [ebx+50h], cl//KPCR结构体中DebugActive设置为【即将切换的下一个线程的DebugActive的值】
.text:00404954 cli//禁止中断发生
.text:00404955 mov [edi+28h], esp//将当前esp放入【当前CPU正在跑的线程的KernelStack位置】
.text:00404958 mov eax, [esi+18h]//eax取即将切换的下一个线程的三环堆栈顶esp3
.text:0040495B mov ecx, [esi+1Ch]//ecx取即将切换的下一个线程的三环堆栈大小
.text:0040495E sub eax, 210h//线程堆栈的前 0x210 字节是浮点寄存器,此时 eax 指向 _KTRAP_FRAME.V86Gs
.text:00404963 mov [ebx+8], ecx//KPCR中记录的栈大小设置为即将切换的下一个线程的三环堆栈大小
.text:00404966 mov [ebx+4], eax//KPCR中记录的栈底设置为即将切换的下一个线程的三环堆栈顶esp3-0x210
.text:00404969 xor ecx, ecx//ecx清零
.text:0040496B mov cl, [esi+31h]//cl取【即将切换的下一个线程的NpxState】
.text:0040496E and edx, 0FFFFFFF1h//cr0的1,2,3位清0,分别是cr0中的TS,EM,MP位
.text:00404971 or ecx, edx
.text:00404973 or ecx, [eax+20Ch]//ecx或上_FX_SAVE_AREA.Cr0NpxState
.text:00404979 cmp ebp, ecx
.text:0040497B jnz loc_404A3F//相等就不跳转,不相等就跳转,跳转后只做mov cr0,ecx再跳回loc_404983
.text:00404981 lea ecx, [ecx]//取自己无用
.text:00404983
.text:00404983 loc_404983: ; CODE XREF: SwapContext+11E↓j
.text:00404983 test dword ptr [eax-1Ch], 20000h//判断[eax-1Ch],即_Ktrap_frame中的eflags第17位VM位是否1
.text:0040498A jnz short loc_40498F//如果是虚拟8086模式则跳转
.text:0040498C sub eax, 10h//eax此时是_Ktrap_frame中HardwareSegSs地址,减掉10是为了跳过陷阱帧中虚拟8086才会用到的16字节部分
.text:0040498F
.text:0040498F loc_40498F: ; CODE XREF: SwapContext+66↑j
.text:0040498F mov ecx, [ebx+40h]//ecx取KPCR中的TSS
.text:00404992 mov [ecx+4], eax//把此时的_Ktrap_frame中HardwareSegSs+4的地址的esp放入TSS中的esp0中
//!!!!!!esp切换!!!!!!!!!
.text:00404995 mov esp, [esi+28h]//新线程结构体_KTHREAD中的KernelStack放入esp中
.text:00404998 mov eax, [esi+20h]//eax取新线程_KTHREAD中的TEB
.text:0040499B mov [ebx+18h], eax//新线程_KTHREAD中的TEB挂到KPCR结构中的NtTib结构中的Self上
.text:0040499E sti//允许中断
.text:0040499F mov eax, [edi+44h]//eax取老线程_KTHREAD.ApcState.Process
.text:004049A2 cmp eax, [esi+44h]//比较新老线程的_KTHREAD.ApcState.Process,判断线程切换是否同一个进程中的线程切换
.text:004049A5 mov byte ptr [edi+50h], 0//老线程的_KTHREAD的IdleSwapBlock清0,没有必要换出
.text:004049A9 jz short loc_4049D7//同一个进程就跳转
.text:004049AB mov edi, [esi+44h]//edi取新线程的进程结构体pcState.Process
.text:004049AE test word ptr [edi+20h], 0FFFFh//老进程的TEB中LdtDescriptor。。。16位系统相关
.text:004049B4 jnz short loc_404A11//是DOS系统跳转
.text:004049B6 xor eax, eax//eax清零
.text:004049B8
.text:004049B8 loc_4049B8: ; CODE XREF: SwapContext+116↓j
.text:004049B8 lldt ax//ldt加载一个0
.text:004049BB xor eax, eax//eax清0
.text:004049BD mov gs, eax//gs段寄存器加载0,因此windows并未使用gs段寄存器
.text:004049BF assume gs:GAP
.text:004049BF mov eax, [edi+18h]//eax取新线程的进程结构体中的页目录表基址,_EPROCESS.Pcb.DirectoryTableBase
.text:004049C2 mov ebp, [ebx+40h]//ebp取KPCR中的TSS
.text:004049C5 mov ecx, [edi+30h]//ecx取KPCR中的IDR
.text:004049C8 mov [ebp+1Ch], eax//新线程的进程结构体中的页目录表基址放入tss中的CR3位置
.text:004049CB mov cr3, eax//新线程的进程结构体中的页目录表基址加载进CR3
.text:004049CE mov [ebp+66h], cx//KPCR中的IDR的低16位放入TSS中的IoMapBase位置,windows2000以后已经不使用了,所以没有意义
.text:004049D2 jmp short loc_4049D7
.text:004049D2 ; ---------------------------------------------------------------------------
.text:004049D4 db 8Dh, 49h, 0
.text:004049D7 ; ---------------------------------------------------------------------------
.text:004049D7//进程一样的话
.text:004049D7 loc_4049D7: ; CODE XREF: SwapContext+85↑j
.text:004049D7 ; SwapContext+AE↑j
.text:004049D7 mov eax, [ebx+18h]//eax取_KPCR.NtTib.Self
.text:004049DA mov ecx, [ebx+3Ch]//ecx取_KPCR.GDT
//下面4步修改fs在GDT中的baseAddr为新线程KPCR结构地址或TEB地址
.text:004049DD mov [ecx+3Ah], ax//ecx+38是段描述符3B对应的gdt中的地址,所以3A的位置正是段描述符低32位中的高16位的BaseAddress的位置
.text:004049E1 shr eax, 10h//得到剩下的高16位
.text:004049E4 mov [ecx+3Ch], al
.text:004049E7 mov [ecx+3Fh], ah
.text:004049EA inc dword ptr [esi+4Ch]//_ETHREAD.Tcb.ContextSwitches次数+1(当前线程被切换的次数+1)
.text:004049ED inc dword ptr [ebx+61Ch]//_KPCR.PrcbData.KeContextSwitches次数+1(总的线程切换次数+1)
.text:004049F3 pop ecx//取出异常链表
.text:004049F4 mov [ebx], ecx//存入异常链表
.text:004049F6 cmp byte ptr [esi+49h], 0//APC相关判断
.text:004049FA jnz short loc_404A00
.text:004049FC popf
.text:004049FD xor eax, eax
.text:004049FF retn//跳转到线程执行入口(由于esp已经跳转了,因此这个ret并不是返回到【call进SwapContext的指令地址的下一个地址】),但当切换过去再切换回来的时候,这个位置依然存的是【call进SwapContext的指令地址的下一个地址】,因此依然会继续正常返回。只有IdleThread比较特殊,这个栈位置存的直接是IdleThread线程入口地址。因为IdleThread线程入口对应的函数KiIdleLoop并未调用SwapContext切换线程,而是靠手动更换esp更换线程,在更换esp前给这个位置留的是IdleThread的线程函数入口地址(即KiIdleLoop)
.text:00404A00 ; ---------------------------------------------------------------------------
.text:00404A00
.text:00404A00 loc_404A00: ; CODE XREF: SwapContext+D6↑j
.text:00404A00 popf
.text:00404A01 jnz short loc_404A06
.text:00404A03 mov al, 1
.text:00404A05 retn
.text:00404A06 ; ---------------------------------------------------------------------------
.text:00404A06
.text:00404A06 loc_404A06: ; CODE XREF: SwapContext+DD↑j
.text:00404A06 mov cl, 1
.text:00404A08 call ds:__imp_@HalRequestSoftwareInterrupt@4 ; HalRequestSoftwareInterrupt(x)
.text:00404A0E xor eax, eax
.text:00404A10 retn
.text:00404A11 ; ---------------------------------------------------------------------------
.text:00404A11
.text:00404A11 loc_404A11: ; CODE XREF: SwapContext+90↑j
.text:00404A11 mov ebp, [ebx+3Ch]
.text:00404A14 mov eax, [edi+20h]
.text:00404A17 mov [ebp+48h], eax
.text:00404A1A mov eax, [edi+24h]
.text:00404A1D mov [ebp+4Ch], eax
.text:00404A20 mov eax, 48h
.text:00404A25 mov ebp, [ebx+38h]
.text:00404A28 mov ecx, [edi+28h]
.text:00404A2B mov [ebp+108h], ecx
.text:00404A31 mov ecx, [edi+2Ch]
.text:00404A34 mov [ebp+10Ch], ecx
.text:00404A3A jmp loc_4049B8
.text:00404A3F ; ---------------------------------------------------------------------------
.text:00404A3F
.text:00404A3F loc_404A3F: ; CODE XREF: SwapContext+57↑j
.text:00404A3F mov cr0, ecx
.text:00404A42 jmp loc_404983
.text:00404A47 ; ---------------------------------------------------------------------------
.text:00404A47
.text:00404A47 loc_404A47: ; CODE XREF: SwapContext+1F↑j
.text:00404A47 mov eax, ds:_PPerfGlobalGroupMask
.text:00404A4C cmp eax, 0
.text:00404A4F jz loc_404949
.text:00404A55 mov edx, esi
.text:00404A57 mov ecx, edi
.text:00404A59 test dword ptr [eax+4], 4
.text:00404A60 jz loc_404949
.text:00404A66 call @WmiTraceContextSwap@8 ; WmiTraceContextSwap(x,x)
.text:00404A6B jmp loc_404949
.text:00404A70 ; ---------------------------------------------------------------------------
.text:00404A70
.text:00404A70 loc_404A70: ; CODE XREF: SwapContext+12↑j
.text:00404A70 push 0B8h ; BugCheckCode
.text:00404A75 call _KeBugCheck@4 ; KeBugCheck(x)//该函数内部几层后有蓝屏
.text:00404A75 SwapContext endp

SwapContext这个函数是Windows线程切换的核心,无论是主动切换还是系统时钟导致的线程切换,最终都会调用这个函数。

KiIdleLoop函数流程:

image-20210917180706498

内核堆栈的结构

image-20210913123031866image-20210913123046400

_Trap_Frame结构

image-20210913123224608

调用API进0环TSS细节

  • 普通调用:通过TSS.ESP0得到0环堆栈
  • 快速调用:从MSR得到一个临时0环栈,代码执行后仍然通过TSS.ESP0得到当前线程的0环堆栈

Intel设计TSS的目的是为了任务切换(线程切换),但Windows与Linux并没有使用。而是采用堆栈来保存线程的各种寄存器。

上文逆向线程主动切换部分有SwapContext的具体代码逆向。

调用API进0环FS细节

FS:[0]寄存器在3环时指向TEB,进入0环后FS:[0]指向KPCR

系统中同时存在很多个线程,这就意味着FS:[0]在3环时指向的TEB要有多个(每个线程一份)。

但在实际使用中我们发现,当我们在3环查看不同线程的FS寄存器时,FS的端选择子都是相同的,那是如何实现通过一个FS寄存器指向多个TEB呢?

模拟线程切换

模拟windows的线程切换机制实现用一个线程当n个线程使用

image-20210913122330491image-20210913122350154image-20210913122429778

实验代码

ThreadCore.h

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
#pragma once

#define GMTHREADSTACKSIZE 0x10000//堆栈大小,之前设定为0x1000,总是莫名其妙的报错,原来是调用某些函数爆堆栈了,一个物理页根本不够用的。

#define MAXGMTHREAD 99//最大允许同时有99个线程

//线程结构体(仿ETHREAD)
typedef struct
{
char *name; //线程名 相当于线程TID
int Flags; //线程状态
int SleepMillisecondDot; //休眠时间
int suspendNum;//挂起计数(自己添加的挂起计数,未逆向windows这部分,不一定正确)

void *InitialStack; //线程堆栈起始位置
void *StackLimit; //线程堆栈界限
void *KernelStack; //线程堆栈当前位置,也就是ESP

void *lpParameter; //线程函数的参数
void(*func)(void *lpParameter); //线程函数


} GMThread_t;


//创建线程
int RegisterGMThread(char* name, void(*func)(void*lpParameter), void* lpParameter);

void IdleGMThread(void *lpParameter);

//push函数
void PushStack(unsigned int **Stackpp, unsigned int v);

//线程函数启动函数
void GMThreadStartup(GMThread_t *GMThreadp);

//初始化线程
void initGMThread(GMThread_t *GMThreadp, char *name, void(*func)(void *lpParameter), void *lpParameter);

//调度线程函数
void Scheduling(void);

//子线程休眠函数
void GMSleep(int Milliseconds);

//交换线程上下文
void SwitchContext(GMThread_t *SrcGMThreadp, GMThread_t *DstGMThreadp);

//线程挂起(自己添加的线程挂起,未逆向windows这部分,不一定正确)
void suspendThread(char* name);

//挂起线程恢复(自己添加的线程挂起恢复,未逆向windows这部分,不一定正确)
void resumeThread(char* name);

ThreadCore.cpp

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#include "ThreadCore.h"
#include <iostream>
#include <windows.h>
using namespace std;
int CurrentThreadindex = 0;

//线程结构体数组
//实际的线程是挂在调度链表和等待链表中,此处仿真为了简单,把所有状态的线程都存到下面定义的数组中,数组零标的位置存main线程本身。
//仿真中所谓创建线程,就是创建一个结构体,并且挂到这个数组中,此时的线程状态为:创建
GMThread_t GMThreadList[MAXGMTHREAD] = { NULL, 0 };

//线程状态
//线程状态的标志
enum FLAGS
{
GMTHREAD_CREATE = 0x1,
GMTHREAD_READY = 0x2,
GMTHREAD_SLEEP = 0x4,
GMTHREAD_EXIT = 0x8,
};

int RegisterGMThread(char * name, void(*func)(void *lpParameter), void * lpParameter)
{
int i;
for (i = 1; GMThreadList[i].name; i++)
{
if (0 == _stricmp(GMThreadList[i].name, name))//防止线程唯一标识重复
{
break;
}
}
initGMThread(&GMThreadList[i], name, func, lpParameter);
return (i | 0x55AA0000);
}

void IdleGMThread(void*lpParameter)
{
printf("IdleGMThread---------\r\n");
Scheduling();
return;
}

//子线程休眠函数
void GMSleep(int Milliseconds)
{
GMThread_t *GMThreadp;
GMThreadp = &GMThreadList[CurrentThreadindex];
if ((GMThreadp->Flags)!=0)
{
GMThreadp->SleepMillisecondDot = GetTickCount() + Milliseconds;
GMThreadp->Flags = GMTHREAD_SLEEP;
}
Scheduling();//线程在休眠的时候主动让出线程执行权
return;
}

void PushStack(unsigned int **Stackpp,unsigned int v)
{
*Stackpp -= 1;
**Stackpp = v;
return;
}

void GMThreadStartup(GMThread_t *GMThreadp)
{
GMThreadp->func(GMThreadp->lpParameter);
GMThreadp->Flags = GMTHREAD_EXIT;//线程函数执行完毕后设置状态为退出
Scheduling();//调度新线程
return;//只在Scheduling没找到目标线程时才能释放。
}

void initGMThread(GMThread_t * GMThreadp, char * name, void(*func)(void *lpParameter), void * lpParameter)
{
unsigned char *StackPages;
unsigned int *StackDWORDParam;
//结构初始化赋值
GMThreadp->Flags = GMTHREAD_CREATE;
GMThreadp->name = name;
GMThreadp->func = func;
GMThreadp->suspendNum = 0;
GMThreadp->lpParameter = lpParameter;
//申请空间
StackPages = (unsigned char*)VirtualAlloc(NULL, GMTHREADSTACKSIZE, MEM_COMMIT, PAGE_READWRITE);
//清零
ZeroMemory(StackPages, GMTHREADSTACKSIZE);
//堆栈初始化地址
GMThreadp->InitialStack = StackPages + GMTHREADSTACKSIZE;
//堆栈限制
GMThreadp->StackLimit = StackPages;
//堆栈地址esp
StackDWORDParam = (unsigned int*)GMThreadp->InitialStack;
//入栈
PushStack(&StackDWORDParam, (unsigned int)GMThreadp);
PushStack(&StackDWORDParam, (unsigned int)9);//平衡堆栈
PushStack(&StackDWORDParam, (unsigned int)GMThreadStartup);//线程入口函数
PushStack(&StackDWORDParam, (unsigned int)5);//push ebp
PushStack(&StackDWORDParam, (unsigned int)7);//push edi
PushStack(&StackDWORDParam, (unsigned int)6);//push esi
PushStack(&StackDWORDParam, (unsigned int)3);//push ebx
PushStack(&StackDWORDParam, (unsigned int)2);//push edx
PushStack(&StackDWORDParam, (unsigned int)1);//push edx
PushStack(&StackDWORDParam, (unsigned int)0);//push eax
GMThreadp->KernelStack = StackDWORDParam;//填入对应的KernelStack
GMThreadp->Flags = GMTHREAD_READY;//创建的线程设置为准备状态
return;
}

void Scheduling()
{
int i;
int TickCount;
GMThread_t *SrcGMThreadp;
GMThread_t *DstGMThreadp;
TickCount = GetTickCount();
SrcGMThreadp = &GMThreadList[CurrentThreadindex];
DstGMThreadp = &GMThreadList[0];
//遍历数组找到第一个处于GMTHREAD_READY状态的线程
for (i = 1; GMThreadList[i].name; i++)
{
if ((GMThreadList[i].Flags&GMTHREAD_SLEEP)&& GMThreadList[i].suspendNum==0)
{
if (TickCount>GMThreadList[i].SleepMillisecondDot)
{
GMThreadList[i].Flags = GMTHREAD_READY;
}
}
if (GMThreadList[i].Flags&GMTHREAD_READY)
{
DstGMThreadp = &GMThreadList[i];
break;
}
}
CurrentThreadindex = DstGMThreadp - GMThreadList;
SwitchContext(SrcGMThreadp, DstGMThreadp);
return;//只在Scheduling没找到目标线程时才能释放。
}

//线程上下文切换
__declspec(naked) void SwitchContext(GMThread_t *SrcGMThreadp, GMThread_t *DstGMThreadp)
{
__asm
{
push ebp;
mov ebp, esp;
push edi;
push esi;
push ebx;
push ecx;
push edx;
push eax;
mov esi, SrcGMThreadp;
mov edi, DstGMThreadp;
mov[esi + GMThread_t.KernelStack], esp;
//=======经典堆栈切换,另一个线程复活===========
mov esp, [edi + GMThread_t.KernelStack];
pop eax;
pop edx;
pop ecx;
pop ebx;
pop esi;
pop edi;
pop ebp;
ret;//线程函数地址正好弹到了eip中
}
}

void suspendThread(char * name)
{
for (int i = 1; GMThreadList[i].name; i++)//遍历线程数组找到对应名称
{
if (0 == _stricmp(GMThreadList[i].name, name))
{
GMThreadList[i].Flags = GMTHREAD_SLEEP;
GMThreadList[i].suspendNum += 1;
}
}
}

void resumeThread(char * name)
{
for (int i = 1; GMThreadList[i].name; i++)//遍历线程数组找到对应名称
{
if (0 == _stricmp(GMThreadList[i].name, name))
{
GMThreadList[i].Flags = GMTHREAD_READY;
GMThreadList[i].suspendNum -= GMThreadList[i].suspendNum > 0 ? 1 : 0;
}
}
}

main.cpp

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
#include <iostream>
#include "ThreadCore.h"
#include <windows.h>
using namespace std;
//对应创建线程的线程函数

extern int CurrentThreadindex;

#define MAXGMTHREAD 99//最大允许同时有99个线程

extern GMThread_t GMThreadList[MAXGMTHREAD];

//线程回调函数
void Thread1(void* a)
{
while (1)
{
cout << "线程1--"<<*(int*)a << endl;
GMSleep(1000);//绝大部分winAPI都会涉及到线程切换,这个GMSleep象征所有winAPI
}
}
void Thread2(void* a)
{
while (1)
{
cout << "线程2--" << *(int*)a << endl;
GMSleep(800);
}
}
void Thread3(void* a)
{
while (1)
{
cout << "线程3--" << *(int*)a << endl;
GMSleep(2000);
}
}
void Thread4(void* a)
{
while (1)
{
cout << "线程4--" << *(int*)a << endl;
GMSleep(1500);
}
}


int main()
{
//参数
int a = 1;
int b = 2;
int c = 3;
int d = 4;
RegisterGMThread("Thread1", Thread1, &a);
RegisterGMThread("Thread2", Thread2, &b);
RegisterGMThread("Thread3", Thread3, &c);
RegisterGMThread("Thread4", Thread4, &d);

//仿Windows线程切换
while (1)
{
Sleep(20);
Scheduling();
}

return 0;
}

效果如下

image-20210912205808625

模拟线程切换总结

模拟线程切换总结:

  1. 线程不是被动切换的,而是主动让出CPU.
  2. 线程切换并没有使用TSS来保存寄存器,而是使用堆栈.
  3. 线程切换的过程就是堆栈切换的过程.

时钟中断线程切换

绝大部分系统内核函数都会调用SwapContext函数来进行线程的切换,这种切换是线程主动调用的。

  • Q:那么如果当前的线程不去调用系统API,操作系统如何实现线程切换呢?
  • A:那就是时钟切换

中断一个正在执行的程序有两种办法

  1. 异常,比如缺页,或者INT N指令
  2. 中断,比如时钟中断

系统时钟中断

image-20210913113450583

Windows系统操作系统每隔10~20毫秒触发一次时钟中断。

可使用如下Win32 API,获取当前的时钟中断间隔值:

1
GetSystemTimeAdjustment

时钟中断的执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
KiStartUnexpectedRange//实际上在windbg下断点根本不断下来
->KiEndUnexpectedRange
->KiUnexpectedInterruptTail
->HalBeginSystemInterrupt(HAL)

HalEndSystemInterrupt
->KiDispatchInterrupt(KiReadyProcess)
->SwapContext

//KeUpdateRunTime来自下面?
HalInitSystem

/*KiReadyProcess做了什么:
1. 根据你的当前老线程,亲核,你最喜欢的哪个核softAffinity,闲置核,去计算当前老的线程要挂在哪一个KPCR中的nextThread上
2. 如果affinity,softAffinity,闲置核计算,没有找到,那么就会找下一个KPCR,挂上。如果KPCR中有下一个了,那么就对比优先级,看是挂还是用KPCR以前的。
3. 如果都挂不上,就挂在WaitList中。
如果,当前老线程的优先级大于KPCR中当前线程优先级,那么在WAITLIST就挂在当前优先级链表第一个位置
否则是当前优先级链表的最后一个
*/

HalpClockInterrup函数是时钟中断跳转到的位置

如何避免线程切换

线程切换有几种情况

  • 主动调用API函数
  • 时钟中断
  • 异常处理

如果一个线程不调用API,在代码中屏蔽中断(CLI指令),并且不会出现异常,那么当前线程将永久占有CPU,单核占有率100%,2核也就是50%

硬件时钟中断是没办法屏蔽的

时钟线程切换流程

时钟中断并不一定会导致线程切换,只有两种情况会导致线程切换

  1. 当前的线程CPU时间片到期
  2. 有备用线程(KPCR.PrcbData.NextThread)

CPU时间片到期线程切换

当前的线程CPU时间片到期的切换

  • 当一个新的线程开始执行时,初始化程序会在_KTHREAD.Quantum赋初始值,该值的大小由_KPROCESS.ThreadQuantum决定
  • 有个KeUpdateRunTime函数,该函数每次将当前线程Quantum减少3个单位,如果减到0,则将KPCR.PrcbData.QuantumEnd的值设置为非0
  • KiDispatchInterrupt函数判断时间片到期:调用KiQuantumEnd重新设置时间片,内部调用KiFindReadyThread找到要运行的线程

存在备用线程的线程切换

KPCR.PrcbData.NextThread被设置时,即使当前线程的CPU时间片没有到期,仍然会被切换。

线程切换情况总结

  1. 当前线程主动调用API:

    ​ API函数->KiSwapThread->KiSwapContext->SwapContext

  2. 当前线程时间片到期:

    ​ KiDispatchInterrupt->KiQuantumEnd->KiSwapContext->SwapContext

  3. 有备份线程(KPCR.Prcb.NextThread)

    ​ KiDispatchInterrupt->SwapContext

线程优先级(KiFindReadyThread详解)

在KiSwapThread与KiQuantumEnd函数中都是通过KiFindReadyThread来找下一个要切换的线程。

32个调度链表

image-20210913143736703

KiFindReadyThread查找方式:按照优先级别进行查找:31…30…29…28…

在查找中,如果级别31的链表里面有线程,那么就不会查找级别为30的链表

高效查询优化

调度链表有32个,每次都从头开始查找效率太低,所以windows通过一个DWORD类型变量_kiReadySummary来记录:

当向调度链表(32个)中挂入或者摘除某个线程时,会判断当前级别的链表是否为空,为空将_kiReadySummary对应的位清零,否则置1

_kiReadySummary结构

image-20210913144629239

_kiReadySummary中的每一位分别对应调度链表每一个链表中是否有线程,第0位对应的就是KiDispatcherReadyListHead中最低级优先级的调度链表

如果KiDispatcherReadyListHead一个就绪线程也没了,那么切换为空闲线程IdleThread

多CPU会随机寻找KiDispatcherReadyListHead指向的数组中的线程。线程可以绑定某个cpu(使用API:setThreadAffinityMask)

进程挂靠

进程与线程的关系:

  • 一个进程可以包含多个线程
  • 一个进程至少要有一个线程

进程为线程提供资源,也就是提供Cr3的值,Cr3中存储的是页目录表基址,Cr3确定了,线程能访问的内存也就确定了

线程代码:

1
mov eax,dword ptr ds:[0x12345678]

CPU如何解析0x12345678这个地址呢?

  1. CPU解析线性地址时要通过页目录表来找对应的物理页,页目录表基址存在Cr3寄存器中
  2. 当前的Cr3的值来源于当前的进程(_KPROCESS.DiectoryTableBase(+0x018))

线程找进程:(线程结构中有两个成员指向了其进程结构体)

image-20210913170303489

虽然有两个指向进程的结构体,但是他们两个的含义不同。

参考SwapContext的汇编会发现:线程切换的时候,会比较新老线程_KTHREAD结构体0x044处指定的_EPROCESS是否为同一个,如果不是同一个,会将_EPROCESS的DirectoryTableBase的值取出,赋值给Cr3

所以线程需要的Cr3的值来源于线程结构体0x44偏移指向的_EPROCESS。

区别总结

  • 0x220 亲身父母:这个线程是哪个进程创建的
  • 0x44 养父母:创建后的线程是属于哪个进程的(谁在为这个线程提供资源,也就是提供Cr3)

一般情况下,0x220和0x44指向的是同一个进程。(远线程创建就是这两个值不一样)

进程挂靠

正常情况下,Cr3的值是由养父母提供的,但Cr3的值也可以改成和当前线程毫不相干的其他进程的DirectoryTableBase。

1
2
3
4
5
6
mov cr3,A.DirectoryTableBase;
mov eax,dword ptr ds:[0x12345678];eax为A进程的0x12345678内存
mov cr3,B.DirectoryTableBase;
mov eax,dword ptr ds:[0x12345678];eax为B进程的0x12345678内存
mov cr3,C.DirectoryTableBase;
mov eax,dword ptr ds:[0x12345678];eax为C进程的0x12345678内存

将当前Cr3的值改为其他进程,称为“进程挂靠”

分析NtReadVirtualMemory函数

1
2
3
4
NtReadVirtualMemory
->KiAttachProcess
->修改养父母(0x44偏移的进程进程结构体基址)
->修改Cr3

NtReadVirtualMemory逆向

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
PAGE:004A72CE ; NTSTATUS __stdcall NtReadVirtualMemory(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, SIZE_T NumberOfBytesToRead, PSIZE_T NumberOfBytesRead)
PAGE:004A72CE _NtReadVirtualMemory@20 proc near ; DATA XREF: .text:0040B990↑o
PAGE:004A72CE
PAGE:004A72CE var_2C = dword ptr -2Ch
PAGE:004A72CE var_28 = dword ptr -28h
PAGE:004A72CE Object = dword ptr -24h
PAGE:004A72CE AccessMode = byte ptr -20h
PAGE:004A72CE var_1C = dword ptr -1Ch
PAGE:004A72CE ms_exc = CPPEH_RECORD ptr -18h
PAGE:004A72CE ProcessHandle = dword ptr 8
PAGE:004A72CE BaseAddress = dword ptr 0Ch
PAGE:004A72CE Buffer = dword ptr 10h
PAGE:004A72CE NumberOfBytesToRead= dword ptr 14h
PAGE:004A72CE NumberOfBytesRead= dword ptr 18h
PAGE:004A72CE
PAGE:004A72CE ; FUNCTION CHUNK AT PAGE:0051B87E SIZE 0000000B BYTES
PAGE:004A72CE ; FUNCTION CHUNK AT PAGE:0051B88E SIZE 0000000E BYTES
PAGE:004A72CE ; FUNCTION CHUNK AT PAGE:0051B8A1 SIZE 0000000F BYTES
PAGE:004A72CE ; FUNCTION CHUNK AT PAGE:0051B8B5 SIZE 00000004 BYTES
PAGE:004A72CE ; FUNCTION CHUNK AT PAGE:0051B8BE SIZE 00000008 BYTES
PAGE:004A72CE
PAGE:004A72CE ; __unwind { // __SEH_prolog
PAGE:004A72CE push 1Ch
PAGE:004A72D0 push offset stru_420E18
PAGE:004A72D5 call __SEH_prolog
PAGE:004A72DA mov eax, large fs:124h
PAGE:004A72E0 mov edi, eax
PAGE:004A72E2 mov al, [edi+140h]
PAGE:004A72E8 mov [ebp+AccessMode], al
PAGE:004A72EB mov esi, [ebp+NumberOfBytesToRead]
PAGE:004A72EE test al, al
PAGE:004A72F0 jz loc_4A73BC
PAGE:004A72F6 mov eax, [ebp+BaseAddress]
PAGE:004A72F9 lea edx, [esi+eax]
PAGE:004A72FC cmp edx, eax
PAGE:004A72FE jb loc_4A73B5
PAGE:004A7304 mov eax, [ebp+Buffer]
PAGE:004A7307 lea ecx, [esi+eax]
PAGE:004A730A cmp ecx, eax
PAGE:004A730C jb loc_4A73B5
PAGE:004A7312 mov eax, _MmHighestUserAddress
PAGE:004A7317 cmp edx, eax
PAGE:004A7319 ja loc_4A73B5
PAGE:004A731F cmp ecx, eax
PAGE:004A7321 ja loc_4A73B5
PAGE:004A7327 mov ebx, [ebp+NumberOfBytesRead]
PAGE:004A732A test ebx, ebx
PAGE:004A732C jz short loc_4A7347
PAGE:004A732E ; __try { // __except at loc_51B8A1
PAGE:004A732E and [ebp+ms_exc.registration.TryLevel], 0
PAGE:004A7332 mov eax, _MmUserProbeAddress
PAGE:004A7337 cmp ebx, eax
PAGE:004A7339 jnb loc_51B87E
PAGE:004A733F
PAGE:004A733F loc_4A733F: ; CODE XREF: NtReadVirtualMemory(x,x,x,x,x)+745B6↓j
PAGE:004A733F mov eax, [ebx]
PAGE:004A7341 mov [ebx], eax
PAGE:004A7341 ; } // starts at 4A732E
PAGE:004A7343 or [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
PAGE:004A7347
PAGE:004A7347 loc_4A7347: ; CODE XREF: NtReadVirtualMemory(x,x,x,x,x)+5E↑j
PAGE:004A7347 ; NtReadVirtualMemory(x,x,x,x,x)+F1↓j
PAGE:004A7347 xor eax, eax
PAGE:004A7349 mov [ebp+var_28], eax
PAGE:004A734C mov [ebp+var_1C], eax
PAGE:004A734F cmp esi, eax
PAGE:004A7351 jz short loc_4A7396
PAGE:004A7353 push eax ; HandleInformation
PAGE:004A7354 lea eax, [ebp+Object]
PAGE:004A7357 push eax ; Object
PAGE:004A7358 push dword ptr [ebp+AccessMode] ; AccessMode
PAGE:004A735B push _PsProcessType ; ObjectType
PAGE:004A7361 push 10h ; DesiredAccess
PAGE:004A7363 push [ebp+ProcessHandle] ; Handle
PAGE:004A7366 call _ObReferenceObjectByHandle@24 ; ObReferenceObjectByHandle(x,x,x,x,x,x)
PAGE:004A736B mov [ebp+var_1C], eax
PAGE:004A736E test eax, eax
PAGE:004A7370 jnz short loc_4A7396
PAGE:004A7372 lea eax, [ebp+var_28]
PAGE:004A7375 push eax
PAGE:004A7376 push dword ptr [ebp+AccessMode]
PAGE:004A7379 push esi
PAGE:004A737A push [ebp+Buffer]
PAGE:004A737D push dword ptr [edi+44h]
PAGE:004A7380 push [ebp+BaseAddress]
PAGE:004A7383 push [ebp+Object]
PAGE:004A7386 call _MmCopyVirtualMemory@28 ; MmCopyVirtualMemory(x,x,x,x,x,x,x)//核心代码
PAGE:004A738B mov [ebp+var_1C], eax
PAGE:004A738E mov ecx, [ebp+Object] ; Object
PAGE:004A7391 call @ObfDereferenceObject@4 ; ObfDereferenceObject(x)
PAGE:004A7396
PAGE:004A7396 loc_4A7396: ; CODE XREF: NtReadVirtualMemory(x,x,x,x,x)+83↑j
PAGE:004A7396 ; NtReadVirtualMemory(x,x,x,x,x)+A2↑j
PAGE:004A7396 test ebx, ebx
PAGE:004A7398 jz short loc_4A73AA
PAGE:004A739A ; __try { // __except at loc_51B8BE
PAGE:004A739A mov [ebp+ms_exc.registration.TryLevel], 1
PAGE:004A73A1 mov eax, [ebp+var_28]
PAGE:004A73A4 mov [ebx], eax
PAGE:004A73A4 ; } // starts at 4A739A
PAGE:004A73A6
PAGE:004A73A6 loc_4A73A6: ; CODE XREF: NtReadVirtualMemory(x,x,x,x,x)+745F3↓j
PAGE:004A73A6 or [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
PAGE:004A73AA
PAGE:004A73AA loc_4A73AA: ; CODE XREF: NtReadVirtualMemory(x,x,x,x,x)+CA↑j
PAGE:004A73AA mov eax, [ebp+var_1C]
PAGE:004A73AD
PAGE:004A73AD loc_4A73AD: ; CODE XREF: NtReadVirtualMemory(x,x,x,x,x)+EC↓j
PAGE:004A73AD ; NtReadVirtualMemory(x,x,x,x,x)+745DD↓j
PAGE:004A73AD call __SEH_epilog
PAGE:004A73B2 retn 14h
PAGE:004A73B5 ; ---------------------------------------------------------------------------
PAGE:004A73B5
PAGE:004A73B5 loc_4A73B5: ; CODE XREF: NtReadVirtualMemory(x,x,x,x,x)+30↑j
PAGE:004A73B5 ; NtReadVirtualMemory(x,x,x,x,x)+3E↑j ...
PAGE:004A73B5 mov eax, 0C0000005h
PAGE:004A73BA jmp short loc_4A73AD
PAGE:004A73BC ; ---------------------------------------------------------------------------
PAGE:004A73BC
PAGE:004A73BC loc_4A73BC: ; CODE XREF: NtReadVirtualMemory(x,x,x,x,x)+22↑j
PAGE:004A73BC mov ebx, [ebp+NumberOfBytesRead]
PAGE:004A73BF jmp short loc_4A7347
PAGE:004A73BF ; } // starts at 4A72CE
PAGE:004A73BF _NtReadVirtualMemory@20 endp

MmCopyVirtualMemory

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
PAGE:004A1634 ; __stdcall MmCopyVirtualMemory(x, x, x, x, x, x, x)
PAGE:004A1634 _MmCopyVirtualMemory@28 proc near ; CODE XREF: IopCheckHardErrorsDisabled(x)+7D↑p
PAGE:004A1634 ; NtReadVirtualMemory(x,x,x,x,x)+B8↓p ...
PAGE:004A1634
PAGE:004A1634 BugCheckParameter1= dword ptr 8
PAGE:004A1634 arg_4 = dword ptr 0Ch
PAGE:004A1634 arg_8 = dword ptr 10h
PAGE:004A1634 Address = dword ptr 14h
PAGE:004A1634 Length = dword ptr 18h
PAGE:004A1634 AccessMode = byte ptr 1Ch
PAGE:004A1634 arg_18 = dword ptr 20h
PAGE:004A1634
PAGE:004A1634 ; FUNCTION CHUNK AT PAGE:004A5AF4 SIZE 00000029 BYTES
PAGE:004A1634 ; FUNCTION CHUNK AT PAGE:0051B865 SIZE 00000019 BYTES
PAGE:004A1634
PAGE:004A1634 mov edi, edi
PAGE:004A1636 push ebp
PAGE:004A1637 mov ebp, esp
PAGE:004A1639 cmp [ebp+Length], 0
PAGE:004A163D jz loc_51B865
PAGE:004A1643 push ebx
PAGE:004A1644 mov ebx, [ebp+BugCheckParameter1]
PAGE:004A1647 mov ecx, ebx
PAGE:004A1649 mov eax, large fs:124h
PAGE:004A164F cmp ebx, [eax+44h]
PAGE:004A1652 jnz short loc_4A1657
PAGE:004A1654 mov ecx, [ebp+arg_8]
PAGE:004A1657
PAGE:004A1657 loc_4A1657: ; CODE XREF: MmCopyVirtualMemory(x,x,x,x,x,x,x)+1E↑j
PAGE:004A1657 add ecx, 80h
PAGE:004A165D mov [ebp+BugCheckParameter1], ecx
PAGE:004A1660 call @ExAcquireRundownProtection@4 ; ExAcquireRundownProtection(x)
PAGE:004A1665 test al, al
PAGE:004A1667 jz loc_51B86C
PAGE:004A166D cmp [ebp+Length], 1FFh
PAGE:004A1674 push esi
PAGE:004A1675 push edi
PAGE:004A1676 mov edi, [ebp+arg_18]
PAGE:004A1679 ja loc_4A5AF4
PAGE:004A167F
PAGE:004A167F loc_4A167F: ; CODE XREF: MmCopyVirtualMemory(x,x,x,x,x,x,x)+7A245↓j
PAGE:004A167F push edi ; int
PAGE:004A1680 push dword ptr [ebp+AccessMode] ; char
PAGE:004A1683 push [ebp+Length] ; Length
PAGE:004A1686 push [ebp+Address] ; Address
PAGE:004A1689 push [ebp+arg_8] ; ULONG_PTR
PAGE:004A168C push [ebp+arg_4] ; int
PAGE:004A168F push ebx ; BugCheckParameter1
PAGE:004A1690 call _MiDoPoolCopy@28 ; MiDoPoolCopy(x,x,x,x,x,x,x)//真正的copy函数
PAGE:004A1695 mov esi, eax
PAGE:004A1697
PAGE:004A1697 loc_4A1697: ; CODE XREF: MmCopyVirtualMemory(x,x,x,x,x,x,x)+44DE↓j
PAGE:004A1697 mov ecx, [ebp+BugCheckParameter1]
PAGE:004A169A call @ExReleaseRundownProtection@4 ; ExReleaseRundownProtection(x)
PAGE:004A169F pop edi
PAGE:004A16A0 mov eax, esi
PAGE:004A16A2 pop esi
PAGE:004A16A3
PAGE:004A16A3 loc_4A16A3: ; CODE XREF: MmCopyVirtualMemory(x,x,x,x,x,x,x)+7A23D↓j
PAGE:004A16A3 pop ebx
PAGE:004A16A4
PAGE:004A16A4 loc_4A16A4: ; CODE XREF: MmCopyVirtualMemory(x,x,x,x,x,x,x)+7A233↓j
PAGE:004A16A4 pop ebp
PAGE:004A16A5 retn 1Ch
PAGE:004A16A5 _MmCopyVirtualMemory@28 endp

MiDoPoolCopy

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
PAGE:004A180D ; int __stdcall MiDoPoolCopy(ULONG_PTR BugCheckParameter1, int, ULONG_PTR, PVOID Address, SIZE_T Length, char, int)
PAGE:004A180D _MiDoPoolCopy@28 proc near ; CODE XREF: MmCopyVirtualMemory(x,x,x,x,x,x,x)+5C↑p
PAGE:004A180D
PAGE:004A180D var_258 = byte ptr -258h
PAGE:004A180D var_58 = dword ptr -58h
PAGE:004A180D var_40 = dword ptr -40h
PAGE:004A180D var_3C = dword ptr -3Ch
PAGE:004A180D var_38 = dword ptr -38h
PAGE:004A180D var_34 = dword ptr -34h
PAGE:004A180D var_30 = dword ptr -30h
PAGE:004A180D var_2C = dword ptr -2Ch
PAGE:004A180D var_28 = dword ptr -28h
PAGE:004A180D var_24 = dword ptr -24h
PAGE:004A180D var_20 = dword ptr -20h
PAGE:004A180D P = dword ptr -1Ch
PAGE:004A180D ms_exc = CPPEH_RECORD ptr -18h
PAGE:004A180D BugCheckParameter1= dword ptr 8
PAGE:004A180D arg_4 = dword ptr 0Ch
PAGE:004A180D arg_8 = dword ptr 10h
PAGE:004A180D Address = dword ptr 14h
PAGE:004A180D Length = dword ptr 18h
PAGE:004A180D arg_14 = byte ptr 1Ch
PAGE:004A180D arg_18 = dword ptr 20h
PAGE:004A180D
PAGE:004A180D ; FUNCTION CHUNK AT PAGE:004E783C SIZE 0000000D BYTES
PAGE:004A180D ; FUNCTION CHUNK AT PAGE:0051B7A5 SIZE 00000034 BYTES
PAGE:004A180D ; FUNCTION CHUNK AT PAGE:0051B7DE SIZE 00000019 BYTES
PAGE:004A180D ; FUNCTION CHUNK AT PAGE:0051B7FC SIZE 00000069 BYTES
PAGE:004A180D
PAGE:004A180D ; __unwind { // __SEH_prolog
PAGE:004A180D push 248h
PAGE:004A1812 push offset stru_41A608
PAGE:004A1817 call __SEH_prolog
PAGE:004A181C mov eax, large fs:124h
PAGE:004A1822 mov eax, [ebp+arg_4]
PAGE:004A1825 mov [ebp+var_24], eax
PAGE:004A1828 mov eax, [ebp+Address]
PAGE:004A182B mov [ebp+var_30], eax
PAGE:004A182E mov eax, 10000h
PAGE:004A1833 mov edi, eax
PAGE:004A1835 cmp [ebp+Length], eax
PAGE:004A1838 ja short loc_4A183D
PAGE:004A183A mov edi, [ebp+Length]
PAGE:004A183D
PAGE:004A183D loc_4A183D: ; CODE XREF: MiDoPoolCopy(x,x,x,x,x,x,x)+2B↑j
PAGE:004A183D and [ebp+var_2C], 0
PAGE:004A1841 mov esi, 200h
PAGE:004A1846 cmp [ebp+Length], esi
PAGE:004A1849 ja loc_51B7A5
PAGE:004A184F
PAGE:004A184F loc_4A184F: ; CODE XREF: MiDoPoolCopy(x,x,x,x,x,x,x)+79FB0↓j
PAGE:004A184F lea eax, [ebp+var_258]
PAGE:004A1855 mov [ebp+P], eax
PAGE:004A1858
PAGE:004A1858 loc_4A1858: ; CODE XREF: MiDoPoolCopy(x,x,x,x,x,x,x)+79FC7↓j
PAGE:004A1858 xor eax, eax
PAGE:004A185A mov [ebp+var_34], eax
PAGE:004A185D mov [ebp+var_38], eax
PAGE:004A1860 mov ecx, [ebp+Length]
PAGE:004A1863 mov [ebp+var_20], ecx
PAGE:004A1866 mov ebx, edi
PAGE:004A1868 mov [ebp+var_28], eax
PAGE:004A186B
PAGE:004A186B loc_4A186B: ; CODE XREF: MiDoPoolCopy(x,x,x,x,x,x,x)+141↓j
PAGE:004A186B xor eax, eax
PAGE:004A186D cmp [ebp+var_20], eax
PAGE:004A1870 jbe loc_4A1953
PAGE:004A1876 cmp [ebp+var_20], ebx
PAGE:004A1879 jb loc_51B7C5
PAGE:004A187F
PAGE:004A187F loc_4A187F: ; CODE XREF: MiDoPoolCopy(x,x,x,x,x,x,x)+79FBB↓j
PAGE:004A187F lea eax, [ebp+var_58]
PAGE:004A1882 push eax ; int
PAGE:004A1883 push [ebp+BugCheckParameter1] ; BugCheckParameter1
PAGE:004A1886 call _KeStackAttachProcess@8 ; KeStackAttachProcess(x,x)//开始挂靠目标进程函数
PAGE:004A188B xor esi, esi
PAGE:004A188D mov [ebp+var_3C], esi
PAGE:004A1890 ; __try { // __except at loc_51B7FC
PAGE:004A1890 mov [ebp+ms_exc.registration.TryLevel], esi
PAGE:004A1893 mov ecx, [ebp+arg_4]
PAGE:004A1896 cmp [ebp+var_24], ecx
PAGE:004A1899 jnz short loc_4A18C8
PAGE:004A189B cmp [ebp+arg_14], 0
PAGE:004A189F jz short loc_4A18C8
PAGE:004A18A1 mov [ebp+var_28], 1
PAGE:004A18A8 mov eax, [ebp+Length]
PAGE:004A18AB cmp eax, esi
PAGE:004A18AD jz short loc_4A18C5
PAGE:004A18AF add eax, ecx
PAGE:004A18B1 cmp eax, ecx
PAGE:004A18B3 jb loc_4E7844
PAGE:004A18B9 cmp eax, _MmUserProbeAddress
PAGE:004A18BF ja loc_4E7844
PAGE:004A18C5
PAGE:004A18C5 loc_4A18C5: ; CODE XREF: MiDoPoolCopy(x,x,x,x,x,x,x)+A0↑j
PAGE:004A18C5 ; PAGE:004E7849↓j
PAGE:004A18C5 mov [ebp+var_28], esi
PAGE:004A18C8
PAGE:004A18C8 loc_4A18C8: ; CODE XREF: MiDoPoolCopy(x,x,x,x,x,x,x)+8C↑j
PAGE:004A18C8 ; MiDoPoolCopy(x,x,x,x,x,x,x)+92↑j
PAGE:004A18C8 mov ecx, ebx
PAGE:004A18CA mov esi, [ebp+var_24]
PAGE:004A18CD mov edi, [ebp+P]
PAGE:004A18D0 mov eax, ecx
PAGE:004A18D2 shr ecx, 2
PAGE:004A18D5 rep movsd//开始复制
PAGE:004A18D7 mov ecx, eax
PAGE:004A18D9 and ecx, 3
PAGE:004A18DC rep movsb
PAGE:004A18DE lea eax, [ebp+var_58]
PAGE:004A18E1 push eax
PAGE:004A18E2 call _KeUnstackDetachProcess@4 ; KeUnstackDetachProcess(x)
PAGE:004A18E7 lea eax, [ebp+var_58]
PAGE:004A18EA push eax ; int
PAGE:004A18EB push [ebp+arg_8] ; BugCheckParameter1
PAGE:004A18EE call _KeStackAttachProcess@8 ; KeStackAttachProcess(x,x)
PAGE:004A18F3 mov eax, [ebp+arg_4]
PAGE:004A18F6 cmp [ebp+var_24], eax
PAGE:004A18F9 jnz loc_4E783C
PAGE:004A18FF cmp [ebp+arg_14], 0
PAGE:004A1903 jz loc_4E783C
PAGE:004A1909 xor esi, esi
PAGE:004A190B inc esi
PAGE:004A190C mov [ebp+var_28], esi
PAGE:004A190F push esi ; Alignment
PAGE:004A1910 push [ebp+Length] ; Length
PAGE:004A1913 push [ebp+Address] ; Address
PAGE:004A1916 call _ProbeForWrite@12 ; ProbeForWrite(x,x,x)
PAGE:004A191B and [ebp+var_28], 0
PAGE:004A191F
PAGE:004A191F loc_4A191F: ; CODE XREF: MiDoPoolCopy(x,x,x,x,x,x,x)+46032↓j
PAGE:004A191F mov [ebp+var_3C], esi
PAGE:004A1922 mov ecx, ebx
PAGE:004A1924 mov esi, [ebp+P]
PAGE:004A1927 mov edi, [ebp+var_30]
PAGE:004A192A mov eax, ecx
PAGE:004A192C shr ecx, 2
PAGE:004A192F rep movsd
PAGE:004A1931 mov ecx, eax
PAGE:004A1933 and ecx, 3
PAGE:004A1936 rep movsb
PAGE:004A1936 ; } // starts at 4A1890
PAGE:004A1938 or [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
PAGE:004A193C lea eax, [ebp+var_58]
PAGE:004A193F push eax
PAGE:004A1940 call _KeUnstackDetachProcess@4 ; KeUnstackDetachProcess(x)
PAGE:004A1945 sub [ebp+var_20], ebx
PAGE:004A1948 add [ebp+var_24], ebx
PAGE:004A194B add [ebp+var_30], ebx
PAGE:004A194E jmp loc_4A186B
PAGE:004A1953 ; ---------------------------------------------------------------------------
PAGE:004A1953
PAGE:004A1953 loc_4A1953: ; CODE XREF: MiDoPoolCopy(x,x,x,x,x,x,x)+63↑j
PAGE:004A1953 cmp [ebp+var_2C], eax
PAGE:004A1956 jnz loc_51B857
PAGE:004A195C
PAGE:004A195C loc_4A195C: ; CODE XREF: MiDoPoolCopy(x,x,x,x,x,x,x)+7A053↓j
PAGE:004A195C mov eax, [ebp+arg_18]
PAGE:004A195F mov ecx, [ebp+Length]
PAGE:004A1962 mov [eax], ecx
PAGE:004A1964 xor eax, eax
PAGE:004A1966
PAGE:004A1966 loc_4A1966: ; CODE XREF: MiDoPoolCopy(x,x,x,x,x,x,x)+7A018↓j
PAGE:004A1966 ; MiDoPoolCopy(x,x,x,x,x,x,x)+7A045↓j
PAGE:004A1966 call __SEH_epilog
PAGE:004A196B retn 1Ch
PAGE:004A196B ; } // starts at 4A180D
PAGE:004A196B _MiDoPoolCopy@28 endp

KeStackAttachProcess

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
.text:0041A5A3 ; int __stdcall KeStackAttachProcess(ULONG_PTR BugCheckParameter1, int)
.text:0041A5A3 public _KeStackAttachProcess@8
.text:0041A5A3 _KeStackAttachProcess@8 proc near ; CODE XREF: MmAttachSession(x,x)+58↓p
.text:0041A5A3 ; MiAttachToSecureProcessInSession(x)+43↓p ...
.text:0041A5A3
.text:0041A5A3 BugCheckParameter1= dword ptr 8
.text:0041A5A3 arg_4 = dword ptr 0Ch
.text:0041A5A3
.text:0041A5A3 ; FUNCTION CHUNK AT .text:00445FAF SIZE 0000002E BYTES
.text:0041A5A3
.text:0041A5A3 mov edi, edi
.text:0041A5A5 push ebp
.text:0041A5A6 mov ebp, esp
.text:0041A5A8 push esi
.text:0041A5A9 push edi
.text:0041A5AA mov eax, large fs:124h
.text:0041A5B0 mov esi, eax
.text:0041A5B2 mov eax, large fs:994h
.text:0041A5B8 test eax, eax
.text:0041A5BA jnz loc_445FAF
.text:0041A5C0 mov edi, [ebp+BugCheckParameter1]
.text:0041A5C3 cmp [esi+44h], edi
.text:0041A5C6 jz short loc_41A5FC
.text:0041A5C8 call ds:__imp__KeRaiseIrqlToDpcLevel@0 ; KeRaiseIrqlToDpcLevel()
.text:0041A5CE cmp byte ptr [esi+165h], 0
.text:0041A5D5 mov byte ptr [ebp+BugCheckParameter1], al
.text:0041A5D8 jnz loc_445FCB
.text:0041A5DE lea eax, [esi+14Ch]
.text:0041A5E4 push eax
.text:0041A5E5 push [ebp+BugCheckParameter1]
.text:0041A5E8 push edi
.text:0041A5E9 push esi
.text:0041A5EA call _KiAttachProcess@16 ; KiAttachProcess(x,x,x,x)//挂靠进程函数
.text:0041A5EF mov eax, [ebp+arg_4]
.text:0041A5F2 and dword ptr [eax+10h], 0
.text:0041A5F6
.text:0041A5F6 loc_41A5F6: ; CODE XREF: KeStackAttachProcess(x,x)+63↓j
.text:0041A5F6 ; KeStackAttachProcess(x,x)+2BA35↓j
.text:0041A5F6 pop edi
.text:0041A5F7 pop esi
.text:0041A5F8 pop ebp
.text:0041A5F9 retn 8
.text:0041A5FC ; ---------------------------------------------------------------------------
.text:0041A5FC
.text:0041A5FC loc_41A5FC: ; CODE XREF: KeStackAttachProcess(x,x)+23↑j
.text:0041A5FC mov eax, [ebp+arg_4]
.text:0041A5FF mov dword ptr [eax+10h], 1
.text:0041A606 jmp short loc_41A5F6
.text:0041A606 _KeStackAttachProcess@8 endp

KiAttachProcess

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
.text:00413166 ; __stdcall KiAttachProcess(x, x, x, x)
.text:00413166 _KiAttachProcess@16 proc near ; CODE XREF: KeAttachProcess(x)+47↓p
.text:00413166 ; KeStackAttachProcess(x,x)+47↓p ...
.text:00413166
.text:00413166 arg_0 = dword ptr 8
.text:00413166 arg_4 = dword ptr 0Ch
.text:00413166 arg_8 = byte ptr 10h
.text:00413166 arg_C = dword ptr 14h
.text:00413166
.text:00413166 ; FUNCTION CHUNK AT .text:00445F0D SIZE 0000001E BYTES
.text:00413166 ; FUNCTION CHUNK AT .text:00445F30 SIZE 00000064 BYTES
.text:00413166
.text:00413166 mov edi, edi
.text:00413168 push ebp
.text:00413169 mov ebp, esp
.text:0041316B push ebx
.text:0041316C mov ebx, [ebp+arg_4]
.text:0041316F inc word ptr [ebx+60h]
.text:00413173 push esi
.text:00413174 mov esi, [ebp+arg_0]
.text:00413177 push edi
.text:00413178 push [ebp+arg_C]
.text:0041317B lea edi, [esi+34h]
.text:0041317E push edi
.text:0041317F call _KiMoveApcState@8 ; KiMoveApcState(x,x)
.text:00413184 mov [edi+4], edi
.text:00413187 mov [edi], edi
.text:00413189 lea eax, [esi+3Ch]
.text:0041318C mov [eax+4], eax
.text:0041318F mov [eax], eax
.text:00413191 lea eax, [esi+14Ch]
.text:00413197 cmp [ebp+arg_C], eax
.text:0041319A mov [esi+44h], ebx;将该线程的KTHREAD.ApcState.Process改为要读取的进程的_KPROCESS
.text:0041319D mov byte ptr [esi+48h], 0
.text:004131A1 mov byte ptr [esi+49h], 0
.text:004131A5 mov byte ptr [esi+4Ah], 0
.text:004131A9 jnz short loc_4131BE
.text:004131AB mov [esi+138h], eax
.text:004131B1 mov [esi+13Ch], edi
.text:004131B7 mov byte ptr [esi+165h], 1
.text:004131BE
.text:004131BE loc_4131BE: ; CODE XREF: KiAttachProcess(x,x,x,x)+43↑j
.text:004131BE cmp byte ptr [ebx+65h], 0
.text:004131C2 jnz loc_445F30
.text:004131C8 lea esi, [ebx+40h]
.text:004131CB
.text:004131CB loc_4131CB: ; CODE XREF: KiAttachProcess(x,x,x,x)+32DC0↓j
.text:004131CB mov eax, [esi]
.text:004131CD cmp eax, esi
.text:004131CF jnz loc_445F0D
.text:004131D5 mov eax, [ebp+arg_C]
.text:004131D8 push dword ptr [eax+10h]
.text:004131DB push ebx
.text:004131DC call _KiSwapProcess@8 ; KiSwapProcess(x,x)//真正挂靠进程的函数
.text:004131E1 mov cl, [ebp+arg_8]
.text:004131E4 call @KiUnlockDispatcherDatabase@4 ; KiUnlockDispatcherDatabase(x)
.text:004131E9
.text:004131E9 loc_4131E9: ; CODE XREF: .text:00445F2B↓j
.text:004131E9 ; KiAttachProcess(x,x,x,x)+32E29↓j
.text:004131E9 pop edi
.text:004131EA pop esi
.text:004131EB pop ebx
.text:004131EC pop ebp
.text:004131ED retn 10h
.text:004131ED _KiAttachProcess@16 endp

KiSwapProcess

真正挂靠进程的函数

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
.text:00404AC4 ; __stdcall KiSwapProcess(x, x)
.text:00404AC4 _KiSwapProcess@8 proc near ; CODE XREF: KiAttachProcess(x,x,x,x)+76↓p
.text:00404AC4 ; KeDetachProcess()+73↓p ...
.text:00404AC4
.text:00404AC4 arg_0 = dword ptr 4
.text:00404AC4
.text:00404AC4 mov edx, [esp+arg_0]
.text:00404AC8 xor eax, eax
.text:00404ACA cmp [edx+20h], ax
.text:00404ACE jz short loc_404AFF
.text:00404AD0 mov ecx, ds:0FFDFF03Ch
.text:00404AD6 mov eax, [edx+20h]
.text:00404AD9 mov [ecx+48h], eax
.text:00404ADC mov eax, [edx+24h]
.text:00404ADF mov [ecx+4Ch], eax
.text:00404AE2 mov ecx, ds:0FFDFF038h
.text:00404AE8 mov eax, [edx+28h]
.text:00404AEB mov [ecx+108h], eax
.text:00404AF1 mov eax, [edx+2Ch]
.text:00404AF4 mov [ecx+10Ch], eax
.text:00404AFA mov eax, 48h
.text:00404AFF
.text:00404AFF loc_404AFF: ; CODE XREF: KiSwapProcess(x,x)+A↑j
.text:00404AFF lldt ax
.text:00404B02 mov ecx, ds:0FFDFF040h
.text:00404B08 mov edx, [esp+arg_0]
.text:00404B0C xor eax, eax
.text:00404B0E mov gs, eax
.text:00404B10 mov eax, [edx+18h];eax取要读取进程的页目录表基址
.text:00404B13 mov [ecx+1Ch], eax
.text:00404B16 mov cr3, eax;修改当前Cr3为要读取进程的Cr3
.text:00404B19 mov ax, [edx+30h]
.text:00404B1D mov [ecx+66h], ax
.text:00404B21 retn 8
.text:00404B21 _KiSwapProcess@8 endp

进程挂靠总结

可不可以只修改Cr3而不修改养父母?不可以,如果不修改养父母的值,一旦产生线程切换,再切回来的时候,就会变成自己读自己!(因为线程切换就是去0x44取值放入Cr3中,但0x44并未修改,因此线程切换回自己的时候又会把自己的Cr3再取回来)

如果我们自己写代码,在切换Cr3后关闭中断,并且不调用会导致线程切换的API,就可以不用修改养父母的值。

跨进程读写内存

跨进程的本质是“进程挂靠”,正常情况下,A进程的线程只能访问A进程的地址空间,如果A进程的线程想访问B进程的地址空间,就要修改当前的Cr3的值为B进程的页目录表基址(KPROCESS.DirectoryTableBase)

NtReadVirtualMemory流程解析:

image-20210913175616484

  1. 切换Cr3
  2. 读取数据复制到高2G
  3. 切换Cr3
  4. 从高2G复制到目标位置

NtWriteVirtualMemory流程解析:

image-20210913180101685

  1. 将数据从目标位置复制到高2G地址
  2. 切换Cr3
  3. 从高2G复制到目标位置
  4. 切换Cr3

跨进程读内存实验:

跨进程写内存实验:

驱动另类通信实验

另类通信,诸如内存映射,门调用等等非常多方法。

inlineHOOK SSDT某一个有输入,有输出的函数,来实现驱动另类通信

内核改PE文件思路?只读不能改?运行不能改?似乎有办法

DPC 延迟过程调用

DPC调用过程参考

DPC是“Deferred Procedure Call”的缩写,意为推迟了的过程(函数)调用。这是因为,逻辑上应该放在中断服务程序中完成的操作并非都是那么紧迫,其中有一部分可能相对而言不那么紧迫,而又比较费时间,实际上可以放在开中断的条件下执行。如果把这些操作都放在中断服务程序中,就会使关闭中断的时间太长而引起中断请求的丢失,因为整个中断服务程序通常都是在关中断的条件下执行的。为此,把中断服务程序中不那么紧迫却比较费时,而又不必在关中断条件下执行的操作分割出来,放在另一个函数中,在开中断的条件下加以执行,就可以缩短关中断的时间。这样的函数就是DPC函数。一般而言,中断服务前期的操作是比较紧迫的,并且是必须关中断的,此时可以很快地对外部设备进行操作。此后,剩下的那部分操作便可以稍后在开中断的条件下执行。所以有人曾经把这部分操作称为中断服务的“后半(Bottom Half)”,也有人把这两半分别称为“硬中断”和“软中断”。之所以要把中断服务分成前后两半,是因为一次中断服务的后半不如另一次中断的前半那么紧迫。

为此,内核中要有个DPC请求队列,中断服务程序执行完它的“前半”之后就把一个DPC请求挂入这个队列,要求内核调用相应的DPC函数,然后(形式上)就从中断返回了。接着,如果没有别的中断请求,内核就会扫描这个DPC请求队列,依次在开中断的条件下执行这些DPC函数,直至又发生中断或执行完队列中的所有DPC函数。至于当前线程所要执行的程序,则只有在DPC请求队列为空的时候才会继续得到执行。显然,这里所体现的是“急事急办”的原则,中断是最急的,DPC函数其次,最后才是当前线程。Windows内核的IRQL(即运行级别)就反映了这些活动的轻重缓急,DPC函数是在DISPATCH_LEVEL级别上执行的。

与DPC函数的执行有关的另一个问题是堆栈的使用。我们知道,中断服务程序所使用的堆栈就是当前线程的系统空间堆栈。中断服务程序一般都是比较轻小的,占用一下当前线程的堆栈不至于会有问题;但是DPC函数就不同了,DPC函数有可能是比较大的,如果仍旧占用当前线程的堆栈,在最坏的情况下有可能造成堆栈溢出,所以最好是为DPC函数的执行另外配备一个堆栈

ISR 中断处理,简单,指令少

重要的部分中断处理

不那么重要的部分,操作系统封装成KDPC里面,等下再执行。

_KPRCB结构体中0x860的DpcListHead就是dpc链表

image-20210915203346309

DPC一定比APC先执行

_KDPC结构

1
2
3
4
5
6
7
8
9
10
11
kd> dt _KDPC
nt!_KDPC
+0x000 Type : Int2B//类型
+0x002 Number : UChar//属于哪个核的,即属于哪个KPCR
+0x003 Importance : UChar//三档优先级,0,1,2优先级依次降低
+0x004 DpcListEntry : _LIST_ENTRY//DPC链表,挂在腰上
+0x00c DeferredRoutine : Ptr32 void //回调函数
+0x010 DeferredContext : Ptr32 Void//回调函数的上下文
+0x014 SystemArgument1 : Ptr32 Void//回调函数的参数1
+0x018 SystemArgument2 : Ptr32 Void//回调函数的参数2
+0x01c Lock : Ptr32 Uint4B//DPC的锁

初始化函数KeInitializeDpc

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
;参数分别是_KDPC结构体指针,DPC回调函数,DPC上下文
.text:0040EA12 ; void __stdcall KeInitializeDpc(PRKDPC Dpc, PKDEFERRED_ROUTINE DeferredRoutine, PVOID DeferredContext)
.text:0040EA12 public _KeInitializeDpc@12
.text:0040EA12 _KeInitializeDpc@12 proc near ; CODE XREF: IopInitializeIrpStackProfiler()+29↓p
.text:0040EA12 ; VdmpDelayInterrupt(x)+26D↓p ...
.text:0040EA12
.text:0040EA12 Dpc = dword ptr 8
.text:0040EA12 DeferredRoutine = dword ptr 0Ch
.text:0040EA12 DeferredContext = dword ptr 10h
.text:0040EA12
.text:0040EA12 mov edi, edi
.text:0040EA14 push ebp
.text:0040EA15 mov ebp, esp
.text:0040EA17 mov eax, [ebp+Dpc];eax取_KDPC结构体
.text:0040EA1A mov ecx, [ebp+DeferredRoutine];ecx取回调函数
.text:0040EA1D and dword ptr [eax+1Ch], 0;_KDPC中Lock清零
.text:0040EA21 mov [eax+0Ch], ecx;_KDPC中回调函数设为参数传入的回调函数
.text:0040EA24 mov ecx, [ebp+DeferredContext];ecx取参数dpc上下文
.text:0040EA27 mov word ptr [eax], 13h;_KDPC中TYPE初始设置0x13
.text:0040EA2C mov byte ptr [eax+2], 0;_KDPC只有一个核,所以number设为0
.text:0040EA30 mov byte ptr [eax+3], 1;_KDPC中Importance初始设为中等优先级
.text:0040EA34 mov [eax+10h], ecx;参数上下文放到_KDPC中DeferredContext
.text:0040EA37 pop ebp
.text:0040EA38 retn 0Ch;三个参数,平栈0xC
.text:0040EA38 _KeInitializeDpc@12 endp

插入DPC链表函数KeInsertQueueDpc

IRQL中断执行的优先级

image-20210915222903975

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
.text:0040C8A2 ; BOOLEAN __stdcall KeInsertQueueDpc(PRKDPC Dpc, PVOID SystemArgument1, PVOID SystemArgument2)
.text:0040C8A2 public _KeInsertQueueDpc@12
.text:0040C8A2 _KeInsertQueueDpc@12 proc near ; CODE XREF: KeSetTimerEx(x,x,x,x,x)+25224↓p
.text:0040C8A2 ; KiCalibrateTimeAdjustment(x)+58↓p ...
.text:0040C8A2
.text:0040C8A2 var_8 = dword ptr -8
.text:0040C8A2 NewIrql = byte ptr -1;(实际上该局部变量名称不是很合理,应该是老的Irql)
.text:0040C8A2 Dpc = dword ptr 8
.text:0040C8A2 SystemArgument1 = dword ptr 0Ch
.text:0040C8A2 SystemArgument2 = dword ptr 10h
.text:0040C8A2
.text:0040C8A2 ; FUNCTION CHUNK AT .text:00446240 SIZE 00000028 BYTES
.text:0040C8A2
.text:0040C8A2 mov edi, edi
.text:0040C8A4 push ebp
.text:0040C8A5 mov ebp, esp
.text:0040C8A7 push ecx
.text:0040C8A8 push ecx
.text:0040C8A9 push esi
.text:0040C8AA push edi
.text:0040C8AB mov cl, 1Fh ; NewIrql设置为最高31
.text:0040C8AD call ds:__imp_@KfRaiseIrql@4 ; KfRaiseIrql(x) 提升IRQL函数
.text:0040C8B3 mov [ebp+NewIrql], al;al返回原来的等级,存入NewIrql局部变量中
.text:0040C8B6 db 3Eh
.text:0040C8B6 mov eax, ds:0FFDFF020h;eax取_KPRCB结构
.text:0040C8BC mov edi, [ebp+Dpc];edi取参数的_KDPC结构
.text:0040C8BF mov esi, eax;esi取_KPRCB结构
.text:0040C8C1 lea eax, [esi+8A0h];eax取_KPRCB结构中的DPCLock地址
.text:0040C8C7 mov [ebp+var_8], eax;局部变量var_8存_KPRCB结构中的DPCLock地址
.text:0040C8CA lea eax, [edi+1Ch];eax取_KDPC.Lock地址
.text:0040C8CD mov [ebp+Dpc], eax;参数Dpc空间存放_KDPC.Lock地址
.text:0040C8D0 mov eax, 0;eax清零
.text:0040C8D5 mov ecx, [ebp+Dpc];ecx取_KDPC.Lock地址
.text:0040C8D8 mov edx, [ebp+var_8];edx取_KPRCB结构中的DPCLock地址
.text:0040C8DB cmpxchg [ecx], edx;将累加器AL/AX/EAX/RAX中的值与首操作数(目的操作数)比较,如果相等,第2操作数(源操作数)的值装载到首操作数,zf置1。如果不等, 首操作数的值装载到AL/AX/EAX/RAX并将zf清0。_KDPC.Lock是否等于零,等于零的话,eax取_KPRCB结构中的DPCLock地址;_KDPC.Lock不等于零的话,直接返回不插入了。
.text:0040C8DE test eax, eax;_KPRCB结构中的DPCLock地址是否为零或者_KDPC.Lock是否为零
.text:0040C8E0 mov [ebp+Dpc], eax;eax存入参数空间Dpc
.text:0040C8E3 jnz short loc_40C95A;_KDPC.Lock不等于零就跳转就直接降低IQRL并返回了,不插入了;_KDPC.Lock等于零就不跳,之后eax就是_KPRCB结构中的DPCLock地址
.text:0040C8E5 inc dword ptr [esi+86Ch];_KPRCB.DpcCount+1
.text:0040C8EB add dword ptr [esi+870h], 1;_KPRCB.DpcQueueDepth+1 dpc队列的深度+1
.text:0040C8F2 cmp byte ptr [edi+3], 2;判断_KDPC.Importance优先级是否最高,即2
.text:0040C8F6 mov eax, [ebp+SystemArgument1];
.text:0040C8F9 mov [edi+14h], eax;将该函数参数放入_KDPC.SystemArgument1
.text:0040C8FC mov eax, [ebp+SystemArgument2];
.text:0040C8FF mov [edi+18h], eax;将该函数参数放入_KDPC.SystemArgument2
.text:0040C902 lea ecx, [esi+860h];ecx取_KPRCB.DpcListHead地址
.text:0040C908 lea eax, [edi+4];eax取_KDPC.DpcListEntry地址
.text:0040C90B jz loc_446240;_KDPC.Importance优先级为2(最高)的话跳转
;下面5步将kdpc插入_KPRCB.DpcListHead双向链表中的最末位
.text:0040C911 mov edx, [ecx+4]
.text:0040C914 mov [eax], ecx;
.text:0040C916 mov [eax+4], edx;
.text:0040C919 mov [edx], eax;
.text:0040C91B mov [ecx+4], eax;
.text:0040C91E
.text:0040C91E loc_40C91E: ; CODE XREF: KeInsertQueueDpc(x,x,x)+399AA↓j
.text:0040C91E cmp dword ptr [esi+874h], 0;
.text:0040C925 jnz short loc_40C95A;_KPRCB.DpcRoutineActive不等于0跳转
.text:0040C927 cmp dword ptr [esi+878h], 0
.text:0040C92E jnz short loc_40C95A;_KPRCB.DpcInterruptRequested不等于0则跳转
.text:0040C930 cmp byte ptr [edi+3], 0;判断_KDPC.Importance优先级是否最低,即0
.text:0040C934 jnz short loc_40C948;优先级不是最低的话,跳转
.text:0040C936 mov eax, [esi+870h];eax取_KPRCB.DpcQueueDepth
.text:0040C93C cmp eax, [esi+884h];比较_KPRCB.DpcQueueDepth与_KPRCB.MaximumDpcQueueDepth
.text:0040C942 jb loc_446251;_KPRCB.DpcQueueDepth小于_KPRCB.MaximumDpcQueueDepth就跳转,也是马上执行dpc
.text:0040C948
.text:0040C948 loc_40C948: ; CODE XREF: KeInsertQueueDpc(x,x,x)+92↑j
.text:0040C948 ; KeInsertQueueDpc(x,x,x)+399C1↓j
.text:0040C948 mov cl, 2
.text:0040C94A mov dword ptr [esi+878h], 1;_KPRCB.DpcInterruptRequested置1
.text:0040C954 call ds:__imp_@HalRequestSoftwareInterrupt@4 ; HalRequestSoftwareInterrupt(x); 软中断执行dpc
.text:0040C95A
.text:0040C95A loc_40C95A: ; CODE XREF: KeInsertQueueDpc(x,x,x)+41↑j 降低IRQL并返回
.text:0040C95A ; KeInsertQueueDpc(x,x,x)+83↑j ...
.text:0040C95A mov cl, [ebp+NewIrql] ; NewIrql
.text:0040C95D call ds:__imp_@KfLowerIrql@4 ; KfLowerIrql(x)降低IRQL等级
.text:0040C963 xor eax, eax
.text:0040C965 cmp [ebp+Dpc], eax
.text:0040C968 pop edi
.text:0040C969 setz al
.text:0040C96C pop esi
.text:0040C96D leave
.text:0040C96E retn 0Ch;结束
.text:0040C96E _KeInsertQueueDpc@12 endp

.text:00446240 ; START OF FUNCTION CHUNK FOR _KeInsertQueueDpc@12
.text:00446240
.text:00446240 loc_446240: ; CODE XREF: KeInsertQueueDpc(x,x,x)+69↑j 将kdpc插入_KPRCB.DpcListHead双向链表中的首位
.text:00446240 mov edx, [ecx];
.text:00446242 mov [eax], edx;【_KPRCB.DpcListHead._List_Entry.Flink】存入【_KDPC.DpcListEntry._List_Entry.Flink】
.text:00446244 mov [eax+4], ecx;【_KPRCB.DpcListHead】存入【_KDPC.DpcListEntry._List_Entry.Blink】
.text:00446247 mov [edx+4], eax;【_KDPC.DpcListEntry】存入【_KPRCB.DpcListHead._List_Entry.Flink.Blink】
.text:0044624A mov [ecx], eax;【_KDPC.DpcListEntry】存入【_KPRCB.DpcListHead._List_Entry.Flink】
.text:0044624C jmp loc_40C91E
.text:00446251 ; ---------------------------------------------------------------------------
.text:00446251
.text:00446251 loc_446251: ; CODE XREF: KeInsertQueueDpc(x,x,x)+A0↑j
.text:00446251 mov eax, [esi+880h];eax取_KPRCB.DpcRequestRate
.text:00446257 cmp eax, [esi+888h];比较_KPRCB.DpcRequestRate和_KPRCB.MinimumDpcRate
.text:0044625D jnb loc_40C95A;
.text:00446263 jmp loc_40C948;
.text:00446263 ; END OF FUNCTION CHUNK FOR _KeInsertQueueDpc@12

软中断理解参考

上面的loc_446240画图展示操作

image-20210915232306154

中断和异常是使处理转向的系统事件总结

DPC更详尽的参考

DPC定时器实验

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 <ntddk.h>

//放到全局变量是防止放到栈中,被释放掉了
KDPC dpc = { 0 };
KTIMER timer = { 0 };
LARGE_INTEGER dueTimer = { 0 };//存时间间隔

VOID dpcCall(
_In_ struct _KDPC* Dpc,
_In_opt_ PVOID DeferredContext,
_In_opt_ PVOID SystemArgument1,
_In_opt_ PVOID SystemArgument2
)
{
DbgPrint("dpcCall调用\n");
}

//卸载函数
VOID DriverUnload(PDRIVER_OBJECT driver)
{
KeCancelTimer(&timer);//取消定时器
DbgPrint("停止运行了\n");
}

//入口函数,相当于main函数
NTSTATUS DriverEntry(PDRIVER_OBJECT pdriver, PUNICODE_STRING pReg)
{
//设置一个卸载函数,用于退出
pdriver->DriverUnload = DriverUnload;
KeInitializeDpc(&dpc, dpcCall, NULL);//初始化dpc
KeInitializeTimer(&timer);//初始化计时器
dueTimer.QuadPart = -100 * 1000 * 1000;//负数表示时间间隔,这里是10秒
KeSetTimer(&timer, dueTimer,&dpc);//绑定定时器和dpc,使间隔定时器执行dpc
//KeInsertQueueDpc(&dpc, NULL, NULL);//直接把dpc插入队列准备执行。
return STATUS_SUCCESS;
}

十秒后打印如下结果:

image-20210916200240912