JIURL玩玩Win2k内存篇 内存共享(二) CopyOnWrite
作者: JIURL
日期: 2003-7-30
CopyOnWrite
对于同一个可执行文件运行的两个进程,或者被多个进程共享的动态链接库,只读共享代码部分的物理页是没有问题的。而对于向代码页中写(调试器就可能向代码页中写),这将会影响其他共享这个代码页的进程的正确执行。或者数据页比如说初始化了的全局变量所在的页,每个进程可能会写入不同的数据,也将影响其他共享这个页的进程的正常运行。为了使程序正常运行,应该使每个进程的可能被写的页映射到自己的物理页上,这样就不会影响别的进程了。如果为每个进程每个可能会被写入的页都直接分配新的物理页的话,很有可能会造成不必要的浪费,如果这个一个进程自始至终都没有向某页写入数据的话,那么分配该页所花的执行时间,该页所占的物理内存,都是没有用的。Win2k 为了避免这种浪费,提高效率,使用叫做 Copy On Write 的机制来处理这种情况。这是叫做 Lazy Evaluation 技术的一部分。
Copy On Write 就是对于一页,多个使用进程共享,直到一个进程要向该页写入数据的时候,系统会给该进程一个新的物理页,并把原来的页的数据复制过来,更新该进程的页表项,使该进程映射新的物理页,数据会写入该进程新的自己的物理页中。这样就不会影响原来的数据页。而其他共享该页的进程仍然可以继续共享,直到他们也试图写入数据。
Win2k 中 Copy On Write 机制的主要应用是,可以方便调试器向一个进程的代码页写入东西,而不会影响别的共享这个代码页的进程。某些数据页。
Copy On Write 机制的实现。Win2k 把需要 Copy On Write 的页的页表项(PTE)的标志位中的读写位设为只读,并设置 CopyOnWrite 标志位。这样当一个进程向该页写入数据的时候,因为页表项设为只读,所以会引起 Page-Fault 异常(Exception)。从而使 CPU 转去执行异常处理程序,异常处理程序检查页表项发现设置了 CopyOnWrite 标志,就会完成分配新物理页,更新进程的页表项,把新的页表项的读写标志设为读写等工作。最后 CPU 重新执行引起异常的指令,这时该指令所写的虚拟地址已经是新的物理页了,并且该虚拟地址的页表项的标志也设为了可写,于是就可以顺利执行。下面我们针对 x86 CPU 做更详细的说明。x86 CPU 的页表项定义如下
struct _HARDWARE_PTE_X86 (sizeof=4)
bits0-0 Valid
bits1-1 Write
bits2-2 Owner
bits3-3 WriteThrough
bits4-4 CacheDisable
bits5-5 Accessed
bits6-6 Dirty
bits7-7 LargePage
bits8-8 Global
bits9-11 reserved
bits12-31 PageFrameNumber
首先要说明的是,这个格式是由 CPU 定义的,CPU 将按照这个定义,对每一位做出解释,然后决定处理方式。其中要注意的是 bits1-1 Write 这将决定该页是否只读,为 0 表示只读,为 1 表示可读可写。bits9-11 reserved 这3位,CPU 没有定义,留给操作系统使用。Win2k 用 bits9-9 CopyOnWrite 来表示是否使用 CopyOnWrite,该位为0表示不使用,为1表示使用。
当某页的页表项中标志位设置了只读,和 CopyOnWrite 之后,当某条指令向该页中写入的时候,比如指令 MOV AddressInCopyOnWritePage,1 ,执行这条指令时,CPU 会自动通过页目录和页表把 虚拟地址AddressInCopyOnWritePage 转换成物理地址,在地址转换过程中,CPU 在从页表项得到物理页地址的同时,会进行页保护检查,比如看该页表项是否有效,是否是只读等等。在这里我们指令中地址的页表项设置了只读标志,于是就会引发异常。异常也是由 CPU 实现的。这里引起的是一个 Page Fault 异常,它的中断号是 0xe (十进制14),需要注意的是 Page Fault 的中断号是 0xe 这是由 CPU 定义的( x86 CPU 的 从 0 - 31 这32个中断是由 CPU 定义的,CPU 将根据这个定义做相应工作)。在发生异常时,CPU 自动把一些寄存器压入堆栈,然后根据中断号,(通过IDTR找到中断描述符表)在中断描述符表中找到相应的中断描述符,根据中断描述符中的地址,转到异常处理程序。中断描述符是由Win2k设置,异常处理程序也是由Win2k决定。对于 Win2k Build 2195 来说,中断 0xe 的处理程序是 ntoskrnl!KiTrap0E 地址在 804648a4 。当转到 KiTrap0E 时,CPU 已经在堆栈中压入了下面的内容
|-------------|
| EFLAGS |
|-------------|
| CS |
|-------------|
| EIP |
|-------------|
| Error Code |
|-------------|<---- [ ESP ]
page-fault 异常 (#PF) 的 Error Code 定义如下( CPU 定义 )
| 3 | 2 | 1 | 0 |
+---------------------------------------------------+
| Reserved |RSVD|U/S|R/W| P |
+---------------------------------------------------+
P 0 错误由无效页引起
1 错误由违反页保护引起
W/R 0 引起错误的内存访问是读
1 引起错误的内存访问是写
U/S 0 访问错误时处理器处在管理模式
1 访问错误时处理器处在用户模式
需要说明的是堆栈中压入的 EIP 就是引发异常的指令地址,将来将根据这个地址重新执行该指令。而寄存器 cr2 中是引发异常时访问的地址。
#PF异常处理程序 KiTrap0E(由Win2k提供)将会调用 ntoskrnl!MmAccessFault ,MmAccessFault 根据是写操作引起的异常,以及CR2中的访问地址,计算出相应的 PDE,PTE地址,检查 PTE 发现设置了CopyOnWrite 标志(这个标志是由 Win2k 定义的,也是由它来处理),最终会调用 ntoskrnl!MiCopyOnWrite 来完成 Copy On Write 的相关工作。比如分配新的物理页,改变页表项中的物理地址,改变页表项的标志,把只读改为可读可写。
执行完异常处理程序之后,CPU 重新执行引起异常的指令,这时指令可以正常执行了。至此 Copy On Write 已经被实现了。
下面我们通过一个例子来观察 Copy On Write 的实际情况
//----------------------------------------------------------------------
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#pragma data_seg(".jiurl")
char JiurlSegData[32]="aaaaaaaaaaaaaaaaaaaaaaaa";
#pragma data_seg()
void main()
{
printf("JiurlSegData Address: 0x%08x\n",JiurlSegData);
printf("JiurlSegData Pte Address: 0x%08x\n",
((ULONG)(JiurlSegData)>>12)*4+0xC0000000);
printf("JiurlSegData: %s\n",JiurlSegData);
printf("\n");
printf("Input New String to JiurlSegData\n:");
scanf("%s",JiurlSegData);
printf("JiurlSegData: %s\n",JiurlSegData);
getch();
}
//----------------------------------------------------------------------
这个程序一个叫 ".jiurl" 的节中,有一个初始化了的全局变量(把这个全局变量放在单独的一个节中,是为了避免受其他变量的影响)。分析编译生成的 PE 可执行文件,就可以看到这个节,以及这个节的属性,读,写,初始化数据。程序运行时,将输出这个全局变量的地址,以及根据这个地址计算出的该地址的PTE的地址,并 printf 全局变量中的内容。然后等待输入一个新的字符串写入全局变量中。我们将运行这个程序的两个实例,使用 SoftICE 来观察 Copy On Write 。需要强调一点,运行程序之后,Ctrl+D 动作快点,因为时间长了系统很有可能把该页换出物理内存。
这个程序的名字叫做 CopyOnWrite-Er
1. 运行一个 CopyOnWrite-Er ,再运行一个 CopyOnWrite-Er
首先用 SoftICE 的 addr 命令列出当前运行的进程
:addr
CR3 LDT Base:Limit KPEB Addr PID Name
00030000 8141E020 0008 System
04E2B000 810F75C0 008C smss
06562000 810E8C40 00A8 csrss
07547000 810CC0C0 00BC winlogon
078E9000 810C14E0 00D8 services
078FA000 810BFD60 00E4 lsass
00ABD000 8109F200 0170 svchost
00324000 810924C0 0190 svchost
00564000 81054880 0204 Explorer
024CF000 8108B960 0250 internat
07FEC000 82F873C0 01F4 conime
036C1000 8331C180 034C NOTEPAD
0328F000 84130860 01F8 NOTEPAD
018EF000 8323F180 0100 CopyOnWrite-Er
06AB8000 83091020 02BC CopyOnWrite-Er
*00030000 8046BB60 0000 Idle
看到了2个 CopyOnWrite-Er ,进程ID 分别是 0100 和 02BC
这2个 CopyOnWrite-Er 的输出都是
_________________________________________
JiurlSegData Address: 0x0040a000
JiurlSegData Pte Address: 0xc0001028
JiurlSegData: aaaaaaaaaaaaaaaaaaaaaaaa
Input New String to JiurlSegData
_________________________________________
CopyOnWrite-Er 100
使用 addr 命令转换到 CopyOnWrite-Er 100 的地址空间
:addr 100
再看一下 CopyOnWrite-Er 100 的段的情况,看到了 ".jiurl" 段,
地址范围在 0040A000 大小为 00000020
和程序的输出 JiurlSegData Address: 0x0040a000 相符
:map32 -u
Owner Obj Name Obj# Address Size Type
CopyOnWrit.text 0001 001B:00401000 00004D73 CODE RO
CopyOnWrit.rdata 0002 0023:00406000 00000890 IDATA RO
CopyOnWrit.data 0003 0023:00407000 000021C8 IDATA RW
CopyOnWrit.jiurl 0004 0023:0040A000 00000020 IDATA RW
kernel32 .text 0001 001B:77E61000 0005D1AE CODE RO
kernel32 .data 0002 0023:77EBF000 00001A30 IDATA RW
kernel32 .rsrc 0003 0023:77EC1000 00070000 IDATA RO
kernel32 .reloc 0004 0023:77F31000 0000359C IDATA RO
ntdll .text 0001 001B:77F81000 00042492 CODE RO
ntdll ECODE 0002 001B:77FC4000 00004371 CODE RO
ntdll PAGE 0003 001B:77FC9000 00003983 CODE RO
ntdll .data 0004 0023:77FCD000 00002350 IDATA RW
ntdll .rsrc 0005 0023:77FD0000 00026D08 IDATA RO
ntdll .reloc 0006 0023:77FF7000 00001DA8 IDATA RO
根据我们计算出的 JiurlSegData Pte Address: 0xc0001028 ,显示该页表项
:dd c0001028 l 10
0010:C0001028 06AC7225 00000000 00000000 00000000 %r..............
可以看到
CopyOnWrite-Er 100 的 JiurlSegData 地址范围的 页表项值为 06AC7225
注意物理页为 06AC7 ,标志为 225(hex)= 0010 0010 0101 (bin)
bits1-1 Write 标志为0,表示只读。bits9-9 CopyOnWrite 标志为1,表示使用 Copy On Write 。
CopyOnWrite-Er 2bc
转换到 CopyOnWrite-Er 2bc 的地址空间
:addr 2bc
根据我们计算出的 JiurlSegData Pte Address: 0xc0001028 ,显示该页表项
:dd c0001028 l 10
0010:C0001028 06AC7225 00000000 00000000 00000000 %r..............
可以看到
CopyOnWrite-Er 2bc 的 JiurlSegData 地址范围的 页表项值也为 06AC7225
首先就是它和 CopyOnWrite-Er 100 映射了同样的物理页 06AC7 ,页中的内容当然是一样的。他们共享了这个数据页。标志位同样也说明了使用 Copy On Write。
2. 向 CopyOnWrite-Er 2bc 的全局变量写入
向 CopyOnWrite-Er 2bc 的全局变量写入 bbbbbbbbb
程序输出如下
_________________________________________
Input New String to JiurlSegData
:bbbbbbbbb
JiurlSegData: bbbbbbbbb
_________________________________________
可以看到正确读出了输入的 bbbbbbbbb
转换到 CopyOnWrite-Er 2bc 的地址空间
:addr 2bc
显示 CopyOnWrite-Er 2bc 的 0xc0001028 处的页表项
:dd c0001028 l 10
0010:C0001028 04427067 00000000 00000000 00000000 gpB.............
该页表项值为 04427067 ,可以看到映射的物理页变成了 04427 ,这说明分配并使用了新的物理页。
标志为 067(hex)= 0000 0110 0111 (bin) 可以看到
bits1-1 Write 标志为1,表示可写了。bits9-9 CopyOnWrite 标志为0。
对于 CopyOnWrite-Er 2bc ,Copy On Write 机制得到了验证
对于 CopyOnWrite-Er 100 的情况
转换到 CopyOnWrite-Er 100 的地址空间
:addr 100
显示 CopyOnWrite-Er 100 的 0xc0001028 处的页表项
:dd c0001028 l 10
0010:C0001028 06AC7225 00000000 00000000 00000000 %r..............
可以看到该页表项的值仍然是 06AC7225 ,说明
CopyOnWrite-Er 100 继续使用原来的物理页
这也符合前面描述的 Copy On Write 机制
3. 向 CopyOnWrite-Er 100 的全局变量写入
向 CopyOnWrite-Er 2bc 的全局变量写入 cccccccccc
程序输出如下
_________________________________________
Input New String to JiurlSegData
:cccccccccc
JiurlSegData: cccccccccc
_________________________________________
可以看到正确读出了输入的 cccccccccc
转换到 CopyOnWrite-Er 100 的地址空间
:addr 100
显示 CopyOnWrite-Er 100 的 0xc0001028 处的页表项
:dd c0001028 l 10
0010:C0001028 00D07067 00000000 00000000 00000000 gp..............
可以看到,写入导致了使用新的物理页,并且改变了标志
转换到 CopyOnWrite-Er 2bc 的地址空间
:addr 2bc
显示 CopyOnWrite-Er 2bc 的 0xc0001028 处的页表项
:dd c0001028 l 10
0010:C0001028 04427067 00000000 00000000 00000000 gpB.............
还是刚才的
通过这个例子,我们看到了 Win2k 使用 Copy On Write 的效果
4. PfnDataBaseEntry 和 Copy On Write
我们再来观察一下被共享的物理页的 PfnDataBaseEntry 中的情况。
运行一个 CopyOnWrite-Er 我们观察它的物理页的页帧号
:addr CopyOnWrite-Er
:dd c0001028 l 10
0010:C0001028 06AC7225 00000000 00000000 00000000 %r..............
页帧号为 06AC7
计算 06AC7 的页帧号数据库项的地址,页帧号数据库的首地址在全局变量 MmPfnDatabase 中,
在我机子上它的值为 81456000 ,每项大小 0x18 个字节
我们可以看到 物理页 6ac7
/*08*/ uint32 blink / share count = 00000001
/*0D*/ byte page state = 06
:dd 81456000+18*6ac7 l 18
0010:814F62A8 0000009B E301F2A8 00000001 00010608 ................
0010:814F62B8 907B64B8 00004727 00000000 C03B37D8 .d{.'G.......7;.
说明该物理页处在 Active 状态,并且共享数为1
我们又运行了两个 CopyOnWrite-Er
我们可以看到 物理页 6ac7
/*08*/ uint32 blink / share count = 00000003
/*0D*/ byte page state = 06
:dd 81456000+18*6ac7 l 18
0010:814F62A8 0000009B E301F2A8 00000003 00010608 ................
0010:814F62B8 907B64B8 00004727 00000000 C03B37D8 .d{.'G.......7;.
说明该物理页处在 Active 状态,并且共享数增为3
对一个 CopyOnWrite-Er 写入数据
我们可以看到 物理页 6ac7
/*08*/ uint32 blink / share count = 00000002
/*0D*/ byte page state = 06
:dd 81456000+18*6ac7 l 18
0010:814F62A8 0000009B E301F2A8 00000002 00010608 ................
0010:814F62B8 907B64B8 00004727 00000000 C03B37D8 .d{.'G.......7;.
并且共享减为2,写数据的进程由于 Copy On Write 而不再使用本物理页
对剩下所有的 CopyOnWrite-Er 写入数据
我们可以看到 物理页 6ac7
/*08*/ uint32 blink / share count = 000068B9
/*0D*/ byte page state = 02
:dd 81456000+18*6ac7 l 18
0010:814F62A8 FFFFFFFF E301F2A8 000068B9 00000208 .........h......
0010:814F62B8 907B64B8 00004727 00000000 C03B37D8 .d{.'G.......7;.
没有进程再使用这个物理页,所以状态变为了 Standby ,而且 /*08*/ 由于状态的变化含义也变成了 blink
广泛存在的 Copy On Write
观察一个进程的页表,你会发现除了我们设计的例子之外,很难找到其他的设置了 CopyOnWrite 标志的页表项(还是可以找到的,我用一个小工具就在记事本进程中找到了几项,标志位为 205),难道 Copy On Write 只有如此少量的应用?实际上 Win2k 中大量使用了 Copy On Write 我们使用 SoftICE 就可以观察到。
当向 Copy On Write 页写时,会引发 Page-Fault 异常,CPU 会执行异常处理程序 ntoskrnl!KiTrap0E ntoskrnl!KiTrap0E 会调用 ntoskrnl!MmAccessFault ,分析 ntoskrnl!MmAccessFault 的汇编代码,可以知道 CopyOnWrite 引起的异常将由 ntoskrnl!MiCopyOnWrite 处理。分析汇编代码我们还可以看出 MiCopyOnWrite 函数有两个传入参数,第一个参数是引发异常的指令访问的虚拟地址,我们叫做 BadAddress 。第二个参数是该虚拟地址的页表项地址,我们叫做 PteAddress 。MiCopyOnWrite 使用 fastcall 调用协议,第一个参数 BadAddress 通过 ecx 寄存器传入,第二个参数 PteAddress 通过 edx 寄存器传入。只要向 CopyOnWrite 页写入数据,我们就应该可以断到 MiCopyOnWrite ,并且从 ecx 和 edx 当中看到 引发异常的指令访问的虚拟地址 和 该地址的 PTE 地址。
我们在 SoftICE 中下这样的断点
BPX 8044BE22 DO "db ((ffdff124->0)+44)->0+1fc l 10 ; dd ecx l 4 ; dd edx l 4"
其中,8044BE22 是 MiCopyOnWrite 的入口地址( Win2k Build 2195 ),通过使用 kd , u MiCopyOnWrite 获得。断到之后,让 SoftICE 执行 db ((ffdff124->0)+44)->0+1fc l 10 ,这将显示当前运行的进程名。然后再显示出 ecx 和 edx 中的内容。
当我们运行一个程序时,这里我运行了一记事本程序,SoftICE 窗口立刻弹出,说明有 Copy On Write 发生。
// 这就是下的断点。
:bl
00) * BPX #0008:8044BE22 DO "db ((ffdff124->0)+44)->0+1fc l 10 ; dd ecx l 4 ; d
:be 0
// 退出 SoftICE 窗口
// 打开一个文本文件,运行了记事本程序,SoftICE 窗口立刻弹出
NTICE: Load32 START=1000000 SIZE=10000 KPEB=80D5AD40 MOD=NOTEPAD
NTICE: Load32 START=77F80000 SIZE=79000 KPEB=80D5AD40 MOD=ntdll
Break due to BPX #0008:8044BE22 DO "db ((ffdff124->0)+44)->0+1fc l 10 ; dd ecx
l 4 ; dd edx l 4" (ET=4.09 seconds)
// 可以看到当前进程是 NOTEPAD.EXE
0023:80D5AF3C 4E 4F 54 45 50 41 44 2E-45 58 45 00 00 00 00 00 NOTEPAD.EXE.....
// ecx 也就是 BadAddress 为 77FCD34C
0023:77FCD34C FFFFFFFF 00000000 00000000 00000000 ................
// edx 也就是 PteAddress 为 C01DFF34 ,可以看到页表项中标志位的 CopyOnWrite位 被设置,Write位 没// 有被设置
0023:C01DFF34 006AA225 00000000 00000000 00000000 %.j.............
// 只有 NOTEPAD 和 ntdll ,BadAddress= 77FCD34C 在 ntdll .data 范围内。
// 只有 NOTEPAD 和 ntdll 说明了其他的 dll 还没有被载入,也就是说在 ntdll 被载入,执行 DllMain 中// 对 ntdll 初始化时,就向使用了 CopyOnWrite 的数据页中写入数据,这将导致数据页的页表项中的物理页// 为新分配的物理页,并且标志将从 ReadOnly CopyOnWrite 变成 ReadWrite。
:map32 -u
Owner Obj Name Obj# Address Size Type
NOTEPAD .text 0001 001B:01001000 000065CA CODE RO
NOTEPAD .data 0002 0023:01008000 00001944 IDATA RW
NOTEPAD .rsrc 0003 0023:0100A000 00005238 IDATA RO
ntdll .text 0001 001B:77F81000 00042492 CODE RO
ntdll ECODE 0002 001B:77FC4000 00004371 CODE RO
ntdll PAGE 0003 001B:77FC9000 00003983 CODE RO
ntdll .data 0004 0023:77FCD000 00002350 IDATA RW
ntdll .rsrc 0005 0023:77FD0000 00026D08 IDATA RO
ntdll .reloc 0006 0023:77FF7000 00001DA8 IDATA RO
// Ctrl+D , SoftICE 立刻(322ms后)又被中断,ET=322.23 microseconds
Break due to BPX #0008:8044BE22 DO "db ((ffdff124->0)+44)->0+1fc l 10 ; dd ecx
l 4 ; dd edx l 4" (ET=322.23 microseconds)
0023:80D5AF3C 4E 4F 54 45 50 41 44 2E-45 58 45 00 00 00 00 00 NOTEPAD.EXE.....
0023:77FCE340 00000000 00000000 00000000 00000000 ................
0023:C01DFF38 0200B225 00000000 00000000 00000000 %...............
NTICE: Load32 START=76AF0000 SIZE=3E000 KPEB=80D5AD40 MOD=comdlg32
NTICE: Load32 START=77C50000 SIZE=4A000 KPEB=80D5AD40 MOD=SHLWAPI
NTICE: Load32 START=77F40000 SIZE=3C000 KPEB=80D5AD40 MOD=gdi32
NTICE: Load32 START=77E60000 SIZE=D5000 KPEB=80D5AD40 MOD=kernel32
NTICE: Load32 START=77DF0000 SIZE=64000 KPEB=80D5AD40 MOD=user32
NTICE: Load32 START=77D90000 SIZE=5A000 KPEB=80D5AD40 MOD=advapi32
NTICE: Load32 START=77D20000 SIZE=6F000 KPEB=80D5AD40 MOD=rpcrt4
NTICE: Load32 START=77B30000 SIZE=8A000 KPEB=80D5AD40 MOD=COMCTL32
NTICE: Load32 START=77560000 SIZE=240000 KPEB=80D5AD40 MOD=shell32
NTICE: Load32 START=78000000 SIZE=46000 KPEB=80D5AD40 MOD=MSVCRT
// ET=4.43 milliseconds
Break due to BPX #0008:8044BE22 DO "db ((ffdff124->0)+44)->0+1fc l 10 ; dd ecx
l 4 ; dd edx l 4" (ET=4.43 milliseconds)
0023:80D5AF3C 4E 4F 54 45 50 41 44 2E-45 58 45 00 00 00 00 00 NOTEPAD.EXE.....
0023:78033000 0003A09A 00039BC6 00039BD2 00039BE2 ................
0023:C01E00CC 00714225 00000000 00000000 00000000 %Bq.............
NTICE: Load32 START=777C0000 SIZE=1D000 KPEB=80D5AD40 MOD=WINSPOOL
// ET=2.41 milliseconds
Break due to BPX #0008:8044BE22 DO "db ((ffdff124->0)+44)->0+1fc l 10 ; dd ecx
l 4 ; dd edx l 4" (ET=2.41 milliseconds)
0023:80D5AF3C 4E 4F 54 45 50 41 44 2E-45 58 45 00 00 00 00 00 NOTEPAD.EXE.....
0023:77EBF060 00000000 00000000 00000000 00000000 ................
0023:C01DFAFC 04B56225 00000000 00000000 00000000 %b..............
// ET=250.33 microseconds
Break due to BPX #0008:8044BE22 DO "db ((ffdff124->0)+44)->0+1fc l 10 ; dd ecx
l 4 ; dd edx l 4" (ET=250.33 microseconds)
0023:80D5AF3C 4E 4F 54 45 50 41 44 2E-45 58 45 00 00 00 00 00 NOTEPAD.EXE.....
0023:77EC0800 00000000 00000000 00000000 00000000 ................
0023:C01DFB00 04737225 00000000 00000000 00000000 %rs.............
// 几个中断之后,可以看到每个中断的 PteAddress 中的页表项的标志 ReadOnly CopyOnWrite 都被设置
Break due to BPX #0008:8044BE22 DO "db ((ffdff124->0)+44)->0+1fc l 10 ; dd ecx
l 4 ; dd edx l 4" (ET=2.07 milliseconds)
0023:80D5AF3C 4E 4F 54 45 50 41 44 2E-45 58 45 00 00 00 00 00 NOTEPAD.EXE.....
0023:77E484D0 00000000 00000000 00000000 00000000 ................
0023:C01DF920 03B1A225 00000000 00000000 00000000 %...............
// 我们看这时载入的 dll,已经载入了比刚才多的多的 dll ,这说明了刚才的判断是对的,在程序载入 dll // 时,dll 初始化时就引发了 Copy On Write
:map32 -u
Owner Obj Name Obj# Address Size Type
NOTEPAD .text 0001 001B:01001000 000065CA CODE RO
NOTEPAD .data 0002 0023:01008000 00001944 IDATA RW
NOTEPAD .rsrc 0003 0023:0100A000 00005238 IDATA RO
comdlg32 .text 0001 001B:76AF1000 00029D3A CODE RO
comdlg32 .data 0002 0023:76B1B000 00003668 IDATA RW
comdlg32 .rsrc 0003 0023:76B1F000 0000B2A8 IDATA RO
comdlg32 .reloc 0004 0023:76B2B000 000022B0 IDATA RO
shell32 .text 0001 001B:77561000 0011A686 CODE RO
shell32 .data 0002 0023:7767C000 00003AE8 IDATA RW
shell32 .rsrc 0003 0023:77680000 00110ED8 IDATA RO
shell32 .reloc 0004 0023:77791000 0000EA90 IDATA RO
WINSPOOL .text 0001 001B:777C1000 00016B56 CODE RO
WINSPOOL .data 0002 0023:777D8000 00002AF0 IDATA RW
WINSPOOL .rsrc 0003 0023:777DB000 000009A8 IDATA RO
WINSPOOL .reloc 0004 0023:777DC000 00000FC8 IDATA RO
COMCTL32 .text 0001 001B:77B31000 000643C2 CODE RO
COMCTL32 .data 0002 0023:77B96000 000004A0 IDATA RW
COMCTL32 .rsrc 0003 0023:77B97000 0001E948 IDATA RO
COMCTL32 .reloc 0004 0023:77BB6000 00003784 IDATA RO
SHLWAPI .text 0001 001B:77C51000 0004243C CODE RO
SHLWAPI .data 0002 0023:77C94000 00000C28 IDATA RW
SHLWAPI .rsrc 0003 0023:77C95000 000010E8 IDATA RO
SHLWAPI .reloc 0004 0023:77C97000 0000256C IDATA RO
rpcrt4 .text 0001 001B:77D21000 0005FABA CODE RO
rpcrt4 .orpc 0002 001B:77D81000 00007CFC CODE RO
rpcrt4 .data 0003 0023:77D89000 00000F5C IDATA RW
rpcrt4 .rsrc 0004 0023:77D8A000 000003D0 IDATA RO
rpcrt4 .reloc 0005 0023:77D8B000 00003800 IDATA RO
advapi32 .text 0001 001B:77D91000 0004F330 CODE RO
advapi32 .data 0002 0023:77DE1000 00002E4C IDATA RW
advapi32 .rsrc 0003 0023:77DE4000 00001250 IDATA RO
advapi32 .reloc 0004 0023:77DE6000 0000381C IDATA RO
user32 .text 0001 001B:77DF1000 0005692A CODE RO
user32 .data 0002 0023:77E48000 00000E60 IDATA RW
user32 .rsrc 0003 0023:77E49000 0000742C IDATA RO
user32 .reloc 0004 0023:77E51000 00002ACC IDATA RO
kernel32 .text 0001 001B:77E61000 0005D1AE CODE RO
kernel32 .data 0002 0023:77EBF000 00001A30 IDATA RW
kernel32 .rsrc 0003 0023:77EC1000 00070000 IDATA RO
kernel32 .reloc 0004 0023:77F31000 0000359C IDATA RO
gdi32 .text 0001 001B:77F41000 0003649E CODE RO
gdi32 .data 0002 0023:77F78000 00000CFC IDATA RW
gdi32 .rsrc 0003 0023:77F79000 00000398 IDATA RO
gdi32 .reloc 0004 0023:77F7A000 0000151C IDATA RO
ntdll .text 0001 001B:77F81000 00042492 CODE RO
ntdll ECODE 0002 001B:77FC4000 00004371 CODE RO
ntdll PAGE 0003 001B:77FC9000 00003983 CODE RO
ntdll .data 0004 0023:77FCD000 00002350 IDATA RW
ntdll .rsrc 0005 0023:77FD0000 00026D08 IDATA RO
ntdll .reloc 0006 0023:77FF7000 00001DA8 IDATA RO
MSVCRT .text 0001 001B:78001000 0003174D CODE RO
MSVCRT .rdata 0002 0023:78033000 000075B4 IDATA RO
MSVCRT .data 0003 0023:7803B000 00006D84 IDATA RW
MSVCRT .rsrc 0004 0023:78042000 000003A8 IDATA RO
MSVCRT .reloc 0005 0023:78043000 00002600 IDATA RO
:bd *
// 还有更多的中断,不过这些对我们已经够了,所以关中断。
我们看到了这些 dll 的数据页初始都是设置了 CopyOnWrite 标志,只是由于在载入时,DllMain 对 dll 初始化时就向数据页中写数据,使得页表项的标志从 255 变成了 67。这就解释了为什么我们在一个进程的页表中几乎看不到设置 CopyOnWrite 标志的页。
下面我们用 SoftICE 观察对页表项的改变,以及找到引起异常的指令
:bl
00) BPX #0008:8044BE22 DO "db ((ffdff124->0)+44)->0+1fc l 10 ; dd ecx l 4 ; d
// 仍然在 MiCopyOnWrite 的入口地址下断点
// 运行一个记事本程序,断到了
NTICE: Load32 START=1000000 SIZE=10000 KPEB=8409E700 MOD=NOTEPAD
NTICE: Load32 START=77F80000 SIZE=79000 KPEB=8409E700 MOD=ntdll
Break due to BPX #0008:8044BE22 DO "db ((ffdff124->0)+44)->0+1fc l 10 ; dd ecx
l 4 ; dd edx l 4" (ET=2.08 seconds)
0023:8409E8FC 4E 4F 54 45 50 41 44 2E-45 58 45 00 00 00 00 00 NOTEPAD.EXE.....
0023:77FCD34C FFFFFFFF 00000000 00000000 00000000 ................
0023:C01DFF34 006AA225 00000000 00000000 00000000 %.j.............
// 可以看到 BadAddress= 77FCD34C ,PteAddress=C01DFF34 ,Pte= 006AA225
// 我们在 PteAddress 的 4个字节内存上 下一个断点,这样,如果改变 Pte 就会被我们断到
:bpmd c01dff34 w
// 被断到了
Break due to BPMD #0023:C01DFF34 W DR3 (ET=280.80 microseconds)
MSR LastBranchFromIp=80001ECA
MSR LastBranchToIp=8044BF69
:bd 1
// 关闭断点1
// 显示 Pte
:dd c01dff34 l 10
0023:C01DFF34 00FEA067 00000000 00000000 00000000 g...............
// 可以看到 Pte 的改变,物理页从 006AA 变成了 新的物理页 00FEA
// 标志 从 225 变成了 067
// 反汇编向 Pte 写入新内容的指令
// 使用 kd, u 8044bf6f 可以看到 MiCopyOnWrite+15c ,也就是说 MiCopyOnWrite 改变了 Pte 中的内容
:u 8044bf6f l 10
0008:8044BF6F MOV [EAX],ESI
0008:8044BF71 MOV EAX,[EBP-24]
0008:8044BF74 INVLPG [EAX]
0008:8044BF77 MOV ECX,EBX
0008:8044BF79 CALL 80449BCE
0008:8044BF7E MOV EAX,FS:[00000124]
// F12 (F12=^p ret;)
// F12
// 反汇编调用 MiCopyOnWrite 的部分
// 使用 kd, u 80447672 可以看到 ntoskrnl!MmAccessFault+834
:u 80447672 l 10
0008:80447672 CALL 8044BE21
0008:80447677 MOV EBX,00000112
0008:8044767C MOV EAX,[EBP-1C]
0008:8044767F MOV ECX,[EBP-20]
// F12
// 反汇编调用 MmAccessFault 的部分
// 使用 kd, u 80464961 可以看到 ntoskrnl!KiTrap0E+be
:u 80464961 l 10
0008:80464961 CALL 80446DD4
0008:80464966 CMP BYTE PTR [80471A38],00
0008:8046496D JZ 8046497A
0008:8046496F MOV EBX,[EBP+68]
// 在 KiTrap0E 中我们可以找到 CPU 压入的引起异常的指令,在 EBP+68 处
:dd ebp+68 l 10
0010:EBA0CDCC 77F89798 0000001B 00000246 0006FCA4 ...w....F.......
// 引起异常的指令地址是 77F89798
// 我们向前一点反汇编就可以看到是 ntdll!RtlTryEnterCriticalSection 中的指令
// 0008:77F89798 CMPXCHG [ECX+04],EDX
// 写入 CopyOnWrite 页时引起的异常
:u 77f89784 l 20
0008:77F89784 JMP 77F8350F
ntdll!RtlTryEnterCriticalSection
0008:77F89789 MOV ECX,[ESP+04]
0008:77F8978D MOV EAX,FFFFFFFF
0008:77F89792 MOV EDX,00000000
0008:77F89797 NOP
0008:77F89798 CMPXCHG [ECX+04],EDX
0008:77F8979C JNZ 77F8F47B
0008:77F897A2 MOV EAX,FS:[00000024]
:bd *
欢迎交流,欢迎交朋友,