Skip to content

TryNodeUpdate turns GitHub and BSC into a TCP control lane

Kirk
9 min read
malwaregithubblockchainnodejswindowsreverse-engineering
On this page

TryNodeUpdate is a Windows malware chain that uses GitHub as its first controller host and a BNB Smart Chain contract as its backend pointer. The original PowerShell sample installs a scheduled task, fetches a Node.js controller from a public GitHub repository, passes a contract address into that controller, and lets the controller resolve a raw TCP endpoint from the chain.

The backend is not in the PowerShell. It is not hardcoded as a normal hostname in the Node controller either. The controller reads TUNNEL_URL from a contract call, strips tcp://, connects to the returned host on port 8446, and starts a newline-delimited JSON control lane. On elevated Windows hosts, it downloads a native helper named rpc.exe from the same backend. The helper then reconnects as cpp-tcp-client and runs its own contract resolver, heartbeat, upload path, and local helper branches.

This sits near earlier Derp blockchain-C2 work, but the shape is different. OCRFix used BNB Smart Chain contracts to store C2 URLs. HellsUchecker used EtherHiding to pull encrypted config. TryNodeUpdate uses public GitHub for the first controller, public BSC RPC for the lookup, and raw TCP for the control channel.

If you operate a threat intelligence platform with API access and can provide a researcher account, please reach out to [email protected]. Additional data sources directly increase the quality and coverage of the threat intel published here.


Sample overview

ArtifactSHA256Role
4.ps1b5b771bd9796284faa24f4f9c45d1e77567432049b1e897416bf6f6534b659e9PowerShell bootstrap
blockchain.js / main.js5e80a76a758262a1eab6c9d0371e9921f30bc744de55cf01e4c16051be2e8a12GitHub-hosted Node controller
nodeupdate.vbs89a6ad4f88bf731b59b690aac4f0082bb25b22309478a7d5d04b702f07b68287Rendered VBS launcher
client-config.json897a8df220b0813794d465d2b44adfd5a7b253660b77b75115a905675f0f80bcClient ID state
rpc.exe42f469992efb5e80e4ddedf21be8504b5d7e0b4ec78f72511f94170f567bdad5Native Windows helper
new_write.js32cce13ce1d029abfd14df74a755f74a88025c497d122b5aaa53a0c35e54024aComplete JS payload received by the helper

The starting sample was 260422-wfaznads5j. The saved sandbox archive preserved main.js, nodeupdate.vbs, client-config.json, and rpc.exe from the infected working directory, so the Node-to-native handoff is not inferred from strings. The packet capture also reassembles the same rpc.exe bytes from the backend stream.

Chain

4.ps1
  -> %LOCALAPPDATA%\TryNodeUpdate\
  -> nodeupdate.vbs
  -> node-v20.11.0-win-x64\node.exe
  -> main.js from raw.githubusercontent.com
  -> BSC eth_call
  -> JSON.TUNNEL_URL
  -> raw TCP backend on 8446
  -> rpc.exe
  -> cpp-tcp-client
  -> uploaded new.js

The PowerShell bootstrap checks http://www.google.com, creates %LOCALAPPDATA%\TryNodeUpdate, downloads the controller from:

https://raw.githubusercontent.com/1CodeDev-hub/electronAI/refs/heads/main/blockchain.js

If the expected Node runtime is missing, it fetches:

https://nodejs.org/download/release/v20.11.0/node-v20.11.0-win-x64.zip

Persistence is a scheduled task named TryNodeUpdateTask. The task runs:

%SystemRoot%\System32\wscript.exe //nologo "%LOCALAPPDATA%\TryNodeUpdate\nodeupdate.vbs"

The VBS launcher starts Node against main.js with one important argument:

BLOCKCHAIN_CONTRACT_ADDRESS=0x75E1eDFA0d0f96D5f8F228358376d6ecdB22d802

That value is a contract lookup input. It is not the C2 server.

The GitHub controller

The public GitHub repository 1CodeDev-hub/electronAI was the source of the controller pulled by the PowerShell sample. Metadata collected on April 24, 2026 showed the repo was created on November 5, 2025, last pushed on April 22, 2026, and had 40 sampled commits.

The commit history lines up with the delivery chain rather than a normal app:

DateCommit message
2025-12-14Create install script for Node.js on Windows
2025-12-14Refactor Node.js installer to use BITS for downloads
2026-01-22Add Node.js installer script for user-level installation
2026-01-22Implement scheduled task for node update
2026-03-25Update main.js download URL and add exit code
2026-03-25Create VBS script and execute it silently
2026-04-22Add files via upload

The repo also had a release named For downloads, tag danlod, published on March 24, 2026. At collection time it exposed nine assets, including rate.cmd, rate.js, rate.vbs, RateConformation.exe, several VBS wrappers, and task.exe. The task.exe asset was added on April 23, one day after the selected sample.

That repo context is useful, but it is not enough on its own. The files from this run close the gap: 4.ps1 downloads blockchain.js, the sandbox archive preserves the matching main.js, and the packet capture shows that controller talking to the contract-derived backend.

Contract-backed backend resolution

The Node controller is obfuscated with a rotated string table. Once decoded, the backend path is direct.

The controller parses key=value arguments, reads BLOCKCHAIN_CONTRACT_ADDRESS, and sends a BSC eth_call using selector:

0x6d4ce63c

The controller has a list of public BSC RPC endpoints, including:

https://bsc.blockrazor.xyz
https://bsc.therpc.io
https://bsc-dataseed2.bnbchain.org
https://bsc-dataseed.bnbchain.org
https://bsc-dataseed-public.bnbchain.org

The contract response is ABI-decoded as a string. The decoded string is JSON. The controller reads:

TUNNEL_URL

On April 22, 2026, three tested contract inputs all returned:

{"TUNNEL_URL":"tcp://206.206.127.94:8446"}

Those inputs were:

ContractSource
0x75E1eDFA0d0f96D5f8F228358376d6ecdB22d802Original PowerShell sample
0xB9F8e457Cd0E97f0CF7aEd57D0654F1Ad61759C1Repo variant
0x5728Fe164B5a9c599325F6fed9F744306F4D772bBaked-in fallback

The controller strips tcp://, splits the host and port, and opens a raw TCP socket. The protocol is newline-delimited JSON, not HTTP.

Packet capture

The saved packet capture gives the cleanest view of the handoff from the Node controller to the native helper:

TimeObservation
82.792040DNS answer for bsc.blockrazor.xyz
82.830666TLS client hello with SNI bsc.blockrazor.xyz
82.946166TCP SYN to 206.206.127.94:8446
83.357425DNS answer for api.ipify.org
84.528915Second TCP SYN to 206.206.127.94:8446
85.835952Helper TCP SYN to 206.206.127.94:8446

Three backend streams matter:

StreamMeaning
32Node register, rpc-download-request, helper chunks, rpc-download-end
34Node register, rpc-download-meta-request, rpc-download-meta
36Launched helper registers as cpp-tcp-client and sends periodic heartbeats

The helper metadata returned by the backend names rpc.exe, size 886272, mtime 2026-04-13T13:09:57Z, and SHA256:

42f469992efb5e80e4ddedf21be8504b5d7e0b4ec78f72511f94170f567bdad5

The helper reassembled from stream 32 is byte-identical to the rpc.exe preserved in the sandbox archive. That proves the handoff without needing live infrastructure.

What rpc.exe adds

rpc.exe is not a downloader stub. It repeats the contract-mediated backend lookup in native code and then runs its own control client.

Native analysis mapped the helper into the same broad pieces as the Node controller:

AreaWhat the helper does
Backend selectionBuilds the BSC eth_call, decodes the ABI string, and extracts TUNNEL_URL
Local stateReads and writes the client config and PID files beside the helper
RegistrationSends host, user, privilege, platform, CPU, memory, public IP, and contract fields
TransportConnects over raw TCP, receives newline-delimited JSON, and sends JSON responses
KeepaliveSends 30-second heartbeats
UploadsReceives files in start, chunk, and end messages

The helper's register object includes host and user data, admin state, platform details, CPU and memory fields, public IP, the contract address, and:

connection_platform: cpp-tcp-client

The same helper owns the upload path used to write new.js:

Message typeAction
file-upload-startResolves destination path and opens the active file handle
file-upload-chunkBase64-decodes inbound data and writes decoded bytes
file-upload-endCloses the handle and emits file-upload-complete

In the observed path, chunks are written silently. The helper sends completion only after file-upload-end.

Later controlled runs tied the live new.js writes back to that native upload chain. Short runs produced exact prefixes of the final payload. A fresh client ID and longer run produced a complete 1518476-byte new_write.js payload that passes node --check.

The complete new_write.js uses the same rotated string-table family as blockchain.js. After decoding, it exposed the same main controller command surface as the GitHub controller, with file-zip and file-unzip added.

Endpoint drift

The original packet capture and April 22 contract read point at:

206.206.127.94:8446

Later controlled runs recorded the same contract returning:

84.200.87.236:8446

That distinction matters. 206.206.127.94:8446 is the backend proven by the original packet capture. 84.200.87.236:8446 is proven by later run logs, not by the first capture.

Passive enrichment put 206.206.127.94 in AS396356 Latitude.sh with PTR host.trynode.cloud. Current forward and reverse pivots tied the host to www.trynode.cloud, mesh.trynode.cloud, trynode.info, and mail.trynode.info. The same IP exposed a mixed service surface: SSH, Apache redirects to https://grailed.lnks.space/, PPTP, Express, and a phpMyAdmin login page on 8232.

The later observed endpoint 84.200.87.236 landed in AS214036 Ultahost, Inc. in Germany. Passive data tied it to sjjh.jacktake.website, 80fa.jacktake.website, pmta236.qigaf84.caymedin.com, and doodles.yachts. It also exposed a phpMyAdmin login page on 8232.

Those adjacent surfaces are pivots, not automatic malware lanes. The proven malware lane is raw TCP on 8446.

Boundaries

There are a few things this case does not prove.

bsc.blockrazor.xyz is treated here as a public BSC RPC dependency. The evidence shows the malware used it to read a contract. It does not show BlockRazor operating the malware backend.

85.203.46.39 appears as the helper-reported public_ip in the detonation register. It is not the backend.

Cloudflare IPs for bsc.blockrazor.xyz are Cloudflare edges. They are not operator-owned infrastructure.

The native helper has branches for embedder.exe, get_data.exe, and output.zip, but those side-helper binaries were absent from the saved sandbox archive. The branches are real. The helper contents are not recovered here.

The backend-side rule that decides whether a session gets a prefix of new.js or the complete new_write.js remains server-side. Local evidence rules out a one-chunk client cap: every shorter artifact is an exact prefix of the complete payload, and longer runs with fresh IDs received more data.

Indicators

TypeValue
Scheduled taskTryNodeUpdateTask
Working directory%LOCALAPPDATA%\TryNodeUpdate\
Controller URLhttps://raw.githubusercontent.com/1CodeDev-hub/electronAI/refs/heads/main/blockchain.js
Node runtime URLhttps://nodejs.org/download/release/v20.11.0/node-v20.11.0-win-x64.zip
Contract0x75E1eDFA0d0f96D5f8F228358376d6ecdB22d802
BSC selector0x6d4ce63c
Original backend206.206.127.94:8446
Later observed backend84.200.87.236:8446
RPC endpoint dependencyhttps://bsc.blockrazor.xyz
Public IP checkhttps://api.ipify.org/?format=json

The useful detection shape is the combination: a user-level TryNodeUpdate directory, Node runtime bootstrap, raw.githubusercontent.com controller fetch, BSC eth_call traffic, and raw TCP on 8446 to the contract-returned host. Any one piece can look normal. Together, they describe the chain.

Share this article