When "ollama.js" Isn't Ollama
- Threat Hunting
- Incident Response
I almost didn’t look twice at this one. The file was named ollama.js, and the machine it came off had a working Ollama install sitting right there. Everything lined up with someone setting up a local model and moving on. The only thing that didn’t fit was a winget command that had quietly installed Deno a few seconds earlier, and nothing about running Ollama needs a JavaScript runtime.
That winget call is the whole trick. It’s there so a script named ollama.js can load a Rust DLL into memory through Deno’s foreign function interface, strip every saved browser credential off the box, and leave a remote-controlled bot living inside the browser. The real Ollama is cover. You get a working tool, so you never go looking for the part that ran next to it.
This turned out to be a live build of the “Evil Deno” technique that Taggart Tech published as a proof of concept last year. The DLL it carries was compiled on 2026-06-22, so this isn’t someone replaying an old PoC. It’s fresh, and it’s being deployed.
How it lands
The entry point is a fake Ollama installer, a PowerShell script pulled from ollama.lnk/install.ps1. From what I can tell it was promoted through a YouTube video pushing fake ChatGPT and Claude tooling, the same lure pattern that’s been dropping Deno-based malware through fake AI installers for a couple of months now. No ClickFix prompt, no terminal paste. The victim just runs an installer for a tool they actually wanted.
That installer forks into two chains. One installs the genuine Ollama, so the machine ends up with a real, functioning app and the victim has no reason to suspect anything. The other pulls Deno down through winget and starts the infection. Both run from the same script, and that’s what makes the decoy work: the legitimate install isn’t a side effect, it’s part of the cover.
Once Deno is on the box, the malicious chain runs ollama.js.
The loader
The loader script is obfuscated: a single 2.7 MB base64 string plus the usual mangling where every API call is rebuilt from character codes. Strip that away and the logic is short. It base64-decodes the embedded blob, raw-inflates it into a PE32+ DLL, writes that DLL to %APPDATA%, then uses Deno’s foreign function interface (FFI) to load the file and call one exported function in-process. When the call returns, it deletes the DLL.
Here’s the flow with the runnable parts taken out:
| |
The FFI call is the entire reason to use Deno. Most loaders I take apart end up calling LoadLibrary or shelling out to rundll32 or regsvr32 somewhere, and those are the artifacts I pivot on. This one runs native code straight out of a signed, trusted JavaScript runtime. There’s no separate loader process to flag, and the DLL is already gone from disk by the time anything scans the folder.
If you want one behavioral tell, that’s it: deno.exe writing a PE into a user-writable directory and then loading it. That pattern has no legitimate reason to exist.
The stealer
The DLL is written in Rust, internal project name app_edge, and a leaked PDB path left the build user as plain old root. Its job is browser credentials.
It reads each browser’s Local State, pulls the encrypted key, and recovers it by defeating App-Bound Encryption (ABE), falling back to DPAPI when the newer scheme isn’t there. The strings left in the binary read like a developer talking to himself, things like "trying to get comet v10 key" and "v20 dpapi key empty, trying v10 fallback." To reach the locked cookie and credential databases, it force-closes the browser with taskkill first, then decrypts everything with AES-GCM using the recovered keys.
The target list is Chrome, Edge, Brave, and Perplexity Comet. The Comet entry is the part that made me stop. An AI browser showing up as a first-class credential target, with its own dedicated key-recovery path in the code, is new to me. I expect it to become normal fast.
The bot in the browser
Stealing saved passwords is the loud part. The quiet part is what the DLL drops next.
It unpacks an MV3 browser extension into %APPDATA%\ext\ and registers a native messaging host, which is the piece that turns a browser extension into something much worse. The native host bridges the extension to PowerShell through a run.bat that launches a roughly 2 MB obfuscated script, so commands coming from the extension can reach a real shell on the host.
The extension itself is a full modular bot. It talks to a WebSocket C2 and reconnects every five seconds. Decoding the background script, the command set covers:
- Cookie theft exported in Netscape format
- Interval screenshots through the browser’s own capture API
- Web injects, form-grabbing, and payment-card capture
- A remote shell through the native host
- Targeted file exfiltration
- Basic host recon
- Self-update
It pulls per-victim config and DPAPI keys down from the C2.
So this isn’t a smash-and-grab stealer that runs once and leaves. It’s persistent remote access living inside the browser, with cookie and session theft that walks straight past MFA on any session that’s already logged in.
The PowerShell backend
I went back for the PowerShell, and it turned out to be where most of the actual theft happens. The native messaging host runs zqpbm.ps1, and reading it took two passes. The `<#...#>` comment injection strips out to about 640 KB of arithmetic char-code assembly, that decodes to a second inner array, and the second array finally yields around 26 KB of clean PowerShell. Two layers to peel before anything is readable.
The first thing the cleartext changed my mind about was the C2 layout. The WebSocket at qweuyquigwebq[.]com isn’t the exfil channel; it only carries commands. The stolen data goes somewhere else entirely, over plain HTTP to adyitegqbk[.]com:
/gatetakes the browser database ZIPs and extension data/filegrabbertakes targeted file grabs/api/config/extensionshands back a server-controlled list of extension IDs to loot
There’s also a bestgrillde31[.]com reference left commented out, an earlier backend the developer never cleaned up. Every POST carries the machine’s MachineGuid, read from HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid, as an id header, which is how the operator keys victims apart.
What the script does is broader than the extension let on. It copies the Web Data and Login Data SQLite files out of every Chrome, Edge, and Brave profile across every user account on the machine, opening them with FileShare read-write so locked files don’t stop it, then ZIPs and ships them. It pulls that server-configurable extension list and loots Local Extension Settings and IndexedDB for each ID, which is built for draining crypto wallets. The file grabber supports full recursive globbing and uploads matches per path. And it runs a hidden interactive shell, PowerShell or cmd, with an async stderr reader so the pipe doesn’t deadlock. That last detail is the kind of thing you only add after you’ve watched a shell hang on you, and it’s part of why this reads as a mature backend rather than something thrown together.
One more thing fell out of the cleartext. The developer left Russian comments in place, #читать из экста + писать в экст (“read from ext + write to ext”) and #сепаратор (“separator”), both pointing to a Russian-speaking author. I haven’t found any public reporting on these domains or this build, so as far as I can tell the IOCs here are first-seen.
All four domains are fresh, and the two HTTP backends go on my block list next to the WebSocket C2. Given how clean the engineering is across the whole chain, I doubt this is the last build I’ll see from whoever wrote it.
Indicators of compromise
Network
| |
Files
| |
Registry (Native Messaging host)
| |
Host artifacts
| |
Hashes
| |
Resources
- Evil Deno: Abusing the Nicest JavaScript Runtime (Taggart Tech, May 2025): The proof of concept this sample weaponizes, including the FFI and native-DLL load tricks.
- Fake ChatGPT and Claude installers are dropping Deno RAT malware (Help Net Security, May 2026): Documents the same trojanized-AI-installer playbook, including the YouTube channels driving traffic and the Deno-based RAT it delivers.
- How Infostealer Malware Bypasses Chrome’s App-Bound Encryption (SpyCloud, February 2025): Background on why the ABE-to-DPAPI fallback in the Rust stealer still works.