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.
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.
- No code injection — reuses gadgets in
ntdll,kernel32,kernelbase,msvcrt - No
WriteProcessMemory— memory ops are performed by hijacking the target thread to call its ownmsvcrtfunctions - Auto-discovery — scans modules lazily via
Module.scan() - Reversible — saves full register context before hijacking; restores on
proxy.close() - CRT bridge — resolves
msvcrt!malloc,calloc,memset,fwrite, etc. and calls them from inside the target thread - Write optimization —
romemtracks known region contents and letswrite()skip unchanged bytes automatically - Heap allocator —
NThreadHeappre-allocates a single heap block in the target and sub-allocates from it, minimising round-trips
bun add @cheatron/nthreadimport { NThread, createReadOnlyMemory } from '@cheatron/nthread';
const nthread = new NThread();
// Hijack an existing thread by TID
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]));
// Allocate and write a wide string in one step
const strPtr = await proxy.allocString('Hello, target!');
// Read memory back
const buf = await proxy.read(ptr, 4);
// Restore original context and release
await proxy.close();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] |
threadCall(thread, target, args, timeout) |
Execute a function call on a captured thread |
writeMemory(thread, dest, source) |
Write via decomposed memset calls; romem-aware |
Overridable hooks (for subclasses):
threadClose(proxy, captured, suicide?)— called byproxy.close()threadAlloc(proxy, size, opts?)— called byproxy.alloc()threadFree(proxy, ptr)— called byproxy.free()
Subclass of NThread. Pre-allocates a single heap block (calloc) in the target and sub-allocates from it. The heap 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 = 65536 * 8All proxy allocations are freed atomically on proxy.close().
Extends Native.Thread. Owns the hardware context cache, suspend tracking, and register manipulation for a single captured thread.
| 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 |
High-level interface for a captured thread. Each operation is a replaceable delegate.
new ProxyThread(close: CloseFn, process?: Native.Process)| Method | Description |
|---|---|
read(address, size) |
Read memory |
write(address, data, size?) |
Write memory |
call(address, ...args) |
Call a function |
alloc(size, opts?) |
Allocate memory (malloc/calloc/realloc) |
free(ptr) |
Free memory |
allocString(str, encoding?, opts?) |
Alloc + write a string; null-terminated; default encoding utf16le |
close(suicide?) |
Release the thread (or terminate with exit code) |
setReader/setWriter/setCaller/setCloser/setAllocer/setFreer |
Replace delegates |
Manages sleep and pushret gadget pools. Auto-discovery runs once lazily on first use, scanning ntdll, kernel32, kernelbase, msvcrt. Register priority: Rbx → Rbp → Rdi → Rsi.
Resolves msvcrt.dll exports at load time (malloc, calloc, free, memset, realloc, fopen, fread, fwrite, fflush, fclose). All values are NativePointer — set as RIP on the hijacked thread.
romem tracks a known-content region as a (remote: NativePointer, local: Buffer) 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 changed bytes are written
unregisterReadOnlyMemory(romem);bun install
bun run build
wine /path/to/bun.exe testMIT