在最近的一次渗透测试中,我需要对抗某EDR产品,这个产品使我难以访问lsass的内存,也就不能使用自定义的Mimikatz来dump明文密码。
作为一个前恶意软件作者—我知道好几种在驱动中实现该EDR产品中这一功能的方法。我首先想到的第一种方法是Obregistercallback ,它通常用在反病毒产品中。微软实现了这一回调,因为许多反病毒产品对winapi的hook非常简陋,从而使得恶意软件能够实现rootkits。然而,在msdn页面的最下方,有一段文字:“从_Windows Vista 的Service Pack 1(SP1)和Windows Server 2008 开始可用。”为了利用某些环境相关的功能,我当时使用的是Windows Server 2003,因此,要采用这一方案缺少必要的函数。
在又花了好几个小时之后,通过修改csrss.exe的magic字段,并通过csrss.exe继承lsass.exe的句柄,我成功地拿到了具有PROCESS_ALL_ACCESS访问权限的lsass.exe句柄。这种方式通过利用csrss生成一个子进程,来继承lsass当前的句柄。
然而,当我准备享受攻破一个EDR产品的喜悦时,我得出一个令人失望的结论。EDR仍然阻止我把shellcode注入进csrss中,以及通过RtlCreateUserThread
创建线程。然而,出于某些原因——虽然不能生成子进程并继承其句柄,但不知道怎么回事,我仍然能获取到lsass.exe的PROCESS_ALL_ACCESS权限的句柄。
什么?还有这种事?
别着急,我不抱任何期待地用下面这条语句试着打开lsass.exe的句柄:HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, lsasspid);
你知道发生了什么吗?我获取了对lsass.exe的FULL CONTROL
权限。EDR甚至没有做一点点简单的混淆。此时我意识到,我从一开始就采用了错误的方式,EDR并不在乎你获取句柄的访问权限,它在意的是你拿到句柄之后做了些什么。
现在我们已经知道了要获取lsass.exe句柄的完全控制权限并不困难,我们接下来需要考虑下一个问题。在这个句柄上调用MiniDumpWriteDump()
失败了。
接下为我们进一步分析这条警告:“Violation: LsassRead”。我没看懂什么意思,他到底想表达什么?我只是想要dump进程。然而,我也知道要dump一个远程进程,必然要调用某些WINAPI,比如 MiniDumpWriteDump()
中的ReadProcessMemory (RPM)
。我们来看一下ReactOS 中MiniDumpWriteDump()
的源代码。
如你所见,函数(2)dump_exception_info()
,以及一些其他函数,依赖(3)RPM
来实现它的功能。这些函数会被MiniDumpWriteDump
(1)引用,这可能就是我们这一问题的根源。到这里就到了经验发挥作用的时候了。你必须理解Windows System Internal以及WINAPIs是怎样被处理的。拿ReadProcessMemory
来举例,它工作机制是这样的。ReadProcessMemory
只是一层封闭。它会进行一系列正确性检查,比如空指针检查。这就是RPM的所有功能。然而,RPM也会调用“NtReadVirtualMemory
”函数,它会在执行syscall 之前设置寄存器。syscall 仅仅是告诉CPU进入内核模式,接下来另一个函数ALSO(也就是NtReadVirtualMemory
)会被调用。它的功能逻辑才是ReadProcessMemory
的核心逻辑。 — — — — — -Userland — — — —-— — | — — — Kernel Land — — —RPM — > NtReadVirtualMemory --> SYSCALL->NtReadVirtualMemoryKernel32 — — -ntdll — — — — — — — — — - — — — — — ntoskrnl
在掌握上述知识后,我们就能理解EDR产品是怎么检测并拦截RPM/NtReadVirtualMemory调用的。答案很简单,就是hook。你可以在我之前的文章 中找到关于hook的更多信息。简而言之,它使你能够把你的代码放到任意一个函数中间,并获取参数和返回值的访问权限。我100%确定EDR是用了我之前提到过的一种或几种hook技术。
然而,读者该知道的是,并不是所有的EDR产品都会运行一个service,尤其是运行在内核态的驱动。因为拥有内核态的访问权限,驱动可以在RPM的调用栈中执行任意级别的hook。然而,这也会导致Windows环境中的巨大的安全隐患。因此,一种解决方案是预先预防这种修改,也就是Kernel Patch Protection(KPP或Patch Guard)。KPP会扫描内核中每一个层级,如果检测到修改,就会触发BSOD。其中也包括ntoskrnl部分,它封闭了WINAPI的内核级的逻辑。基于这些知识,我们可以假设EDR不会也不能hook调用栈中任何内核级的函数,只会操作用户态的RPM和NtReadVirtualMemory调用。
要看函数位于我们应用内存的哪个位置,可以使用printf,将%p作为格式化字符串,函数名作为参数。如下所示:
然而,不像RPM,NtReadVirtualMemory并不是ntdll中的一个导出函数,因此你不能直接引用这个函数。你首先要指定函数的签名以及将ntdll.lib链接进你的项目中。
当一切就续之后,我们让它跑起来看看结果~
现在,我们拿到了RPM和ntReadVirtualMemory的地址。接下来我用我最喜欢的逆向工具Cheat Engine来读取内存并分析它的结构
对于RPM函数,看起来一切运行良好。它进行了一些栈操作并设置寄存器,然后调用Kernelbase(下次再详细介绍)中的ReadProcessMemory()。它最终会调用到ntdll的NtReadVirtualMemory。然而,如果你看一看NtReadVirtualMemory,并对最基本的hook框架detour有所了解,你马上能看出这一块指令是不正常的。函数的前5个字节被修改了,其他的没变。你可以看看其他相似的函数来识别这个。其他的函数都是类似这样的格式:
区别在于syscall的id,(用于在内核态区分WINAPI函数)。然而,NtReadVirtualMemory中第一条指令是一条跳转到其他地址的JMP指令,让我们跟下去看看。
ok,现在我们不在ntdll模块中了,而是进入到了CyMemdef64.dll模块。现在我懂了。
EDR放了一个跳转指令到NtReadVirtualMemory函数的开始位置,把代码流转向了他自己的模块,然后检查恶意行为,如果检查失败,会返回给Nt*函数一个错误码,不再进行内核态执行。
现在我们已经很清楚EDR是怎么检测并拦截我们的WINAPI调用的了。我们应该怎么办呢?有两个解决方法。
我们已经知道了NtReadVirtualMemory函数原来的实现,我们可以把jmp指令修改回它原本的指令。这样我们的操作就不会再被CyMemDef64.dll拦截,可以毫无阻拦地进入他们控制不到的内核态。
我们也可以创建一个我们自己的函数,和上一种方法类似,但是我们不会修改被hook的函数,我们会重新在另一个地方创建它。然后,修改Ntdll的IAT表,把NtReadVirtualMemory的地址和fixed_NtReadVirtualMemory的地址互换。这一方式的好处是如果EDR会检查它的hook有没有被修改,它就检测不出来。它仅仅是不会再被调用了,因为ntdll IAT 指向了其他地方。
我采用了第一种方法。它很简单,这样我就能迅速完成这篇博客:)。然而,如果采用第二种方法的话,就会很繁琐,接下来的几天我打算试试。在这里我要提一下我的主管Andrew,他现在在医院做阑尾割除手术。希望他快点好起来。
这一方法当前仅对这一EDR有效,然而,要逆向其他的EDR产品并写出一种通用的绕过方式也并不算困难,因为它在hook时会有各种各样的限制(感谢KPP)
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)