@memoryprocess/bindings 0.0.0-stage → 0.0.3-beta.2
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/binding.gyp +46 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +27 -0
- package/dist/memoryprocess.node +0 -0
- package/dist/native.d.ts +128 -0
- package/native/hde/LICENSE-minhook.txt +81 -0
- package/native/hde/hde64.c +538 -0
- package/native/hde/hde64.h +113 -0
- package/native/hde/pstdint.h +39 -0
- package/native/hde/table64.h +74 -0
- package/native/memory.cc +21 -0
- package/native/memory.h +31 -0
- package/native/memoryprocess.cc +43 -0
- package/native/module.cc +1720 -0
- package/native/module.h +55 -0
- package/native/pattern.cc +304 -0
- package/native/process.cc +113 -0
- package/native/process.h +36 -0
- package/package.json +49 -4
- package/prebuilds/win32-x64/@memoryprocess+bindings.node +0 -0
- package/README.md +0 -3
package/native/module.h
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#include <napi.h>
|
|
3
|
+
#include "process.h"
|
|
4
|
+
|
|
5
|
+
namespace mp {
|
|
6
|
+
|
|
7
|
+
Napi::Value OpenProcessSync(const Napi::CallbackInfo& info);
|
|
8
|
+
Napi::Value CloseHandleSync(const Napi::CallbackInfo& info);
|
|
9
|
+
Napi::Value ListProcessesSync(const Napi::CallbackInfo& info);
|
|
10
|
+
Napi::Value GetModulesSync(const Napi::CallbackInfo& info);
|
|
11
|
+
Napi::Value GetRegionsSync(const Napi::CallbackInfo& info);
|
|
12
|
+
Napi::Value ReadValueSync(const Napi::CallbackInfo& info);
|
|
13
|
+
Napi::Value WriteValueSync(const Napi::CallbackInfo& info);
|
|
14
|
+
Napi::Value ReadBufferSync(const Napi::CallbackInfo& info);
|
|
15
|
+
Napi::Value WriteBufferSync(const Napi::CallbackInfo& info);
|
|
16
|
+
Napi::Value ScanAsync(const Napi::CallbackInfo& info);
|
|
17
|
+
|
|
18
|
+
// v2 Tier-1/2 additions
|
|
19
|
+
Napi::Value AllocMemorySync(const Napi::CallbackInfo& info); // VirtualAllocEx
|
|
20
|
+
Napi::Value FreeMemorySync(const Napi::CallbackInfo& info); // VirtualFreeEx
|
|
21
|
+
Napi::Value ProtectMemorySync(const Napi::CallbackInfo& info); // VirtualProtectEx
|
|
22
|
+
Napi::Value IsWow64Sync(const Napi::CallbackInfo& info); // IsWow64Process
|
|
23
|
+
Napi::Value ListThreadsSync(const Napi::CallbackInfo& info); // TH32CS_SNAPTHREAD
|
|
24
|
+
Napi::Value SuspendThreadSync(const Napi::CallbackInfo& info); // OpenThread+SuspendThread
|
|
25
|
+
Napi::Value ResumeThreadSync(const Napi::CallbackInfo& info); // OpenThread+ResumeThread
|
|
26
|
+
|
|
27
|
+
// v2 final-batch additions
|
|
28
|
+
Napi::Value ReadSafeSync(const Napi::CallbackInfo& info); // VirtualQueryEx walk + zero-fill
|
|
29
|
+
Napi::Value FlushInstructionsSync(const Napi::CallbackInfo& info);// FlushInstructionCache
|
|
30
|
+
Napi::Value InjectDllSync(const Napi::CallbackInfo& info); // LoadLibraryA remote thread
|
|
31
|
+
Napi::Value EjectDllSync(const Napi::CallbackInfo& info); // FreeLibrary remote thread
|
|
32
|
+
|
|
33
|
+
// v2 Tier-3 additions
|
|
34
|
+
Napi::Value QueryAddressSync(const Napi::CallbackInfo& info); // single VirtualQueryEx
|
|
35
|
+
Napi::Value GetThreadContextSync(const Napi::CallbackInfo& info); // GetThreadContext (CONTEXT_ALL|DEBUG)
|
|
36
|
+
Napi::Value SetThreadContextSync(const Napi::CallbackInfo& info); // patch fields + SetThreadContext
|
|
37
|
+
Napi::Value CallRemoteSync(const Napi::CallbackInfo& info); // CreateRemoteThread + wait
|
|
38
|
+
|
|
39
|
+
// v2 disasm (HDE64 length decode)
|
|
40
|
+
Napi::Value DisasmInstructionsSync(const Napi::CallbackInfo& info); // hde64_disasm walk
|
|
41
|
+
|
|
42
|
+
// v2.1 additions
|
|
43
|
+
Napi::Value ScanValueAsync(const Napi::CallbackInfo& info); // typed value scan (rolling window)
|
|
44
|
+
Napi::Value SuspendProcessSync(const Napi::CallbackInfo& info); // NtSuspendProcess
|
|
45
|
+
Napi::Value ResumeProcessSync(const Napi::CallbackInfo& info); // NtResumeProcess
|
|
46
|
+
Napi::Value EnableDebugPrivilegeSync(const Napi::CallbackInfo& info);// SeDebugPrivilege on self
|
|
47
|
+
Napi::Value WaitForExitAsync(const Napi::CallbackInfo& info); // async WaitForSingleObject
|
|
48
|
+
Napi::Value FindWhatWritesAsync(const Napi::CallbackInfo& info); // DebugActiveProcess + HW write-bp loop
|
|
49
|
+
|
|
50
|
+
// QA4 hardening additions
|
|
51
|
+
Napi::Value HookStealSync(const Napi::CallbackInfo& info); // steal+relocate-check for hook
|
|
52
|
+
Napi::Value WriteProtectedMemorySync(const Napi::CallbackInfo& info);// per-region protect/write/restore
|
|
53
|
+
Napi::Value CurrentThreadIdSync(const Napi::CallbackInfo& info); // GetCurrentThreadId (suspendAll)
|
|
54
|
+
|
|
55
|
+
} // namespace mp
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#include "module.h"
|
|
2
|
+
#include "memory.h"
|
|
3
|
+
#include <string>
|
|
4
|
+
#include <vector>
|
|
5
|
+
#include <optional>
|
|
6
|
+
#include <cstring>
|
|
7
|
+
|
|
8
|
+
namespace mp {
|
|
9
|
+
|
|
10
|
+
struct PatternByte { std::uint8_t value; bool wildcard; };
|
|
11
|
+
|
|
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 - the caller turns
|
|
14
|
+
// this into a JS error; no C++ exception can cross the N-API boundary.
|
|
15
|
+
static std::optional<std::vector<PatternByte>> ParsePattern(const std::string& pat) {
|
|
16
|
+
std::vector<PatternByte> out;
|
|
17
|
+
std::string tok;
|
|
18
|
+
auto flush = [&]() -> bool {
|
|
19
|
+
if (tok.empty()) return true;
|
|
20
|
+
if (tok == "?" || tok == "??") { out.push_back({0, true}); tok.clear(); return true; }
|
|
21
|
+
if (tok.size() > 2 || tok.find('?') != std::string::npos) return false;
|
|
22
|
+
unsigned long v = 0;
|
|
23
|
+
for (char c : tok) {
|
|
24
|
+
v <<= 4;
|
|
25
|
+
if (c >= '0' && c <= '9') v |= static_cast<unsigned long>(c - '0');
|
|
26
|
+
else if (c >= 'a' && c <= 'f') v |= static_cast<unsigned long>(c - 'a' + 10);
|
|
27
|
+
else if (c >= 'A' && c <= 'F') v |= static_cast<unsigned long>(c - 'A' + 10);
|
|
28
|
+
else return false;
|
|
29
|
+
}
|
|
30
|
+
out.push_back({static_cast<std::uint8_t>(v & 0xff), false});
|
|
31
|
+
tok.clear();
|
|
32
|
+
return true;
|
|
33
|
+
};
|
|
34
|
+
for (size_t i = 0; i <= pat.size(); i++) {
|
|
35
|
+
char c = i < pat.size() ? pat[i] : ' ';
|
|
36
|
+
if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
|
|
37
|
+
if (!flush()) return std::nullopt;
|
|
38
|
+
} else tok += c;
|
|
39
|
+
}
|
|
40
|
+
if (out.empty()) return std::nullopt;
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static const std::size_t CHUNK = 64 * 1024;
|
|
45
|
+
static const std::uintptr_t USER_TOP = 0x7FFFFFFFFFFFULL;
|
|
46
|
+
|
|
47
|
+
static inline bool RegionReadable(const MEMORY_BASIC_INFORMATION& mbi) {
|
|
48
|
+
return (mbi.State == MEM_COMMIT) &&
|
|
49
|
+
!(mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) &&
|
|
50
|
+
(mbi.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY |
|
|
51
|
+
PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY));
|
|
52
|
+
}
|
|
53
|
+
static inline bool RegionWritable(const MEMORY_BASIC_INFORMATION& mbi) {
|
|
54
|
+
return (mbi.State == MEM_COMMIT) &&
|
|
55
|
+
!(mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) &&
|
|
56
|
+
(mbi.Protect & (PAGE_READWRITE | PAGE_WRITECOPY |
|
|
57
|
+
PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Async region-scan worker shared by the AOB scan and the typed-value scan.
|
|
61
|
+
// Runs with a PRIVATE least-privilege process handle opened from the pid (a
|
|
62
|
+
// JS-side close() can never invalidate it mid-scan), and scans each committed
|
|
63
|
+
// range with a rolling window - 64KB chunks plus a (needle-1)-byte overlap -
|
|
64
|
+
// so multi-hundred-MB regions never accumulate in memory.
|
|
65
|
+
class ScanWorker : public Napi::AsyncWorker {
|
|
66
|
+
public:
|
|
67
|
+
ScanWorker(Napi::Env env, Napi::Promise::Deferred deferred, DWORD pid,
|
|
68
|
+
std::vector<PatternByte> pat, std::uintptr_t start, std::uintptr_t end,
|
|
69
|
+
bool onlyReadable)
|
|
70
|
+
: Napi::AsyncWorker(env), deferred_(deferred), pid_(pid), pat_(std::move(pat)),
|
|
71
|
+
start_(start), end_(end), onlyReadable_(onlyReadable) {}
|
|
72
|
+
|
|
73
|
+
ScanWorker(Napi::Env env, Napi::Promise::Deferred deferred, DWORD pid,
|
|
74
|
+
std::vector<std::uint8_t> needle, std::uintptr_t start, std::uintptr_t end,
|
|
75
|
+
bool onlyWritable, std::size_t alignment)
|
|
76
|
+
: Napi::AsyncWorker(env), deferred_(deferred), pid_(pid), needle_(std::move(needle)),
|
|
77
|
+
start_(start), end_(end), valueMode_(true), onlyWritable_(onlyWritable),
|
|
78
|
+
alignment_(alignment) {}
|
|
79
|
+
|
|
80
|
+
~ScanWorker() override { if (proc_) CloseHandle(proc_); }
|
|
81
|
+
|
|
82
|
+
void Execute() override {
|
|
83
|
+
proc_ = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, pid_);
|
|
84
|
+
if (!proc_) { SetError("OpenProcess failed (pid gone or access denied)"); return; }
|
|
85
|
+
|
|
86
|
+
// QA4 lifecycle-M1: a C++ exception (bad_alloc in hits_ growth or the
|
|
87
|
+
// per-chunk vector) escaping Execute() used to skip CloseHandle - a real
|
|
88
|
+
// handle leak under memory pressure, every scan call. Guard all exits.
|
|
89
|
+
try {
|
|
90
|
+
const std::uintptr_t limit = end_ ? end_ : USER_TOP;
|
|
91
|
+
MEMORY_BASIC_INFORMATION mbi{};
|
|
92
|
+
std::uintptr_t addr = start_;
|
|
93
|
+
while (addr < limit) {
|
|
94
|
+
if (!VirtualQueryEx(proc_, reinterpret_cast<LPCVOID>(addr), &mbi, sizeof(mbi))) break;
|
|
95
|
+
const std::uintptr_t rBase = reinterpret_cast<std::uintptr_t>(mbi.BaseAddress);
|
|
96
|
+
const std::uintptr_t rEnd = rBase + mbi.RegionSize;
|
|
97
|
+
if (mbi.RegionSize == 0) break;
|
|
98
|
+
const bool ok = valueMode_
|
|
99
|
+
? (onlyWritable_ ? RegionWritable(mbi) : RegionReadable(mbi) ||
|
|
100
|
+
((mbi.State == MEM_COMMIT) && !(mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD))))
|
|
101
|
+
: (onlyReadable_ ? RegionReadable(mbi)
|
|
102
|
+
: ((mbi.State == MEM_COMMIT) && !(mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD))));
|
|
103
|
+
if (ok) {
|
|
104
|
+
// clamp window to [start_, limit)
|
|
105
|
+
const std::uintptr_t s = rBase > start_ ? rBase : start_;
|
|
106
|
+
const std::uintptr_t e = rEnd < limit ? rEnd : limit;
|
|
107
|
+
if (s < e) ScanRange(s, e);
|
|
108
|
+
}
|
|
109
|
+
addr = rEnd;
|
|
110
|
+
}
|
|
111
|
+
} catch (const std::exception& e) {
|
|
112
|
+
CloseHandle(proc_);
|
|
113
|
+
proc_ = nullptr;
|
|
114
|
+
SetError(std::string("scan failed: ") + e.what());
|
|
115
|
+
return;
|
|
116
|
+
} catch (...) {
|
|
117
|
+
CloseHandle(proc_);
|
|
118
|
+
proc_ = nullptr;
|
|
119
|
+
SetError("scan failed: unknown exception");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
CloseHandle(proc_);
|
|
123
|
+
proc_ = nullptr;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
void OnOK() override {
|
|
127
|
+
auto arr = Napi::Array::New(Env(), hits_.size());
|
|
128
|
+
for (size_t i = 0; i < hits_.size(); i++)
|
|
129
|
+
arr.Set(i, Napi::BigInt::New(Env(), static_cast<std::uint64_t>(hits_[i])));
|
|
130
|
+
deferred_.Resolve(arr);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
void OnError(const Napi::Error& e) override { deferred_.Reject(e.Value()); }
|
|
134
|
+
|
|
135
|
+
private:
|
|
136
|
+
std::size_t NeedleLen() const { return valueMode_ ? needle_.size() : pat_.size(); }
|
|
137
|
+
|
|
138
|
+
bool MatchAt(const std::uint8_t* p) const {
|
|
139
|
+
if (valueMode_) return std::memcmp(p, needle_.data(), needle_.size()) == 0;
|
|
140
|
+
for (std::size_t j = 0; j < pat_.size(); j++)
|
|
141
|
+
if (!pat_[j].wildcard && p[j] != pat_[j].value) return false;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Rolling-window scan of the committed VA range [lo, hi).
|
|
146
|
+
void ScanRange(std::uintptr_t lo, std::uintptr_t hi) {
|
|
147
|
+
const std::size_t patLen = NeedleLen();
|
|
148
|
+
const std::size_t overlap = patLen - 1;
|
|
149
|
+
std::vector<std::uint8_t> tail; // last `overlap` bytes read (contiguity kept)
|
|
150
|
+
std::uintptr_t pos = lo;
|
|
151
|
+
while (pos < hi) {
|
|
152
|
+
const std::size_t toRead =
|
|
153
|
+
(hi - pos > CHUNK) ? CHUNK : static_cast<std::size_t>(hi - pos);
|
|
154
|
+
std::vector<std::uint8_t> chunk(toRead);
|
|
155
|
+
SIZE_T got = 0;
|
|
156
|
+
if (!ReadProcessMemory(proc_, reinterpret_cast<LPCVOID>(pos),
|
|
157
|
+
chunk.data(), toRead, &got) || got == 0) {
|
|
158
|
+
tail.clear(); // VA discontinuity: a match can't straddle a failed read
|
|
159
|
+
// Don't blindly skip the whole 64KB chunk - a single inaccessible
|
|
160
|
+
// page inside a readable region would drop signatures CE finds.
|
|
161
|
+
// Re-query the sub-region at `pos`: skip only up to its end, or just
|
|
162
|
+
// one 4KB page when the sub-region still claims to be readable (the
|
|
163
|
+
// failure was a commit/protection race beyond the first page).
|
|
164
|
+
MEMORY_BASIC_INFORMATION smbi{};
|
|
165
|
+
std::uintptr_t next = pos + toRead;
|
|
166
|
+
if (VirtualQueryEx(proc_, reinterpret_cast<LPCVOID>(pos), &smbi, sizeof(smbi))) {
|
|
167
|
+
const std::uintptr_t sEnd =
|
|
168
|
+
reinterpret_cast<std::uintptr_t>(smbi.BaseAddress) + smbi.RegionSize;
|
|
169
|
+
const bool subOk = valueMode_
|
|
170
|
+
? (onlyWritable_ ? RegionWritable(smbi) : RegionReadable(smbi))
|
|
171
|
+
: (onlyReadable_ ? RegionReadable(smbi)
|
|
172
|
+
: ((smbi.State == MEM_COMMIT) &&
|
|
173
|
+
!(smbi.Protect & (PAGE_NOACCESS | PAGE_GUARD))));
|
|
174
|
+
next = subOk ? pos + 0x1000 : (sEnd > pos ? sEnd : pos + 0x1000);
|
|
175
|
+
}
|
|
176
|
+
if (next <= pos) next = pos + toRead; // never stall the walk
|
|
177
|
+
pos = next > hi ? hi : next;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
chunk.resize(got);
|
|
181
|
+
const std::size_t keep = tail.size() < overlap ? tail.size() : overlap;
|
|
182
|
+
// window = tail(keep) + chunk(got); window[0] is at VA (pos - keep)
|
|
183
|
+
std::vector<std::uint8_t> win;
|
|
184
|
+
win.reserve(keep + got);
|
|
185
|
+
win.insert(win.end(), tail.end() - static_cast<std::ptrdiff_t>(keep), tail.end());
|
|
186
|
+
win.insert(win.end(), chunk.begin(), chunk.end());
|
|
187
|
+
const std::uintptr_t winBase = pos - keep;
|
|
188
|
+
// match starts that touch >=1 new byte: i >= keep - overlap (clamped 0)
|
|
189
|
+
const std::size_t firstI = keep > overlap ? keep - overlap : 0;
|
|
190
|
+
if (win.size() >= patLen) {
|
|
191
|
+
const std::size_t lastI = win.size() - patLen; // inclusive
|
|
192
|
+
for (std::size_t i = firstI; i <= lastI; i++) {
|
|
193
|
+
const std::uintptr_t va = winBase + i;
|
|
194
|
+
if (va < start_ || va >= (end_ ? end_ : USER_TOP)) continue;
|
|
195
|
+
if (valueMode_ && alignment_ > 1 && (va % alignment_) != 0) continue;
|
|
196
|
+
if (MatchAt(win.data() + i)) hits_.push_back(va);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// keep the last `overlap` bytes for the next iteration
|
|
200
|
+
if (win.size() > overlap) tail.assign(win.end() - static_cast<std::ptrdiff_t>(overlap), win.end());
|
|
201
|
+
else tail = std::move(win);
|
|
202
|
+
pos += got; // short reads shrink the window, next read resumes at pos+got
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
Napi::Promise::Deferred deferred_;
|
|
207
|
+
DWORD pid_ = 0;
|
|
208
|
+
HANDLE proc_ = nullptr;
|
|
209
|
+
std::vector<PatternByte> pat_;
|
|
210
|
+
std::vector<std::uint8_t> needle_;
|
|
211
|
+
std::uintptr_t start_ = 0, end_ = 0;
|
|
212
|
+
bool onlyReadable_ = true;
|
|
213
|
+
bool valueMode_ = false;
|
|
214
|
+
bool onlyWritable_ = false;
|
|
215
|
+
std::size_t alignment_ = 0;
|
|
216
|
+
std::vector<std::uintptr_t> hits_;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
Napi::Value ScanAsync(const Napi::CallbackInfo& info) {
|
|
220
|
+
Napi::Env env = info.Env();
|
|
221
|
+
// scan(pid, pattern[, {start,end,onlyReadable}]) -> Promise<bigint[]>
|
|
222
|
+
if (info.Length() < 2) {
|
|
223
|
+
Napi::TypeError::New(env, "expected (pid, pattern[, opts])").ThrowAsJavaScriptException();
|
|
224
|
+
return env.Null();
|
|
225
|
+
}
|
|
226
|
+
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
|
227
|
+
std::string pat = info[1].As<Napi::String>().Utf8Value();
|
|
228
|
+
auto bytes = ParsePattern(pat);
|
|
229
|
+
if (!bytes) {
|
|
230
|
+
auto err = Napi::Error::New(env, "invalid pattern: expected hex bytes like \"DE AD ?? EF\"");
|
|
231
|
+
err.Set("code", "INVALID_PATTERN");
|
|
232
|
+
err.ThrowAsJavaScriptException();
|
|
233
|
+
return env.Null();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
std::uintptr_t start = 0, end = 0;
|
|
237
|
+
bool onlyReadable = true;
|
|
238
|
+
if (info.Length() > 2 && info[2].IsObject()) {
|
|
239
|
+
auto o = info[2].As<Napi::Object>();
|
|
240
|
+
if (o.Get("start").IsNumber()) start = static_cast<std::uintptr_t>(o.Get("start").As<Napi::Number>().DoubleValue());
|
|
241
|
+
if (o.Get("end").IsNumber()) end = static_cast<std::uintptr_t>(o.Get("end").As<Napi::Number>().DoubleValue());
|
|
242
|
+
if (o.Get("onlyReadable").IsBoolean()) onlyReadable = o.Get("onlyReadable").As<Napi::Boolean>().Value();
|
|
243
|
+
// module/section ranges are resolved to start/end in the TS facade
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
247
|
+
auto promise = deferred.Promise();
|
|
248
|
+
auto* w = new ScanWorker(env, std::move(deferred), pid, std::move(*bytes), start, end, onlyReadable);
|
|
249
|
+
w->Queue();
|
|
250
|
+
return promise;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// scanValue(pid, needle[, {start,end,onlyWritable,alignment}]) -> Promise<bigint[]>
|
|
254
|
+
// `needle` is a Buffer/Uint8Array holding the little-endian value bytes.
|
|
255
|
+
Napi::Value ScanValueAsync(const Napi::CallbackInfo& info) {
|
|
256
|
+
Napi::Env env = info.Env();
|
|
257
|
+
if (info.Length() < 2 || !info[0].IsNumber()) {
|
|
258
|
+
Napi::TypeError::New(env, "expected (pid, needle[, opts])").ThrowAsJavaScriptException();
|
|
259
|
+
return env.Null();
|
|
260
|
+
}
|
|
261
|
+
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
|
262
|
+
if (!info[1].IsBuffer() && !info[1].IsTypedArray()) {
|
|
263
|
+
Napi::TypeError::New(env, "needle must be a Buffer/Uint8Array").ThrowAsJavaScriptException();
|
|
264
|
+
return env.Null();
|
|
265
|
+
}
|
|
266
|
+
std::vector<std::uint8_t> needle;
|
|
267
|
+
if (info[1].IsBuffer()) {
|
|
268
|
+
auto b = info[1].As<Napi::Buffer<std::uint8_t>>();
|
|
269
|
+
needle.assign(b.Data(), b.Data() + b.Length());
|
|
270
|
+
} else {
|
|
271
|
+
auto ta = info[1].As<Napi::TypedArray>();
|
|
272
|
+
const auto* p = static_cast<const std::uint8_t*>(ta.ArrayBuffer().Data()) + ta.ByteOffset();
|
|
273
|
+
needle.assign(p, p + ta.ByteLength());
|
|
274
|
+
}
|
|
275
|
+
if (needle.empty() || needle.size() > 16) {
|
|
276
|
+
Napi::RangeError::New(env, "needle must be 1..16 bytes").ThrowAsJavaScriptException();
|
|
277
|
+
return env.Null();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
std::uintptr_t start = 0, end = 0;
|
|
281
|
+
bool onlyWritable = true;
|
|
282
|
+
std::size_t alignment = needle.size();
|
|
283
|
+
if (info.Length() > 2 && info[2].IsObject()) {
|
|
284
|
+
auto o = info[2].As<Napi::Object>();
|
|
285
|
+
if (o.Get("start").IsNumber()) start = static_cast<std::uintptr_t>(o.Get("start").As<Napi::Number>().DoubleValue());
|
|
286
|
+
if (o.Get("end").IsNumber()) end = static_cast<std::uintptr_t>(o.Get("end").As<Napi::Number>().DoubleValue());
|
|
287
|
+
if (o.Get("onlyWritable").IsBoolean()) onlyWritable = o.Get("onlyWritable").As<Napi::Boolean>().Value();
|
|
288
|
+
if (o.Get("onlyReadable").IsBoolean() && !o.Get("onlyWritable").IsBoolean())
|
|
289
|
+
onlyWritable = !o.Get("onlyReadable").As<Napi::Boolean>().Value();
|
|
290
|
+
if (o.Get("alignment").IsNumber()) {
|
|
291
|
+
double a = o.Get("alignment").As<Napi::Number>().DoubleValue();
|
|
292
|
+
if (a >= 1 && a <= 4096) alignment = static_cast<std::size_t>(a);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
297
|
+
auto promise = deferred.Promise();
|
|
298
|
+
auto* w = new ScanWorker(env, std::move(deferred), pid, std::move(needle),
|
|
299
|
+
start, end, onlyWritable, alignment);
|
|
300
|
+
w->Queue();
|
|
301
|
+
return promise;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
} // namespace mp
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#include "process.h"
|
|
2
|
+
#include <tlhelp32.h>
|
|
3
|
+
#include <psapi.h>
|
|
4
|
+
#include <system_error>
|
|
5
|
+
|
|
6
|
+
namespace mp {
|
|
7
|
+
|
|
8
|
+
static DWORD LastErr() { return GetLastError(); }
|
|
9
|
+
|
|
10
|
+
HANDLE OpenByPid(DWORD pid, DWORD access) {
|
|
11
|
+
HANDLE h = OpenProcess(access, FALSE, pid);
|
|
12
|
+
if (!h) throw std::system_error(static_cast<int>(LastErr()), std::system_category(), "OpenProcess");
|
|
13
|
+
return h;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
DWORD PidFromName(const std::string& name) {
|
|
17
|
+
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
|
18
|
+
if (snap == INVALID_HANDLE_VALUE) return 0;
|
|
19
|
+
PROCESSENTRY32W pe{};
|
|
20
|
+
pe.dwSize = sizeof(pe);
|
|
21
|
+
// case-insensitive wide compare
|
|
22
|
+
std::wstring want(name.begin(), name.end());
|
|
23
|
+
for (wchar_t& c : want) c = towlower(c);
|
|
24
|
+
DWORD found = 0;
|
|
25
|
+
if (Process32FirstW(snap, &pe)) {
|
|
26
|
+
do {
|
|
27
|
+
std::wstring cur(pe.szExeFile);
|
|
28
|
+
for (wchar_t& c : cur) c = towlower(c);
|
|
29
|
+
if (cur == want) { found = pe.th32ProcessID; break; }
|
|
30
|
+
} while (Process32NextW(snap, &pe));
|
|
31
|
+
}
|
|
32
|
+
CloseHandle(snap);
|
|
33
|
+
return found;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
std::vector<ProcInfo> ListProcesses() {
|
|
37
|
+
std::vector<ProcInfo> out;
|
|
38
|
+
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
|
39
|
+
if (snap == INVALID_HANDLE_VALUE) return out;
|
|
40
|
+
PROCESSENTRY32W pe{};
|
|
41
|
+
pe.dwSize = sizeof(pe);
|
|
42
|
+
if (Process32FirstW(snap, &pe)) {
|
|
43
|
+
do {
|
|
44
|
+
char name[MAX_PATH];
|
|
45
|
+
size_t n = 0;
|
|
46
|
+
wcstombs_s(&n, name, pe.szExeFile, sizeof(name) - 1);
|
|
47
|
+
out.push_back({ pe.th32ProcessID, std::string(name) });
|
|
48
|
+
} while (Process32NextW(snap, &pe));
|
|
49
|
+
}
|
|
50
|
+
CloseHandle(snap);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static std::string WideToUtf8(const wchar_t* w) {
|
|
55
|
+
if (!w || !*w) return {};
|
|
56
|
+
int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, nullptr, 0, nullptr, nullptr);
|
|
57
|
+
if (n <= 1) return {};
|
|
58
|
+
std::string s(static_cast<size_t>(n) - 1, '\0');
|
|
59
|
+
WideCharToMultiByte(CP_UTF8, 0, w, -1, s.data(), n, nullptr, nullptr);
|
|
60
|
+
return s;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
std::vector<ModuleInfo> GetModules(HANDLE /*proc*/, DWORD pid) {
|
|
64
|
+
// Toolhelp snapshot - unlike EnumProcessModulesEx this lists BOTH 64-bit and
|
|
65
|
+
// 32-bit (WoW64) modules from a 64-bit reader, has no fixed-cap buffer, and
|
|
66
|
+
// hands us base+size+path in one shot.
|
|
67
|
+
std::vector<ModuleInfo> out;
|
|
68
|
+
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
|
|
69
|
+
if (snap == INVALID_HANDLE_VALUE) return out;
|
|
70
|
+
MODULEENTRY32W me{};
|
|
71
|
+
me.dwSize = sizeof(me);
|
|
72
|
+
if (Module32FirstW(snap, &me)) {
|
|
73
|
+
do {
|
|
74
|
+
out.push_back({ reinterpret_cast<std::uintptr_t>(me.modBaseAddr),
|
|
75
|
+
static_cast<std::uint32_t>(me.modBaseSize),
|
|
76
|
+
WideToUtf8(me.szExePath) });
|
|
77
|
+
} while (Module32NextW(snap, &me));
|
|
78
|
+
}
|
|
79
|
+
CloseHandle(snap);
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ModuleInfo GetModule(HANDLE proc, DWORD pid, const std::string& name) {
|
|
84
|
+
auto mods = GetModules(proc, pid);
|
|
85
|
+
std::string want = name;
|
|
86
|
+
for (auto& c : want) c = static_cast<char>(tolower(c));
|
|
87
|
+
for (auto& m : mods) {
|
|
88
|
+
std::string base = m.path.substr(m.path.find_last_of("\\/") + 1);
|
|
89
|
+
for (auto& c : base) c = static_cast<char>(tolower(c));
|
|
90
|
+
if (base == want) return m;
|
|
91
|
+
}
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
std::vector<RegionInfo> GetRegions(HANDLE proc) {
|
|
96
|
+
std::vector<RegionInfo> out;
|
|
97
|
+
MEMORY_BASIC_INFORMATION mbi{};
|
|
98
|
+
std::uintptr_t addr = 0;
|
|
99
|
+
const std::uintptr_t max = 0x7FFFFFFFFFFFULL;
|
|
100
|
+
while (addr < max && VirtualQueryEx(proc, reinterpret_cast<LPCVOID>(addr), &mbi, sizeof(mbi))) {
|
|
101
|
+
if (mbi.State == MEM_COMMIT) {
|
|
102
|
+
out.push_back({ reinterpret_cast<std::uintptr_t>(mbi.BaseAddress),
|
|
103
|
+
static_cast<std::size_t>(mbi.RegionSize),
|
|
104
|
+
static_cast<std::uint32_t>(mbi.Protect),
|
|
105
|
+
static_cast<std::uint32_t>(mbi.Type) });
|
|
106
|
+
}
|
|
107
|
+
addr = reinterpret_cast<std::uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
|
|
108
|
+
if (mbi.RegionSize == 0) break;
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
} // namespace mp
|
package/native/process.h
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#include <napi.h>
|
|
3
|
+
#include <windows.h>
|
|
4
|
+
#include <string>
|
|
5
|
+
#include <vector>
|
|
6
|
+
|
|
7
|
+
namespace mp {
|
|
8
|
+
|
|
9
|
+
struct ModuleInfo {
|
|
10
|
+
std::uintptr_t base;
|
|
11
|
+
std::uint32_t size;
|
|
12
|
+
std::string path;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
struct RegionInfo {
|
|
16
|
+
std::uintptr_t baseAddress;
|
|
17
|
+
std::size_t size;
|
|
18
|
+
std::uint32_t protection;
|
|
19
|
+
std::uint32_t type;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
struct ProcInfo {
|
|
23
|
+
std::uint32_t pid;
|
|
24
|
+
std::string name;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Open a process by pid. Returns handle or throws via SetLastError mapping.
|
|
28
|
+
HANDLE OpenByPid(DWORD pid, DWORD access);
|
|
29
|
+
// Resolve a process name (e.g. "notepad.exe") to pid. 0 if not found.
|
|
30
|
+
DWORD PidFromName(const std::string& name);
|
|
31
|
+
std::vector<ProcInfo> ListProcesses();
|
|
32
|
+
std::vector<ModuleInfo> GetModules(HANDLE proc, DWORD pid);
|
|
33
|
+
ModuleInfo GetModule(HANDLE proc, DWORD pid, const std::string& name);
|
|
34
|
+
std::vector<RegionInfo> GetRegions(HANDLE proc);
|
|
35
|
+
|
|
36
|
+
} // namespace mp
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memoryprocess/bindings",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.3-beta.2",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"packageManager": "bun@1.4.4",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"bun": "./src/index.ts",
|
|
12
|
+
"development": "./src/index.ts",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"prebuilds",
|
|
20
|
+
"binding.gyp",
|
|
21
|
+
"native"
|
|
22
|
+
],
|
|
23
|
+
"os": [
|
|
24
|
+
"win32"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "node scripts/build.mjs",
|
|
31
|
+
"clean": "node scripts/clean.mjs",
|
|
32
|
+
"check": "bun check",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"prebuild:all": "prebuildify --napi -t 22.17.0 -t 24.4.0 -t 25.0.0 -t 26.0.0",
|
|
35
|
+
"prepublishOnly": "npm run build && npm run prebuild:all"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/memoryprocess/memoryprocess.git",
|
|
40
|
+
"directory": "packages/bindings"
|
|
41
|
+
},
|
|
42
|
+
"author": "JoShMiQueL",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"node-addon-api": "8.3.1",
|
|
46
|
+
"node-gyp-build": "4.8.4"
|
|
47
|
+
},
|
|
48
|
+
"trustedDependencies": [
|
|
49
|
+
"node-gyp-build"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
Binary file
|
package/README.md
DELETED