写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。
看此教程之前,问几个问题,基础知识储备好了吗?保护模式篇学会了吗?练习做完了吗?没有的话就不要继续了。
华丽的分割线
概述
在64位下,有两种CPU
模式,一种是IA-32e
模式,是IA-32
模式的扩展,另一个是Legacy
模式。IA-32e
模式是指内核64位,用户64或32位均可,它强制平坦段,不支持任务切换;而Legacy
模式指内核32位,用户32位支持非平坦段、任务切换、虚拟8086、实模式等。
在IA-32e
模式下,代码段和数据段仍使用64位描述符,强制平坦(FS
,GS
除外);TSS
段描述符扩展到128
位,TSS
段不用来任务切换,主要保存一堆rsp
备用指针;中断门描述符扩展到128
位。
有些寄存器我们需要知道的,如下所示:
MSR 寄存器名 | 索引数值 |
---|---|
IA32_EFER_MSR | C0000080H |
IA32_FS_BASE | C0000100H |
IA32_GS_BASE | C0000101H |
IA32_KERNEL_GS_BASE | C0000102H |
IA32_EFER_MSR
该MSR
结构如下所示:
索引0位表示SYSCALL/SYSRET
这类指令是否启用,索引8位表示IA-32e
模式是否启用,索引10位表示IA-32e
模式是否处于处于活动状态,索引11位知识是否启用PAE
分页的XD
位是否有效。如果到这里有不会的地方,请回去复习。
下面我们在虚拟机中读取该寄存器的值:
kd> rdmsr C0000080
msr[c0000080] = 00000000`00000d01
kd> .formats 00000000`00000d01
Evaluate expression:
Hex: 00000000`00000d01
Decimal: 3329
Octal: 0000000000000000006401
Binary: 00000000 00000000 00000000 00000000 00000000 00000000 00001101 00000001
Chars: ........
Time: Thu Jan 1 08:55:29 1970
Float: low 4.66492e-042 high 0
Double: 1.64474e-320
从上面的值我们可以看出寄存器里面的所有的功能处于启用状态。
IA32_FS_BASE
这个MSR
寄存器保存的是fs
的基址,由于是64位内核,直接读取的话该值是0,我们可以读取一下:
kd> rdmsr C0000100
msr[c0000100] = 00000000`00000000
IA32_GS_BASE
这个MSR
寄存器保存的是gs
的基址,内核指向KPCR
结构体(和32位的fs
一样的作用),我们可以尝试以下:
kd> rdmsr C0000101
msr[c0000101] = fffff805`5e089000
kd> dt _KPCR fffff805`5e089000
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 GdtBase : 0xfffff805`65690fb0 _KGDTENTRY64
+0x008 TssBase : 0xfffff805`6568f000 _KTSS64
+0x010 UserRsp : 0
+0x018 Self : 0xfffff805`5e089000 _KPCR
+0x020 CurrentPrcb : 0xfffff805`5e089180 _KPRCB
+0x028 LockArray : 0xfffff805`5e089870 _KSPIN_LOCK_QUEUE
+0x030 Used_Self : 0x00000079`22c2a000 Void
+0x038 IdtBase : 0xfffff805`6568e000 _KIDTENTRY64
+0x040 Unused : [2] 0
+0x050 Irql : 0 ''
+0x051 SecondLevelCacheAssociativity : 0xc ''
+0x052 ObsoleteNumber : 0 ''
+0x053 Fill0 : 0 ''
+0x054 Unused0 : [3] 0
+0x060 MajorVersion : 1
+0x062 MinorVersion : 1
+0x064 StallScaleFactor : 0xb58
+0x068 Unused1 : [3] (null)
+0x080 KernelReserved : [15] 0
+0x0bc SecondLevelCacheSize : 0x900000
+0x0c0 HalReserved : [16] 0xad178600
+0x100 Unused2 : 0
+0x108 KdVersionBlock : (null)
+0x110 Unused3 : (null)
+0x118 PcrAlign1 : [24] 0
+0x180 Prcb : _KPRCB
IA32_KERNEL_GS_BASE
这个MSR
的作用是作为GS
基址的交换目标。说人话的话就是一个缓存,交换比较方便。我们从3环到0环。从0环到3环,GS
内核指向KPCR
,3环指向TEB
,是如何做到呢?这个就是一个非常重要的寄存器.进0环的时候,把3环的交给它;出0环的时候,把0环的交给它。
我们来读一下:
kd> rdmsr C0000102
msr[c0000102] = 00000079`22c2a000
按照上述说法,这个就是TEB
了,但由于这个是3环的地址,我们无法读取里面的结构体内容。
由于现在64位CPU
一般安装的系统都是IA-32e
模式下的,即内核64位的,故后续所有介绍都会以IA-32e
模式为主。
段与段描述符
在64位系统下,段的保护被进一步弱化。绝大多数的段被强制平坦。何为强制平坦,就是段的首地址和界限的值不再起作用,被保留置0。首地址默认是0,界限默认十六进制全是F
。但也有例外,比如GS
和FS
,因为它们指向一些结构体,这些结构体的首地址肯定不是0。
64位的段和32位的段大体上就上述变化,但有一个段比较特别,那就是代码段,我们来看一下它的结构:
代码段比32位的多了个位L
,它的作用是来指示是32位的还是64位的,如果是0表示兼容模式(x86
模式),为1则表示x64
模式。
里面还有一个D
位,它的作用是指示默认大小的。如果L == 0
,如果该位是0,默认的数据和地址大小为16位;反之为32位。如果L == 1
,D
位必须为0,否则会触发通用保护异常。
最后要介绍的段就是TSS
段,被扩展为128位,用于满足寻址要求,如下结构所示:
TSS
和32位的操作系统一样,只是用来保存一堆寄存器的,并不用于任务切换。
页
在64位下分页不再是PAE
分页,而是被Intel
称之为4级分页(4-LEVEL PAGING)的东西。当CR0.PG == 1 && CR4.PAE == 1 && IA32_EFER.LME == 1
时会启用该分页模式。4级分页将48
位线性地址转换为52
位物理地址。虽然52
位对应于4 PBytes
,但线性地址仅限于48
位,最多可以访问256 TBytes
的线性地址空间。也就是说剩余的16
位是被保留的。
4级分页使用分页结构的层次结构来生成线性地址的转换。CR3
用于定位第一个分页结构,即PML4
表。将CR3
与4级分页一起使用取决于是否已通过设置CR4.PCIDE
启用进程上下文标识符。
这个CR3
也是结构的,并不直接是物理地址的,如果CR4.PCIDE
为0,则它的结构如下:
如果CR4.PCIDE
为1,则它的结构如下:
其中M
是MAXPHYADDR
的缩写,最大是52,这个值通过CPUID.80000008H:EAX[7:0]
进行查询,在我的虚拟机操作系统这个值为十进制的39
。
kd> r cr4
cr4=0000000000070678
kd> .formats 0000000000070678
Evaluate expression:
Hex: 00000000`00070678
Decimal: 460408
Octal: 0000000000000001603170
Binary: 00000000 00000000 00000000 00000000 00000000 00000111 00000110 01111000
Chars: .......x
Time: Tue Jan 6 15:53:28 1970
Float: low 6.45169e-040 high 0
Double: 2.27472e-318
首先我们得确认这个CR4.PCIDE
是在哪个位上,发现是在索引17位:
这个位被置1了,说明是第二种形式。有关这部分细节,对于我们不写操作系统的人来说似乎没啥用处,就不追究了。
下面我们来看看4级分页线性地址的结构,如下是线性地址如何翻译成物理地址的:
按照之前的说法套路,给一个非正式的名字9-9-9-9-12
分页。又了之前的底子,我就不赘述内部的详细结构,直接用一张图进行总结(在Intel
白皮书的第2832页):
细心的你可能会发现protection key
这个东西,这是啥呢?这个就是4级分页的一个新的机制,提供更完全的保护,当CR4.PKE == 1
时有效,不幸的是,在我的虚拟机的操作系统并不使用该位,所以无效,如果对这块技术感兴趣请仔细阅读Intel
白皮书的第2835页。
当然上述分页还有大页的情形,如下所示:
下面我们就用x86
下的实验方式来学习分页,首先你得有新版的CheatEngine
,下面开始:
如上图所示,我打开了一个CheatEngine
和一个Notepad
,并且在记事本里面写上hello,world!
。然后我们开始老套路搜索内存:
把,
改为%
,定位到指定的地址,如下图所示:
我们先用vtop
执行偷懒看看结果:
kd> !process 0 0 notepad.exe
PROCESS ffff8f0fec1be080
SessionId: 1 Cid: 091c Peb: 4e42d08000 ParentCid: 0b70
DirBase: 9bd65002 ObjectTable: ffffdd8291064c40 HandleCount: 356.
Image: notepad.exe
kd> !vtop 9bd65000 1DEFB0044B0
Amd64VtoP: Virt 000001defb0044b0, pagedir 000000009bd65000
Amd64VtoP: PML4E 000000009bd65018
Amd64VtoP: PDPE 000000009d581bd8
Amd64VtoP: PDE 00000000a3882ec0
Amd64VtoP: PTE 00000000827ae020
Amd64VtoP: Mapped phys 0000000027fca4b0
Virtual address 1defb0044b0 translates to physical address 27fca4b0.
kd> !db 27fca4b0
#27fca4b0 68 00 65 00 6c 00 6c 00-6f 00 25 00 77 00 6f 00 h.e.l.l.o.%.w.o.
#27fca4c0 72 00 6c 00 64 00 21 00-0d 00 0a 00 00 00 00 00 r.l.d.!.........
#27fca4d0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
#27fca4e0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
#27fca4f0 00 00 00 00 00 00 00 00-08 00 79 fe de 01 00 00 ..........y.....
#27fca500 00 00 00 00 00 00 00 00-2a f8 73 48 50 71 00 10 ........*.sHPq..
#27fca510 30 48 00 fb de 01 00 00-00 42 00 fb de 01 00 00 0H.......B......
#27fca520 40 48 00 fb de 01 00 00-10 42 00 fb de 01 00 00 @H.......B......
为什么我在遍历进程的后面加上notepad.exe
呢?是因为在Win10
上,就算你原装最低配置,也有一大串的进程,带来很大的烦恼。我明确是记事本的进程叫这个名字,我就在后面限定它。
你或许还有一个疑问,为什么命名目录表基址的值为9bd65002
,但为什么vtop
的时候后面的2
没了呢?那么我们再回过头来看看上图的Cr3
结构你就会明白为什么。
在做这个实验的时候,发现了一个十分有意思的现象,当你恢复虚拟机执行其他操作的时候,再次中断回到Windbg
,注意我没有退出记事本,回来发现它的DirBase
发生了变化。
好,我们继续我们的实验:
kd> !process 0 0 notepad.exe
PROCESS ffff8f0fec1be080
SessionId: 1 Cid: 091c Peb: 4e42d08000 ParentCid: 0b70
DirBase: 9c065002 ObjectTable: ffffdd8291064c40 HandleCount: 359.
Image: notepad.exe
kd> !vtop 9c065000 1DEFB0044B0
Amd64VtoP: Virt 000001defb0044b0, pagedir 000000009c065000
Amd64VtoP: PML4E 000000009c065018
Amd64VtoP: PDPE 0000000036881bd8
Amd64VtoP: PDE 00000000a3982ec0
Amd64VtoP: PTE 00000000828ae020
Amd64VtoP: Mapped phys 0000000027fca4b0
好我们手动拆分虚拟地址并用访问物理地址的方式进行该实验。首先拆分以下我们的虚拟地址:
000000011 = 0x3
101111011 = 0x17B
111011000 = 0x1D8
000000100 = 0x4
010010110000 = 0x4B0
拆解完毕后,我们来开始进行测试:
kd> !dq 9c065000
#9c065000 8a000000`13a79867 00000000`00000000
#9c065010 00000000`00000000 8a000000`36881867
#9c065020 00000000`00000000 00000000`00000000
#9c065030 00000000`00000000 00000000`00000000
#9c065040 00000000`00000000 00000000`00000000
#9c065050 00000000`00000000 00000000`00000000
#9c065060 00000000`00000000 00000000`00000000
#9c065070 00000000`00000000 00000000`00000000
kd> !dq 36881000+17b*8
#36881bd8 0a000000`a3982867 00000000`00000000
#36881be8 00000000`00000000 00000000`00000000
#36881bf8 00000000`00000000 00000000`00000000
#36881c08 00000000`00000000 00000000`00000000
#36881c18 00000000`00000000 00000000`00000000
#36881c28 00000000`00000000 00000000`00000000
#36881c38 00000000`00000000 00000000`00000000
#36881c48 00000000`00000000 00000000`00000000
kd> !dq A3982000+1D8*8
#a3982ec0 0a000000`828ae867 0a000000`27aa6867
#a3982ed0 0a000000`1222f867 00000000`00000000
#a3982ee0 00000000`00000000 00000000`00000000
#a3982ef0 00000000`00000000 00000000`00000000
#a3982f00 00000000`00000000 00000000`00000000
#a3982f10 00000000`00000000 00000000`00000000
#a3982f20 0a000000`99493867 0a000000`91a7b867
#a3982f30 00000000`00000000 00000000`00000000
kd> !dq 828ae000+4*8
#828ae020 81000000`27fca867 81000000`10be0867
#828ae030 81000000`892f5867 81000000`980f7847
#828ae040 81000000`2aef6867 81000000`93dfe867
#828ae050 81000000`12f06867 81000000`5210f867
#828ae060 81000000`98211867 81000000`a4310847
#828ae070 81000000`b011f867 81000000`2b920867
#828ae080 81000000`95d23847 81000000`6da22847
#828ae090 81000000`0e728847 81000000`88827867
kd> !db 27fca000+4b0
#27fca4b0 68 00 65 00 6c 00 6c 00-6f 00 25 00 77 00 6f 00 h.e.l.l.o.%.w.o.
#27fca4c0 72 00 6c 00 64 00 21 00-0d 00 0a 00 00 00 00 00 r.l.d.!.........
#27fca4d0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
#27fca4e0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
#27fca4f0 00 00 00 00 00 00 00 00-08 00 79 fe de 01 00 00 ..........y.....
#27fca500 00 00 00 00 00 00 00 00-2a f8 73 48 50 71 00 10 ........*.sHPq..
#27fca510 30 48 00 fb de 01 00 00-00 42 00 fb de 01 00 00 0H.......B......
#27fca520 40 48 00 fb de 01 00 00-10 42 00 fb de 01 00 00 @H.......B......
当然上述我们有不严谨的地方,我们并没有判断是否是大页的情况,但对于一般变量来说,大页的情况还是比较少的。
学过我之前写的教程都知道,操作系统可是不能这么直接访问物理内存的,它依旧通过线性地址进行访问内容,必然在某个地址放着这几张表。按照之前的学习路线,我们必然得需要逆向MmIsAddressValid
,下面我们开始继续:
; BOOLEAN __stdcall MmIsAddressValid(PVOID VirtualAddress)
public MmIsAddressValid
MmIsAddressValid proc near ; CODE XREF: KiMarkBugCheckRegions+B3↑p
; KiMarkBugCheckRegions+1E8↑p ...
sub rsp, 28h
call MmIsAddressValidEx
add rsp, 28h
retn
MmIsAddressValid endp
可以看出,这个啥也没做,只是调用了MmIsAddressValidEx
这个函数,我们点进去看看:
; BOOLEAN __fastcall MmIsAddressValidEx(PVOID VirtualAddress)
MmIsAddressValidEx proc near ; CODE XREF: MiIncreaseUsedPtesCount+64↑p
; MiCommitExistingVad+65D↓p ...
PXE = qword ptr -28h
PPE = qword ptr -20h
PDE = qword ptr -18h
PTE = qword ptr -10h
VirtualAddress = qword ptr 8
; FUNCTION CHUNK AT .text:00000001402234DC SIZE 00000056 BYTES
mov [rsp+VirtualAddress], rbx
push rdi
sub rsp, 20h
mov rax, rcx ; VirtualAddress
mov r10, rcx
sar rax, 47 ; 实现的虚拟地址是48位的,如果是内核地址,则最后是-1,用户地址是0
inc rax ; 自增1
cmp rax, 1 ; 如果大于1,有问题
ja InvalidAddr
mov rdx, rcx ; VirtualAddress
mov rdi, 0FFFFF68000000000h ; PML4 表基址
shr rdx, 9
mov rcx, 7FFFFFFFF8h
and rdx, rcx
mov rax, rdi
add rdx, rax
mov [rsp+28h+PXE], rdx ; 获取 PXE
shr rdx, 9
and rdx, rcx
mov rax, rdi
add rdx, rax
mov [rsp+28h+PPE], rdx ; 获取 PPE
shr rdx, 9
and rdx, rcx
mov rax, rdi
add rdx, rax
mov [rsp+28h+PDE], rdx ; 获取 PDE
shr rdx, 9
and rdx, rcx
mov rax, rdi
add rdx, rax
mov r11, 0FFFFF6FB7DBED000h
mov [rsp+28h+PTE], rdx ; 获取 PTE
mov edx, 4
mov rbx, 0FFFFF6FB7DBED7F8h
loc_1400AB248: ; CODE XREF: MmIsAddressValidEx+BF↓j
mov r9, [rsp+rdx*8-8] ; 初始值 rdx = 4
dec rdx
mov rcx, [r9]
mov rax, r11
cmp r9, rax
jb short loc_1400AB263
mov rax, rbx
cmp r9, rax
jbe short loc_1400AB27E
loc_1400AB263: ; CODE XREF: MmIsAddressValidEx+A9↑j
; MmIsAddressValidEx+D9↓j ...
test cl, 1
jz short InvalidAddr
test cl, cl
js short loc_1400AB2BD
test rdx, rdx
jnz short loc_1400AB248
loc_1400AB271: ; CODE XREF: MmIsAddressValidEx+113↓j
; MmIsAddressValidEx+122↓j
mov al, 1
loc_1400AB273: ; CODE XREF: MmIsAddressValidEx+126↓j
mov rbx, [rsp+28h+VirtualAddress]
add rsp, 20h
pop rdi
retn
; ---------------------------------------------------------------------------
loc_1400AB27E: ; CODE XREF: MmIsAddressValidEx+B1↑j
mov eax, cs:MiFlags
test eax, 0C00000h
jz short loc_1400AB263
mov rax, gs:_KPCR.Prcb.CurrentThread
mov r8, [rax+_KTHREAD.___u25.ApcState.Process]
cmp [r8+_EPROCESS.Pcb.AddressPolicy], 1
jz short loc_1400AB263
test cl, 1
jz short InvalidAddr
test cl, 20h
jz loc_1402234DC
test cl, 42h
jnz short loc_1400AB263
jmp loc_1402234DC
; ---------------------------------------------------------------------------
loc_1400AB2BD: ; CODE XREF: MmIsAddressValidEx+BA↑j
mov rax, rdi
cmp r10, rax
jb short loc_1400AB271
mov rax, 0FFFFF6FFFFFFFFFFh
cmp r10, rax
ja short loc_1400AB271
InvalidAddr: ; CODE XREF: MmIsAddressValidEx+1B↑j
; MmIsAddressValidEx+B6↑j ...
xor al, al
jmp short loc_1400AB273
MmIsAddressValidEx endp
但不幸的是,我根本无法弄懂这块代码的所有细节,完全弄懂的部分均用注释标注,主要是如下代码看起来十分奇怪:
loc_1400AB248: ; CODE XREF: MmIsAddressValidEx+BF↓j
mov r9, [rsp+rdx*8-8] ; 初始值 rdx = 4
dec rdx
mov rcx, [r9]
mov rax, r11
cmp r9, rax
jb short loc_1400AB263
mov rax, rbx
cmp r9, rax
jbe short loc_1400AB27E
根据堆栈图,它是在该函数直接提栈的局部变量区,而我未发现任何初始化该区域数据的汇编代码。该代码里面还有ShadowMapping
这个东西,从名字上来看是影子映射相关的,但我同样弄不出来相关细节。网络上有关x64
的资料少之又少,我无法解决,所以有关页的相关内容,只能在此告一段落。
门
门在x86
上是十分重要的东西,尤其是中断门在Win
系统扮演了十分重要的角色。鉴于之前在基础篇保护模式介绍的内容基础,这里就只讲调用门和中断门。
注意:我不太建议做和学WinXP
一样的内核实验,因为PG
有可能会被触发导致蓝屏,搞懂原理即可。
调用门
在x64
下,调用门的结构如下:
可以看出,调用门被扩展为128位,基本内容并没有发生任何变化。我们看看GDT
表里面的内容:
kd> dq gdtr
fffff803`86c90fb0 00000000`00000000 00000000`00000000
fffff803`86c90fc0 00209b00`00000000 00409300`00000000
fffff803`86c90fd0 00cffb00`0000ffff 00cff300`0000ffff
fffff803`86c90fe0 0020fb00`00000000 00000000`00000000
fffff803`86c90ff0 86008bc8`f0000067 00000000`fffff803
fffff803`86c91000 0040f300`00003c00 00000000`00000000
fffff803`86c91010 00000000`00000000 00000000`00000000
fffff803`86c91020 00000000`00000000 00000000`00000000
里面的内容少了不少,相比于XP
系统来说。有关调用门细节就介绍这些。
中断门
中断门也被同样的扩展为128位,其余的功能并没有发生任何变化。
然后我们再看一下IDT
表:
kd> dq idtr
fffff803`86c8e000 81158e00`00101100 00000000`fffff803
fffff803`86c8e010 81158e04`00101180 00000000`fffff803
fffff803`86c8e020 81158e03`00101200 00000000`fffff803
fffff803`86c8e030 8115ee00`00101280 00000000`fffff803
fffff803`86c8e040 8115ee00`00101300 00000000`fffff803
fffff803`86c8e050 81158e00`00101380 00000000`fffff803
fffff803`86c8e060 81158e00`00101400 00000000`fffff803
fffff803`86c8e070 81158e00`00101480 00000000`fffff803
我们可以用!idt
获取每一个索引对应的函数,如下为部分展示,后面有stack
的说明该函数有自己独占的栈。
kd> !idt
Dumping IDT: fffff80386c8e000
00: fffff80381151100 nt!KiDivideErrorFaultShadow
01: ffff80381151180 nt!KiDebugTrapOrFaultShadow Stack = 0xFFFFF80386C929D0
02: fffff80381151200 nt!KiNmiInterruptShadow Stack = 0xFFFFF80386C927D0
03: fffff80381151280 nt!KiBreakpointTrapShadow
04: fffff80381151300 nt!KiOverflowTrapShadow
05: fffff80381151380 nt!KiBoundFaultShadow
06: fffff80381151400 nt!KiInvalidOpcodeFaultShadow
07: fffff80381151480 nt!KiNpxNotAvailableFaultShadow
08: fffff80381151500 nt!KiDoubleFaultAbortShadow Stack = 0xFFFFF80386C923D0
……
有关中断门相关内容,暂时就这么多。
SMEP 与 SMAP
x64
的CPU
又增加了一些标识来增加安全性:SMEP
与SMAP
。这两个标识在CR4
中,我们再拿来看看:
SMEP
英文全称是supervisor-mode execution prevention
,看意思就是限制执行代码的。SMAP
英文全称为supervisor-mode access prevention
,看意思就是限制访问内存的。这些限制是限制谁的,限制内核的。在WinXP
下,只要是内核就具有至高无上的权限,我想读哪块内存就读,想执行啥代码就执行。
说这两个标志位的功能倒是挺容易的,为什么要说这个东西呢?之前的实验应用层程序通过调用门提权并执行代码就会出现问题,我们需要对这两个标志位进行清0操作,否则就会出错。
有一个特殊的汇编指令CLAC
,它的作用是清除EFLAGS
寄存器中的AC
标志位。这将禁用用户模式数据访问的任何对齐检查。如果在CR4
寄存器中设置了SMAP
位,这个位就不会起作用;STAC
可以恢复使用SMAP
。有关这两个位介绍就这么多。
下一篇
x64 番外篇—— WOW64