对抗临近 | 红队大佬的私人秘籍:EDR绕过技术曝光!

文摘   科技   2023-06-21 08:00   广东  

一、背景

    大家好,我是顺丰安全研究员观沧海,我一直致力于探索最新的威胁和攻击技术,以确保公司可以提前感知和抵御国内外最新出现的安全威胁。最近,在我进行的一项关于EDR绕过技术的深入研究中,偶然间发现了一个令人担忧的事实:EDR作为安全的最后一道防线,也存在着被绕过的风险!

    这个发现让我倍感震惊,因为EDR系统一直被视为网络安全防御的最后一道壁垒。然而,现实却狠狠地给了我一巴掌,我亲自验证了如何利用该绕过技术绕过EDR设备的检测。

    在本篇分享中,我将分享自己通过一系列实验和深入分析研究出来的成果,并深入探讨这些EDR绕过技术的工作原理和防御方式。通过揭示EDR绕过技术,我希望能让大家认识到EDR绕过的风险,引起更多人对EDR绕过问题的重视,并为该问题提供有针对性的解决方案。

二、前言   

    这篇文章首先会介绍用户态/内核态和PPL等概念,让各位读者对Windows相关机制有一个基础的了解。再通过拆解EDR的功能组件,来讲述EDR的工作原理和防护能力来源。

    在对EDR的防护能力和运行机制有一个较整体的了解后,再详细分析如何利用驱动杀掉EDR(BYOVD攻击)。最后,文章的结尾会给出此类攻击的防护建议。

三、正文

(1)相关概念介绍

    0x01 用户态和内核态

    在Windows32位系统上,Microsoft并没有区分用户态(User Land)和内核态(Kernel Land),程序可以直接操作内核。为了保护系统的安全和稳定性,从Windows 64位起,Windows机器有两种不同的运行模式:用户模式和内核模式,用户模式即我们平时使用各种应用程序时所处的交互模式,应用可以访问CPU和内存等资源,维持自身的正常运转。内核模式则只有操作系统代码、设备驱动和系统服务可以使用。自内核空间隔离推出后,安全厂商无法再随意hook内核来实现监控和防护,必须使用Windows提供的各类安全接口来访问。为了解决被踢出内核而导致监控能力不足的问题,目前已经有许多安全厂商在内核中安装了自己的驱动,比如CrowdStrike、卡巴斯基、McAfee等。

    0x02 PPL进程

    Windows Vista最先引入的是Protected Process的概念,即受保护进程。从Windows 8.1开始,PPL(Protected Process Light)开始启用。最初其实是用于保护媒体的版权,既让媒体播放器可以读取文件,又能防止随意复制盗版其内容。对于PPL进程,我们需要搞清楚以下概念:

  1. PP进程可以全权访问同权限或更低权限的PP进程或PPL进程,如都是系统权限的PP进程可以彼此访问对方,而管理员权限的PP进程则无法访问系统权限的PP进程
  2. PPL进程可以全权访问同权限或更低权限的PPL进程
  3. PPL进程无法全权访问PP进程
    正常情况下,我们在用户态下运行的只是普通进程,即使提权到SYSTEM权限,依然会被拒绝访问。

    0x03 回调函数

    EDR为了保证自己的防护效果,通过Windows提供的接口,在内核注册了若干回调函数(callback)。这些回调函数通常用来监控进/线程创建销毁、文件创建删除、网络连接和注册表相关操作。例如,内核里有一个叫PspCreateProcessNotifyRoutine的数组,该数组通过指针的形式(也就是将回调函数的内存地址放在数组里)最多可保存64个回调函数。每当一个进程被创建或销毁,就会遍历这个数组,找到对应回调函数的位置,依次调用,从而通知EDR有进程创建或销毁。类似的文件、网络和注册表等也是通过这一过程实现,由此EDR便具备了核心的监控能力。具体实现步骤会在下文详细分析。

    0x04 服务重启机制

    Windows会为某些服务设置启动失败后的重试机制,以确保服务的正常持续运行。Windows服务控制管理器(Service Control Manager)会为每个服务定义一个重试间隔时间和最大重试次数,当SCM反复拉起达到最大重试次数后,SCM将不再尝试重启该服务。例如Win10_1709及更高版本中,Windows Defender的默认最大重启次数为5次。为了演示,上文仅使用了Windows的防护进程,而实际对抗过程中,EDR一般还会存在一个类似"xxSrv.exe"的进程。当我们反复杀掉这个进程后,服务进程不再重启,此时再杀掉防护进程,就可以在窗口期内自由操作,争取到一段冲锋的时间。

(2)EDR的监控响应能力如何而来

    0x01 EDR的功能组件

    EDR(终端检测与响应)实现防护的组件主要有以下几部分:
  1. 终端防护进程,负责扫描和识别是否为恶意文件,如MsMpEng.exe;

  2. 终端服务进程,负责启动和停止EDR相关服务、进程,如MsMpSvc.exe;

  3. 注册表,保证EDR相关进程在系统开机后立即启动,并初始化为PPL进程;

  4. 内核驱动,通过回调函数,监测来自内核的各种事件,如进程创建、内存分配等等;

    从以上描述可以看出,防护进程、服务进程和注册表,主要完成用户态层面的工作;内核驱动则主要负责处理Windows提供的各项内核回调函数,实现深度监控。

    接下来我们来详细看看各个组件的情况。

    0x02 防护进程与服务进程

    当一个文件落地或被运行时,防护进程会动静态结合的扫描该文件,因此EDR的直接防护能力主要来自于该进程。如果我们可以杀掉该进程,是否意味着EDR会丧失一部分的防护能力。但你会发现,哪怕你已经获取到了系统权限,你依然无法干掉它。
    为什么会出现这种情况?我们看一下PPL进程列表也许会得到答案。

    标识为Antimalware(反恶意软件)的3676号进程赫然在列。所以,至少你需要用系统级别的PPL进程才能杀掉它。而当你真的将防护进程杀掉后,再次执行tasklist,你会发现它又静静的陈列在那,仿佛根本没有消失过。事实上,当终端服务进程检测到防护进程下线时,会立马再次拉起防护进程。因此,EDR的服务进程理所当然的成为了众矢之的。那么我们是否可以用同样的办法杀掉服务进程后,再来杀防护进程呢?

    答案是否定的。自Windows 8.1起,微软引入了保护服务的概念。简单来说,就是通过内核驱动(Early Launch Antimalware driver)启动的服务进程,将无法被暂停、停止或着禁用。所以如果只是停留在用户态下,不管获取何种权限,都无法杀掉服务进程。

    0x03 注册表

    EDR的注册表设置了相关的进程和服务以何种方式启动,如下图所示,假如我们能将Start项的值从0x2(开机后立即自动加载)改成0x4(禁用,禁止启动),那就能在重启之后禁用掉EDR。
    但事实情况却没这么简单,篡改注册表的行为会被内核驱动里的回调函数捕获到,从而立即发起拦截。

    0x04 内核驱动

    微软在保证系统运转稳定和让安全厂商肆意发挥之间找到了一个中间值,禁止随意hook内核的同时又为EDR厂商提供了一系列API用于监测从内核发出的事件,这一机制叫Kernel Callback Objects(内核回调对象)。它允许安全厂商注册自己的回调函数(Callback Routine),来监视文件读写、内存读写、进程创建等各类操作。例如常用的回调函数及对应的功能有下面这些:
  1. PsSetCreateProcessNotifyRoutine:注册监测新进程的回调函数

  2. PsSetLoadImageNotifyRoutine:注册监测新模块加载、文件映射的回调函数

  3. PsSetCreateThreadNotifyRoutine:注册监测新线程的创建销毁

  4. CmRegisterCallbackEx:监测注册表的创建、修改或删除操作

  5. FltRegisterFilter:监测文件访问、创建或删除操作

    这就解释了为什么我们哪怕获取到管理员权限,依然无法更改EDR的注册表。同时,也揭示了为什么红队队员通过CS执行命令会被发现,因为EDR通过内核回调监测到你调用了cmd.exe 并执行了诸如ipconfig/whoami之类的敏感命令。

(3)用魔法打败魔法

    想要击败守在内核的EDR,你就也得从内核出发。别急,这并不难,并不是挂上了驱动的名号就代表它很复杂。无需恐慌,让我带你一步步见识魔法,学会魔法,使用魔法。

    0x01 实现原理

    如前面提到的,PsCreateProcessNotifyRoutine数组中存储了若干进程回调函数的地址,EDR注册的回调函数也存放在其中。当进程创建时,内核会根据数组里存放的地址调用对应的监控函数。那么,如果我们将这个数组中的EDR函数抹除,是否就意味着内核无法意识到EDR的回调函数需要被调用,EDR也就丧失了这一块的监控能力?

    为了验证这一概念,让我们先用WinDBG看一下PsSetCreateProcessNotifyRoutine函数在内核中的内存内容:

    我们可以看到PspSetCreateProcessNotify函数中以LEA r13 nt!PspCreateProcessNotifyRoutine的方式,获取了进程回调存储数组的相对偏移地址,对了简便我们称呼它为X。如果我们能够获取到X,就可以在内核中找到回调存储数组的具体位置。

    那么问题来了,如何找到PspSetCreatProcessNotifyRoutine的地址呢?先看一段代码:
DWORD64 Findkrnlbase() {    DWORD cbNeeded = 0;    LPVOID drivers[1024];
if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded)) { // 获取驱动列表 return (DWORD64)drivers[0]; // 驱动列表第一个即内核基址,也就是内核起始地址 }
return NULL;}
DWORD64 GetFunctionAddress(LPCSTR function) {
DWORD64 Ntoskrnlbaseaddress = Findkrnlbase(); // 获取到内核基址 HMODULE Ntoskrnl = LoadLibraryW(L"ntoskrnl.exe"); // 通过LoadLibraryW API拿到内核模块句柄
// GetProcAddress拿到目标API相对内核基址的偏移地址 DWORD64 Offset = reinterpret_cast<DWORD64>(GetProcAddress(Ntoskrnl, function)) - reinterpret_cast<DWORD64>(Ntoskrnl);
// 内核基址 + API相对内核基址的偏移地址 = API实际存储地址 DWORD64 address = Ntoskrnlbaseaddress + Offset; FreeLibrary(Ntoskrnl); Log("[+] %s address: %p", function, address); return address;}
int main(){ const DWORD64 PsSetCreateProcessNotifyRoutineAddress = GetFunctionAddress("PsSetCreateProcessNotifyRoutine");}

    我们通过找到内核基址,再利用代码注入中常用的GetProcAddress这个API,获取到PspSetCreateProcessNotifyRoutine相对于内核基址的偏移地址,于是拿到了它的完整内存地址。

    在获取到目标API的地址后,我们接着看上图中LEA指令的下面几行:XOR r8d,r8d;ADD rcx,r13; MOV rdx,rdi;这几行命令,并不涉及每次执行时的相对地址,也就是说,它们在内存中的内容是固定的。我们将这几条转换为机器码,得到4533c04903cd488bd7。也就是说,如果我们在PspSetCreateProcessNotify函数的范围里,找到了这一串机器码,就证明我们定位到了LEA r13 nt!PspCreateProcessNotifyRoutine指令的附近。再看上图中的LEA rcx,[rbx*8],机器码长度也固定为8个字节,而LEA后面跟的地址长度为4个字节(32位地址)。所以我们只需要在找到特征机器码后,往前移12位(0xC),就找到了X的起始地址。从X起读取32位,就获得了PspCreateProcessNotifyRoutine数组的偏移地址。

    具体实现方式我们看@br-sn的实现方式:
DWORD64 PatternSearch(HANDLE Device, DWORD64 start, DWORD64 end, DWORD64 pattern) {    int range = end - start;
for (int i = 0; i < range; i++) { DWORD64 contents = ReadMemoryDWORD64(Device, start + i); // 比对寻找内存中特征机器码的位置 if (contents == pattern) {
return start + i; // 返回机器码起始位置 } }
}
// 为方便阅读,将这一部分命名成了main函数,@br-sn的代码并非如此int main(){const DWORD64 PsSetCreateProcessNotifyRoutineAddress = GetFunctionAddress("PsSetCreateProcessNotifyRoutine");const DWORD64 IoCreateDriverAddress = GetFunctionAddress("IoCreateDriver"); //the address returned by the patternsearch is just below the offsets. DWORD64 patternaddress = PatternSearch(Device, PsSetCreateProcessNotifyRoutineAddress, IoCreateDriverAddress, offsets.process);
// 从机器码起始位置减去12位,定位到存放相对偏移地址的位置;读取32位,获得具体偏移地址是多少DWORD offset = ReadMemoryDWORD(Device, patternaddress - 0x0c); // 0x0c = 12
// 由于找到的patternaddress是64位,而偏移地址是32位,直接相加可能会导致溢出,因此分成高32位和低32位分别计算DWORD64 PspCreateProcessNotifyRoutineAddress = (((patternaddress) >> 32) << 32) + ((DWORD)(patternaddress) + offset) - 8;}

    为什么计算地址时最后还需要减8?因为LEA相对偏移地址是根据LEA下一条指令的位置计算的,而我们根据特征机器码比对得到的地址是LEA rcx,[rbx*8]之后的地址。所以我们还需要减去LEA rcx,[rbx*8]指令的长度,最后得到的才是正确的偏移量。WinDBG给出的API的位置为fffff8001E4Ec0A0,我们可以验证一下上述计算方式是否正确:

    到目前为止,我们找到了进程回调函数存储数组的位置,那么让我们看一下具体它里面存了什么:
    (此处的截图和上面的不是同一天截的,所以地址会变动,但原理与跟踪步骤是一样的)
    地址的最后四位并不重要,抹掉归零后返回EX_CALLBACK_ROUTINE_BLOCK的位置,该结构体原型为:
typedef struct _EX_CALLBACK_ROUTINE_BLOCK {EX_RUNDOWN_REF RundownProtect;   // EX_RUNDOWN_REF长度为8位PEX_CALLBACK_FUNCTION Function;  // 回调函数位置PVOID Context;} EX_CALLBACK_ROUTINE_BLOCK, *PEX_CALLBACK_ROUTINE_BLOCK;
让我们跟踪一下:
    执行lm a <address>命令,追踪一下这个Routine来自于哪个模块,可以看到是mfehidk,Mcafee在内核中的驱动之一。
    类似的,可以看看注册了线程回调函数的驱动,可以看到来自于mmcss,这是媒体文件控制的内核驱动。
    同理,文件映射和模块加载的回调函数PspLoadImageNotifyRoutine也可以定位到:
    如果你想要一劳永逸,EDR在用户重启后也不上线,则需要去动它的注册表。对于注册表的回调,如何跟踪?与上面的有些许差别。注册表回调函数存储在CallbackListHead结构体中,在偏移0x28处,存放了回调函数数组,结构体定义如下:
typedef struct _CMREG_CALLBACK {    LIST_ENTRY List;    ULONG Unknown1;    ULONG Unknown2;    LARGE_INTEGER Cookie;    PVOID Unknown3;    PEX_CALLBACK_FUNCTION Function;   // Callback Routine} CMREG_CALLBACK, *PCMREG_CALLBACK;

在Windbg中找到它

追踪一下来源
    抹除掉注册表回调后,EDR就无法再监测注册表的篡改操作。此时如果将EDR注册表中的启动项Start修改成4,EDR在用户重启后将无法再拉起。
    让我们看看效果。首先,让服务正常运行:
找到内核回调:
移除内核回调:
    在EDR开启的情况下注册表被篡改,服务被禁用。
    重启后与EDR彻底说再见,红队队员收获一台"单纯又干净"的终端。
    但上述操作,有一个问题:在Web后台的管理端会发现这台终端下线,从而引起防守队员的注意。更好的操作应该是仅移除EDR在内核中的所有回调函数,达到终端在线,却使其失去防护功能的效果(无进程线程日志、无文件创建删除日志、无注册表操作日志,仅留下一些想让防守队员看到的日志)。如下图,实现了某EDR在线,但成功加载了未经任何免杀处理的mimikatz,并Dump了内存,且未产生告警的效果。
    当然,上面这一步需要进一步地开发改造。

    这边依然给出一个实际可行的方案,利用backstab或offensivePH,反复杀掉EDR的防护进程,利用服务重启机制,获得一段无EDR防护的空窗期(大约3~5分钟),去执行敏感操作。一般而言,这一操作并不会告警,只会在EDR日志里留下kill的记录。

    0x02 补充

    @br-sn的工具仅支持Win10_1909和Win10_2004版本,原因大概是作者认为不同的版本需要匹配的特征机器码可能不一样。所以如果在其他版本上执行,它会返回版本不支持的信息。但我实际还比对了一下Win10_2009,内核代码依然是一致的,如下:
Win10_1909:
Win10_2009:
    所以如果不同的版本内核代码有改动,也只需要在Offset里添加对应版本的特征即可:
// Win10各版本之间这些函数开头的内容分别是什么// 只需要定位到函数的开头内容,再去匹配,就能适配各Win10版本struct Offsets getVersionOffsets() {    wchar_t value[255] = { 0x00 };    DWORD BufferSize = 255;    RegGetValue(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", L"ReleaseId", RRF_RT_REG_SZ, NULL, &value, &BufferSize);    wprintf(L"[+] Windows Version %s Found\n", value);    auto winVer = _wtoi(value);    switch (winVer) {        //case 1903:    case 1909:        return { 0x8b48cd0349c03345, 0xe8d78b48d90c8d48, 0xe8cd8b48f92c8d48, 0x4024448948f88b48 };    case 2004:        return { 0x8b48cd0349c03345, 0xe8d78b48d90c8d48, 0xe8cd8b48f92c8d48, 0x4024448948f88b48 };    case 2009:        return { 0x8b48cd0349c03345, 0xe8d78b48d90c8d48, 0xe8cd8b48f92c8d48, 0x4024448948f88b48 };    default:        wprintf(L"[!] Version Offsets Not Found!\n");
}}

(四)防护手段

    此类攻击在开始实施之前,至少必须要是管理员权限,因为Windows的普通用户是无法安装驱动的。此外,未被签名的驱动在Windows里也是禁止普通用户安装的(管理员可以关闭驱动程序,强制安装)。BYOVD攻击往往都需要借助市面上已被厂商数字签名的驱动实现,且在攻击发起前都会有文件落地,所以可以通过以下方式做出行之有效的防护:
  1. 监控已知的BYOVD驱动文件Hash,一旦落地立即删除或告警

  2. 禁止自行安装驱动,建立统一管控的驱动库。(危险驱动列表可以参考微软的清单.)

(五)总结

    这次的研究让我深刻认识到,网络安全永远不是一劳永逸的事情。攻击者会不断进化和创新,他们总能找到绕过安全措施的方法。作为安全研究员,我们也必须与时俱进,不断学习和进步,以保持我们的网络安全不受威胁。

(六参考文献

  • https://learn.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/load-and-unload-device-drivers

  • https://br-sn.github.io/Removing-Kernel-Callbacks-Using-Signed-Drivers/

  • https://posts.specterops.io/mimidrv-in-depth-4d273d19e148

  • https://blog.csdn.net/liqiang981/article/details/51895009

  • https://rootclay.gitbook.io/windows-access-control/qi-an-quan-biao-shi-fu

  • https://learn.microsoft.com/zh-cn/windows-hardware/drivers/gettingstarted/user-mode-and-kernel-mode

  • https://learn.microsoft.com/en-us/windows/win32/services/protecting-anti-malware-services-#anti-malware-service-signing-requirements

  • https://redops.at/en/blog/a-story-about-tampering-edrs

  • https://synzack.github.io/Blinding-EDR-On-Windows/

  • https://www.matteomalvica.com/blog/2020/07/15/silencing-the-edr/

  • https://www.first.org/resources/papers/telaviv2019/Ensilo-Omri-Misgav-Udi-Yavo-Analyzing-Malware-Evasion-Trend-Bypassing-User-Mode-Hooks.pdf

  • 《An Empirical Assessment of Endpoint Detection and Response Systems against Advanced Persistent Threats Attack Vectors》




关注我们,及时获取前沿研究技术

顺丰安全应急响应中心
顺丰安全应急响应中心(SFSRC)官方微信
 最新文章