版本:1.0

作者: xIkUg/RCT/CCG          xikug.xp [at] gmail [dot] com

我常去的网站:
http://debugman.wintoolspro.com
http://www.fcgchina.com
http://bbs.pediy.com
http://www.unpack.cn

说明:为了不至于混淆视线,本文以Win2000专业版,非3G,非PAE系统为蓝本进行讲述。其他系统稍有不同,但不在本文的讲述范围。

SharedUserData 是操作系统为每个进程提供的个共享数据结构,里面存放有很多重要的系统信息,如TickCount、系统时间、SystemRoot等。。。

其在DDK定义为:

代码:

#define KI_USER_SHARED_DATA         0xffdf0000
#define SharedUserData  ((KUSER_SHARED_DATA * const) KI_USER_SHARED_DATA)

他在内核中的地址是0xffdf0000,操作系统通过共享映射把这个结构以只读方式映射到每个进程的0x7ffe0000的地方。

操作系统为每个进程提供4G虚拟空间的访问能力(空间访问能力不代表空间容量),不过实际访问不了这么多,因为4G空间中低2G是用户空间,
高2G是系统空间(非3G模式的情况下),其中低2G空间中某些地址是受系统保护的,普通用户进程也不能访问,而高2G访问需要ring0权限。

操作系统采用分页机制来映射物理内存,并会为每个进程分配一个PT(Pages Table)和PD(Pages Directory),
每个进程的PTE(Page Table Entry)和PDE(Page Directory Entry)通常映射了不同的物理页,所以不同进程的同一虚拟地址的内容是不相干的。
上面说的“通常”也有例外,那就是不同进程的PTE或PDE也可能会映射相同的物理页,如果他们映射了相同的物理页,说明他们共享了一段内存,
SharedUserData就是这个例外,每个进程用户空间的0x7ffe0000都以只读方式映射到相同的物理页面上,而这个物理页面上就是KUSER_SHARED_DATA结构
的数据,因此操作系统上的每个进程都有一个这个结构,而操作系统只需要维护这一个结构就行了。

实际上在Windows NT操作系统的实现中大量的使用了这种内存共享机制,分页机制处理也远比这个我说的复杂,具体可参考《JIURL玩玩Win2k内存篇》。

我们回到我们的主题,我们要替换指定进程的SharedUserData。了解了上面我说的共享机制,可能你已经知道了,使进程的0x7ffe0000不要映射到0xffdf0000
指向的物理页,而是映射到我们自己的物理内存页就行了。没错。。。话是这么说,但实际上Windows的内存管理机制相当复杂,我在具体实现想法的时候碰
到了许多问题,真正彻底解决这些问题花了我足足两周的时间,其中最棘手的问题的是:
1. 死锁问题
2. 内存被置换到Standby链表上去的问题

事出必有因,我坚信操作系统能做到的事情我们也同样能做到。通过不断的调试操作系统代码终于解决了各种问题,现在我的代码能在2000-2003的系统下跑起来了。

我们需要定义一些结构,PTE和PDE的基地址如下:

代码:

#define PTE_BASE    0xC0000000
#define PDE_BASE    0xC0300000

根据虚拟地址得到PTE的地址和PDE的地址:

代码:

#define GetPteAddress(va) ((PMMPTE)(((((ULONG)(va)) >> 12) << 2) + PTE_BASE))
#define GetPdeAddress(va)  ((PMMPTE)(((((ULONG)(va)) >> 22) << 2) + PDE_BASE))

我们需要分配一段非分页内存,必须以小页边界(4K)对齐,0x7ffe0000也以页对齐的:

代码:

ProcessPTE->p = ExAllocatePool(NonPagedPool, PAGE_SIZE)
现在我们就替换SharedUserData,并把日期锁定:

代码:

RtlCopyMemory(ProcessPTE->p, SharedUserData, sizeof(KUSER_SHARED_DATA));
pMySharedData = ProcessPTE->p;
RtlTimeToTimeFields((PLARGE_INTEGER)&pMySharedData->SystemTime, &TimeFields);
TimeFields.Year = 2007;
TimeFields.Month = 1;
TimeFields.Day = 1;
RtlTimeFieldsToTime(&TimeFields, (PLARGE_INTEGER)&pMySharedData->SystemTime);
pMySharedData->SystemTime.High2Time = pMySharedData->SystemTime.High1Time;

这样日期就被我们锁定在2007-1-1了。。。,现在我们要让时间继续走,于是建一个系统线程去更新SystemTime:

代码:

pSharedData = ProcessPTE->p;
RtlTimeToTimeFields((PLARGE_INTEGER)&SharedUserData->SystemTime, &TimeFields);
TimeFields.Year = 2007;
TimeFields.Month = 1;
TimeFields.Day = 1;
RtlTimeFieldsToTime(&TimeFields, (PLARGE_INTEGER)&pSharedData->SystemTime);
pSharedData->SystemTime.High2Time = pSharedData->SystemTime.High1Time;

测试一下,我以现程序有些问题,在被替换进程的窗口最小化的时候会让系统死锁,是于我中断下来,查看调用堆栈:

代码:

80473ae0 8046950e 00000001 82060102 000000d1 nt!RtlpBreakWithStatusInstruction
80473ae0 80069b02 00000001 82060102 000000d1 nt!KeUpdateSystemTime+0x13e
80473b64 80464b59 0000000e 00000000 00000000 hal!HalProcessorIdle+0x2
80473b68 00000000 00000000 00000000 00000000 nt!KiIdleLoop+0x10

系统在这里空转, 查看我们目标进程的调用堆栈:

代码:

be482a50 8042d87a 00cc0020 00000000 0000032c nt!KiSwapThread+0xc5
be482a78 a007dbad 00000001 00000000 be482a8c nt!KeDelayExecutionThread+0x180
be482a94 a007e005 00000001 a03106e0 01010057 win32k!UserSleep+0x2b
be482af4 a007e11a 0000032c 01010057 000000c0 win32k!xxxAnimateCaption+0x229
be482b48 a004d809 a0332cf8 00000003 a0332cf8 win32k!xxxDrawAnimatedRects+0xc8
be482c20 a00151d3 e1f32468 00000006 00010000 win32k!xxxMinMaximize+0x1fc
be482c48 a003e872 00000010 00010006 00000112 win32k!xxxShowWindow+0x147
be482c7c a008977d a0332cf8 0000f020 00000000 win32k!xxxSysCommand+0x2a6
be482cdc a00045fa a0332cf8 00000112 0000f020 win32k!xxxDefWindowProc+0xc43
be482cf0 a00045e1 a0332cf8 00000112 0000f020 win32k!xxxWrapDefWindowProc+0x15
be482d0c a0004588 a0332cf8 00000112 0000f020 win32k!NtUserfnDWORD+0x25
be482d40 80465691 00050100 00000112 0000f020 win32k!NtUserMessageCall+0x89
be482d40 77df37e7 00050100 00000112 0000f020 nt!KiSystemService+0xc4
0012fe0c 00000000 00000000 00000000 00000000 +0x77df37e7

xxxAnimateCaption之上是UserSleep,这看起来是一个循环,于是我们转去对xxxAnimateCaption进行调度,发现之所以在这里卡住,是因为
我们没有更新SharedUserData的TickCountLow,而xxxAnimateCaption会从0x7ffe0000取这个值来画出动画效果,
具体可看xxxAnimateCaption的反汇编代码,比较多,我就不列出来了。。。
当然光是一个死循环还不至于让系统死锁,我们再向上看,xxxDrawAnimatedRects的时候调用GetDCEx导致锁定一个对象,具体见函数
GreLockDisplay的代码。而一个死循环导致了系统一直没有机会ReleaseDC。系统会不断的等待这个对象,所以导致了系统的死锁。

通过调试,我们发现GreLockDisplay锁定的对象是0x81e1bc08,

代码:

kd> dt 0x81e1bc08 _ERESOURCE -b
   +0x000 SystemResourcesList : _LIST_ENTRY [ 0x81e1bbc8 - 0x81e1bd08 ]
      +0x000 Flink            : 0x81e1bbc8 
      +0x004 Blink            : 0x81e1bd08 
   +0x008 OwnerTable       : (null) 
   +0x00c ActiveCount      : 1
   +0x00e Flag             : 0x80
   +0x010 SharedWaiters    : 0x81cf4168 
   +0x014 ExclusiveWaiters : 0x81e0c068 
   +0x018 OwnerThreads     : 
    [00] _OWNER_ENTRY
      +0x000 OwnerThread      : 0x81d0d020
      +0x004 OwnerCount       : 1
      +0x004 TableSize        : 1
    [01] 
      +0x000 OwnerThread      : 0
      +0x004 OwnerCount       : 0
      +0x004 TableSize        : 0
   +0x028 ContentionCount  : 0x5e9
   +0x02c NumberOfSharedWaiters : 0
   +0x02e NumberOfExclusiveWaiters : 3
   +0x030 Address          : (null) 
   +0x030 CreatorBackTraceIndex : 0
   +0x034 SpinLock         : 0

0x81d0d020就是我们的线程,从这个结构来看,我们的线程占有了一个对象,而还有3个等待者(NumberOfExclusiveWaiters : 3),
这3个等待者在ExclusiveWaiters : 0x81e0c068指向的地方,

代码:

kd> dt 0x81e0c068 _KEVENT -b
   +0x000 Header           : _DISPATCHER_HEADER
      +0x000 Type             : 0x1 ''
      +0x001 Absolute         : 0 ''
      +0x002 Size             : 0x4 ''
      +0x003 Inserted         : 0 ''
      +0x004 SignalState      : 0
      +0x008 WaitListHead     : _LIST_ENTRY [ 0x81cf962c - 0x81da15cc ]
         +0x000 Flink            : 0x81cf962c 
         +0x004 Blink            : 0x81da15cc 

kd> dt 0x81cf962c _KWAIT_BLOCK -b
   +0x000 WaitListEntry    : _LIST_ENTRY [ 0x81e0ca6c - 0x81e0c070 ]
      +0x000 Flink            : 0x81e0ca6c 
      +0x004 Blink            : 0x81e0c070 
   +0x008 Thread           : 0x81cf95c0 
   +0x00c Object           : 0x81e0c068 
   +0x010 NextWaitBlock    : 0x81cf9674 
   +0x014 WaitKey          : 0
   +0x016 WaitType         : 1

kd> dt 0x81e0ca6c _KWAIT_BLOCK -b
   +0x000 WaitListEntry    : _LIST_ENTRY [ 0x81da15cc - 0x81cf962c ]
      +0x000 Flink            : 0x81da15cc 
      +0x004 Blink            : 0x81cf962c 
   +0x008 Thread           : 0x81e0ca00 
   +0x00c Object           : 0x81e0c068 
   +0x010 NextWaitBlock    : 0x81e0cab4 
   +0x014 WaitKey          : 0
   +0x016 WaitType         : 1

kd> dt 0x81da15cc _KWAIT_BLOCK -b
   +0x000 WaitListEntry    : _LIST_ENTRY [ 0x81e0c070 - 0x81e0ca6c ]
      +0x000 Flink            : 0x81e0c070 
      +0x004 Blink            : 0x81e0ca6c 
   +0x008 Thread           : 0x81da1560 
   +0x00c Object           : 0x81e0c068 
   +0x010 NextWaitBlock    : 0x81da1614 
   +0x014 WaitKey          : 0
   +0x016 WaitType         : 1

其中两个线程0x81da1560和0x81cf95c0是VMWare的关键线程(看来VMWare需要跟系统配合运行的,并不是完全模拟硬件),
另一个线程0x81e0ca00是csrss.exe(csrss负责win32子系统)的关键线程。以上就是导致系统死锁的根本原因。
(windbg中可以用!locks来搜索系统中所有的锁,当然有时候搜索出来的结果比较多,还没有手工来得直接)

要解决这个问题,我们可采用以下方法:
1. 关闭系统的动画效果
2. 我们来更新这个值

如果采用1的方法,我难保系统的其他地方没有用SharedUserData的TickCountLow这个值来做循环因子,因此最彻底的方法是我们自己更新这个值,于是
我们在我们的系统线程中加入:

代码:

pSharedData->TickCountLow = SharedUserData->TickCountLow;
现在死锁问题解决了,我们又迎来了一个新的问题:我们申请的用于替换的内存会被置换到Standby链表上去。当有程序要访问0x7ffe0000的时候就会非法访问。

用Arm加壳的程序在调用SetProcessWorkingSetSize或程序最小化时会出现这个问题。这是因为Windows分页机制造成的,Windows会根据需要调整进程的工作集空间。
什么情况下内存会置换出去呢?我们申请的明明是非分页内存是不允许被置换出去的,为什么会出现这种情况呢?事出必有因,什么时候Windows会调整进程工作集,
调整工作集的条件是什么?为什么有些页不会被置换出去?要解答这些问题,我们需要分析Windows的相关代码。

通过不断的调试分析,最终我把相当代码锁定到了MiFreeWsle函数,拿出我们手里都有的那份代码,经过简化后,关键的地方就是如下:

代码:

if ((Pfn1->u2.ShareCount > 1) &&
    (Pfn1->u3.e1.PrototypePte == 0)) {

    UNLOCK_PFN (OldIrql);
    return FALSE;
}

//
// Found a candidate, remove the page from the working set.
//
MiEliminateWorkingSetEntry (WorkingSetIndex,
      PointerPte,
      Pfn1,
      Wsle);

如果一个页的ShareCount(共享数)为1,就会调用MiEliminateWorkingSetEntry把页从工作集中移走。

了解了吧?跃然我们在系统空间中申请了一页非分页内存,但映射到0x7ffe000后相关PfnDatabase并不知道,而我们这页的共享数为1,所以0x7ffe000会被置换走.
当有程序试图访问0x7ffe0000的时候会产生页访问异常。此时系统不知道如何处理这个突如其来的异常而导致系统崩溃。

所以要解决这个问题,我们需要手工改PfnDatabase的SharedCount为大于1的数,这里蛮干,我机器上PfnDatabase的地址是0x820bf000。

代码:

PMMPFN      MmPfnDatabase = (PMMPFN)0x820bf000;

PointerPte = GetPteAddress (0x7ffe0000);
PageFrameNumber = PointerPte->u.Hard.PageFrameNumber;
Pfn1 = &MmPfnDatabase[PageFrameIndex];
Pfn1->u3.e2.ReferenceCount = 2;
Pfn1->u2.ShareCount = 2;
Pfn1->u3.e1.PrototypePte = 0;

到这里我们已经把重要的问题都解决了,但还有一个比较重要的问题。。。有借有还,在进程退出的时候我们需要还原原始的PTE,这样就不至于让系统产生PFN_LIST_CORRUPT异常了。

结束语:我们在进程创建的时候就替换SharedUserData,可根据普通应用程序的特性做出一些与时间有关的应用,如加速,减速,Anti GetTickCount等。。。

感谢:Forever, fly, heXer, jwh51, +dEMON, kanxue, shoooo, 南蛮妈妈, sucsor, cnbragon, 堀北真希, winroot,
        DarkNess0ut and RCT & FCG & CCG all Members and you.