🚀 A CloudSEK se torna a primeira empresa de segurança cibernética de origem indiana a receber investimentos da
Leia mais

An operator has spent the summer of 2026 publishing malicious packages to the public npm registry under names that belong to somebody else. Not names resembling popular libraries, which is the usual pattern, but the internal package names of one company. We track the activity as TXTBOOK. It began on PyPI, moved into npm with its execution trigger rebuilt for the new ecosystem, and now runs to 993 npm packages.
CloudSEK's supply chain monitoring surfaced them and from those packages we pivoted to the rest: the staging infrastructure, the command and control estate, the third stage implant, and finally the target.
What matters is who the 993 packages are named after. They reproduce the internal, private package namespace of a single organisation: Tinkoff, the Russian financial group now trading as T-Bank. The names cover the group's BNPL product, its business banking platform, its analytics client libraries, and the proprietary counterparts of its own published open source projects. This is a dependency confusion operation aimed at one company, run at a scale normally associated with indiscriminate registry flooding.
The targeting is not inferred from names alone. The second stage loader carries a hardcoded, obfuscated table of hostnames, and three of its nine entries are victim infrastructure rather than attacker infrastructure. All three belong to the T-Bank group. Before contacting its own command and control servers, the loader resolves the group's internal artifact repository and proceeds only if it answers. This ensures that the malware has been run inside the bank’s internal network. The malware carries its own definition of the target.
A small secondary cluster of online betting names is present in the same advisory data. It is four packages out of 993, none of which were held in our corpus, and it is reported here as an observation rather than a co-equal target.
TXTBOOK's delivery technique remains its most distinctive feature. Rather than downloading a binary over HTTP, the loader reassembles a native executable from several hundred DNS TXT records, a few hundred bytes per record. To a network monitor the activity is a burst of DNS lookups. The third stage that arrives by this route is a Sliver implant with an effectively complete command set, and its command and control protocol has now been reversed from captured traffic, recovered key material and static analysis of the binary.
The operation did not begin with npm. It was first documented on PyPI in July 2026, as the campaign tracked 2026-07-andreiiiiiii_i and, before that, as activity around a configuration client package. The move into npm reused the infrastructure wholesale: the same staging domains, the same command and control estate, the same third stage. What changed was the execution trigger, which was rebuilt for the new ecosystem. On PyPI the operator installs a path-configuration file that the interpreter runs at startup. On npm it appends a required call to the package entry point, which runs on import. Both achieve code execution without declaring an install hook. That is a deliberate port, not a reused payload.
Publishing proceeded in waves rather than continuously, and the version numbers separate them cleanly. Each wave changed the thing that had made the previous one detectable. The earliest packages avoided npm lifecycle scripts altogether. A later wave reintroduced a postinstall hook. The most recent returned to import-time execution and added per-package payload variation, so that no two packages share a hash and the loader filename differs in every one.
The supporting infrastructure was provisioned in batches ahead of the packages that would use it. The command and control estate spans roughly two dozen Cloudflare accounts in two clearly separate naming batches, and a second staging domain was registered, shipped inside live packages, and left dormant, resolving but serving nothing. The operator was building the next stage of the campaign while the current one was still running.
Registry enforcement has not displaced them. When npm removed nine of the first ten names on 1 August 2026, publishing resumed within roughly a day, and by 3 August three of those names carried live malicious versions again. The publishing accounts explain why: they are disposable, and almost every one holds a single package, so removing an account removes a package rather than an operator.
CloudSEK's supply chain monitoring surfaced the first ten packages of the npm arm, and those names are what made the rest reachable. Everything that follows, the staging channel, the native loader, the implant and the target, was recovered by following them.
Published version numbers cluster into distinct bands, and the bands correspond to changes in execution technique. The earliest avoid npm lifecycle scripts entirely, triggering from a single line appended to the package entry point so that importing the module suffices. A later band reintroduces a postinstall hook. The most recent returns to import-time execution and adds per-package payload variation, defeating hash-based clustering.
The 20.x band is a correction to the previously published four-generation model. It is the largest single band in our holdings and reassembles the same command and control account batch as the 33.x to 35.x bands, so it belongs to the same infrastructure era rather than to an earlier one. Any taxonomy of this campaign built on the four generations alone is incomplete.
Following the packages meant following the payload, and the payload did not cooperate. Each step below depended on the one before it, so the order is the order of work. The chain has four stages: an npm package that executes on import, a loader that retrieves its payload over DNS, a native second stage, and a Sliver implant. Recovering the fourth stage required building a purpose-made detonation environment, reading keys out of the running process, and modifying that process in memory while it ran.
The sample refuses to run in commercial sandboxes, so a bespoke environment was built. A Windows 10 guest runs under a QEMU/KVM hypervisor on an isolated virtual bridge with no uplink. A local service answers every DNS query and accepts every TCP connection, minting per-name TLS certificates from a certificate authority already trusted in the guest's machine store, so the implant's HTTPS beacons complete a handshake and expose their payloads at the capture layer. Packet capture happens on the host, outside the guest's reach.

The first detonation produced no beacon. A memory capture taken during execution explained why: the third stage payload was resident in memory still compressed, its packer section names intact and no runtime initialised. The loader maps its payload reflectively, allocating memory and parsing the executable header by hand, but it performs no decompression, and the Windows third stage is packed.

This is the most likely explanation for the sparse public behavioural record on this family. The third stage must be executed directly to be observed at all.
The loader stores its hostname table obfuscated rather than as plaintext strings, using a single-byte XOR key that differs per build. Recovery needs nothing more sophisticated than trying all 256 keys and scoring each result by how many well formed hostnames appear, without reference to any previously recorded key. Nine hostnames emerged. Six are the operator's own infrastructure. The other three are not, and they are what the rest of this report turns on.
The implant is a statically linked Go binary with no import table beyond the bare runtime, no package paths and no program strings, because it was compiled with an obfuscator. Conventional string-based capability profiling returns nothing. What survives is Go's reflection metadata, which the runtime requires in order to marshal messages and which therefore cannot be stripped.
Probing that metadata for the framework's command-message type names recovered 79 of 82. The three absent names are spelling variants, not missing capabilities, so the implant carries an effectively complete command set.
This was confirmed exhaustively rather than assumed. The operator key, the implant keys, the protocol identifiers and even the command and control hostname are all absent from the unpacked binary under single-byte XOR, additive and subtractive constants, position-dependent XOR, and repeating-key XOR up to period eight. A full pairwise search, testing every offset in the file against an index of every eight-byte window for the relation ciphertext XOR key equals plaintext, also returned nothing.
If the secrets exist only at runtime, the runtime is where they must be read. The guest's entire physical memory is a single mapping inside the hypervisor process on the host, and guest physical addresses are a fixed offset within it, so the host can read guest memory directly while the guest runs.
Scanning the guest's memory for the format's key encoding recovered both the operator's public key and the implant's own key pair. Note that a search for conventional padded base64 finds nothing here, because the format uses unpadded encoding, which is why the keys had not surfaced earlier.

Two representations of the operator key exist in memory: the human-readable encoded string that the implant parses, and the raw decoded bytes derived from it. Only the encoded string is authoritative. The decoded bytes are a by-product regenerated on every use, so overwriting them achieves nothing durable, which cost considerable time to establish. Overwriting the single encoded string, with the guest paused and the target verified before and after the write, causes every subsequent beacon to be sealed to a key we hold.
With beacons sealed to a controlled key, the plaintext of the command and control exchange becomes readable. Each beacon carries a freshly generated symmetric session key wrapped in the framework's session-initialisation message. Every beacon carried a new one, which established that no session was ever completed during analysis: the implant was repeatedly restarting a handshake that the isolated environment never answered.

Before encryption is even reached, each message is wrapped in one of five interchangeable transport encoders, chosen at random per request. The choice is communicated to the server in a single-letter query parameter whose value is a numeric nonce; the encoder identifier is that nonce modulo 65537. Random letters are spliced into the decimal value before transmission, so the digits must be extracted before the modulus is taken.

The image encoder is not simple pixel packing. Payload bytes are written down columns rather than along rows, and the values zero and one are escape-encoded before packing, because a raw zero byte would be lost to the image encoder's padding. A decoder that reads rows in the obvious order recovers only the first thirty bytes and appears to have found a truncated message.
The verification keys were recovered from the same memory image, and they are the most durable indicator in this report. A signing key is generated once per server installation and signs every implant that server builds, so it identifies the operator's infrastructure independently of any hostname.
Function names are destroyed by the obfuscator, so the cryptographic routines were located structurally instead. Two properties survive obfuscation: the constants a cipher must use, and the arithmetic a construction must perform. The authenticated-encryption routines are identifiable from length arithmetic alone, because sealing appends a sixteen-byte authentication tag and opening removes one.

Measuring the proportion of indirect calls established that only names and strings are obfuscated, and that control flow is intact: 0.2 percent in the cryptographic routines and 2.09 percent across a sample of the code section, consistent with ordinary compiled Go rather than with flattened control flow, which would show fifteen to forty percent.
Extracting the signing key from each platform build partitions the campaign, and the encryption keys partition it identically. Two independent cryptographic identities agreeing on the same split is strong evidence of two distinct server installations.
Obfuscation was applied inconsistently across the four builds, and the inconsistency is itself useful. String-literal encryption was applied to the Windows and macOS builds but to neither Linux build. The Linux implants therefore carry the operator's keys, their own private keys and their command and control hostname in plaintext on disk. Captured server to implant traffic for those builds is decryptable offline with no runtime analysis whatsoever, and the macOS build is the only one whose server cannot be attributed by this method.
Recovering the loader's configuration in the previous section produced something the package names alone could not: a list of hostnames the malware carries but does not own. Three of them belong to the victim. That is where the campaign stops looking like registry spam and starts looking like an operation with an address.
The 993 package names are the primary evidence, and they are unusually legible. A conventional typosquat imitates something popular: a character transposition of a widely installed library, or a plausible-sounding utility. These names do neither. They are internally structured, product-specific and organisationally coherent, in the way that a company's private package registry is coherent.
Name analysis establishes intent but not much more, so the second line of evidence matters more. The second stage loader stores its hostnames as an obfuscated table rather than as plaintext strings. Recovering the table by exhaustive key search, rather than by reusing a previously recorded key, yields the same nine entries from the Windows, macOS and Linux ARM64 builds. Six are attacker infrastructure. Three are not.

All three victim entries belong to one corporate group. CloudPayments is a Russian payment processor that T-Bank acquired in stages, taking a 55 percent holding in October 2017, 95 percent by August 2019 and full ownership in January 2023. It reads as an unrelated third party only if the ownership is not known.
The gate is also behavioural, not merely declarative. Across three independent detonations, nexus.tcsbank.ru was the first DNS query the sample issued, resolved twice, before any command and control host was contacted. The loader proceeds down the list only until one entry answers, which is why the second and third victim hostnames appear in traffic only when the first is unavailable. The malware is asking whether it has landed inside the target network.
The analytics client family is the most revealing part of the squat list. These packages follow a rigid convention in which the trailing segment is an internal service path, and the paths are far too specific to be invented.

A name such as beaver-ui-drawer could be guessed. A path such as sme.rko.conversionpayments.web could not. The operator worked from a real inventory of the target's internal systems, obtained before the packages were published. How that inventory was obtained is not established by this analysis, but the plausible sources are narrow: a leaked lockfile or build manifest, an exposed internal registry index, a public code-search hit on a private configuration file, or a former insider.
T-Bank maintains a visible open source presence, including the Tramvai application framework and the Taiga UI design system. The campaign registers names in both lineages, among them tramvai-module-feature-toggle, tramvai-tinkoff-module-legacy-popup and taiga-ui-proprietary-navigation. The last is the most instructive: the public design system is Taiga UI, and the name asserts the existence of a proprietary counterpart.
This is a low-cost, high-yield reconnaissance route that generalises well beyond this victim. An organisation's public repositories reveal its internal naming conventions, its module boundaries and often the names of private packages referenced in configuration. Any company publishing open source under a corporate identity should assume the private namespace adjacent to it is inferable.
Knowing who the operator was aiming at changes the question from what the ten packages were to how many more of them exist. The answer came from sweeping the package corpus for the loader's structure rather than for any name or hostname.
The expansion happened in three steps, and the provenance of each matters when weighing the total.
The final step swept all 190,045 npm archives held in CloudSEK's package corpus for the loader's structural signature. Because the loader assembles its hostnames from string fragments at runtime, a search for whole hostnames finds nothing; the sweep therefore keyed on fragment-safe substrings combined with the campaign's characteristic file layout. Every archive that matched was then opened and confirmed by reassembling the fragments back into command and control hostnames, which is the part that cannot occur by coincidence.
Each of the 187 new packages was verified individually rather than in aggregate. Every one yields at least one campaign hostname when the fragmented string literals in its loader are reassembled, and none was accepted on file layout alone. The loader filename is randomised per package across at least fifteen forms in this set, among them _adapter.js, _bridge.js, _init.js, _compat.js, _platform.js, _runtime.js and setup.js, so filename heuristics do not cluster them.

The resulting figure is a floor rather than a census. It counts only what our corpus already held, and the analytics client names are formulaic enough that the operator's published set is very likely larger still.
These are the highest-value pivots in this report. A minisign key identifies a Sliver server installation, not a campaign, and therefore clusters samples across infrastructure rotation, renaming and rebuilds.

These are victim infrastructure, not attacker infrastructure. They should be used to recognise the campaign's targeting, and must not be blocked or sinkholed as though they were malicious.



For the named target the exposure is direct. A dependency confusion attack succeeds when a build system resolves a private package name against the public registry, which happens through misconfiguration rather than through user error. Any build that resolved one of the 993 names would have executed native code on import, with the resulting Sliver implant providing an effectively complete post-exploitation capability inside a build environment, which is typically among the most credential-rich contexts an organisation operates.
For everyone else the applicable lesson is the reconnaissance, not the payload. The operator assembled a detailed inventory of a target's internal package and service names before publishing anything, and part of that inventory was inferable from the target's own public open source repositories. Any organisation with a visible open source presence and a private registry shares this exposure.
Nothing in this analysis evidences a successful compromise. Publication of a squatted name demonstrates intent and capability; it does not demonstrate that any build resolved it.
TXTBOOK was first assessed as a dropper distinguished by DNS payload staging. That was correct but incomplete. DNS staging is how the payload arrives. The campaign around it is a sustained dependency confusion operation against a single financial group, run at a scale that resembles indiscriminate flooding without being indiscriminate.
The distinction has practical consequences. Read as registry flooding, the appropriate response is to block a list of names. Read as targeted dependency confusion, the appropriate response is to fix dependency resolution policy, audit which internal names are publicly inferable, and treat the package list as a symptom. Only the second response addresses the next 993 names.
The reconnaissance deserves more attention than the payload. Internal service paths embedded in the squat list indicate access to a real inventory of the target's systems, and a further portion of that inventory was inferable from the target's own published open source at no cost and no risk. The expensive part of this campaign was the reconnaissance, and it is repeatable against any organisation that publishes code under a corporate identity.
The initial ten packages surfaced by CloudSEK's supply chain monitoring are what made the remainder reachable. Every subsequent finding, the staging infrastructure, the command and control estate, the third stage implant, the two operator servers and the 993 package inventory, was reached by pivoting from that seed.
All 993 package names attributed to this campaign, grouped by namespace family and ordered by family size. Names marked with a dagger were newly identified in the corpus sweep described under How Far the Campaign Reaches, and do not appear in previously published advisory data.




































