@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.
@@ -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
@@ -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.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.