文章简介

    在之前的文章中,我们讨论了目前的rootkit开发技术,在这篇文章中,我们更进一步讨论并侧重于接下来的划时代的rootkit发展技术。然后再最后的这个系列的第三篇文章中将会讨论各种检测rootkit的方法和应对措施,他们可以被安全人员应用于安全软件中。

    在这篇文章中提到的技术在我们在2005年度的BlackHat会议上的概念证据软件Shadow Walker上对实现了,这些方法使得攻击者通过在硬件层次控制内存读写的方式将已知的或未知的恶意代码隐藏起来以防止被安全扫描软件发现成为可能。尽管我们聚焦于rootkit技术,低层实现就不披露了,因为这些技术可以被应用于从蠕虫到间谍软件等恶意代码。

持久的rootkit VS 基于内存的rootkit

      总的说来,有两种类型的rootkit:持久的rootkit和基于内存的rootkit。二者的首要的区别在于:在被感染的系统重启后对系统的持续性。持久的rootkit可以在系统重启后继续发挥作用而基于内存的就不行。为了能在系统重启后继续发挥作用,持久的rootkit必须满足两个条件。首先,它们必须能把自己的代码永久的存放在宿主系统中(例如硬盘)。其次。它们必须在系统的启动序列中放入钩子这样才能把它们自己的代码可以从磁盘载入到内存并得到执行。

    和持久的rootkit不同,基于内存的rootkit不需要把自己的代码永久的存储到磁盘上或在系统启动顺序上挂钩。它们的代码仅仅存在于那些被感染的内存中,并且是通过软件漏洞安装到系统中的。这个原因使得它们比那些持续的rootkit兄弟们更加隐蔽,并因此获得了防止自己被检测到的优势。尽管看起来它们在系统重启之后就消失的这一特性会限制它们的用处,但是服务器系统一旦启动起来就会持续数天,数周,甚至数月的时间在线。实际上,这种潜在的失去感染目标系统的机会会最攻击者在反跟踪方面得到补偿。

隐藏rootkit的行踪

    rootkit的编写者已经发展了很多种技巧来在入侵的系统中隐藏他们的rootkit的行踪。这些技术涉及从各种各样的钩子策略到直接内核对象操作的方方面面。但是,即使是最先进的内核rootkit,例如FU,都有一个内在的缺陷。尽管这些rootkit都擅长于控制执行路径,但是它们之中的大多数没有办法控制其他程序看到的内存视图。所以,这些rootkit为了防止自己被检测到必须解决两方面的问题:首先它们必须能够欺瞒自己的可执行代码的行踪,其次它们必须在操作系统组件前面瞒过自己对内存的修改(比如钩子)。如果他们不具备上述的能力,即使是最先进的公开的内核rootkit,对于那些很简单的基于内核签名检测的扫描工具而言,也是煮熟的鸭子同样的扫描工具在过去的20年中一直被反病毒软件所使用。持久的rootkit必须更进一步的处理如何在永久性存储介质上隐藏自身的可执行代码,并且要暗地里在系统的引导序列上装好钩子。在这篇文章中,我们会阐明前两个问题而略过第三个。从实际的角度讲,这把我们讨论的话题限制在了基于内存的rootkit。

    隐藏代码并瞒过内存上面做过的修改使我们想起了早期的病毒编写者试图在文件系统中隐藏它们的病毒代码时所面临的问题。病毒编写者通过编写变形和多态技术来应对基于文件系统签名的病毒扫描程序。多态病毒改变一组代码的外在表现但是在函数层面则保持了功能的等效性。举个简单的例子,我们可以使用英语的同义词(这些词拼写不同但几乎具有相同的含义),一个多态的病毒用不同的操作码替换了特定的几条指令,但是它们却完成同样的功能,就像用同义词替换对应的词一样。同样的道理,从表面上看变种病毒和变形钱的病毒是不同的,借此就会避开简单的基于模式匹配的检测。
    公开的rootkit几乎都没有做花很大力气去整合病毒多态技术。尽管多态技术可以使人rootkit的代码有效地避过基于签名的扫描程序的检测,但是这一技术在如何隐藏那些对已有的其他操作系统组件的二进制代码的所作的修改方面却很不理想。换句话说,被劫持的系统组件在基于内存完整性检测的扫描软件面前仍然很脆弱。所以,更好的解决办法是更改其他操作系统组件如何看待     rootkit的代码而不是费尽心思去隐藏它。在之后的几节中,我们会展示目前的操作系统架构允许我们颠覆虚拟内存管理,从而一个没有多态性的内核模式rootkit也能控制操作系统和其他程序对内存的读取。在第2小节中我们会复习操作系统对虚拟内存框架的支持。在第3 小节,我们会讨论那个叫做ShadowWalker的作为概念证据的具有欺瞒性的rootkit如何颠覆虚拟内存子系统以防止自己的代码被安全扫描程序发现。最后,在第4小节,我们会讨论不管是黑客还是安全人员如何使用这些技术。

2虚拟内存欺骗

    绝大多数的现代操作系统架构都会区分虚拟内存和物理内存。通常一个计算机系统拥有的虚拟内存比物理内存多的多。以一个只有256MB的RAM的32位的系统为例。这台主机我们拥有4GB的虚拟内存,尽管我们的物理内存只有256MB。简而言之,物理内存在物理上受限于RAM的大小,而虚拟内存则受限于处理器的地址总线的带宽。所以,32位带宽的处理器就会有2的32 此方,即4GB的虚拟内存。要是主机的处理器位64位,那么我们就会有2的64此方,即16EB的虚拟内存。
虚拟内存的实现有两种不同的实现方式,段式内存管理和页式内存管理。两种方式都被x86架构所支持。然而这篇文章重点关注页式内存分配,因为ShadowWalker软件颠覆了页式内存分配。分页的基本思想是无论是虚拟内存还是物理内存都被划分成固定大小的内存块,这种划分出的虚拟内存块被称为虚拟页面,它们被映射到被称为物理页面的物理内存块。页表和页目录保留了这些必要的用于把虚拟页面和物理页面链接起来的映射信息。一个关键点是当分页机制被启动之后,每次内存访问都必须要查询物理页面映射到那个虚拟页面以及设个物理页面是否位于贮存之中。这一过程会带来额外的性能开销,尤其是当计算机的框架建立在两级页表结构之上,这正是Intel的CPU所采用的方法。
    通过区分虚拟内存和物理内存,硬件和操作系统就能够提供这样一种假象:进程获得了比实际的物理内存多得多的内存。分页对于应用进程是透明的,从应用程序的角度看,它拥有4GB的可用的虚拟内存来实现自己的动能。它不必知道主机中实际装了多大的RAM,也不必过问虚拟地址如何映射到物理内存。既然虚拟地址空间远大于物理地址空间,那么一个进程获取大于物理内存大小的存储空间就成为可能。如果发生这种情况,操作系统需要临时的把某些数据从物理内存倒换到磁盘中,这样就可以为目前的内存请求腾出空间。具体的动作就是把这些页面复制出去并把对应的页表入口置为不在内存。当这些页面再度被访问时,它们可能不再主存中,这时会引发一个缺页中断,缺页中断会调用操作系统的缺页中断处理例程,这个例程会引发必要的IO请求将需要的页面从页文件中复制到内存。如果所有的可用物理页面满了,处理例程会在复制进请求的页面之前倒换其他的页面到磁盘中去。

    在两层分页机制中,一次内存访问按如下步骤进行:

    1查询页目录以确定是否这个地址的页表在主存中

    2如果不在,引发一个IO请求并将这个页表倒入主存

    3查询这个页表以确定请求的页面是否在主存中

    4如果不在,引发一个IO请求并将这个页面从磁盘倒入内存

    5查询相对于页首地址的偏移

    图片1表示了x86的地址转换过程。从以上的步骤可以看出,在最坏的情况下,一次内存访问会请求3次内存访问以及两次磁盘访问。硬件设计者开发了地址转换便查缓冲区来解决这个问题。当内存访问发生时,在查询页目录和页表之前首先会查询TLB来获得虚拟页面和物理页面的转换关系。如果转换关系存在,称为命中,否则,称为未命中。由于TLB的访问速度比查询页表快得多,内存访问通过TLB解决了大多数情况下的额外的性能开销。
 

3 ShadowWalker:怎么实现的

    ShadowWalker这个软件由两个驱动程序文件组成,一个由叫FU的rootkit修改而来的驱动和被用来隐藏FU内存挂钩驱动。由于ShadowWalker仅仅是作为一个概念证据的例子开发的,所以没有实现在内存中隐藏驱动、欺骗系统默认的页面管理例程和挂钩相关的中断。它的实现也有一些列的局限性,包括缺乏物理地址扩展和多核心处理器支持等。ShadowWalker并不是作为完整功能的或者可以可以实现某种危害的rootkit开发的,目前的实现的版本还不能避开很多种检测软件。但是,通过它可以管窥未来的rootkit技术。那些基于ShadowWalker的极具危害性的rootkit,蠕虫或者是间谍软件对于熟练地攻击者来说是举手之劳,而且被赋予了对抗针对恶意代码检测的技术。这确实是一个可怕的想法。这些方法使得攻击者隐藏已知的或未知的恶意代码以避开基于签名的扫描,启发式检测以及完整性校验等方法的检测称为可能,在下一段中,我们会讨论应用于ShadowWalker中的位于虚拟内存颠覆之后的技术的实现细节。

3.1背景知识
     尽管众所周知,内存访问的类型有3种(读,写,执行),但是事实上鲜为人知的是大多数的32位的x86架构的cpu只支持两种方式(读/执行和写/执行)。执行权限是暗含的,那意味着所有的内存都是可执行的,并且没有直接的将它标记为不可执行的cpu支持。这中有点巧合的架构一直是系统堆栈溢出检测的遗毒,因为没有其他容易的方式来来使堆栈称为不可执行的。但是不必烦恼,一些软件安全开发者发现了另一个这种架构的巧合,这个巧合使得在Unix系统下通过额外的软件支持来实现不可执行的内存称为巧合。这中实现技术就是广为人知的PaX。ShadowWalker利用了一些PaX小组的一些研究成果来在Windows系统下隐藏可执行代码。

    简要概括一下:为了使他么自己不被检测到,最新的rootkit修要解决两方面的问题:

    1 它们必须可以在内存签名扫描软件面前瞒过它们的可执行代码的行踪

    2 它们必须在被启发式检测软件(如像VICE的工具软件)和完整性检测软件用到的操作系统组件面前掩藏自己对内存的修改(例如,挂钩的函数)。

3.2ShadowWalker对虚拟内存的颠覆
    ShadowWalker必须在颠覆内存时解决3方面的问题。首先,它必须能区分和过滤对特定内存序列的执行和读权限(例如那些存放自己可执行代码的内存和被颠覆的操作系统的API函数所在的内存)。其次,当这些内存被检测到时它必须可以提供“伪造”的读权限。最后,它必须保证寄主系统的性能不受太大的负面影响。
    
    第一个问题通过标记那些要隐藏的页表入口为不再内存中,并且挂钩默认的分页处理历程,这保证了ShadowWalker将可以捕获对这些内存页的访问并在之后的缺页中断例程中过滤它们。在缺页中断处理例程中,我们可以访问存在堆栈中的指令指针和中断地址。如果指令指针和中断地址相同,我们可以认为下一步的操作会是执行,否则,将会是一个读/写操作。另外需要注意的一点是ShadowWalker需要区分挂钩的缺页中断处理例程和可以为操作系统服务的正常的缺页中断例程。目前,ShadowWalker通过保证所有的隐藏的页面位于未分页内存来解决这个问题。这不是一个缺陷,因为目前ShadowWalker被设计成只会隐藏那些通常位于未分页内存的驱动页面。
第二个问题,伪造读权限的问题,在某种意义上说,是耍了一个把戏,并且这个把戏不会降低系统性能。一旦缺页中断处理例程被调用了,我们检测到并证实它是一个对隐藏页面的读操作,对我们来说,修改PTE表并且把对应的物理页面改成捏造的的页面。这样,我们可以保证对我们要保护的页面的执行操作会映射到真实的页面,而对这些页面的读/写操作会转换到干净的(即为分配的)页面。记住一点,TLB通常是内存访问路径的第一个实体,执行内存操作会载入TLB并作相应的转换。奔腾处理器通常采用命中TLB机制,而ShadowWalker在利用了这一事实来迂回获得了好处。命中TLB机制意思是通常会有两个TLB,其中一个负责执行权限的转换(指令TLB),另一个负责读写访问的转换(数据TLB),正常情况下,两个TLB是同步的,并且维持着相同的映射信息。然而,使它们两个不同步也是有可能的,这样就会使它们维护者不同的虚拟页面-物理页面的映射信息。

    像ShadowWalker的rootkit可以使用这种不同步的策略来隐藏可执行代码。换句话说,它在TLB中装入映射到赶紧的拷贝的映射信息。TLB内容的载入的实现是由缺页中断处理例程在回应由对隐藏的页面的内存的访问引发的缺页中断实现的。图2阐述了这个概念。所有其他的缺页中断会传给操作系统的缺页中断处理例程来处理。
 
    一旦映射信息被装入TLB中,大多数的内存引用会通过TLB路径被处理,并且不会引发缺页中断。然而,缺页中断会在第一次访问那些页面,TLB缓存项倒换,上下文转换以及清空TLB倒换等情况下引发。这些引发的很少的中断不会造成任何问题并且仅仅会导致调用缺页中断处理例程来重新使两个TLB中的内容不同步。由于现代TLB中高命中率的保证,在缺页中断处理例程中相关的开销对性能的影响几乎微不足道。

4第二部分总结
    
    ShadowWalker提供了一个具有令人啼笑皆非但是在计算机安全领域普遍存在的阴阳相克的的例子。它把最初的防御方法应用到解决一个安全问题(基于PaX的缓冲区溢出保护),并且把它转化成具有攻击性的漏洞技术。最近的围绕索尼将rootkit技术应用到数字版权管理的争论是另外的显著地例子。正如大多数人所相信的,黑客与安全人员、漏洞利用和保护之间的界限并没有给出清晰地定义。

    在这个系列的第一篇文章中,我们展示了安全软件不嫩信任系统api的完整性。现在,我们展示了远比那个深入的对系统信任度的破坏。ShadowWalker向用户态的恶意软件扫描程序说了再见,并且将rootkit检测的策略带入了内核的国度。在第三部分即最后的部分,我们将会讨论rootkit的检测技术以及如何缓解这个问题的威胁。