logs-bin and logs-dump: Republished Keylogger Packages on npm

WindowsmacOSLinux

IOC Hunt medium Contagious InterviewSupply Chain

Two npm packages published by one maintainer that republish the node-global-key-listener library under new names, one of them carrying a Windows binary thirty-two times the size of the legitimate one it replaces.

What this hunt looks for

A May 2026 researcher post named logs-dump and logs-bin as a live DPRK keylogger on npm. The registry confirms one maintainer, topnotcher847, publishing both on the same day in June 2025 under bland descriptions. Both republish the node-global-key-listener library, its build output and its three key-server binaries, under their own names rather than depending on it.

The packages are not equally interesting. Every binary in logs-dump, and the Linux binary in logs-bin, is byte-identical to upstream. Only logs-bin replaces the Windows binary, at 75 MB against upstream's 2.3 MB, thirty-two times the size, which a rebuild of the same source does not produce. That file is the one indicator worth alerting on.

The obvious mistake is hashing both packages' binaries. The researcher cites an antivirus detection on WinKeyServer.exe without saying which copy was scanned, and logs-dump's copy is the upstream artefact every legitimate consumer installs. A keylistening binary trips antivirus honestly, so the score says nothing about DPRK involvement.

Technique
Supply-chain compromise through a public package registry (MITRE ATT&CK T1195.002), delivering an input-capture capability (T1056.001) to a developer workstation.
Data sources
Package-manager install logs and lockfiles, Software composition analysis and SBOM inventory, EDR file-write and process-execution telemetry, Proxy logs for registry.npmjs.org
Last updated
2026-10-04

Indicators

The atomic values this hunt matches on, listed separately from the queries so they can be lifted into tooling that speaks none of the query languages below. Every value here is reported by one of the sources cited at the foot of this page.

TypeValueNote
npm package logs-dump Published 2025-06-04 by topnotcher847. Two versions, latest 1.7.5. Republishes node-global-key-listener in full; its binaries are byte-identical to the legitimate upstream release, so the package name is the indicator and its file hashes are not.
npm package logs-bin Published 2025-06-04 by the same maintainer. Eight versions, latest 3.11.14. This is the package carrying a replaced Windows binary.
npm maintainer topnotcher847 The registry account behind both packages. Any further package published by it is worth treating the same way.
Email address josemalheiro105@gmail.com The maintainer address the npm registry publishes for that account. It is registry metadata rather than a value recovered from the payload.
SHA-256 8587edcb8c9e384a7c3b4726a23e93d0bd2fdd2267984a7982484ee327983902 bin/WinKeyServer.exe from logs-bin@3.11.14, 75,105,792 bytes. This is the only binary in either package that differs from the legitimate upstream release, and it is thirty-two times its size. Alert on this one.
File path node_modules/logs-bin/bin/WinKeyServer.exe, node_modules/logs-dump/bin/WinKeyServer.exe Where the binary lands after an install. The same file names appear under node-global-key-listener for legitimate consumers of that library, so the parent directory is what separates them.

Logic

Every query below implements these steps, so the same hunt returns the same result in each product.

  1. Collect: file and process events on Windows, macOS and Linux; proxy requests; packages, hashes and lockfiles via osquery and YARA
  2. Match: the SHA-256 8587edcb... of the replaced WinKeyServer.exe from logs-bin@3.11.14 is written, run or found on disk (alert on this alone)
  3. Match: or a file lands under node_modules/logs-bin or node_modules/logs-dump, or either package is installed (exposure only)
  4. Match: or a proxy request fetches either package from registry.npmjs.org, or a lockfile or manifest names either package (exposure only)
  5. Report: devices with the signal kinds seen, first and last time, newest first

Queries

Defender / Sentinel KQL

Written for this site, from indicators reported by npm

Alert on the SHA-256 hit alone, and read a path or registry row as historical exposure to investigate.

// logs-bin and logs-dump: the replaced binary by hash, optionally the package paths and registry downloads
union
  (DeviceFileEvents
   | where Timestamp > ago(90d)
   | where SHA256 == "8587edcb8c9e384a7c3b4726a23e93d0bd2fdd2267984a7982484ee327983902"
        // optional: logs-dump's binaries equal upstream, so a path hit shows exposure only
        or FolderPath matches regex @"(?i)[/\\]node_modules[/\\]logs-(bin|dump)[/\\]"
   | project Timestamp, DeviceName, Signal = "file", Detail = FolderPath),
  (DeviceProcessEvents
   | where Timestamp > ago(90d)
   | where SHA256 == "8587edcb8c9e384a7c3b4726a23e93d0bd2fdd2267984a7982484ee327983902"
   | project Timestamp, DeviceName, Signal = "process", Detail = FolderPath)
  // optional: a registry download shows exposure only
  , (CommonSecurityLog
   | where TimeGenerated > ago(90d)
   | where RequestURL contains "registry.npmjs.org/logs-bin" or RequestURL contains "registry.npmjs.org/logs-dump"
   | project Timestamp = TimeGenerated, DeviceName = SourceIP, Signal = "registry", Detail = RequestURL)
| summarize Signals = make_set(Signal), Events = count(), Examples = make_set(Detail, 5), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by DeviceName
| sort by LastSeen desc
Lower-fidelity parts (2)
  • the node_modules/logs-bin and logs-dump path match — logs-dump's binaries equal the upstream release, so a path hit shows exposure to a deceptive package, not DPRK involvement. Remove: delete the `// optional: logs-dump's binaries` line and the `or FolderPath matches regex ...` line below it.
  • the registry download match — a download shows exposure only and the packages date from 2025. Remove: delete the `// optional: a registry download` line and the `, (CommonSecurityLog ...` branch that follows it, up to the line before `| summarize`.

Splunk SPL

Written for this site, from indicators reported by npm

Matches the hash on file and process events and optionally the package path and proxy download, one row per device.

```index=endpoint holds EDR file and process events with the Endpoint fields, index=proxy holds proxy events```
(index=endpoint OR index=proxy) earliest=-90d
| eval path=replace(lower(file_path), "\\\\", "/"), web=lower(url)
| eval signal=case(
    lower(file_hash)="8587edcb8c9e384a7c3b4726a23e93d0bd2fdd2267984a7982484ee327983902", "hash",
    ```optional: logs-dump's binaries equal upstream, so a path hit shows exposure only```
    match(path, "/node_modules/logs-(bin|dump)/"), "package-path",
    ```optional: a registry download shows exposure only```
    match(web, "registry\\.npmjs\\.org/logs-(bin|dump)"), "registry",
    true(), null())
| where isnotnull(signal)
| eval detail=coalesce(path, web), device=if(signal="registry", src_ip, host)
| stats count AS events values(signal) AS signals values(detail) AS examples min(_time) AS first_seen max(_time) AS last_seen BY device
| convert ctime(first_seen) ctime(last_seen)
| sort - last_seen
Lower-fidelity parts (2)
  • the node_modules/logs-bin and logs-dump path match — logs-dump's binaries equal the upstream release, so a path hit shows exposure to a deceptive package, not DPRK involvement. Remove: delete the ```optional: logs-dump's binaries``` comment and the `match(path, ...)` line below it.
  • the registry download match — a download shows exposure only and the packages date from 2025. Remove: delete the ```optional: a registry download``` comment and the `match(web, ...)` line below it.

Sigma

Written for this site, from indicators reported by npm

A hash rule for execution of the replaced binary, then a file-path rule and a proxy rule that show exposure only.

title: Replaced WinKeyServer.exe From logs-bin Executed
id: d4a7c1e9-5b32-5f86-a0c4-9e1b3d7f2a58
status: experimental
description: >
  Execution of the one binary in either package that differs from the legitimate
  node-global-key-listener release, bin/WinKeyServer.exe from logs-bin@3.11.14 at
  75,105,792 bytes, identified by its SHA-256.
references:
  - https://registry.npmjs.org/logs-bin
  - https://x.com/meow_mfer/status/2055981353674654105
logsource:
  category: process_creation
detection:
  selection:
    Hashes|contains: 'SHA256=8587edcb8c9e384a7c3b4726a23e93d0bd2fdd2267984a7982484ee327983902'
  condition: selection
falsepositives:
  - None expected; do not match the upstream WinKeyServer.exe hash, which every legitimate consumer installs
level: high
---
title: logs-bin or logs-dump Package Files Written
id: 1b9e5d37-8a64-5c20-b7f1-4d6a2e8c0f93
status: experimental
description: >
  Files written under node_modules/logs-bin or node_modules/logs-dump, the two
  npm packages that republish node-global-key-listener. A hit shows exposure
  rather than DPRK involvement, because logs-dump's binaries equal the upstream
  release.
references:
  - https://registry.npmjs.org/logs-bin
  - https://registry.npmjs.org/logs-dump
logsource:
  category: file_event
detection:
  # optional: logs-dump's binaries equal upstream, so a path hit shows exposure only
  selection_path:
    TargetFilename|re: '(?i)[\\/]node_modules[\\/]logs-(bin|dump)[\\/]'
  condition: selection_path
falsepositives:
  - A legitimate package that happens to be named logs-bin or logs-dump
level: medium
---
title: Download of the logs-bin or logs-dump npm Package
id: 6c3f8a52-9d17-5e04-8b26-1a7e4c9d3b80
status: experimental
description: >
  A proxy request to the npm registry for logs-bin or logs-dump, which shows
  exposure even where no endpoint telemetry exists.
references:
  - https://registry.npmjs.org/logs-bin
  - https://registry.npmjs.org/logs-dump
logsource:
  category: proxy
detection:
  # optional: a download shows exposure only
  selection_registry:
    c-uri|contains:
      - 'registry.npmjs.org/logs-bin'
      - 'registry.npmjs.org/logs-dump'
  condition: selection_registry
falsepositives:
  - A legitimate package that happens to be named logs-bin or logs-dump
level: medium
Lower-fidelity parts (2)
  • the node_modules/logs-bin and logs-dump path match — logs-dump's binaries equal the upstream release, so a path hit shows exposure to a deceptive package, not DPRK involvement. Remove: delete the whole second rule, from `title: logs-bin or logs-dump Package Files Written` to the next `---`.
  • the registry download match — a download shows exposure only and the packages date from 2025. Remove: delete the whole third rule, from `title: Download of the logs-bin or logs-dump npm Package` to the end.

CrowdStrike LogScale

Written for this site, from indicators reported by npm

Matches the hash on any event that carries SHA256HashData, and has no proxy URL for the download line.

// Falcon has no proxy URL in this field set, so the registry-download line is not expressible here
SHA256HashData="8587edcb8c9e384a7c3b4726a23e93d0bd2fdd2267984a7982484ee327983902"
// optional: logs-dump's binaries equal upstream, so a path hit shows exposure only
or (#event_simpleName=/^(NewExecutableWritten|PeFileWritten)$/ and FilePath=/node_modules[\\\/]logs-(bin|dump)[\\\/]/i)
| groupBy([ComputerName, aid, #event_simpleName], function=[count(as=events), collect([FileName, FilePath]), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)])
| sort(last_seen, order=desc)
Lower-fidelity parts (1)
  • the node_modules/logs-bin and logs-dump path match — logs-dump's binaries equal the upstream release, so a path hit shows exposure to a deceptive package, not DPRK involvement. Remove: delete the `// optional: logs-dump's binaries` line and the `or (#event_simpleName=/^(NewExecutableWritten|PeFileWritten)$/ ...` line below it.

Elastic

Written for this site, from indicators reported by npm

Each signal is a boolean column and the two exposure-only matches are labelled in the result, so they can be dropped without touching the hash condition.

FROM logs-*
| WHERE @timestamp > NOW() - 90 days
| EVAL path = REPLACE(TO_LOWER(file.path), "\\\\", "/"), web = TO_LOWER(url.original)
| EVAL f_hash = TO_LOWER(file.hash.sha256) == "8587edcb8c9e384a7c3b4726a23e93d0bd2fdd2267984a7982484ee327983902" OR TO_LOWER(process.hash.sha256) == "8587edcb8c9e384a7c3b4726a23e93d0bd2fdd2267984a7982484ee327983902"
| EVAL f_path = path RLIKE ".*/node_modules/logs-(bin|dump)/.*"
| EVAL f_registry = web LIKE "*registry.npmjs.org/logs-bin*" OR web LIKE "*registry.npmjs.org/logs-dump*"
| WHERE f_hash OR f_path OR f_registry
| EVAL signal = CASE(f_hash, "hash", "exposure only")
| STATS events = COUNT(*), signals = VALUES(signal), paths = VALUES(path), urls = VALUES(web), first_seen = MIN(@timestamp), last_seen = MAX(@timestamp) BY host.name, source.ip
| SORT last_seen DESC
| LIMIT 100
Lower-fidelity parts (2)
  • the node_modules/logs-bin and logs-dump path match — logs-dump's binaries equal the upstream release, so a path hit shows exposure to a deceptive package, not DPRK involvement. Remove: delete the `| EVAL f_path` line and ` OR f_path` from the `| WHERE` line.
  • the registry download match — a download shows exposure only and the packages date from 2025. Remove: delete the `| EVAL f_registry` line and ` OR f_registry` from the `| WHERE` line.

Cortex XQL

Written for this site, from indicators reported by npm

Matches the hash on file and process events and optionally the package path, with no proxy URL for the download line.

config case_sensitive = false
| dataset = xdr_data
| filter (event_type = ENUM.FILE and (
            action_file_sha256 = "8587edcb8c9e384a7c3b4726a23e93d0bd2fdd2267984a7982484ee327983902"
         // optional: logs-dump's binaries equal upstream, so a path hit shows exposure only
         or action_file_path ~= "node_modules[^a-z0-9_.-]logs-(bin|dump)[^a-z0-9_.-]"
         ))
    or (event_type = ENUM.PROCESS and action_process_image_sha256 = "8587edcb8c9e384a7c3b4726a23e93d0bd2fdd2267984a7982484ee327983902")
| alter signal = if(event_type = ENUM.FILE, "file", "process")
| comp count() as events, min(_time) as first_seen, max(_time) as last_seen by agent_hostname, signal
| sort desc last_seen
Lower-fidelity parts (1)
  • the node_modules/logs-bin and logs-dump path match — logs-dump's binaries equal the upstream release, so a path hit shows exposure to a deceptive package, not DPRK involvement. Remove: delete the `// optional: logs-dump's binaries` line and the `or action_file_path ~= ...` line below it.

osquery

Written for this site, from indicators reported by npm

Hashes files in the bin directory of any package at a fixed depth under a home folder, and optionally lists installed copies of either package.

-- The file and hash tables need an absolute path to expand, and % matches one directory level,
-- so add a level for each deeper checkout location in use
SELECT 'hash' AS signal, f.path AS path, h.sha256 AS detail
FROM file f
JOIN hash h USING (path)
WHERE (f.path LIKE '/Users/%/%/node_modules/%/bin/%' OR f.path LIKE '/home/%/%/node_modules/%/bin/%' OR f.path LIKE 'C:\Users\%\%\node_modules\%\bin\%')
  AND h.sha256 = '8587edcb8c9e384a7c3b4726a23e93d0bd2fdd2267984a7982484ee327983902'
-- optional: logs-dump's binaries equal upstream, so an installed package shows exposure only
UNION ALL
SELECT 'package' AS signal, path, name || '@' || version AS detail
FROM npm_packages
WHERE name IN ('logs-bin', 'logs-dump')
Lower-fidelity parts (1)
  • the installed-package check — an installed copy shows exposure only because logs-dump's binaries equal upstream. Remove: delete from the `-- optional: logs-dump's binaries` line to the end of the query, which removes the UNION ALL and the second SELECT.

YARA

Written for this site, from indicators reported by npm

Scans lockfiles and package manifests for either package name or the maintainer account and address, which shows exposure rather than DPRK involvement.

rule Npm_Logs_Bin_Logs_Dump_Exposure
{
    meta:
        description = "Lockfile or package.json naming logs-bin or logs-dump, or the maintainer account and address behind them"
        reference = "https://registry.npmjs.org/logs-bin"
    strings:
        $lock_npm_bin = "node_modules/logs-bin\"" ascii
        $lock_npm_dump = "node_modules/logs-dump\"" ascii
        $tgz_bin = "registry.npmjs.org/logs-bin/-/" ascii
        $tgz_dump = "registry.npmjs.org/logs-dump/-/" ascii
        $yarn_bin = /\nlogs-bin@/ ascii
        $yarn_dump = /\nlogs-dump@/ ascii
        $pnpm_bin = "/logs-bin@" ascii
        $pnpm_dump = "/logs-dump@" ascii
        $maintainer = "topnotcher847" ascii
        $email = "josemalheiro105@gmail.com" ascii
        $manifest = /"name"\s*:\s*"logs-(bin|dump)"/ ascii
    condition:
        filesize < 20MB and any of them
}

False positives and tuning

Only one value should alert on its own: the SHA-256 of the Windows binary inside logs-bin@3.11.14. The package names and maintainer are strong in a corporate estate but alone show deception, not DPRK involvement.

Do not alert on the upstream hashes: e58864cc22cd8ec17ae35dd810455d604aadab7c3f145b6c53b3c261855a4bb1 is bin/WinKeyServer.exe in node-global-key-listener@0.3.0 and logs-dump@1.7.5, and 6e09249262d9a605180dfbd0939379bbf9f37db076980d6ffda98d650f70a16d is bin/X11KeyServer, identical across upstream and both packages.

The researcher attributes the packages to Lazarus and cites an antivirus detection on a file name shared by the legitimate library, without saying which copy was scanned; a keystroke binary trips antivirus honestly.

Both packages were last modified in July 2025 and the post dates from May 2026, so treat a lockfile hit as historical exposure to investigate, not current activity.

Sources

Tuning required before use This hunt is a starting point. Adapt the queries to your schema, validate them against your baseline and tune them for the false positives noted above. Indicators go stale as operators rotate hosts.