@memoryprocess/bindings 0.0.1 → 0.0.3-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/memoryprocess.node +0 -0
- package/dist/native.d.ts +4 -4
- package/native/hde/LICENSE-minhook.txt +1 -1
- package/native/hde/hde64.c +1 -1
- package/native/hde/hde64.h +1 -1
- package/native/hde/pstdint.h +1 -1
- package/native/hde/table64.h +1 -1
- package/native/module.cc +63 -63
- package/native/pattern.cc +5 -5
- package/native/process.cc +1 -1
- package/package.json +2 -3
- package/prebuilds/win32-x64/@memoryprocess+bindings.node +0 -0
package/dist/index.d.ts
CHANGED
package/dist/memoryprocess.node
CHANGED
|
Binary file
|
package/dist/native.d.ts
CHANGED
|
@@ -77,11 +77,11 @@ interface Native {
|
|
|
77
77
|
* RELOCATE position-relative operands (rel32 branch/call immediates,
|
|
78
78
|
* RIP-relative disp32) so the stolen bytes are correct when executed from
|
|
79
79
|
* `stubAddr` (must be within rel32 range of `address`). External rel8/16
|
|
80
|
-
* branches (incl. LOOP/JECXZ) can't be widened
|
|
80
|
+
* branches (incl. LOOP/JECXZ) can't be widened -- refused `HOOK_RELOCATE`,
|
|
81
81
|
* like MinHook; rel8/16 internal (target inside the stolen window) and
|
|
82
82
|
* all non-relative instructions copy verbatim. Without `stubAddr` any
|
|
83
83
|
* position-relative instruction is refused (inspection mode).
|
|
84
|
-
* Throws `HOOK_STEAL`/`HOOK_RELOCATE`
|
|
84
|
+
* Throws `HOOK_STEAL`/`HOOK_RELOCATE` -- returns the relocated bytes.
|
|
85
85
|
*/
|
|
86
86
|
hookSteal(handle: number, address: number, minBytesOrOpts?: number | {
|
|
87
87
|
minBytes?: number;
|
|
@@ -92,7 +92,7 @@ interface Native {
|
|
|
92
92
|
original: Buffer;
|
|
93
93
|
};
|
|
94
94
|
/**
|
|
95
|
-
* Per-region VirtualProtectEx
|
|
95
|
+
* Per-region VirtualProtectEx->write->restore across the whole write span --
|
|
96
96
|
* records each region's real old protection before writing, restores each
|
|
97
97
|
* individually. `restored=false` means part of the span is left weakened.
|
|
98
98
|
*/
|
|
@@ -101,7 +101,7 @@ interface Native {
|
|
|
101
101
|
restored: boolean;
|
|
102
102
|
protected: boolean;
|
|
103
103
|
};
|
|
104
|
-
/** Current thread id (TID)
|
|
104
|
+
/** Current thread id (TID) -- used to refuse self-suspend. */
|
|
105
105
|
currentThreadId(): number;
|
|
106
106
|
suspendProcess(pid: number): boolean;
|
|
107
107
|
resumeProcess(pid: number): boolean;
|
package/native/hde/hde64.c
CHANGED
package/native/hde/hde64.h
CHANGED
package/native/hde/pstdint.h
CHANGED
package/native/hde/table64.h
CHANGED
package/native/module.cc
CHANGED
|
@@ -12,12 +12,12 @@ extern "C" {
|
|
|
12
12
|
|
|
13
13
|
namespace mp {
|
|
14
14
|
|
|
15
|
-
// Max bytes a single read call may materialize
|
|
15
|
+
// Max bytes a single read call may materialize - guards against negative /
|
|
16
16
|
// non-finite / absurd `size` values blowing up std::vector across the N-API
|
|
17
17
|
// boundary (a length_error crossing N-API aborts the process).
|
|
18
18
|
static const double MAX_READ_BYTES = 512.0 * 1024.0 * 1024.0;
|
|
19
19
|
|
|
20
|
-
// NOTE: handles and addresses cross N-API as doubles
|
|
20
|
+
// NOTE: handles and addresses cross N-API as doubles - above 2^53 they would
|
|
21
21
|
// silently round. Harmless today (canonical user-space VA < 2^48, real HANDLE
|
|
22
22
|
// values are small) but don't feed pointers >2^53 through these bindings.
|
|
23
23
|
|
|
@@ -165,8 +165,8 @@ Napi::Value WriteBufferSync(const Napi::CallbackInfo& info) {
|
|
|
165
165
|
// ---------- v2 Tier-1/2 ----------
|
|
166
166
|
|
|
167
167
|
// allocMemory(handle, size, protection[, nearAddress]) -> bigint
|
|
168
|
-
// `nearAddress` is passed to VirtualAllocEx as lpAddress
|
|
169
|
-
// trampoline stubs within rel32 range (
|
|
168
|
+
// `nearAddress` is passed to VirtualAllocEx as lpAddress - a hint used to land
|
|
169
|
+
// trampoline stubs within rel32 range (+/-2GB) of a hook site.
|
|
170
170
|
Napi::Value AllocMemorySync(const Napi::CallbackInfo& info) {
|
|
171
171
|
Napi::Env env = info.Env();
|
|
172
172
|
if (info.Length() < 3) { Napi::TypeError::New(env, "expected (handle,size,protection[,nearAddress])").ThrowAsJavaScriptException(); return env.Null(); }
|
|
@@ -234,7 +234,7 @@ Napi::Value SuspendThreadSync(const Napi::CallbackInfo& info) {
|
|
|
234
234
|
if (info.Length() < 1 || !info[0].IsNumber()) { Napi::TypeError::New(env, "expected tid").ThrowAsJavaScriptException(); return env.Null(); }
|
|
235
235
|
DWORD tid = info[0].As<Napi::Number>().Uint32Value();
|
|
236
236
|
// QA4-C3: suspending OUR OWN thread from a sync binding freezes the JS
|
|
237
|
-
// engine mid-call
|
|
237
|
+
// engine mid-call - it can never reach resumeThread (unrecoverable
|
|
238
238
|
// deadlock). Refuse silently (false) so suspendAll() on the caller's own
|
|
239
239
|
// pid skips the calling thread instead of hanging.
|
|
240
240
|
if (tid == GetCurrentThreadId()) return Napi::Boolean::New(env, false);
|
|
@@ -384,13 +384,13 @@ Napi::Value FlushInstructionsSync(const Napi::CallbackInfo& info) {
|
|
|
384
384
|
|
|
385
385
|
// writeProtectedMemory(handle, address, buffer[, protection]) -> boolean
|
|
386
386
|
// QA4-M1: the old TS flow did protect(addr,len)->oldProt(first page only)->
|
|
387
|
-
// write->protect(addr,len,oldProt)
|
|
387
|
+
// write->protect(addr,len,oldProt) - a write spanning a protection boundary
|
|
388
388
|
// restored EVERY page in the span to the first page's old protection,
|
|
389
389
|
// silently changing protections on the second region. This native
|
|
390
390
|
// implementation walks VirtualQueryEx over the write span, records each
|
|
391
391
|
// region's real old protection, unprotects the whole span, writes, then
|
|
392
392
|
// restores each region individually. Single-region writes keep identical
|
|
393
|
-
// behavior. Restore failures are reported via the `restored` flag
|
|
393
|
+
// behavior. Restore failures are reported via the `restored` flag - a
|
|
394
394
|
// restored=false result means part of the span is left at `protection`.
|
|
395
395
|
Napi::Value WriteProtectedMemorySync(const Napi::CallbackInfo& info) {
|
|
396
396
|
Napi::Env env = info.Env();
|
|
@@ -410,7 +410,7 @@ Napi::Value WriteProtectedMemorySync(const Napi::CallbackInfo& info) {
|
|
|
410
410
|
if (info.Length() > 3 && info[3].IsNumber())
|
|
411
411
|
newProt = info[3].As<Napi::Number>().Uint32Value();
|
|
412
412
|
if (buf.Length() == 0) {
|
|
413
|
-
// Nothing to do
|
|
413
|
+
// Nothing to do - return the same result shape as a real write.
|
|
414
414
|
auto o = Napi::Object::New(env);
|
|
415
415
|
o.Set("ok", Napi::Boolean::New(env, true));
|
|
416
416
|
o.Set("restored", Napi::Boolean::New(env, true));
|
|
@@ -419,7 +419,7 @@ Napi::Value WriteProtectedMemorySync(const Napi::CallbackInfo& info) {
|
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
// QA4-M1: record per-region old protection across the whole write span
|
|
422
|
-
// BEFORE unprotecting
|
|
422
|
+
// BEFORE unprotecting - VirtualProtectEx only reports the first page's
|
|
423
423
|
// old value, so the query walk is the only correct source.
|
|
424
424
|
struct RegionSnap { std::uintptr_t base; std::uintptr_t end; DWORD oldProt; };
|
|
425
425
|
std::vector<RegionSnap> regions;
|
|
@@ -445,13 +445,13 @@ Napi::Value WriteProtectedMemorySync(const Napi::CallbackInfo& info) {
|
|
|
445
445
|
}
|
|
446
446
|
|
|
447
447
|
// Unprotect each region span (VirtualProtectEx clamps to whole pages, which
|
|
448
|
-
// is fine
|
|
448
|
+
// is fine - only pages intersecting [addr,end) are touched).
|
|
449
449
|
bool allProtected = true;
|
|
450
450
|
for (const auto& r : regions) {
|
|
451
451
|
DWORD old = 0;
|
|
452
452
|
if (!VirtualProtectEx(h, reinterpret_cast<LPVOID>(r.base), r.end - r.base,
|
|
453
453
|
newProt, &old)) {
|
|
454
|
-
allProtected = false; // continue
|
|
454
|
+
allProtected = false; // continue - best effort
|
|
455
455
|
}
|
|
456
456
|
}
|
|
457
457
|
|
|
@@ -487,9 +487,9 @@ static void ThrowInject(Napi::Env env, const char* what, DWORD code) {
|
|
|
487
487
|
|
|
488
488
|
// injectDll(pid, dllPath) -> boolean
|
|
489
489
|
// QA4-M2: dllPath must be ABSOLUTE. A relative name resolves against the
|
|
490
|
-
// TARGET's cwd + DLL search order
|
|
490
|
+
// TARGET's cwd + DLL search order - a DLL-planting primitive when that cwd is
|
|
491
491
|
// attacker-writable. Reject anything that isn't `X:\...` or a `\\` UNC path.
|
|
492
|
-
// QA4-M2 also: the path is written UTF-16LE and LoadLibraryW is used
|
|
492
|
+
// QA4-M2 also: the path is written UTF-16LE and LoadLibraryW is used - the
|
|
493
493
|
// old LoadLibraryA path mangled non-ANSI filenames per the target's codepage.
|
|
494
494
|
static bool IsAbsolutePath(const std::string& p) {
|
|
495
495
|
if (p.size() >= 3 && ((p[0] >= 'A' && p[0] <= 'Z') || (p[0] >= 'a' && p[0] <= 'z')) &&
|
|
@@ -516,7 +516,7 @@ Napi::Value InjectDllSync(const Napi::CallbackInfo& info) {
|
|
|
516
516
|
ThrowInject(env, "dllPath exceeds maximum NT path length (32767)", ERROR_FILENAME_EXCED_RANGE);
|
|
517
517
|
return env.Null();
|
|
518
518
|
}
|
|
519
|
-
// Optional 3rd arg: remote-thread wait timeout (bounded
|
|
519
|
+
// Optional 3rd arg: remote-thread wait timeout (bounded - a negative or
|
|
520
520
|
// huge value would wrap DWORD into an ~infinite sync wait, QA4-H1).
|
|
521
521
|
DWORD waitMs = 10000;
|
|
522
522
|
if (info.Length() > 2 && info[2].IsNumber()) {
|
|
@@ -541,7 +541,7 @@ Napi::Value InjectDllSync(const Napi::CallbackInfo& info) {
|
|
|
541
541
|
HANDLE h = OpenProcess(access, FALSE, pid);
|
|
542
542
|
if (!h) { ThrowInject(env, "OpenProcess failed", GetLastError()); return env.Null(); }
|
|
543
543
|
|
|
544
|
-
// WoW64 targets load a 32-bit kernel32 at a different base
|
|
544
|
+
// WoW64 targets load a 32-bit kernel32 at a different base - LoadLibraryW
|
|
545
545
|
// resolved in *our* (x64) process would be the wrong address. Refuse rather
|
|
546
546
|
// than silently corrupting the target.
|
|
547
547
|
{
|
|
@@ -587,10 +587,10 @@ Napi::Value InjectDllSync(const Napi::CallbackInfo& info) {
|
|
|
587
587
|
if (wr != WAIT_OBJECT_0) {
|
|
588
588
|
// QA4-C1: do NOT TerminateThread here. TerminateThread is asynchronous
|
|
589
589
|
// and killing a thread inside LoadLibraryW can leave the target's
|
|
590
|
-
// loader lock permanently held
|
|
590
|
+
// loader lock permanently held - every subsequent LoadLibrary /
|
|
591
591
|
// thread-creation in the victim deadlocks. Instead we close our handle
|
|
592
592
|
// and deliberately LEAK the small path buffer so a late-finishing
|
|
593
|
-
// thread never reads freed memory. Bounded leak
|
|
593
|
+
// thread never reads freed memory. Bounded leak <=64KB; a wedged target
|
|
594
594
|
// is worse.
|
|
595
595
|
CloseHandle(t);
|
|
596
596
|
ThrowInject(env, "LoadLibraryW remote thread timed out (leaked path buffer; target not terminated to avoid loader-lock deadlock)",
|
|
@@ -628,7 +628,7 @@ Napi::Value EjectDllSync(const Napi::CallbackInfo& info) {
|
|
|
628
628
|
HANDLE h = OpenProcess(access, FALSE, pid);
|
|
629
629
|
if (!h) { ThrowInject(env, "OpenProcess failed", GetLastError()); return env.Null(); }
|
|
630
630
|
|
|
631
|
-
// QA4-C4: same WoW64 guard as injectDll
|
|
631
|
+
// QA4-C4: same WoW64 guard as injectDll - our 64-bit FreeLibrary address is
|
|
632
632
|
// meaningless inside a 32-bit target's address space; the remote thread
|
|
633
633
|
// would execute a wild pointer.
|
|
634
634
|
{
|
|
@@ -652,7 +652,7 @@ Napi::Value EjectDllSync(const Napi::CallbackInfo& info) {
|
|
|
652
652
|
if (!t) { ThrowInject(env, "CreateRemoteThread failed", GetLastError()); break; }
|
|
653
653
|
DWORD wr = WaitForSingleObject(t, 10000);
|
|
654
654
|
if (wr != WAIT_OBJECT_0) {
|
|
655
|
-
// QA4-C1: never TerminateThread inside FreeLibrary
|
|
655
|
+
// QA4-C1: never TerminateThread inside FreeLibrary - the remote thread
|
|
656
656
|
// can hold the loader lock; killing it wedges the target permanently.
|
|
657
657
|
// Just close our handle and report the timeout.
|
|
658
658
|
CloseHandle(t);
|
|
@@ -660,7 +660,7 @@ Napi::Value EjectDllSync(const Napi::CallbackInfo& info) {
|
|
|
660
660
|
wr == WAIT_FAILED ? GetLastError() : ERROR_TIMEOUT);
|
|
661
661
|
break;
|
|
662
662
|
}
|
|
663
|
-
// QA4-C4: FreeLibrary returns BOOL as the thread exit code
|
|
663
|
+
// QA4-C4: FreeLibrary returns BOOL as the thread exit code - verify it.
|
|
664
664
|
// Without this check, ejecting kernel32/main.exe/garbage bases reported
|
|
665
665
|
// success while nothing (or worse, the wrong thing) happened.
|
|
666
666
|
DWORD exitCode = 0;
|
|
@@ -805,7 +805,7 @@ Napi::Value CallRemoteSync(const Napi::CallbackInfo& info) {
|
|
|
805
805
|
}
|
|
806
806
|
DWORD timeout = 10000;
|
|
807
807
|
if (info.Length() > 3 && info[3].IsNumber()) {
|
|
808
|
-
// QA4-H1: Uint32Value() on -1 wraps to 0xFFFFFFFF = INFINITE
|
|
808
|
+
// QA4-H1: Uint32Value() on -1 wraps to 0xFFFFFFFF = INFINITE - a sync
|
|
809
809
|
// binding that never returns bricks the whole JS process. Clamp to a
|
|
810
810
|
// bounded range; negative/non-finite values are a caller bug.
|
|
811
811
|
double t = info[3].As<Napi::Number>().DoubleValue();
|
|
@@ -825,13 +825,13 @@ Napi::Value CallRemoteSync(const Napi::CallbackInfo& info) {
|
|
|
825
825
|
if (!t) { CloseHandle(h); return env.Null(); }
|
|
826
826
|
DWORD wr = WaitForSingleObject(t, timeout);
|
|
827
827
|
DWORD code = 0;
|
|
828
|
-
// QA4-C1: TerminateThread is asynchronous
|
|
828
|
+
// QA4-C1: TerminateThread is asynchronous - it only *requests* termination.
|
|
829
829
|
// Returning immediately after TerminateThread lets the caller (execShellcode
|
|
830
830
|
// finally-block, injectDll) free memory the zombie thread may still be
|
|
831
831
|
// executing in. Wait for the thread object itself to be signaled (max 2s)
|
|
832
832
|
// so a timeout-return really means the thread is dead. NOTE: terminating a
|
|
833
833
|
// thread that was mid-LoadLibrary can still leave the target's loader lock
|
|
834
|
-
// held
|
|
834
|
+
// held - an inherent TerminateThread hazard; callers needing guaranteed
|
|
835
835
|
// non-wedging behavior should prefer a longer timeout over termination.
|
|
836
836
|
if (wr != WAIT_OBJECT_0) {
|
|
837
837
|
TerminateThread(t, 0);
|
|
@@ -862,7 +862,7 @@ Napi::Value DisasmInstructionsSync(const Napi::CallbackInfo& info) {
|
|
|
862
862
|
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
863
863
|
auto count = static_cast<std::size_t>(info[2].As<Napi::Number>().DoubleValue());
|
|
864
864
|
if (count == 0) return Napi::Array::New(env, 0);
|
|
865
|
-
if (count > 256) count = 256; // 256*15
|
|
865
|
+
if (count > 256) count = 256; // 256*15 ~ 4KB window; also prevents count*16 wraparound
|
|
866
866
|
|
|
867
867
|
std::size_t readSize = count * 16; // max x64 instruction length is 15
|
|
868
868
|
if (readSize > 4096) readSize = 4096;
|
|
@@ -882,7 +882,7 @@ Napi::Value DisasmInstructionsSync(const Napi::CallbackInfo& info) {
|
|
|
882
882
|
std::size_t off = 0, n = 0;
|
|
883
883
|
while (off < buf.size() && n < count) {
|
|
884
884
|
// hde64_disasm takes no length and may read up to 15 bytes for one
|
|
885
|
-
// instruction
|
|
885
|
+
// instruction - feeding it buf.data()+off directly walks past the end of
|
|
886
886
|
// the allocation on a truncated tail. Decode from a 16-byte padded stack
|
|
887
887
|
// scratch instead; the trailing zeros make a truncated insn fail cleanly.
|
|
888
888
|
std::uint8_t scratch[16] = {};
|
|
@@ -915,18 +915,18 @@ Napi::Value DisasmInstructionsSync(const Napi::CallbackInfo& info) {
|
|
|
915
915
|
// MinHook-style relocation: steals whole instructions until >= minBytes are
|
|
916
916
|
// covered, then rewrites position-relative operands so the stolen bytes are
|
|
917
917
|
// correct when executed from `stubAddr` (which must be within rel32 range of
|
|
918
|
-
// `address`
|
|
919
|
-
//
|
|
918
|
+
// `address` - the caller's #allocNear guarantees this):
|
|
919
|
+
// - rel32 branch/call (E8/E9/0F8x, xbegin): rel32 rewritten in place, OR
|
|
920
920
|
// copied verbatim when the target is INSIDE the stolen window (MinHook's
|
|
921
|
-
// "internal jump" rule
|
|
922
|
-
//
|
|
921
|
+
// "internal jump" rule - distances are preserved by the copy).
|
|
922
|
+
// - RIP-relative ModRM (mod=00 rm=101): disp32 rewritten in place so it
|
|
923
923
|
// still reaches the original absolute target.
|
|
924
|
-
//
|
|
925
|
-
// the operand is position-independent within the window
|
|
924
|
+
// - rel8/rel16 branches incl. LOOP/JECXZ (E0-E3): verbatim if internal -
|
|
925
|
+
// the operand is position-independent within the window - otherwise
|
|
926
926
|
// REFUSED (rel8 can't be widened to a same-length rewrite; MinHook
|
|
927
927
|
// refuses LOOP/JECXZ-external too).
|
|
928
928
|
// Without `stubAddr` every position-relative instruction is refused
|
|
929
|
-
// (HOOK_RELOCATE)
|
|
929
|
+
// (HOOK_RELOCATE) - the old verify-only behavior for raw inspection calls.
|
|
930
930
|
// Throws HOOK_STEAL when <minBytes of decodable instructions exist, or
|
|
931
931
|
// HOOK_RELOCATE naming the offending opcode/offset. Returns the stolen bytes
|
|
932
932
|
// so the caller doesn't need a second remote read.
|
|
@@ -942,9 +942,9 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
|
|
|
942
942
|
std::size_t minBytes = 5;
|
|
943
943
|
std::uintptr_t stub = 0;
|
|
944
944
|
// Accepted arg shapes:
|
|
945
|
-
// hookSteal(h, a, 5)
|
|
946
|
-
// hookSteal(h, a, 5, stub)
|
|
947
|
-
// hookSteal(h, a, {minBytes, stubAddr})
|
|
945
|
+
// hookSteal(h, a, 5) - minBytes number
|
|
946
|
+
// hookSteal(h, a, 5, stub) - minBytes + stubAddr (number|bigint)
|
|
947
|
+
// hookSteal(h, a, {minBytes, stubAddr}) - options object
|
|
948
948
|
if (info.Length() > 2) {
|
|
949
949
|
if (info[2].IsNumber()) {
|
|
950
950
|
double m = info[2].As<Napi::Number>().DoubleValue();
|
|
@@ -967,7 +967,7 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
|
|
|
967
967
|
}
|
|
968
968
|
}
|
|
969
969
|
}
|
|
970
|
-
// An instruction beginning <15 bytes before minBytes can overshoot past it
|
|
970
|
+
// An instruction beginning <15 bytes before minBytes can overshoot past it -
|
|
971
971
|
// read minBytes+15 so no valid tail instruction truncates.
|
|
972
972
|
std::vector<std::uint8_t> buf;
|
|
973
973
|
try {
|
|
@@ -986,7 +986,7 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
|
|
|
986
986
|
err.ThrowAsJavaScriptException();
|
|
987
987
|
};
|
|
988
988
|
|
|
989
|
-
// Pass 1: decode
|
|
989
|
+
// Pass 1: decode - collect each instruction's decode state for relocation.
|
|
990
990
|
struct StolenInsn {
|
|
991
991
|
std::size_t off; // offset within buf
|
|
992
992
|
unsigned len;
|
|
@@ -1009,14 +1009,14 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
|
|
|
1009
1009
|
if (len == 0 || (hs.flags & F_ERROR)) {
|
|
1010
1010
|
char msg[128];
|
|
1011
1011
|
std::snprintf(msg, sizeof(msg),
|
|
1012
|
-
"hookSteal: undecodable instruction at +%zu
|
|
1012
|
+
"hookSteal: undecodable instruction at +%zu - refusing to steal", off);
|
|
1013
1013
|
throwAt("HOOK_STEAL", msg);
|
|
1014
1014
|
return env.Null();
|
|
1015
1015
|
}
|
|
1016
1016
|
if (off + len > buf.size()) {
|
|
1017
1017
|
char msg[128];
|
|
1018
1018
|
std::snprintf(msg, sizeof(msg),
|
|
1019
|
-
"hookSteal: truncated instruction at +%zu
|
|
1019
|
+
"hookSteal: truncated instruction at +%zu - cannot cover %zu bytes", off, minBytes);
|
|
1020
1020
|
throwAt("HOOK_STEAL", msg);
|
|
1021
1021
|
return env.Null();
|
|
1022
1022
|
}
|
|
@@ -1028,7 +1028,7 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
|
|
|
1028
1028
|
si.opcode2 = hs.opcode2;
|
|
1029
1029
|
si.ripRel = (hs.flags & F_MODRM) && hs.modrm_mod == 0 && hs.modrm_rm == 5;
|
|
1030
1030
|
// Field sizes: the immediate/disp land at the tail of the instruction.
|
|
1031
|
-
// imm is the LAST field; disp precedes it
|
|
1031
|
+
// imm is the LAST field; disp precedes it - MinHook's offset math:
|
|
1032
1032
|
// disp field = len - immSize - dispSize (same as len-((flags&0x3C)>>2)-4)
|
|
1033
1033
|
si.immSize = (hs.flags & F_IMM64) ? 8 :
|
|
1034
1034
|
(hs.flags & F_IMM32) ? 4 :
|
|
@@ -1045,14 +1045,14 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
|
|
|
1045
1045
|
if (off < minBytes) {
|
|
1046
1046
|
char msg[128];
|
|
1047
1047
|
std::snprintf(msg, sizeof(msg),
|
|
1048
|
-
"hookSteal: only %zu decodable bytes at 0x%llX
|
|
1048
|
+
"hookSteal: only %zu decodable bytes at 0x%llX - need >= %zu",
|
|
1049
1049
|
off, static_cast<unsigned long long>(addr), minBytes);
|
|
1050
1050
|
throwAt("HOOK_STEAL", msg);
|
|
1051
1051
|
return env.Null();
|
|
1052
1052
|
}
|
|
1053
1053
|
const std::size_t stolenLen = off;
|
|
1054
1054
|
|
|
1055
|
-
// Pass 2: relocate
|
|
1055
|
+
// Pass 2: relocate - rewrite position-relative operands in the copied
|
|
1056
1056
|
// bytes. `bytes` becomes the relocated copy (what the caller writes to the
|
|
1057
1057
|
// stub); `original` keeps the pristine bytes for unhook.
|
|
1058
1058
|
std::vector<std::uint8_t> original(buf.begin(), buf.begin() + stolenLen);
|
|
@@ -1091,18 +1091,18 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
|
|
|
1091
1091
|
const std::uintptr_t oldTarget =
|
|
1092
1092
|
static_cast<std::uintptr_t>(addr + si.off + si.len + oldRel);
|
|
1093
1093
|
// Internal jump: target lands inside the stolen window. The copy
|
|
1094
|
-
// preserves the operand
|
|
1094
|
+
// preserves the operand->target distance - leave the bytes verbatim.
|
|
1095
1095
|
if (oldTarget >= addr && oldTarget < addr + stolenLen) continue;
|
|
1096
1096
|
if (si.immSize != 4) {
|
|
1097
1097
|
return refuse("rel8/rel16 external target cannot be widened in place "
|
|
1098
|
-
"(MinHook refuses LOOP/JECXZ-external too)
|
|
1098
|
+
"(MinHook refuses LOOP/JECXZ-external too) - hook a "
|
|
1099
1099
|
"different instruction-aligned site");
|
|
1100
1100
|
}
|
|
1101
1101
|
const std::int64_t newRel =
|
|
1102
1102
|
static_cast<std::int64_t>(oldTarget) -
|
|
1103
1103
|
(static_cast<std::int64_t>(stub) + si.off + si.len);
|
|
1104
1104
|
if (newRel < INT32_MIN || newRel > INT32_MAX) {
|
|
1105
|
-
return refuse("rel32 rewrite lands out of
|
|
1105
|
+
return refuse("rel32 rewrite lands out of +/-2GB range from the stub - "
|
|
1106
1106
|
"allocate the stub nearer to the branch target");
|
|
1107
1107
|
}
|
|
1108
1108
|
const std::int32_t r32 = static_cast<std::int32_t>(newRel);
|
|
@@ -1111,7 +1111,7 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
|
|
|
1111
1111
|
}
|
|
1112
1112
|
|
|
1113
1113
|
// RIP-relative ModRM memory operand: the disp32 field is the position-
|
|
1114
|
-
// dependent part
|
|
1114
|
+
// dependent part - rewrite it to reach the same absolute address.
|
|
1115
1115
|
if (si.ripRel) {
|
|
1116
1116
|
if (si.dispSize != 4) continue; // mod=00 rm=101 always disp32
|
|
1117
1117
|
std::int32_t d32 = 0;
|
|
@@ -1123,7 +1123,7 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
|
|
|
1123
1123
|
static_cast<std::int64_t>(oldTarget) -
|
|
1124
1124
|
(static_cast<std::int64_t>(stub) + si.off + si.len);
|
|
1125
1125
|
if (newDisp < INT32_MIN || newDisp > INT32_MAX) {
|
|
1126
|
-
return refuse("disp32 rewrite lands out of
|
|
1126
|
+
return refuse("disp32 rewrite lands out of +/-2GB range from the stub - "
|
|
1127
1127
|
"allocate the stub nearer to the operand target");
|
|
1128
1128
|
}
|
|
1129
1129
|
const std::int32_t n32 = static_cast<std::int32_t>(newDisp);
|
|
@@ -1144,7 +1144,7 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
|
|
|
1144
1144
|
|
|
1145
1145
|
// suspendProcess(pid) / resumeProcess(pid) via undocumented-but-stable
|
|
1146
1146
|
// NtSuspendProcess / NtResumeProcess exported by ntdll. Atomic whole-process
|
|
1147
|
-
// suspend
|
|
1147
|
+
// suspend - strictly better than per-thread suspendAll (no TOCTOU where a
|
|
1148
1148
|
// new thread spawns mid-enumeration).
|
|
1149
1149
|
static Napi::Value NtSuspendResume(const Napi::CallbackInfo& info, bool suspend) {
|
|
1150
1150
|
Napi::Env env = info.Env();
|
|
@@ -1153,11 +1153,11 @@ static Napi::Value NtSuspendResume(const Napi::CallbackInfo& info, bool suspend)
|
|
|
1153
1153
|
return env.Null();
|
|
1154
1154
|
}
|
|
1155
1155
|
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
|
1156
|
-
// QA4-C3: suspending OUR OWN process freezes the JS engine
|
|
1156
|
+
// QA4-C3: suspending OUR OWN process freezes the JS engine - even though
|
|
1157
1157
|
// NtSuspendProcess skips the calling thread, every other thread (including
|
|
1158
1158
|
// any that would resume us) is suspended and the object can never reach
|
|
1159
1159
|
// resumeProcess(). Suspend on self-pid is an unrecoverable deadlock; refuse
|
|
1160
|
-
// with a clear error. Resume on self-pid is harmless
|
|
1160
|
+
// with a clear error. Resume on self-pid is harmless - it just clears
|
|
1161
1161
|
// suspend counts on our own threads.
|
|
1162
1162
|
if (suspend && pid == GetCurrentProcessId()) {
|
|
1163
1163
|
auto err = Napi::Error::New(env,
|
|
@@ -1271,13 +1271,13 @@ Napi::Value WaitForExitAsync(const Napi::CallbackInfo& info) {
|
|
|
1271
1271
|
// ---------- findWhatWrites (DebugActiveProcess + HW write breakpoints) ----------
|
|
1272
1272
|
|
|
1273
1273
|
// Hardware breakpoints only deliver their #DB exception to a debugger attached
|
|
1274
|
-
// to the TARGET
|
|
1274
|
+
// to the TARGET - setting Dr regs without a debug loop just crashes the victim
|
|
1275
1275
|
// with an unhandled EXCEPTION_SINGLE_STEP. So the CE "find what writes" flow
|
|
1276
1276
|
// must own a real debugger session: DebugActiveProcess -> arm RW=01 data
|
|
1277
1277
|
// breakpoints on every thread -> pump WaitForDebugEvent collecting
|
|
1278
1278
|
// EXCEPTION_SINGLE_STEP hits -> restore Dr regs, ContinueDebugEvent, detach.
|
|
1279
1279
|
//
|
|
1280
|
-
// NOTE on `rip`: a data breakpoint is a *trap*
|
|
1280
|
+
// NOTE on `rip`: a data breakpoint is a *trap* - it fires AFTER the write, so
|
|
1281
1281
|
// the reported address is the instruction FOLLOWING the write, not the writer
|
|
1282
1282
|
// itself. CE shows the same caveat; disassemble backwards to find the store.
|
|
1283
1283
|
|
|
@@ -1379,7 +1379,7 @@ public:
|
|
|
1379
1379
|
// protected targets. DebugSetProcessKillOnExit(false) keeps the debuggee
|
|
1380
1380
|
// alive when our process exits while still attached.
|
|
1381
1381
|
void Execute() override {
|
|
1382
|
-
// Detach completion is asynchronous on Windows
|
|
1382
|
+
// Detach completion is asynchronous on Windows - a fresh
|
|
1383
1383
|
// DebugActiveProcess immediately after DebugActiveProcessStop can return
|
|
1384
1384
|
// ERROR_ACCESS_DENIED while the kernel tears down the debug port. Retry
|
|
1385
1385
|
// for up to ~500ms so back-to-back findWhatWrites calls work.
|
|
@@ -1399,7 +1399,7 @@ public:
|
|
|
1399
1399
|
// QA4-M3 (lifecycle): wrap all post-attach phases so ANY C++ exception
|
|
1400
1400
|
// (bad_alloc under a flooding target, length_error in hits_/armed_
|
|
1401
1401
|
// growth) still runs disarm+drain+Stop. Without this the debuggee is
|
|
1402
|
-
// left attached with armed Dr7 breakpoints and suspended threads
|
|
1402
|
+
// left attached with armed Dr7 breakpoints and suspended threads - the
|
|
1403
1403
|
// next write to the watched addr would fire an unhandled #DB and kill it.
|
|
1404
1404
|
try {
|
|
1405
1405
|
RunPhases();
|
|
@@ -1423,7 +1423,7 @@ public:
|
|
|
1423
1423
|
// LOAD_DLLs, CREATE_THREAD per existing thread, initial breakpoint) and
|
|
1424
1424
|
// arm each reported thread. Threads reported by CREATE_THREAD are already
|
|
1425
1425
|
// debug-suspended at this point; our suspend/resume pair is balanced so
|
|
1426
|
-
// the bp is on before they can run. Hits are collected here too
|
|
1426
|
+
// the bp is on before they can run. Hits are collected here too - a thread
|
|
1427
1427
|
// armed+continued early in this loop can fire while we drain the rest.
|
|
1428
1428
|
int quietWaits = 0;
|
|
1429
1429
|
while (quietWaits < 3 && GetTickCount64() < armDeadline &&
|
|
@@ -1433,7 +1433,7 @@ public:
|
|
|
1433
1433
|
quietWaits = 0;
|
|
1434
1434
|
ContinueDebugEvent(ev.dwProcessId, ev.dwThreadId, HandleEvent(ev));
|
|
1435
1435
|
}
|
|
1436
|
-
// Fallback: arm anything the event burst missed (rare
|
|
1436
|
+
// Fallback: arm anything the event burst missed (rare - attach reports
|
|
1437
1437
|
// every thread, but belt-and-suspenders for filtered/racy cases).
|
|
1438
1438
|
ArmSnapshotThreads();
|
|
1439
1439
|
|
|
@@ -1446,10 +1446,10 @@ public:
|
|
|
1446
1446
|
}
|
|
1447
1447
|
}
|
|
1448
1448
|
|
|
1449
|
-
// Phase 3: disarm every armed thread (restoring ORIGINAL Dr regs
|
|
1449
|
+
// Phase 3: disarm every armed thread (restoring ORIGINAL Dr regs - the
|
|
1450
1450
|
// target may legitimately use its own hw breakpoints), drain pending
|
|
1451
1451
|
// events, disarm any stragglers hit mid-disarm, drain again, detach.
|
|
1452
|
-
// Everything runs on this thread
|
|
1452
|
+
// Everything runs on this thread - DebugActiveProcess/Continue/DebugStop
|
|
1453
1453
|
// are debugger-thread-bound. Idempotent: runs only while attached_ and is
|
|
1454
1454
|
// invoked from every Execute() exit path (normal + exception).
|
|
1455
1455
|
void CleanupDebuggee() {
|
|
@@ -1472,7 +1472,7 @@ public:
|
|
|
1472
1472
|
CloseHandle(ev.u.CreateProcessInfo.hFile);
|
|
1473
1473
|
if (ev.dwDebugEventCode == LOAD_DLL_DEBUG_EVENT && ev.u.LoadDll.hFile)
|
|
1474
1474
|
CloseHandle(ev.u.LoadDll.hFile);
|
|
1475
|
-
// A queued single-step is our bp firing mid-disarm
|
|
1475
|
+
// A queued single-step is our bp firing mid-disarm - swallow it
|
|
1476
1476
|
// (DBG_CONTINUE). A queued genuine exception gets NOT_HANDLED so it
|
|
1477
1477
|
// reaches the debuggee's SEH as it would without us.
|
|
1478
1478
|
DWORD disp = DBG_CONTINUE;
|
|
@@ -1489,7 +1489,7 @@ public:
|
|
|
1489
1489
|
disarmAll();
|
|
1490
1490
|
drain(); // flush pending bp hits + resume stopped threads
|
|
1491
1491
|
disarmAll(); // threads stopped on a queued event during pass 1 skipped
|
|
1492
|
-
// SetThreadContext
|
|
1492
|
+
// SetThreadContext - disarm them now that they're resumed
|
|
1493
1493
|
drain(); // release the last stopped threads, empty the port
|
|
1494
1494
|
DebugActiveProcessStop(pid_);
|
|
1495
1495
|
attached_ = false;
|
|
@@ -1543,7 +1543,7 @@ private:
|
|
|
1543
1543
|
const DWORD tid = ev.dwThreadId;
|
|
1544
1544
|
// QA4-m4: hThread here is system-owned (unlike CREATE_PROCESS's hFile
|
|
1545
1545
|
// which the debugger must close). Microsoft's debug samples and docs
|
|
1546
|
-
// never close it
|
|
1546
|
+
// never close it - the system cleans it up on EXIT_THREAD. Verified:
|
|
1547
1547
|
// do NOT close; closing could later collide with a recycled handle
|
|
1548
1548
|
// value the debugger also sees. Disarm uses OpenThread-by-tid anyway.
|
|
1549
1549
|
HANDLE t = ev.u.CreateThread.hThread;
|
|
@@ -1560,7 +1560,7 @@ private:
|
|
|
1560
1560
|
return DBG_CONTINUE;
|
|
1561
1561
|
}
|
|
1562
1562
|
case EXIT_THREAD_DEBUG_EVENT:
|
|
1563
|
-
// If it was armed there's nothing to restore
|
|
1563
|
+
// If it was armed there's nothing to restore - the thread is gone.
|
|
1564
1564
|
return DBG_CONTINUE;
|
|
1565
1565
|
case EXIT_PROCESS_DEBUG_EVENT:
|
|
1566
1566
|
exited_ = true;
|
|
@@ -1690,7 +1690,7 @@ Napi::Value FindWhatWritesAsync(const Napi::CallbackInfo& info) {
|
|
|
1690
1690
|
if (std::isfinite(m) && m >= 1) maxHits = m > 1024 ? 1024u : static_cast<unsigned>(m);
|
|
1691
1691
|
}
|
|
1692
1692
|
|
|
1693
|
-
// QA4-H7: CONTEXT/Dr registers use the x64 struct
|
|
1693
|
+
// QA4-H7: CONTEXT/Dr registers use the x64 struct - on a WoW64 (32-bit)
|
|
1694
1694
|
// target Get/SetThreadContext operate on WOW64_CONTEXT and the arm would
|
|
1695
1695
|
// silently corrupt the debuggee or never fire. Refuse up front (same
|
|
1696
1696
|
// pattern as injectDll).
|
package/native/pattern.cc
CHANGED
|
@@ -10,7 +10,7 @@ namespace mp {
|
|
|
10
10
|
struct PatternByte { std::uint8_t value; bool wildcard; };
|
|
11
11
|
|
|
12
12
|
// Parse a hex-byte pattern ("DE AD ?? EF"). Tokens must be 1-2 hex digits or
|
|
13
|
-
// ?/?? wildcards. Returns nullopt on ANY malformed token
|
|
13
|
+
// ?/?? wildcards. Returns nullopt on ANY malformed token - the caller turns
|
|
14
14
|
// this into a JS error; no C++ exception can cross the N-API boundary.
|
|
15
15
|
static std::optional<std::vector<PatternByte>> ParsePattern(const std::string& pat) {
|
|
16
16
|
std::vector<PatternByte> out;
|
|
@@ -60,7 +60,7 @@ static inline bool RegionWritable(const MEMORY_BASIC_INFORMATION& mbi) {
|
|
|
60
60
|
// Async region-scan worker shared by the AOB scan and the typed-value scan.
|
|
61
61
|
// Runs with a PRIVATE least-privilege process handle opened from the pid (a
|
|
62
62
|
// JS-side close() can never invalidate it mid-scan), and scans each committed
|
|
63
|
-
// range with a rolling window
|
|
63
|
+
// range with a rolling window - 64KB chunks plus a (needle-1)-byte overlap -
|
|
64
64
|
// so multi-hundred-MB regions never accumulate in memory.
|
|
65
65
|
class ScanWorker : public Napi::AsyncWorker {
|
|
66
66
|
public:
|
|
@@ -84,7 +84,7 @@ public:
|
|
|
84
84
|
if (!proc_) { SetError("OpenProcess failed (pid gone or access denied)"); return; }
|
|
85
85
|
|
|
86
86
|
// QA4 lifecycle-M1: a C++ exception (bad_alloc in hits_ growth or the
|
|
87
|
-
// per-chunk vector) escaping Execute() used to skip CloseHandle
|
|
87
|
+
// per-chunk vector) escaping Execute() used to skip CloseHandle - a real
|
|
88
88
|
// handle leak under memory pressure, every scan call. Guard all exits.
|
|
89
89
|
try {
|
|
90
90
|
const std::uintptr_t limit = end_ ? end_ : USER_TOP;
|
|
@@ -156,7 +156,7 @@ private:
|
|
|
156
156
|
if (!ReadProcessMemory(proc_, reinterpret_cast<LPCVOID>(pos),
|
|
157
157
|
chunk.data(), toRead, &got) || got == 0) {
|
|
158
158
|
tail.clear(); // VA discontinuity: a match can't straddle a failed read
|
|
159
|
-
// Don't blindly skip the whole 64KB chunk
|
|
159
|
+
// Don't blindly skip the whole 64KB chunk - a single inaccessible
|
|
160
160
|
// page inside a readable region would drop signatures CE finds.
|
|
161
161
|
// Re-query the sub-region at `pos`: skip only up to its end, or just
|
|
162
162
|
// one 4KB page when the sub-region still claims to be readable (the
|
|
@@ -185,7 +185,7 @@ private:
|
|
|
185
185
|
win.insert(win.end(), tail.end() - static_cast<std::ptrdiff_t>(keep), tail.end());
|
|
186
186
|
win.insert(win.end(), chunk.begin(), chunk.end());
|
|
187
187
|
const std::uintptr_t winBase = pos - keep;
|
|
188
|
-
// match starts that touch
|
|
188
|
+
// match starts that touch >=1 new byte: i >= keep - overlap (clamped 0)
|
|
189
189
|
const std::size_t firstI = keep > overlap ? keep - overlap : 0;
|
|
190
190
|
if (win.size() >= patLen) {
|
|
191
191
|
const std::size_t lastI = win.size() - patLen; // inclusive
|
package/native/process.cc
CHANGED
|
@@ -61,7 +61,7 @@ static std::string WideToUtf8(const wchar_t* w) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
std::vector<ModuleInfo> GetModules(HANDLE /*proc*/, DWORD pid) {
|
|
64
|
-
// Toolhelp snapshot
|
|
64
|
+
// Toolhelp snapshot - unlike EnumProcessModulesEx this lists BOTH 64-bit and
|
|
65
65
|
// 32-bit (WoW64) modules from a 64-bit reader, has no fixed-cap buffer, and
|
|
66
66
|
// hands us base+size+path in one shot.
|
|
67
67
|
std::vector<ModuleInfo> out;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memoryprocess/bindings",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3-beta.9",
|
|
4
4
|
"description": "Native N-API bindings for Windows process memory access (read/write, VirtualQueryEx, pattern scan, thread ops). The .node binary layer of the memoryprocess toolkit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1.4.4",
|
|
@@ -31,8 +31,7 @@
|
|
|
31
31
|
"clean": "node scripts/clean.mjs",
|
|
32
32
|
"check": "bun check",
|
|
33
33
|
"typecheck": "tsc --noEmit",
|
|
34
|
-
"prebuild:all": "prebuildify --
|
|
35
|
-
"prepublishOnly": "npm run build && npm run prebuild:all"
|
|
34
|
+
"prebuild:all": "prebuildify --napi -t 22.17.0 -t 24.4.0 -t 25.0.0 -t 26.0.0"
|
|
36
35
|
},
|
|
37
36
|
"repository": {
|
|
38
37
|
"type": "git",
|
|
Binary file
|