npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cheatron/nthread

v1.0.5

Published

Non-invasive x64 Windows thread hijacking library — seize control of existing threads without shellcode, remote allocation, or CreateRemoteThread.

Readme

@cheatron/nthread

NThread is a thread hijacking library for x64 Windows that seizes control of existing threads — without injecting shellcode, allocating remote memory, or using CreateRemoteThread.

Built on @cheatron/native. TypeScript port of the original C/C++ NThread.

[!IMPORTANT] 64-bit Windows only. Requires Wine to develop/test on Linux.


How It Works

NThread reuses two tiny instruction sequences (gadgets) already present in loaded modules:

| Gadget | Pattern | Purpose | |--------|---------|---------| | Sleep | jmp . (EB FE) | Parks the thread in an infinite loop | | Pivot | push reg; ret | Redirects RIP to the sleep gadget |

Hijack sequence: suspend → capture context → redirect RIP through pivot → spin until RIP lands on sleep gadget. No shellcode, no remote allocation — just register writes.

suspend thread
    → save full register context
    → set RIP = pushret gadget
    → set RSP = safe scratch area (current RSP − 8192, 16-aligned)
    → set target register = sleep address
    → apply context → resume
    → poll until RIP == sleep address
    → thread is now parked and ready for commands

Features

  • No code injection — reuses gadgets in ntdll, kernel32, kernelbase, msvcrt
  • No WriteProcessMemory — memory ops use the target thread's own msvcrt functions
  • Auto-discovery — scans loaded modules lazily via Module.scan()
  • Reversible — saves full register context before hijacking; restores on proxy.close()
  • CRT bridge — resolves msvcrt exports (malloc, calloc, memset, fopen, fread, etc.) and calls them from inside the target thread
  • String args — pass strings directly to proxy.call(), automatically allocated and freed
  • Write optimizationromem tracks known region contents and skips unchanged bytes automatically
  • Heap allocatorNThreadHeap pre-allocates a heap block in the target and sub-allocates from it, minimising CRT round-trips
  • File channelNThreadFile replaces RPM/WPM with bidirectional filesystem I/O through a single temp file

Installation

bun add @cheatron/nthread

Quick Start

Basic — NThread

import { NThread, crt } from '@cheatron/nthread';

const nthread = new NThread();
const [proxy, captured] = await nthread.inject(tid);

// Call a function inside the target thread (x64 calling convention)
const ptr = await proxy.call(crt.malloc, 1024n);

// Write memory via hijacked memset calls
await proxy.write(ptr, Buffer.from([0xDE, 0xAD, 0xBE, 0xEF]));

// Read memory back
const buf = await proxy.read(ptr, 4);

// Restore original context and release
await proxy.close();

Heap — NThreadHeap

import { NThreadHeap } from '@cheatron/nthread';

const nt = new NThreadHeap();
const [proxy] = await nt.inject(tid);

// alloc/free go through the local heap — fewer CRT calls
const ptr = await proxy.alloc(256, { fill: 0 });
await proxy.write(ptr, myData);
await proxy.dealloc(ptr);

// Allocate and write a null-terminated string in one step
const strPtr = await proxy.allocString('Hello, target!');

await proxy.close(); // frees all heap blocks atomically

File Channel — NThreadFile

import { NThreadFile } from '@cheatron/nthread';

const nt = new NThreadFile();
const [proxy] = await nt.inject(tid);

// read/write now go through a temp file — no RPM/WPM
const ptr = await proxy.alloc(4096, { fill: 0 });
await proxy.write(ptr, largeBuffer);          // fs.writeFileSync → fread
const data = await proxy.read(ptr, 4096);     // fwrite + fflush → fs.readFileSync

await proxy.close(); // closes FILE*, deletes temp file, destroys heaps, restores thread

File I/O Helpers

// Open a file in the target process
const stream = await nthread.fileOpen(proxy, 'C:\\data\\log.txt', 'rb');

// Read 512 bytes from the stream → local Buffer
const buf = await nthread.fileRead(proxy, stream, 512);

// Write data to a stream (string, Buffer, or NativeMemory)
await nthread.fileWrite(proxy, stream, 'hello');

// Flush and close
await nthread.fileFlush(proxy, stream);
await nthread.fileClose(proxy, stream);

Read-Only Memory (romem)

Tracks a known-content region as a (remote, local) pair. proxy.write() auto-detects overlaps and skips unchanged bytes.

import { createReadOnlyMemory, unregisterReadOnlyMemory } from '@cheatron/nthread';

const romem = await createReadOnlyMemory(proxy, 256); // calloc in target
const data = Buffer.alloc(256);
data.writeUInt32LE(0xDEADBEEF, 0);
await proxy.write(romem.remote, data); // only the 4 changed bytes are written

unregisterReadOnlyMemory(romem);

Class Hierarchy

NThread              — Orchestrator: inject, threadCall, writeMemory, CRT helpers
  └─ NThreadHeap     — Heap sub-allocator per proxy (doubles up to maxSize)
       └─ NThreadFile — Filesystem-based I/O channel (single temp file)

NThread

Lightweight orchestrator — holds resolved gadget addresses and runs the hijack sequence.

new NThread(processId?, sleepAddress?, pushretAddress?, regKey?)

| Method | Description | |--------|-------------| | inject(thread) | Hijack a thread (TID or Thread), returns [ProxyThread, CapturedThread] | | allocString(proxy, str, opts?) | Allocate + write a null-terminated string | | writeString(proxy, dest, str) | Write a null-terminated string to an existing address | | fileOpen(proxy, path, mode) | fopen in the target; auto-allocates/frees string args | | fileWrite(proxy, stream, data) | fwrite — accepts Buffer, string, or NativeMemory | | fileRead(proxy, stream, dest) | freadNativeMemory or byte-count → Buffer | | fileFlush(proxy, stream) | fflush | | fileClose(proxy, stream) | fclose |

Overridable hooks (for subclasses):

  • threadClose(proxy, captured, suicide?) — called by proxy.close()
  • threadAlloc(proxy, size, opts?) — called by proxy.alloc()
  • threadDealloc(proxy, ptr) — called by proxy.dealloc()

NThreadHeap

Subclass of NThread. Pre-allocates a heap block in the target and sub-allocates from it. The block doubles on full (up to maxSize); oversized requests fall back to msvcrt!malloc.

new NThreadHeap(heapSize?, maxSize?, processId?, sleepAddress?, pushretAddress?, regKey?)
// Defaults: heapSize = 65536, maxSize = 524288

All heap blocks are freed atomically on proxy.close().

NThreadFile

Subclass of NThreadHeap. Replaces ReadProcessMemory/WriteProcessMemory (and the base class's decomposed memset writes) with a bidirectional filesystem channel.

new NThreadFile(heapSize?, maxSize?, processId?, sleepAddress?, pushretAddress?, regKey?)
  • Opens a single temp file in the target with fopen(path, "w+b") during inject()
  • Write (attacker → target): write locally → fseek(0) + fread in target
  • Read (target → attacker): fseek(0) + fwrite + fflush in target → read locally
  • proxy.close() calls fclose, deletes the temp file, then destroys heaps and restores the thread

ProxyThread

High-level interface returned by inject(). Each operation is a replaceable delegate.

| Method | Description | |--------|-------------| | read(address, size) | Read memory from the target | | write(address, data, size?) | Write memory to the target | | call(address, ...args) | Call a function (up to 4 args: RCX, RDX, R8, R9) | | alloc(size, opts?) | Allocate memory (AllocOptions: fill, readonly, address) | | dealloc(ptr) | Deallocate memory (routes through delegate; subclasses may use managed heap) | | allocString(str, encoding?, opts?) | Alloc + write null-terminated string (default utf16le) | | close(suicide?) | Release thread, or terminate with exit code |

Delegate setters: setReader, setWriter, setCaller, setCloser, setAllocer, setDeallocer — replace any operation with a custom function.

CRT auto-binding: All resolved msvcrt functions (including free) are bound as methods on the proxy (e.g. proxy.malloc(size), proxy.free(ptr), proxy.fopen(path, mode)).

CapturedThread

Extends Native.Thread. Owns the hardware context cache, suspend tracking, and register manipulation.

| Method | Description | |--------|-------------| | fetchContext() / applyContext() | Sync hardware ↔ cache | | getRIP() / setRIP(addr) | RIP convenience accessors | | wait(timeoutMs?) | Poll until RIP == sleep address | | release() | Restore saved context without closing handle | | close() | release() → drain suspends → close handle |

AllocOptions

interface AllocOptions {
  fill?: number;                // byte value to fill allocated memory
  readonly?: boolean;           // allocate in the readonly zone of the heap
  address?: NativePointer;      // realloc mode: resize an existing allocation
}

Gadget Auto-Discovery

Gadgets are discovered lazily on first inject(). The scanner searches ntdll, kernel32, kernelbase, and msvcrt for:

  • Sleep: EB FE (jmp .)
  • Pushret: push reg; ret — register priority: Rbx → Rbp → Rdi → Rsi (least-clobbered by msvcrt calls)

You can also provide explicit gadget addresses in the constructor if you prefer manual control:

const nt = new NThread(pid, mySleepAddr, myPushretAddr, 'Rbx');

CRT Bridge

crt.ts resolves msvcrt.dll exports at load time. All values are NativePointer — used as RIP targets for threadCall.

import { crt } from '@cheatron/nthread';

crt.malloc   // msvcrt!malloc
crt.calloc   // msvcrt!calloc
crt.realloc  // msvcrt!realloc
crt.free     // msvcrt!free
crt.memset   // msvcrt!memset
crt.fopen    // msvcrt!fopen
crt.fread    // msvcrt!fread
crt.fwrite   // msvcrt!fwrite
crt.fseek    // msvcrt!fseek
crt.fflush   // msvcrt!fflush
crt.fclose   // msvcrt!fclose

Error Hierarchy

NThreadError
  ├─ NoSleepAddressError      — no sleep gadget found
  ├─ NoPushretAddressError     — no pushret gadget found
  ├─ InjectError
  │    ├─ InjectTimeoutError   — thread didn't reach sleep in time
  │    └─ MsvcrtNotLoadedError — msvcrt.dll not in target process
  ├─ CallError
  │    ├─ CallNotInjectedError — call before inject
  │    ├─ CallTooManyArgsError — more than 4 args
  │    ├─ CallRipMismatchError — RIP not at sleep before call
  │    ├─ CallTimeoutError     — function didn't return in time
  │    └─ CallThreadDiedError  — thread exited during call (e.g. ExitThread)
  ├─ WriteError
  │    └─ WriteSizeRequiredError — NativePointer write without size
  ├─ AllocError
  │    └─ ReallocNullError     — realloc with null address
  ├─ FileError                 — fopen returned NULL
  └─ GadgetError
       └─ GadgetScanError      — pattern scan failed

Development

bun install
bun run build
bun run lint
bun run format

# Tests require Wine (Windows x64 on Linux)
wine /path/to/bun-windows-x64/bun.exe test

License

MIT