@memoryprocess/bindings 0.0.3-beta.9 → 0.1.0-beta.0
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/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/memoryprocess.node +0 -0
- package/native/module.cc +57 -18
- package/native/pattern.cc +2 -2
- package/package.json +8 -2
- package/prebuilds/win32-x64/@memoryprocess+bindings.node +0 -0
- package/src/index.ts +6 -0
- package/src/native.ts +209 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 JoShMiQueL
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# @memoryprocess/bindings
|
|
2
|
+
|
|
3
|
+
The raw N-API native layer of the [memoryprocess](https://github.com/memoryprocess/memoryprocess) toolkit: the `.node` binary plus a typed TypeScript interface over the stateless process-memory functions (open/close, read/write, VirtualQueryEx walk, pattern scan, thread ops, hooks primitives).
|
|
4
|
+
|
|
5
|
+
Most consumers want [`@memoryprocess/core`](https://www.npmjs.com/package/@memoryprocess/core) instead -- it wraps this layer in the `Process` class with typed reads/writes, CE-style scanning, and remote structs.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @memoryprocess/bindings
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Windows only (`"os": ["win32"]`), Node >= 22. Prebuilt binaries ship under `prebuilds/` for the CI-covered Node majors via `node-gyp-build`.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { native } from '@memoryprocess/bindings';
|
|
19
|
+
|
|
20
|
+
const { handle, pid } = native.openProcess('notepad.exe');
|
|
21
|
+
try {
|
|
22
|
+
const mods = native.getModules(handle, pid);
|
|
23
|
+
const buf = native.readBuffer(handle, Number(mods[0].base), 16);
|
|
24
|
+
} finally {
|
|
25
|
+
native.closeHandle(handle);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Every function is stateless: it takes a `handle`/`pid`/`tid` argument and holds no shared native state. The full function surface is declared in `src/native.ts` (`Native` interface).
|
|
30
|
+
|
|
31
|
+
## Binary resolution
|
|
32
|
+
|
|
33
|
+
At load time the addon resolves in order:
|
|
34
|
+
|
|
35
|
+
1. `dist/memoryprocess.node` (bundled in the published tarball)
|
|
36
|
+
2. `prebuilds/<platform>-<arch>/memoryprocess.node` via `node-gyp-build`
|
|
37
|
+
3. `build/Release/memoryprocess.node` (local `node-gyp` build)
|
|
38
|
+
|
|
39
|
+
Future Node majors not covered by the shipped prebuilds will need a local `node-gyp rebuild` (`binding.gyp` + `native/` sources are included in the tarball), or wait for a new release.
|
|
40
|
+
|
|
41
|
+
## Part of
|
|
42
|
+
|
|
43
|
+
[`memoryprocess`](https://github.com/memoryprocess/memoryprocess) -- a Windows process-memory toolkit (`@memoryprocess/{bindings,core,cli,mcp}`), a ground-up rewrite of `memoryjs`.
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
MIT
|
package/dist/memoryprocess.node
CHANGED
|
Binary file
|
package/native/module.cc
CHANGED
|
@@ -21,6 +21,14 @@ static const double MAX_READ_BYTES = 512.0 * 1024.0 * 1024.0;
|
|
|
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
|
|
|
24
|
+
// Converts a JS number|bigint to u64. Numbers are truncated via the double -
|
|
25
|
+
// callers document the >2^53 rounding caveat (see header note above).
|
|
26
|
+
static bool JsToU64(const Napi::Value& v, std::uint64_t& out) {
|
|
27
|
+
if (v.IsBigInt()) { bool ls; out = v.As<Napi::BigInt>().Uint64Value(&ls); return true; }
|
|
28
|
+
if (v.IsNumber()) { out = static_cast<std::uint64_t>(v.As<Napi::Number>().DoubleValue()); return true; }
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
static bool CheckSize(Napi::Env env, double size) {
|
|
25
33
|
if (!std::isfinite(size) || size < 0 || size > MAX_READ_BYTES) {
|
|
26
34
|
Napi::RangeError::New(env, "size must be a finite number between 0 and 512MB")
|
|
@@ -133,7 +141,9 @@ Napi::Value GetRegionsSync(const Napi::CallbackInfo& info) {
|
|
|
133
141
|
|
|
134
142
|
Napi::Value ReadBufferSync(const Napi::CallbackInfo& info) {
|
|
135
143
|
Napi::Env env = info.Env();
|
|
136
|
-
if (info.Length() < 3
|
|
144
|
+
if (info.Length() < 3 || !info[0].IsNumber() || !info[1].IsNumber() || !info[2].IsNumber()) {
|
|
145
|
+
Napi::TypeError::New(env, "expected (handle:number,address:number,size:number)").ThrowAsJavaScriptException(); return env.Null();
|
|
146
|
+
}
|
|
137
147
|
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
138
148
|
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
139
149
|
double sizeD = info[2].As<Napi::Number>().DoubleValue();
|
|
@@ -153,7 +163,9 @@ Napi::Value ReadBufferSync(const Napi::CallbackInfo& info) {
|
|
|
153
163
|
|
|
154
164
|
Napi::Value WriteBufferSync(const Napi::CallbackInfo& info) {
|
|
155
165
|
Napi::Env env = info.Env();
|
|
156
|
-
if (info.Length() < 3
|
|
166
|
+
if (info.Length() < 3 || !info[0].IsNumber() || !info[1].IsNumber()) {
|
|
167
|
+
Napi::TypeError::New(env, "expected (handle:number,address:number,buffer:Buffer)").ThrowAsJavaScriptException(); return env.Null();
|
|
168
|
+
}
|
|
157
169
|
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
158
170
|
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
159
171
|
if (!info[2].IsBuffer()) { Napi::TypeError::New(env, "value must be a Buffer").ThrowAsJavaScriptException(); return env.Null(); }
|
|
@@ -169,7 +181,9 @@ Napi::Value WriteBufferSync(const Napi::CallbackInfo& info) {
|
|
|
169
181
|
// trampoline stubs within rel32 range (+/-2GB) of a hook site.
|
|
170
182
|
Napi::Value AllocMemorySync(const Napi::CallbackInfo& info) {
|
|
171
183
|
Napi::Env env = info.Env();
|
|
172
|
-
if (info.Length() < 3
|
|
184
|
+
if (info.Length() < 3 || !info[0].IsNumber() || !info[1].IsNumber() || !info[2].IsNumber()) {
|
|
185
|
+
Napi::TypeError::New(env, "expected (handle:number,size:number,protection:number[,nearAddress:number])").ThrowAsJavaScriptException(); return env.Null();
|
|
186
|
+
}
|
|
173
187
|
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
174
188
|
auto size = static_cast<SIZE_T>(info[1].As<Napi::Number>().DoubleValue());
|
|
175
189
|
DWORD prot = info[2].As<Napi::Number>().Uint32Value();
|
|
@@ -183,7 +197,9 @@ Napi::Value AllocMemorySync(const Napi::CallbackInfo& info) {
|
|
|
183
197
|
|
|
184
198
|
Napi::Value FreeMemorySync(const Napi::CallbackInfo& info) {
|
|
185
199
|
Napi::Env env = info.Env();
|
|
186
|
-
if (info.Length() < 2
|
|
200
|
+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
|
|
201
|
+
Napi::TypeError::New(env, "expected (handle:number,address:number)").ThrowAsJavaScriptException(); return env.Null();
|
|
202
|
+
}
|
|
187
203
|
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
188
204
|
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
189
205
|
return Napi::Boolean::New(env, VirtualFreeEx(h, reinterpret_cast<LPVOID>(addr), 0, MEM_RELEASE) != 0);
|
|
@@ -191,7 +207,10 @@ Napi::Value FreeMemorySync(const Napi::CallbackInfo& info) {
|
|
|
191
207
|
|
|
192
208
|
Napi::Value ProtectMemorySync(const Napi::CallbackInfo& info) {
|
|
193
209
|
Napi::Env env = info.Env();
|
|
194
|
-
if (info.Length() <
|
|
210
|
+
if (info.Length() < 4 || !info[0].IsNumber() || !info[1].IsNumber() ||
|
|
211
|
+
!info[2].IsNumber() || !info[3].IsNumber()) {
|
|
212
|
+
Napi::TypeError::New(env, "expected (handle:number,address:number,size:number,prot:number)").ThrowAsJavaScriptException(); return env.Null();
|
|
213
|
+
}
|
|
195
214
|
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
196
215
|
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
197
216
|
auto size = static_cast<SIZE_T>(info[2].As<Napi::Number>().DoubleValue());
|
|
@@ -267,7 +286,9 @@ Napi::Value ResumeThreadSync(const Napi::CallbackInfo& info) {
|
|
|
267
286
|
// "int32","uint32","int64","uint64","float","double","pointer"
|
|
268
287
|
Napi::Value ReadValueSync(const Napi::CallbackInfo& info) {
|
|
269
288
|
Napi::Env env = info.Env();
|
|
270
|
-
if (info.Length() < 3
|
|
289
|
+
if (info.Length() < 3 || !info[0].IsNumber() || !info[1].IsNumber() || !info[2].IsString()) {
|
|
290
|
+
Napi::TypeError::New(env, "expected (handle:number,address:number,type:string)").ThrowAsJavaScriptException(); return env.Null();
|
|
291
|
+
}
|
|
271
292
|
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
272
293
|
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
273
294
|
std::string type = info[2].As<Napi::String>().Utf8Value();
|
|
@@ -292,19 +313,39 @@ Napi::Value ReadValueSync(const Napi::CallbackInfo& info) {
|
|
|
292
313
|
|
|
293
314
|
Napi::Value WriteValueSync(const Napi::CallbackInfo& info) {
|
|
294
315
|
Napi::Env env = info.Env();
|
|
295
|
-
if (info.Length() < 4
|
|
316
|
+
if (info.Length() < 4 || !info[0].IsNumber() || !info[1].IsNumber() || !info[2].IsString()) {
|
|
317
|
+
Napi::TypeError::New(env, "expected (handle:number,address:number,type:string,value:number|bigint)").ThrowAsJavaScriptException(); return env.Null();
|
|
318
|
+
}
|
|
296
319
|
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
297
320
|
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
298
321
|
std::string type = info[2].As<Napi::String>().Utf8Value();
|
|
322
|
+
// `value` must match the type family: number for int32/uint32/float/double;
|
|
323
|
+
// number|bigint for int64/uint64/pointer (converted via JsToU64 - the TS
|
|
324
|
+
// signature declares `number|bigint`). The old As<BigInt>() on a Number was
|
|
325
|
+
// an unchecked static_cast: Int64Value() failed, the out-param stayed
|
|
326
|
+
// uninitialized and WriteProcessMemory wrote stack garbage. No C++
|
|
327
|
+
// exception is thrown under NAPI_DISABLE_CPP_EXCEPTIONS, so catch(...)
|
|
328
|
+
// could not save it - validate BEFORE converting.
|
|
329
|
+
const bool is64 = (type == "int64" || type == "uint64" || type == "pointer");
|
|
330
|
+
const bool is32 = (type == "int32" || type == "uint32" || type == "float" || type == "double");
|
|
331
|
+
if (is32 && !info[3].IsNumber()) {
|
|
332
|
+
Napi::TypeError::New(env, "value must be a number for type " + type).ThrowAsJavaScriptException();
|
|
333
|
+
return env.Null();
|
|
334
|
+
}
|
|
335
|
+
std::uint64_t u64 = 0;
|
|
336
|
+
if (is64 && !JsToU64(info[3], u64)) {
|
|
337
|
+
Napi::TypeError::New(env, "value must be number|bigint for type " + type).ThrowAsJavaScriptException();
|
|
338
|
+
return env.Null();
|
|
339
|
+
}
|
|
299
340
|
bool ok = false;
|
|
300
341
|
try {
|
|
301
342
|
if (type == "int32") ok = WriteValue<std::int32_t>(h, addr, info[3].As<Napi::Number>().Int32Value());
|
|
302
343
|
else if (type == "uint32") ok = WriteValue<std::uint32_t>(h, addr, info[3].As<Napi::Number>().Uint32Value());
|
|
303
344
|
else if (type == "float") ok = WriteValue<float>(h, addr, info[3].As<Napi::Number>().FloatValue());
|
|
304
345
|
else if (type == "double") ok = WriteValue<double>(h, addr, info[3].As<Napi::Number>().DoubleValue());
|
|
305
|
-
else if (type == "int64")
|
|
306
|
-
else if (type == "uint64")
|
|
307
|
-
else if (type == "pointer")
|
|
346
|
+
else if (type == "int64") ok = WriteValue<std::int64_t>(h, addr, static_cast<std::int64_t>(u64));
|
|
347
|
+
else if (type == "uint64") ok = WriteValue<std::uint64_t>(h, addr, u64);
|
|
348
|
+
else if (type == "pointer") ok = WriteValue<std::uintptr_t>(h, addr, static_cast<std::uintptr_t>(u64));
|
|
308
349
|
else { Napi::TypeError::New(env, "unknown type " + type).ThrowAsJavaScriptException(); return env.Null(); }
|
|
309
350
|
return Napi::Boolean::New(env, ok);
|
|
310
351
|
} catch (...) {
|
|
@@ -322,7 +363,9 @@ Napi::Value WriteValueSync(const Napi::CallbackInfo& info) {
|
|
|
322
363
|
// gaps/unreadable spans. Only throws if the handle is invalid for VirtualQueryEx.
|
|
323
364
|
Napi::Value ReadSafeSync(const Napi::CallbackInfo& info) {
|
|
324
365
|
Napi::Env env = info.Env();
|
|
325
|
-
if (info.Length() < 3
|
|
366
|
+
if (info.Length() < 3 || !info[0].IsNumber() || !info[1].IsNumber() || !info[2].IsNumber()) {
|
|
367
|
+
Napi::TypeError::New(env, "expected (handle:number,address:number,size:number)").ThrowAsJavaScriptException(); return env.Null();
|
|
368
|
+
}
|
|
326
369
|
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
327
370
|
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
328
371
|
double sizeD = info[2].As<Napi::Number>().DoubleValue();
|
|
@@ -374,7 +417,9 @@ Napi::Value ReadSafeSync(const Napi::CallbackInfo& info) {
|
|
|
374
417
|
// flushInstructions(handle, address, size) -> boolean (FlushInstructionCache)
|
|
375
418
|
Napi::Value FlushInstructionsSync(const Napi::CallbackInfo& info) {
|
|
376
419
|
Napi::Env env = info.Env();
|
|
377
|
-
if (info.Length() < 3
|
|
420
|
+
if (info.Length() < 3 || !info[0].IsNumber() || !info[1].IsNumber() || !info[2].IsNumber()) {
|
|
421
|
+
Napi::TypeError::New(env, "expected (handle:number,address:number,size:number)").ThrowAsJavaScriptException(); return env.Null();
|
|
422
|
+
}
|
|
378
423
|
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
379
424
|
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
380
425
|
auto size = static_cast<SIZE_T>(info[2].As<Napi::Number>().DoubleValue());
|
|
@@ -744,12 +789,6 @@ Napi::Value GetThreadContextSync(const Napi::CallbackInfo& info) {
|
|
|
744
789
|
return o;
|
|
745
790
|
}
|
|
746
791
|
|
|
747
|
-
static bool JsToU64(const Napi::Value& v, std::uint64_t& out) {
|
|
748
|
-
if (v.IsBigInt()) { bool ls; out = v.As<Napi::BigInt>().Uint64Value(&ls); return true; }
|
|
749
|
-
if (v.IsNumber()) { out = static_cast<std::uint64_t>(v.As<Napi::Number>().DoubleValue()); return true; }
|
|
750
|
-
return false;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
792
|
// setThreadContext(tid, { rip?, rsp?, rbp?, dr0?..dr3?, dr6?, dr7?, eflags? }) -> boolean
|
|
754
793
|
// Reads the current context, patches provided fields, writes it back.
|
|
755
794
|
Napi::Value SetThreadContextSync(const Napi::CallbackInfo& info) {
|
package/native/pattern.cc
CHANGED
|
@@ -219,8 +219,8 @@ private:
|
|
|
219
219
|
Napi::Value ScanAsync(const Napi::CallbackInfo& info) {
|
|
220
220
|
Napi::Env env = info.Env();
|
|
221
221
|
// scan(pid, pattern[, {start,end,onlyReadable}]) -> Promise<bigint[]>
|
|
222
|
-
if (info.Length() < 2) {
|
|
223
|
-
Napi::TypeError::New(env, "expected (pid, pattern[, opts])").ThrowAsJavaScriptException();
|
|
222
|
+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsString()) {
|
|
223
|
+
Napi::TypeError::New(env, "expected (pid:number, pattern:string[, opts])").ThrowAsJavaScriptException();
|
|
224
224
|
return env.Null();
|
|
225
225
|
}
|
|
226
226
|
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memoryprocess/bindings",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
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",
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
|
+
"src",
|
|
19
20
|
"prebuilds",
|
|
20
21
|
"binding.gyp",
|
|
21
|
-
"native"
|
|
22
|
+
"native",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"README.md"
|
|
22
25
|
],
|
|
23
26
|
"os": [
|
|
24
27
|
"win32"
|
|
@@ -33,6 +36,9 @@
|
|
|
33
36
|
"typecheck": "tsc --noEmit",
|
|
34
37
|
"prebuild:all": "prebuildify --napi -t 22.17.0 -t 24.4.0 -t 25.0.0 -t 26.0.0"
|
|
35
38
|
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
36
42
|
"repository": {
|
|
37
43
|
"type": "git",
|
|
38
44
|
"url": "git+https://github.com/memoryprocess/memoryprocess.git",
|
|
Binary file
|
package/src/index.ts
ADDED
package/src/native.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
interface Native {
|
|
9
|
+
openProcess(
|
|
10
|
+
nameOrPid: string | number,
|
|
11
|
+
access?: number,
|
|
12
|
+
): { handle: number; pid: number };
|
|
13
|
+
closeHandle(handle: number): boolean;
|
|
14
|
+
listProcesses(): { pid: number; name: string }[];
|
|
15
|
+
getModules(
|
|
16
|
+
handle: number,
|
|
17
|
+
pid: number,
|
|
18
|
+
): { base: number; size: number; path: string }[];
|
|
19
|
+
getRegions(
|
|
20
|
+
handle: number,
|
|
21
|
+
): { baseAddress: number; size: number; protection: number; type: number }[];
|
|
22
|
+
readValue(handle: number, address: number, type: string): number | bigint;
|
|
23
|
+
writeValue(
|
|
24
|
+
handle: number,
|
|
25
|
+
address: number,
|
|
26
|
+
type: string,
|
|
27
|
+
value: number | bigint,
|
|
28
|
+
): boolean;
|
|
29
|
+
readBuffer(handle: number, address: number, size: number): Buffer;
|
|
30
|
+
writeBuffer(handle: number, address: number, data: Buffer): boolean;
|
|
31
|
+
scan(
|
|
32
|
+
pid: number,
|
|
33
|
+
pattern: string,
|
|
34
|
+
opts?: { start?: number; end?: number; onlyReadable?: boolean },
|
|
35
|
+
): Promise<bigint[]>;
|
|
36
|
+
scanValue(
|
|
37
|
+
pid: number,
|
|
38
|
+
needle: Uint8Array,
|
|
39
|
+
opts?: {
|
|
40
|
+
start?: number;
|
|
41
|
+
end?: number;
|
|
42
|
+
onlyWritable?: boolean;
|
|
43
|
+
alignment?: number;
|
|
44
|
+
},
|
|
45
|
+
): Promise<bigint[]>;
|
|
46
|
+
allocMemory(
|
|
47
|
+
handle: number,
|
|
48
|
+
size: number,
|
|
49
|
+
protection: number,
|
|
50
|
+
nearAddress?: number,
|
|
51
|
+
): bigint;
|
|
52
|
+
freeMemory(handle: number, address: number): boolean;
|
|
53
|
+
protectMemory(
|
|
54
|
+
handle: number,
|
|
55
|
+
address: number,
|
|
56
|
+
size: number,
|
|
57
|
+
protection: number,
|
|
58
|
+
): number;
|
|
59
|
+
isWow64(handle: number): boolean;
|
|
60
|
+
listThreads(pid: number): number[];
|
|
61
|
+
suspendThread(tid: number): boolean;
|
|
62
|
+
resumeThread(tid: number): boolean;
|
|
63
|
+
readSafe(handle: number, address: number, size: number): Buffer;
|
|
64
|
+
flushInstructions(handle: number, address: number, size: number): boolean;
|
|
65
|
+
injectDll(pid: number, dllPath: string, timeoutMs?: number): boolean;
|
|
66
|
+
ejectDll(pid: number, moduleBase: number): boolean;
|
|
67
|
+
queryAddress(
|
|
68
|
+
handle: number,
|
|
69
|
+
address: number,
|
|
70
|
+
): {
|
|
71
|
+
baseAddress: number;
|
|
72
|
+
allocationBase: number;
|
|
73
|
+
size: number;
|
|
74
|
+
protection: number;
|
|
75
|
+
state: number;
|
|
76
|
+
type: number;
|
|
77
|
+
} | null;
|
|
78
|
+
getThreadContext(tid: number): {
|
|
79
|
+
rip: bigint;
|
|
80
|
+
rsp: bigint;
|
|
81
|
+
rbp: bigint;
|
|
82
|
+
dr0: bigint;
|
|
83
|
+
dr1: bigint;
|
|
84
|
+
dr2: bigint;
|
|
85
|
+
dr3: bigint;
|
|
86
|
+
dr6: bigint;
|
|
87
|
+
dr7: bigint;
|
|
88
|
+
eflags: bigint;
|
|
89
|
+
} | null;
|
|
90
|
+
setThreadContext(
|
|
91
|
+
tid: number,
|
|
92
|
+
ctx: Partial<
|
|
93
|
+
Record<
|
|
94
|
+
| 'rip'
|
|
95
|
+
| 'rsp'
|
|
96
|
+
| 'rbp'
|
|
97
|
+
| 'dr0'
|
|
98
|
+
| 'dr1'
|
|
99
|
+
| 'dr2'
|
|
100
|
+
| 'dr3'
|
|
101
|
+
| 'dr6'
|
|
102
|
+
| 'dr7'
|
|
103
|
+
| 'eflags',
|
|
104
|
+
number | bigint
|
|
105
|
+
>
|
|
106
|
+
>,
|
|
107
|
+
): boolean;
|
|
108
|
+
callRemote(
|
|
109
|
+
pid: number,
|
|
110
|
+
fnAddress: number | bigint,
|
|
111
|
+
param: number | bigint,
|
|
112
|
+
timeoutMs?: number,
|
|
113
|
+
): number | null;
|
|
114
|
+
disasmInstructions(
|
|
115
|
+
handle: number,
|
|
116
|
+
address: number,
|
|
117
|
+
count: number,
|
|
118
|
+
): { offset: number; length: number; relative: boolean }[];
|
|
119
|
+
/**
|
|
120
|
+
* Walk instructions at `address` until `minBytes` are covered, then
|
|
121
|
+
* RELOCATE position-relative operands (rel32 branch/call immediates,
|
|
122
|
+
* RIP-relative disp32) so the stolen bytes are correct when executed from
|
|
123
|
+
* `stubAddr` (must be within rel32 range of `address`). External rel8/16
|
|
124
|
+
* branches (incl. LOOP/JECXZ) can't be widened -- refused `HOOK_RELOCATE`,
|
|
125
|
+
* like MinHook; rel8/16 internal (target inside the stolen window) and
|
|
126
|
+
* all non-relative instructions copy verbatim. Without `stubAddr` any
|
|
127
|
+
* position-relative instruction is refused (inspection mode).
|
|
128
|
+
* Throws `HOOK_STEAL`/`HOOK_RELOCATE` -- returns the relocated bytes.
|
|
129
|
+
*/
|
|
130
|
+
hookSteal(
|
|
131
|
+
handle: number,
|
|
132
|
+
address: number,
|
|
133
|
+
minBytesOrOpts?: number | { minBytes?: number; stubAddr?: number | bigint },
|
|
134
|
+
stubAddr?: number | bigint,
|
|
135
|
+
): { length: number; bytes: Buffer; original: Buffer };
|
|
136
|
+
/**
|
|
137
|
+
* Per-region VirtualProtectEx->write->restore across the whole write span --
|
|
138
|
+
* records each region's real old protection before writing, restores each
|
|
139
|
+
* individually. `restored=false` means part of the span is left weakened.
|
|
140
|
+
*/
|
|
141
|
+
writeProtectedMemory(
|
|
142
|
+
handle: number,
|
|
143
|
+
address: number,
|
|
144
|
+
buffer: Buffer,
|
|
145
|
+
protection?: number,
|
|
146
|
+
): { ok: boolean; restored: boolean; protected: boolean };
|
|
147
|
+
/** Current thread id (TID) -- used to refuse self-suspend. */
|
|
148
|
+
currentThreadId(): number;
|
|
149
|
+
suspendProcess(pid: number): boolean;
|
|
150
|
+
resumeProcess(pid: number): boolean;
|
|
151
|
+
enableDebugPrivilege(): boolean;
|
|
152
|
+
waitForExit(pid: number, timeoutMs?: number): Promise<number | null>;
|
|
153
|
+
findWhatWrites(
|
|
154
|
+
pid: number,
|
|
155
|
+
address: number | bigint,
|
|
156
|
+
size: number,
|
|
157
|
+
timeoutMs: number,
|
|
158
|
+
maxHits: number,
|
|
159
|
+
): Promise<
|
|
160
|
+
{
|
|
161
|
+
rip: bigint;
|
|
162
|
+
tid: number;
|
|
163
|
+
threadContext: {
|
|
164
|
+
rip: bigint;
|
|
165
|
+
rsp: bigint;
|
|
166
|
+
rbp: bigint;
|
|
167
|
+
dr0: bigint;
|
|
168
|
+
dr1: bigint;
|
|
169
|
+
dr2: bigint;
|
|
170
|
+
dr3: bigint;
|
|
171
|
+
dr6: bigint;
|
|
172
|
+
dr7: bigint;
|
|
173
|
+
eflags: bigint;
|
|
174
|
+
};
|
|
175
|
+
}[]
|
|
176
|
+
>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Resolution order: dist/memoryprocess.node (built/packaged) -> prebuilds via
|
|
180
|
+
// node-gyp-build -> build/Release/memoryprocess.node (dev). Works whether this
|
|
181
|
+
// file is loaded from src/ (bun test) or bundled into dist/index.js.
|
|
182
|
+
const candidates = [
|
|
183
|
+
join(__dir, 'memoryprocess.node'), // dist/ or src/ (copied for tests)
|
|
184
|
+
join(__dir, '..', 'dist', 'memoryprocess.node'), // src/ -> dist/
|
|
185
|
+
join(__dir, '..', 'build', 'Release', 'memoryprocess.node'), // src/ -> build/
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
export const native: Native = (() => {
|
|
189
|
+
// Keep the last real load error: a .node that EXISTS but fails to load
|
|
190
|
+
// (missing VC++ CRT, arch or N-API mismatch, wrong Node ABI) must not be
|
|
191
|
+
// reported as "not found" -- surface the underlying loader message.
|
|
192
|
+
let lastErr: unknown;
|
|
193
|
+
for (const path of candidates) {
|
|
194
|
+
try {
|
|
195
|
+
return require(path) as Native;
|
|
196
|
+
} catch (e) {
|
|
197
|
+
lastErr = e;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
return require('node-gyp-build')(join(__dir, '..')) as Native;
|
|
202
|
+
} catch (e) {
|
|
203
|
+
lastErr = lastErr ?? e;
|
|
204
|
+
}
|
|
205
|
+
const detail = lastErr instanceof Error ? `: ${lastErr.message}` : '';
|
|
206
|
+
throw new Error(
|
|
207
|
+
`memoryprocess native addon failed to load (run \`bun run build\`)${detail}`,
|
|
208
|
+
);
|
|
209
|
+
})();
|