老铁们,敲黑板划重点啦! 这玩意儿,简单说,就是Windows 10内核里管内存那块地儿,叫“池子”,最近来了个“新管家”(Segment Heap),看着挺唬人,其实还是留了点“后门”!这帮哥们儿(作者)贼精,发现了新管家的“小毛病”,研究出一套骚操作:用个针尖儿大的堆溢出(就是不小心多写了几个字节),就能一路火花带闪电,从“看大门的权限”(Low Integrity)直接窜到“玉皇大帝级别”(SYSTEM)!你说逗不逗?!
你想啊,皇上(内核)干活也得用纸笔吧?这“池子”(Pool)就是皇上专用的文具库。以前这库房规矩森严,跟外面老百姓用的(用户态堆)完全两码事,贼讲究。
但是!微软这大哥寻思:“哎呀妈呀,管两套库房太累了!” 于是乎,从Win10的19H1版本开始,就把外面那套比较溜的“分段管理法”(Segment Heap
)也搬进宫里了。想着省事儿呗?
可皇宫毕竟是皇宫,要求多啊!所以这套管理法到了内核里,也得稍微改改,加点“御用专供”的规矩。这文章就专门扒拉这“御用版”管理法(主要瞅x64),看看有啥能让人钻空子偷笔墨纸砚的地方。
以前想偷东西(攻击),主要打那个叫POOL_HEADER
的“户口本+门牌号”的主意。每个内存块前面都有这玩意儿,写着“这块地儿多大”、“谁家的”等等。后来微软学精了,加了各种防盗措施:比如搞了个叫ExpPoolQuotaCookie
的“祖传动态防伪码”,把户口本里一个重要信息(ProcessBilled
指针,记着是谁花的钱)给加密了,让你瞎改就露馅!还有NonPagedPoolNx
,相当于“不仅锁门窗,还把电闸给拉了”,不让你在里面点灯(执行代码)。
关键转折点,笑点来了:换了新管家后,虽然小块地(小于0xFE0字节)前面还挂着老式的“户口本”(POOL_HEADER
),但里面好多格子都空着,跟领导办公室似的,就差没落灰了! 这不就给咱这种“闲杂人等”可乘之机了嘛!
这新管家大体上还是按“大小个儿”分活儿:
而且还不止一个库房,按内存种类分了好几个(NonPagedPool
、PagedPool
、NonPagedPoolNx
等),每个都有个_SEGMENT_HEAP
结构体管着。
_HEAP_啥_CONTEXT
的内部构造图,反正挺复杂。)
还有个叫“动态旁路列表”(Dynamic Lookaside
)的小灶,专门给刚用完、大小合适的“盘子碗筷”(0x200到0xF80字节的块)留着,下次有人要,直接递过去,省得再去后厨洗了,快!
POOL_HEADER
(老户口本)现在混成啥样了?虽然新时代了,但小于0xFE0的小块地前面,还得挂着这老古董。但它现在:
PreviousSize
(隔壁老王家多大):一直是0,“隔壁没人,不用惦记!”PoolIndex
(第几号池塘):没用,“池塘都合并了,瞎写!”BlockSize
(自家地多大):还有点用! 主要为了扔进“小灶”(旁路列表)时“对号入座,别把砂锅当碗使”。PoolType
(户口性质+特殊标记):非常有用! 记录着你是“常住户口”(NonPaged)还是“暂住证”(Paged),能不能“房里点炮”(Nx)。还有俩关键标记:PoolQuota
(是不是得记账,找谁报销)和CacheAligned
(“爷就要住南北通透、采光好的单间!”——缓存对齐)。PoolTag
(谁申请的):还有用,方便查水表(调试)时知道是谁捣的鬼。ProcessBilled
(找谁报销):只有标记了PoolQuota
才有用,而且还被那个贼烦人的“防伪码”加密着,不好动。CacheAligned
)这档子事你要是申请时非要“住单间”(标记了CacheAligned
),系统一看,哎呀,现在这地址不凑巧,不是“门牌号带8”的吉利数(没对齐),咋办?
它可能就得在前面给你垫块砖(加个padding),甚至给你挂俩门牌号(两个POOL_HEADER
)! 第一个在最前面,第二个在对齐的那个“吉利”地址。只有第二个门牌号才写着“单间已对齐”,而且它里面的PreviousSize
(隔壁老王)那里偷偷记着:“我离真正的大门牌有多远”。
以前那户口本查得严,跟政审似的,不好下手。现在新管家来了,老户口本好多地方不看了,机会这不就来了嘛!
BlockSize
,“小捣蛋”变“大破坏”!你要是有个小溢出,能把隔壁邻居(下一个块)户口本上的BlockSize
(第3个字节)给偷偷改成个大胖子尺寸(比如改成大于0x200的值)。等这个邻居搬家(释放)时,系统一看:“嚯!这家伙是个大户啊!” 就把它扔进了“大户专用回收站”(对应大小的旁路列表)。
然后,你再去跟系统说:“给我来个大户型的房子!” 系统可能就把那个实际上“小身板儿、假户口”的邻居房给你了!你往里搬家具(写数据),以为地方大着呢,结果“咣当”一下,家具全怼到隔壁老老王家去了!(二次溢出,而且溢出范围可能大得多!) 把个只能捣乱3字节的小坏蛋,升级成能祸害将近4KB的大魔王!刺激不?
PoolType
里的CacheAligned
位!—— “指鹿为马,乾坤大挪移!”还记得要“住单间”时可能有俩门牌号,搬家(释放)时系统要用第二个门牌上的小纸条(PreviousSize
)算回大门牌地址这事儿不?
天大的BUG来了(当时是):在老的管理制度下,算回去之后,系统还得反复核对:“这纸条写的距离对不?俩门牌加起来大小对不?” 等等一堆检查。但换了新管家后,这些保安大哥估计集体去隔壁村打麻将了,岗哨全空了!检查都没了!(作者写文章时就是这样,可能后来补上了,但当时就是个大漏勺!)
这就好比啥?你改了人家的门牌号,人家系统还傻乎乎地信了!
咱的操作贼简单粗暴:
POOL_HEADER
)给改了:把PoolType
(第4字节)那里,强行打上“我要住单间!”(CacheAligned
)的标记。PreviousSize
(第1字节,就是那个记距离的小纸条)改成咱想要的一个数,比如0x15
。PoolType
里的PoolQuota
(记账)标记给打开了,不然那个加密狗(Cookie检查)会出来咬人,容易翻车。然后,坐等好戏!等“倒霉蛋B”搬家(释放)的时候:
PreviousSize
)。真正该拆的房子地址 = 倒霉蛋B的地址 - 小纸条上的数 * 16
来算。0x15
了,它一算,算到了 倒霉蛋B地址 - 0x150
这个十万八千里外的地方!结果就是:系统在错误的地方,拆了一栋根本不存在(或者不是它该拆)的房子! 这个“错误的地方”被咱精准地控制在“倒霉蛋B”前面,最多能偏0xFF0字节远!这简直是“指哪儿拆哪儿”的神技啊!
假设咱现在手里有个“小喷枪”(堆溢出漏洞),能把口水(数据)喷到隔壁邻居家的门牌号上(覆盖头几个字节,至少能改PoolType
和PreviousSize
)。目标:从“扫地僧”(Low Integrity)直接飞升成“天庭扛把子”(SYSTEM权限)!
核心战术:就用上面那个“指哪儿拆哪儿”的绝技,在咱能控制的“肇事者A”(有漏洞的块)家里,挖个坑,伪造一个“地下室”(Ghost Chunk),然后通过控制这个地下室里的“租客”(特殊对象),一步步拿到“透视眼+隔空取物”(任意地址读写或减法)的超能力,最后把自己的身份证(Token)改成皇帝的,大功告成!
具体步骤,笑点密集,跟上别掉队:
POOL_HEADER
)给糊上一层“修正液”:PoolType
改成“要单间!”,PreviousSize
改成咱算好的值(比如0x15
),让它指向A家客厅某个位置(比如A地址 + 0x30
)。
PipeAttribute
对象填满)。关键是在刚才算好的客厅位置(A地址 + 0x30
),偷偷放一个假的门牌号(Fake POOL_HEADER)。这假门牌得装得像:大小写个0x21
(这样拆迁队以为是0x210的房子,方便咱后面回收利用),户口性质跟A一样,但千万别写“要单间”和“要记账”,不然容易穿帮。
A地址 + 0x30
开始)拆了个0x210大小的“违章建筑”!这块被错误拆掉的地儿,就成了“地下幽灵”(Ghost Chunk),它神不知鬼不觉地覆盖了A和B的一部分。这块“三不管”的幽灵地皮会被扔进0x210大小的“待回收垃圾桶”(旁路列表)。
PipeAttribute
或PipeQueueEntry
)把它占了。现在,像审贼一样读里面的东西,就能知道这幽灵的确切地址,还能看到它压在下面的“秘密”(被覆盖的内核数据),里面说不定就有内核地址、甚至那个烦人的“防伪码”(ExpPoolQuotaCookie)!情报到手!芜湖~
PipeAttribute
能改数据指针),把幽灵地皮里那个“租客”(特殊对象)的关键“联系方式”(比如数据地址、大小指针)全都改成咱家的电话号码(指向用户态内存)! 以后内核再想找这个“租客”办事,实际上就得听咱用户态的遥控了!“报告长官,我们已经成功打入敌人内部!”
EPROCESS
结构地址),还有最重要的“身份证”(TOKEN)地址。跟开了全图挂似的!
PipeQueueEntry
对象),搭一个假的“县衙”(Fake EPROCESS)。再把“肇事者A”请出去、请回来。然后又把“幽灵地皮”弄回来。这次,在幽灵地皮的入口(A地址 + 0x30
)再换个新的假门牌,内容是:大小还是0x21
,户口性质这次要打上“需要查户口!”(PoolQuota)的标记,户主(ProcessBilled
)那里填上一个“加密电报”:假衙门地址 ^ 防伪码 ^ 幽灵地皮地址
。(用刚才偷来的情报算好)。
BlockSize * 0x10
,也就是0x210)。因为衙门是假的,它实际上就在咱指定的那个内存地址上,咣咣来了个“大额减法”! 咱就得到了在任意地方减任意数(0到0xFF0之间,16的倍数)的超能力!“乾坤大挪移第七层,成了!”
SeDebugPrivilege
)还不够,因为它身份证(Token)上有俩本本:一本叫Privileges.Present
(“理论上你能有啥本事”),一本叫Privileges.Enabled
(“实际上你现在开了哪些挂”)。默认情况下,咱这“青铜”号,Present
本本上就没写着能开“调试挂”。所以,得用两次“隔空打牛”:
Token->Privileges.Present
这个本本,咣咣一顿减,把“调试挂”对应的那个格子给“减”出来(变成1)。Token->Privileges.Enabled
这个本本,再咣咣来一下,把“调试挂”也给“减”开。AdjustTokenPrivileges
,跟系统说:“爷要开调试挂!” 系统一看,俩本本都许可了,没毛病!立马给你开了!有了这挂,整个系统都是你的后花园了! 找个倒霉的系统进程(比如winlogon.exe
),塞个小纸条(shellcode)进去,让它给你弹个SYSTEM权限的命令行窗口!砰!搞定!原地起飞,俯视众生!可以开始在系统里瞎溜达了! 🎉🎉🎉
上面老说用“私货”、“特殊对象”,到底是啥玩意儿能这么好使?得满足:咱能随便申请随便扔、大小能凑合(至少到0x210)、还得能帮咱偷看(任意读)甚至动手脚(任意写)。
PipeAttribute
(管道属性)。这玩意儿好比“管道装修材料”,通过一个叫NtFsControlFile
的咒语往管道上加就能造出来,尺寸、里面写啥字儿咱说了算。它里面记着装修材料放哪儿(AttributeValue
指针)和有多少(AttributeValueSize
)。咱要是能控制这个记录本,就能让它去读皇宫任何角落的秘密。PipeQueueEntry
(管道快递包裹)。往管道里寄快递(WriteFile
)就会产生这玩意儿。如果快递单上标记了特殊要求(isDataInKernel=1
),那包裹里的东西(数据)就不直接放这儿,而是放在另一个地方(IRP结构里的SystemBuffer
),包裹上只写着那个地方的地址(通过linkedIRP
指针)。咱要是能控制这个快递包裹,把地址改成咱家(用户态)的假地址,那快递员(内核)来取货时,就会跑到咱家来拿咱准备好的“假货”!也能实现偷看(任意读)。“堆喷”说白了就是“占坑技术哪家强?” 目的是让咱的“肇事者A”和“倒霉蛋B”能手拉手做邻居。具体咋占坑,得看A多大,掉进哪个“车间”(LFH还是VS)。
总之,“堆喷”就是个“又得有耐心,又得有点小运气”的活儿。
这篇破文章,絮絮叨叨地给咱揭了揭Win10 19H1之后,内核那嘎达内存池的“底裤”。虽然请了新管家(Segment Heap),但那个老掉牙的“户口本”(POOL_HEADER
)还在小户型前面赖着不走,而且好多规矩都忘了,以前的保安(检查)也撤了岗,这就给了咱这些“爱搞事”的一条“溜门撬锁”的新路子。
咱看到了咋利用这变化,特别是那个“指鹿为马还不用负责任”的“缓存对齐”漏洞(对齐混淆攻击),只需要一丢丢“手滑”(极小的堆溢出),就能像玩多米诺骨牌一样,一步步搞出“隔空打牛”(任意地址减法),最后把自己的平民身份证换成龙袍!(拿到SYSTEM权限)。
这招儿,对付那些看起来“人畜无害”的小漏洞,说不定就能“四两拨千斤,蚂蚁拱大象”!Windows安全这场“猫鼠游戏”,真是越来越刺激,越来越好玩(对攻击者来说)!