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

MS05-055漏洞分析


创建时间:2006-01-02
文章属性:原创
文章提交:SoBeIt (kinsephi_at_hotmail.com)

MS05-055漏洞分析

                                                                                    SoBeIt

    EEYE的公告中对这个漏洞已经描述得比较详细了。线程在退出的时候,PspExitThread会从ETHREAD.ApcState.ApcListHead[0]和ApcListHead[1]分离线程的APC队列,这样每个队列都会是一个被摘除链表头的循环双向链表。如果有一个APC是从配额池(Quota Pool)中分配的,则占用分配进程内核对象结构一个引用,PspExitThread在处理配额池中的APC时,若分配进程已终止且该配额池的引用是进程内核对象的最后一个引用,则会在调用ExFreePool释放该APC的过程进而调用PspProcessDelete销毁该进程对象。漏洞成因是在销毁进程对象过程中会调用KeStackAttachProcess和KeUnstackDetachProcess,这两个函数都会调用KiMoveApcState来分别保存和恢复APC链表,问题在于在第二次调用KiMoveApcState时,重新把前面已经被摘除的链表头接回到APC双向链表中,导致处理后续的APC队列时会发生ExFreePool(ETHREAD+0x30),ETHREAD是正在退出的线程的内核对象。

    引用EEYE的公告,发生漏洞时函数的调用顺序:

. PspExitThread
. . KeFlushQueueApc
. . (detaches APC queues from ETHREAD.ApcState.ApcListHead)
. . (APC free loop begins)
. . ExFreePool(1st_APC -- queued by exited_process)
. . . ExFreePoolWithTag(1st_APC)
. . . . ObfDereferenceObject(exited_process)
. . . . . ObpRemoveObjectRoutine
. . . . . . PspProcessDelete
. . . . . . . KeStackAttachProcess(exited_process)
. . . . . . . . KiAttachProcess
. . . . . . . . . KiMoveApcState(ETHREAD.ApcState --> duplicate)
. . . . . . . . . KiSwapProcess
. . . . . . . PspExitProcess(0)
. . . . . . . KeUnstackDetachProcess
. . . . . . . . KiMoveApcState(duplicate --> ETHREAD.ApcState)
. . . . . . . . KiSwapProcess
. . ExFreePool(2nd_APC)

    现在详细分析一下,在PspExitThread调用的KeFlushQueueApc中一段代码:

        RemoveEntryList(&Thread->ApcState.ApcListHead[ApcMode]);
        NextEntry = FirstEntry;

#define RemoveEntryList(Entry) {\
    PLIST_ENTRY _EX_Blink;\
    PLIST_ENTRY _EX_Flink;\
    _EX_Flink = (Entry)->Flink;\
    _EX_Blink = (Entry)->Blink;\
    _EX_Blink->Flink = _EX_Flink;\
    _EX_Flink->Blink = _EX_Blink;\
    }

    RemoveEntryList对以ApcListHead为链表头的双向链表进行摘除链表头处理,而原链表头指向其中第一项。

    问题代码在第二个KiMoveApcState,把备份的APC状态结构复制回原来的ETHREAD结构时:

    First = Source->ApcListHead[KernelMode].Flink;
        Last = Source->ApcListHead[KernelMode].Blink;
        Destination->ApcListHead[KernelMode].Flink = First;
        Destination->ApcListHead[KernelMode].Blink = Last;
**       First->Blink = &Destination->ApcListHead[KernelMode];
**        Last->Flink = &Destination->ApcListHead[KernelMode];

    **这2句是原因,把链表头重新接回双向链表。

    结果在后续循环释放链表中的APC结构时,就循环到了ETHREAD+0x3c(UserMode的APC),把它当成了KAPC+0xc(LIST_ENTRY)来处理,调用ExFreePool(Apc)时,这个"Apc"的地址也就是ETHREAD+0x30,实际POOL的头结构从ETHREAD+0x28 KernelStack开始。

    当获得KernelStack可以为合法的头结构时,将会把头结构中的ProcessBilled当成一个EPROCESS结构进行释放,这时这个"EPROCESS"结构为一用户态地址,一般是为0x200(因为State=2),因为这个地址在地址空间的第一个页,是不允许访问的,所以我们还要使ETHREAD结构中在State之后的一个成员不为0,也就是Alerted数组中某一项不为0。Alerted数组分别对应于记录该线程在用户态和内核态下是否以被提醒过,分别为一个字节大小。在用户态下使对应内核态的Alerted位为1不太可能,却能使对应用户态的Alerted位为1。在将目标线程暂停后,可以在该线程被插入APC之前先调用ZwAlertThread来使其调用KeAlertThread,可以使Alerted[UserMode]为1,原因参考下面代码,记得此时这个线程不能是Alertable状态的。这时对应于伪造的EPROCESS地址为0x1000200:

    这段代码是用于在KeAlertThread里置位Alerted:

if (Alerted == FALSE) {

        //
        // If the thread is currently in a Wait state, the Wait is alertable,
        // and the specified processor mode is less than or equal to the Wait
        // mode, then the thread is unwaited with a status of "alerted".
        //

        if ((Thread->State == Waiting) && (Thread->Alertable == TRUE) &&
            (AlertMode <= Thread->WaitMode)) {
            KiUnwaitThread(Thread, STATUS_ALERTED, ALERT_INCREMENT);

        } else {
            Thread->Alerted[AlertMode] = TRUE;
        }
    }


    我们可以在这个用户态地址0x1000200映射伪造的数据,之后在ObfDerefrenceObject处理中会减少EPROCESS对象头结构的引用,当引用数递减到0时就会销毁该结构,并会调用对象头结构(OBJECT_HEADER)中对象类型结构(OBJECT_TYPE)中的一些函数指针,我们将在这里获取控制权。

    利用难度一是在一般用户权限的用户态下如何获取内核堆栈的地址,二是如何使APC结构是从配额池(quato pool)中分配。后者通过调用ZwQueueApcThread即可,这时APC都是从配额池中分配:

    Apc = ExAllocatePoolWithQuotaTag(
                    (NonPagedPool | POOL_QUOTA_FAIL_INSTEAD_OF_RAISE),
                    sizeof(*Apc),
                    'pasP'
                    );

        if ( !Apc ) {
                st = STATUS_NO_MEMORY;
                }
            else {
                KeInitializeApc(
                    Apc,
                    &Thread->Tcb,
                    OriginalApcEnvironment,
                    PspQueueApcSpecialApc,
                    NULL,
                    (PKNORMAL_ROUTINE)ApcRoutine,
                    UserMode,
                    ApcArgument1
                    );


    至于怎么在用户态下获取内核态堆栈地址,我们知道GetThreadContext和SetThreadContext在内核态下都是通过插入APC到目标线程中来实现。但是在内核中的实现函数NtGetContextThread和NtSetContextThread在处理上有些问题,Apc和用于装载返回的环境信息的CONTEXT结构都是从堆栈中分配且未初始化为0,所以用于返回的CONTEXT中默认的数据都是内核堆栈中的数据。若未设置CONTEXT.ContextFlags中的某个标志,则不会覆盖该标志所对应的一些寄存器值,导致可以获取原来内核堆栈中数据。一般来说,内核堆栈中的数据是基本没有顺序的,但可以让内核先进行一系列比较长的代用,比如在用户态执行CreateThread,这时内核堆栈的数据基本都是有序的,而且肯定会包含内核栈帧。我们也就可以准确获取内核堆栈地址也就是ETHREAD.KernelStack的高24位,也就是可以控制伪造池头结构中的PoolType和PoolIndex项。要求是PoolType & 32(SESSION_POOL_MASK)和& 64(POOL_VERIFIER_MASK)都不为1,且& 8(POOL_QUOTA_MASK)为1,还有PoolType & 3不为0来决定是PagedPool或NonPagedPool。且PoolIndex必须置位0x80,来标志该pool是已分配的,不然会蓝屏。

    要触发漏洞必须在条件适合的目标线程(也就是ETHREAD.KernelStack满足上面提到的要求)插入至少2个APC,一个辅助进程的APC,一个无关APC。这就需要一个辅助进程的配合,一般我们自己启动这个进程,该进程向已暂停的目标线程调用ZwAlertThread置位Alerted,并调用ZwQueueApcThread插入APC,然后自己终止。目标线程在辅助进程终止后恢复运行,调用ZwQueueApcThread向自己插入APC,然后终止,漏洞就会触发。

    在触发漏洞前,可以看到APC链表头已被摘除:

03c                  ApcListHead[1]
+03c                  struct   _LIST_ENTRY *Flink =             814B0354
+040                  struct   _LIST_ENTRY *Blink =             814B5C14

kd> dd 814B0354 l 2
814b0354  814b5c14 814b5c14
kd> dd 814B5C14 l 2
814b5c14  814b0354 814b0354

    触发后,可以看到链表头已经被接上:

+148                  ApcListHead[1]
+148                  struct   _LIST_ENTRY *Flink =             814B0354
+14c                  struct   _LIST_ENTRY *Blink =             814B5C14

kd> dd 814B0354 l 2
814b0354  814b5c14 814ad05c
kd> dd 814b5c14 l 2
814b5c14  814ad05c 814b0354

    开始释放ETHREAD+0x30:

kd> p
nt!PspExitThread+0x4bc:
804fc09d e835f2f6ff       call    nt!ExFreePool (8046b2d7)
kd> dd esp l 1
be989c78  8157e370

    值得注意的是在ObDerefenceObject之前要调用PsRetrunPoolQuota,所以要在伪造的EPROCESS(在地址0x1000200处)+0x1b8处伪造一个EPROCESS_POOL_QUOTA结构。

    一个正常EPROCESS中的配额块结构应该如下,照着伪造就行

kd> !strct EPROCESS_QUOTA_BLOCK 8151f708
struct   _EPROCESS_QUOTA_BLOCK (sizeof=44)
+00 uint32   QuotaLock =                         00000000
+04 uint32   ReferenceCount =                    00000221
+08 uint32   QuotaPeakPoolUsage[2] =             0001f4e4 00078134        .... ...4
+10 uint32   QuotaPoolUsage[2] =                 0001e5e8 00073f64        .... ..?d
+18 uint32   QuotaPoolLimit[2] =                 00020000 00080000        .... ....
+20 uint32   PeakPagefileUsage =                 000005e9
+24 uint32   PagefileUsage =                     000005bb
+28 uint32   PagefileLimit =                     ffffffff

    EPROCESS结构的对象头结构开始与EPROCESS-0x18,为保证后面ObDerefenceObject正常进行,还要伪造对象头结构(OBJECT_HEADER)。对象头结构里要设置两个地方,一个是PointCount成员为1,来表明是对该内核对象的最后一个引用,还有设置对象类型结构(OBJECT_TYPE)指针,此外保持Flags成员为0可以保证最短的调用路径,减少所有不必要的麻烦。对象类型结构中设置DeleteRoutine为我们的提升权限的函数地址。

    在执行到提升权限的函数里,通过把SYSTEM进程的Token复制到任意目标进程的Token,目标进程可以获取最高权限。因为在用户态的那个EPROCESS结构是伪造的,所以任何与处理该EPROCESS结构的函数都是多余的,返回时都要跳过,通过设置ESP直接返回PspExitThread中释放APC的循环,并使其满足退出条件来退出这个释放循环,系统继续正常运行。

    值得注意的是,在2000中不同的SP在处理PspExitThread中退出APC释放循环的判断代码有些不一样,所以要实验几个版本,才能找到通用的方法。

    最后感谢Derek Soeder的帮助,以及Polymeta帮忙测试exploit。祝大家新年快乐,万事如意!

                            email:kinvis@hotmail.com