Skip to content
D6c049a09ba8ac3085c51ee9df24a4e5

D6c049a09ba8ac3085c51ee9df24a4e5

gist.github.com • November 4, 2026

From 2026-09-27, four official JavaScript files on Level Infinite's CDN contained attacker code. The NIKKE PC launcher loads these files. From Oct 23 at the latest, opening the launcher could install a remote-control agent: while the launcher was open, the attacker could run any command on your machine, read any file and take screenshots.

Update, 2026-11-03 22:40 GMT: three of the files were replaced on Nov 3 at 17:04, 17:13 and 17:21 GMT. The fourth, FeedList.feb647c9.js , was cleaned at the origin on Oct 28, but two CDN servers still delivered the modified copy at 22:26 GMT. The attacker's server no longer answered at 18:31 GMT. None of this undoes anything on machines that were already infected, and the old files may still be in your launcher's cache. Do not open the NIKKE PC launcher until Level Infinite confirms the fix, and clear its cache before you do.

This report comes from one infected machine, plus findings from other victims posted in the . The payload was analyzed statically and never executed. Everything below was checked against the downloaded files, the launcher's logs and caches, the decompiled payload and the live servers. Findings from the are marked as reported when they could not be checked here.

Last updated: 2026-11-04, 02:00 GMT. Added in this update: a DNS log can miss the agent's lookups when the PC also has a second DNS server (luminesce01); the earliest recorded insightjs.com lookup, Oct 5 07:09 GMT (luminesce01); an older build still served on Nov 1 at 18:03 (axm2). Earlier at 01:10 GMT: a fifth payload build, Nov 3 12:31:04 (vvs); the earliest reported agent start is now Oct 23 16:40 GMT (zbeosf). Earlier at 00:45 GMT, corrected: creds decrypts the NIKKE login that the launcher saved, not Windows Credential Manager ( keyscan reads Credential Manager, for developer-tool entries only); wallet_files only reads Chrome's profiles; Korean translation added. Earlier at 00:30 GMT: 1984 ehf told a reporter that it shut down 89.127.233.127 (seth12619); how to measure what was sent with SRUM (rnikontas, asyadayo); what the agent does on its own and what the server decides. Earlier at 22:40 GMT: the Chrome DLL can be installed without Chrome (checked in the code); a second attacker IP, 89.126.250.172, used until Oct 21 (D-Maledicte); a fourth payload build, Nov 2 17:48:47 (malevolenttortoise, Iwassoobsessedwith); all agent traffic goes through the launcher's embedded browser; startup already in progress is not a reliable sign either way. Earlier at 20:40 GMT: a fourth modified file, vue-bucket.min.js (mikansubre); agent starts from Oct 24, not Nov 1 (malevolenttortoise, CuauhtliHC); at least three builds of the payload (neutralnikke, confirmed here); what the Chrome DLL does and why it must be removed after a restart (cheejordan). Earlier in the day: a third modified file, FeedList.feb647c9.js (found by RichyRich515 and Enerccio in the , confirmed here); the payload served earlier than Nov 2-3; debug.log is appended to, not overwritten on each launch; collect_cookies: false does not mean cookies were safe. Earlier corrections: the PlatformExperienceHelper folder itself is normal Chrome (only a 64-hex subfolder with the DLL is malicious), and onominan.tmp is not a malware marker.

Check if you are infected

What the attacker can do on an infected machine

Linux and Steam Deck (Proton)

Indicators of compromise

Check if you are infected

Open your NIKKE install folder.

Open Launcher/intl_service/debug.log in a text editor.

for k2f_chan .

If you find it, the agent ran on your machine, even when the adopted or resuming line after it is missing (that line comes from l.js once the server answers). An infected log looks like this:

Launcher/log/tinyget/tinyget.*.log also records the download of . Some infected launches also log startup already in progress just before the download, but others do not, so for k2f_chan registered .

Two quick checks outside debug.log : the loader entries in %APPDATA%\nikke_launcher\tbs_cache\Code Cache\js\ have the same file names on every machine ( dd65fa4d61079e7e_0 aegis, ce2fd3f1007a9fe9_0 images, 1416c638b3830103_0 feedlist, fd520c7c0f65c2d7_0 vue), and in DNS or router logs a lookup of insightjs.com means the loader ran, while a lookup of analytics.insightjs.com most likely means the agent stage was reached. A DNS log only sees the lookups sent to it: on one infected PC that also used an IPv6 DNS server from the router, the Pi-hole log had no analytics.insightjs.com at all (reported), so a log without it does not rule out the agent.

The launcher appends to debug.log : on the analyzed machine it held all 9 launches from Oct 29 to Nov 3. It is gone if the NIKKE folder was deleted or reinstalled. FETCH_LIST suc success lines come from the normal FeedList module and appear on clean launches too, so they are not a sign.

The agent also saves its state in the launcher's Local Storage, outside the NIKKE folder: the files in %APPDATA%\nikke_launcher\tbs_cache\Local Storage\leveldb\ for k2f.agent . On Windows, also check for the persistence files listed in What to do .

Do not open the NIKKE PC launcher until Level Infinite confirms the fix.

Before you open it again, delete the launcher's cache folder %APPDATA%\nikke_launcher\tbs_cache (inside your prefix on Proton). It may still hold the modified files from before the fix.

Block these in your hosts file, router or firewall: insightjs.com analytics.insightjs.com insightjs.org 89.127.233.127 89.126.250.172 This only stops the current server. How the attacker got write access to Level Infinite's CDN is not known yet, so they may be able to try again with a new domain.

Close the launcher, or disconnect from the internet, so the attacker loses access.

On Windows, remove the persistence: First delete HKCU\Software\Classes\CLSID\{E77CC89B-7401-4C04-8CED-149DB35ADD04} if it exists. The same CLSID also exists under HKLM on every Windows install, pointing to wpc.dll (WindowsParentalControls). Leave that one. Only the HKCU copy is malicious. Restart Windows. The DLL stays locked while it is loaded, even with every Chrome window closed (reported). If the key exists with an empty default value, delete it anyway: that is what the agent's own chrome_uninstall leaves behind. Then open %LOCALAPPDATA%\Google\Chrome\User Data\PlatformExperienceHelper\ and delete any subfolder whose name is a 64-character hex string and that holds platform_experience_helper.dll . Do not delete the whole folder: Chrome creates PlatformExperienceHelper itself for its own platform_experience_helper.exe and an empty LoadFeatures file, so the folder on its own is normal. Microsoft Defender Offline did not detect the DLL on one machine (signatures 1.463.110.0), so a clean antivirus scan does not mean it is gone. Reinstalling Windows is the safest option, because the attacker could run any command while the agent was active.

First delete HKCU\Software\Classes\CLSID\{E77CC89B-7401-4C04-8CED-149DB35ADD04} if it exists. The same CLSID also exists under HKLM on every Windows install, pointing to wpc.dll (WindowsParentalControls). Leave that one. Only the HKCU copy is malicious.

Restart Windows. The DLL stays locked while it is loaded, even with every Chrome window closed (reported).

If the key exists with an empty default value, delete it anyway: that is what the agent's own chrome_uninstall leaves behind.

Then open %LOCALAPPDATA%\Google\Chrome\User Data\PlatformExperienceHelper\ and delete any subfolder whose name is a 64-character hex string and that holds platform_experience_helper.dll . Do not delete the whole folder: Chrome creates PlatformExperienceHelper itself for its own platform_experience_helper.exe and an empty LoadFeatures file, so the folder on its own is normal.

Microsoft Defender Offline did not detect the DLL on one machine (signatures 1.463.110.0), so a clean antivirus scan does not mean it is gone.

Reinstalling Windows is the safest option, because the attacker could run any command while the agent was active.

From a clean device, change your passwords: NIKKE / Level Infinite first, then email, Discord, banking and every password saved in a browser. Turn on two-factor authentication.

Log out of all sessions (Discord, Google, Steam, GitHub and others). The agent can steal browser cookies, which work without your password.

Revoke and regenerate API tokens, SSH keys and any developer credentials (GitHub, npm, AWS, OpenAI, Anthropic and others).

Move crypto wallet funds to a new wallet with a new seed phrase.

What the attacker can do on an infected machine

The agent is a remote-access tool controlled by the operator. It does not only run a fixed stealing routine. While the launcher is open, the operator can send these commands:

exec alone means the operator can do anything your user account can do: install other malware, read any document or change files. The other commands are shortcuts.

The Lua agent does nothing on its own. It registers k2f_chan with the launcher's comm_center and then only answers open , read , write , ping and close requests. Every command comes from the server through l.js . Victim data shows that the first commands arrive within seconds of the agent starting: one Chrome DLL was written 3 seconds after the start, and the launcher's embedded browser sent 0.9 MB and 16.6 MB in the hour of the start on two machines (reported, SRUM). So a first collection seems to run automatically on every new agent, and missing temp files or a collect_cookies: false config do not mean nothing was taken. whoami reports the launcher's integrity level and whether it runs elevated; a victim reports that the launcher asks for administrator rights by default, and an elevated launcher lets browserscan use the SYSTEM token for Chrome and Edge app-bound encryption.

To see how much left a Windows PC: SRUM ( C:\Windows\System32\sru\SRUDB.dat ) keeps bytes sent per program per hour. Open it as administrator with NirSoft NetworkUsageView, or copy it and parse it with SrumECmd, then compare the hours of the agent starts with the usual values for tbs_browser.exe . All agent traffic goes out under that process, not under the game.

Only exec starts new programs (it is the only module that loads win32.exec and CreateProcessW ). The other commands run inside the launcher process through LuaJIT FFI, so Prefetch and process-creation logs cannot show them. The agent stays active as long as the launcher is open, and the launcher often stays open in the notification area while the game runs: one reported agent checked in 22 times over 9 hours.

None of these commands leaves an entry in the launcher logs. A victim cannot tell from the logs which commands were run, so assume the worst.

Linux and Steam Deck (Proton)

Proton does not protect you.

The automatic scans look for Windows paths, so they find little in a Proton prefix. On the machine analyzed here, the only target present inside the prefix was the NIKKE launcher data.

The remote commands are a different matter. Wine maps your Linux folder and / as drive letters ( Z: at least). It also passes your environment variables to the game, and the agent reads all of them. With exec , ls , cat and download , the operator can read anything your Linux user can read: ~/.ssh , browser profiles, shell config files with API keys, password stores and documents. The Windows Chrome persistence does not apply on Linux, but treat every secret in your folder and environment as exposed.

Stage 1: modified official files

Code was added to four files on Level Infinite's CDN:

Modified versions (the first two fetched on Nov 3 before the fix, the third on Nov 3 at 18:57 GMT, the fourth recovered by mikansubre from a launcher HTTP cache; size decompressed):

Clean versions (no loader and no reference to insightjs ):

All versions are served by tencent-cos . Which copy of FeedList.feb647c9.js a launcher gets depends on the CDN server it reaches. On Nov 3 between 18:57 and 18:59 GMT, 43.152.43.115 (the address Cloudflare and Google DNS returned to the analyzing machine) served the clean copy. 43.175.144.148 and 43.175.145.181 served the modified copy, SHA-256 2f2aac505f486067af6831d6c75ea74df63acb1ed4e4002709c5962c68bd9203 , from a cache dated Sep 30, and returned it for a cache-busting query string too. images.7fec4c32.js was clean on all three servers. At 22:26 GMT, FeedList.feb647c9.js was still modified on the same two servers, for both na-webassets and sg-gpts (the sg-gpts copy was modified at 06:42:09 on Sep 27 and cleaned at 15:38:49 on Oct 28, reported). The clean copy restored on Oct 28 has the same ETag as the Jul 23 original. In late October some CDN servers still had pre-attack copies of FeedList.feb647c9.js and vue-bucket.min.js (the analyzed machine's cache holds the pre-attack vue-bucket.min.js , ETag 4d0dc9cc23650db9631a2280cfa50186 ), so which loaders a launcher ran depended on the server it reached. At the same check and vue-bucket.min.js was clean on all three (SHA-256 459343517635a96caa109b050361b4d32393e8727b2f54b44749db783feb4199 ). The modified vue-bucket.min.js has SHA-256 3aaafbd612f04501a6fdb0bb8b2f8a87796223e5584ac18ea5eb180feeafb193 .

The injected code hides the URL in an integer array ( new TextDecoder().decode(new Uint8Array(new Uint32Array([...]).buffer)) ). It adds a tag, then removes it from the page after loading. If insightjs.com fails, it retries with the TLD replaced by .org (built with String.fromCharCode(111,114,103) in FeedList.feb647c9.js ). In FeedList.feb647c9.js the 708 added bytes sit just before the module's final export{...} statement, so a check that compares only the end of a file misses them.

The added code logs nothing itself. It loads l.js through a tag and moves to insightjs.org when insightjs.com fails or does not answer within 30 seconds. The empty-source lines in debug.log ( startup already in progress , k2f_chan registered , adopted ) come from l.js . startup already in progress has only been seen while a payload download was in progress (on the analyzed machine at 01:47:35, one second before the download), but several infected runs do not have it.

The launcher's V8 code cache ( tbs_cache\Code Cache\js\ ) stores the scripts under keys such as , with as origin. The analyzed machine had the aegis and images keys. Other victims reported ?f=feedlist and ?f=vue as well. The launcher's HTTP cache ( tbs_cache\Cache\data_* ) can also hold the modified files themselves: on the analyzed machine it held the modified aegis-fix.min.js and images.7fec4c32.js .

Stage 2: payload download

l.js calls the launcher's own JS bridge ( Srv_DownloadFile ) to fetch:

Victims ran different builds of the same size. The launcher's tinyget.mem.log keeps the response headers, and nginx builds the ETag from the file's modification time and size in hex. Five uploads are known: Nov 1 18:08:55 (ETag 6ae78037-3214f , reported by neutralnikke and malevolenttortoise), Nov 2 17:48:47 ( 6ae8ccff-3214f , reported by malevolenttortoise), Nov 2 21:48:40 ( 6ae90538-3214f , on the analyzed machine, also reported on another machine at 10:58 GMT on Nov 3), Nov 3 12:31:04 ( 6ae9d408-3214f , reported by vvs, downloaded at 14:19 GMT) and Nov 3 14:56:40 (the published sample, ETag 6ae9f628-3214f by the same rule). Agents started from Oct 23, so older builds exist whose headers nobody has posted. One of them was still served on Nov 1 at 18:03:18, 5 min 37 s before 6ae78037 was uploaded (axm2, reported). The same builds reached Windows and Proton machines, so they are not per OS. The hash above therefore does not match every infection. tinyget.mem.log is a fixed-size buffer rewritten from the start at each launch, so the end of the file can still hold an older session's headers.

The launcher saves the payload under a misleading .log name in Launcher\log\tinyget\ , runs it, then deletes it. On early runs reported on Oct 24 it was saved as Launcher\log\tinyget\k2f.lua .

An earlier version of this report listed a 22-byte hex file in %LOCALAPPDATA%\Temp\ ( onominan.tmp ) as a marker. That was wrong. The same file name with the same kind of content exists in Delta Force Wine prefixes dated 2026-09-20, before the CDN files were changed, so it comes from Tencent software and does not show infection.

Format: LuaJIT 2.0 bytecode (32-bit, stripped), obfuscated with Prometheus. The strings are stored in a shuffled table with custom base64 and base85 encoding.

Internal build name: K2F_LUA_BUILD .

Windows access through LuaJIT FFI: CredEnumerateW , CryptUnprotectData , NCryptDecrypt , BCrypt* , Reg* , CreateProcessW , CreatePipe , GetEnvironmentStringsW , GetLogicalDrives , GetAdaptersInfo , GDI+.

Protocol: JSON requests over k2f_chan ( {op: "open", cmd: " ", arg: {...}} , then read and write on a session ID). Output is returned base64-encoded in chunks of up to 2 MiB.

State: the agent stores k2f.agent.state in the launcher's Local Storage (origin , tbs_cache\Local Storage\leveldb\ ). On the analyzed machine it held agent_id (the same UUID as the adopted log line), agent_pass , c2s_counter: 4 , an s2c_counter that looks like a microsecond timestamp (2026-11-03 00:47:37 GMT), and this config: poll_interval_seconds: 1800 , jitter_seconds: 360 , collect_cookies: false . The agent checks in with the server every 30 minutes, give or take 6. The s2c_counter value, 1793666857535416, is 2026-11-03 00:47:37.535 GMT in microseconds, two seconds after the agent started. Older values of the key stay in the LevelDB .log file until LevelDB compacts it. Victims' files show c2s_counter going up by one per check-in, and the server switching the interval between 600, 1800 and 900 seconds, at the same minutes on unrelated machines, so it is one setting for all agents (reported in the ). On Oct 24 the interval was 600 s with 150 s of jitter. The same s2c_counter value on two machines suggests it is the time of a server message, such as a config update, not of each check-in. The Lua payload never reads or writes the two counters, so they are handled by l.js , and what makes c2s_counter go up could not be confirmed from code.

Network: the Lua agent has no network code. It loads only advapi32 , bcrypt , crypt32 , gdi32 , gdiplus , iphlpapi , kernel32 , ncrypt , psapi , shcore and user32 , and only answers requests on k2f_chan . Every check-in and every upload goes out through l.js in the launcher's embedded browser ( tbs_browser.exe on Windows), so firewall, proxy and SRUM records show that process. Victims reported uploads of 947 KB within seconds of an agent start and 146 MB in one hour (reported, SRUM).

browserscan sends the rows of the browser databases (site, user name or cookie name, encrypted value) and the keys, and the server decrypts them. For Chrome and Edge app-bound encryption it first tries the SYSTEM token of services.exe , which only works when the launcher runs as administrator. Firefox key4.db and logins.json are sent whole. Its temporary copy %TEMP%\k2f-bcreds-*.db is deleted right after use, and so is each k2f_screen_*.jpg , so finding none proves nothing. payload_cleanup deletes tinyget*.log and k2f*.lua files in log\tinyget that start with LuaJIT bytecode ( 1B 4C 4A 01 ).

chrome_install gets the DLL from the server as base64. Before writing it, the agent puts the agent ID after a /px/ placeholder and a 64-hex token over a BEARER000… placeholder, then saves it under a server-supplied SHA-256 folder name. Every installed DLL therefore has its own hash. The agent creates the folder itself and never checks that Chrome is installed. chrome_scan reports Chrome installs, profiles and extension IDs first, and wallet_files reads the extension IDs the server sends, so the server picks the targets.

collect_cookies is an argument of browserscan . When it is false, the cookie stores are skipped and saved passwords are read anyway. cat , download and exec can still copy a browser's Cookies file, so false in the saved config does not mean the cookies were safe.

Modules: base64 , json , channel , dispatch , commands.* , keyscan.* , win32.chrome , win32.creds , win32.exec , win32.extensions , win32.firefox , win32.fs , win32.hook , win32.registry , win32.screens , win32.sysinfo , win32.whoami .

Files the agent reads

The full list of 264 paths, plus browser subpaths, registry keys and file patterns, is in target-paths.md in this gist. Main groups:

SSH and remote access: ~\.ssh\* including known_hosts , PuTTY and WinSCP sessions in the registry, FileZilla sitemanager.xml and recentservers.xml

GPG: %APPDATA%\gnupg , private-keys-v1.d

AI tools: ~\.claude\.credentials.json , %APPDATA%\Claude\claude_desktop_config.json , $CODEX_HOME\auth.json , Gemini CLI oauth_creds.json , GitHub Copilot CLI, Cursor, Windsurf, VS Code ( state.vscdb , mcp.json ), Kimi, Qwen Code, OpenCode, Aider, Continue.dev

Cloud: AWS ( ~\.aws\credentials , SSO cache), Azure, Google Cloud, Oracle Cloud, DigitalOcean, Hetzner, Linode, Alibaba, Tencent, Huawei, Fly.io, Railway, Vercel, Netlify, Cloudflare Wrangler, Supabase, Neon, PlanetScale, MongoDB Atlas, Snowflake, Databricks

Developer tools: GitHub CLI hosts.yml , GitLab CLI, npm, pip, Poetry, Cargo, Composer, NuGet, RubyGems, Gradle, Maven, Docker and Podman auth.json , Kubernetes, Helm, Terraform, Pulumi, Stripe, Twilio, Sentry, Hugging Face, Kaggle

Shell history: PowerShell ConsoleHost_history.txt , ~\.zsh_history , fish, psql

Project folders: %USERPROFILE%\code , dev , source , Programming , Documents , Desktop , scanned for .env , appsettings*.json , application*.yml , docker-compose.yml , .mcp.json

NIKKE: %APPDATA%\nikke_launcher , %APPDATA%\nikke_launcher_hmt

Environment variables: 38 variables that point to config files, such as $CLAUDE_CONFIG_DIR , $GH_CONFIG_DIR , $AWS_SHARED_CREDENTIALS_FILE , $KUBECONFIG , $DOCKER_CONFIG

Crypto wallets: 28 browser extensions by ID, including MetaMask, Phantom, Coinbase, Binance, OKX, Bybit, Bitget, Trust, Rabby, Rainbow, Keplr, Solflare, Backpack, Tonkeeper, TronLink, Xverse, UniSat, Petra, Ronin and Magic Eden

Indicators of compromise

Four modified files on Tencent COS had last-modified Sep 27 2026, 06:41-06:42 GMT. aegis-fix.min.js , vue-bucket.min.js and images.7fec4c32.js were replaced on Nov 3 at 17:04, 17:13 and 17:21 GMT.

FeedList.feb647c9.js was replaced at the origin on Oct 28 at 15:46 GMT, but 43.175.144.148 and 43.175.145.181 still served the Sep 27 copy on Nov 3 at 22:26 GMT, for both na-webassets and sg-gpts , also for requests with a query string. Every CDN server needs a purge for this path, and the other paths should be checked on each server, not only on the one a given resolver returns.

Players who launched since Sep 27 may still have the modified files in tbs_cache , so a forced cache reset in the launcher update would help.

The attacker had write access to the CDN bucket, or to the pipeline that publishes to it. That access has to be found and closed. Otherwise a new loader with a new domain can be pushed at any time.

Other files in the same buckets may also be modified. They should be checked against build output.

The decompiled payload, the decoded string table and a password-protected sample are available to Level Infinite, hosting providers and security researchers on request. They are not posted here because they would give anyone a working stealer.