gl00my-内存与进程管理器2

gl00my-内存与进程管理器2
==========================

                                   But I fear tomorrow I'll be crying,

07.page fault的处理
==============================

对于转向对pagefault的研究,我们现在有了所有必须的信息了。转换线性地址时,当线性地址(分页机制打开)的所用的PDE/PTE的P(present)位无效或是违反了保护规则,在+i386处理器里会产生异常14。这时,在堆栈中有错误代号,包含有以下信息:用户/内核错误位(异常发生在ring3还是ring0?),读写错误位(试图读还是写?),页存在位。除此之外,在CR2寄存器中存有产生异常的32位线性地址。内核中处理14号中断的是_KiTrap0E。

当要转换的页没有相应的物理页时,内存管理器执行确定好的工作来“修正”。这些是由异常处理函数调用高层函数MmAccessFault   (Wr,Addr,P);来完成的。在对伪代码的进行分析之前,想一下在什么样的情况下会发生page fault是很有用的。

最显然的就是访问错误,这时ring3的代码试图写入PTE/PDE中未设置U位的页或是写入了只读的页(PTE/PDE中未设置W位)。再有,页可以被换出到页面文件中,对应于这些页的PTE中未设置P位,但有信息指示在哪个页面文件中寻找frame,以及frame的偏移。还有一个类似的情况——frame属于映象文件。除此之外,所转换的页可能只属于已分配的内存区(使用NtAllocateMemory),也可能转换的是原先没转换过的页,这中情况下,VMM分配清零过的frame(这是C2的要求)。最后,异常还可能是由写copy on write页和转换共享内存引发。以上只列出了主要的情况。

处理的结果通常是向当前进程的Working Set中添加相应的frame。

异常的每一种情况都相应有一个内部的结构体与之相关联,VMM就处理这些结构体。这些结构体十分复杂,要对它们进行完整的描述的话,需要反汇编大量的函数。目前还没有大部分结构体的完整信息,但对于理解异常处理程序来说并不要求知道这些。我来大致描述一下VAD和PPTE的概念,研究异常处理程序的伪代码要用到。


VAD

操作虚拟地址需要用到VAD (Virtual Address Descriptor)。我们熟知的(有一个几乎与之同名的Win32函数调用这个函数)未公开函数NtAllocateVirtualMemory(ring0下是ZwAllocateVirtualMemory)操作这些结构体。

每一个VAD都描述了虚地址空间中的区域,实际上,除了区域的起止地址外还有保护信息(见ZwAllocateVirualMemory函数的参数)。而同时还有其它一些特殊的信息(目前除了首部之外还没有VAD的完整信息)。VAD结构体只对用户地址(低2GB)有意义,使用这些结构体VMM可以捕获到发生异常的区域。VAD的结构是一个平衡二叉树(有内部函数负责修整此树),这是为查找而进行的优化。在VAD中有两个指向后面元素——左右子树——的指针。树的根位于EPROCESS结构体的VadRoot域(NT 4.0下是偏移0x170)。当然,每一个进程都有自己的VAD树。VAD的首部形式如下:

typedef struct vad_header {
     void *StartingAddress;
     void *EndingAddress;
     struct vad *ParentLink;
     struct vad *LeftLink;
     struct vad *RightLink;
     ULONG Flags;
}VAD_HEADER, *PVAD;


PPTE

Prototype Pte是又一级的线性地址转换并用于共享内存。假设有个文件映射到了几个(3个)进程的地址空间。PPTE表包含有PPTE,这些PPTE描述了加载到内存的文件的物理页。某些PPTE可以有P位(其位置与含义与PTE/PDE的相同),而某些则没有,没有P位的有信息用来决定是从页来加载frame还是从映象文件来加载文件。所有三个进程的文件都映射在不同的地址上,对应于这些页的PTE的P位未设置,并且包含有文件页的PPTE的引用。这样,在转换映射到文件的线性地址的时候,在一号进程中发生异常14,VMM找到PTE,得到对PPTE的引用,现在可以直接“修正”相应的PTE,以使其指向属于文件的frame,这时必需从文件中加载frame。我给出未设置P位PTE的格式,在页表中其指向原型PTE。

PTE points to PPTE
+-----------------------------------------+-+---+-------------+-+
|3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1|1|0 0|0 0 0 0 0 0 0|0|
|1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1|0|9 8|7 6 5 4 3 2 1|0|
+-----------------------------------------+-+---+-------------+-+
| Address [7:27]                          |1|Un | Address     |0|
|                                         | |use|   [0:6]     | |
|                                         | |d  |             | |
+-----------------------------------------+-+---+-------------+-+


*MmAccessFault

我们开始来研究一下MmAccessFault的伪代码。其原型:

NTSTATUS MmAccessFault (BOOL Wr,DWORD Addr, BOOL P)

参数的意义很明显:写入标志,发生异常的地址和页存在位。对于确定异常的原因,这些信息就足够了。根据Addr是属于内核地址空间还是用户地址空间,处理程序从两个执行分支中选择一个。第一种情况下的处理程序较为简单,跟踪ACCESS VIOLATION或是收回在Working Set中的页(MiDispatchFault)。若是用户空间的地址情况就就更为复杂一些。首先,如果PDE不在内存中则执行用于PDE的异常处理程序。然后,出现了一个分支。第一个分支——页存在。这表示要么是ACCESS VIOLATION,要么就是对copy on write的处理。第二个分支——处理清零页请求、ACCESS VIOLATION、页边界(GUARD)(堆栈增长)以及必须的对working set中页的回收。有趣的是,在大量发生page fault的时候,系统会增大working set的大小。在零PTE的情况下,为确定状况,处理程序不得不使用VAD树来确定试图访问区域的属性。这些都是MiAccessCheck的工作,这个函数返回访问的状态。

一般情况下,异常处理程序的主要奠基工作是由MiDispatchFault函数执行的。它能更精确的确定状况并决定下一步的工作。

轮到MiDispatchFault了,它主要是基于一些更低级的函数:MiResolveTransitionFault、MiResolveDemandZeroFault、MiResolveDemandZeroFault、MiResolveProtoPteFault和MiResolvePageFileFault。从这些函数的名字可以明显看出,这个函数用于确定更为具体的情况:状态为'transition'(可能会很快回收入Working Set)的页应该是空白的frame,PTE指向PPTE并且frame换出到相应的页面文件中。在与页面文件有关的和某些与PPTE有关的情况下,接着可能需要从文件中读取frame,此时函数返回值为0xc0033333,表示必须从文件中读取页。这在MiDispatchFault中是靠IoPageRead进行的。我们来更仔细的研究一下所提到的函数。我们从MiResolveDemandZeroFault开始。

如果看一下这个函数的伪代码,则可以轻易的明白它的工作逻辑。请求zero frame并且进程得到这个frame。这时执行函数MiRemoveZeroPage或是MiRemoveAnyPage。第一个函数从zero页的链表中取一页。如果未能成功,则通过第二个函数选择任何一页。这样的话,该页就由MiZeroPhysicalPage来清零。最终,在MiAddValidPageToWorkingSet中,该清零的页被添加到工作集中(恰好,这个事实证明在分配内存时进程不能取得对未处理页的访问)。现在我们来研究一下更为复杂的情况——页位于页面文件中。

前面的伪代码需要一个结构体。在准备从文件中读取页的时候,会填充PAGE_SUPPORT_BLOCK结构体。之后,对所有即将参与到操作中来的PFN进行以下操作:设置read in progress标志并在Misc域中写入PAGE_SUPPORT_BLOCK的地址(函数MiInitializeReadInProgressPfn)。最后,函数返回magic number 0xc0033333,表示随后要在IoPageRead调用中使用此结构体(恰巧,IoPageRead被导出了,但是未公开的。从其伪码中可以很容易地得到其原型)。

typedef struct _PAGE_SUPPORT_BLOCK{     //  size: 0x98
         DISPATCHER_HEADER DispHeader;  // 0 FastMutex
         IO_STATUS_BLOCK IoStatusBlock; // 0x10
         LARGE_INTEGER AddrInPageFile;  // 0x18 (file offset)
         DWORD RefCounter;              // 0x20 (0|1) ???
         KTHREAD Thread;                // 0x24
         PFILE_OBJECT FileObject;       // 0x28
         DWORD AddrPte;            // 0x2c
         PPFN pPfn;                // 0x30
         MDL Mdl;                       // 0x34
         DWORD MdlFrameBuffer[0x10];    // 0x50
         LIST_ENTRY PageSupportList;    // 0x90 与MmInPageSupportList有关的链表
}PAGE_SUPPORT_BLOCK *PAGE_SUPPORT_BLOCK;

struct _MmInPageSupportList{
     LIST_ENTRY PageSupportList;
     DWORD Count;
     }MmInPageSupportList;

函数MiResolvePageFileFault本身非常简单,除了填充相应的结构体并返回0xc0033333之外什么也不干。剩下的就是执行MiDispatchFault。这很合乎情理,如果还记得复用代码的原则的话。

还有一个不太复杂的函数MiResolveTransitionFault。对于状态为transition的frame还需要再多说几句。从这个状态中frame可以很快地返回到进程的Working Set中。

于是,剩下了最后一种情况——PROTO PTE。这种情况的处理函数也不太复杂,而且支撑其的基础我们已经讲过了。实际上还有一个函数与这种情况有关,这就是MiCompleteProtoPteFault,从MiDispatchFault中调用。要想理解这些函数的工作就去看一下伪代码。


07. section 对象
================

NT 中的section对象就是一块内存,这块内存由一个进程独有或几个进程共享。在Win32子系统中section就是文件映射(file mapping object)。我们来看一下section对象到底是什么。

section是NT下非常常用的对象,执行系统使用section来将可执行映象加载到内存中并用其来管理cache。section同时也用在向进程地址空间中映射文件。这时访问文件就像访问内存。section对象,就像其它的对象一样,是由对象管理器创建的。高层次的信息告诉我们,对象的body中包含着以下类型的信息:section的最大值,保护属性,其它属性。什么是section的最大可访问值,这不说也知道。保护属性是用于section页的属性。其它section属性有表示是文件section还是为空值(映射入页面文件)的标志,以及section是否是base的。base的section以相同的虚拟地址映射入所有进程的地址空间。

为了得到此对象结构的真实信息,我反汇编了一些用于section的内存管理器函数。下面的信息可是在别的地方见不到的。我们先来看结构体。

系统中的每一个文件都是对象(NTDDK.H中有描述)FILE_OBJECT。在这个结构体中有SectionObjectPointer。NTDDK.H中同样有它的结构。

//
:
PSECTION_OBJECT_POINTERS SectionObjectPointer;
:
//

typedef struct _SECTION_OBJECT_POINTERS {
    PVOID DataSectionObject;
    PVOID SharedCacheMap;
    PVOID ImageSectionObject;
} SECTION_OBJECT_POINTERS;

在结构体中有两个指针——DataSectionObject 和 ImageSectionObject。NTDDK.H把它们写成了PVOID,因为它们引用的是未公开的结构体。DataSectionObject用在将文件作为数据打开的时候。ImageSectionObject——此时当作映象。这些指针的类型全都一样,且可以称之为PCONTROL_AREA。所有下面这些结构体都是Windows 2K的,较之NT 4.0的有些变化。

typedef struct _CONTROL_AREA { // for NT 5.0, size = 0x38
                    PSEGMENT  pSegment;      //00
                    PCONTROL_AREA  Flink;         //04
                    PCONTROL_AREA  Blink;         //08
                    DWORD          SectionRef;    //0c
                    DWORD          PfnRef;           //10
                    DWORD          MappedViews;   //14
                    WORD      Subsections;   //18
                    WORD      FlushCount;    //1a
                    DWORD          UserRef;       //1c
                    DWORD          Flags;         //20
                    PFILE_OBJECT   FileObject;    //24
                    DWORD          Unknown;       //28
                    WORD      ModWriteCount; //2c
                    WORD      SystemViews;   //2e
                    DWORD PagedPoolUsage;         //30
                    DWORD NonPagedPoolUsage; //34
                    } CONTROL_AREA, *PCONTROL_AREA;

我们可以看到,CONTROL_AREA形成了一个链表,结构体中包含着统计值和标志。为了理解标志所代表的信息,我给出它们的值(用于NT5.0

/******************** nt5.0 ******************/
#define BeingDeleted               0x1
#define BeingCreated               0x2
#define BeingPurged                0x4
#define NoModifiedWriting          0x8
#define FailAllIo                  0x10
#define Image                      0x20
#define Based                      0x40
#define File                       0x80
#define Networked                  0x100
#define NoCache                    0x200
#define PhysicalMemory             0x400
#define CopyOnWrite                0x800
#define Reserve                    0x1000
#define Commit                     0x2000
#define FloppyMedia                0x4000
#define WasPurged                  0x8000
#define UserReference              0x10000
#define GlobalMemory               0x20000
#define DeleteOnClose              0x40000
#define FilePointerNull            0x80000
#define DebugSymbolsLoaded         0x100000
#define SetMappedFileIoComplete    0x200000
#define CollidedFlush              0x400000
#define NoChange                   0x800000
#define HadUserReference           0x1000000
#define ImageMappedInSystemSpace   0x2000000

紧随CONTROL_AREA之后的是Subsection的数目Subsections。每一个Subsection都描述了关于具体的文件映射section的信息。例如,read-only, read-write, copy-on-write等等的section。NT5.0的SUBSECTION结构体:

typedef struct _SUBSECTION { // size=0x20 nt5.0
               // +0x10 if GlobalOnlyPerSession
               PCONTROL_AREA   ControlArea; //38, 00
               DWORD           Flags;       //3c, 04
               DWORD           StartingSector;//40, 08
               DWORD           NumberOfSectors; //44, 0c
               PVOID           BasePte;       //48, 10 pointer to start pte
               DWORD           UnusedPtes;    //4c, 14
               DWORD           PtesInSubsect; //50, 18
               PSUBSECTION     pNext;         //54, 1c
               }SUBSECTION, *PSUBSECTION;

在subsection中有指向CONTROL_AREA的指针,标志,指向base Proto PTE的指针,Proto PTE的数目。StartingSector是4K block的编号,文件中的section起始于此。在标志中还有额外的信息:

#define SS_PROTECTION_MASK               0x1f0
#define SS_SECTOR_OFFSET_MASK            0xfff00000 // (low 12 bits)
#define SS_STARTING_SECTOR_HIGH_MASK     0x000ffc00  //  (nt5 only) (in pages)

//other 5 bit(s)

#define ReadOnly       1
#define ReadWrite      2
#define CopyOnWrite    4
#define GlobalMemory   8
#define LargePages 0x200

我们来看剩下的最后一个结构体SEGMENT,它描述了所有的映射和用于映射section的Proto PTE。SEGMENT的内存是从paged pool中分配的。我给出SEGMENT结构体(NT 5.0)

typedef struct  _SEGMENT {
          PCONTROL_AREA ControlArea;    //00
          DWORD         BaseAddr;       //04
          DWORD         TotalPtes;      //08
          DWORD         NonExtendedPtes;//0c
          LARGE_INTEGER SizeOfsegemnt;  //10
          DWORD        ImageCommit;     //18
          DWORD        ImageInfo;       //1c
          DWORD        ImageBase;       //20
          DWORD        Commited;        //24
          PTE          PteTemplate;     //28 or 64 bits if pae enabled
          DWORD BasedAddr;         //2c
          DWORD BaseAddrPae;       //30 if PAE enabled
          DWORD ProtoPtes;         //34
          DWORD ProtoPtesPae;           //38 if PAE enabled
          }SEGMENT,*PSEGMENT;

正如我所料,结构体包含着对CONTROL_AREA的引用,指向Proto PTE的pool的指针和所有section的信息。有个东西需要解释一下。结构体的样子依赖于是否支持PAE。PAE就是Physical Address Extenion。从第5版开始,Windows NT包含了支持PAE的内核Ntkrnlpa.exe。总的来讲,支持PAE就意味着在NT里可以使用的虚拟地址不是4GB而是64GB。在使用PAE时的地址转换又多了一级——所有的虚地址空间被分为4部分。在打开PAE时PTE和PDE的大小不是4B而是8B,这我们可以从SEGMENT结构体中看出。现在还不需要进一步详细的讲PAE,毕竟很少用到,所以我们就此打住。

描述section的所有结构体都介绍过了,而section对象结构体本身还没有提到。从直观上可以想到,它应该会引用到SEGMENT或是CONTROL_AREA,因为有了这两个结构体后就可以得到保存的所有信息。通过反汇编得到的section对象的body为以下形式:

typedef struct _SECTION_OBJECT { // size 0x28
               VAD_HEADER VadHeader; // 0
               PSEGMENT pSegment;         //0x14 Segment
               LARGE_INTEGER SectionSize; //0x18
               DWORD ControlFlags;        //0x20
               DWORD PgProtection;        //0x24
               } SECTION_OBJECT, *SECTION_OBJECT;

#define PageFile      0x10000
#define MappingFile   0x8000000
#define Based         0x40
#define Unknown       0x800000 // not sure, in fact it's AllocAttrib&0x400000

我们看到,所得的结构体完全符合现有的高层信息的描述。唯一可能有疑问的就是VAD_HEADER。它描述了base section在地址空间中的位置。VAD_HEADER位于顶点为_MmSectionBasedRoot的VAD树中。我们再次体会到,要理解操作系统的工作原理,就要理解其内部的结构。为了有一个总体上的把握,下面给出了描述section的结构体间互相联系的一个图。

SECTION_OBJECT->SEGMENT<->CONTROL_AREA->FILE_OBJECT->SECTION_OBJECT_POINTERS+
                               ^                                            |
                               +--------------------------------------------+


08.从内存管理器角度看进程的创建
====================================================

前面我们从Win32角度介绍过进程的创建,也讲过内存管理器和对象管理器的工作原理,以及section对象结构体。现在最有意思的当然就是在进程创建中将内存管理器也考虑进来。

进程是用未公开的系统调用NtCreateProcess()创建的。下面给出其伪代码:

/*****************************************************************/
/* -- Here it is, just wrapper -- */
NtCreateProcess(
      OUT Handle,
      IN ACCESS_MASK Access,
      IN POBJECT_ATTRIBUTES ObjectAttrib,
      IN HANDLE Parent,
      IN BOOLEAN InheritHandles,
      IN HANDLE SectionHandle,
      IN HANDLE DebugPort,
      IN HANDLE ExceptionPort
      )
{
if(Parent)
        {
     ret=PspCreateProcess(Handle,
                    Access,
                    ObjectAttrib,
                    Parent,
                    InheritHandles,
                    SectionHandle,
                    DebugPort,
                    ExceptionPort);
        }
else ret=STATUS_INVALID_PARAMETER;
return ret;
}

我们看到,NtCreateProcess是对另一个内部函数PspCreateProcess的封装。NtCreateProcess进行的唯一工作就是检查Parent(父进程句柄)。但是接下来我们看到,对于NT来说这并没有什么意义,因为总的来说,进程的继承性本身没有特别的意义。现在我们来看PspCreateProcess()。

PspCreateProcess(
      OUT PHANDLE Handle,
      IN ACCESS_MASK Access,
      IN POBJECT_ATTRIBUTES ObjectAttrib,
      IN HANDLE Parent,
      IN BOOLEAN InheritHandles,
      IN HANDLE SectionHandle,
      IN HANDLE DebugPort,
      IN HANDLE ExceptionPort
      );

我很快注意到,函数中的Parent参数可以接受值0,这就表明在NtCreateProcess中检验此参数是为了限制用户模式。函数的参数中有对section、debug port和exception port、父进程的引用。通过调用ObReferenceObjectByHandle,可以得到指向这些对象的指针。实际上父进程句柄通常传递的是-1,这表示是当前进程。如果Parent等于0,则进程的affinity就不从父进程处取得,而是从系统变量中取得。

if(Parent)
      { //Get pointer to father's body
        ObReferenceObjectByHandle(Parent,0x80,PsProcessType,PrevMode,&pFather,0);
        AffinityMask=pFather->Affinity; // on witch processors will be executed
        Prior=8;
      }
else {
      pFather=0;
      AffinityMask=KeActiveProcessors;
      Prior=8;
     }

优先级总是为8。随后,创建进程对象。NT4.0下其大小为504字节。

// size of process body - 504 bytes
// creating process object... (type object PsProcessType)
ObCreateObject(PrevMode,PsProcessType,ObjectAttrib,PrevMode,0,504,&pProcess);
// clear body
memset(pProcess,0,504);

初始化某些域和Quota Block(见对象管理器的相关介绍)。

pProcess->CreateProcessReported=0;
pProcess->DebugPort=pDebugPort;
pProcess->ExceptPort=pExceptPort;

// Inherit Quota Block, if pFather==NULL, PspDefaultQuotaBlock
PspInheritQuota(pProcess,pFather);

if(pFather){
           pProcess->DefaultHardErrorMode=pFather->DefaultHardErrorMode;
           pProcess->InheritedFromUniqueProcessId=pFather->UniqueProcessId;
           }
else {
       pProcess->InheritedFromUniqueProcessId=0;
       pProcess->DefaultHardErrorMode=1;
     }

之后,调用MmCreateProcessAddressSpace,创建地址上下文。参数是函数得到的指向进程的指针、工作集的大小和指向结果结构体的指针。这个结构体形式如下:

struct PROCESS_ADDRESS_SPACE_RESULT{
 dword Dt; // dict. table phys. addr.
 dword HypSpace; // hyp space page phys. addr.
 dword WorkingSet; // working set page phys. addr.
}CASResult;

MmCreateProcessAddressSpace(PsMinimumWorkingSet,pProcess,&CASResult);

我们看到,函数向我们返回的是页表的物理地址描述符(用于新地址空间的CR3的内容),Hyper Space的页地址和工作集的页地址。在此之后是初始化进程对象的某些域:

 pProcess->MinimumWorkingSet=MinWorkingSet;
 pProcess->MaximumWorkingSet=MaximumWorkingSet;

KeInitializeProcess(pProcess,Prior,AffinityMask,&CASResult,pProcess->
                                             DefaultHardErrorProcessing&0x4);

pProcess->ForegroundQuantum=PspForegroundQuantum;

如果有父进程且设置了标志参数,则会继承父进程的句柄表:

 if(pFather) // if there is father and inherithandle, so, inherit handle db
      {
       pFather2=0;
       if(bInheritHandle)pFather2=pFather;
       ObInitProcess(pFather2,pProcess); // see info about ObjectManager
      }

下面的东西比较有意思,证明了NT执行系统的灵活性,从表面上是看不出来的。如果在参数中有指定的section,则使用这个section来初始化进程的地址空间,否则其工作就会像*UNIX中的fork()。

 if(pSection)
     {
       MmInitializeProcessAddressSpace(pProcess,0,pSection);
       ObDereferenceObject(pSection);
       res=ObInitProcess2(pProcess); //work with unknown byte +0x22 in process
       if(res>=0)PspMapSystemDll(pProcess,0);
       Flag=1; //Created addr space
     }
else { // if there is futher, but no section, so, do operation like fork()
      if(pFatherProcess){
                   if(PsInitialSystemProcess==pFather){
                             MmRes=MmInitializeProcessAddressSpace(pProcess,0,0);
                                                   }
                   else {
                      pProcess->SectionBaseAddress=pFather->SectionBaseAddress;
                      MmRes=MmInitializeProcessAddressSpace(pProcess,pFather,0);
                      Flag=1; //created addr space
                         }
                  }
     }

接下来是使用PsActiveProcessHead将进程插入Active Process链表,创建Peb和做其它辅助性的工作。我们不再赘述。最后,当所有的工作都做完后,进行安全子系统方面的工作。我们过去曾研究过安全子系统(见对象管理器部分),所以这里只简单的给出其伪代码。只是我注意到,如果父进程是system(句柄值等于PspInitialSystemProcessHandle),则不对其安全性进行检验。

// finally, security operations
if(pFather&&PspInitialSystemProcessHandle!=Father)
           {
            ObGetObjectSecurity(pProcess,&SecurityDescriptor,&MemoryAllocated);
            pToken=PsReferencePrimaryToken(pProcess);
            AccessRes=SeAccessCheck(SecurityDescriptor,&SecurityContext,
                             0,0x2000000,
                             0,0,&PsProcessToken->GenericMapping,
                             PrevMode,pProcess->GrantedAccess,
                             &AccessStatus);
            ObDereferenceObject(pToken);
            ObReleaseObjectSecuryty(SecurityDescriptor,MemoryAllocated);
            if(!AccessRes)pProcess->GrantedAccess=0;
            pProcess->GrantedAccess|=0x6fb;
            }
   else{
        pProcess->GrantedAccess=0x1f0fff;
       }

   if(SeDetailedAuditing)SeAuditProcessCreation(pProcess,pFather);

最有意思的是函数KeInitializeProcess和MmCreateProcessAddressSpace。前一个函数除了初始化进程对象的其它成员之外,还要初始化TSS中的IO位图的偏移。

pProcess->IopmOffset=0x20ad; // IOMAP BASE!!!
                            // You can patch kernel here and
                            // got i/o port control ;)

偏移的选取是这样的,它指向I/O位图,这样就能阻止进程直接使用I/O端口。

在函数MmCreateProcessAddressSpace中进行的是进程地址空间的创建。我就不给出所有的伪代码了,只简要的写写主要的操作。它为Hyper Space, Working Set和Page Directory选择页。反汇编后的代码证实了,它们是从zero frame链表中选出或是由MiZeroPhysicalPage函数来清零的。之后初始化新创建的Page Directory。

pProcess->WorkingSetPage=Frame3; // WorkingSetPage
(MmPfnDatabase+0x18*Frame)->Pte=0xc0300000;
ValidPde_U=ValidPdePde&0xeff^Frame2; // HyperSpace

/**************IMPORTANT!!!!!!!!!!!!!!************************/
/* 重要! 这里初始化PD                                        */
/*************************************************************/

Va=MiMapPageInHyperSpace(Frame,&LastIrql);
// no we got Va of our new Page Directory
// Fill some fields
*(Va+0xc04)=ValidPde_U; // HyperSpace
ValidPde_U=ValidPde_U&0xfff^PhysAddr; // DT
*(Va+0xc00)=ValidPde_U; // self-pde

// copy from current process, kernel address mapping
memcpy(
     (MmVirtualBias+0x80000000)>>0x14+Va, // it's like that we found,
                                          // what MmVirtualBias is it ;)
     (MmVirtualBias+0x80000000)>>0x14+0xc0300000,
     0x80 // 32 pdes -> 4Mb*32=128Mb
     );

memcpy( // copy pdes, corresponding to NonPagedArea
     MmNonPagedSystemStart>>0x14+Va,
     MmNonPagedSystemStart>>0x14+0xc0300000,
     (0xc0300ffc-MmNonPagedSystemStart>>0x14+0xc0300000)&0xfffffffc+4);

memcpy(Va+0xc0c, // cache, forgot about it now, it's another story ;)
       0xc0300c0c,
       (MmSystemCacheEnd>>0x14)-0xc0c+4
       );

也就是将PDE拷贝到内核地址空间中去(其对所有的进程不变,Hyper Space除外),而且是拷贝到不可换出的区域。同时这个空间是属于系统cache的。


09.上下文切换
==========================

知道了ETHREAD、EPROCESS结构体和内存管理器的工作原理,就不难猜到上下文切换时会发生什么。Windows NT的设计者使用线程,不关心共享的是谁的地址空间,也就是说有两种可能:线程属于当前进程——必需要切换到另一个线程(更新堆栈并更换GDT描述符),而线程属于另一个进程,必需切换到那个进程(重新加载CR3)。对此,为了证实我的推测,我反汇编了KeAttachProcess函数。这个函数是未公开的,但所有已知的函数都用其来切换到另一进程的地址空间。通过KeDetachProcess可以返回到当前进程。KeAttachProcess使用下述内部函数:

KiAttachProcess - KeAttachProcess仅仅是对这个函数的封装
KiSwapProcess   - 更换地址空间。(本质上就是重新加载CR3)
SwapContext     - 更换上下文。一般不管地址空间的切换,只调整线程上下文。
KiSwapThred     - 切换到链表中的下一个线程(SwapContext)调用

下面给出这些内部函数的伪代码。
-----------------------------------------------------------------------------
/************************ KeAttachProcess ***************************/
// just wrapper
//
KeAttachProcess(EPROCESS *Process)
{
KiAttachProcess(Process,KeRaiseIrqlToSynchLevel);
}
/************************ KiAttachProcess ***************************/

KiAttachProcess(EPROCESS *Process,Irql){

//CurThread=fs:124h
//CurProcess=CurThread->ApcState.Process;

if(CurProcess!=Process){
     if(CurProcess->ApcStateIndex || KPCR->DpcRoutineActive)KeBugCheckEx...
     }

//if we already in process's context
if(CurProcess==Process){KiUnlockDispatcherDatabase(Irql);return;}

Process->StackCount++;
KiMoveApcState(&CurThread->ApcState,&CurThread->SavedApcState);

// init lists
CurThread->ApcState.ApcListHead[0].Blink=&CurThread->ApcState.ApcListHead[0];
CurThread->ApcState.ApcListHead[0].Flink=&CurThread->ApcState.ApcListHead[0];
CurThread->ApcState.ApcListHead[1].Blink=&CurThread->ApcState.ApcListHead[1];
CurThread->ApcState.ApcListHead[1].Flink=&CurThread->ApcState.ApcListHead[1];;

//fill curtheads's fields
CurThread->ApcState.Process=Process;

CurThread->ApcState.KernelApcInProgress=0;
CurThread->ApcState.KernelApcPending=0;
CurThread->ApcState.UserApcPending=0;

CurThread->ApcState.ApcStatePointer.SavedApcState=&CurThread->SavedApcState;
CurThread->ApcState.ApcStatePointer.ApcState=&CurThread->ApcState;

CurThread->ApcStateIndex=1;

//if process ready, just swap it...
if(!Process->State)//state==0, ready
     {
     KiSwapProcess(Process,CurThread->SavedApcState.Process);
     KiUnlockDispatcherDatabase(Irql);
     return;
     }

CurThread->State=1; //ready?
CurThread->ProcessReadyQueue=1;

//put Process in Thread's waitlist
CurThread->WaitListEntry.Flink=&Process->ReadyListHead.Flink;
CurThread->WaitListEntry.Blink=Process->ReadyListHead.Blink;

Process->ReadyListHead.Flink->Flink=&CurThread->WaitListEntry.Flink;
Process->ReadyListHead.Blink=&CurThread->WaitListEntry.Flink;

// else, move process to swap list and wait
if(Process->State==1){//idle?
     Process->State=2; //trans
     Process->SwapListEntry.Flink=&KiProcessInSwapListHead.Flink;
     Process->SwapListEntry.Blink=KiProcessInSwapListHead.Blink;
        KiProcessInSwapListHead.Blink=&Process->SwapListEntry.Flink;
     KiSwapEvent.Header.SignalState=1;
     if(KiSwapEvent.Header.WaitListHead.Flink!=&KiSwapEvent.Header.WaitListHead.
Flink)
                    KiWaitTest(&KiSwapEvent,0xa); //fastcall
     }

CurThread->WaitIrql=Irql;
KiSwapThread();
return;
}

从这个函数可以得到以下结论。进程可以处于以下状态——0(准备),1(Idle),2(Trans——切换)。这证实了高层次的信息。KiAttachProcess使用了另外两个函数KiSwapProcess和KiSwapThread。

/************************* KiSwapProcess ****************************/

KiSwapProcess(EPROCESS* NewProcess, EPROCESS* OldProcess)
{
// just reload cr3 and small work with TSS

     // TSS=KPCR->TSS;
     // xor eax,eax
     // mov gs,ax
TSS->CR3=NewProcess->DirectoryTableBase;//0x1c
     // mov cr3,NewProcess->DirectoryTableBase
TSS->IopmOffset=NewProcess->IopmOffset;//0x66
if(WORD(NewProcess->LdtDescriptor)==0){lldt 0x00; return;//}
     //GDT=KPCR->GDT;
(QWORD)GDT->0x48=(QWORD)NewProcess->LdtDescriptor;
(QWORD)GDT->0x108=(QWORD)NewProcess->Int21Descriptor;
lldt 0x48;
return;
}

切换进程上下文。正如我所料,这个函数只是重新加载CR3寄存器,再加上一点相关的操作。例如,用IopmOffset域的值建立TSS中的I/O位图的偏移。还必需将选择子的值加载到ldt(只用于VDM session)。

/************************* SwapContext ******************************/

SwapContext(NextThread,CurThread,WaitIrql)
{

NextThread.State=ThreadStateRunning; //2
KPCR.DebugActive=NextThread.DebugActive;

cli();

//Save Stack
CurThread.KernelStack=esp;

//Set stack
KPCR.StackLimit=NextThread.StackLimit;
KPCR.StackBase=NextThread.InitialStack;

tmp=NextThread.InitialStack-0x70;
newcr0=cr0&0xfffffff1|NextThread.NpxState|*(tmp+0x6c);
if(newcr0!=cr0)reloadcr0();
if(!*(tmp-0x1c)&0x20000)tmp-=0x10;
TSS=KPCB.TSS;
TSS->ESP0=tmp;

//set pTeb
KPCB.Self=NextThread.pTeb;
esp=NextThread.KernelStack;
sti();

//correct GDT
GDT=KPCB.GDT;
WORD(GDT->0x3a)=NextThread.pTeb;
BYTE(GDT->0x3c)=NextThread.pTeb>>16;
BYTE(GDT->0x3f)=NextThread.pTeb>>24;

//if we must swap processes, do it (like KiSwapProcess)

if(CurThread.ApcState.Process!=NextThread.ApcState.Process)
     {
     //******** like KiSwapProcess
     }

NextThread->ContextSwitches++;

KPCB->KeContextSwitches++;

if(!NextThread->ApcState.KernelApcPending)return 0;

//popf;
// jnz HalRequestSoftwareInterrupt// return 0

return 1;
}

切换堆栈,修正GDT,以使FS寄存器指向TEB。如果线程属于当前进程,则不进行上下文切换。否则,进行的操作和KiSwapProcess中的大致差不多。


为了一致,我给出KeDetachProcess的原型。

KeDetachProcess(EPROCESS *Process,Irql);

我们看到——这些函数的伪码实际上完全描述出了操作系统的上下文切换。总的说来,代码分析表明,理解OS的主要途径就是要知道它的内部结构。


0a.某些未公开的内存管理器函数
==========================================================
SP3的ntoskrnl.exe的内存管理器导出了以下符号:

        467  1D0 00051080 MmAdjustWorkingSetSize
        468  1D1 0001EDFA+MmAllocateContiguousMemory
        469  1D2 00051A14+MmAllocateNonCachedMemory
        470  1D3 0001EAE8+MmBuildMdlForNonPagedPool
        471  1D4 000206BC MmCanFileBeTruncated
        472  1D5 0001EF5A+MmCreateMdl
        473  1D6 0002095C MmCreateSection
        474  1D7 00021224 MmDbgTranslatePhysicalAddress
        475  1D8 000224AC MmDisableModifiedWriteOfSection
        476  1D9 000230C8 MmFlushImageSection
        477  1DA 0001FA9C MmForceSectionClosed
        478  1DB 0001EEA0+MmFreeContiguousMemory
        479  1DC 00051AFE+MmFreeNonCachedMemory
        480  1DD 0001EEAC+MmGetPhysicalAddress
        481  1DE 00024028 MmGrowKernelStack
        482  1DF 0004E144 MmHighestUserAddress
        483  1E0 0002645A+MmIsAddressValid
        484  1E1 00026CD8+MmIsNonPagedSystemAddressValid
        485  1E2 0001F5D8 MmIsRecursiveIoFault
        486  1E3 00026D56+MmIsThisAnNtAsSystem
        487  1E4 000766C8+MmLockPagableDataSection
        488  1E5 000766C8 MmLockPagableImageSection
        489  1E6 0001F160+MmLockPagableSectionByHandle
        490  1E7 0001ED18+MmMapIoSpace
        491  1E8 0001EB74+MmMapLockedPages
        492  1E9 0001F5F6 MmMapMemoryDumpMdl
        493  1EA 00076A14 MmMapVideoDisplay
        494  1EB 0005206C MmMapViewInSystemSpace
        495  1EC 00079B0E MmMapViewOfSection
        496  1ED 0007A7EE+MmPageEntireDriver
        497  1EE 0001E758+MmProbeAndLockPages
        498  1EF 00026D50+MmQuerySystemSize
        499  1F0 00052A8A+MmResetDriverPaging
        500  1F1 0004E0A4 MmSectionObjectType
        501  1F2 00079D28 MmSecureVirtualMemory
        502  1F3 0001EFCE MmSetAddressRangeModified
        503  1F4 0007684E MmSetBankedSection
        504  1F5 0001EF2C+MmSizeOfMdl
        505  1F6 0004E0A0 MmSystemRangeStart
        506  1F7 0001F516+MmUnlockPagableImageSection
        507  1F8 0001EA16+MmUnlockPages
        508  1F9 0007669A+MmUnmapIoSpace
        509  1FA 0001ECA8+MmUnmapLockedPages
        510  1FB 00076A2E MmUnmapVideoDisplay
        511  1FC 00052284 MmUnmapViewInSystemSpace
        512  1FD 0007AFE4 MmUnmapViewOfSection
        513  1FE 0007A00A MmUnsecureVirtualMemory
        514  1FF 0004DDCC MmUserProbeAddress

这里的符号'+'表示函数在DDK中有记载。我这里给出某些未公开函数的原型。

// 调整working set的大小.

NTOSKRNL NTSTATUS MmAdjustWorkingSetSize(
          DWORD MinimumWorkingSet OPTIONAL, // if both == -1
          DWORD MaximumWorkingSet OPTIONAL, // empty working set
          PVM Vm OPTIONAL);

//can file be truncated???
NTOSKRNL BOOLEAN MmCanFileBeTruncated(
          PSECTION_OBJECT_POINTERS SectionPointer, // see FILE_OBJECT
             PLARGE_INTEGER NewFileSize
          );

// create section. NtCreateSection call this function...

NTOSKRNL NTSTATUS MmCreateSection (
    OUT PVOID               *SectionObject,
    IN ACCESS_MASK          DesiredAccess,
    IN POBJECT_ATTRIBUTES   ObjectAttributes OPTIONAL,
    IN PLARGE_INTEGER       MaximumSize,
    IN ULONG                SectionPageProtection,//PAGE_XXXX
    IN ULONG                AllocationAttributes,//SEC_XXX
    IN HANDLE               FileHandle OPTIONAL,
    IN PFILE_OBJECT         File OPTIONAL
);

typedef enum _MMFLUSH_TYPE {
    MmFlushForDelete,
    MmFlushForWrite
} MMFLUSH_TYPE;

NTOSKRNL BOOLEAN MmFlushImageSection (
    IN PSECTION_OBJECT_POINTERS     SectionObjectPointer,
    IN MMFLUSH_TYPE                 FlushType
);

NTOSKRNL DWORD MmHighestUserAddress; // 一般为0x7ffeffff


NTOSKRNL BOOLEAN MmIsRecursiveIoFault();
//其代码
#define _MmIsRecursiveIoFault() ( /
    (PsGetCurrentThread()->DisablePageFaultClustering) | /
    (PsGetCurrentThread()->ForwardClusterOnly) /
)

NTOSKRNL POBJECT_TYPE MmSectionObjectType; //标准的Section对象

NTOSKRNL DWORD MmSystemRangeStart; //一般为0x80000000
NTOSKRNL DWORD MmUserProbeAddress; //一般为0x7fff0000

NTOSKRNL PVOID MmMapVideoDisplay( // для i386 враппер в MmMapIoSpace
       IN PHYSICAL_ADDRESS PhysicalAddress,
       IN ULONG NumberOfBytes,
       IN BOOLEAN CacheEnable
       );

NTOSKRNL VOID MmUnmapVideoDisplay ( // для i386 враппер в MmUnmapIoSpace
       IN PVOID BaseAddress,
       IN ULONG NumberOfBytes
       );

// 将frame的范围标记为更改并进行相应的操作
NTOSKRNL VOID MmSetAddressRangeModified(
     PVOID StartAddress,
     DWORD Length
     );

// 在NtMapViewOfSection中调用
typedef enum _SECTION_INHERIT {
 ViewShare=1;
 ViewUnmap=2;
 }SECTION_INHERIT;

NTOSKRNL NTSTATUS MmMapViewOfSection(
     PVOID pSection,
     PEPROCESS pProcess,
     OUT PVOID *BaseAddress,
     DWORD ZeroBits,
     DWORD CommitSize,
     OUT PLARGE_INTEGER SectionOffset OPTIONAL,
     OUT PDWORD ViewSize,
     SECTION_INHERIT InheritDisposition,
     DWORD AllocationType,
     DWORD ProtectionType
     );

NTOSKRNL NTSTATUS MmUnmapViewOfSection(
     PEPROCESS Process,
     PVOID Address
     );

PVOID MmLockPagableImageSection(
     PVOID AddressWithinImageSection // same entry as MmLockPagableDataSection
     );

// 减少StackLimit(堆栈增长)
NTSTATUS MmGrowKernelStack(
     PVOID CurESP //栈顶的地址
     );


                                   I talk to the wind
                              My words are all carried away
                                   I talk to the wind
                                  The wind does not hear
                                   The wind cannot hear.

                                            King Crimson'69 -I Talk to the Wind


0b.结语
=============

就到这里吧。如果综合的来看所有这些描述,对内存管理器多少可以得到一些概念。遗憾的是,这些东西还远不能称之为完整。内存管理器,大概是最复杂和最重要的内核组件,对其要进行完整的描述,我还得深挖不止十个八个的函数。但是主要的基本的东西我这里都写到了。对于进一步反汇编内核来说,这些应该是很有帮助的吧,谁知道呢... ;)

                         Best Regards, Peter Kosy aka Gloomy.
                                  Melancholy Coding '2001.

                            mailto:gl00my@mail.ru

P.S. 我知道我的“大作”不可避免的会有错误。我将非常高兴的听取批评和建议。


附录


0c.某些未公开的系统调用
==================================================

这里我描述了一些有用的Zw/Nt函数,这些函数可以在USER模式下或是驱动程序中调用(Zw类的)。几乎所有这些函数都来自于
Коберниченко的“Недокументированные возмождности Windows NT”一书。再加上Working   Set结构体的值,就可以描述用于NtQueryVirtualMemory的MEMORY_WORKING_SET_INFORMATION。

NTSYSAPI NTSTATUS NTAPI NtAllocateVirtualMemory(
               HANDLE Process,
                        OUT PVOID *BaseAddr,
                        DWORD ZeroBits,
                        OUT PDWORD RegionSize,
                        DWORD AllocationType,// MEM_RESERVE|MEM_COMMIT|MEM_TOP_DOWN
                        DWORD Protect); // PAGE_XXXX...

NTSYSAPI NTSTATUS NTAPI NtFreeVirtualMemory(
               HANDLE Process,
                        OUT PVOID* BaseAddr,
                        OUT PULONG RegionSize,
                        DWORD FreeType //MEM_DECOMMIT|MEM_RELEASE
               );

NTSYSAPI NTSTATUS NTAPI NtCreateSection(
               OUT PHANDLE Section,
               ACCESS_MASK DesirdAccess, //SECTION_MAP_XXX...
               POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
               PLARGE_IBTEGER MaximumSize OPTIONAL,
               DWORD SectionPageProtection, //PAGE_...
               DWORD AllocationAttributes, //SEC_XXX
               HANDLE FileHandle OPTIONAL // NULL - pagefile
               );

typedef enum _SECTION_INHERIT {
 ViewShare=1;
 ViewUnmap=2;
 }SECTION_INHERIT;

NTSYSAPI NTSTATUS NTAPI NtMapViewOfSection(
     HANDLE Section,
     HANDLE Process,
     OUT PVOID *BaseAddress,
     DWORD ZeroBits,
     DWORD CommitSize,
     OUT PLARGE_INTEGER SectionOffset OPTIONAL,
     OUT PDWORD ViewSize,
     SECTION_INHERIT InheritDisposition,
     DWORD AllocationType, //MEM_TOP_DOWN,MEM_LARGE_BAGE,MEM_AUTO_ALIGN=0x40000000
     DWORD ProtectionType // PAGE_...
     );

#define UNLOCK_TYPE_NON_PRIVILEGED 0x00000001L
#define UNLOCK_TYPE_PRIVILEGED          0x00000002L

NTSYSAPI NTSTATUS NTAPI NtLockVirtualMemory(
     IN HANDLE ProcessHandle,
     IN OUT PVOID *RegionAddress,
     IN OUT PULONG RegionSize,
     IN ULONG UnlockTypeRequired
     );

NTSYSAPI NTSTATUS NTAPI NtUnlockVirtualMemory(
     IN HANDLE ProcessHandle,
     IN OUT PVOID *RegionAddress,
     IN OUT PULONG RegionSize,
     IN ULONG UnlockTypeRequiested
     );

NTSYSAPI NTSTATUS NTAPI NtReadVirtualMemory(
     IN HANDLE ProcessHandle,
     IN PVOID StartAddress,
     OUT PVOID Buffer,
     IN ULONG BytesToRead,
     OUT PULONG BytesReaded OPTIONAL
     );

NTSYSAPI NTSTATUS NTAPI NtWriteVirtualMemory(
     IN HANDLE ProcessHandle,
     IN PVOID StartAddress,
     IN PVOID Buffer,
     IN ULONG BytesToWrite,
     OUT PULONG BytesWritten OPTIONAL
     );

NTSYSAPI NTSTATUS NTAPI NtProtectVirtualMemory(
     IN HANDLE ProcessHandle,
     IN OUT PVOID *RegionAddress,
     IN OUT PULONG RegionSize,
     IN ULONG DesiredProtection,
     OUT PULONG OldProtection
     );

NTSYSAPI NTSTATUS NTAPI NtFlushVirtualMemory(
     IN HANDLE ProcessHandle,
     IN PVOID* StartAddress,
     IN PULONG BytesToFlush,
     OUT PIO_STATUS_BLOCK StatusBlock
     );

typedef enum _MEMORYINFOCLASS {
     MemoryBasicInformation,
     MemoryWorkingSetInformation,

     // 还有class 2 - 这是VAD中的信息, 我目前还不完全了解VAD结构体,也就不能写出相应的INFO结构。

} MEMORYINFOCLASS;

typedef struct _MEMORY_BASIC_INFORMATION {
    PVOID BaseAddress;
    PVOID AllocationBase;
    ULONG AllocationProtect;
    ULONG RegionSize;
    ULONG State;
    ULONG Protect;
    ULONG Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

#define WSFRAMEINFO_SHARED_FRAME 0x100
#define WSFRAMEINFO_INTERNAL_USE 0x4
#define WSFRAMEINFO_UNKNOWN 0x3

typedef struct _MEMORY_WORKING_SET_INFORMATION {
     ULONG SizeOfWorkingSet;
     DWORD WsEntries[ANYSIZE_ARRAY]; // is Page VA | WSFRAMEINFO...


} MEMORY_ENTRY_INFORMATION, *PMEMORY_ENTRY_INFORMATION;

NTSYSAPI NTSTATUS NTAPI NtQueryVirtualMemory(
     IN HANDLE ProcessHandle,
     IN PVOID RegionAddress,
     IN MEMORYINFOCLASS MemoryInformationClass,
     IN PVOID VirtualMemoryInfo,
     IN ULONG Length,
     OUT PULONG ActualLength OPTIONAL
     );

0d.附注及代码分析草稿
==========================================

**** К MmCreateProcessAddressSpace ... ****
=============================================

__fastcall MiTotalCommitLimit(PVOID pProcess, DWORD NumOfPages); // edx:ecx
有statistic

dd MmTotalCommitLimit
dd MmTotalCommitedPages

如果NumOfPages+MmTotalCommitedPages不超过Limit - 一切OK,并只是简单的修正statistic.

否则开始线程间的协作。

选择time out值(如果请求>=10页,则为20秒),否则为-1秒。接着填充某个结构体,大概是这个样子:

typedef struct _REQUEST_FOR_COMMITED_MEMORY{
     LIST_ENTRY ListEntry;
     DWORD PagesToCommit;
     DWORD Result;
     KSEMAPHORE Semaphore;
           }_REQUEST_FOR_COMMITED_MEMORY;

这个结构体(或链表的元素)被插入到全局结构体中的全局链表ListOfRequest:

       [Pre List Item]<->[Our List Item]<->[ListOfRequest]

typedef struct _COMMIT_MEMORY_REQUEST_LIST{
     KSEMAPHORE CommitMemorySemaphore;
     LIST_ENTRY ListOfRequest;
          }COMMIT_MEMORY_REQUEST_LIST;

之后对CommitMemorySemaphore使用KeReleaseSemaphore并等待REQUEST_FOR_COMMITED_MEMORY中带有time out的信号量。

如果未超出time out并因此Result不为0,则再校验一次Limit并输出OK(如果limit有问题——则所有都重新开始)。如果结果为0,MiCouseOverCommitPopup。如果发生了time out,分析如下:

如果ListOfReques.Flink==&ListOfReques.Flink,也就是说所有的请求都在队列的尾部,则再一次等待信号量——并且已经没有time out了,因为不是我们的问题;)

如果ListOfReques.Flink==&RequestForCommitedMemory.ListEntry,就是说队列中的下一个是我们的请求(???)。则从队列中收回请求,因为
是从我们这里来的。

现在来看我们想看的几个页。如果>=10则MiCouseOverCommitPopup,否则MiChargeCommitmentCantExpand,之后输出。

所有的操作都需要cli sti,同时使用FastMutex(进程的10ch偏移),在进程创建时调用这个函数不会进行此操作。

现在,MiCouseOverCommitPopup(PagesNum,CommitTotalLimitDelta);又做些什么呢——如果我们想要页数大于128——则ExRaiseStatus(STATUS_COMMITMENT_LIMIT); 如果小于则IoRaiseInformationalHardError(STATUS_COMMITMENT_LIMIT,0,0);(这些函数都是公开的)。如果成功调用最后一个函数——则累加statistic:

MiOverCommitCallCount++;
MmTotalCommitLimit+=CommitTotalLimitDelta;
MmExtendedCommit+=CommitTotalLimitDelta;
MmTotalCommittedPages+=PagesNum;
且不修正 MmPeakCommintment;

如果不成功但MiOverCommitCallCount==0,所有都等于statistic,否则ExRaiseStatus(STATUS_COMMITMENT_LIMIT);

辅助函数:

DWORD NTOSKRNL RtlRandom(PDWORD Seed);

不奇怪,这个函数没有公开。该函数使用一个128个DWORD的表。在操作之后被此表和Seed被修正。可以看到,这给出了最大周期。

如果有两个event
MmAvailablePagesEventHigh 和
MmAvailablePagesEventHigh.

MiSectionInitialization:
MmDereferenceSegmentHeader: это структура описанная выша с добавленным
spinlock сверху.
创建线程MiDereferenceSegmentThread

PsChargePoolQuota(PVOID Process,DWORD Type(NP/P),DWORD Charge);

[TO DO] -->> MmInfoCounters!!!! 使用相应的NtQueryInfo...可以获得非常多有用的信息,ПОСМОТРЕТЬ!!!

---------------------------------------------------------------------------
                (c)Gloomy aka Peter Kosyh, Melancholy Coding'2001

                                                  http://gloomy.cjb.net
                                                  mailto:gl00my@mail.ru

                                                                董岩 译
                                           http://greatdong.blog.edu.cn

一:SSDT表的hook检测和恢复 ~!~~~ 二:IDT表的hook检测和恢复 ~~~~~~(idt多处理器的恢复没处理,自己机器是单核的,没得搞,不过多核的列举可以) 三:系统加载驱动模块的检测 通过使用一个全局hash表(以DRIVEROBJECT为对象)来使用以下的方法来存储得到的结果,最终显示出来 1.常规的ZwQuerySystemInformation来列举 2通过打开驱动对象目录来列举 3搜索内核空间匹配驱动的特征来列举(这个功能里面我自己的主机一运行就死机,别的机器都没事,手动设置热键来蓝屏都不行,没dump没法分析,哎,郁闷) 4从本驱动的Modulelist开始遍历来列举驱动 四:进程的列举和进程所加载的dll检测 采用以下方法来列举进程: 1ZwQuerySystemInformation参数SystemProcessesAndThreadsInformation来枚举 2进程EPROCESS 结构的Activelist遍历来枚举 3通过解析句柄表来枚举进程 4通过Handletablelisthead枚举进程 5进程创建时都会向csrss来注册,从这个进程里面句柄表来枚举进程 6通过自身进程的HANDLETABLE来枚举进程 7通过EPROCESS的SessionProcessLinks来枚举进程 8通过EPROCESS ---VM---WorkingSetExpansionLinks获取进程 9暴力搜索内存MmSystemRangeStart以上查找PROCESS对象 进程操作: 进程的唤醒和暂停通过获取PsSuspendProcess和PsResumeProcess来操作的 进程结束通过进程空间清0和插入apc。 采用以下方法查找DLL: 1遍历VAD来查找dll 2挂靠到对应的进程查找InLoadOrderLinks来枚举dll 3暴力搜索对应进程空间查找pe特征来枚举dll DLL的操作: Dll的卸载是通过MmUnmapViewOfSection和MmmapViewOfSection(从sdt表中相应函数搜索到的)来实现的(本来想直接清0 dll空间,有时行有时不行)(只要将这个进程的ntdll卸载了,进程就结束了,一个好的杀进程的办法撒,绿色环保无污染),注入dll使用的是插入apc实现的。(注入的dll必须是realse版的。Debug版会出现***错误,全局dll注入貌似也是)插入apc效果不是很好,要有线程有告警状态才执行。 五:线程信息的检测 遍历ThreadList来枚举线程 线程的暂停和唤醒都是通过反汇编获取PsResumeThread和PsSuspendThread直接从r3传来ETHREAD来操作的,通过插入APC来结束线程 六:shadow sdt表的hook检测与恢复 没有采用pdb来解决函数名问题,直接写入xp和03的shandow表函数名(主要是自己的网不稳定,连windbg有时都连不上微软) 七:系统所有的过滤驱动的检测 查看各device下是否挂接有驱动之类的,可直接卸载 八:系统常用回调历程的检测和清除 只检查了PsSetLoadImageNotifyRoutine PsSetCreateThreadNotifyRoutine PsSetCreateProcessNotifyRoutine CmRegisterCallback这几个,至于那个什么shutdown回调不知道是啥玩意,就没搞了,有知道的顺便告诉我下撒,谢谢 九:文件系统fat和ntfs的分发函数检测 直接反汇编fat和ntfs的DriverEntry得到对应的填充分发的偏移,然后和当前已经运行的文件系统的分发相比是否被hook,并附带恢复 十:文件查看功能 自己解析ntfs和fat的结构,来实现列举文件和直接写磁盘删除。附带有普通的删除和发生IRP来删除。不过这里面有点问题,ntfs删除有时把目录给搞坏了,大家凑合着吧, Ntfs网上删除这些操作的代码不多,就是sudami大大的利用ntfs-3g来实现的,看了下,太多了,充满了结构。然后自己对照着系统删除文件时目录的变化来自己实现的。只处理了$BITMAP对应的位清除,父目录的对应文件的索引项的覆盖,删除文件对应的filerecord清0. 另外偷懒时间都没处理,呵呵,y的,一个破时间都都搞好几个字节移来移去的。 十一:常用内核模块钩子的检测和恢复 这里只检测了主要的内核模块nkrnlpa**.exe的.win32k.sys,hal.dll,比对它们的原始文件从而查找eat inline hook,iat hook ,和inline hook。Inline是从TEXT段开始一段位置开始比较的。(有点慢貌似,等待显示扫描完成就好了) 十二:应用层进程所加载dll的钩子 应用层钩子检测和内核模块钩子检测原理一样,不过为了能读写别的进程的空间,并没有使用openprocess去打开进程,而是通过KiattachProcess挂靠到当前进程,然后通过在r0直接读写进程空间的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值