红队内核开发技巧

Kernel Mar 28, 2023

说明

本文想要分享一些对于红队比较有用的Windows驱动开发相关的内容

对于一些前置的开发知识,默认读者已经掌握,如果没有掌握的可以参考Pavel YosifovichWindows 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

当然还有前面提到的关闭DSE的方法,但现在公开的方法都受到PG的保护,所以这里不会过多介绍.还有一些其他的关于BYOVD的内容,以后有机会再分享.

提权

在Windows内核中,进程相关的结构体是_EPROCESS,该结构包含一个_TOKEN结构,描述进程的安全上下文并包含进程令牌权限,登录ID,会话ID,令牌类型等信息.
内核利用提权的方法之一是用高权限的令牌替换低权限的令牌:

  • 1.内核中找到低权限进程的_TOKEN地址
  • 2.内核中找到SYSTEM权限的_TOKEN地址
  • 3.使用SYSTEM权限的令牌替换低权限进程的令牌

例如下图中使用system进程(PID:4)的令牌替换掉powershell进程的令牌。

image-1

如果开启了双机调试,可以在windbg中手动操作:
首先使用命令:!process 0 0找到SYSTEM的PROCESS
如下图:
system
启动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})
    sys
  • 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})
    cmd
  • 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.
20230328102201
offset+0x40的位置为TOKEN权限的地方.
20230328102304
我们将目标进程的TOKEN权限修改成和System一样的
20230328102408
可以看到修改前和修改后使用whoami /priv的变化
20230328102503

进程保护

对于进程保护来说,通常有两种需求:

  • 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.

20230328112813
对于取消进程保护来说,最简单粗暴的方法就是将这些值全部置0.但是因为不同的系统版本,这里的offset是不同的.
20230328113203
我们可以使用硬编码的方法获取offset,如Win10 19044版本的offset是0x878,也可以使用内置函数PsGetProcessSignatureLevel来获取.
20230328113440
此处还有一个小技巧,对于内核中很多需要硬编码的值来说,每次通过双机调试dt来看会很麻烦.一种是通过geoffchappell来查询,但是该网站不是非常全.如果碰到没有的版本,还可以通过IDA查看.
使用IDA解析目标内核文件后,快捷键Shift+F1查看本地类型.
比如我们想看EPROCESS的结构
20230328114014
在类型上右键点击修改,就可以很方便的看到我们想要的offset了.
20230328114100
对于添加保护来说结果是类似的.通过设置相关的保护属性即可添加保护.这里的值是通过对比受保护进程得来的.
20230328114240
设置完保护后可以发现,任务管理器中进程无法被结束
20230328114623
进程黑客在不开启管理员权限时也是无法结束进程的
20230328114744
也可以看到保护是全部开启的状态.
20230328114909

摘除回调

熟悉Windows的可能会知道,Windows内核有很多通知机制,通过内核回调方法来监控进程,线程,文件,注册表等相关的操作.很多EDR或者杀软都会使用该方法进行安全监控.
那么了解该回调方法,并想办法阻止其响应是一个很有价值的操作.
这里只介绍进程回调,其他的都大同小异.
进程回调通过PsSetCreateProcessNotifyRoutine函数来创建.
20230328120656
查看IDA发现其内部调用PspSetCreateProcessNotifyRoutine,这是一个未文档化的函数.
20230328120750
该函数内部有一个PspCreateProcessNotifyRoutine数组,其中存放了所有的回调函数地址.
20230328120917
我们想要摘除目标模块的回调,那么首先要找到该数组,然后通过地址来获取是哪些模块,如果这些模块是安全产品相关的,那么就清除掉该回调.
对于PspSetCreateProcessNotifyRoutine函数的获取,可以通过PsSetCreateProcessNotifyRoutine内部查找call指令或者jmp指令来定位.经过分析发现,不同的系统版本,调用PspSetCreateProcessNotifyRoutine的方法是不同的,上面的图中显示是通过call指令来调用的,但是下面的图显示通过jmp指令来调用.
20230328121505
当我们获取到该内部函数后,即可通过同样的方法获取PspCreateProcessNotifyRoutine数组,经过分析发现该数组总是在lea指令后.我们定位到8d 2d的地方即可以获取到了.
20230328121947
定位到之后,我们可以遍历模块地址,判断数组中的地址在哪些模块的地址范围内,那么就认为该地址是对应模块的回调.
20230328122213
可以看到第四项是sysmon的模块.对于摘除来说,直接将目标的回调地址设为0即可.Windows内部会进行地址校验.
摘除掉之后查看sysmon的日志会发现已经没有进程创建相关的事件了.
20230328122853
没有摘除之前sysmon的日志
20230328123137

结尾

至此,在红队操作中,和Windows内核相关的一些很有用的技巧就介绍到这里.还有一些其他非常有意思的操作,后续有机会再分享.

标签