Hunting Lazarus Part IX: The Google Mirror
Five trojanized browser extensions extracted Google profile identity through chrome.identity and routed it through an Aptos blockchain dead drop. Before any wallet artifact moved, the extension asked Chrome who owned the browser.
The full investigation is consolidated in the Inside the Machine research article.
The wallet trojan that asked Chrome who owned the browser.
Before the wallet trojan stole anything, it asked Chrome who was sitting at the browser.
It did not have to look the answer up. Chrome already knew it.
The extension called chrome.identity.getProfileUserInfo(), took the Google profile identifier and email address the browser handed back, wrapped them into a small JSON object, and sent that object to a server it had just looked up on the Aptos blockchain.
Then it set a flag so it would not ask twice.
That was the first request the extension made. Before any wallet artifact moved. Before any user activity. Before the badge in the browser toolbar turned green.
BeaverTail asked the developer to run code.
OtterCookie watched the workstation.
The Google Mirror was the first thing the extension said when it woke up.
Executive Summary
- Five trojanized browser extensions – Bitwarden, Phantom, TronLink, Trust Wallet, and a Brave/MetaMask-themed trojan – share a single boot sequence.
- The C2 base URL is not hardcoded. Each extension reads it from a transaction payload on an Aptos mainnet account (
0xa3047f3ccd...455e88) at runtime. - Once the C2 is resolved, the first POST is an identity beacon: a URL-encoded
head=<JSON>body sent to${ping}/inz-notify, carrying the browser profile'sprofileIdandprofileEmailfromchrome.identity.getProfileUserInfo(). - A second endpoint,
${ping}/inz, carries wallet-data exfiltration in the sameheadshape plus abodyfield. Wallet activity uses this channel later in the session. - The identity beacon fires exactly once per browser session. A one-shot guard (
ping9x = 1) suppresses repeats. - A consistent
manifest.json.commands.__metablock is the build-pipeline marker: campaign tag_V,client_id, and aninstalled_verfield that decodes to a build date. - The pattern turns wallet-extension compromise into identity-attributed collection. The operator does not only know what was taken. The operator knows whose browser took it.
Where Part VIII Left Off
Part VIII ended at the live workstation. OtterCookie was reading clipboard contents, capturing keystrokes, taking screenshots, watching the active workspace on a thirty-second clock. The collection window had moved from what the developer had already saved to what the developer was doing now.
Part IX moves one layer further in. Not to what was typed. Not to what was on screen. To who the browser said the user was.
The Five Extensions
The trojanized set is broad. Two wallet-extension families that hold seed phrases. One Tron-specific wallet. One general crypto wallet. One password manager. One Brave/MetaMask-themed build.
| Brand | Family | Path |
|---|---|---|
| Bitwarden | password manager | bitwarden/nngceckbapebfimnlniiiahkandclblb/ |
| Phantom | Solana wallet | phantom/bfnaelmomeimhlpmgjnjophhpkkoljpa/ |
| TronLink | TRON wallet | tronlink/ibnejdfjmmkpcnlpebklmnkoeoihofec/ |
| Trust Wallet | multi-chain wallet | trust-wallet/egjidjbpglichdcondbcbdnbeeppgdph/ |
| Brave/MetaMask-themed trojan | EVM wallet, Brave-adjacent build | brave-metamask-trojan/nkbihfbeogaeaoehlefnkodbefgpgknn/ |
The spread matters because it crosses trust categories. This is not one poisoned wallet clone. It is one injected identity routine carried across password-manager and wallet surfaces.
A password manager is a custody-of-credentials brand. A wallet extension is a custody-of-keys brand. The two have different user-trust gradients and different security-engineering teams behind them. The operators chose both. The identity routine inside is the same.
Bitwarden gives the campaign coverage of the developer's saved-credential surface – the browser's password vault, the corporate SSO state, the OAuth login traces, anything the developer has parked behind a master password. Phantom and TronLink and Trust Wallet give the campaign coverage of the developer's crypto surface – Solana, TRON, EVM, Cosmos-adjacent chains. The Brave/MetaMask-themed build extends the EVM reach into a separate user base that uses Brave's privacy-positioned browser as its primary chain interface.
None of these differences mattered for the identity move. The same boot sequence appears in each, and the cleanest read of it is in TronLink's script_loader.js, where the bundled code is short enough to follow line by line.
The catalog is not the point. The pattern is.
The Aptos Dead Drop
The first thing each of the five extensions does on load is ask a public blockchain where its server is.
That is not how malware traditionally talks to its operator. A traditional implant carries its C2 address compiled into the binary. The compilation happens once, the address is fixed for the lifetime of the build, and the implant is dependent on that address being reachable. When the address is blocklisted, the build dies. When the host gets seized, the build dies. The operator's only mitigation is to ship a new build with a new address, and to hope the new build reaches victims faster than the old one gets cleaned up.
A dead drop changes that geometry. Instead of carrying the address, the implant carries a pointer – a place to look the address up at runtime. The implant's hard dependency is on the dead drop, not on the C2 server. The C2 server can move; the dead drop tells the implant where it moved to.
Lazarus has used dead drops for this purpose in earlier campaign generations. Part II of this series documented the move to blockchain dead drops in the Contagious Interview infrastructure. The pattern here is a continuation of that idea, applied at browser-extension scale.
The mechanic is straightforward. Each extension carries a single base64-encoded URL. When the URL is decoded, it points to the Aptos public mainnet node:
https://fullnode.mainnet.aptoslabs.com/v1/accounts/0xa3047f3ccd...455e88/transactions?limit=1
The extension calls this URL with no authentication. The Aptos public node responds with the latest transaction on that account. The transaction has a payload field. The payload is a hex string with the actual C2 base URL inside it, reversed and hex-encoded so that someone glancing at the blockchain explorer does not see a working URL.
The extension reverses the hex string, decodes it, splits on a pipe character, and reads the first segment. That string is ping, the current C2 base URL.
Three things are happening at once.
First, the operator does not need to own the Aptos mainnet. They only need to publish transactions on one account on it. Aptos provides the availability. The ledger gives the record persistence. Public nodes give the extension a convenient HTTPS API for reading it. Neither has to know that the transaction payload is being used as malware configuration. The cost to the operator is the gas fee on each rotation transaction.
Second, the blockchain is the pointer, not the payload host. The actual C2 server is still a normal server somewhere – Evoxt, Tencent, Vodafone DSL, Korea Telecom. The blockchain does not handle implant traffic. It does not store seed phrases. It does not respond to wallet exfiltration POSTs. It only answers the question where is the server today.
Third, the lookup is updateable but not editable. The operator can publish a new transaction with a new C2 address at any time. The next extension that boots and calls the dead drop will get the new address. But the operator cannot rewrite the transaction history that already exists. Past addresses remain on chain for anyone willing to walk back through the account's transaction list. That is not perfect operator OPSEC, but it is fine OPSEC – by the time a defender walks the history, the operator has already moved.
The blockchain was not the payload host. It was the pointer.
A takedown can remove a server. It cannot remove the transaction the extension is reading. The operator can rotate with one Aptos transaction. Defenders cannot erase that transaction, but they can still break the channel locally: block the Aptos lookup, detect the account query, remove the extension, or cut off the resolved C2.
The same Aptos address resolves the C2 for all five extension families. That is the cleanest single evidence point that the five extensions are one campaign, not a coincidence of similar-looking trojans. They share the same dead drop. At minimum, they share the same campaign control plane.
The hunting implication runs the other direction. A wallet or password-manager extension making outbound HTTPS calls to fullnode.mainnet.aptoslabs.com from a background context is not by itself proof of compromise – some legitimate wallet extensions touch Aptos for in-app features – but the call is suspicious enough to ask: what is the wallet doing with the result?
If the answer is resolving its server, the rest of the story follows.
The Boot Sequence
Once the dead drop has been resolved, the rest of the boot runs in five short steps. The cleanest implementation is in TronLink's script_loader.js.
Step one: the extension reads its own manifest, specifically chrome.runtime.getManifest().commands.__meta. That block is the build-pipeline marker the operators left inside the extension. It carries fields like _V, client_id, installed_ver, and hash_mode. The trojan strips the description field – the one that would carry the brand name – and adds ext_ver, the running extension version.
Step two: the extension calls chrome.identity.getProfileUserInfo(). Chrome answers with the signed-in profile's identifier and email address. The trojan adds both as profileId and profileEmail to the same object that holds the manifest metadata.
Step three: the object is JSON-encoded, wrapped in a URL-encoded form body under the key head, and POSTed to ${ping}/inz-notify.
Step four: the server responds. The trojan reads the response and looks for success: true.
Step five: on success, the trojan sets a one-shot guard (ping9x = 1) so the identity beacon does not fire again for the rest of the session. It also flips the extension's browser-toolbar badge to green for five seconds – a small UI tell that, to anyone watching, would look like a wallet-sync success notification. That flourish matters because it gives the victim a benign interpretation of the first network exchange: the extension appears to have synchronized successfully.
That is the entire identity-attribution layer. From extension boot to identity beacon is a handful of lines of code in each of the five samples. The work is done before the user has clicked anything. The user does not see any of it.
The Identity Call
chrome.identity.getProfileUserInfo() is a documented Chrome API. When an extension has the relevant permission and the user is signed in to a Google profile, the call returns the profile's identifier and email address. The API exists for legitimate reasons – an extension that wants to know who it is running for, without rebuilding identity from scratch, can ask the browser.
In these samples, the API became part of the exfiltration path.
chrome.identity.getProfileUserInfo()
-> info.id -> head["profileId"]
-> info.email -> head["profileEmail"]
-> JSON-encode head
-> fetch the notify endpoint
The implementation is small. Three or four lines of code. The technical surface the call uses is mundane. What is not mundane is the use: a wallet-extension trojan does not need the Google profile identifier to do wallet things. The presence of the call is the point.
A wallet extension reading wallet storage is what a wallet extension does. A wallet extension reading Chrome's profile identity is a different statement about what the operator wanted to know.
The Extension Boundary
A web page running in a browser tab cannot call chrome.identity.getProfileUserInfo(). Chrome refuses. The API is exposed to extensions only, and only to extensions that declared the identity permission in their manifest at install time.
That is the boundary this attack crosses.
A phishing page can ask the user for their Google credentials, but it has to ask. It has to render a login form, convince the user to type into it, and rely on the user not noticing the address bar. Every step in that chain is visible to a moderately careful user, and most of it is mitigated by browser security telemetry built specifically for this attack surface.
An extension does not need to ask. An extension that already has the identity permission – granted when the user installed it, recorded in the manifest, never re-prompted afterwards – can read the profile identifier and email any time it wants. There is no consent ceremony at the read site. There is no user-visible event. The extension simply calls the API and the browser hands back the answer.
That is what these five trojans are taking advantage of. Not a Google vulnerability. Not a Chrome vulnerability. Not a phishing pattern. A trust-zone difference.
A wallet or password-manager extension is, structurally, the perfect host for an attack like this. Users install these extensions specifically because they want them to handle sensitive material – seed phrases, passwords, session tokens, signing keys. The user's mental model of the extension is this is the thing inside my browser that handles secrets. The extension's manifest declares broad permissions, and the user accepts them at install time, because accepting them is the entire reason the extension exists.
Once installed, the extension lives inside the browser's privilege envelope. It can read DOM contents on pages it has permissions for. It can read its own extension storage. It can call any documented Chrome API it has the matching permission for, including chrome.identity. The user's only ongoing consent ceremony is the act of leaving the extension installed.
A malicious page has to cross into execution. A malicious binary has to land on the disk. A malicious extension, once installed, is already inside.
The extension was not outside the browser trying to steal identity. It was inside the browser asking for it.
That is also why the defensive picture is different. The remediations for malicious pages are well-developed: certificate transparency, browser-side phishing detection, password-manager autofill checks, browser-managed sign-in flows. The remediations for malicious binaries are well-developed: EDR, application allowlisting, signature scanning. The remediations for malicious extensions in trusted brand-name wrappers are weaker, because the wrapper is doing what the user expected the wrapper to do – it is the inside of the wrapper that turned.
Two Endpoints, Two Jobs
/inz-notify is not the wallet-data channel. It is the identity beacon.
The wallet-data channel is a different endpoint on the same ping base URL: ${ping}/inz. Same wrapping shape, same head object, plus a body field that carries the wallet payload – seed material, keystore data, extension storage, transaction artifacts. /inz-notify runs once per session, on extension load. /inz runs whenever there is wallet activity worth sending.
The two endpoints are doing different jobs.
/inz-notify answers: who is this browser?
/inz answers: what did this browser just give us?
The first call labels the session. Every later call rides under that label. The identity attribution does not have to repeat – the operator's server has already been told once who the browser profile belongs to, and every subsequent /inz POST from the same session can be correlated against the same identity by source.
That is what the one-shot guard is for. The operators are not interested in receiving the same Google identity from the same browser twice. One label is sufficient.
| Endpoint | First call? | Carries identity? | Carries wallet data? | Job |
|---|---|---|---|---|
/inz-notify | yes | yes | no | create identity label |
/inz | no | yes | yes | attach wallet artifacts to identity |
The Shared head
The head object is where all five extensions converge.
Each extension builds it from two sources. The manifest's __meta block provides the build-pipeline fields. The chrome.identity call provides the user-identity fields.
| Field | Source | What it is |
|---|---|---|
_V | manifest.commands.__meta | Campaign tag (same value across the five-extension cohort) |
client_id | manifest.commands.__meta | Build pipeline identifier with victim-username substitution |
installed_ver | manifest.commands.__meta | Decimal encoding the build date |
hash_mode | manifest.commands.__meta | Collection-mode flag |
ext_ver | added at runtime from manifest.version | Host-extension version |
profileId | chrome.identity.getProfileUserInfo() | Google profile identifier |
profileEmail | chrome.identity.getProfileUserInfo() | Google profile email address |
The operator sent the build markers and the user identity. The brand label was stripped before transmission. Whether because it was redundant or noisy, it was not part of the telemetry they chose to keep.
That stripping behavior is itself evidence of how this layer was designed. The trojan is not exfiltrating the manifest because it could. It is exfiltrating the parts of the manifest the operator left there as their own field, plus the parts of Chrome the operator wanted the answer to.
This is what makes the samples feel generated. Not identical as full extensions, but identical where the operator needed them to be identical. The host extension's source code – the wallet logic, the password-vault UI, the chain RPC calls – differs across the five. The five-line block that builds and sends the head object does not.
The brand changed across the five extensions. The head object did not.
The Build Marker
commands is a Chrome manifest object documented in the extension developer guide. Its purpose is keyboard shortcuts. A developer who wants users to be able to press Ctrl+Shift+L to lock the vault registers a lock_vault command under commands, declares a default key binding, and writes a handler that listens for chrome.commands.onCommand.
__meta is not part of that spec.
Chrome does not look at __meta. The Chrome extension parser ignores keys it does not recognize – it does not warn about them, does not strip them, does not surface them to the user. An extension can have any number of operator-private fields hanging off the commands object without affecting how Chrome treats the extension. The hide-spot is well chosen. commands is exactly the kind of object that looks expected inside a manifest. A random __meta block at the top level of the manifest would be more obviously out of place; __meta tucked inside commands is camouflage.
The fields the operators stashed there are not UX metadata. They are operator telemetry.
_V is the campaign tag. It is the same value across all five trojanized extensions, which is what makes it diagnostic: a researcher who finds a commands.__meta._V matching the documented value can be confident the extension belongs to this campaign cohort, independent of any other indicator.
client_id is a build pipeline identifier with a username component baked in. The username is substituted at build time, which means each build is personalized to its intended recipient. That fact is interesting in its own right: this is not bulk distribution. Each extension was built for a specific target.
installed_ver is a decimal number that decodes to a build date. The format is compact and obviously machine-generated, the kind of field that exists because someone wrote a build script and wanted to track when each artifact came off the line.
hash_mode is a collection-mode flag. Its values affect what gets sent on /inz. That makes it operator-facing control metadata, not extension functionality.
None of these fields exist to make the extension work. They exist to tell the operator's back office which build is reporting in.
The description field that the operator strips before send is the one field that would make sense as a user-visible label. The fact that it is stripped is the giveaway: the operator does not want the brand. The operator wants the build identity, which the brand label would only confuse.
The manifest was not only describing the extension to Chrome. It was describing the build to the operator.
For defenders, the detection rule that falls out of this is concrete: a browser extension whose manifest contains commands.__meta deserves immediate scrutiny. The Chrome extension spec has no use for that block. In this cohort, its presence is a signature, and finding it does not require behavioral detonation.
The Join Key
The cleanest way to think about what these extensions are sending is to think like the back office.
The operator does not receive identity material. The operator receives rows. Each /inz-notify POST is a row that says: a browser profile labeled <profileEmail>, with profile identifier <profileId>, belonging to campaign <_V>, under build <client_id>, on extension version <ext_ver>, came online at time <receipt>.
Each /inz POST is also a row, but a different kind. It says: a browser profile labeled <profileEmail> just produced a wallet artifact. Here is the artifact, in body. Here is the same identity envelope, in head.
The identity envelope is the join key. It is what lets the operator's database take the two kinds of rows and stitch them into a single record.
The first POST creates the row. The later POSTs fill it.
That is the data model the campaign was built to feed.
Every /inz POST carries the same head block, so the back office never has to maintain server-side session state to know which wallet artifact belongs to which profile. The artifact and the identity travel together, in every POST. Even if the operator's database resets, even if the C2 server reboots, every individual POST is self-attributing. The head block is the record's primary key.
This is what makes the /inz-notify design make sense as a one-shot. The identity row only has to be written once. The operator's database can deduplicate later identity POSTs against (profileId, _V) and discard them. Subsequent identity beacons would not add information – the profile is already labeled. The operator's job is to receive the data POSTs and attach them to the existing identity record by join key.
That is also what makes the campaign tag matter. _V is what lets the back office separate one campaign's records from another's. A single back office can serve multiple campaigns concurrently, because every record carries the campaign tag and gets sorted into the right pile on receipt. The same client_id distinguishes builds within a campaign. The same installed_ver distinguishes build dates within a client_id. The schema is hierarchical: campaign, then build pipeline, then build date, then profile, then artifact.
A wallet drain inside that schema is not an event. It is a row attached to a profile attached to a build attached to a pipeline attached to a campaign. The drain is the value. The schema is what makes the value addressable.
From a defender's seat, the data model has one useful implication. The campaign tag is the most diagnostic single field in the head block. A search for _V equal to the documented value across any captured POST body – on a victim machine, in a proxy log, in an outbound HTTPS body – will surface every belt-and-braces telemetry POST in the cohort, regardless of which of the five extensions sent it and regardless of which endpoint received it.
Why This Was More Than Wallet Theft
A wallet artifact is an object. A browser profile is a subject.
Wallet extensions hold the kind of material that lets an operator drain a wallet – seed phrases, keystore files, session state, transaction history. That material is enough on its own to monetize. Wallet theft is a complete attack pattern. It does not need anything else attached to it to be valuable.
The identity move attaches something else anyway. A wallet file on its own tells the operator what they took. A Google profile identifier attached to that wallet file tells the operator whose wallet they took.
The Google identifier is not stolen money. It is the index against which everything else can be looked up.
Why "Google Mirror" Still Fits
The mirror was not rendered in HTML.
It was returned by an API.
There was no need to draw a fake Google page when the extension was already running inside Chrome. The victim had already signed into a browser profile. Chrome already had the profile identifier. The extension asked for it, copied it, and sent it to the operator.
That is the mirror: not a page pretending to be Google, but Chrome reflecting the Google identity of the local profile into code the operator controlled.
The literal reading of the name – a Google login clone, a phishing page styled like Google's sign-in screen, an attacker-in-the-middle relay – is a real attack pattern. It is not what the trojanized extensions are doing. The mirror they constitute is a behavioral one. The reflection happens at the API boundary, not the user interface.
The name still fits because the result is the same. Whatever path the identity takes to leave the browser, the operator ends up holding the same data: the Google profile identifier and email address of the user the browser belongs to. The relay path is different. The output is identical.
The Browser Profile as a Target
A browser profile is not only a place credentials get stored. It is the binding layer for an entire developer surface.
A developer profile typically binds extensions, cookies, saved sessions, OAuth state, source-control tokens, cloud-dashboard logins, and messaging-client web sessions, all under one Google account identity. The profile is the seam where the work the developer does in a browser meets the identity the rest of the internet knows them by.
Identifying the profile collapses that geometry into a single label.
A stolen wallet artifact is hard to attribute to a person. Wallet addresses are pseudonymous. Browser storage is bound to a profile but the profile, in turn, can be anonymous from the outside.
A Google profile identifier is not just another browser artifact. It is a stable account label assigned to the signed-in profile. In many cases, the email address attached to that profile is enough to connect the browser to a person, an employer, a recovery path, or another account system. From the operator's seat, that label is the difference between we have a wallet and we have a wallet tied to this browser identity.
That distinction is what makes the Google Mirror its own layer in the operation rather than a wallet-theft footnote. Wallet theft monetizes the artifact. Profile identification monetizes the attribution.
How It Fits the Operation
The campaign stack across the series so far:
| Layer | What it targeted | Output |
|---|---|---|
| BeaverTail | saved workstation data | files, credentials, tokens |
| OtterCookie | active workstation activity | clipboard, screen, keystrokes |
| Google Mirror / extension layer | browser profile identity | profileId, profileEmail |
| Cash-out layer | financial value | covered in a later article |
Three of the four layers were already on the developer's machine in some form. BeaverTail came in through code execution. OtterCookie ran as a Node.js implant. The Google Mirror is the one that sat inside the browser itself, in extension code that the developer had every reason to think they had installed deliberately.
The four layers are not redundant. They collect on different windows of the same victim. The workstation, the live session, the browser identity, the financial flow downstream. The same operation runs all of them, against the same population, for different reasons.
Detection Opportunities
The useful detections sit between the browser and the identity provider, not inside either alone.
- Browser extensions invoking
chrome.identity.getProfileUserInfo()from a background context, particularly when the host extension's stated function does not require profile identity. A Bitwarden-themed extension callingchrome.identity.getProfileUserInfo()on load and then POSTing the result to non-vendor infrastructure should be treated as malicious until proven otherwise. Wallet extensions sometimes touch identity APIs for in-app account management, so the signal there needs context: is the call tied to a user-visible feature, or does it run on extension load without any UI trigger? - Extension background scripts POSTing URL-encoded request bodies with a
headfield carrying profile metadata, to non-vendor infrastructure. - Browser extensions making outbound HTTPS calls to
fullnode.mainnet.aptoslabs.comor other public chain RPC endpoints from a background context. A Bitwarden-themed extension calling Aptos mainnet is immediately suspect. Wallet-themed extensions calling Aptos require context: is Aptos a supported chain in the extension's published feature set, is the call tied to a wallet operation, and what is the result being used for? If the result is feeding afetch()against an unrelated host, the call is not doing what the wallet vendor would do. - A
commands.__metablock in a browser extension manifest. Thecommandsobject is a documented Chrome API for keyboard shortcuts.__metais not part of the spec. Its presence insidecommandsis a high-signal anomaly. In the five-extension cohort described here, it is a build-pipeline marker, not a user-facing shortcut configuration. - Newly installed or side-loaded extensions appearing on developer machines after a technical-interview or code-review interaction.
- Browser profile identity access correlated in time with wallet-data access.
- Enterprise allowlisting gaps for browser extensions on developer endpoints.
The commands.__meta rule is the tightest of these. In the five-extension cohort described here, commands.__meta is a build-pipeline marker the operators wrote into the extension. Defenders can scan installed extension manifests for that block directly; no behavioral analysis is required.
The Browser Was Also an Identity Sensor
Part VIII ended at the live workstation.
Part IX ends inside the browser profile.
In this campaign, the developer's machine was one boundary. The developer's identity was another. The operators built for both.
A wallet trojan that asks Chrome who owns the profile is not a wallet trojan with an extra field.
It is a wallet trojan with an index.
The wallet artifacts answer what can be taken. The profile identity answers where the rest of the victim graph begins.
The investigation continues.
Share this article
Help spread the word about security best practices.
Related Articles
A Fake Coding Interview Is an Execution Request: Developer Safety Checklist
A coding interview repo is a request to run unknown code on a machine that holds your browser sessions, SSH keys, GitHub tokens, and cloud credentials. This checklist covers what to check before the call, what to look for in the repo, and what to do if you already ran it.
Hunting Lazarus Part VIII: OtterCookie
OtterCookie is a separate JavaScript / Node.js RAT running beside BeaverTail in the Contagious Interview operation. Its Socket.IO control plane maintains a live roster of connected developer workstations and broadcasts it on a thirty-second clock. Part VIII breaks down the protocol, the collection profile, the uid/userKey batch labels, the npm and Vercel delivery layer, and the operational shift from stored-data theft to live surveillance of developer machines.
Need Security Help?
Our team can help secure your blockchain, web applications, and infrastructure.