红队内核开发技巧
说明
本文想要分享一些对于红队比较有用的Windows驱动开发相关的内容
对于一些前置的开发知识,默认读者已经掌握,如果没有掌握的可以参考Pavel Yosifovich
的Windows Kernel Programming
一书的内容
开始之前首先要强调一下关于驱动开发的风险:
- 驱动签名
- 默认情况下,在Windows Vista x64及之后的系统版本,加载驱动程序都必须具有有效的数字签名.
- Windows 10 1607之前,微软信任第三方对驱动进行签名,这些第三方可能没有进行尽责的代码审计,所以很难确保所签名的驱动不是恶意的.
- 目前所有驱动程序都必须通过微软的审查来进行签名.但是旧的签名证书(2015年7月29日之前颁发的)仍然是有效的.
- 此外,即使签名过期,签名的驱动依然可以被加载.
- 少数第三方签名密钥已经被泄露.
- 第三方不能签署EV签名,Windows Server2019引入了仅EV模式,只能加载微软签名和EV签名的驱动程序.
- 32位系统不强制执行KMCS.
- Windows10最新版本和Windows11带有易受攻击的驱动列表,可以防止已知的易受攻击的驱动程序加载到内核中.
- Windows10之前可以使用CI!CiInitialize方法来关闭DSE,但是该方法受KPP监视,可能会造成BSOD.
- Windows11开始,CI.dll使用KDP(Kernel Data Protection)保护,但该保护需要VBS(基于虚拟化的保护)环境.飞塔发布了一篇文章可以绕过该保护.
- 蓝屏
- 与应用程序开发不同的时,驱动程序运行在内核模式下,当它们出错时,操作系统可能无法处理,从而导致损坏系统的行为发生导致系统宕机,也就是常说的蓝屏(Blue Screen Of Death,BSOD)问题.
- 所以在内核开发时需要经过完全的严谨的测试,才能在红队操作中不至于使客户系统崩溃.
- KPP
- Kernel Patch Protection(KPP)又叫Patch Guard(PG),是微软2005年在x64版本的操作系统上推出的一项保护措施,可以防止对内核代码进行修补(patch)操作.
- 它的工作原理是定期检查其所保护的区域是否被修改,如果检测到修改则触发BSOD.
- 从Windows 8和Windows 8.1开始,ntoskrnl.exe中的回调结构和CI!g_CiOptions变量受到PG保护.
步入正题
对于红队操作来说,我们使用驱动模块可能有几个目标:
- 提权
- 保护进程或去掉进程保护
- 摘除回调
- MiniFilter
- _FLT_FILTER
- _FLT_VOLUME
- _FLT_INSTANCE
- nt!PspNotifyEnableMask
- nt!PspLoadImageNotifyRoutine
- nt!PspCreateThreadNotifyRoutine
- nt!PspCreateProcessNotifyRoutine
- nt!CallbackListHead
- nt!ObTypeIndexTable / nt!ObGetObjectType
- NETIO!gwfpGlobal WfpProcessFlowDelete
- EtwRegister
- nt!EtwpHostSiloState
- MiniFilter
当然还有前面提到的关闭DSE的方法,但现在公开的方法都受到PG的保护,所以这里不会过多介绍.还有一些其他的关于BYOVD的内容,以后有机会再分享.
提权
在Windows内核中,进程相关的结构体是_EPROCESS,该结构包含一个_TOKEN结构,描述进程的安全上下文并包含进程令牌权限,登录ID,会话ID,令牌类型等信息.
内核利用提权的方法之一是用高权限的令牌替换低权限的令牌:
- 1.内核中找到低权限进程的_TOKEN地址
- 2.内核中找到SYSTEM权限的_TOKEN地址
- 3.使用SYSTEM权限的令牌替换低权限进程的令牌
例如下图中使用system进程(PID:4)的令牌替换掉powershell进程的令牌。
如果开启了双机调试,可以在windbg中手动操作:
首先使用命令:!process 0 0
找到SYSTEM的PROCESS
如下图:
启动powershell
并执行$pid命令获取到当前PID,例如为2648(0xa58)
使用!process a58 0
找到Powershell的PROCESS
使用dt nt!_EPROCESS ffffc50259224680
,来查看System进程的token成员,在1903系统上位于offset 0x358
+0x358 Token: _EX_FAST_REF
注意:0x358指向的是_EX_FAST_REF指针,并不是实际的_TOKEN结构
RefCnt表示引用的次数,可以直接把EX FAST REF指针的最后一位清零即可得到TOKEN的地址
或者使用!process addr 1
可以查看到token的地址
使用!token (EX FAST REF & 0xFFFFFFF0)
查看TOKEN
使用eq命令将System的TOKEN替换即可完成提权,这也是很多内核漏洞利用的提权方法.
另一种简单的方法是使用Windbg的对象模型,不过只支持在Windbg Preview中使用:
- 1.查找system的token
dx -g @$cursession.Processes.Where(p => [p.Name](http://p.Name) == "System").Select(p => new { Name = [p.Name](http://p.Name), EPROCESS = &p.KernelObject, Token = p.KernelObject.Token.Object})
- 2.查找cmd的token
dx -g @$cursession.Processes.Where(p => p.Name == "cmd.exe").Select(p => new { Name = p.Name, EPROCESS = &p.KernelObject, Token = p.KernelObject.Token.Object})
- 3.使用ep来修改
ep 0xffffa50d0f759080+0x358 0xffff8e8584806874
此时cmd即为system权限
内核利用提权的第二种方法是修改TOKEN的权限:
- Token有一个成员Privileges位于offset 0x40,定义为SEP_TOKEN_PRIVILEGES结构,可以使用
whoami /priv
命令查看,确认哪些权限被启用,哪些被禁用 - 可以通过查看System进程的权限来修改目标进程的权限或直接将该结构设置为全F
内核中使用PsLookupProcessByProcessId
来获取目标PID的EPROCESS,然后使用PsReferencePrimaryToken
获取目标TOKEN,TOKEN+offset 0x40的地方是进程权限的地方.
如下图中0xffff90885764c7f0
是System进程的TOKEN.
offset+0x40的位置为TOKEN权限的地方.
我们将目标进程的TOKEN权限修改成和System一样的
可以看到修改前和修改后使用whoami /priv
的变化
进程保护
对于进程保护来说,通常有两种需求:
- 1.设置自己的进程保护,从而无法被结束掉
- 2.取消目标进程的保护,如PPL下的Lsass.exe
从Windows8.1开始,有了PPL和signer types的概念.主要的一个结构体如下:
typedef struct _PS_PROTECTION {
union {
UCHAR Level;
struct {
UCHAR Type : 3;
UCHAR Audit : 1; // Reserved
UCHAR Signer : 4;
};
};
} PS_PROTECTION, *PPS_PROTECTION;
当在LSASS应用时,即使是SYSTEM也无法从该进程中导出凭证.
保护进程有两种类型,分别为Protected Process(PP)和 Protected Process Light(PPL),还有一个Signer,用于数字签名相关的属性.
进程的保护属性存储在EPROCESS中.
有三个字段,SignatureLevel
,SectionSignatureLevel
,Protection
.
对于取消进程保护来说,最简单粗暴的方法就是将这些值全部置0.但是因为不同的系统版本,这里的offset是不同的.
我们可以使用硬编码的方法获取offset,如Win10 19044版本的offset是0x878,也可以使用内置函数PsGetProcessSignatureLevel
来获取.
此处还有一个小技巧,对于内核中很多需要硬编码的值来说,每次通过双机调试dt来看会很麻烦.一种是通过geoffchappell来查询,但是该网站不是非常全.如果碰到没有的版本,还可以通过IDA查看.
使用IDA解析目标内核文件后,快捷键Shift+F1查看本地类型.
比如我们想看EPROCESS的结构
在类型上右键点击修改,就可以很方便的看到我们想要的offset了.
对于添加保护来说结果是类似的.通过设置相关的保护属性即可添加保护.这里的值是通过对比受保护进程得来的.
设置完保护后可以发现,任务管理器中进程无法被结束
进程黑客在不开启管理员权限时也是无法结束进程的
也可以看到保护是全部开启的状态.
摘除回调
熟悉Windows的可能会知道,Windows内核有很多通知机制,通过内核回调方法来监控进程,线程,文件,注册表等相关的操作.很多EDR或者杀软都会使用该方法进行安全监控.
那么了解该回调方法,并想办法阻止其响应是一个很有价值的操作.
这里只介绍进程回调,其他的都大同小异.
进程回调通过PsSetCreateProcessNotifyRoutine
函数来创建.
查看IDA发现其内部调用PspSetCreateProcessNotifyRoutine
,这是一个未文档化的函数.
该函数内部有一个PspCreateProcessNotifyRoutine
数组,其中存放了所有的回调函数地址.
我们想要摘除目标模块的回调,那么首先要找到该数组,然后通过地址来获取是哪些模块,如果这些模块是安全产品相关的,那么就清除掉该回调.
对于PspSetCreateProcessNotifyRoutine
函数的获取,可以通过PsSetCreateProcessNotifyRoutine
内部查找call
指令或者jmp
指令来定位.经过分析发现,不同的系统版本,调用PspSetCreateProcessNotifyRoutine
的方法是不同的,上面的图中显示是通过call
指令来调用的,但是下面的图显示通过jmp
指令来调用.
当我们获取到该内部函数后,即可通过同样的方法获取PspCreateProcessNotifyRoutine
数组,经过分析发现该数组总是在lea
指令后.我们定位到8d 2d
的地方即可以获取到了.
定位到之后,我们可以遍历模块地址,判断数组中的地址在哪些模块的地址范围内,那么就认为该地址是对应模块的回调.
可以看到第四项是sysmon
的模块.对于摘除来说,直接将目标的回调地址设为0即可.Windows内部会进行地址校验.
摘除掉之后查看sysmon
的日志会发现已经没有进程创建相关的事件了.
没有摘除之前sysmon
的日志
结尾
至此,在红队操作中,和Windows内核相关的一些很有用的技巧就介绍到这里.还有一些其他非常有意思的操作,后续有机会再分享.