首页
论坛
专栏
课程

[原创][05] HEVD 内核漏洞之未初始化栈变量

2019-7-30 12:54 1980

[原创][05] HEVD 内核漏洞之未初始化栈变量

2019-7-30 12:54
1980

0x00 前言

上一篇研究了一下基础的内核整数溢出漏洞,漏洞利用还有瑕疵,但是原理上还是比较清楚的,后续有时间继续搞一哈,这篇我们继续下一个内核漏洞:未初始化栈变量。

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

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

0x01 漏洞原理

顾名思义,即内核函数栈中局部变量未初始化。

分析

typedef struct _UNINITIALIZED_MEMORY_STACK
{
    ULONG Value;
    FunctionPointer Callback;
    ULONG Buffer[58];
} UNINITIALIZED_MEMORY_STACK, *PUNINITIALIZED_MEMORY_STACK;

NTSTATUS
TriggerUninitializedMemoryStack( _In_ PVOID UserBuffer)
{
    ULONG UserValue = 0;
    ULONG MagicValue = 0xBAD0B0B0;
    NTSTATUS Status = STATUS_SUCCESS;
#ifdef SECURE
    //安全版本
    UNINITIALIZED_MEMORY_STACK UninitializedMemory = { 0 };
#else
    //漏洞版本
    UNINITIALIZED_MEMORY_STACK UninitializedMemory;
#endif
    PAGED_CODE();
    __try
    {
        ProbeForRead(UserBuffer, sizeof(UNINITIALIZED_MEMORY_STACK), (ULONG)__alignof(UCHAR));

        // Get the value from user mode
        UserValue = *(PULONG)UserBuffer;

        DbgPrint("[+] UserValue: 0x%p\n", UserValue);
        DbgPrint("[+] UninitializedMemory Address: 0x%p\n", &UninitializedMemory);

        // Validate the magic value
        if (UserValue == MagicValue) 
		{
            UninitializedMemory.Value = UserValue;
            UninitializedMemory.Callback = &UninitializedMemoryStackObjectCallback;//自定义内核函数  无意义
        }

        DbgPrint("[+] UninitializedMemory.Value: 0x%p\n", UninitializedMemory.Value);
        DbgPrint("[+] UninitializedMemory.Callback: 0x%p\n", UninitializedMemory.Callback);

#ifndef SECURE
        DbgPrint("[+] Triggering Uninitialized Memory in Stack\n");
#endif
        // Call the callback function
        if (UninitializedMemory.Callback)
        {
            UninitializedMemory.Callback();
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

很明显可以看到,不安全的版本中,结构体变量UninitializedMemory未进行初始化。栈上的局部变量,未初始化时,则会拥有前调用函数的随机垃圾值。

如果我们传入一个正确MagicValue,它会填充变量UninitializedMemory以及回调成员。如果传递的值不正确那么就不会填充。后面看到Callback检查,但是并没有什么用。


驱动程序拖进IDA看下:




此时数组偏移下标为(0xB)*4


var_C变量为控制码0x222003,那么IO控制码为0x222003+(0xB)*4 = 0x22202f

找到漏洞函数,我们看到:



 

查看代码逻辑,比较成功会命中绿色块,在这里变量被填充了适当的值,此后在红色块中当回调函数被调用时并不会出错。
(我的调试过程断点总是出问题,借用 fuzzsecurity的调试过程):


这个值是不固定的,如果你尝试重现,很可能会在Windbg中看到不同的值。在虚拟机BSOD之前,让我们快速的看一下该变量距离当前栈起始位置有多远。


计算方式:0x8a15ced0 - 0x8a15c9cc = 0x504 (1284 bytes)

让我们通过回溯对BSOD进行检查。


可以看到,回调指针指向了一个垃圾地址,执行出错。

0x02 漏洞利用:

覆盖内核栈上的整型指针为我们shellcode的地址是最终目标。

参考j00ru的文章

其中提到了内核栈喷射(Kernel Stack-Spraying)的概念。

在Windows系统中,内核栈不同于用户栈,其为一片共用空间,使用常规的的喷射方法是有很大风险的,比如覆盖到其他关键的地址。而原作者提到的用NtMapUserPhysicalPages的方法,使我们在用户态完成对内核栈的操作。

NTSTATUS
 NtMapUserPhysicalPages (
   __in PVOID VirtualAddress,
   __in ULONG_PTR NumberOfPages,
   __in_ecount_opt(NumberOfPages) PULONG_PTR UserPfnArray
 )
(...)
  ULONG_PTR StackArray[COPY_STACK_SIZE];

//
// This local stack size definition is deliberately large as ISVs have told
// us they expect to typically do up to this amount.
//
#define COPY_STACK_SIZE             1024

未文档化的函数,NtMapUserPhysicalPages,我们不关心它用于干什么,但它的一部分功能是拷贝输入的字节到内核栈上的一个本地缓冲区。最大尺寸可以拷贝1024*IntPtr::Size(32位机器上是4字节=>4096字节)。

seebug的文章中可以看到NtMapUserPhysicalPages的实现:

NTSTATUS __stdcall NtMapUserPhysicalPages(PVOID BaseAddress, PULONG NumberOfPages, PULONG PageFrameNumbers)

  if ( (unsigned int)NumberOfPages > 0xFFFFF )

    return -1073741584;

  BaseAddressa = (unsigned int)BaseAddress & 0xFFFFF000;
  v33 = ((_DWORD)NumberOfPages << 12) + BaseAddressa - 1;
  if ( v33 <= BaseAddressa )
    return -1073741584;
  v4 = &P;//栈地址
  v39 = 0;
  v37 = &P;
  if ( PageFrameNumbers )
  {
    if ( !NumberOfPages )
      return 0;
    if ( (unsigned int)NumberOfPages > 0x400 )//如果要超过1024,就要扩展池,不过这里不用
    {
      v4 = (char *)ExAllocatePoolWithTag(0, 4 * (_DWORD)NumberOfPages, 0x77526D4Du);
      v37 = v4;
      if ( !v4 )
        return -1073741670;
    }
    v5 = MiCaptureUlongPtrArray((int)NumberOfPages, (unsigned int)PageFrameNumbers, v4);//v4 要拷贝的目标 内核栈  a2,要覆盖的EoPBuffer  长度是4*NumberOfPages

调用过程NtMapUserPhysicalPages -> MiCaptureUlongPtrArray -> memcpy。
int __fastcall MiCaptureUlongPtrArray(int a1, unsigned int a2, void *a3)//4*a1为长度   a2为构造的用户缓冲区 a3为内核地址
{
  size_t v3; // ecx@1
  v3 = 4 * a1; //长度
  if ( v3 )
  {
    if ( a2 & 3 )
      ExRaiseDatatypeMisalignment();
    if ( v3 + a2 > (unsigned int)MmUserProbeAddress || v3 + a2 < a2 )
      *(_BYTE *)MmUserProbeAddress = 0;
  }
  memcpy(a3, (const void *)a2, v3);
  return 0;
}

那么在用户态下构造好栈缓冲区,填充好payload地址。在HacksTeam的利用代码中,可以看到这个过程。

整理一下:

exp工作流将如下:(1)将shellcode放在内存任意位置,(2)使用指向shellcode的指针喷射内核栈,(3)触发未初始化变量漏洞 (4)调用执行。

测试一哈,成功提权。


0x03 漏洞反思

安全版本中提到的变量初始化的好习惯可以很好的帮助我们规避这类漏洞。

#ifdef SECURE
    //安全版本
    UNINITIALIZED_MEMORY_STACK UninitializedMemory = { 0 };
#else


[公告]安全服务和外包项目请将项目需求发到看雪企服平台:https://qifu.kanxue.com

最新回复 (4)
严启真 2019-8-3 07:32
2
0
受教了,楼主厉害啊
wjllz 3 2019-8-3 09:16
3
0
> [+] var_C变量为控制码0x222003,那么IO控制码为0x222003+(0xB)*4 = 0x22202f
这里的话, 其实HEVD是提供源码的. 你可以在用IDA打开他的`IrpDeviceIoCtlHandler`函数, 然后就可以直接查看IO控制码了, 可以省掉计算这一步.
> [+] 第二个方式是: 你可以看下触发不同类型的漏洞, 然后windbg看一下堆栈.. 应该能找到一个公用的函数. 在那里也能直接IO控制码.
>  [+] 第三个方式是, 分发函数是和pDeviceObject(这个结果体在DriverEntry里面可以看到) 相关的(这里我可能记错了..) 大概在偏移0x11(我以前调试过, 但是忘记了是32还是64, 32位的可能性比较大.)的地方, 这个方法k0师傅在他的某篇博客里面提到过的某篇论文有写, 但是我忘了具体的论文名字...(https://www.codeproject.com/Articles/Toby-Opferman#Article)
Saturn丶 3 2019-8-3 11:33
4
0
wjllz > [+] var_C变量为控制码0x222003,那么IO控制码为0x222003+(0xB)*4 = 0x22202f 这里的话, 其实HEVD是提供源码的. 你可以在用IDA打开他的`I ...
ok,感谢,学习下
Saturn丶 3 2019-8-3 11:34
5
0
严启真 受教了,楼主厉害啊
菜鸡入门,大佬轻踩
游客
登录 | 注册 方可回帖
返回