Skip to content

Cheatron/cheatron-nthread

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@cheatron/nthread

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

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

Important

64-bit Windows only. Requires Wine to develop/test on Linux.


How It Works

Instead of injecting new code into a process, NThread reuses 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 (e.g. 53 C3) Redirects RIP by pushing a register onto the stack and returning to it

Hijack sequence

1. Suspend target thread
2. Capture current register state (RIP, RSP, regKey, ...)
3. Set RIP  → [push reg; ret]   ← the pivot
4. Set REG  → [jmp .]           ← the sleep destination
5. Set RSP  → safe scratch area (current RSP - 8192, 16-byte aligned)
6. Apply context & resume
7. Poll (suspend → read RIP → resume) until RIP == sleep address
8. Thread is now parked at 'jmp .'

No remote allocation, no shellcode, no hooks — just register writes.


Features

  • No code injection — reuses gadgets already in loaded DLLs (ntdll, kernel32, kernelbase, msvcrt)
  • No WriteProcessMemory — memory operations are performed by hijacking the target thread to call its own msvcrt functions
  • Auto-discovery — scans modules automatically to find sleep and pushret gadgets via Module.scan()
  • Reversible — saves full register context before hijacking; restores on demand
  • CRT bridge — resolves msvcrt.dll exports to call malloc, calloc, memset, fwrite, etc. from inside the target thread
  • Write optimization — read-only memory regions (romem) track known contents, letting writeMemory skip unchanged bytes automatically
  • TypeScript-native — fully typed, composable architecture built on @cheatron/native

Installation

bun add @cheatron/nthread

Quick Start

import { NThread } from '@cheatron/nthread';
import * as Native from '@cheatron/native';

// Create an NThread orchestrator (gadgets auto-discovered)
const nthread = new NThread();

// Hijack an existing thread by TID — returns [ProxyThread, CapturedThread]
const [proxy, captured] = await nthread.inject(tid);

// Thread is now parked at the sleep address.
// Access the CapturedThread for register-level control:
console.log(`RIP: ${captured.getRIP()}`);
console.log(`TID: ${captured.tid}`);

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

// Write memory via hijacked memset calls
const data = Buffer.from([0xDE, 0xAD, 0xBE, 0xEF]);
await proxy.writeMemory(targetAddr, data);

// Cleanup — restores original context and releases the thread
await proxy.close();

Core Components

NThread — Orchestrator

Lightweight orchestrator that manages the hijack lifecycle. Does not extend Native.Thread — thread state is owned by CapturedThread.

new NThread(
  processId?: number,              // Optional: for diagnostics and logging
  sleepAddress?: NativePointer,    // Optional: explicit 'jmp .' gadget
  pushretAddress?: NativePointer,  // Optional: explicit 'push reg; ret' gadget
  regKey?: GeneralPurposeRegs,     // Optional: register preference (default: auto)
)
Method Description
inject(thread) Hijack a thread (by TID or Thread object), returns [ProxyThread, CapturedThread]
call(thread, target, args, timeout) Execute a function call on a specific captured thread
writeMemory(thread, dest, source) Write data via decomposed memset calls. Auto-detects overlap with read-only memory regions and routes through safe write
writeMemoryWithPointer(thread, dest, source, size) Write from a NativePointer source — reads locally then decomposes to memset. No romem check
writeMemorySafe(thread, dest, source, lastDest) Safe write — skips bytes that haven't changed

CapturedThread — Thread State

Extends Native.Thread. Holds all low-level state for a single captured thread: context cache, suspend tracking, register manipulation.

new CapturedThread(
  thread: Native.Thread | number,   // Thread object or TID
  regKey: GeneralPurposeRegs,        // Register for the pushret pivot
  sleepAddress: NativePointer,       // Sleep gadget address
  processId?: number,                // Optional: for diagnostics
)
Method Description
getContext() Returns cached thread context (does not re-fetch from hardware)
setContext(ctx) Updates context cache only (use applyContext() to write to hardware)
fetchContext() Reads current hardware context into cache
applyContext() Writes cached context to the thread hardware registers
calcStackBegin() Returns a 16-byte aligned scratch RSP (currentRSP - 8192)
getRIP() / setRIP(addr) Convenience RIP getter/setter
getRSP() / setRSP(addr) Convenience RSP getter/setter
getTargetReg() / setTargetReg(val) Get/set the pivot register value
wait(timeoutMs) Polls until RIP reaches sleepAddress
suspend() / resume() Balanced suspend/resume — tracked via suspendCount
release() Restores saved context (suspend → set → apply → resume) without closing the handle
close() Calls release() (safe for dead threads), drains remaining suspends, then closes the handle

ProxyThread — High-Level Interface

Provides an extensible interface for interacting with a captured thread. Each operation can be independently replaced via setter methods. Does not hold a reference to the underlying CapturedThread — the thread is returned separately from inject().

new ProxyThread(
  close: CloseFn,               // Required: close/cleanup strategy
  process?: Native.Process,     // Optional: enables default read/write via process memory
)
Method Description
readMemory(address, size) Read memory (delegates to configurable function)
writeMemory(address, data, size?) Write memory (delegates to configurable function)
call(address, ...args) Call a function (delegates to configurable function)
close() Restores thread to original state and releases it
setReader(fn) Replace the read memory implementation
setWriter(fn) Replace the write memory implementation
setCaller(fn) Replace the call implementation
setCloser(fn) Replace the close/cleanup implementation

All delegate functions receive the ProxyThread instance as their first argument:

type ReadMemoryFn  = (proxy: ProxyThread, address, size) => Promise<Buffer>;
type WriteMemoryFn = (proxy: ProxyThread, address, data, size?) => Promise<number>;
type CallFn        = (proxy: ProxyThread, address, ...args) => Promise<NativePointer>;
type CloseFn       = (proxy: ProxyThread) => Promise<void>;

globals.ts — Gadget Registry

Manages the global pools of discovered gadgets. Auto-discovery runs once lazily on first use.

// Manual registration (if you pre-know the addresses)
registerSleepAddress(ptr: NativePointer): void
registerPushretAddress(ptr: NativePointer, regKey: GeneralPurposeRegs): void

// Auto-discovery (scans ntdll, kernel32, kernelbase, msvcrt)
autoDiscoverAddresses(): void

// Random selection (triggers auto-discovery if pools are empty)
getRandomSleepAddress(): NativePointer | undefined
getRandomPushretAddress(regKey?): { address: NativePointer, regKey } | undefined

Register priority for auto-selection: Rbx → Rbp → Rdi → Rsi (non-volatile, least likely to be clobbered at randomsuspension point).

crt.ts — CRT Bridge

Most memory manipulation tools use WriteProcessMemory / ReadProcessMemory to read or write into a target process. NThread takes a different approach: it makes the target thread call its own CRT functions.

Because msvcrt.dll is loaded in virtually every Windows process, its exports (malloc, memset, fwrite, ...) exist at known addresses. By:

  1. Parking the thread at the sleep gadget
  2. Setting up registers as function arguments (RCX, RDX, R8, R9 per x64 calling convention)
  3. Setting RIP to the target CRT function (e.g. msvcrt!memset)
  4. Resuming the thread and waiting for it to return back to the sleep gadget

...you can execute arbitrary CRT calls inside the target process without ever calling WriteProcessMemory, VirtualAllocEx, or CreateRemoteThread.

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

// crt.malloc, crt.calloc, crt.free, crt.fopen, crt.fwrite,
// crt.fread, crt.fclose, crt.fflush, crt.memset
// All are NativePointer — set as RIP on the hijacked thread,
// pass arguments via register context (RCX, RDX, R8, R9)

Memory I/O Strategies

NThread provides several mechanisms for reading from and writing to a target process, all without ReadProcessMemory or WriteProcessMemory.

1. Write via memset (ntm_push_with_memset)

The simplest write strategy. Calls the target thread's own msvcrt!memset:

RCX = dest (pointer in target process)
RDX = byte value
R8  = length
RIP = msvcrt!memset
→ resume → wait for return

For arbitrary data (not just a single byte fill), ntu_write_with_memset decomposes the source buffer into runs of equal bytes and issues one memset call per run. This is efficient for zero-initialization or structured data with repeated byte patterns.

2. Heap allocation in the target (malloc / free)

By setting RIP to msvcrt!malloc / msvcrt!free and passing size/pointer via RCX, the target thread allocates and frees memory in its own heap:

RCX = size
RIP = msvcrt!malloc
→ resume → wait for return → RAX = pointer in target heap

The returned address lives in the target's address space and is a valid pointer for subsequent memset writes or tunnel I/O.

3. Bidirectional I/O via filesystem tunnel (nttunnel)

For reading arbitrary data back from the target — or writing larger/structured payloads — NThread uses a file-system channel: two temporary files that both sides open simultaneously from opposite ends.

                 ┌────────────────────────────────────┐
  Attacker       │            Temp file A             │       Target thread
  (write side)   │attacker fwrite → ... → target fread│   (read side / hijacked)
                 └────────────────────────────────────┘

                 ┌────────────────────────────────────┐
  Attacker       │            Temp file B             │       Target thread
  (read side)    │target fwrite → ... → attacker fread│  (write side / hijacked)
                 └────────────────────────────────────┘

Write channel (attacker → target):

  1. Attacker writes payload to local side of temp file A and flushes
  2. Hijacked thread calls msvcrt!_wfopen + msvcrt!fread to pull the data into its own memory

Read channel (target → attacker):

  1. Hijacked thread calls msvcrt!fwrite to stream data from target memory into temp file B
  2. Attacker reads temp file B natively

Both channels use the same CRT bridge — the target thread calls _wfopen, fread/fwrite, and fflush from inside via register-driven hijacks, with no ReadProcessMemory/WriteProcessMemory at any point.

Temp file paths are automatically rotated after a configurable transfer threshold (max_transfer) to avoid seeking back to position 0.

4. ntmem — Memory region abstraction

ntmem wraps a target memory region as a triple-buffer:

Field Location Description
remote Target process heap Actual allocation in the target (malloc-allocated)
local Attacker process Working copy — modifications are made here
local_cpy Attacker process Snapshot of last pushed state — used for dirty-range detection

push (local → remote): Detects which byte range changed between local and local_cpy, then automatically selects the most efficient strategy:

  • If the tunnel is available and the diff range is ≥ 3 bytes → tunnel write
  • Otherwise → memset write

pull (remote → local): Calls ntu_fwrite on the target side (hijacked thread writes target memory into the read channel), then attacker reads it locally.

Note: nttunnel and ntmem are part of the original C architecture. The TypeScript port currently implements the CRT bridge, memset-based writes, and a partial ntmem equivalent via the read-only memory system (romem). Tunnel-based read/write is planned for a future iteration.


Read-Only Memory (romem)

When the hijacking side allocates and writes memory in the target, it always knows the exact contents of that region. romem exploits this by keeping a local snapshot (the local Buffer) that mirrors the remote state, so writeMemory can automatically skip unchanged bytes.

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

// Allocate a zero-initialized region in the target via calloc
const romem = await createReadOnlyMemory(proxy, 256);
// romem.remote → NativePointer in target heap
// romem.local  → Buffer (all zeroes, mirrors calloc output)

// writeMemory now auto-detects the overlap and uses safe write:
const data = Buffer.alloc(256);
data.writeUInt32LE(0xDEADBEEF, 0);
await proxy.writeMemory(romem.remote, data);
// Only the first 4 bytes are actually written — the rest are skipped
// because romem.local says they're already zero.

// Manual registration for externally allocated regions:
const manual = registerReadOnlyMemory(somePointer, someBuffer);

// Unregister when no longer needed (does NOT free remote memory):
unregisterReadOnlyMemory(romem);

How it integrates with writeMemory

When NThread.writeMemory() is called, it checks the global romem registry:

  1. If the destination overlaps a registered region, the write is split into up to 3 parts:
    • Before overlap → standard decomposed memset
    • OverlapwriteMemorySafeBuffer (skips bytes matching the snapshot)
    • After overlap → standard decomposed memset
  2. After the safe write, the snapshot (romem.local) is updated to reflect the new contents.
  3. If no overlap is found, the write proceeds as a standard decomposed memset.


Development

# Install dependencies
bun install

# Run tests (requires Wine on Linux)
wine /path/to/bun.exe test

# Build
bun run build

Differences from C version

Feature C (NThread) TypeScript (@cheatron/nthread)
Gadget discovery Manual / test-provided Auto via Module.scan()
Thread API Raw Win32 (SuspendThread, GetThreadContext) @cheatron/native Thread class
CRT resolution Compile-time linking Runtime via getProcAddress on msvcrt.dll
Module scanning Not included @cheatron/native Scanner + Pattern
Memory write (simple) ntu_write_with_memset (runs of equal bytes) writeMemory — decomposes buffer into runs of equal bytes, one memset call per run
Memory write (safe) ntu_write_with_memset_dest (skip unchanged) writeMemorySafe / writeMemorySafeBuffer — compares against snapshot, skips matching bytes
Memory write (pointer) N/A writeMemoryWithPointer — reads from NativePointer source, then decomposed memset
Read-only memory Part of ntmem romem(remote, local) pair, auto-integrated into writeMemory for skip optimization
Memory read/write (large) nttunnel — two temp files, target calls fread/fwrite Planned — not yet ported
Memory region abstraction ntmem(remote, local, local_cpy) triple, dirty-range push/pull Partially ported via romem

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages