首页
社区
课程
招聘
[原创]Windows 下 KnownDlls 机制的 Section 对象劫持,实现系统 DLL 的加载接管(Ring0 / 内核层)· 上(下篇有空再写)
发表于: 2026-8-16 05:33 145

[原创]Windows 下 KnownDlls 机制的 Section 对象劫持,实现系统 DLL 的加载接管(Ring0 / 内核层)· 上(下篇有空再写)

2026-8-16 05:33
145

前言


记录一次对 KnownDlls 机制的内核层实践:通过驱动关闭系统预建的 Section 对象、重建一个同名的 Section 来接管指定系统 DLL 的加载。上篇把整条链路在普通进程上跑通;系统进程 / 受保护进程为什么即便设置了 WinTCB 信任级别仍然加载失败,留到下篇专门验证


一、先了解 KnownDlls 机制


KnownDlls 是 Windows NT(及更早的 9x)系统引入的一种缓存机制,最初目的是通过预加载常用系统 DLL 来提升应用程序的启动速度。同时它也具备安全意义:能够防止利用应用程序目录权限弱点、通过在程序目录植入木马化系统 DLL 发起的攻击(即 DLL 搜索顺序劫持的一种缓解)。


工作原理大致如下:


系统通过注册表预先定义高频使用的系统 DLL 清单。启动时读取 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\KnownDLLs 下的列表。

系统在启动早期,为这些 DLL 各自创建一个 SEC_IMAGE 类型的 Section 对象,放在对象管理器命名空间的 \KnownDlls\ 目录下。

当加载器在可执行文件的导入表中发现某个 DLL 时,会先到 \KnownDlls\ 下查找是否存在同名的现有 Section。如果存在,加载器直接用这个已存在的 Section 建立映射,省去了重新打开文件、创建 Section 的开销。


我个人对该机制的理解:把 Kernel32.dll、Ntdll.dll 这类常用系统文件当作 KnownDll,系统在启动时为它们各建一个 SEC_IMAGE 的 Section 对象放进 \KnownDlls\。之后进程加载这些 DLL 时,加载器先在 \KnownDlls\ 里找对应的 Section,找到就直接拿这个已存在的 Section 来映射,而不必每次都去读本地文件、重新建 Section。这样既提升了效率,也让关键系统 DLL 的来源相对固定、不易被目录层面的手段替换。


补充一点表述上的精确性:这些 Section 是内核对象(存在于内核的对象管理器命名空间里),但它映射出来的镜像数据,是在进程加载时映射进各个用户态进程的地址空间的,并不是常驻在内核地址空间。下文提到"从 Section 加载"都是这个意思。


下面主要讨论"能否篡改 / 接管"这部分


用 WinObjEx 可以看到,64 位的 Section 目录是 \KnownDlls,32 位的是 \KnownDlls32



二、思路:关掉原 Section,然后建一个同名的,让原本从系统的Section加载Kernel32.dll变成从我们自建的Section加载Kernel32.dll


既然每个KnownDll对应一个SEC_IMAGE的Section,那么可以尝试写一个内核驱动,把某个Section关掉,然后在\KnownDlls(或 \KnownDlls32)目录下创建一个同名Section从而实现 DLL 加载的中转/接管,下面以64 位的Kernel32.dll的Section为例


代码如图:

直接用 ZwClose 去关

跑一下,dbgview输出了 [Remove Object] Open Section: 0x0,0x0 即 STATUS_SUCCESS。回到 WinObjEx 看一下:

被骗了,根本没关掉,Kernel32.dll 的Section还好好的


原因在于:\KnownDlls里的Section是永久对象(相对永久——具体来说是相对"当前这次开机"而言,重启后系统会重建)


这里需要区分对象管理器里的两个计数:


句柄计数(handle count):有多少个打开的句柄指向该对象。ZwClose减的是这个

引用计数/指针计数(pointer count):内核里有多少处引用该对象


永久对象是用OBJ_PERMANENT属性创建的,它被创建时就带着一个额外的指针引用,因此即使句柄计数归零,指针计数也不会归零,对象不会被删除,所以上面ZwClose成功了,Section却纹丝不动——我减掉的只是句柄计数,永久属性顶着的那个引用还在


三、先降级为临时对象,再ZwClose


明确这点之后,思路就清楚了:先把它从永久对象降级成临时对象,再关,降级用 ZwMakeTemporaryObject


函数声明:


c

NTSYSAPI NTSTATUS ZwMakeTemporaryObject(

  [in] HANDLE Handle

);


微软的说明:


ZwMakeTemporaryObject 是通用例程,可对任意类型的对象操作。

如果对象是用 OBJ_PERMANENT 属性创建的,它就是永久对象;永久对象创建时引用计数为 1,因此驱动取消引用时不会被删除。

如果对象不是永久的,就是临时对象。ZwMakeTemporaryObject 会把指定对象转成临时对象;如果它已经是临时的,则什么都不做。

临时对象只有在句柄计数大于零时才有名称。当句柄计数归零,系统会删除对象名称,并相应调整指针计数。

用户态调用时应使用 NtMakeTemporaryObject。


也就是说,正确的顺序是:先 ZwMakeTemporaryObject 把它降级,再 ZwClose,试一下:

ZwMakeTemporaryObject 和 ZwClose 都返回 STATUS_SUCCESS,回到 WinObjEx:

没了,这次真关掉了


四、建一个同名 Section


接下来在该目录下创建一个名为 Kernel32.dll 的 Section,为了严谨,把原版 C:\Windows\System32\Kernel32.dll 复制到 C:\ 下,去掉它的数字签名,用这一份dll来创建,代码如下:

跑一下,Section 确实建出来了,C:\Kernel32.dll确实被占用了:

但此时打开任何 64 位进程都会报错、无法运行


这其实反而印证了接管是生效的:我们创建了Kernel32.dll的Section,任何 64 位进程加载 Kernel32.dll 时都会优先从我们创建这个Section映射,现在它们全都起不来,恰恰说明"优先从我的 Section 加载"这条链路通了,只是加载本身被拦下了,那么为什么从这个 Section 加载会失败呢


回到 WinObjEx 仔细对比原版和我建的这个:

差异很明显,就两点:


Process Trust Label(原版有,我们建的没有)

Security(ACL) 里我们建的这个是几乎空的,只有SYSTEM和管理员组...所以只要不是以管理员身份运行的程序都无法通过自建的Section加载Kernel32.dll,本质上其实就是没有SYSTEM或管理员组的权限就无法访问自建的Section


五、两类不同的拦截:ACL 与 Trust Label

Security / ACL 控制的是谁有权访问这个 Section 对象、以何种方式访问。我们新建的Section的ACL为空,等于进程连访问它的权限都没有,自然无法用它来加载 Kernel32.dll

Process Trust Label 是可信等级,原版 Kernel32.dll 的Section带着PPL-WinTCB这一级的trust label,因为它是签名的系统镜像;受保护进程(PPL 及以上)在加载时会检查来源 Section 的 trust label,等级不够就拒绝。


换句话说:ACL 卡的是普通进程有没有权限用这个Section,Trust Label卡的是受保护 / 系统进程认不认这个来源,我遇到的"所有 64 位进程都起不来",是这两类拦截叠加后的结果,但主要是ACL的问题


明确这一点后,具体分两步:


第一步,设置 ACL

第二步,  把 Process Trust Label 设置成原版那样的 PPL-WinTCB


单独写一个函数做这件事,这里图省事,直接把能给的访问权限无脑全给Everyone:

跑一下:

两处都对上了:既有 PPL-WinTCB,也有 Everyone 的访问权限,这时候打开普通程序已经正常,它们能从我接管的这个Section里加载Kernel32.dll了


六、上篇完结


到这里,上篇的目标基本达成,普通进程的加载接管已经跑通。这套做法的特性是:


一次动作,永久生效(相对当前这次开机而言),做完之后驱动可以直接卸载,后续启动的进程只要加载64 位Kernel32.dll,都会走 C:\ 动过手脚的那一份

DLL 文件本身叫什么名字无所谓,但Section的名字必须是 Kernel32.dll,这样在加载 Kernel32.dll 时才能在 \KnownDlls 里找到它

重启后失效,因为下次开机系统进程会重新创建这些Section

七、还没解决的:系统进程


诚实的标注当前状态:上面这套跑通的是普通进程,系统进程 / 受保护进程即便给Section设置了 WinTCB 信任级别,目前仍然加载不了


这一点还未实测验证,但方向上基本可以判断:系统进程在加载Kernel32.dll时,大概率会对映射进来的镜像本身做完整性校验,而我们用的是一份去掉了数字签名的Kernel32.dll,镜像签名对不上,走校验路径的加载就会被拒,因此可以明确,Trust Label 解决的是来源 Section可不可信,但解决不了这份镜像的字节签名合不合法,这是两码事;也就是说,系统进程通过Section加载dll时可能确实会对比Trust Label等级,但Trust Label并不能代表内存镜像是否可信,而系统进程是同时看两者


不过这只是现在的推断,所以有时间的话下篇我会重新走一遍完整流程


八、下篇的方向


结尾抛一个更进一步的想法,也是下篇要尝试的:既然这些DLL是以SEC_IMAGE类型被映射的,那么能不能绕过自建Section这条路,直接定位到已映射的镜像内存、在内存里改呢?

如果可以,那么这样做的好处是:既保留前面一次动作、卸载驱动仍生效的特性,又能做到无文件落地


当然,这条路可能会撞上新的障碍,受保护镜像的页面完整性保护、以及开启了 HVCI 时虚拟化层对镜像页的保护等等..


冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 2026-8-16 11:48 被mb_owcppmrg编辑 ,原因: 笔误
收藏
免费 2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回