NtReadVirtulMemoery 函数分析

ʕ •ᴥ•ʔ ɔ:

题目

  1. 分析3环函数 ReadProcessMemory ,GetCurrentThread/Process ,在挂靠情况下怎么获取,不挂靠情况下怎么获取。
  2. 了解进程(线程)挂靠、怎么挂靠、挂靠前后APC队列和备用APC队列怎么处理等?
  3. 0x44还是0x220?应该是前者
  4. 分析NtReadVirtulMemoery(内核函数)在挂靠时如何备份和恢复APC队列的。

1 ReadProcessMemory

3环函数ReadProcessMemory:从指定进程读取指定大小内存。

1
2
3
4
5
6
7
BOOL ReadProcessMemory(
[in] HANDLE hProcess, //从该进程读取内存
[in] LPCVOID lpBaseAddress, //指向指定进程中要读取的基本地址的指针
[out] LPVOID lpBuffer, //接收从指定进程的地址空间读取的内容
[in] SIZE_T nSize, //从指定进程中读取的字节数
[out] SIZE_T *lpNumberOfBytesRead //指向变量的指针,该变量接收实际传输到指定缓冲区的字节数
);

该函数的调用过程:

1
2
ReadProcessMemory --> NtReadVirtualMemory(0BAh) --> NtReadVirtualMemory --> MmCopyVirtualMemory --> 
MiDoPoolCopy --> KeStackAttachProcess --> KiAttachProcess

2 NtReadVirtualMemory(内核)

函数NtReadVirtualMemory:该函数从指定进程的指定地址空间读取指定大小的内存 到 当前进程。(This function copies the specified address range from the specified process into the specified address range of the current process.)

1
2
3
4
5
6
7
NTSTATUS __stdcall NtReadVirtualMemory (
IN HANDLE ProcessHandle, //从该进程读取内存
IN PVOID BaseAddress, //指向指定进程中要读取的基本地址的指针
OUT PVOID Buffer, //接收从指定进程的地址空间读取的内容
IN SIZE_T BufferSize, //从指定进程中读取的字节数
OUT PSIZE_T NumberOfBytesRead OPTIONAL //指向一个接收实际读取字节数的变量
)

函数分析处理流程如下:

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
//从3环调用,进行参数校验检查处理
if(PreviousMode != 0)
{
if(BufferSize < 0 || BaseAddress + BufferSize > MmHighestUserAddress
|| BaseAddress + BufferSize > MmHighestUserAddress)
{
status = 0xC0000005; //STATUS_ACCESS_VIOLATION
return status;
}

if(NumberOfBytesRead == NULL) //不需要实际读了多少字节
{
goto StartBusiness;
}else //验证指针NumberOfBytesRead的合法性
{
try
{
if(NumberOfBytesRead >= MmUserProbeAddress)
MmUserProbeAddress = 0; //触发异常
}except(EXCEPTION_EXECUTE_HANDLER)
{
return GetExceptionCode();
}
}

}
StartBusiness:
if(BufferSize == 0) 异常处理;
Process = ObReferenceObjectByHandle();
MmCopyVirtualMemory();
ObfDereferenceObject();
NumberOfBytesRead赋值;
return status;

以上分析过程可以知道:

  1. 如果是从3环调用函数NtReadVirtualMemory,会进行参数校验检查处理。
  2. 然后调用函数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
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
NTSTATUS NtReadVirtualMemory (
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
OUT PVOID Buffer,
IN SIZE_T BufferSize,
OUT PSIZE_T NumberOfBytesRead OPTIONAL
)
/*++
Routine Description:
This function copies the specified address range from the specified
process into the specified address range of the current process.
Arguments:
ProcessHandle - Supplies an open handle to a process object.
BaseAddress - Supplies the base address in the specified process to be read.
Buffer - Supplies the address of a buffer which receives the contents from the specified process address space.
BufferSize - Supplies the requested number of bytes to read from the specified process.
NumberOfBytesRead - Receives the actual number of bytes transferred into the specified buffer.
Return Value: NTSTATUS.
--*/
{
SIZE_T BytesCopied;
KPROCESSOR_MODE PreviousMode;
PEPROCESS Process;
NTSTATUS Status;
PETHREAD CurrentThread;

PAGED_CODE();

// Get the previous mode and probe output argument if necessary.
CurrentThread = PsGetCurrentThread ();
PreviousMode = KeGetPreviousModeByThread(&CurrentThread->Tcb);
if (PreviousMode != KernelMode) {

if (((PCHAR)BaseAddress + BufferSize < (PCHAR)BaseAddress) ||
((PCHAR)Buffer + BufferSize < (PCHAR)Buffer) ||
((PVOID)((PCHAR)BaseAddress + BufferSize) > MM_HIGHEST_USER_ADDRESS) ||
((PVOID)((PCHAR)Buffer + BufferSize) > MM_HIGHEST_USER_ADDRESS)) {

return STATUS_ACCESS_VIOLATION;
}

if (ARGUMENT_PRESENT(NumberOfBytesRead)) {
try {
ProbeForWriteUlong_ptr (NumberOfBytesRead);

} except(EXCEPTION_EXECUTE_HANDLER) {
return GetExceptionCode();
}
}
}

// If the buffer size is not zero, then attempt to read data from the
// specified process address space into the current process address
// space.
BytesCopied = 0;
Status = STATUS_SUCCESS;
if (BufferSize != 0) {

// Reference the target process.
Status = ObReferenceObjectByHandle(ProcessHandle,
PROCESS_VM_READ,
PsProcessType,
PreviousMode,
(PVOID *)&Process,
NULL);
// If the process was successfully referenced, then attempt to
// read the specified memory either by direct mapping or copying
// through nonpaged pool.
if (Status == STATUS_SUCCESS) {

Status = MmCopyVirtualMemory (Process,
BaseAddress,
PsGetCurrentProcessByThread(CurrentThread),
Buffer,
BufferSize,
PreviousMode,
&BytesCopied);
// Dereference the target process.
ObDereferenceObject(Process);
}
}

// If requested, return the number of bytes read.
if (ARGUMENT_PRESENT(NumberOfBytesRead)) {
try {
*NumberOfBytesRead = BytesCopied;

} except(EXCEPTION_EXECUTE_HANDLER) {
NOTHING;
}
}

return Status;
}

MmUserProbeAddress、MmHighestUserAddress

1
2
3
4
5
6
7
8
9
#define MM_HIGHEST_USER_ADDRESS *MmHighestUserAddress
#define MM_SYSTEM_RANGE_START *MmSystemRangeStart
#define MM_USER_PROBE_ADDRESS *MmUserProbeAddress
#define PTE_BASE 0xc0000000

MmHighestUserAddress = 0x7ffeffff
MmUserProbeAddress = 0x7fff0000
MmSystemRangeStart = 0x80000000
MmUserProbeAddress = MmHighestUserAddress + 1
  • 用户空间可以访问的最高地址是MmHighestUserAddress(0x7ffeffff),从MmUserProbeAddress(0x7fff0000)开始就不能访问了。一般文献中讲Windows系统中用户空间与系统空间的分界线是0x80000000,而这里之所以是0x7fff0000而不是0x80000000,是因为在分界下面留了64KB的空间不让访问,以此作为隔离区。(64KB = 2^6*2^10共16位)

  • MmUserProbeAddress是一个界限,该地址不可用。

    • 3环可用地址<=MmHighestUserAddress
    • 3环可用地址<MmUserProbeAddress

这部分内容的讲解在《Windows内核情景分析5.2 P253》有、《Windows内核原理与实现4.2 P207》。

3 MmCopyVirtualMemory

MmCopyVirtualMemory函数:该函数将指定大小的数据 从指定进程的指定地址空间 拷贝至指定进程的指定地址中去。

函数原型如下:

1
2
3
4
5
6
7
8
9
NTSTATUS __stdcall MmCopyVirtualMemory(
IN PEPROCESS FromProcess, //从该进程复制数据到ToProcess进程
IN CONST VOID *FromAddress, //从该地址开始复制数据
IN PEPROCESS ToProcess, //从FromProcess进程复制数据到该进程
OUT PVOID ToAddress, //将数据复制到该地址
IN SIZE_T BufferSize, //要复制的数据大小字节
IN KPROCESSOR_MODE PreviousMode, //函数调用的先前模式
OUT PSIZE_T NumberOfBytesCopied //指向一个变量,该变量保存实际复制的数据大小
)

通过逆向分析,将函数NtReadVirtualMemory压栈的顺序为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NTSTATUS __stdcall NtReadVirtualMemory (
IN HANDLE ProcessHandle, //从该进程读取内存
IN PVOID BaseAddress, //指向指定进程中要读取的基本地址的指针
OUT PVOID Buffer, //接收从指定进程的地址空间读取的内容
IN SIZE_T BufferSize, //从指定进程中读取的字节数
OUT PSIZE_T NumberOfBytesRead OPTIONAL //指向一个接收实际读取字节数的变量
)

push NumberOfBytesRead; //NumberOfBytesCopied
push PreviousMode; //PreviousMode
push BufferSize; //BufferSize
push Buffer; //ToAddress
push KTHREAD.ApcState.Process;//ToProcess
push BaseAddress; //FromAddress
push ProcessHandle->Object; //FromProcess

可以看到,实际上是将目标进程的数据复制到当前线程所属的进程中。

函数分析处理流程如下:

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
if(BufferSize == 0) return STATUS_SUCCESS;

if(FromProcess == KTHREAD.ApcState.Process)
{
ecx = KTHREAD.ApcState.Process.RundownProtect;
}else
{
ecx = FromProcess.RundownProtect;
}
eax = (BOOLEAN)ExAcquireRundownProtection(ecx);
if(eax != TRUE) return 0xC000010A; //STATUS_PROCESS_IS_TERMINATING,尝试将对象句柄复制到退出进程中或退出进程

if(BufferSize > 0x1FF) //#define POOL_MOVE_THRESHOLD 511 如果要复制的字节数大于511,采用内存映射方式
{
eax = (NTSTATUS)MiDoMappedCopy(); //从指定进程的指定地址复制指定大小的数据 到 当前进程的指定地址
if(eax != 0xC00000A1) //STATUS_WORKING_SET_QUOTA,工作集不够大,无法锁定请求的页面
{
ExReleaseRundownProtection();
return eax; //内存拷贝成功,直接返回
}
*NumberOfBytesRead = 0;
goto CompleteService;
}

eax = (NTSTATUS)MiDoPoolCopy();

CompleteService:
return eax;

通过分析,函数MmCopyVirtualMemory主要做了两件事:

  1. 将FromProcess对象开启运行保护,防止在拷贝内存的时候其进程退出(销毁)。
  2. 如果BufferSize > 0x1FF将调用MiDoMappedCopy,否则调用MiDoPoolCopy。

函数MiDoMappedCopy和MiDoPoolCopy函数功能、参数及返回值都是一样的,只是实现的细节不太一样,将会在下面进行详细的分析。

1
2
3
4
5
6
7
8
9
NTSTATUS __stdcall MiDoMappedCopy (
IN PEPROCESS FromProcess,
IN CONST VOID *FromAddress,
IN PEPROCESS ToProcess,
OUT PVOID ToAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T NumberOfBytesRead
)

函数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
66
67
68
69
70
71
72
73
NTSTATUS MmCopyVirtualMemory(
IN PEPROCESS FromProcess,
IN CONST VOID *FromAddress,
IN PEPROCESS ToProcess,
OUT PVOID ToAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T NumberOfBytesCopied
)
{
NTSTATUS Status;
PEPROCESS ProcessToLock;

// 断言 BufferSize != 0
if (BufferSize == 0) {
ASSERT (FALSE); // No one should call with a zero size.
return STATUS_SUCCESS;
}

// 锁定要读取数据的进程
ProcessToLock = FromProcess;
if (FromProcess == PsGetCurrentProcess()) {
ProcessToLock = ToProcess;
}

// Make sure the process still has an address space.
// 确保进程还活着
if (ExAcquireRundownProtection (&ProcessToLock->RundownProtect) == FALSE) {
return STATUS_PROCESS_IS_TERMINATING;
}

// If the buffer size is greater than the pool move threshold,
// then attempt to write the memory via direct mapping.

// #define POOL_MOVE_THRESHOLD 511
// 如果要复制的字节数大于511,采用内存映射方式
if (BufferSize > POOL_MOVE_THRESHOLD) {
Status = MiDoMappedCopy(FromProcess,
FromAddress,
ToProcess,
ToAddress,
BufferSize,
PreviousMode,
NumberOfBytesCopied);

// If the completion status is not a working quota problem,
// then finish the service. Otherwise, attempt to write the memory through nonpaged pool.
if (Status != STATUS_WORKING_SET_QUOTA) {
goto CompleteService;
}

*NumberOfBytesCopied = 0;
}

// There was not enough working set quota to write the memory via
// direct mapping or the size of the write was below the pool move
// threshold. Attempt to write the specified memory through nonpaged pool.
Status = MiDoPoolCopy(FromProcess,
FromAddress,
ToProcess,
ToAddress,
BufferSize,
PreviousMode,
NumberOfBytesCopied);

// Dereference the target process.
CompleteService:

// Indicate that the vm operation is complete.
ExReleaseRundownProtection (&ProcessToLock->RundownProtect);

return Status;
}

4 MiDoMappedCopy

函数MiDoMappedCopy:该函数将指定大小的数据 从指定进程的指定地址空间 拷贝至指定进程的指定地址中去。

函数原型如下:

1
2
3
4
5
6
7
8
9
NTSTATUS __stdcall MiDoMappedCopy (
IN PEPROCESS FromProcess,
IN CONST VOID *FromAddress,
IN PEPROCESS ToProcess,
OUT PVOID ToAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T NumberOfBytesRead
)

在函数MmCopyVirtualMemory调用本函数前,将参数的压栈顺序如下:

1
2
3
4
5
6
7
push NumberOfBytesRead;
push PreviousMode;
push BufferSize;
push ToAddress;
push ToProcess;
push FromAddress;
push FromProcess;

可以看到,实际上参数跟MmCopyVirtualMemory一摸一样。

⚠️需要注意的是:ToProcess实际上是CurreThread.ApcState.Process,即将数据从FromProcess复制到当前线程所属进程空间。

函数分析处理流程如下:

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
MaximumMoved = MAX_LOCK_SIZE;		//#define MAX_LOCK_SIZE ((ULONG)(14 * PAGE_SIZE))
if(BufferSize <= MAX_LOCK_SIZE) //#define PAGE_SIZE 4096 4096 == 4KB == 0x1000 == 1页
{ //0xE000 == 0xE * 1000 == 14页,14页 内存大小(0xE000)
MaximumMoved = BufferSize; //说明每一次可以拷贝的内存大小为 MAX_LOCK_SIZE,但可以循环进行拷贝
} //BufferSize <= MaximumMoved

InVa = FromAddress;
OutVa = ToAddress;
LeftToMove = BufferSize;
AmountToMove = MaximumMoved;

while(LeftToMove)
{
if(LeftToMove < AmountToMove)
{
AmountToMove = LeftToMove;
}

//将当前进程挂靠到 FromProcess 进程
KeStackAttachProcess(IN FromProcess, OUT ApcState);

//检查用户地址FromAddress是否可读
if(InVa == FromAddress && PreviousMode == UserMode) //UserMode == 1
{
//此处是宏 ProbeForRead(FromAddress, BufferSize, sizeof(CHAR));
//对 BufferSize 进行校验,保证其在 FromProcess 中可访问
if(BufferSize != 0)
{
if(FromAddress + BufferSize < FromAddress || FromAddress + BufferSize > MmUserProbeAddress);
{
ExRaiseAccessViolation(); //抛出异常,退出函数
}
}
}

//Initialize MDL for request
//宏初始化MDL的标头
MmInitializeMdl(Mdl, (PVOID)InVa, AmountToMove); //FromAddress &= 0xFFF; 取 FromAddress 的低12位 FromAddress >> 12;
MmProbeAndLockPages(); //探测指定的虚拟内存页面,使其常驻,并将其锁定在内存中(例如DMA传输)。
//这确保了当设备驱动程序(或硬件)仍在使用页面时,页面无法释放和重新分配。
//将 MDL 描述的物理页映射到虚拟地址,并使调用方能够指定用于创建映射的缓存属性。
//将 MDL 映射到的起始用户虚拟地址
MappedAddress = MmMapLockedPagesSpecifyCache(); //将物理内存映射到虚拟内存,MappedAddress为起始地址

KeUnstackDetachProcess(ApcState);
//将当前进程挂靠到 ToProcess 进程
KeStackAttachProcess(IN ToProcess, OUT ApcState); //上面是附加到FromProcess,这里是附加到ToProcess

//检查用户地址ToAddress是否可写
if(InVa == FromAddress && PreviousMode == UserMode) //UserMode == 1
{
//检查用户模式缓冲区是否实际驻留在地址空间的用户模式部分,是否可写,并且是否正确对齐。
//参数一:指定用户模式缓冲区的开头;参数二:指定用户模式缓冲区的长度(以字节为单位);参数三:指定用户模式缓冲区开头所需的对齐(以字节为单位)
ProbeForWrite(ToAddress,BufferSize,1);
}

RtlCopyMemory (OutVa, MappedAddress, AmountToMove); //数据拷贝到ToAddress

KeUnstackDetachProcess(ApcState);
MmUnmapLockedPages(); //释放由之前对MmMapLockedPages或MmMapLockedPagesSpecifyCache例程的调用设置的映射
MmUnlockPages(); //解锁指定内存描述符列表(MDL)描述的物理页面

LeftToMove -= AmountToMove;
InVa = (PVOID)((ULONG_PTR)InVa + AmountToMove);
OutVa = (PVOID)((ULONG_PTR)OutVa + AmountToMove);
}

if(LeftToMove <= 0)
{
*NumberOfBytesRead = BufferSize;
return STATUS_SUCCESS;
}

本函数是实际的数据拷贝的函数,主要做了以下几件事:

  1. 要拷贝的数据大小先和MAX_LOCK_SIZE比较一下大小,是因为函数MmInitializeMdl每次初始化的内存大小最大只能为MAX_LOCK_SIZE,如果要拷贝的数据BufferSize大于MAX_LOCK_SIZE,则就要多初始化几次,所有会有一个while循环。
  2. 调用了两次KeStackAttachProcess:
    • 第一次:将当前线程所属的进程附加到FromProcess进程,然后将FromAddress(InVa)绑定MDL,然后将MDL映射到虚拟地址MappedAddress。
    • 第二次:将当前线程所属的进程附加到ToProcess进程,然后将MappedAddress中的数据拷贝RtlCopyMemory到ToAddress(OutVa)地址。
  3. 关于内存映射MDL的知识,后续学习火哥之前的内容,或者后面海哥关于内存那块的讲解有涉及的话再学习。

函数MiDoMappedCopy源代码如下:

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
190
NTSTATUS MiDoMappedCopy (
IN PEPROCESS FromProcess,
IN CONST VOID *FromAddress,
IN PEPROCESS ToProcess,
OUT PVOID ToAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T NumberOfBytesRead
)
/*++
Routine Description:
This function copies the specified address range from the specified
process into the specified address range of the current process.
Arguments:
FromProcess - Supplies an open handle to a process object.
FromAddress - Supplies the base address in the specified process to be read.
ToProcess - Supplies an open handle to a process object.
ToAddress - Supplies the address of a buffer which receives the
contents from the specified process address space.
BufferSize - Supplies the requested number of bytes to read from the specified process.
PreviousMode - Supplies the previous processor mode.
NumberOfBytesRead - Receives the actual number of bytes transferred into the specified buffer.
Return Value: NTSTATUS.
--*/

{
KAPC_STATE ApcState;
SIZE_T AmountToMove;
ULONG_PTR BadVa;
LOGICAL Moving;
LOGICAL Probing;
LOGICAL LockedMdlPages;
CONST VOID *InVa;
SIZE_T LeftToMove;
PSIZE_T MappedAddress;
SIZE_T MaximumMoved;
PMDL Mdl;
PFN_NUMBER MdlHack[(sizeof(MDL)/sizeof(PFN_NUMBER)) + (MAX_LOCK_SIZE >> PAGE_SHIFT) + 1];
PVOID OutVa;
LOGICAL MappingFailed;
LOGICAL ExceptionAddressConfirmed;

PAGED_CODE();

MappingFailed = FALSE;

InVa = FromAddress;
OutVa = ToAddress;

//#define MAX_LOCK_SIZE ((ULONG)(14 * PAGE_SIZE))
MaximumMoved = MAX_LOCK_SIZE;
if (BufferSize <= MAX_LOCK_SIZE) {
MaximumMoved = BufferSize;
}

Mdl = (PMDL)&MdlHack[0];

// Map the data into the system part of the address space, then copy it.
LeftToMove = BufferSize;
AmountToMove = MaximumMoved;

Probing = FALSE;

// Initializing BadVa & ExceptionAddressConfirmed is not needed for
// correctness but without it the compiler cannot compile this code
// W4 to check for use of uninitialized variables.

BadVa = 0;
ExceptionAddressConfirmed = FALSE;

#if 0

// It is unfortunate that Windows 2000 and all the releases of NT always
// inadvertently returned from this routine detached, as we must maintain this behavior even now.

KeDetachProcess();

#endif

while (LeftToMove > 0) {
if (LeftToMove < AmountToMove) {
// Set to move the remaining bytes.
AmountToMove = LeftToMove;
}

// attach 到目标进程,返回 attach 前的进程的 APC 队列,即 SavedApcState
KeStackAttachProcess (&FromProcess->Pcb, &ApcState);

MappedAddress = NULL;
LockedMdlPages = FALSE;
Moving = FALSE;
ASSERT (Probing == FALSE);

// We may be touching a user's memory which could be invalid, declare an exception handler.
// 读3环内存,确保可读,设置异常处理
try {

// Probe to make sure that the specified buffer is accessible in the target process.
if ((InVa == FromAddress) && (PreviousMode != KernelMode)){
Probing = TRUE;
ProbeForRead (FromAddress, BufferSize, sizeof(CHAR));
Probing = FALSE;
}

// Initialize MDL for request.
MmInitializeMdl (Mdl, (PVOID)InVa, AmountToMove);

MmProbeAndLockPages (Mdl, PreviousMode, IoReadAccess);

LockedMdlPages = TRUE;

MappedAddress = MmMapLockedPagesSpecifyCache (Mdl,
KernelMode,
MmCached,
NULL,
FALSE,
HighPagePriority);

if (MappedAddress == NULL) {
MappingFailed = TRUE;
ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
}

// Deattach from the FromProcess and attach to the ToProcess.
// deattach 即分离,已经读好了
KeUnstackDetachProcess (&ApcState);
KeStackAttachProcess (&ToProcess->Pcb, &ApcState);

// Now operating in the context of the ToProcess.
if ((InVa == FromAddress) && (PreviousMode != KernelMode)){
Probing = TRUE;
ProbeForWrite (ToAddress, BufferSize, sizeof(CHAR));
Probing = FALSE;
}

Moving = TRUE;
RtlCopyMemory (OutVa, MappedAddress, AmountToMove);

} except (MiGetExceptionInfo (GetExceptionInformation(),
&ExceptionAddressConfirmed,
&BadVa)) {

// If an exception occurs during the move operation or probe,
// return the exception code as the status value.
KeUnstackDetachProcess (&ApcState);

if (MappedAddress != NULL) {
MmUnmapLockedPages (MappedAddress, Mdl);
}
if (LockedMdlPages == TRUE) {
MmUnlockPages (Mdl);
}

if (GetExceptionCode() == STATUS_WORKING_SET_QUOTA) {
return STATUS_WORKING_SET_QUOTA;
}

if ((Probing == TRUE) || (MappingFailed == TRUE)) {
return GetExceptionCode();

}

// If the failure occurred during the move operation, determine
// which move failed, and calculate the number of bytes
// actually moved.
*NumberOfBytesRead = BufferSize - LeftToMove;

if (Moving == TRUE) {
if (ExceptionAddressConfirmed == TRUE) {
*NumberOfBytesRead = (SIZE_T)((ULONG_PTR)BadVa - (ULONG_PTR)FromAddress);
}
}

return STATUS_PARTIAL_COPY;
}

KeUnstackDetachProcess (&ApcState);

MmUnmapLockedPages (MappedAddress, Mdl);
MmUnlockPages (Mdl);

LeftToMove -= AmountToMove;
InVa = (PVOID)((ULONG_PTR)InVa + AmountToMove);
OutVa = (PVOID)((ULONG_PTR)OutVa + AmountToMove);
}

// Set number of bytes moved.
*NumberOfBytesRead = BufferSize;
return STATUS_SUCCESS;
}

5 MiDoPoolCopy

在函数MmCopyVirtualMemory中,有个条件判断:BufferSize > 0x1FF时调用MiDoMappedCopy,否则调用MiDoPoolCopy。

函数MiDoPoolCopy:该函数将指定大小的数据 从指定进程的指定地址空间 拷贝至指定进程的指定地址中去。

函数原型如下:

1
2
3
4
5
6
7
8
9
NTSTATUS __stdcall MiDoPoolCopy (
IN PEPROCESS FromProcess,
IN CONST VOID *FromAddress,
IN PEPROCESS ToProcess,
OUT PVOID ToAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T NumberOfBytesRead
)

在函数MmCopyVirtualMemory调用本函数前,将参数的压栈顺序如下:

1
2
3
4
5
6
7
push NumberOfBytesRead;
push PreviousMode;
push BufferSize;
push ToAddress;
push ToProcess;
push FromAddress;
push FromProcess;

可以看到,实际上参数跟MmCopyVirtualMemory一摸一样。

⚠️需要注意的是:ToProcess实际上是CurreThread.ApcState.Process,即将数据从FromProcess复制到当前线程所属进程空间。

实际上,在进入函数MiDoPoolCopy,所有的处理细节和MiDoMappedCopy类似。

函数分析处理流程如下:

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
LONGLONG StackArray[COPY_STACK_SIZE];

//申请非分页内存(非分页内存,不会被换页,当全局变量用)
//每一次可以申请的内存大小为10页,0x10000
MaximumMoved = MAX_MOVE_SIZE; //#define MAX_MOVE_SIZE (LONG)0x10000,10页
if(BufferSize <= MAX_MOVE_SIZE)
{
MaximumMoved = BufferSize;
}

if(BufferSize > 0x200) //ntkrpamp.exe中是0x200,源代码中是8*COPY_STACK_SIZE = 0x800 #define COPY_STACK_SIZE 256
{ //实际上这里可以看到,系统默认已经分配好一个sizeof(StackArray)大小的空间(这里大小是0x200)
do //如果要申请的内存大小(BufferSize)小于sizeof(StackArray),则直接使用StackArray[0]的地址来接收待复制的数据
{ //如果要申请的内存大小(BufferSize)大于sizeof(StackArray),则会调用ExAllocatePoolWithTag重新分配空间
PoolArea = (PVOID)ExAllocatePoolWithTag(NonPagedPool, MaximumMoved, 'wRmM');
if(PoolArea == NULL)
{
MaximumMoved = MaximumMoved >> 1;
if(MaximumMoved <= 0x200)
{
PoolArea = (PVOID)&StackArray[0];
break;
}
}
break;
}while(TRUE)
}else
{
PoolArea = (PVOID)&StackArray[0];
}

InVa = FromAddress;
OutVa = ToAddress;
LeftToMove = BufferSize;
AmountToMove = MaximumMoved;

while(LeftToMove)
{
if(LeftToMove < AmountToMove)
{
AmountToMove = LeftToMove;
}

KeStackAttachProcess(FromProcess,ApcState);

//检查用户地址FromAddress是否可读
if(InVa == FromAddress && PreviousMode == UserMode) //UserMode == 1
{
//AmountToMove = 1;
//ProbeForRead (FromAddress, BufferSize, sizeof(CHAR));
if(BufferSize != 0)
{
BufferSize += FromAddress;
if(BufferSize < FromAddress || BufferSize > MmUserProbeAddress)
ExRaiseAccessViolation(); //抛出异常,退出函数
}
//AmountToMove = 0;
}

RtlCopyMemory (PoolArea, InVa, AmountToMove);
KeUnstackDetachProcess(ApcState);
KeStackAttachProcess(ToProcess,ApcState);

//检查用户地址ToAddress是否可
if(Inva == FromAddress && PreviousMode == UserMode) //UserMode == 1
{
//用户模式缓冲区是否实际驻留在地址空间的用户模式部分,是否可写,并且是否正确对齐。
ProbeForWrite (ToAddress, BufferSize, sizeof(CHAR));
}

RtlCopyMemory (OutVa, PoolArea, AmountToMove);
KeUnstackDetachProcess(ApcState);

LeftToMove -= AmountToMove;
InVa = (PVOID)((ULONG_PTR)InVa + AmountToMove);
OutVa = (PVOID)((ULONG_PTR)OutVa + AmountToMove);
}//while

ExFreePoolWithTag();
*NumberOfBytesRead = BufferSize;
return STATUS_SUCCESS;

本函数MiDoPoolCopy是实际的数据拷贝的函数,作用实际上和MiDoMappedCopy一样,主要做了以下几件事:

  1. 首先会将要拷贝的数据大小BufferSize同MAX_MOVE_SIZE(0x10000)比较大小,这是因为函数ExAllocatePoolWithTag每次申请的非分页内存大小最大为10页。如果BufferSize > MAX_MOVE_SIZE也没关系,因为通过ExAllocatePoolWithTag申请来的首地址PoolArea可以在while里重复使用。
  2. 在使用ExAllocatePoolWithTag申请内存时,实际上系统已经预先分配好一个数组空间LONGLONG StackArray[COPY_STACK_SIZE],COPY_STACK_SIZE == 256,实际大小sizeof(StackArray) == 8*256 == 0x800,但是ntkrpamp.exe中是0x200。当BufferSize < sizeof(StackArray)时是不需要调用函数ExAllocatePoolWithTag去分配空间的,直接使用&StackArray[0]首地址即可。
  3. 然后就是两次使用函数KeStackAttachProcess将当前进程附加到进程FromProcess和ToProcess中,目的和MiDoMappedCopy函数一样,这里就不多解释了。

函数MiDoMappedCopy源代码如下:

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
NTSTATUS MiDoPoolCopy (
IN PEPROCESS FromProcess,
IN CONST VOID *FromAddress,
IN PEPROCESS ToProcess,
OUT PVOID ToAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T NumberOfBytesRead
)
/*++
Routine Description:
This function copies the specified address range from the specified
process into the specified address range of the current process.
Arguments:
ProcessHandle - Supplies an open handle to a process object.
BaseAddress - Supplies the base address in the specified process to be read.
Buffer - Supplies the address of a buffer which receives the
contents from the specified process address space.
BufferSize - Supplies the requested number of bytes to read from the specified process.
PreviousMode - Supplies the previous processor mode.
NumberOfBytesRead - Receives the actual number of bytes transferred into the specified buffer.
Return Value: NTSTATUS.
--*/
{
KAPC_STATE ApcState;
SIZE_T AmountToMove;
LOGICAL ExceptionAddressConfirmed;
ULONG_PTR BadVa;
PEPROCESS CurrentProcess;
LOGICAL Moving;
LOGICAL Probing;
CONST VOID *InVa;
SIZE_T LeftToMove;
SIZE_T MaximumMoved;
PVOID OutVa;
PVOID PoolArea;
LONGLONG StackArray[COPY_STACK_SIZE];
ULONG FreePool;

PAGED_CODE();

ASSERT (BufferSize != 0);

// Get the address of the current process object and initialize copy
// parameters.
CurrentProcess = PsGetCurrentProcess();

InVa = FromAddress;
OutVa = ToAddress;

// Allocate non-paged memory to copy in and out of.
MaximumMoved = MAX_MOVE_SIZE;
if (BufferSize <= MAX_MOVE_SIZE) {
MaximumMoved = BufferSize;
}

FreePool = FALSE;
if (BufferSize <= sizeof(StackArray)) {
PoolArea = (PVOID)&StackArray[0];
} else {
do {
PoolArea = ExAllocatePoolWithTag (NonPagedPool, MaximumMoved, 'wRmM');
if (PoolArea != NULL) {
FreePool = TRUE;
break;
}

MaximumMoved = MaximumMoved >> 1;
if (MaximumMoved <= sizeof(StackArray)) {
PoolArea = (PVOID)&StackArray[0];
break;
}
} while (TRUE);
}

// Initializing BadVa & ExceptionAddressConfirmed is not needed for
// correctness but without it the compiler cannot compile this code
// W4 to check for use of uninitialized variables.
BadVa = 0;
ExceptionAddressConfirmed = FALSE;

// Copy the data into pool, then copy back into the ToProcess.
LeftToMove = BufferSize;
AmountToMove = MaximumMoved;
Probing = FALSE;

#if 0

// It is unfortunate that Windows 2000 and all the releases of NT always
// inadvertently returned from this routine detached, as we must maintain
// this behavior even now.
KeDetachProcess();

#endif

while (LeftToMove > 0) {

if (LeftToMove < AmountToMove) {

// Set to move the remaining bytes.
AmountToMove = LeftToMove;
}

KeStackAttachProcess (&FromProcess->Pcb, &ApcState);

Moving = FALSE;
ASSERT (Probing == FALSE);

// We may be touching a user's memory which could be invalid,
// declare an exception handler.
try {

// Probe to make sure that the specified buffer is accessible in
// the target process.
if ((InVa == FromAddress) && (PreviousMode != KernelMode)){
Probing = TRUE;
ProbeForRead (FromAddress, BufferSize, sizeof(CHAR));
Probing = FALSE;
}

RtlCopyMemory (PoolArea, InVa, AmountToMove);

KeUnstackDetachProcess (&ApcState);

KeStackAttachProcess (&ToProcess->Pcb, &ApcState);

// Now operating in the context of the ToProcess.
if ((InVa == FromAddress) && (PreviousMode != KernelMode)){
Probing = TRUE;
ProbeForWrite (ToAddress, BufferSize, sizeof(CHAR));
Probing = FALSE;
}

Moving = TRUE;

RtlCopyMemory (OutVa, PoolArea, AmountToMove);

} except (MiGetExceptionInfo (GetExceptionInformation(),
&ExceptionAddressConfirmed,
&BadVa)) {

// If an exception occurs during the move operation or probe,
// return the exception code as the status value.
KeUnstackDetachProcess (&ApcState);

if (FreePool) {
ExFreePool (PoolArea);
}
if (Probing == TRUE) {
return GetExceptionCode();

}

// If the failure occurred during the move operation, determine
// which move failed, and calculate the number of bytes
// actually moved.
*NumberOfBytesRead = BufferSize - LeftToMove;

if (Moving == TRUE) {

// The failure occurred writing the data.
if (ExceptionAddressConfirmed == TRUE) {
*NumberOfBytesRead = (SIZE_T)((ULONG_PTR)(BadVa - (ULONG_PTR)FromAddress));
}

}

return STATUS_PARTIAL_COPY;
}

KeUnstackDetachProcess (&ApcState);

LeftToMove -= AmountToMove;
InVa = (PVOID)((ULONG_PTR)InVa + AmountToMove);
OutVa = (PVOID)((ULONG_PTR)OutVa + AmountToMove);
}

if (FreePool) {
ExFreePool (PoolArea);
}

// Set number of bytes moved.
*NumberOfBytesRead = BufferSize;
return STATUS_SUCCESS;
}

6 KeStackAttachProcess

虽然数据的拷贝已经在函数MiDoMappedCopy/MiDoPoolCopy中完成,但是有一个非常重要的环节,就是将当前线程附加到目的进程中,然后进行数据的拷贝。

实现将当前线程附加到目的进程中的函数就是:KeStackAttachProcess。

KeStackAttachProcess函数:将当前线程附加到目标进程的地址空间。(This function attaches a thread to a target process’ address space and returns information about a previous attached process.)

函数原型如下:

1
2
3
4
VOID __stdcall KeStackAttachProcess (
IN PRKPROCESS Process, //指向目标进程对象的指针
OUT PRKAPC_STATE ApcState //指向 KAPC_STATE 结构的不透明指针。调用方必须从非分页池或从调用方的线程堆栈中为此结构分配存储空间。
)

实际上,分析本函数主要是看一下,线程挂靠前,APC队列(ApcState.KAPC)和备用APC队列(SaveApcState.KAPC)是怎么处理的。

函数分析处理流程如下:

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
//如果 DpcRoutineActive != 0,说明当前线程正在执行DPC,就会蓝屏
//If the current thread is executing a DPC, then bug check.
if(_KPCR.PrcbData.DpcRoutineActive != FALSE)
{ //BugCheckCode #define INVALID_PROCESS_ATTACH_ATTEMPT ((ULONG)0x00000005L)
KeBugCheckEx(INVALID_PROCESS_ATTACH_ATTEMPT,(ULONG_PTR)Process,(ULONG_PTR)Thread->ApcState.Process,
(ULONG)Thread->ApcStateIndex,(ULONG)_KPCR.PrcbData.DpcRoutineActive);
}

if(KTHREAD.ApcState.Process == Process)
{
ApcState.Process = (PRKPROCESS)1; //直接返回当前线程已经所属目标进程地址空间
}else
{
// Raise IRQL to dispatcher level and lock dispatcher database.
//通过提高运行优先级达到禁止线程调度《Windows内核情景分析5.12 P436》
KeAcquireQueuedSpinLockRaiseToSynch();

//以下处理传入KiAttachProcess的最后一个参数不同,一个传本函数的参数ApcState,一个传CurrentThread.SavedApcState
//如果当前线程已经挂靠过其他进程
if(KTHREAD.ApcStateIndex != 0) //0:正常状态,1:挂靠状态
{
KiAttachProcess(CurrentThread,Process,OldIrql,ApcState);
}else //当前线程 还未 挂靠过其他进程
{
KiAttachProcess(CurrentThread,Process,OldIrql,CurrentThread.SavedApcState);
ApcState->Process = NULL;
}
}

return;

本函数的功能很简单:

  1. 如果当前线程正在执行DPC,就会蓝屏。然后将当前的IRQL提升至DPC Level。

  2. 调用函数KiAttachProcess,分两种情况:

    • 当前线程已经挂靠过其他线程:传入最后一个参数是本函数的实参ApcState。
    • 当前线程还未挂靠过其他进程:传入最后一个参数是当前线程的备用APCCurrentThread.SavedApcState。
  3. 请注意:KiAttachProcess的最后一个参数,用来保存线程附加到目标进程后的备用APC队列,对KiAttachProcess来说是OUT PRKAPC_STATE SavedApcState。

    但是对于当前函数KeStackAttachProcess的参数OUT PRKAPC_STATE ApcState来说:如果当前线程已经挂靠过其他进程才会输出OUT ApcState(作用在KiAttachProcess最后一个参数)。

举例说明函数KeStackAttachProcess的返回值:

  • 当前线程 T 已经attach到进程A,返回值ApcState == A.
  • 当前线程 T 未attach到进程,属于当前进程B,返回值ApcState没有先前信息。

函数的源码如下:

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
VOID KeStackAttachProcess (
IN PRKPROCESS Process,
OUT PRKAPC_STATE ApcState
)
/*++
Routine Description:
This function attaches a thread to a target process' address space
and returns information about a previous attached process.
Arguments: Process - Supplies a pointer to a dispatcher object of type process.
Return Value: None.
--*/

{

KIRQL OldIrql;
PRKTHREAD Thread;

ASSERT_PROCESS(Process);
ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);

//
// If the current thread is executing a DPC, then bug check.
// 如果当前线程正在执行 DPC,则蓝屏。

Thread = KeGetCurrentThread();
if (KeIsExecutingDpc() != FALSE) {
KeBugCheckEx(INVALID_PROCESS_ATTACH_ATTEMPT,
(ULONG_PTR)Process,
(ULONG_PTR)Thread->ApcState.Process,
(ULONG)Thread->ApcStateIndex,
(ULONG)KeIsExecutingDpc());
}

//
// If the target process is not the current process, then attach the
// target process. Otherwise, return a distinguished process value to
// indicate that an attach was not performed.
// 如果尝试 attach 自己,那么设置ApcState->Process 等于1
// 否则就正常 attach

if (Thread->ApcState.Process == Process) {
ApcState->Process = (PRKPROCESS)1;

} else {

//
// Raise IRQL to dispatcher level and lock dispatcher database.
// 提升 IRQL 等级到 dispatcher level 并锁定 dispatcher database。
// If the current thread is attached to a process, then save the
// current APC state in the callers APC state structure. Otherwise,
// save the current APC state in the saved APC state structure, and
// return a NULL process pointer.
// 如果当前线程已经 attach 了别的进程,那么保存当前 APC state 到调用者的 APC state 结构
// 否则,保存当前 APC state 到 SavedApcState 结构,并设置 ApcState->Process = NULL
//
// N.B. The dispatcher lock is released ay the attach routine.
// dispatcher lock 在 attach 函数中释放

KiLockDispatcherDatabase(&OldIrql);
if (Thread->ApcStateIndex != 0) {
// 当前线程已经 attach 了一个进程
KiAttachProcess(Thread, Process, OldIrql, ApcState);

} else {
// 当前线程的所属进程就是创建线程的进程
KiAttachProcess(Thread, Process, OldIrql, &Thread->SavedApcState);
ApcState->Process = NULL;
}
}

return;
}

7 KiAttachProcess

函数KiAttachProcess:This function attaches a thread to a target process’ address space. The dispatcher database lock must be held when this routine is called.(调用该函数时必须保持调度程序数据库锁起来)

函数原型如下:

1
2
3
4
5
6
VOID __stdcall KiAttachProcess (
IN PRKTHREAD Thread, //指向当前线程的指针
IN PKPROCESS Process, //指向要附加的进程
IN KIRQL OldIrql, //先前的IRQL
OUT PRKAPC_STATE SavedApcState //输出值,保存备用APC状态
)

从函数KeStackAttachProcess调用该函数时,传入的形参不同:

  • CurrentThread.ApcStateIndex == 0:传入CurrentThread.SavedApcState。
  • CurrentThread.ApcStateIndex == 1:传入KeStackAttachProcess的实参ApcState。

函数分析处理流程如下:

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
Process.StackCount++;		//StackCount:记录当前进程有多少个线程栈位于内存中
KiMoveApcState(CurrentThread->ApcState,SavedApcState); //APC队列备份

//由于在函数KiMoveApcState中,两个链表头已经互换,现在的 Current->ApcState 已经指向参数 SavedApcState

//将当前线程的APC队列置空
CurrentThread->ApcState->ApcListHead[0].Blink = &CurrentThread->ApcState[0];
CurrentThread->ApcState->ApcListHead[0].Flink = &CurrentThread->ApcState[0];
CurrentThread->ApcState->ApcListHead[1].Blink = &CurrentThread->ApcState[1];
CurrentThread->ApcState->ApcListHead[1].Flink = &CurrentThread->ApcState[1];

//修改养父母
CurrentThread->ApcState->Process = Process;

CurrentThread->ApcState->KernelApcInProgress = 0; //内核APC是否正在执行,0:不在执,1:正在执行
CurrentThread->ApcState->KernelApcPending = 0; //是否有正在等待执行的内核APC,0:没有,1:有
CurrentThread->ApcState->UserApcPending = 0; //是否有正在等待执行的用户APC,0:没有,1:有

if(&CurrentThread->SavedApcState == SavedApcState);//SavedApcState为空
{
//表示已经挂靠,实际还没有调用KiSwapProcess去修改CR3挂靠
//ApcStatePointer[0] 指向 SavedApcState
//ApcStatePointer[1] 指向 ApcState
CurrentThread->ApcStatePointer[0] = &CurrentThread->SavedApcState;
CurrentThread->ApcStatePointer[1] = &CurrentThread->.ApcState;
CurrentThread->ApcStateIndex = 1; //ApcStateIndex = 1 挂靠状态
}

//#define ProcessInMemory 0x0
//0:表示进程的虚拟地址空间内容在物理内存中(虚拟内存已经映射到物理内存页中)
if(Process.State == 0) //0x00421A50
{
NextEntry = Process->ReadyListHead.Flink;

//该链表记录了这个进程中处于"就绪状态"但尚未被加入"全局就绪链表"的"线程"
//只要ReadyListHead双向循环链表 非空 就会一直摘链
//每一个摘除来的链对应的线程都会调用KiReadyThread使之成为就绪状态

//下面这个while的含义:将链表中的每个线程从Process->ReadyListHead摘除
//然后加入全局就绪线程链表中(通过调用KiReadyThread)《Windows内核原理与实现P108》
while(NextEntry != &Process->ReadyListHead) //非空表
{
OutThread = (PKTHREAD)(NextEntry - 0x60);
//将NextEntry从Process->ReadyListHead里摘除
NextEntry->Blink->Flink = NextEntry->Flink;
NextEntry->Flink->blink = NextEntry->Blink;
//说明线程是否在所属进程的KPROCESS对象的ReadyListEntry链表中
OutThread->ProcessReadyQueue = 0; //TRUE:在此链表中,FALSE:不在此链表中

//将线程OutThread等待的所有对象从被等待对象网上摘掉,并将OutThread从线程等待链上摘掉使其就绪
KiReadyThread(OutThread);
NextEntry = Process->ReadyListHead->Flink;
}
//修改CR3挂靠
KiSwapProcess(Process,SavedApcState->Process);
KiUnlockDispatcherDatabase();
return;

}else //进程的虚拟内存还没有映射到物理内存页中
{
CurrentThread->State = 1; //1:Ready
CurrentThread->ProcessReadyQueue = 1; //说明线程在所属进程的KPROCESS对象的ReadyListEntry链表中

//先将CurrentThread.WaitListEntry加到Process.ReadyListHead尾节点后面,然后使CurrentThread.WaitListEntry指向ReadyListHead头节点
CurrentThread->WaitListEntry.Flink = &Process->ReadyListHead;
CurrentThread->WaitListEntry.Blink = Process->ReadyListHead.Blink;
Process->ReadyListHead.Blink.Flink = &CurrentThread->WaitListEntry;
Process->ReadyListHead.Blink = &CurrentThread->WaitListEntry;

if(Process->State = 1) //ProcessOutOfMemory,进程的虚拟地址空间内容已被换出物理内存
{
Process->State = 2; //ProcessInTransition,进程的虚拟地址空间内容不在物理内存中,但已请求换入

//SwapListEntry、KiProcessInSwapListHead都是单向链表
//将Process->SwapListEntry的地址插入到全局KiProcessInSwapListHead链表中

//这里需要解释一下,这个KiProcessInSwapListHead单向链表挂着一串不同进程的&Process->SwapListEntry
//然后Process = &Process->SwapListEntry - 0x60,可以找到对应的进程是谁
//SwapListEntry作为单向链表的意义是其在KiProcessInSwapListHead中需要用到Next
//但是在Process->SwapListEntry中使用不到Next的
//也可以理解成KiProcessInSwapListHead串的是一个个进程
eax = KiProcessInSwapListHead->Next;
ecx = &Process->SwapListEntry;
do
{
Process->SwapListEntry->Next = eax;
Process = eax;
edx = &Process->SwapListEntry;
ebx = &KiProcessInSwapListHead;
lock cmpxchg [ebx], edx; //将Process->SwapListEntry的地址插入到全局KiProcessInSwapListHead链表中
//如果[ebx] == eax,[ebx] = edx; [ebx] != eax,eax = [ebx];
}while(eax != Process)

//平衡集管理器的交换线程成功等待到 KiSwapEvent,进而调用 KiInSwapProcesses 函数将之前插入到KiProcessInSwapListHead链表中的进程
//换入内存(通过 MmInSwapProcess 函数),之后将进程状态修改为 ProcessInMemory。
KiSetSwapEvent();
}

//Clear the active processor bit in the previous process and set active processor bit in the process being attached to.
SavedApcState->Process->ActiveProcessors &= ~KPRCB.SetMember; //取反并与,线程切换后要运行在哪一个核上
SavedApcState->Process->ActiveProcessors |= KPRCB.SetMembe;
KeAcquireQueuedSpinLockRaiseToSynch();
KiSwapThread();
}//else

return;

函数KiAttachProcess功能相对复杂:

  1. 先调用函数KiMoveApcState将CurrentThread的ApcState备份到实参SavedApcState中。然后将当前线的ApcState初始化为空。
  2. 修改养父母:CurrentThread->ApcState->Process = Process;。
  3. 判断挂靠后的进程状态:
    • ProcessInMemory:虚拟内存已经映射到物理内存页中,目标进程已经在内存中。将目标进程中的每个线程从Process->ReadyListHead摘除,然后加入全局就绪线程链表中(通过调用KiReadyThread)《Windows内核原理与实现P108》。追后调用函数KiSwapProcess去完成进程挂靠修改CR3。
    • ProcessOutOfMemory:进程的虚拟地址空间内容已被换出物理内存。先将CurrentThread.WaitListEntry挂到Process->ReadyListHead链尾,然后更新CurrentThread.WaitListEntry使其指向Process->ReadyListHead。然后再判断一下进程的状态是否是ProcessOutOfMemory,是的话就将状态置为ProcessInTransition,然后将该进程插入到KiProcessInSwapListHead。最后调用KiSetSwapEvent:平衡集管理器的交换线程成功等待到 KiSwapEvent,进而调用 KiInSwapProcesses 函数将之前插入到KiProcessInSwapListHead链表中的进程换入内存(通过 MmInSwapProcess 函数),之后将进程状态修改为 ProcessInMemory(参考Windows线程调度学习(一)、Windows线程调度学习(一)、Windows线程调度学习(一))。
  4. 最后调用函数KiSwapThread(),该函数进行线程切换,如果切换前后线程养父母不属于同一个,也会切换CR3,即进行进程挂靠。

关于函数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
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
VOID KiAttachProcess (
IN PRKTHREAD Thread,
IN PKPROCESS Process,
IN KIRQL OldIrql,
OUT PRKAPC_STATE SavedApcState
)
/*++
Routine Description:
This function attaches a thread to a target process' address space.
N.B. The dispatcher database lock must be held when this routine is called.
Arguments:
Thread - Supplies a pointer to a dispatcher object of type thread.
Process - Supplies a pointer to a dispatcher object of type process.
OldIrql - Supplies the previous IRQL.
SavedApcState - Supplies a pointer to the APC state structure that receives the saved APC state.
Return Value: None.
--*/
{

PRKTHREAD OutThread;
KAFFINITY Processor;
PLIST_ENTRY NextEntry;
KIRQL HighIrql;

ASSERT(Process != Thread->ApcState.Process);

// Bias the stack count of the target process to signify that a
// thread exists in that process with a stack that is resident.
Process->StackCount += 1;

// Save current APC state and initialize a new APC state.
// KiMoveApcState 的功能是将参数1的APC队列复制一份到参数2
// 这里这么做的原因是 attach 前要把父进程的 APC 队列保存起来,保存到 SavedApcState
KiMoveApcState(&Thread->ApcState, SavedApcState);
// InitializeListHead 的功能是让链表头指向自己
InitializeListHead(&Thread->ApcState.ApcListHead[KernelMode]); // ApcListHead[0]
InitializeListHead(&Thread->ApcState.ApcListHead[UserMode]); // ApcListHead[1]
Thread->ApcState.Process = Process;
Thread->ApcState.KernelApcInProgress = FALSE;
Thread->ApcState.KernelApcPending = FALSE;
Thread->ApcState.UserApcPending = FALSE;
if (SavedApcState == &Thread->SavedApcState) {
Thread->ApcStatePointer[0] = &Thread->SavedApcState; // 原进程的 APC state
Thread->ApcStatePointer[1] = &Thread->ApcState; // attach 进程的 APC state
Thread->ApcStateIndex = 1; // 表示现在已经 attach
}

// If the target process is in memory, then immediately enter the
// new address space by loading a new Directory Table Base. Otherwise,
// insert the current thread in the target process ready list, inswap
// the target process if necessary, select a new thread to run on the
// the current processor and context switch to the new thread.
if (Process->State == ProcessInMemory) {
// It is possible that the process is in memory, but there exist
// threads in the process ready list. This can happen when memory
// management forces a process attach.
NextEntry = Process->ReadyListHead.Flink;
while (NextEntry != &Process->ReadyListHead) {
OutThread = CONTAINING_RECORD(NextEntry, KTHREAD, WaitListEntry);
RemoveEntryList(NextEntry);
OutThread->ProcessReadyQueue = FALSE;
KiReadyThread(OutThread);
NextEntry = Process->ReadyListHead.Flink;
}

KiSwapProcess(Process, SavedApcState->Process);
KiUnlockDispatcherDatabase(OldIrql);

} else {
Thread->State = Ready;
Thread->ProcessReadyQueue = TRUE;
InsertTailList(&Process->ReadyListHead, &Thread->WaitListEntry);
if (Process->State == ProcessOutOfMemory) {
Process->State = ProcessInTransition;
InterlockedPushEntrySingleList(&KiProcessInSwapListHead,
&Process->SwapListEntry);

KiSetSwapEvent();
}

// Clear the active processor bit in the previous process and
// set active processor bit in the process being attached to.
#if !defined(NT_UP)

KiLockContextSwap(&HighIrql);
Processor = KeGetCurrentPrcb()->SetMember;
SavedApcState->Process->ActiveProcessors &= ~Processor;
Process->ActiveProcessors |= Processor;
KiUnlockContextSwap(HighIrql);

#endif

Thread->WaitIrql = OldIrql;
KiSwapThread();
}

return;
}

_KPROCESS_STATE

_KPROCESS_STATE:表示进程当前的状态,共有以下5种。

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef enum _KPROCESS_STATE {
ProcessInMemory, //0,表示进程的虚拟地址空间内容在物理内存中
ProcessOutOfMemory, //1,表示进程的虚拟地址空间内容已被换出物理内存
ProcessInTransition, //2,表示进程的虚拟地址空间内容不在物理内存中,但已请求换入
ProcessOutTransition, //3,表示进程的虚拟地址空间内容存在于物理内存中,但已请求换出
ProcessInSwap, //4,表示正在将进程的虚拟地址空间内容换入物理内存,换入完成后,状态将变更为 ProcessInMemory
ProcessOutSwap //5,表示正在将进程的虚拟地址空间内容换出物理内存,换出完成后,状态将变更为 ProcessOutOfMemory
} KPROCESS_STATE;

// Process State Enumerated Type Values
#define ProcessInMemory 0x0
#define ProcessOutOfMemory 0x1
#define ProcessInTransition 0x2

可参考Windows线程调度学习(一)、Windows线程调度学习(一)、Windows线程调度学习(一)

_KTHREAD_STATE

_KTHREAD_STATE:表示线程当前的调度状态。

共有以下7种(WRK有9种)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef enum _KTHREAD_STATE {
Initialized,
Ready,
Running,
Standby,
Terminated,
Waiting,
Transition
} KTHREAD_STATE;

// Thread State Enumerated Type Values
#define Initialized 0x0
#define Ready 0x1
#define Running 0x2
#define Standby 0x3
#define Terminated 0x4
#define Waiting 0x5
状态 枚举值 说明
Initialized 0 已初始化(Initialized):说明一个线程对象的内部状态已经初始化,这是线程创建过程中的一个内部状态,此时线程尚未加入到进程的线程链表中,也没有启动。
Ready 1 就绪(Ready):代表该线程已经准备就绪,等待被调度执行。当线程调度器选择一个线程来执行时,它只考虑处于就绪状态的线程。此时,线程已被加入到某个处理器的就绪线程链表中。
Running 2 运行(Running):线程正在运行。该线程一直占有处理器,直至分到的时限结束,或者被一个更高优先级的线程抢占,或者线程终止,或者主动放弃处理器执行权,或者进入等待状态。
Standby 3 备用(Standby):处于备用状态的线程已经被选中作为某个处理器上下一个要运行的线程。对于系统中的每个处理器,只能有一个线程可以处于备用状态。然而,一个处于备用状态的线程在真正被执行以前,有可能被更高优先级的线程抢占。
Terminated 4 已终止(Terminated):表示线程已经完成任务,正在进行资源回收。KeTerminateThread``函数用于设置此状态。
Waiting 5 等待(Waiting):表示一个线程正在等待某个条件,比如等待一个分发器对象变成有信号状态,也可以等待多个对象。当等待的条件满足时,线程或者立即开始运行,或者回到就绪状态。
Transition 6 转移(Transition):处于转移状态的线程已经准备好运行,但是它的内核栈不在内存中。一旦它的内核栈被换人内存,则该线程进入就绪状态。
DeferredReady 7 延迟的就绪(DeferredReady):处于延迟的就绪状态的线程也已经准备好可以运行了,但是,与就绪状态不同的是,它尚未确定在哪个处理器上运行。当有机会被调度时,或者直接转人备用状态,或者转到就绪状态。因此,此状态是为了多处理器而引入的,对于单处理器系统没有意义。(WRK中)
GateWait 8 门等待(GateWait):线程正在等待一个门对象。此状态与等待状态类似,只不过它是专门针对门对象而设计。关于门对象和门等待,请参考5.4.3节。(WRK中)

与传统的操作系统中的调度状态相比,这里可能多了后三个状态。这些状态之间的转移图并不复杂,图3.5显示了WRK中的线程状态转移图。

1.png

可参考:

KPROCESS.ReadyListHead、KPROCESS.SwapListEntry

  1. 关于CurrentThread.WaitListEntry:在源码中可以看到,只是将CurrentThread->WaitListEntry指向ReadyListHead头节点,然后将CurrentThread.WaitListEntry加到Process.ReadyListHead尾节点后面。

    并没有将CurrentThread.WaitListEntry链上所有的节点都挂上去,只是将当前线程挂上去。

  2. 将当前进程的Process->SwapListEntry挂到全局的KiProcessInSwapListHead链上去,同样的只是将当前进程挂上去即可,并不是将Process->SwapListEntry链上的所有进程都挂上去。

下面是我分析的Process->SwapListEntry挂入KiProcessInSwapListHead的代码与源码对比:

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
//如下是我分析的
eax = KiProcessInSwapListHead->Next;
ecx = &Process->SwapListEntry;
do
{
Process->SwapListEntry->Next = eax;
Process = eax;
edx = &Process->SwapListEntry;
ebx = &KiProcessInSwapListHead;
lock cmpxchg [ebx], edx; //将Process->SwapListEntry的地址插入到全局KiProcessInSwapListHead链表中
//如果[ebx] == eax,[ebx] = edx; [ebx] != eax,eax = [ebx];
}while(eax != Process)

//源码如下
InterlockedPushEntrySingleList(&KiProcessInSwapListHead, &Process->SwapListEntry);
//This function pushes an entry onto the front of a singly linked list.
//将Entry插到ListHead的前面
InterlockedPushEntrySingleList(PSINGLE_LIST_ENTRY ListHead,PSINGLE_LIST_ENTRY Entry)
{

PSINGLE_LIST_ENTRY FirstEntry;
PSINGLE_LIST_ENTRY NextEntry;

FirstEntry = ListHead->Next;
do {
Entry->Next = FirstEntry;
NextEntry = FirstEntry;
FirstEntry = (PSINGLE_LIST_ENTRY)InterlockedCompareExchangePointer((PVOID *)ListHead,
Entry,
FirstEntry);
} while (FirstEntry != NextEntry);
return FirstEntry;
}

LONG InterlockedCompareExchange(IN OUT LONG* Destination,IN LONG ExChange,IN LONG Comperand)
{
LONG temp = *Destination;

if (Comperand == *Destination) {
*Destination = ExChange;
}

return temp;
}

//然后我将InterlockedPushEntrySingleList源码函数简化成如下代码
InterlockedPushEntrySingleList(PSINGLE_LIST_ENTRY ListHead,PSINGLE_LIST_ENTRY Entry)
{

PSINGLE_LIST_ENTRY FirstEntry;
PSINGLE_LIST_ENTRY NextEntry;

FirstEntry = ListHead->Next;
do {
Entry->Next = FirstEntry;
NextEntry = FirstEntry;
if(FirstEntry == ListHead->Next)
{
ListHead->Next = &Process->SwapListEntry;
}
FirstEntry = ListHead->Next;
} while (FirstEntry != NextEntry);
return FirstEntry;
}

CMPXCHG

关于指令cmpxchg:

Opcode/Instruction Op/En 64-Bit Mode Compat/Leg Mode Description
0F B1/r CMPXCHG r/m32, r32 MR Valid Valid* CMPXCHG r/m32, r32
Compare EAX with r/m32. If equal, ZF is set and r32 is loaded into r/m32. Else, clear ZF and load r/m32 into EAX.

8 KiMoveApcState

KiMoveApcState函数:将参数一的APC状态备份复制到参数二的APC状态,并根据需要重新初始化两个列表头。

函数原型如下:

1
2
3
4
VOID __stdcall KiMoveApcState (
IN PKAPC_STATE Source,
OUT PKAPC_STATE Destination
)

函数分析处理流程如下:

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
//*Destination = *Source, 备份
//_KAPC_STATE共 0x17 == 23字节,字节对齐后为4Byte*6 == 24字节。每次传送4字节,共需要传送6次
mov ecx, 6;
mov esi, Source;
mov edi, Destination;
rep movsd;

//备份内核APC队列
//如果Source为空,则将Destination也置为空
if(Source->ApcListHead[0]->Flink == &Source->ApcListHead[0])
{
Destination->ApcListHead[0]->Blink = &Destination->ApcListHead[0];
Destination->ApcListHead[0]->Flink = &Destination->ApcListHead[0];
}else
{
/*
两个链表头互换
互换的原因很简单:就是让 ApcStatePointer[ApcStateIndex] 永远指向 ApcState
因为挂靠前后 ApcStateIndex分别为0 1
*/
Destination->ApcListHead[0]->Flink = Source->ApcListHead[0]->Flink;
Destination->ApcListHead[0]->Blink = Source->ApcListHead[0]->Blink;
Source->ApcListHead[0]->Flink->Blink = &Destination->ApcListHead[0];
Source->ApcListHead[0]->Blink->Flink = &Destination->ApcListHead[0];
}

//备份用户APC队列
if(Source->ApcListHead[1]->Flink == &Source->ApcListHead[1])
{
Destination->ApcListHead[1]->Blink = &Destination->ApcListHead[1];
Destination->ApcListHead[1]->Flink = &Destination->ApcListHead[1];
}else
{
/*
两个链表头互换
互换的原因很简单:就是让 ApcStatePointer[ApcStateIndex] 永远指向 ApcState
因为挂靠前后 ApcStateIndex分别为0 1
*/
Destination->ApcListHead[1]->Flink = Source->ApcListHead[1]->Flink;
Destination->ApcListHead[1]->Blink = Source->ApcListHead[1]->Blink;
Source->ApcListHead[1]->Flink->Blink = &Destination->ApcListHead[1];
Source->ApcListHead[1]->Blink->Flink = &Destination->ApcListHead[1];
}

return;

⚠️:从函数KiMoveApcState逆向分析的过程已经看到,函数中将两个APC状态的链表头已经互换。

函数源码如下:

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
VOID KiMoveApcState (
IN PKAPC_STATE Source,
OUT PKAPC_STATE Destination
)
/*++
Routine Description:
This function moves the APC state from the source structure to the
destination structure and reinitializes list headers as appropriate.
Arguments:
Source - Supplies a pointer to the source APC state structure.
Destination - Supplies a pointer to the destination APC state structure.
Return Value: None.
--*/
{

PLIST_ENTRY First;
PLIST_ENTRY Last;

// Copy the APC state from the source to the destination.
*Destination = *Source;
if (IsListEmpty(&Source->ApcListHead[KernelMode]) != FALSE) {
InitializeListHead(&Destination->ApcListHead[KernelMode]);

} else {
First = Source->ApcListHead[KernelMode].Flink;
Last = Source->ApcListHead[KernelMode].Blink;
Destination->ApcListHead[KernelMode].Flink = First;
Destination->ApcListHead[KernelMode].Blink = Last;
First->Blink = &Destination->ApcListHead[KernelMode];
Last->Flink = &Destination->ApcListHead[KernelMode];
}

if (IsListEmpty(&Source->ApcListHead[UserMode]) != FALSE) {
InitializeListHead(&Destination->ApcListHead[UserMode]);

} else {
First = Source->ApcListHead[UserMode].Flink;
Last = Source->ApcListHead[UserMode].Blink;
Destination->ApcListHead[UserMode].Flink = First;
Destination->ApcListHead[UserMode].Blink = Last;
First->Blink = &Destination->ApcListHead[UserMode];
Last->Flink = &Destination->ApcListHead[UserMode];
}

return;
}