@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.cc
ADDED
|
@@ -0,0 +1,1720 @@
|
|
|
1
|
+
#include "module.h"
|
|
2
|
+
#include "memory.h"
|
|
3
|
+
#include <system_error>
|
|
4
|
+
#include <tlhelp32.h>
|
|
5
|
+
#include <cmath>
|
|
6
|
+
#include <cstdio>
|
|
7
|
+
#include <cstring>
|
|
8
|
+
#include <string>
|
|
9
|
+
extern "C" {
|
|
10
|
+
#include "hde/hde64.h"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
namespace mp {
|
|
14
|
+
|
|
15
|
+
// Max bytes a single read call may materialize - guards against negative /
|
|
16
|
+
// non-finite / absurd `size` values blowing up std::vector across the N-API
|
|
17
|
+
// boundary (a length_error crossing N-API aborts the process).
|
|
18
|
+
static const double MAX_READ_BYTES = 512.0 * 1024.0 * 1024.0;
|
|
19
|
+
|
|
20
|
+
// NOTE: handles and addresses cross N-API as doubles - above 2^53 they would
|
|
21
|
+
// silently round. Harmless today (canonical user-space VA < 2^48, real HANDLE
|
|
22
|
+
// values are small) but don't feed pointers >2^53 through these bindings.
|
|
23
|
+
|
|
24
|
+
static bool CheckSize(Napi::Env env, double size) {
|
|
25
|
+
if (!std::isfinite(size) || size < 0 || size > MAX_READ_BYTES) {
|
|
26
|
+
Napi::RangeError::New(env, "size must be a finite number between 0 and 512MB")
|
|
27
|
+
.ThrowAsJavaScriptException();
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static Napi::Object ErrToJs(Napi::Env env, const std::system_error& e) {
|
|
34
|
+
auto obj = Napi::Object::New(env);
|
|
35
|
+
obj.Set("code", Napi::Number::New(env, e.code().value()));
|
|
36
|
+
obj.Set("message", Napi::String::New(env, e.what()));
|
|
37
|
+
obj.Set("win32", Napi::Boolean::New(env, true));
|
|
38
|
+
return obj;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Napi::Value OpenProcessSync(const Napi::CallbackInfo& info) {
|
|
42
|
+
Napi::Env env = info.Env();
|
|
43
|
+
if (info.Length() < 1) { Napi::TypeError::New(env, "expected (nameOrPid[, access])").ThrowAsJavaScriptException(); return env.Null(); }
|
|
44
|
+
|
|
45
|
+
// Least-privilege default: VM_READ|VM_WRITE|VM_OPERATION|QUERY_INFORMATION
|
|
46
|
+
DWORD access = PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION;
|
|
47
|
+
if (info.Length() > 1 && info[1].IsNumber()) access = info[1].As<Napi::Number>().Uint32Value();
|
|
48
|
+
|
|
49
|
+
DWORD pid = 0;
|
|
50
|
+
if (info[0].IsNumber()) {
|
|
51
|
+
pid = info[0].As<Napi::Number>().Uint32Value();
|
|
52
|
+
} else if (info[0].IsString()) {
|
|
53
|
+
pid = PidFromName(info[0].As<Napi::String>().Utf8Value());
|
|
54
|
+
if (!pid) {
|
|
55
|
+
auto e = Napi::Error::New(env, "ProcessNotFound");
|
|
56
|
+
e.Set("code", "PROCESS_NOT_FOUND");
|
|
57
|
+
e.ThrowAsJavaScriptException();
|
|
58
|
+
return env.Null();
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
Napi::TypeError::New(env, "nameOrPid must be string|number").ThrowAsJavaScriptException();
|
|
62
|
+
return env.Null();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
HANDLE h = OpenByPid(pid, access);
|
|
67
|
+
auto res = Napi::Object::New(env);
|
|
68
|
+
res.Set("handle", Napi::Number::New(env, static_cast<double>(reinterpret_cast<std::uintptr_t>(h))));
|
|
69
|
+
res.Set("pid", Napi::Number::New(env, pid));
|
|
70
|
+
return res;
|
|
71
|
+
} catch (const std::system_error& e) {
|
|
72
|
+
auto err = Napi::Error::New(env, e.what());
|
|
73
|
+
err.Set("code", e.code().value() == 5 ? "ACCESS_DENIED" : "OPEN_FAILED");
|
|
74
|
+
err.Set("win32code", Napi::Number::New(env, static_cast<double>(e.code().value())));
|
|
75
|
+
err.ThrowAsJavaScriptException();
|
|
76
|
+
return env.Null();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
Napi::Value CloseHandleSync(const Napi::CallbackInfo& info) {
|
|
81
|
+
Napi::Env env = info.Env();
|
|
82
|
+
if (info.Length() < 1 || !info[0].IsNumber()) { Napi::TypeError::New(env, "expected handle").ThrowAsJavaScriptException(); return env.Null(); }
|
|
83
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
84
|
+
return Napi::Boolean::New(env, CloseHandle(h) != 0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
Napi::Value ListProcessesSync(const Napi::CallbackInfo& info) {
|
|
88
|
+
Napi::Env env = info.Env();
|
|
89
|
+
auto list = ListProcesses();
|
|
90
|
+
auto arr = Napi::Array::New(env, list.size());
|
|
91
|
+
for (size_t i = 0; i < list.size(); i++) {
|
|
92
|
+
auto o = Napi::Object::New(env);
|
|
93
|
+
o.Set("pid", Napi::Number::New(env, static_cast<double>(list[i].pid)));
|
|
94
|
+
o.Set("name", list[i].name);
|
|
95
|
+
arr.Set(i, o);
|
|
96
|
+
}
|
|
97
|
+
return arr;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Napi::Value GetModulesSync(const Napi::CallbackInfo& info) {
|
|
101
|
+
Napi::Env env = info.Env();
|
|
102
|
+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) { Napi::TypeError::New(env, "expected (handle,pid)").ThrowAsJavaScriptException(); return env.Null(); }
|
|
103
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
104
|
+
DWORD pid = info[1].As<Napi::Number>().Uint32Value();
|
|
105
|
+
auto mods = GetModules(h, pid);
|
|
106
|
+
auto arr = Napi::Array::New(env, mods.size());
|
|
107
|
+
for (size_t i = 0; i < mods.size(); i++) {
|
|
108
|
+
auto o = Napi::Object::New(env);
|
|
109
|
+
o.Set("base", Napi::Number::New(env, static_cast<double>(mods[i].base)));
|
|
110
|
+
o.Set("size", Napi::Number::New(env, static_cast<double>(mods[i].size)));
|
|
111
|
+
o.Set("path", mods[i].path);
|
|
112
|
+
arr.Set(i, o);
|
|
113
|
+
}
|
|
114
|
+
return arr;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
Napi::Value GetRegionsSync(const Napi::CallbackInfo& info) {
|
|
118
|
+
Napi::Env env = info.Env();
|
|
119
|
+
if (info.Length() < 1 || !info[0].IsNumber()) { Napi::TypeError::New(env, "expected handle").ThrowAsJavaScriptException(); return env.Null(); }
|
|
120
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
121
|
+
auto regs = GetRegions(h);
|
|
122
|
+
auto arr = Napi::Array::New(env, regs.size());
|
|
123
|
+
for (size_t i = 0; i < regs.size(); i++) {
|
|
124
|
+
auto o = Napi::Object::New(env);
|
|
125
|
+
o.Set("baseAddress", Napi::Number::New(env, static_cast<double>(regs[i].baseAddress)));
|
|
126
|
+
o.Set("size", Napi::Number::New(env, static_cast<double>(regs[i].size)));
|
|
127
|
+
o.Set("protection", Napi::Number::New(env, static_cast<double>(regs[i].protection)));
|
|
128
|
+
o.Set("type", Napi::Number::New(env, static_cast<double>(regs[i].type)));
|
|
129
|
+
arr.Set(i, o);
|
|
130
|
+
}
|
|
131
|
+
return arr;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Napi::Value ReadBufferSync(const Napi::CallbackInfo& info) {
|
|
135
|
+
Napi::Env env = info.Env();
|
|
136
|
+
if (info.Length() < 3) { Napi::TypeError::New(env, "expected (handle,address,size)").ThrowAsJavaScriptException(); return env.Null(); }
|
|
137
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
138
|
+
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
139
|
+
double sizeD = info[2].As<Napi::Number>().DoubleValue();
|
|
140
|
+
if (!CheckSize(env, sizeD)) return env.Null();
|
|
141
|
+
auto size = static_cast<std::size_t>(sizeD);
|
|
142
|
+
try {
|
|
143
|
+
auto bytes = ReadBytes(h, addr, size);
|
|
144
|
+
return Napi::Buffer<std::uint8_t>::Copy(env, bytes.data(), bytes.size());
|
|
145
|
+
} catch (const std::system_error& e) {
|
|
146
|
+
auto err = Napi::Error::New(env, e.what());
|
|
147
|
+
err.Set("code", "READ_FAILED");
|
|
148
|
+
err.Set("win32code", Napi::Number::New(env, static_cast<double>(e.code().value())));
|
|
149
|
+
err.ThrowAsJavaScriptException();
|
|
150
|
+
return env.Null();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
Napi::Value WriteBufferSync(const Napi::CallbackInfo& info) {
|
|
155
|
+
Napi::Env env = info.Env();
|
|
156
|
+
if (info.Length() < 3) { Napi::TypeError::New(env, "expected (handle,address,buffer)").ThrowAsJavaScriptException(); return env.Null(); }
|
|
157
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
158
|
+
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
159
|
+
if (!info[2].IsBuffer()) { Napi::TypeError::New(env, "value must be a Buffer").ThrowAsJavaScriptException(); return env.Null(); }
|
|
160
|
+
auto buf = info[2].As<Napi::Buffer<std::uint8_t>>();
|
|
161
|
+
bool ok = WriteBytes(h, addr, buf.Data(), buf.Length());
|
|
162
|
+
return Napi::Boolean::New(env, ok);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------- v2 Tier-1/2 ----------
|
|
166
|
+
|
|
167
|
+
// allocMemory(handle, size, protection[, nearAddress]) -> bigint
|
|
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
|
+
Napi::Value AllocMemorySync(const Napi::CallbackInfo& info) {
|
|
171
|
+
Napi::Env env = info.Env();
|
|
172
|
+
if (info.Length() < 3) { Napi::TypeError::New(env, "expected (handle,size,protection[,nearAddress])").ThrowAsJavaScriptException(); return env.Null(); }
|
|
173
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
174
|
+
auto size = static_cast<SIZE_T>(info[1].As<Napi::Number>().DoubleValue());
|
|
175
|
+
DWORD prot = info[2].As<Napi::Number>().Uint32Value();
|
|
176
|
+
LPVOID hint = nullptr;
|
|
177
|
+
if (info.Length() > 3 && info[3].IsNumber())
|
|
178
|
+
hint = reinterpret_cast<LPVOID>(static_cast<std::uintptr_t>(info[3].As<Napi::Number>().DoubleValue()));
|
|
179
|
+
LPVOID p = VirtualAllocEx(h, hint, size, MEM_COMMIT | MEM_RESERVE, prot);
|
|
180
|
+
if (!p) { auto e = Napi::Error::New(env, "VirtualAllocEx failed"); e.Set("code", "ALLOC_FAILED"); e.Set("win32code", Napi::Number::New(env, (double)GetLastError())); e.ThrowAsJavaScriptException(); return env.Null(); }
|
|
181
|
+
return Napi::BigInt::New(env, reinterpret_cast<std::uint64_t>(p));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
Napi::Value FreeMemorySync(const Napi::CallbackInfo& info) {
|
|
185
|
+
Napi::Env env = info.Env();
|
|
186
|
+
if (info.Length() < 2) { Napi::TypeError::New(env, "expected (handle,address)").ThrowAsJavaScriptException(); return env.Null(); }
|
|
187
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
188
|
+
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
189
|
+
return Napi::Boolean::New(env, VirtualFreeEx(h, reinterpret_cast<LPVOID>(addr), 0, MEM_RELEASE) != 0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
Napi::Value ProtectMemorySync(const Napi::CallbackInfo& info) {
|
|
193
|
+
Napi::Env env = info.Env();
|
|
194
|
+
if (info.Length() < 3) { Napi::TypeError::New(env, "expected (handle,address,size,prot)").ThrowAsJavaScriptException(); return env.Null(); }
|
|
195
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
196
|
+
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
197
|
+
auto size = static_cast<SIZE_T>(info[2].As<Napi::Number>().DoubleValue());
|
|
198
|
+
DWORD prot = info[3].As<Napi::Number>().Uint32Value();
|
|
199
|
+
DWORD old = 0;
|
|
200
|
+
if (!VirtualProtectEx(h, reinterpret_cast<LPVOID>(addr), size, prot, &old)) {
|
|
201
|
+
auto e = Napi::Error::New(env, "VirtualProtectEx failed"); e.Set("code", "PROTECT_FAILED"); e.Set("win32code", Napi::Number::New(env, (double)GetLastError())); e.ThrowAsJavaScriptException(); return env.Null();
|
|
202
|
+
}
|
|
203
|
+
return Napi::Number::New(env, static_cast<double>(old));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
Napi::Value IsWow64Sync(const Napi::CallbackInfo& info) {
|
|
207
|
+
Napi::Env env = info.Env();
|
|
208
|
+
if (info.Length() < 1 || !info[0].IsNumber()) { Napi::TypeError::New(env, "expected handle").ThrowAsJavaScriptException(); return env.Null(); }
|
|
209
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
210
|
+
BOOL w = FALSE;
|
|
211
|
+
IsWow64Process(h, &w);
|
|
212
|
+
return Napi::Boolean::New(env, w != 0);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
Napi::Value ListThreadsSync(const Napi::CallbackInfo& info) {
|
|
216
|
+
Napi::Env env = info.Env();
|
|
217
|
+
if (info.Length() < 1 || !info[0].IsNumber()) { Napi::TypeError::New(env, "expected pid").ThrowAsJavaScriptException(); return env.Null(); }
|
|
218
|
+
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
|
219
|
+
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
|
|
220
|
+
if (snap == INVALID_HANDLE_VALUE) return Napi::Array::New(env, 0); // callers expect number[], never null
|
|
221
|
+
std::vector<DWORD> tids;
|
|
222
|
+
THREADENTRY32 te{}; te.dwSize = sizeof(te);
|
|
223
|
+
if (Thread32First(snap, &te)) {
|
|
224
|
+
do { if (te.th32OwnerProcessID == pid) tids.push_back(te.th32ThreadID); } while (Thread32Next(snap, &te));
|
|
225
|
+
}
|
|
226
|
+
CloseHandle(snap);
|
|
227
|
+
auto arr = Napi::Array::New(env, tids.size());
|
|
228
|
+
for (size_t i = 0; i < tids.size(); i++) arr.Set(i, Napi::Number::New(env, tids[i]));
|
|
229
|
+
return arr;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
Napi::Value SuspendThreadSync(const Napi::CallbackInfo& info) {
|
|
233
|
+
Napi::Env env = info.Env();
|
|
234
|
+
if (info.Length() < 1 || !info[0].IsNumber()) { Napi::TypeError::New(env, "expected tid").ThrowAsJavaScriptException(); return env.Null(); }
|
|
235
|
+
DWORD tid = info[0].As<Napi::Number>().Uint32Value();
|
|
236
|
+
// QA4-C3: suspending OUR OWN thread from a sync binding freezes the JS
|
|
237
|
+
// engine mid-call - it can never reach resumeThread (unrecoverable
|
|
238
|
+
// deadlock). Refuse silently (false) so suspendAll() on the caller's own
|
|
239
|
+
// pid skips the calling thread instead of hanging.
|
|
240
|
+
if (tid == GetCurrentThreadId()) return Napi::Boolean::New(env, false);
|
|
241
|
+
HANDLE t = OpenThread(THREAD_SUSPEND_RESUME, FALSE, tid);
|
|
242
|
+
if (!t) return Napi::Boolean::New(env, false);
|
|
243
|
+
DWORD r = SuspendThread(t);
|
|
244
|
+
CloseHandle(t);
|
|
245
|
+
return Napi::Boolean::New(env, r != (DWORD)-1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// currentThreadId() -> number
|
|
249
|
+
// Exported so the TS suspendAll() can skip the calling thread explicitly.
|
|
250
|
+
Napi::Value CurrentThreadIdSync(const Napi::CallbackInfo& info) {
|
|
251
|
+
Napi::Env env = info.Env();
|
|
252
|
+
return Napi::Number::New(env, static_cast<double>(GetCurrentThreadId()));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
Napi::Value ResumeThreadSync(const Napi::CallbackInfo& info) {
|
|
256
|
+
Napi::Env env = info.Env();
|
|
257
|
+
if (info.Length() < 1 || !info[0].IsNumber()) { Napi::TypeError::New(env, "expected tid").ThrowAsJavaScriptException(); return env.Null(); }
|
|
258
|
+
DWORD tid = info[0].As<Napi::Number>().Uint32Value();
|
|
259
|
+
HANDLE t = OpenThread(THREAD_SUSPEND_RESUME, FALSE, tid);
|
|
260
|
+
if (!t) return Napi::Boolean::New(env, false);
|
|
261
|
+
DWORD r = ResumeThread(t);
|
|
262
|
+
CloseHandle(t);
|
|
263
|
+
return Napi::Boolean::New(env, r != (DWORD)-1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// readValue(handle, address, type) where type in:
|
|
267
|
+
// "int32","uint32","int64","uint64","float","double","pointer"
|
|
268
|
+
Napi::Value ReadValueSync(const Napi::CallbackInfo& info) {
|
|
269
|
+
Napi::Env env = info.Env();
|
|
270
|
+
if (info.Length() < 3) { Napi::TypeError::New(env, "expected (handle,address,type)").ThrowAsJavaScriptException(); return env.Null(); }
|
|
271
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
272
|
+
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
273
|
+
std::string type = info[2].As<Napi::String>().Utf8Value();
|
|
274
|
+
try {
|
|
275
|
+
if (type == "int32") return Napi::Number::New(env, ReadValue<std::int32_t>(h, addr));
|
|
276
|
+
if (type == "uint32") return Napi::Number::New(env, ReadValue<std::uint32_t>(h, addr));
|
|
277
|
+
if (type == "int64") return Napi::BigInt::New(env, ReadValue<std::int64_t>(h, addr));
|
|
278
|
+
if (type == "uint64") return Napi::BigInt::New(env, ReadValue<std::uint64_t>(h, addr));
|
|
279
|
+
if (type == "float") return Napi::Number::New(env, ReadValue<float>(h, addr));
|
|
280
|
+
if (type == "double") return Napi::Number::New(env, ReadValue<double>(h, addr));
|
|
281
|
+
if (type == "pointer") return Napi::BigInt::New(env, static_cast<std::uint64_t>(ReadValue<std::uintptr_t>(h, addr)));
|
|
282
|
+
Napi::TypeError::New(env, "unknown type " + type).ThrowAsJavaScriptException();
|
|
283
|
+
return env.Null();
|
|
284
|
+
} catch (const std::system_error& e) {
|
|
285
|
+
auto err = Napi::Error::New(env, e.what());
|
|
286
|
+
err.Set("code", "READ_FAILED");
|
|
287
|
+
err.Set("win32code", Napi::Number::New(env, static_cast<double>(e.code().value())));
|
|
288
|
+
err.ThrowAsJavaScriptException();
|
|
289
|
+
return env.Null();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
Napi::Value WriteValueSync(const Napi::CallbackInfo& info) {
|
|
294
|
+
Napi::Env env = info.Env();
|
|
295
|
+
if (info.Length() < 4) { Napi::TypeError::New(env, "expected (handle,address,type,value)").ThrowAsJavaScriptException(); return env.Null(); }
|
|
296
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
297
|
+
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
298
|
+
std::string type = info[2].As<Napi::String>().Utf8Value();
|
|
299
|
+
bool ok = false;
|
|
300
|
+
try {
|
|
301
|
+
if (type == "int32") ok = WriteValue<std::int32_t>(h, addr, info[3].As<Napi::Number>().Int32Value());
|
|
302
|
+
else if (type == "uint32") ok = WriteValue<std::uint32_t>(h, addr, info[3].As<Napi::Number>().Uint32Value());
|
|
303
|
+
else if (type == "float") ok = WriteValue<float>(h, addr, info[3].As<Napi::Number>().FloatValue());
|
|
304
|
+
else if (type == "double") ok = WriteValue<double>(h, addr, info[3].As<Napi::Number>().DoubleValue());
|
|
305
|
+
else if (type == "int64") { bool ls; ok = WriteValue<std::int64_t>(h, addr, info[3].As<Napi::BigInt>().Int64Value(&ls)); }
|
|
306
|
+
else if (type == "uint64") { bool ls; ok = WriteValue<std::uint64_t>(h, addr, info[3].As<Napi::BigInt>().Uint64Value(&ls)); }
|
|
307
|
+
else if (type == "pointer") { bool ls; ok = WriteValue<std::uintptr_t>(h, addr, static_cast<std::uintptr_t>(info[3].As<Napi::BigInt>().Uint64Value(&ls))); }
|
|
308
|
+
else { Napi::TypeError::New(env, "unknown type " + type).ThrowAsJavaScriptException(); return env.Null(); }
|
|
309
|
+
return Napi::Boolean::New(env, ok);
|
|
310
|
+
} catch (...) {
|
|
311
|
+
auto err = Napi::Error::New(env, "WriteProcessMemory failed");
|
|
312
|
+
err.Set("code", "WRITE_FAILED");
|
|
313
|
+
err.ThrowAsJavaScriptException();
|
|
314
|
+
return env.Null();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------- v2 final batch ----------
|
|
319
|
+
|
|
320
|
+
// readSafe(handle, address, size) -> Buffer
|
|
321
|
+
// Walks regions via VirtualQueryEx; reads committed+readable spans, zero-fills
|
|
322
|
+
// gaps/unreadable spans. Only throws if the handle is invalid for VirtualQueryEx.
|
|
323
|
+
Napi::Value ReadSafeSync(const Napi::CallbackInfo& info) {
|
|
324
|
+
Napi::Env env = info.Env();
|
|
325
|
+
if (info.Length() < 3) { Napi::TypeError::New(env, "expected (handle,address,size)").ThrowAsJavaScriptException(); return env.Null(); }
|
|
326
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
327
|
+
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
328
|
+
double sizeD = info[2].As<Napi::Number>().DoubleValue();
|
|
329
|
+
if (!CheckSize(env, sizeD)) return env.Null();
|
|
330
|
+
auto size = static_cast<std::size_t>(sizeD);
|
|
331
|
+
|
|
332
|
+
std::vector<std::uint8_t> out(size, 0);
|
|
333
|
+
const std::uintptr_t end = addr + size;
|
|
334
|
+
const std::uintptr_t kMax = 0x7FFFFFFFFFFFULL;
|
|
335
|
+
std::uintptr_t cur = addr;
|
|
336
|
+
|
|
337
|
+
while (cur < end && cur <= kMax) {
|
|
338
|
+
MEMORY_BASIC_INFORMATION mbi{};
|
|
339
|
+
if (!VirtualQueryEx(h, reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi))) {
|
|
340
|
+
DWORD e = GetLastError();
|
|
341
|
+
// ERROR_INVALID_HANDLE / ERROR_ACCESS_DENIED => handle problem: throw.
|
|
342
|
+
// Anything else (invalid address etc.) => leave zeros, we're done.
|
|
343
|
+
if (e == ERROR_INVALID_HANDLE || e == ERROR_ACCESS_DENIED) {
|
|
344
|
+
auto err = Napi::Error::New(env, "VirtualQueryEx failed");
|
|
345
|
+
err.Set("code", "READ_FAILED");
|
|
346
|
+
err.Set("win32code", Napi::Number::New(env, static_cast<double>(e)));
|
|
347
|
+
err.ThrowAsJavaScriptException();
|
|
348
|
+
return env.Null();
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
std::uintptr_t rBase = reinterpret_cast<std::uintptr_t>(mbi.BaseAddress);
|
|
354
|
+
std::uintptr_t rEnd = rBase + mbi.RegionSize;
|
|
355
|
+
if (mbi.RegionSize == 0) break;
|
|
356
|
+
if (rEnd > cur && rBase < end) { // overlap
|
|
357
|
+
std::uintptr_t lo = rBase > cur ? rBase : cur;
|
|
358
|
+
std::uintptr_t hi = rEnd < end ? rEnd : end;
|
|
359
|
+
bool readable = (mbi.State == MEM_COMMIT) &&
|
|
360
|
+
!(mbi.Protect & PAGE_NOACCESS) &&
|
|
361
|
+
!(mbi.Protect & PAGE_GUARD);
|
|
362
|
+
if (readable) {
|
|
363
|
+
SIZE_T got = 0;
|
|
364
|
+
ReadProcessMemory(h, reinterpret_cast<LPCVOID>(lo),
|
|
365
|
+
out.data() + (lo - addr), hi - lo, &got);
|
|
366
|
+
// short/failed reads leave those bytes zeroed
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
cur = rEnd;
|
|
370
|
+
}
|
|
371
|
+
return Napi::Buffer<std::uint8_t>::Copy(env, out.data(), out.size());
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// flushInstructions(handle, address, size) -> boolean (FlushInstructionCache)
|
|
375
|
+
Napi::Value FlushInstructionsSync(const Napi::CallbackInfo& info) {
|
|
376
|
+
Napi::Env env = info.Env();
|
|
377
|
+
if (info.Length() < 3) { Napi::TypeError::New(env, "expected (handle,address,size)").ThrowAsJavaScriptException(); return env.Null(); }
|
|
378
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
379
|
+
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
380
|
+
auto size = static_cast<SIZE_T>(info[2].As<Napi::Number>().DoubleValue());
|
|
381
|
+
return Napi::Boolean::New(env,
|
|
382
|
+
FlushInstructionCache(h, reinterpret_cast<LPCVOID>(addr), size) != 0);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// writeProtectedMemory(handle, address, buffer[, protection]) -> boolean
|
|
386
|
+
// QA4-M1: the old TS flow did protect(addr,len)->oldProt(first page only)->
|
|
387
|
+
// write->protect(addr,len,oldProt) - a write spanning a protection boundary
|
|
388
|
+
// restored EVERY page in the span to the first page's old protection,
|
|
389
|
+
// silently changing protections on the second region. This native
|
|
390
|
+
// implementation walks VirtualQueryEx over the write span, records each
|
|
391
|
+
// region's real old protection, unprotects the whole span, writes, then
|
|
392
|
+
// restores each region individually. Single-region writes keep identical
|
|
393
|
+
// behavior. Restore failures are reported via the `restored` flag - a
|
|
394
|
+
// restored=false result means part of the span is left at `protection`.
|
|
395
|
+
Napi::Value WriteProtectedMemorySync(const Napi::CallbackInfo& info) {
|
|
396
|
+
Napi::Env env = info.Env();
|
|
397
|
+
if (info.Length() < 3 || !info[0].IsNumber() || !info[1].IsNumber()) {
|
|
398
|
+
Napi::TypeError::New(env, "expected (handle,address,buffer[,protection])")
|
|
399
|
+
.ThrowAsJavaScriptException();
|
|
400
|
+
return env.Null();
|
|
401
|
+
}
|
|
402
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
403
|
+
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
404
|
+
if (!info[2].IsBuffer()) {
|
|
405
|
+
Napi::TypeError::New(env, "value must be a Buffer").ThrowAsJavaScriptException();
|
|
406
|
+
return env.Null();
|
|
407
|
+
}
|
|
408
|
+
auto buf = info[2].As<Napi::Buffer<std::uint8_t>>();
|
|
409
|
+
DWORD newProt = PAGE_EXECUTE_READWRITE;
|
|
410
|
+
if (info.Length() > 3 && info[3].IsNumber())
|
|
411
|
+
newProt = info[3].As<Napi::Number>().Uint32Value();
|
|
412
|
+
if (buf.Length() == 0) {
|
|
413
|
+
// Nothing to do - return the same result shape as a real write.
|
|
414
|
+
auto o = Napi::Object::New(env);
|
|
415
|
+
o.Set("ok", Napi::Boolean::New(env, true));
|
|
416
|
+
o.Set("restored", Napi::Boolean::New(env, true));
|
|
417
|
+
o.Set("protected", Napi::Boolean::New(env, true));
|
|
418
|
+
return o;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// QA4-M1: record per-region old protection across the whole write span
|
|
422
|
+
// BEFORE unprotecting - VirtualProtectEx only reports the first page's
|
|
423
|
+
// old value, so the query walk is the only correct source.
|
|
424
|
+
struct RegionSnap { std::uintptr_t base; std::uintptr_t end; DWORD oldProt; };
|
|
425
|
+
std::vector<RegionSnap> regions;
|
|
426
|
+
const std::uintptr_t end = addr + buf.Length();
|
|
427
|
+
{
|
|
428
|
+
std::uintptr_t cur = addr;
|
|
429
|
+
while (cur < end) {
|
|
430
|
+
MEMORY_BASIC_INFORMATION mbi{};
|
|
431
|
+
if (!VirtualQueryEx(h, reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi)) ||
|
|
432
|
+
mbi.State != MEM_COMMIT || mbi.RegionSize == 0) {
|
|
433
|
+
auto e = Napi::Error::New(env, "VirtualQueryEx failed or region not committed");
|
|
434
|
+
e.Set("code", "PROTECT_FAILED");
|
|
435
|
+
e.Set("win32code", Napi::Number::New(env, (double)GetLastError()));
|
|
436
|
+
e.ThrowAsJavaScriptException();
|
|
437
|
+
return env.Null();
|
|
438
|
+
}
|
|
439
|
+
std::uintptr_t rBase = reinterpret_cast<std::uintptr_t>(mbi.BaseAddress);
|
|
440
|
+
std::uintptr_t rEnd = rBase + mbi.RegionSize;
|
|
441
|
+
regions.push_back({ rBase < cur ? cur : rBase,
|
|
442
|
+
rEnd > end ? end : rEnd, mbi.Protect });
|
|
443
|
+
cur = rEnd;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Unprotect each region span (VirtualProtectEx clamps to whole pages, which
|
|
448
|
+
// is fine - only pages intersecting [addr,end) are touched).
|
|
449
|
+
bool allProtected = true;
|
|
450
|
+
for (const auto& r : regions) {
|
|
451
|
+
DWORD old = 0;
|
|
452
|
+
if (!VirtualProtectEx(h, reinterpret_cast<LPVOID>(r.base), r.end - r.base,
|
|
453
|
+
newProt, &old)) {
|
|
454
|
+
allProtected = false; // continue - best effort
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
SIZE_T written = 0;
|
|
459
|
+
const bool wrote = WriteProcessMemory(
|
|
460
|
+
h, reinterpret_cast<LPVOID>(addr), buf.Data(), buf.Length(), &written) &&
|
|
461
|
+
written == buf.Length();
|
|
462
|
+
|
|
463
|
+
// Restore each region to ITS recorded protection (not the first page's).
|
|
464
|
+
bool allRestored = true;
|
|
465
|
+
for (const auto& r : regions) {
|
|
466
|
+
DWORD old = 0;
|
|
467
|
+
if (!VirtualProtectEx(h, reinterpret_cast<LPVOID>(r.base), r.end - r.base,
|
|
468
|
+
r.oldProt, &old)) {
|
|
469
|
+
allRestored = false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
FlushInstructionCache(h, reinterpret_cast<LPCVOID>(addr), buf.Length());
|
|
473
|
+
|
|
474
|
+
auto o = Napi::Object::New(env);
|
|
475
|
+
o.Set("ok", Napi::Boolean::New(env, wrote));
|
|
476
|
+
o.Set("restored", Napi::Boolean::New(env, allRestored));
|
|
477
|
+
o.Set("protected", Napi::Boolean::New(env, allProtected));
|
|
478
|
+
return o;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
static void ThrowInject(Napi::Env env, const char* what, DWORD code) {
|
|
482
|
+
auto err = Napi::Error::New(env, what);
|
|
483
|
+
err.Set("code", "INJECT_FAILED");
|
|
484
|
+
err.Set("win32code", Napi::Number::New(env, static_cast<double>(code)));
|
|
485
|
+
err.ThrowAsJavaScriptException();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// injectDll(pid, dllPath) -> boolean
|
|
489
|
+
// QA4-M2: dllPath must be ABSOLUTE. A relative name resolves against the
|
|
490
|
+
// TARGET's cwd + DLL search order - a DLL-planting primitive when that cwd is
|
|
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 - the
|
|
493
|
+
// old LoadLibraryA path mangled non-ANSI filenames per the target's codepage.
|
|
494
|
+
static bool IsAbsolutePath(const std::string& p) {
|
|
495
|
+
if (p.size() >= 3 && ((p[0] >= 'A' && p[0] <= 'Z') || (p[0] >= 'a' && p[0] <= 'z')) &&
|
|
496
|
+
p[1] == ':' && (p[2] == '\\' || p[2] == '/'))
|
|
497
|
+
return true; // X:\...
|
|
498
|
+
if (p.size() >= 2 && p[0] == '\\' && p[1] == '\\')
|
|
499
|
+
return true; // \\server\share or \\?\...
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
Napi::Value InjectDllSync(const Napi::CallbackInfo& info) {
|
|
504
|
+
Napi::Env env = info.Env();
|
|
505
|
+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsString()) {
|
|
506
|
+
Napi::TypeError::New(env, "expected (pid, dllPath)").ThrowAsJavaScriptException();
|
|
507
|
+
return env.Null();
|
|
508
|
+
}
|
|
509
|
+
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
|
510
|
+
std::string path = info[1].As<Napi::String>().Utf8Value();
|
|
511
|
+
if (!IsAbsolutePath(path)) {
|
|
512
|
+
ThrowInject(env, "dllPath must be an absolute path (X:\\... or \\\\UNC)", ERROR_INVALID_NAME);
|
|
513
|
+
return env.Null();
|
|
514
|
+
}
|
|
515
|
+
if (path.size() > 32767) {
|
|
516
|
+
ThrowInject(env, "dllPath exceeds maximum NT path length (32767)", ERROR_FILENAME_EXCED_RANGE);
|
|
517
|
+
return env.Null();
|
|
518
|
+
}
|
|
519
|
+
// Optional 3rd arg: remote-thread wait timeout (bounded - a negative or
|
|
520
|
+
// huge value would wrap DWORD into an ~infinite sync wait, QA4-H1).
|
|
521
|
+
DWORD waitMs = 10000;
|
|
522
|
+
if (info.Length() > 2 && info[2].IsNumber()) {
|
|
523
|
+
double t = info[2].As<Napi::Number>().DoubleValue();
|
|
524
|
+
if (!std::isfinite(t) || t < 0 || t > 60000) {
|
|
525
|
+
Napi::RangeError::New(env, "timeoutMs must be a finite number in [0, 60000]")
|
|
526
|
+
.ThrowAsJavaScriptException();
|
|
527
|
+
return env.Null();
|
|
528
|
+
}
|
|
529
|
+
waitMs = static_cast<DWORD>(t);
|
|
530
|
+
}
|
|
531
|
+
// UTF-8 -> UTF-16LE for LoadLibraryW.
|
|
532
|
+
std::wstring wpath;
|
|
533
|
+
{
|
|
534
|
+
int n = MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, nullptr, 0);
|
|
535
|
+
if (n <= 0) { ThrowInject(env, "dllPath is not valid UTF-8", ERROR_NO_UNICODE_TRANSLATION); return env.Null(); }
|
|
536
|
+
wpath.assign(static_cast<size_t>(n) - 1, L'\0');
|
|
537
|
+
MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, wpath.data(), n);
|
|
538
|
+
}
|
|
539
|
+
const DWORD access = PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION |
|
|
540
|
+
PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION;
|
|
541
|
+
HANDLE h = OpenProcess(access, FALSE, pid);
|
|
542
|
+
if (!h) { ThrowInject(env, "OpenProcess failed", GetLastError()); return env.Null(); }
|
|
543
|
+
|
|
544
|
+
// WoW64 targets load a 32-bit kernel32 at a different base - LoadLibraryW
|
|
545
|
+
// resolved in *our* (x64) process would be the wrong address. Refuse rather
|
|
546
|
+
// than silently corrupting the target.
|
|
547
|
+
{
|
|
548
|
+
BOOL wow = FALSE;
|
|
549
|
+
IsWow64Process(h, &wow);
|
|
550
|
+
if (wow) {
|
|
551
|
+
CloseHandle(h);
|
|
552
|
+
ThrowInject(env, "injectDll does not support WoW64 (32-bit) targets", ERROR_NOT_SUPPORTED);
|
|
553
|
+
return env.Null();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
bool ok = false;
|
|
558
|
+
do {
|
|
559
|
+
const SIZE_T wbytes = (wpath.size() + 1) * sizeof(wchar_t);
|
|
560
|
+
LPVOID remote = VirtualAllocEx(h, nullptr, wbytes,
|
|
561
|
+
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
|
562
|
+
if (!remote) { DWORD e = GetLastError(); ThrowInject(env, "VirtualAllocEx failed", e); break; }
|
|
563
|
+
SIZE_T w = 0;
|
|
564
|
+
if (!WriteProcessMemory(h, remote, wpath.c_str(), wbytes, &w)) {
|
|
565
|
+
DWORD e = GetLastError();
|
|
566
|
+
VirtualFreeEx(h, remote, 0, MEM_RELEASE);
|
|
567
|
+
ThrowInject(env, "WriteProcessMemory failed", e); break;
|
|
568
|
+
}
|
|
569
|
+
// NOTE: kernel32 is loaded at the same base in every x64 process, so this
|
|
570
|
+
// locally-resolved LoadLibraryW address is valid in the target. This holds
|
|
571
|
+
// because kernel32 is a KnownDll mapped at a shared ASLR base.
|
|
572
|
+
HMODULE k32 = GetModuleHandleW(L"kernel32.dll");
|
|
573
|
+
FARPROC loadLib = k32 ? GetProcAddress(k32, "LoadLibraryW") : nullptr;
|
|
574
|
+
if (!loadLib) {
|
|
575
|
+
DWORD e = GetLastError();
|
|
576
|
+
VirtualFreeEx(h, remote, 0, MEM_RELEASE);
|
|
577
|
+
ThrowInject(env, "LoadLibraryW resolution failed", e); break;
|
|
578
|
+
}
|
|
579
|
+
HANDLE t = CreateRemoteThread(h, nullptr, 0,
|
|
580
|
+
reinterpret_cast<LPTHREAD_START_ROUTINE>(loadLib), remote, 0, nullptr);
|
|
581
|
+
if (!t) {
|
|
582
|
+
DWORD e = GetLastError();
|
|
583
|
+
VirtualFreeEx(h, remote, 0, MEM_RELEASE);
|
|
584
|
+
ThrowInject(env, "CreateRemoteThread failed", e); break;
|
|
585
|
+
}
|
|
586
|
+
DWORD wr = WaitForSingleObject(t, waitMs);
|
|
587
|
+
if (wr != WAIT_OBJECT_0) {
|
|
588
|
+
// QA4-C1: do NOT TerminateThread here. TerminateThread is asynchronous
|
|
589
|
+
// and killing a thread inside LoadLibraryW can leave the target's
|
|
590
|
+
// loader lock permanently held - every subsequent LoadLibrary /
|
|
591
|
+
// thread-creation in the victim deadlocks. Instead we close our handle
|
|
592
|
+
// and deliberately LEAK the small path buffer so a late-finishing
|
|
593
|
+
// thread never reads freed memory. Bounded leak <=64KB; a wedged target
|
|
594
|
+
// is worse.
|
|
595
|
+
CloseHandle(t);
|
|
596
|
+
ThrowInject(env, "LoadLibraryW remote thread timed out (leaked path buffer; target not terminated to avoid loader-lock deadlock)",
|
|
597
|
+
wr == WAIT_FAILED ? GetLastError() : ERROR_TIMEOUT);
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
DWORD exitCode = 0;
|
|
601
|
+
GetExitCodeThread(t, &exitCode);
|
|
602
|
+
CloseHandle(t);
|
|
603
|
+
VirtualFreeEx(h, remote, 0, MEM_RELEASE);
|
|
604
|
+
// LoadLibraryW returns the module HMODULE as the thread exit code; 0 means
|
|
605
|
+
// the DLL failed to load (not found, bad bitness, missing dependency).
|
|
606
|
+
if (exitCode == 0) {
|
|
607
|
+
ThrowInject(env, "LoadLibraryW returned NULL in target", ERROR_MOD_NOT_FOUND);
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
ok = true;
|
|
611
|
+
} while (0);
|
|
612
|
+
CloseHandle(h);
|
|
613
|
+
if (!ok) return env.Null(); // exception already thrown
|
|
614
|
+
return Napi::Boolean::New(env, true);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ejectDll(pid, moduleBase) -> boolean
|
|
618
|
+
Napi::Value EjectDllSync(const Napi::CallbackInfo& info) {
|
|
619
|
+
Napi::Env env = info.Env();
|
|
620
|
+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
|
|
621
|
+
Napi::TypeError::New(env, "expected (pid, moduleBase)").ThrowAsJavaScriptException();
|
|
622
|
+
return env.Null();
|
|
623
|
+
}
|
|
624
|
+
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
|
625
|
+
auto base = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
626
|
+
const DWORD access = PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION |
|
|
627
|
+
PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION;
|
|
628
|
+
HANDLE h = OpenProcess(access, FALSE, pid);
|
|
629
|
+
if (!h) { ThrowInject(env, "OpenProcess failed", GetLastError()); return env.Null(); }
|
|
630
|
+
|
|
631
|
+
// QA4-C4: same WoW64 guard as injectDll - our 64-bit FreeLibrary address is
|
|
632
|
+
// meaningless inside a 32-bit target's address space; the remote thread
|
|
633
|
+
// would execute a wild pointer.
|
|
634
|
+
{
|
|
635
|
+
BOOL wow = FALSE;
|
|
636
|
+
IsWow64Process(h, &wow);
|
|
637
|
+
if (wow) {
|
|
638
|
+
CloseHandle(h);
|
|
639
|
+
ThrowInject(env, "ejectDll does not support WoW64 (32-bit) targets", ERROR_NOT_SUPPORTED);
|
|
640
|
+
return env.Null();
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
bool ok = false;
|
|
645
|
+
do {
|
|
646
|
+
HMODULE k32 = GetModuleHandleW(L"kernel32.dll");
|
|
647
|
+
FARPROC freeLib = k32 ? GetProcAddress(k32, "FreeLibrary") : nullptr;
|
|
648
|
+
if (!freeLib) { ThrowInject(env, "FreeLibrary resolution failed", GetLastError()); break; }
|
|
649
|
+
HANDLE t = CreateRemoteThread(h, nullptr, 0,
|
|
650
|
+
reinterpret_cast<LPTHREAD_START_ROUTINE>(freeLib),
|
|
651
|
+
reinterpret_cast<LPVOID>(base), 0, nullptr);
|
|
652
|
+
if (!t) { ThrowInject(env, "CreateRemoteThread failed", GetLastError()); break; }
|
|
653
|
+
DWORD wr = WaitForSingleObject(t, 10000);
|
|
654
|
+
if (wr != WAIT_OBJECT_0) {
|
|
655
|
+
// QA4-C1: never TerminateThread inside FreeLibrary - the remote thread
|
|
656
|
+
// can hold the loader lock; killing it wedges the target permanently.
|
|
657
|
+
// Just close our handle and report the timeout.
|
|
658
|
+
CloseHandle(t);
|
|
659
|
+
ThrowInject(env, "FreeLibrary remote thread timed out (target thread left running to avoid loader-lock deadlock)",
|
|
660
|
+
wr == WAIT_FAILED ? GetLastError() : ERROR_TIMEOUT);
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
// QA4-C4: FreeLibrary returns BOOL as the thread exit code - verify it.
|
|
664
|
+
// Without this check, ejecting kernel32/main.exe/garbage bases reported
|
|
665
|
+
// success while nothing (or worse, the wrong thing) happened.
|
|
666
|
+
DWORD exitCode = 0;
|
|
667
|
+
GetExitCodeThread(t, &exitCode);
|
|
668
|
+
CloseHandle(t);
|
|
669
|
+
if (exitCode == 0) {
|
|
670
|
+
ThrowInject(env, "FreeLibrary returned FALSE in target (bad/foreign module base or refcount pinned)",
|
|
671
|
+
ERROR_MOD_NOT_FOUND);
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
ok = true;
|
|
675
|
+
} while (0);
|
|
676
|
+
CloseHandle(h);
|
|
677
|
+
if (!ok) return env.Null();
|
|
678
|
+
return Napi::Boolean::New(env, true);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ---------- v2 Tier-3 ----------
|
|
682
|
+
|
|
683
|
+
// queryAddress(handle, address) -> { baseAddress, allocationBase, size,
|
|
684
|
+
// protection, state, type } | null
|
|
685
|
+
Napi::Value QueryAddressSync(const Napi::CallbackInfo& info) {
|
|
686
|
+
Napi::Env env = info.Env();
|
|
687
|
+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
|
|
688
|
+
Napi::TypeError::New(env, "expected (handle,address)").ThrowAsJavaScriptException();
|
|
689
|
+
return env.Null();
|
|
690
|
+
}
|
|
691
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
692
|
+
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
693
|
+
MEMORY_BASIC_INFORMATION mbi{};
|
|
694
|
+
if (!VirtualQueryEx(h, reinterpret_cast<LPCVOID>(addr), &mbi, sizeof(mbi)))
|
|
695
|
+
return env.Null();
|
|
696
|
+
if (mbi.State == MEM_FREE) return env.Null(); // unmapped
|
|
697
|
+
auto o = Napi::Object::New(env);
|
|
698
|
+
o.Set("baseAddress", Napi::Number::New(env, static_cast<double>(reinterpret_cast<std::uintptr_t>(mbi.BaseAddress))));
|
|
699
|
+
o.Set("allocationBase", Napi::Number::New(env, static_cast<double>(reinterpret_cast<std::uintptr_t>(mbi.AllocationBase))));
|
|
700
|
+
o.Set("size", Napi::Number::New(env, static_cast<double>(mbi.RegionSize)));
|
|
701
|
+
o.Set("protection", Napi::Number::New(env, static_cast<double>(mbi.Protect)));
|
|
702
|
+
o.Set("state", Napi::Number::New(env, static_cast<double>(mbi.State)));
|
|
703
|
+
o.Set("type", Napi::Number::New(env, static_cast<double>(mbi.Type)));
|
|
704
|
+
return o;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
static bool GetFullContext(DWORD tid, CONTEXT& ctx) {
|
|
708
|
+
HANDLE t = OpenThread(THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, tid);
|
|
709
|
+
if (!t) {
|
|
710
|
+
// Some systems require SET_CONTEXT rights for debug registers read too;
|
|
711
|
+
// retry with ALL access on failure.
|
|
712
|
+
t = OpenThread(THREAD_ALL_ACCESS, FALSE, tid);
|
|
713
|
+
if (!t) return false;
|
|
714
|
+
}
|
|
715
|
+
ZeroMemory(&ctx, sizeof(ctx));
|
|
716
|
+
ctx.ContextFlags = CONTEXT_ALL | CONTEXT_DEBUG_REGISTERS;
|
|
717
|
+
BOOL ok = GetThreadContext(t, &ctx);
|
|
718
|
+
CloseHandle(t);
|
|
719
|
+
return ok != 0;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// getThreadContext(tid) -> { rip,rsp,rbp,dr0..dr3,dr6,dr7,eflags } | null
|
|
723
|
+
// NOTE: caller is expected to have suspended the thread first.
|
|
724
|
+
Napi::Value GetThreadContextSync(const Napi::CallbackInfo& info) {
|
|
725
|
+
Napi::Env env = info.Env();
|
|
726
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
727
|
+
Napi::TypeError::New(env, "expected (tid)").ThrowAsJavaScriptException();
|
|
728
|
+
return env.Null();
|
|
729
|
+
}
|
|
730
|
+
DWORD tid = info[0].As<Napi::Number>().Uint32Value();
|
|
731
|
+
CONTEXT ctx{};
|
|
732
|
+
if (!GetFullContext(tid, ctx)) return env.Null();
|
|
733
|
+
auto o = Napi::Object::New(env);
|
|
734
|
+
o.Set("rip", Napi::BigInt::New(env, static_cast<std::uint64_t>(ctx.Rip)));
|
|
735
|
+
o.Set("rsp", Napi::BigInt::New(env, static_cast<std::uint64_t>(ctx.Rsp)));
|
|
736
|
+
o.Set("rbp", Napi::BigInt::New(env, static_cast<std::uint64_t>(ctx.Rbp)));
|
|
737
|
+
o.Set("dr0", Napi::BigInt::New(env, static_cast<std::uint64_t>(ctx.Dr0)));
|
|
738
|
+
o.Set("dr1", Napi::BigInt::New(env, static_cast<std::uint64_t>(ctx.Dr1)));
|
|
739
|
+
o.Set("dr2", Napi::BigInt::New(env, static_cast<std::uint64_t>(ctx.Dr2)));
|
|
740
|
+
o.Set("dr3", Napi::BigInt::New(env, static_cast<std::uint64_t>(ctx.Dr3)));
|
|
741
|
+
o.Set("dr6", Napi::BigInt::New(env, static_cast<std::uint64_t>(ctx.Dr6)));
|
|
742
|
+
o.Set("dr7", Napi::BigInt::New(env, static_cast<std::uint64_t>(ctx.Dr7)));
|
|
743
|
+
o.Set("eflags", Napi::BigInt::New(env, static_cast<std::uint64_t>(ctx.EFlags)));
|
|
744
|
+
return o;
|
|
745
|
+
}
|
|
746
|
+
|
|
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
|
+
// setThreadContext(tid, { rip?, rsp?, rbp?, dr0?..dr3?, dr6?, dr7?, eflags? }) -> boolean
|
|
754
|
+
// Reads the current context, patches provided fields, writes it back.
|
|
755
|
+
Napi::Value SetThreadContextSync(const Napi::CallbackInfo& info) {
|
|
756
|
+
Napi::Env env = info.Env();
|
|
757
|
+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsObject()) {
|
|
758
|
+
Napi::TypeError::New(env, "expected (tid, ctx)").ThrowAsJavaScriptException();
|
|
759
|
+
return env.Null();
|
|
760
|
+
}
|
|
761
|
+
DWORD tid = info[0].As<Napi::Number>().Uint32Value();
|
|
762
|
+
auto patch = info[1].As<Napi::Object>();
|
|
763
|
+
|
|
764
|
+
CONTEXT ctx{};
|
|
765
|
+
if (!GetFullContext(tid, ctx)) return Napi::Boolean::New(env, false);
|
|
766
|
+
|
|
767
|
+
std::uint64_t v;
|
|
768
|
+
auto set = [&](const char* name, std::uint64_t& field) {
|
|
769
|
+
if (patch.Has(name) && JsToU64(patch.Get(name), v)) field = v;
|
|
770
|
+
};
|
|
771
|
+
set("rip", *reinterpret_cast<std::uint64_t*>(&ctx.Rip));
|
|
772
|
+
set("rsp", *reinterpret_cast<std::uint64_t*>(&ctx.Rsp));
|
|
773
|
+
set("rbp", *reinterpret_cast<std::uint64_t*>(&ctx.Rbp));
|
|
774
|
+
set("dr0", *reinterpret_cast<std::uint64_t*>(&ctx.Dr0));
|
|
775
|
+
set("dr1", *reinterpret_cast<std::uint64_t*>(&ctx.Dr1));
|
|
776
|
+
set("dr2", *reinterpret_cast<std::uint64_t*>(&ctx.Dr2));
|
|
777
|
+
set("dr3", *reinterpret_cast<std::uint64_t*>(&ctx.Dr3));
|
|
778
|
+
set("dr6", *reinterpret_cast<std::uint64_t*>(&ctx.Dr6));
|
|
779
|
+
set("dr7", *reinterpret_cast<std::uint64_t*>(&ctx.Dr7));
|
|
780
|
+
if (patch.Has("eflags") && JsToU64(patch.Get("eflags"), v)) ctx.EFlags = static_cast<DWORD>(v);
|
|
781
|
+
|
|
782
|
+
HANDLE t = OpenThread(THREAD_SET_CONTEXT, FALSE, tid);
|
|
783
|
+
if (!t) {
|
|
784
|
+
t = OpenThread(THREAD_ALL_ACCESS, FALSE, tid);
|
|
785
|
+
if (!t) return Napi::Boolean::New(env, false);
|
|
786
|
+
}
|
|
787
|
+
ctx.ContextFlags = CONTEXT_ALL | CONTEXT_DEBUG_REGISTERS;
|
|
788
|
+
BOOL ok = SetThreadContext(t, &ctx);
|
|
789
|
+
CloseHandle(t);
|
|
790
|
+
return Napi::Boolean::New(env, ok != 0);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// callRemote(pid, fnAddress, param, timeoutMs?) -> exitCode | null
|
|
794
|
+
Napi::Value CallRemoteSync(const Napi::CallbackInfo& info) {
|
|
795
|
+
Napi::Env env = info.Env();
|
|
796
|
+
if (info.Length() < 3 || !info[0].IsNumber()) {
|
|
797
|
+
Napi::TypeError::New(env, "expected (pid, fnAddress, param[, timeoutMs])").ThrowAsJavaScriptException();
|
|
798
|
+
return env.Null();
|
|
799
|
+
}
|
|
800
|
+
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
|
801
|
+
std::uint64_t fn = 0, param = 0;
|
|
802
|
+
if (!JsToU64(info[1], fn) || !JsToU64(info[2], param)) {
|
|
803
|
+
Napi::TypeError::New(env, "fnAddress and param must be number|bigint").ThrowAsJavaScriptException();
|
|
804
|
+
return env.Null();
|
|
805
|
+
}
|
|
806
|
+
DWORD timeout = 10000;
|
|
807
|
+
if (info.Length() > 3 && info[3].IsNumber()) {
|
|
808
|
+
// QA4-H1: Uint32Value() on -1 wraps to 0xFFFFFFFF = INFINITE - a sync
|
|
809
|
+
// binding that never returns bricks the whole JS process. Clamp to a
|
|
810
|
+
// bounded range; negative/non-finite values are a caller bug.
|
|
811
|
+
double t = info[3].As<Napi::Number>().DoubleValue();
|
|
812
|
+
if (!std::isfinite(t) || t < 0 || t > 60000) {
|
|
813
|
+
Napi::RangeError::New(env, "timeoutMs must be a finite number in [0, 60000]")
|
|
814
|
+
.ThrowAsJavaScriptException();
|
|
815
|
+
return env.Null();
|
|
816
|
+
}
|
|
817
|
+
timeout = static_cast<DWORD>(t);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
HANDLE h = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION, FALSE, pid);
|
|
821
|
+
if (!h) return env.Null();
|
|
822
|
+
HANDLE t = CreateRemoteThread(h, nullptr, 0,
|
|
823
|
+
reinterpret_cast<LPTHREAD_START_ROUTINE>(static_cast<std::uintptr_t>(fn)),
|
|
824
|
+
reinterpret_cast<LPVOID>(static_cast<std::uintptr_t>(param)), 0, nullptr);
|
|
825
|
+
if (!t) { CloseHandle(h); return env.Null(); }
|
|
826
|
+
DWORD wr = WaitForSingleObject(t, timeout);
|
|
827
|
+
DWORD code = 0;
|
|
828
|
+
// QA4-C1: TerminateThread is asynchronous - it only *requests* termination.
|
|
829
|
+
// Returning immediately after TerminateThread lets the caller (execShellcode
|
|
830
|
+
// finally-block, injectDll) free memory the zombie thread may still be
|
|
831
|
+
// executing in. Wait for the thread object itself to be signaled (max 2s)
|
|
832
|
+
// so a timeout-return really means the thread is dead. NOTE: terminating a
|
|
833
|
+
// thread that was mid-LoadLibrary can still leave the target's loader lock
|
|
834
|
+
// held - an inherent TerminateThread hazard; callers needing guaranteed
|
|
835
|
+
// non-wedging behavior should prefer a longer timeout over termination.
|
|
836
|
+
if (wr != WAIT_OBJECT_0) {
|
|
837
|
+
TerminateThread(t, 0);
|
|
838
|
+
WaitForSingleObject(t, 2000);
|
|
839
|
+
CloseHandle(t);
|
|
840
|
+
CloseHandle(h);
|
|
841
|
+
return env.Null();
|
|
842
|
+
}
|
|
843
|
+
bool ok = GetExitCodeThread(t, &code) != 0;
|
|
844
|
+
CloseHandle(t);
|
|
845
|
+
CloseHandle(h);
|
|
846
|
+
if (!ok) return env.Null();
|
|
847
|
+
return Napi::Number::New(env, static_cast<double>(code));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// ---------- v2 disasm (HDE64 length decode) ----------
|
|
851
|
+
|
|
852
|
+
// disasmInstructions(handle, address, count) -> [{offset, length}]
|
|
853
|
+
// Reads up to count*16 bytes (capped 4096) once via ReadBytes, then walks
|
|
854
|
+
// the buffer with hde64_disasm returning per-instruction offsets+lengths.
|
|
855
|
+
Napi::Value DisasmInstructionsSync(const Napi::CallbackInfo& info) {
|
|
856
|
+
Napi::Env env = info.Env();
|
|
857
|
+
if (info.Length() < 3 || !info[0].IsNumber() || !info[1].IsNumber() || !info[2].IsNumber()) {
|
|
858
|
+
Napi::TypeError::New(env, "expected (handle, address, count)").ThrowAsJavaScriptException();
|
|
859
|
+
return env.Null();
|
|
860
|
+
}
|
|
861
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
862
|
+
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
863
|
+
auto count = static_cast<std::size_t>(info[2].As<Napi::Number>().DoubleValue());
|
|
864
|
+
if (count == 0) return Napi::Array::New(env, 0);
|
|
865
|
+
if (count > 256) count = 256; // 256*15 ~ 4KB window; also prevents count*16 wraparound
|
|
866
|
+
|
|
867
|
+
std::size_t readSize = count * 16; // max x64 instruction length is 15
|
|
868
|
+
if (readSize > 4096) readSize = 4096;
|
|
869
|
+
|
|
870
|
+
std::vector<std::uint8_t> buf;
|
|
871
|
+
try {
|
|
872
|
+
buf = ReadBytes(h, addr, readSize);
|
|
873
|
+
} catch (const std::system_error& e) {
|
|
874
|
+
auto err = Napi::Error::New(env, e.what());
|
|
875
|
+
err.Set("code", "READ_FAILED");
|
|
876
|
+
err.Set("win32code", Napi::Number::New(env, static_cast<double>(e.code().value())));
|
|
877
|
+
err.ThrowAsJavaScriptException();
|
|
878
|
+
return env.Null();
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
auto arr = Napi::Array::New(env);
|
|
882
|
+
std::size_t off = 0, n = 0;
|
|
883
|
+
while (off < buf.size() && n < count) {
|
|
884
|
+
// hde64_disasm takes no length and may read up to 15 bytes for one
|
|
885
|
+
// instruction - feeding it buf.data()+off directly walks past the end of
|
|
886
|
+
// the allocation on a truncated tail. Decode from a 16-byte padded stack
|
|
887
|
+
// scratch instead; the trailing zeros make a truncated insn fail cleanly.
|
|
888
|
+
std::uint8_t scratch[16] = {};
|
|
889
|
+
const std::size_t avail = buf.size() - off;
|
|
890
|
+
std::memcpy(scratch, buf.data() + off, avail < sizeof(scratch) ? avail : sizeof(scratch));
|
|
891
|
+
hde64s hs{};
|
|
892
|
+
unsigned int len = hde64_disasm(scratch, &hs);
|
|
893
|
+
if (len == 0 || (hs.flags & F_ERROR)) break;
|
|
894
|
+
if (off + len > buf.size()) break; // truncated tail instruction
|
|
895
|
+
auto o = Napi::Object::New(env);
|
|
896
|
+
o.Set("offset", Napi::Number::New(env, static_cast<double>(off)));
|
|
897
|
+
o.Set("length", Napi::Number::New(env, static_cast<double>(len)));
|
|
898
|
+
// QA4-C2: expose position-dependence so callers (hook) can refuse to copy
|
|
899
|
+
// the instruction to a differently-based trampoline. F_RELATIVE covers
|
|
900
|
+
// rel8/16/32 branch+call immediates (E8/E9/EB, 70-7F, 0F 8x, E0-E3, xbegin).
|
|
901
|
+
// A ModRM mod=00 rm=101 encoding is RIP-relative ([rip+disp32]).
|
|
902
|
+
const bool ripRel = (hs.flags & F_MODRM) && hs.modrm_mod == 0 &&
|
|
903
|
+
hs.modrm_rm == 5;
|
|
904
|
+
o.Set("relative",
|
|
905
|
+
Napi::Boolean::New(env, (hs.flags & F_RELATIVE) != 0 || ripRel));
|
|
906
|
+
arr.Set(n, o);
|
|
907
|
+
off += len;
|
|
908
|
+
n++;
|
|
909
|
+
}
|
|
910
|
+
return arr;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// hookSteal(handle, address[, minBytes][, stubAddr]) -> { length, bytes:Buffer }
|
|
914
|
+
//
|
|
915
|
+
// MinHook-style relocation: steals whole instructions until >= minBytes are
|
|
916
|
+
// covered, then rewrites position-relative operands so the stolen bytes are
|
|
917
|
+
// correct when executed from `stubAddr` (which must be within rel32 range of
|
|
918
|
+
// `address` - the caller's #allocNear guarantees this):
|
|
919
|
+
// - rel32 branch/call (E8/E9/0F8x, xbegin): rel32 rewritten in place, OR
|
|
920
|
+
// copied verbatim when the target is INSIDE the stolen window (MinHook's
|
|
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
|
+
// still reaches the original absolute target.
|
|
924
|
+
// - rel8/rel16 branches incl. LOOP/JECXZ (E0-E3): verbatim if internal -
|
|
925
|
+
// the operand is position-independent within the window - otherwise
|
|
926
|
+
// REFUSED (rel8 can't be widened to a same-length rewrite; MinHook
|
|
927
|
+
// refuses LOOP/JECXZ-external too).
|
|
928
|
+
// Without `stubAddr` every position-relative instruction is refused
|
|
929
|
+
// (HOOK_RELOCATE) - the old verify-only behavior for raw inspection calls.
|
|
930
|
+
// Throws HOOK_STEAL when <minBytes of decodable instructions exist, or
|
|
931
|
+
// HOOK_RELOCATE naming the offending opcode/offset. Returns the stolen bytes
|
|
932
|
+
// so the caller doesn't need a second remote read.
|
|
933
|
+
Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
|
|
934
|
+
Napi::Env env = info.Env();
|
|
935
|
+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
|
|
936
|
+
Napi::TypeError::New(env, "expected (handle, address[, minBytes][, stubAddr])")
|
|
937
|
+
.ThrowAsJavaScriptException();
|
|
938
|
+
return env.Null();
|
|
939
|
+
}
|
|
940
|
+
HANDLE h = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(info[0].As<Napi::Number>().DoubleValue()));
|
|
941
|
+
auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
942
|
+
std::size_t minBytes = 5;
|
|
943
|
+
std::uintptr_t stub = 0;
|
|
944
|
+
// Accepted arg shapes:
|
|
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
|
+
if (info.Length() > 2) {
|
|
949
|
+
if (info[2].IsNumber()) {
|
|
950
|
+
double m = info[2].As<Napi::Number>().DoubleValue();
|
|
951
|
+
if (std::isfinite(m) && m >= 1 && m <= 64) minBytes = static_cast<std::size_t>(m);
|
|
952
|
+
if (info.Length() > 3) {
|
|
953
|
+
std::uint64_t s = 0;
|
|
954
|
+
if (JsToU64(info[3], s)) stub = static_cast<std::uintptr_t>(s);
|
|
955
|
+
}
|
|
956
|
+
} else if (info[2].IsObject() && !info[2].IsNull()) {
|
|
957
|
+
auto opts = info[2].As<Napi::Object>();
|
|
958
|
+
auto mb = opts.Get("minBytes");
|
|
959
|
+
if (mb.IsNumber()) {
|
|
960
|
+
double m = mb.As<Napi::Number>().DoubleValue();
|
|
961
|
+
if (std::isfinite(m) && m >= 1 && m <= 64) minBytes = static_cast<std::size_t>(m);
|
|
962
|
+
}
|
|
963
|
+
auto sa = opts.Get("stubAddr");
|
|
964
|
+
if (!sa.IsUndefined()) {
|
|
965
|
+
std::uint64_t s = 0;
|
|
966
|
+
if (JsToU64(sa, s)) stub = static_cast<std::uintptr_t>(s);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
// An instruction beginning <15 bytes before minBytes can overshoot past it -
|
|
971
|
+
// read minBytes+15 so no valid tail instruction truncates.
|
|
972
|
+
std::vector<std::uint8_t> buf;
|
|
973
|
+
try {
|
|
974
|
+
buf = ReadBytes(h, addr, minBytes + 15);
|
|
975
|
+
} catch (const std::system_error& e) {
|
|
976
|
+
auto err = Napi::Error::New(env, e.what());
|
|
977
|
+
err.Set("code", "READ_FAILED");
|
|
978
|
+
err.Set("win32code", Napi::Number::New(env, static_cast<double>(e.code().value())));
|
|
979
|
+
err.ThrowAsJavaScriptException();
|
|
980
|
+
return env.Null();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
auto throwAt = [&](const char* code, const std::string& msg) {
|
|
984
|
+
auto err = Napi::Error::New(env, msg);
|
|
985
|
+
err.Set("code", code);
|
|
986
|
+
err.ThrowAsJavaScriptException();
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
// Pass 1: decode - collect each instruction's decode state for relocation.
|
|
990
|
+
struct StolenInsn {
|
|
991
|
+
std::size_t off; // offset within buf
|
|
992
|
+
unsigned len;
|
|
993
|
+
std::uint32_t flags;
|
|
994
|
+
std::uint8_t opcode, opcode2;
|
|
995
|
+
bool ripRel; // RIP-relative ModRM operand (mod=00 rm=101)
|
|
996
|
+
std::size_t immOff; // offset of immediate field (valid if immSize>0)
|
|
997
|
+
unsigned immSize; // immediate field size in bytes
|
|
998
|
+
std::size_t dispOff; // offset of displacement field (valid if dispSize>0)
|
|
999
|
+
unsigned dispSize; // displacement field size in bytes
|
|
1000
|
+
};
|
|
1001
|
+
std::vector<StolenInsn> insns;
|
|
1002
|
+
std::size_t off = 0;
|
|
1003
|
+
while (off < minBytes && off < buf.size()) {
|
|
1004
|
+
std::uint8_t scratch[16] = {};
|
|
1005
|
+
const std::size_t avail = buf.size() - off;
|
|
1006
|
+
std::memcpy(scratch, buf.data() + off, avail < sizeof(scratch) ? avail : sizeof(scratch));
|
|
1007
|
+
hde64s hs{};
|
|
1008
|
+
unsigned int len = hde64_disasm(scratch, &hs);
|
|
1009
|
+
if (len == 0 || (hs.flags & F_ERROR)) {
|
|
1010
|
+
char msg[128];
|
|
1011
|
+
std::snprintf(msg, sizeof(msg),
|
|
1012
|
+
"hookSteal: undecodable instruction at +%zu - refusing to steal", off);
|
|
1013
|
+
throwAt("HOOK_STEAL", msg);
|
|
1014
|
+
return env.Null();
|
|
1015
|
+
}
|
|
1016
|
+
if (off + len > buf.size()) {
|
|
1017
|
+
char msg[128];
|
|
1018
|
+
std::snprintf(msg, sizeof(msg),
|
|
1019
|
+
"hookSteal: truncated instruction at +%zu - cannot cover %zu bytes", off, minBytes);
|
|
1020
|
+
throwAt("HOOK_STEAL", msg);
|
|
1021
|
+
return env.Null();
|
|
1022
|
+
}
|
|
1023
|
+
StolenInsn si{};
|
|
1024
|
+
si.off = off;
|
|
1025
|
+
si.len = len;
|
|
1026
|
+
si.flags = hs.flags;
|
|
1027
|
+
si.opcode = hs.opcode;
|
|
1028
|
+
si.opcode2 = hs.opcode2;
|
|
1029
|
+
si.ripRel = (hs.flags & F_MODRM) && hs.modrm_mod == 0 && hs.modrm_rm == 5;
|
|
1030
|
+
// Field sizes: the immediate/disp land at the tail of the instruction.
|
|
1031
|
+
// imm is the LAST field; disp precedes it - MinHook's offset math:
|
|
1032
|
+
// disp field = len - immSize - dispSize (same as len-((flags&0x3C)>>2)-4)
|
|
1033
|
+
si.immSize = (hs.flags & F_IMM64) ? 8 :
|
|
1034
|
+
(hs.flags & F_IMM32) ? 4 :
|
|
1035
|
+
(hs.flags & F_IMM16) ? 2 :
|
|
1036
|
+
(hs.flags & F_IMM8) ? 1 : 0;
|
|
1037
|
+
si.dispSize = (hs.flags & F_DISP32) ? 4 :
|
|
1038
|
+
(hs.flags & F_DISP16) ? 2 :
|
|
1039
|
+
(hs.flags & F_DISP8) ? 1 : 0;
|
|
1040
|
+
si.immOff = si.immSize ? off + len - si.immSize : 0;
|
|
1041
|
+
si.dispOff = si.dispSize ? off + len - si.immSize - si.dispSize : 0;
|
|
1042
|
+
insns.push_back(si);
|
|
1043
|
+
off += len;
|
|
1044
|
+
}
|
|
1045
|
+
if (off < minBytes) {
|
|
1046
|
+
char msg[128];
|
|
1047
|
+
std::snprintf(msg, sizeof(msg),
|
|
1048
|
+
"hookSteal: only %zu decodable bytes at 0x%llX - need >= %zu",
|
|
1049
|
+
off, static_cast<unsigned long long>(addr), minBytes);
|
|
1050
|
+
throwAt("HOOK_STEAL", msg);
|
|
1051
|
+
return env.Null();
|
|
1052
|
+
}
|
|
1053
|
+
const std::size_t stolenLen = off;
|
|
1054
|
+
|
|
1055
|
+
// Pass 2: relocate - rewrite position-relative operands in the copied
|
|
1056
|
+
// bytes. `bytes` becomes the relocated copy (what the caller writes to the
|
|
1057
|
+
// stub); `original` keeps the pristine bytes for unhook.
|
|
1058
|
+
std::vector<std::uint8_t> original(buf.begin(), buf.begin() + stolenLen);
|
|
1059
|
+
for (const StolenInsn& si : insns) {
|
|
1060
|
+
if (!(si.flags & F_RELATIVE) && !si.ripRel) continue;
|
|
1061
|
+
|
|
1062
|
+
const char* which = (si.flags & F_RELATIVE) ? "relative branch/call"
|
|
1063
|
+
: "RIP-relative operand";
|
|
1064
|
+
auto refuse = [&](const char* detail) {
|
|
1065
|
+
char msg[192];
|
|
1066
|
+
std::snprintf(msg, sizeof(msg),
|
|
1067
|
+
"hookSteal: stolen instruction at +%zu (opcode %02X%s) is %s: %s",
|
|
1068
|
+
si.off, si.opcode2 ? si.opcode2 : si.opcode,
|
|
1069
|
+
si.opcode2 ? " (0F map)" : "", which, detail);
|
|
1070
|
+
throwAt("HOOK_RELOCATE", msg);
|
|
1071
|
+
return env.Null();
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
if (stub == 0) {
|
|
1075
|
+
return refuse("pass stubAddr to enable relocation (copying it verbatim "
|
|
1076
|
+
"would produce a wild jump/read)");
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Immediate-carried relative branches/calls (E8/E9/EB, 70-7F, 0F8x, E0-E3,
|
|
1080
|
+
// xbegin C7 F8): the rel field IS the immediate.
|
|
1081
|
+
if (si.flags & F_RELATIVE) {
|
|
1082
|
+
if (si.immSize == 0) continue;
|
|
1083
|
+
// Old absolute branch target.
|
|
1084
|
+
std::int64_t oldRel = 0;
|
|
1085
|
+
std::memcpy(&oldRel, buf.data() + si.immOff, si.immSize);
|
|
1086
|
+
switch (si.immSize) { // sign-extend
|
|
1087
|
+
case 1: oldRel = static_cast<std::int8_t>(oldRel); break;
|
|
1088
|
+
case 2: oldRel = static_cast<std::int16_t>(oldRel); break;
|
|
1089
|
+
case 4: oldRel = static_cast<std::int32_t>(oldRel); break;
|
|
1090
|
+
}
|
|
1091
|
+
const std::uintptr_t oldTarget =
|
|
1092
|
+
static_cast<std::uintptr_t>(addr + si.off + si.len + oldRel);
|
|
1093
|
+
// Internal jump: target lands inside the stolen window. The copy
|
|
1094
|
+
// preserves the operand->target distance - leave the bytes verbatim.
|
|
1095
|
+
if (oldTarget >= addr && oldTarget < addr + stolenLen) continue;
|
|
1096
|
+
if (si.immSize != 4) {
|
|
1097
|
+
return refuse("rel8/rel16 external target cannot be widened in place "
|
|
1098
|
+
"(MinHook refuses LOOP/JECXZ-external too) - hook a "
|
|
1099
|
+
"different instruction-aligned site");
|
|
1100
|
+
}
|
|
1101
|
+
const std::int64_t newRel =
|
|
1102
|
+
static_cast<std::int64_t>(oldTarget) -
|
|
1103
|
+
(static_cast<std::int64_t>(stub) + si.off + si.len);
|
|
1104
|
+
if (newRel < INT32_MIN || newRel > INT32_MAX) {
|
|
1105
|
+
return refuse("rel32 rewrite lands out of +/-2GB range from the stub - "
|
|
1106
|
+
"allocate the stub nearer to the branch target");
|
|
1107
|
+
}
|
|
1108
|
+
const std::int32_t r32 = static_cast<std::int32_t>(newRel);
|
|
1109
|
+
std::memcpy(buf.data() + si.immOff, &r32, 4);
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// RIP-relative ModRM memory operand: the disp32 field is the position-
|
|
1114
|
+
// dependent part - rewrite it to reach the same absolute address.
|
|
1115
|
+
if (si.ripRel) {
|
|
1116
|
+
if (si.dispSize != 4) continue; // mod=00 rm=101 always disp32
|
|
1117
|
+
std::int32_t d32 = 0;
|
|
1118
|
+
std::memcpy(&d32, buf.data() + si.dispOff, 4);
|
|
1119
|
+
const std::uintptr_t oldTarget =
|
|
1120
|
+
static_cast<std::uintptr_t>(addr + si.off + si.len + d32);
|
|
1121
|
+
if (oldTarget >= addr && oldTarget < addr + stolenLen) continue; // internal
|
|
1122
|
+
const std::int64_t newDisp =
|
|
1123
|
+
static_cast<std::int64_t>(oldTarget) -
|
|
1124
|
+
(static_cast<std::int64_t>(stub) + si.off + si.len);
|
|
1125
|
+
if (newDisp < INT32_MIN || newDisp > INT32_MAX) {
|
|
1126
|
+
return refuse("disp32 rewrite lands out of +/-2GB range from the stub - "
|
|
1127
|
+
"allocate the stub nearer to the operand target");
|
|
1128
|
+
}
|
|
1129
|
+
const std::int32_t n32 = static_cast<std::int32_t>(newDisp);
|
|
1130
|
+
std::memcpy(buf.data() + si.dispOff, &n32, 4);
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
auto o = Napi::Object::New(env);
|
|
1136
|
+
o.Set("length", Napi::Number::New(env, static_cast<double>(stolenLen)));
|
|
1137
|
+
o.Set("bytes", Napi::Buffer<std::uint8_t>::Copy(env, buf.data(), stolenLen));
|
|
1138
|
+
o.Set("original",
|
|
1139
|
+
Napi::Buffer<std::uint8_t>::Copy(env, original.data(), stolenLen));
|
|
1140
|
+
return o;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// ---------- v2.1 additions ----------
|
|
1144
|
+
|
|
1145
|
+
// suspendProcess(pid) / resumeProcess(pid) via undocumented-but-stable
|
|
1146
|
+
// NtSuspendProcess / NtResumeProcess exported by ntdll. Atomic whole-process
|
|
1147
|
+
// suspend - strictly better than per-thread suspendAll (no TOCTOU where a
|
|
1148
|
+
// new thread spawns mid-enumeration).
|
|
1149
|
+
static Napi::Value NtSuspendResume(const Napi::CallbackInfo& info, bool suspend) {
|
|
1150
|
+
Napi::Env env = info.Env();
|
|
1151
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
1152
|
+
Napi::TypeError::New(env, "expected (pid)").ThrowAsJavaScriptException();
|
|
1153
|
+
return env.Null();
|
|
1154
|
+
}
|
|
1155
|
+
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
|
1156
|
+
// QA4-C3: suspending OUR OWN process freezes the JS engine - even though
|
|
1157
|
+
// NtSuspendProcess skips the calling thread, every other thread (including
|
|
1158
|
+
// any that would resume us) is suspended and the object can never reach
|
|
1159
|
+
// resumeProcess(). Suspend on self-pid is an unrecoverable deadlock; refuse
|
|
1160
|
+
// with a clear error. Resume on self-pid is harmless - it just clears
|
|
1161
|
+
// suspend counts on our own threads.
|
|
1162
|
+
if (suspend && pid == GetCurrentProcessId()) {
|
|
1163
|
+
auto err = Napi::Error::New(env,
|
|
1164
|
+
"refusing to suspend the calling process (self-suspend deadlock)");
|
|
1165
|
+
err.Set("code", "SELF_SUSPEND");
|
|
1166
|
+
err.ThrowAsJavaScriptException();
|
|
1167
|
+
return env.Null();
|
|
1168
|
+
}
|
|
1169
|
+
HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
|
|
1170
|
+
if (!ntdll) return Napi::Boolean::New(env, false);
|
|
1171
|
+
using NtProcFn = LONG (NTAPI*)(HANDLE);
|
|
1172
|
+
FARPROC fp = GetProcAddress(ntdll, suspend ? "NtSuspendProcess" : "NtResumeProcess");
|
|
1173
|
+
if (!fp) return Napi::Boolean::New(env, false);
|
|
1174
|
+
HANDLE h = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
|
|
1175
|
+
if (!h) return Napi::Boolean::New(env, false);
|
|
1176
|
+
LONG status = reinterpret_cast<NtProcFn>(fp)(h);
|
|
1177
|
+
CloseHandle(h);
|
|
1178
|
+
return Napi::Boolean::New(env, status == 0 /* STATUS_SUCCESS */);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
Napi::Value SuspendProcessSync(const Napi::CallbackInfo& info) { return NtSuspendResume(info, true); }
|
|
1182
|
+
Napi::Value ResumeProcessSync(const Napi::CallbackInfo& info) { return NtSuspendResume(info, false); }
|
|
1183
|
+
|
|
1184
|
+
// enableDebugPrivilege() -> boolean
|
|
1185
|
+
// Grants the CALLING process SeDebugPrivilege so it can open
|
|
1186
|
+
// protected/system processes. No target handle needed.
|
|
1187
|
+
Napi::Value EnableDebugPrivilegeSync(const Napi::CallbackInfo& info) {
|
|
1188
|
+
Napi::Env env = info.Env();
|
|
1189
|
+
HANDLE tok = nullptr;
|
|
1190
|
+
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &tok))
|
|
1191
|
+
return Napi::Boolean::New(env, false);
|
|
1192
|
+
LUID luid{};
|
|
1193
|
+
bool ok = LookupPrivilegeValueW(nullptr, L"SeDebugPrivilege", &luid) != 0;
|
|
1194
|
+
if (ok) {
|
|
1195
|
+
TOKEN_PRIVILEGES tp{};
|
|
1196
|
+
tp.PrivilegeCount = 1;
|
|
1197
|
+
tp.Privileges[0].Luid = luid;
|
|
1198
|
+
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
|
|
1199
|
+
ok = AdjustTokenPrivileges(tok, FALSE, &tp, sizeof(tp), nullptr, nullptr) != 0 &&
|
|
1200
|
+
GetLastError() == ERROR_SUCCESS;
|
|
1201
|
+
}
|
|
1202
|
+
CloseHandle(tok);
|
|
1203
|
+
return Napi::Boolean::New(env, ok);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// WaitForExitWorker: WaitForSingleObject on the (dup'd) process handle, then
|
|
1207
|
+
// GetExitCodeProcess. Resolve(exitCode) or resolve(null) on timeout.
|
|
1208
|
+
class WaitExitWorker : public Napi::AsyncWorker {
|
|
1209
|
+
public:
|
|
1210
|
+
WaitExitWorker(Napi::Env env, Napi::Promise::Deferred deferred, DWORD pid, DWORD timeoutMs)
|
|
1211
|
+
: Napi::AsyncWorker(env), deferred_(deferred), pid_(pid), timeout_(timeoutMs) {}
|
|
1212
|
+
~WaitExitWorker() override { if (h_) CloseHandle(h_); }
|
|
1213
|
+
|
|
1214
|
+
void Execute() override {
|
|
1215
|
+
h_ = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, pid_);
|
|
1216
|
+
if (!h_) { SetError("OpenProcess failed"); return; }
|
|
1217
|
+
DWORD wr = WaitForSingleObject(h_, timeout_);
|
|
1218
|
+
if (wr == WAIT_OBJECT_0) {
|
|
1219
|
+
DWORD code = 0;
|
|
1220
|
+
if (GetExitCodeProcess(h_, &code)) { exited_ = true; exitCode_ = code; }
|
|
1221
|
+
}
|
|
1222
|
+
// WAIT_TIMEOUT / WAIT_FAILED -> exited_ stays false -> resolve null
|
|
1223
|
+
}
|
|
1224
|
+
void OnOK() override {
|
|
1225
|
+
if (exited_) deferred_.Resolve(Napi::Number::New(Env(), static_cast<double>(exitCode_)));
|
|
1226
|
+
else deferred_.Resolve(Env().Null());
|
|
1227
|
+
}
|
|
1228
|
+
void OnError(const Napi::Error& e) override { deferred_.Reject(e.Value()); }
|
|
1229
|
+
private:
|
|
1230
|
+
Napi::Promise::Deferred deferred_;
|
|
1231
|
+
DWORD pid_, timeout_;
|
|
1232
|
+
HANDLE h_ = nullptr;
|
|
1233
|
+
bool exited_ = false;
|
|
1234
|
+
DWORD exitCode_ = 0;
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
// waitForExit(pid, timeoutMs) -> Promise<exitCode | null>
|
|
1238
|
+
Napi::Value WaitForExitAsync(const Napi::CallbackInfo& info) {
|
|
1239
|
+
Napi::Env env = info.Env();
|
|
1240
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
1241
|
+
Napi::TypeError::New(env, "expected (pid[, timeoutMs])").ThrowAsJavaScriptException();
|
|
1242
|
+
return env.Null();
|
|
1243
|
+
}
|
|
1244
|
+
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
|
1245
|
+
DWORD timeout = INFINITE;
|
|
1246
|
+
if (info.Length() > 1 && info[1].IsNumber()) {
|
|
1247
|
+
double t = info[1].As<Napi::Number>().DoubleValue();
|
|
1248
|
+
// QA4-H1: a negative timeout would wrap DWORD and become a ~49-day
|
|
1249
|
+
// blocking wait. Reject negatives/NaN; Infinity stays INFINITE by design.
|
|
1250
|
+
if (!std::isfinite(t)) {
|
|
1251
|
+
if (std::isinf(t) && t > 0) { timeout = INFINITE; }
|
|
1252
|
+
else {
|
|
1253
|
+
Napi::RangeError::New(env, "timeoutMs must be >= 0 (Infinity allowed)")
|
|
1254
|
+
.ThrowAsJavaScriptException();
|
|
1255
|
+
return env.Null();
|
|
1256
|
+
}
|
|
1257
|
+
} else if (t < 0) {
|
|
1258
|
+
Napi::RangeError::New(env, "timeoutMs must be >= 0 (Infinity allowed)")
|
|
1259
|
+
.ThrowAsJavaScriptException();
|
|
1260
|
+
return env.Null();
|
|
1261
|
+
} else if (t > 0xFFFFFFFE) timeout = INFINITE;
|
|
1262
|
+
else timeout = static_cast<DWORD>(t);
|
|
1263
|
+
}
|
|
1264
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
1265
|
+
auto promise = deferred.Promise();
|
|
1266
|
+
auto* w = new WaitExitWorker(env, std::move(deferred), pid, timeout);
|
|
1267
|
+
w->Queue();
|
|
1268
|
+
return promise;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// ---------- findWhatWrites (DebugActiveProcess + HW write breakpoints) ----------
|
|
1272
|
+
|
|
1273
|
+
// Hardware breakpoints only deliver their #DB exception to a debugger attached
|
|
1274
|
+
// to the TARGET - setting Dr regs without a debug loop just crashes the victim
|
|
1275
|
+
// with an unhandled EXCEPTION_SINGLE_STEP. So the CE "find what writes" flow
|
|
1276
|
+
// must own a real debugger session: DebugActiveProcess -> arm RW=01 data
|
|
1277
|
+
// breakpoints on every thread -> pump WaitForDebugEvent collecting
|
|
1278
|
+
// EXCEPTION_SINGLE_STEP hits -> restore Dr regs, ContinueDebugEvent, detach.
|
|
1279
|
+
//
|
|
1280
|
+
// NOTE on `rip`: a data breakpoint is a *trap* - it fires AFTER the write, so
|
|
1281
|
+
// the reported address is the instruction FOLLOWING the write, not the writer
|
|
1282
|
+
// itself. CE shows the same caveat; disassemble backwards to find the store.
|
|
1283
|
+
|
|
1284
|
+
#ifndef DBG_CONTINUE
|
|
1285
|
+
#define DBG_CONTINUE 0x00010002L
|
|
1286
|
+
#endif
|
|
1287
|
+
#ifndef DBG_EXCEPTION_NOT_HANDLED
|
|
1288
|
+
#define DBG_EXCEPTION_NOT_HANDLED 0x80010001L
|
|
1289
|
+
#endif
|
|
1290
|
+
|
|
1291
|
+
namespace {
|
|
1292
|
+
|
|
1293
|
+
struct FwwHit {
|
|
1294
|
+
std::uint64_t rip = 0;
|
|
1295
|
+
DWORD tid = 0;
|
|
1296
|
+
CONTEXT ctx{};
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
struct ArmedThread {
|
|
1300
|
+
DWORD tid = 0;
|
|
1301
|
+
int slot = -1;
|
|
1302
|
+
std::uint64_t origDr = 0;
|
|
1303
|
+
std::uint64_t origDr7 = 0;
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
// LEN encoding for Dr7: 00=1B, 01=2B, 11=4B, 10=8B (validated 1|2|4|8 by TS).
|
|
1307
|
+
static std::uint64_t LenBits(unsigned size) {
|
|
1308
|
+
switch (size) {
|
|
1309
|
+
case 1: return 0;
|
|
1310
|
+
case 2: return 1;
|
|
1311
|
+
case 8: return 2;
|
|
1312
|
+
default: return 3; // 4
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Arm a RW=01 (data write) breakpoint on the first free Dr slot of `hThread`.
|
|
1317
|
+
// `hThread` may be a handle from CREATE_THREAD/CREATE_PROCESS debug events or
|
|
1318
|
+
// OpenThread; it must allow GET/SET_CONTEXT and SUSPEND_RESUME. Suspend+resume
|
|
1319
|
+
// is balanced (net-zero suspend count) so threads already suspended by the
|
|
1320
|
+
// debug attach stay suspended and running threads stay running.
|
|
1321
|
+
// Returns slot (0-3) on success and fills origDr/origDr7 for restore; -1 on
|
|
1322
|
+
// failure.
|
|
1323
|
+
static int ArmWriteBp(HANDLE hThread, std::uint64_t addr, unsigned size,
|
|
1324
|
+
std::uint64_t& origDr, std::uint64_t& origDr7) {
|
|
1325
|
+
if (SuspendThread(hThread) == (DWORD)-1) return -1;
|
|
1326
|
+
int slot = -1;
|
|
1327
|
+
do {
|
|
1328
|
+
CONTEXT ctx{};
|
|
1329
|
+
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
|
1330
|
+
if (!GetThreadContext(hThread, &ctx)) break;
|
|
1331
|
+
std::uint64_t dr7 = ctx.Dr7;
|
|
1332
|
+
for (int s = 0; s < 4; s++) {
|
|
1333
|
+
if ((dr7 & (0x3ull << (s * 2))) == 0) { slot = s; break; } // L+G clear
|
|
1334
|
+
}
|
|
1335
|
+
if (slot < 0) break;
|
|
1336
|
+
origDr7 = dr7;
|
|
1337
|
+
origDr = (&ctx.Dr0)[slot];
|
|
1338
|
+
const std::uint64_t shift = 16ull + slot * 4;
|
|
1339
|
+
dr7 &= ~(0xfull << shift); // clear RW+LEN
|
|
1340
|
+
dr7 |= (0x1ull | (LenBits(size) << 2)) << shift; // RW=01 write, LEN=size
|
|
1341
|
+
dr7 |= 1ull << (slot * 2); // local enable L{slot}
|
|
1342
|
+
ctx.Dr7 = dr7;
|
|
1343
|
+
(&ctx.Dr0)[slot] = addr;
|
|
1344
|
+
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
|
1345
|
+
if (!SetThreadContext(hThread, &ctx)) { slot = -1; break; }
|
|
1346
|
+
} while (0);
|
|
1347
|
+
ResumeThread(hThread);
|
|
1348
|
+
return slot;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
static void DisarmBp(HANDLE hThread, int slot, std::uint64_t origDr,
|
|
1352
|
+
std::uint64_t origDr7) {
|
|
1353
|
+
if (SuspendThread(hThread) == (DWORD)-1) return;
|
|
1354
|
+
CONTEXT ctx{};
|
|
1355
|
+
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
|
1356
|
+
if (GetThreadContext(hThread, &ctx)) {
|
|
1357
|
+
ctx.Dr7 = origDr7;
|
|
1358
|
+
(&ctx.Dr0)[slot] = origDr;
|
|
1359
|
+
ctx.Dr6 = 0;
|
|
1360
|
+
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
|
1361
|
+
SetThreadContext(hThread, &ctx);
|
|
1362
|
+
}
|
|
1363
|
+
ResumeThread(hThread);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
class FindWhatWritesWorker : public Napi::AsyncWorker {
|
|
1367
|
+
public:
|
|
1368
|
+
FindWhatWritesWorker(Napi::Env env, Napi::Promise::Deferred deferred,
|
|
1369
|
+
DWORD pid, std::uint64_t addr, unsigned size,
|
|
1370
|
+
DWORD timeoutMs, unsigned maxHits)
|
|
1371
|
+
: Napi::AsyncWorker(env), deferred_(deferred), pid_(pid), addr_(addr),
|
|
1372
|
+
size_(size), timeoutMs_(timeoutMs), maxHits_(maxHits) {}
|
|
1373
|
+
|
|
1374
|
+
~FindWhatWritesWorker() override {
|
|
1375
|
+
for (HANDLE h : openHandles_) CloseHandle(h);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// Attach is best-effort: callers may need enableDebugPrivilege() first for
|
|
1379
|
+
// protected targets. DebugSetProcessKillOnExit(false) keeps the debuggee
|
|
1380
|
+
// alive when our process exits while still attached.
|
|
1381
|
+
void Execute() override {
|
|
1382
|
+
// Detach completion is asynchronous on Windows - a fresh
|
|
1383
|
+
// DebugActiveProcess immediately after DebugActiveProcessStop can return
|
|
1384
|
+
// ERROR_ACCESS_DENIED while the kernel tears down the debug port. Retry
|
|
1385
|
+
// for up to ~500ms so back-to-back findWhatWrites calls work.
|
|
1386
|
+
DWORD err = 0;
|
|
1387
|
+
bool ok = false;
|
|
1388
|
+
for (int i = 0; i < 50 && !ok; i++) {
|
|
1389
|
+
ok = DebugActiveProcess(pid_) != 0;
|
|
1390
|
+
if (!ok) { err = GetLastError(); Sleep(10); }
|
|
1391
|
+
}
|
|
1392
|
+
if (!ok) {
|
|
1393
|
+
SetError("DebugActiveProcess failed (win32 " + std::to_string(err) + ")");
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
attached_ = true;
|
|
1397
|
+
DebugSetProcessKillOnExit(FALSE);
|
|
1398
|
+
|
|
1399
|
+
// QA4-M3 (lifecycle): wrap all post-attach phases so ANY C++ exception
|
|
1400
|
+
// (bad_alloc under a flooding target, length_error in hits_/armed_
|
|
1401
|
+
// growth) still runs disarm+drain+Stop. Without this the debuggee is
|
|
1402
|
+
// left attached with armed Dr7 breakpoints and suspended threads - the
|
|
1403
|
+
// next write to the watched addr would fire an unhandled #DB and kill it.
|
|
1404
|
+
try {
|
|
1405
|
+
RunPhases();
|
|
1406
|
+
} catch (const std::exception& e) {
|
|
1407
|
+
CleanupDebuggee();
|
|
1408
|
+
SetError(std::string("findWhatWrites failed: ") + e.what());
|
|
1409
|
+
return;
|
|
1410
|
+
} catch (...) {
|
|
1411
|
+
CleanupDebuggee();
|
|
1412
|
+
SetError("findWhatWrites failed: unknown exception");
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
CleanupDebuggee();
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
void RunPhases() {
|
|
1419
|
+
const ULONGLONG armDeadline = GetTickCount64() + 500;
|
|
1420
|
+
const ULONGLONG deadline = GetTickCount64() + timeoutMs_;
|
|
1421
|
+
|
|
1422
|
+
// Phase 1: collect the attach-time event burst (CREATE_PROCESS,
|
|
1423
|
+
// LOAD_DLLs, CREATE_THREAD per existing thread, initial breakpoint) and
|
|
1424
|
+
// arm each reported thread. Threads reported by CREATE_THREAD are already
|
|
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 - a thread
|
|
1427
|
+
// armed+continued early in this loop can fire while we drain the rest.
|
|
1428
|
+
int quietWaits = 0;
|
|
1429
|
+
while (quietWaits < 3 && GetTickCount64() < armDeadline &&
|
|
1430
|
+
hits_.size() < maxHits_ && !exited_) {
|
|
1431
|
+
DEBUG_EVENT ev{};
|
|
1432
|
+
if (!WaitForDebugEvent(&ev, 20)) { quietWaits++; continue; }
|
|
1433
|
+
quietWaits = 0;
|
|
1434
|
+
ContinueDebugEvent(ev.dwProcessId, ev.dwThreadId, HandleEvent(ev));
|
|
1435
|
+
}
|
|
1436
|
+
// Fallback: arm anything the event burst missed (rare - attach reports
|
|
1437
|
+
// every thread, but belt-and-suspenders for filtered/racy cases).
|
|
1438
|
+
ArmSnapshotThreads();
|
|
1439
|
+
|
|
1440
|
+
// Phase 2: collect hits until timeout / maxHits / process exit.
|
|
1441
|
+
while (!exited_ && hits_.size() < maxHits_ && GetTickCount64() < deadline) {
|
|
1442
|
+
const ULONGLONG left = deadline - GetTickCount64();
|
|
1443
|
+
DEBUG_EVENT ev{};
|
|
1444
|
+
if (!WaitForDebugEvent(&ev, left > 25 ? 25 : (DWORD)left)) continue;
|
|
1445
|
+
ContinueDebugEvent(ev.dwProcessId, ev.dwThreadId, HandleEvent(ev));
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Phase 3: disarm every armed thread (restoring ORIGINAL Dr regs - the
|
|
1450
|
+
// target may legitimately use its own hw breakpoints), drain pending
|
|
1451
|
+
// events, disarm any stragglers hit mid-disarm, drain again, detach.
|
|
1452
|
+
// Everything runs on this thread - DebugActiveProcess/Continue/DebugStop
|
|
1453
|
+
// are debugger-thread-bound. Idempotent: runs only while attached_ and is
|
|
1454
|
+
// invoked from every Execute() exit path (normal + exception).
|
|
1455
|
+
void CleanupDebuggee() {
|
|
1456
|
+
if (!attached_) return;
|
|
1457
|
+
auto disarmAll = [this]() {
|
|
1458
|
+
for (auto& at : armed_) {
|
|
1459
|
+
HANDLE t = OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT |
|
|
1460
|
+
THREAD_SUSPEND_RESUME,
|
|
1461
|
+
FALSE, at.tid);
|
|
1462
|
+
if (!t) continue;
|
|
1463
|
+
DisarmBp(t, at.slot, at.origDr, at.origDr7);
|
|
1464
|
+
CloseHandle(t);
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
auto drain = [this]() {
|
|
1468
|
+
DEBUG_EVENT ev{};
|
|
1469
|
+
for (int i = 0; i < 64 && WaitForDebugEvent(&ev, 2); i++) {
|
|
1470
|
+
if (ev.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT &&
|
|
1471
|
+
ev.u.CreateProcessInfo.hFile)
|
|
1472
|
+
CloseHandle(ev.u.CreateProcessInfo.hFile);
|
|
1473
|
+
if (ev.dwDebugEventCode == LOAD_DLL_DEBUG_EVENT && ev.u.LoadDll.hFile)
|
|
1474
|
+
CloseHandle(ev.u.LoadDll.hFile);
|
|
1475
|
+
// A queued single-step is our bp firing mid-disarm - swallow it
|
|
1476
|
+
// (DBG_CONTINUE). A queued genuine exception gets NOT_HANDLED so it
|
|
1477
|
+
// reaches the debuggee's SEH as it would without us.
|
|
1478
|
+
DWORD disp = DBG_CONTINUE;
|
|
1479
|
+
if (ev.dwDebugEventCode == EXCEPTION_DEBUG_EVENT &&
|
|
1480
|
+
ev.u.Exception.ExceptionRecord.ExceptionCode !=
|
|
1481
|
+
EXCEPTION_SINGLE_STEP &&
|
|
1482
|
+
ev.u.Exception.ExceptionRecord.ExceptionCode !=
|
|
1483
|
+
EXCEPTION_BREAKPOINT)
|
|
1484
|
+
disp = DBG_EXCEPTION_NOT_HANDLED;
|
|
1485
|
+
ContinueDebugEvent(ev.dwProcessId, ev.dwThreadId, disp);
|
|
1486
|
+
if (ev.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT) break;
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
disarmAll();
|
|
1490
|
+
drain(); // flush pending bp hits + resume stopped threads
|
|
1491
|
+
disarmAll(); // threads stopped on a queued event during pass 1 skipped
|
|
1492
|
+
// SetThreadContext - disarm them now that they're resumed
|
|
1493
|
+
drain(); // release the last stopped threads, empty the port
|
|
1494
|
+
DebugActiveProcessStop(pid_);
|
|
1495
|
+
attached_ = false;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
void OnOK() override {
|
|
1499
|
+
Napi::Env env = Env();
|
|
1500
|
+
auto arr = Napi::Array::New(env, hits_.size());
|
|
1501
|
+
for (size_t i = 0; i < hits_.size(); i++) {
|
|
1502
|
+
auto o = Napi::Object::New(env);
|
|
1503
|
+
const FwwHit& h = hits_[i];
|
|
1504
|
+
o.Set("rip", Napi::BigInt::New(env, h.rip));
|
|
1505
|
+
o.Set("tid", Napi::Number::New(env, static_cast<double>(h.tid)));
|
|
1506
|
+
auto c = Napi::Object::New(env);
|
|
1507
|
+
c.Set("rip", Napi::BigInt::New(env, static_cast<std::uint64_t>(h.ctx.Rip)));
|
|
1508
|
+
c.Set("rsp", Napi::BigInt::New(env, static_cast<std::uint64_t>(h.ctx.Rsp)));
|
|
1509
|
+
c.Set("rbp", Napi::BigInt::New(env, static_cast<std::uint64_t>(h.ctx.Rbp)));
|
|
1510
|
+
c.Set("dr0", Napi::BigInt::New(env, static_cast<std::uint64_t>(h.ctx.Dr0)));
|
|
1511
|
+
c.Set("dr1", Napi::BigInt::New(env, static_cast<std::uint64_t>(h.ctx.Dr1)));
|
|
1512
|
+
c.Set("dr2", Napi::BigInt::New(env, static_cast<std::uint64_t>(h.ctx.Dr2)));
|
|
1513
|
+
c.Set("dr3", Napi::BigInt::New(env, static_cast<std::uint64_t>(h.ctx.Dr3)));
|
|
1514
|
+
c.Set("dr6", Napi::BigInt::New(env, static_cast<std::uint64_t>(h.ctx.Dr6)));
|
|
1515
|
+
c.Set("dr7", Napi::BigInt::New(env, static_cast<std::uint64_t>(h.ctx.Dr7)));
|
|
1516
|
+
c.Set("eflags", Napi::BigInt::New(env, static_cast<std::uint64_t>(h.ctx.EFlags)));
|
|
1517
|
+
o.Set("threadContext", c);
|
|
1518
|
+
arr.Set(i, o);
|
|
1519
|
+
}
|
|
1520
|
+
deferred_.Resolve(arr);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
void OnError(const Napi::Error& e) override {
|
|
1524
|
+
deferred_.Reject(e.Value());
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
private:
|
|
1528
|
+
// Returns the ContinueDebugEvent disposition for `ev`; also feeds side
|
|
1529
|
+
// effects (arming, hit collection, handle tracking).
|
|
1530
|
+
DWORD HandleEvent(const DEBUG_EVENT& ev) {
|
|
1531
|
+
switch (ev.dwDebugEventCode) {
|
|
1532
|
+
case EXCEPTION_DEBUG_EVENT: {
|
|
1533
|
+
const auto& er = ev.u.Exception.ExceptionRecord;
|
|
1534
|
+
if (er.ExceptionCode == EXCEPTION_SINGLE_STEP) {
|
|
1535
|
+
return HandleSingleStep(ev);
|
|
1536
|
+
}
|
|
1537
|
+
// The attach-time int3 (and any target int3) is debugger-handled.
|
|
1538
|
+
if (er.ExceptionCode == EXCEPTION_BREAKPOINT) return DBG_CONTINUE;
|
|
1539
|
+
// Everything else belongs to the debuggee's SEH chain.
|
|
1540
|
+
return DBG_EXCEPTION_NOT_HANDLED;
|
|
1541
|
+
}
|
|
1542
|
+
case CREATE_THREAD_DEBUG_EVENT: {
|
|
1543
|
+
const DWORD tid = ev.dwThreadId;
|
|
1544
|
+
// QA4-m4: hThread here is system-owned (unlike CREATE_PROCESS's hFile
|
|
1545
|
+
// which the debugger must close). Microsoft's debug samples and docs
|
|
1546
|
+
// never close it - the system cleans it up on EXIT_THREAD. Verified:
|
|
1547
|
+
// do NOT close; closing could later collide with a recycled handle
|
|
1548
|
+
// value the debugger also sees. Disarm uses OpenThread-by-tid anyway.
|
|
1549
|
+
HANDLE t = ev.u.CreateThread.hThread;
|
|
1550
|
+
ArmOnHandle(tid, t);
|
|
1551
|
+
return DBG_CONTINUE;
|
|
1552
|
+
}
|
|
1553
|
+
case CREATE_PROCESS_DEBUG_EVENT: {
|
|
1554
|
+
if (ev.u.CreateProcessInfo.hFile) CloseHandle(ev.u.CreateProcessInfo.hFile);
|
|
1555
|
+
ArmOnHandle(ev.dwThreadId, ev.u.CreateProcessInfo.hThread);
|
|
1556
|
+
return DBG_CONTINUE;
|
|
1557
|
+
}
|
|
1558
|
+
case LOAD_DLL_DEBUG_EVENT: {
|
|
1559
|
+
if (ev.u.LoadDll.hFile) CloseHandle(ev.u.LoadDll.hFile);
|
|
1560
|
+
return DBG_CONTINUE;
|
|
1561
|
+
}
|
|
1562
|
+
case EXIT_THREAD_DEBUG_EVENT:
|
|
1563
|
+
// If it was armed there's nothing to restore - the thread is gone.
|
|
1564
|
+
return DBG_CONTINUE;
|
|
1565
|
+
case EXIT_PROCESS_DEBUG_EVENT:
|
|
1566
|
+
exited_ = true;
|
|
1567
|
+
return DBG_CONTINUE;
|
|
1568
|
+
case RIP_EVENT:
|
|
1569
|
+
case OUTPUT_DEBUG_STRING_EVENT:
|
|
1570
|
+
case UNLOAD_DLL_DEBUG_EVENT:
|
|
1571
|
+
default:
|
|
1572
|
+
return DBG_CONTINUE;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
DWORD HandleSingleStep(const DEBUG_EVENT& ev) {
|
|
1577
|
+
const DWORD tid = ev.dwThreadId;
|
|
1578
|
+
HANDLE t = OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, FALSE, tid);
|
|
1579
|
+
if (!t) return DBG_EXCEPTION_NOT_HANDLED;
|
|
1580
|
+
CONTEXT ctx{};
|
|
1581
|
+
ctx.ContextFlags = CONTEXT_ALL | CONTEXT_DEBUG_REGISTERS;
|
|
1582
|
+
bool ours = false;
|
|
1583
|
+
if (GetThreadContext(t, &ctx)) {
|
|
1584
|
+
int slot = -1;
|
|
1585
|
+
for (const auto& at : armed_)
|
|
1586
|
+
if (at.tid == tid) { slot = at.slot; break; }
|
|
1587
|
+
if (slot >= 0 && (ctx.Dr6 & (1ull << slot))) ours = true;
|
|
1588
|
+
if (ours) {
|
|
1589
|
+
FwwHit h;
|
|
1590
|
+
h.rip = static_cast<std::uint64_t>(
|
|
1591
|
+
reinterpret_cast<std::uintptr_t>(
|
|
1592
|
+
ev.u.Exception.ExceptionRecord.ExceptionAddress));
|
|
1593
|
+
h.tid = tid;
|
|
1594
|
+
h.ctx = ctx;
|
|
1595
|
+
hits_.push_back(h);
|
|
1596
|
+
ctx.Dr6 = 0; // acknowledge so a stale B bit can't false-trigger
|
|
1597
|
+
SetThreadContext(t, &ctx);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
CloseHandle(t);
|
|
1601
|
+
// Ours -> swallowed; foreign single-steps (e.g. target's own TF use) go
|
|
1602
|
+
// back unhandled so we don't silently swallow them.
|
|
1603
|
+
return ours ? DBG_CONTINUE : DBG_EXCEPTION_NOT_HANDLED;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
void ArmOnHandle(DWORD tid, HANDLE hThread) {
|
|
1607
|
+
if (!hThread) return;
|
|
1608
|
+
for (const auto& at : armed_)
|
|
1609
|
+
if (at.tid == tid) return; // already armed
|
|
1610
|
+
ArmedThread at;
|
|
1611
|
+
at.tid = tid;
|
|
1612
|
+
at.slot = ArmWriteBp(hThread, addr_, size_, at.origDr, at.origDr7);
|
|
1613
|
+
if (at.slot >= 0) armed_.push_back(at);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// Arm threads the event burst didn't report (Toolhelp fallback).
|
|
1617
|
+
void ArmSnapshotThreads() {
|
|
1618
|
+
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
|
|
1619
|
+
if (snap == INVALID_HANDLE_VALUE) return;
|
|
1620
|
+
THREADENTRY32 te{};
|
|
1621
|
+
te.dwSize = sizeof(te);
|
|
1622
|
+
if (Thread32First(snap, &te)) {
|
|
1623
|
+
do {
|
|
1624
|
+
if (te.th32OwnerProcessID != pid_) continue;
|
|
1625
|
+
bool armed = false;
|
|
1626
|
+
for (const auto& at : armed_)
|
|
1627
|
+
if (at.tid == te.th32ThreadID) { armed = true; break; }
|
|
1628
|
+
if (armed) continue;
|
|
1629
|
+
HANDLE t = OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT |
|
|
1630
|
+
THREAD_SUSPEND_RESUME,
|
|
1631
|
+
FALSE, te.th32ThreadID);
|
|
1632
|
+
if (!t) continue;
|
|
1633
|
+
ArmedThread at;
|
|
1634
|
+
at.tid = te.th32ThreadID;
|
|
1635
|
+
at.slot = ArmWriteBp(t, addr_, size_, at.origDr, at.origDr7);
|
|
1636
|
+
if (at.slot >= 0) { armed_.push_back(at); openHandles_.push_back(t); }
|
|
1637
|
+
else CloseHandle(t);
|
|
1638
|
+
} while (Thread32Next(snap, &te));
|
|
1639
|
+
}
|
|
1640
|
+
CloseHandle(snap);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
Napi::Promise::Deferred deferred_;
|
|
1644
|
+
DWORD pid_;
|
|
1645
|
+
std::uint64_t addr_;
|
|
1646
|
+
unsigned size_;
|
|
1647
|
+
DWORD timeoutMs_;
|
|
1648
|
+
unsigned maxHits_;
|
|
1649
|
+
bool attached_ = false;
|
|
1650
|
+
bool exited_ = false;
|
|
1651
|
+
std::vector<ArmedThread> armed_;
|
|
1652
|
+
std::vector<FwwHit> hits_;
|
|
1653
|
+
// Toolhelp-armed threads keep their handle so phase 3 could disarm via the
|
|
1654
|
+
// SAME handle if needed; closed in dtor.
|
|
1655
|
+
std::vector<HANDLE> openHandles_;
|
|
1656
|
+
};
|
|
1657
|
+
|
|
1658
|
+
} // anonymous namespace
|
|
1659
|
+
|
|
1660
|
+
// findWhatWrites(pid, address, size, timeoutMs, maxHits) ->
|
|
1661
|
+
// Promise<{rip, tid, threadContext}[]>
|
|
1662
|
+
Napi::Value FindWhatWritesAsync(const Napi::CallbackInfo& info) {
|
|
1663
|
+
Napi::Env env = info.Env();
|
|
1664
|
+
if (info.Length() < 2 || !info[0].IsNumber()) {
|
|
1665
|
+
Napi::TypeError::New(env, "expected (pid, address[, size, timeoutMs, maxHits])")
|
|
1666
|
+
.ThrowAsJavaScriptException();
|
|
1667
|
+
return env.Null();
|
|
1668
|
+
}
|
|
1669
|
+
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
|
1670
|
+
std::uint64_t addr = 0;
|
|
1671
|
+
if (!JsToU64(info[1], addr)) {
|
|
1672
|
+
Napi::TypeError::New(env, "address must be number|bigint").ThrowAsJavaScriptException();
|
|
1673
|
+
return env.Null();
|
|
1674
|
+
}
|
|
1675
|
+
unsigned size = 4;
|
|
1676
|
+
if (info.Length() > 2 && info[2].IsNumber())
|
|
1677
|
+
size = info[2].As<Napi::Number>().Uint32Value();
|
|
1678
|
+
if (size != 1 && size != 2 && size != 4 && size != 8) {
|
|
1679
|
+
Napi::RangeError::New(env, "size must be 1|2|4|8").ThrowAsJavaScriptException();
|
|
1680
|
+
return env.Null();
|
|
1681
|
+
}
|
|
1682
|
+
DWORD timeout = 1000;
|
|
1683
|
+
if (info.Length() > 3 && info[3].IsNumber()) {
|
|
1684
|
+
double t = info[3].As<Napi::Number>().DoubleValue();
|
|
1685
|
+
if (std::isfinite(t) && t > 0 && t <= 60000) timeout = static_cast<DWORD>(t);
|
|
1686
|
+
}
|
|
1687
|
+
unsigned maxHits = 32;
|
|
1688
|
+
if (info.Length() > 4 && info[4].IsNumber()) {
|
|
1689
|
+
double m = info[4].As<Napi::Number>().DoubleValue();
|
|
1690
|
+
if (std::isfinite(m) && m >= 1) maxHits = m > 1024 ? 1024u : static_cast<unsigned>(m);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// QA4-H7: CONTEXT/Dr registers use the x64 struct - on a WoW64 (32-bit)
|
|
1694
|
+
// target Get/SetThreadContext operate on WOW64_CONTEXT and the arm would
|
|
1695
|
+
// silently corrupt the debuggee or never fire. Refuse up front (same
|
|
1696
|
+
// pattern as injectDll).
|
|
1697
|
+
{
|
|
1698
|
+
HANDLE ph = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
|
1699
|
+
if (ph) {
|
|
1700
|
+
BOOL wow = FALSE;
|
|
1701
|
+
IsWow64Process(ph, &wow);
|
|
1702
|
+
CloseHandle(ph);
|
|
1703
|
+
if (wow) {
|
|
1704
|
+
Napi::Error::New(env,
|
|
1705
|
+
"findWhatWrites does not support WoW64 (32-bit) targets")
|
|
1706
|
+
.ThrowAsJavaScriptException();
|
|
1707
|
+
return env.Null();
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
1713
|
+
auto promise = deferred.Promise();
|
|
1714
|
+
auto* w = new FindWhatWritesWorker(env, std::move(deferred), pid, addr, size,
|
|
1715
|
+
timeout, maxHits);
|
|
1716
|
+
w->Queue();
|
|
1717
|
+
return promise;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
} // namespace mp
|