@bingine/cemem 0.0.0-stage → 0.1.1

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.
Files changed (138) hide show
  1. package/PORT-SCOPE.md +169 -0
  2. package/README.md +451 -2
  3. package/dist/cjs/addr/expression.js +650 -0
  4. package/dist/cjs/addr/expression.js.map +1 -0
  5. package/dist/cjs/addresslist/memory-record.js +612 -0
  6. package/dist/cjs/addresslist/memory-record.js.map +1 -0
  7. package/dist/cjs/asm/assembler.js +354 -0
  8. package/dist/cjs/asm/assembler.js.map +1 -0
  9. package/dist/cjs/asm/autoassembler-directives.js +362 -0
  10. package/dist/cjs/asm/autoassembler-directives.js.map +1 -0
  11. package/dist/cjs/asm/autoassembler.js +2163 -0
  12. package/dist/cjs/asm/autoassembler.js.map +1 -0
  13. package/dist/cjs/asm/backend.js +394 -0
  14. package/dist/cjs/asm/backend.js.map +1 -0
  15. package/dist/cjs/asm/disassembler.js +715 -0
  16. package/dist/cjs/asm/disassembler.js.map +1 -0
  17. package/dist/cjs/backend/dbk-backend.js +683 -0
  18. package/dist/cjs/backend/dbk-backend.js.map +1 -0
  19. package/dist/cjs/backend/native-ce-backend.js +667 -0
  20. package/dist/cjs/backend/native-ce-backend.js.map +1 -0
  21. package/dist/cjs/backend/self-backend.js +449 -0
  22. package/dist/cjs/backend/self-backend.js.map +1 -0
  23. package/dist/cjs/backend/types.js +48 -0
  24. package/dist/cjs/backend/types.js.map +1 -0
  25. package/dist/cjs/cheattable/ct-format.js +533 -0
  26. package/dist/cjs/cheattable/ct-format.js.map +1 -0
  27. package/dist/cjs/index.js +219 -0
  28. package/dist/cjs/index.js.map +1 -0
  29. package/dist/cjs/lua/aa.js +230 -0
  30. package/dist/cjs/lua/aa.js.map +1 -0
  31. package/dist/cjs/lua/addressing.js +118 -0
  32. package/dist/cjs/lua/addressing.js.map +1 -0
  33. package/dist/cjs/lua/api.js +736 -0
  34. package/dist/cjs/lua/api.js.map +1 -0
  35. package/dist/cjs/lua/asm.js +220 -0
  36. package/dist/cjs/lua/asm.js.map +1 -0
  37. package/dist/cjs/lua/bridge.js +526 -0
  38. package/dist/cjs/lua/bridge.js.map +1 -0
  39. package/dist/cjs/lua/marshal.js +562 -0
  40. package/dist/cjs/lua/marshal.js.map +1 -0
  41. package/dist/cjs/lua/memory.js +213 -0
  42. package/dist/cjs/lua/memory.js.map +1 -0
  43. package/dist/cjs/lua/objects.js +1070 -0
  44. package/dist/cjs/lua/objects.js.map +1 -0
  45. package/dist/cjs/lua/scan.js +543 -0
  46. package/dist/cjs/lua/scan.js.map +1 -0
  47. package/dist/cjs/lua/types.js +103 -0
  48. package/dist/cjs/lua/types.js.map +1 -0
  49. package/dist/cjs/scanner/matcher.js +781 -0
  50. package/dist/cjs/scanner/matcher.js.map +1 -0
  51. package/dist/cjs/scanner/pointer-scan.js +546 -0
  52. package/dist/cjs/scanner/pointer-scan.js.map +1 -0
  53. package/dist/cjs/scanner/scanner.js +513 -0
  54. package/dist/cjs/scanner/scanner.js.map +1 -0
  55. package/dist/cjs/scanner/shared-layout.js +207 -0
  56. package/dist/cjs/scanner/shared-layout.js.map +1 -0
  57. package/dist/cjs/types.js +190 -0
  58. package/dist/cjs/types.js.map +1 -0
  59. package/dist/cjs/value-codec.js +515 -0
  60. package/dist/cjs/value-codec.js.map +1 -0
  61. package/dist/cjs/win/process.js +475 -0
  62. package/dist/cjs/win/process.js.map +1 -0
  63. package/dist/cjs/win/scan-worker.js +84 -0
  64. package/dist/cjs/win/scan-worker.js.map +1 -0
  65. package/dist/cjs/win/winapi.js +403 -0
  66. package/dist/cjs/win/winapi.js.map +1 -0
  67. package/dist/types/addr/expression.d.ts +98 -0
  68. package/dist/types/addr/expression.d.ts.map +1 -0
  69. package/dist/types/addresslist/memory-record.d.ts +218 -0
  70. package/dist/types/addresslist/memory-record.d.ts.map +1 -0
  71. package/dist/types/asm/assembler.d.ts +106 -0
  72. package/dist/types/asm/assembler.d.ts.map +1 -0
  73. package/dist/types/asm/autoassembler-directives.d.ts +118 -0
  74. package/dist/types/asm/autoassembler-directives.d.ts.map +1 -0
  75. package/dist/types/asm/autoassembler.d.ts +434 -0
  76. package/dist/types/asm/autoassembler.d.ts.map +1 -0
  77. package/dist/types/asm/backend.d.ts +167 -0
  78. package/dist/types/asm/backend.d.ts.map +1 -0
  79. package/dist/types/asm/disassembler.d.ts +194 -0
  80. package/dist/types/asm/disassembler.d.ts.map +1 -0
  81. package/dist/types/backend/dbk-backend.d.ts +353 -0
  82. package/dist/types/backend/dbk-backend.d.ts.map +1 -0
  83. package/dist/types/backend/native-ce-backend.d.ts +213 -0
  84. package/dist/types/backend/native-ce-backend.d.ts.map +1 -0
  85. package/dist/types/backend/self-backend.d.ts +64 -0
  86. package/dist/types/backend/self-backend.d.ts.map +1 -0
  87. package/dist/types/backend/types.d.ts +176 -0
  88. package/dist/types/backend/types.d.ts.map +1 -0
  89. package/dist/types/cheattable/ct-format.d.ts +100 -0
  90. package/dist/types/cheattable/ct-format.d.ts.map +1 -0
  91. package/dist/types/index.d.ts +54 -0
  92. package/dist/types/index.d.ts.map +1 -0
  93. package/dist/types/lua/aa.d.ts +107 -0
  94. package/dist/types/lua/aa.d.ts.map +1 -0
  95. package/dist/types/lua/addressing.d.ts +41 -0
  96. package/dist/types/lua/addressing.d.ts.map +1 -0
  97. package/dist/types/lua/api.d.ts +46 -0
  98. package/dist/types/lua/api.d.ts.map +1 -0
  99. package/dist/types/lua/asm.d.ts +42 -0
  100. package/dist/types/lua/asm.d.ts.map +1 -0
  101. package/dist/types/lua/bridge.d.ts +129 -0
  102. package/dist/types/lua/bridge.d.ts.map +1 -0
  103. package/dist/types/lua/marshal.d.ts +244 -0
  104. package/dist/types/lua/marshal.d.ts.map +1 -0
  105. package/dist/types/lua/memory.d.ts +61 -0
  106. package/dist/types/lua/memory.d.ts.map +1 -0
  107. package/dist/types/lua/objects.d.ts +210 -0
  108. package/dist/types/lua/objects.d.ts.map +1 -0
  109. package/dist/types/lua/scan.d.ts +106 -0
  110. package/dist/types/lua/scan.d.ts.map +1 -0
  111. package/dist/types/lua/types.d.ts +146 -0
  112. package/dist/types/lua/types.d.ts.map +1 -0
  113. package/dist/types/scanner/matcher.d.ts +122 -0
  114. package/dist/types/scanner/matcher.d.ts.map +1 -0
  115. package/dist/types/scanner/pointer-scan.d.ts +187 -0
  116. package/dist/types/scanner/pointer-scan.d.ts.map +1 -0
  117. package/dist/types/scanner/scanner.d.ts +150 -0
  118. package/dist/types/scanner/scanner.d.ts.map +1 -0
  119. package/dist/types/scanner/shared-layout.d.ts +96 -0
  120. package/dist/types/scanner/shared-layout.d.ts.map +1 -0
  121. package/dist/types/types.d.ts +239 -0
  122. package/dist/types/types.d.ts.map +1 -0
  123. package/dist/types/value-codec.d.ts +134 -0
  124. package/dist/types/value-codec.d.ts.map +1 -0
  125. package/dist/types/win/process.d.ts +190 -0
  126. package/dist/types/win/process.d.ts.map +1 -0
  127. package/dist/types/win/scan-worker.d.ts +51 -0
  128. package/dist/types/win/scan-worker.d.ts.map +1 -0
  129. package/dist/types/win/winapi.d.ts +144 -0
  130. package/dist/types/win/winapi.d.ts.map +1 -0
  131. package/docs/assembler.md +371 -0
  132. package/docs/autoassembler.md +423 -0
  133. package/docs/backend-dbk.md +305 -0
  134. package/docs/backends.md +273 -0
  135. package/docs/evidence/atomics-wait-io.js +20 -0
  136. package/docs/evidence/mini-repro.js +98 -0
  137. package/docs/lua-api.md +718 -0
  138. package/package.json +60 -4
@@ -0,0 +1,305 @@
1
+ # DBK 内核驱动后端
2
+
3
+ 让 `node-cheat-engine` 通过 Cheat Engine 自己的 **DBK 内核驱动**读写受保护进程内存。
4
+
5
+ ## 一、为什么需要它
6
+
7
+ | 后端 | 走什么 | 受保护进程(PPL/反作弊/高权限) |
8
+ | --- | --- | --- |
9
+ | `SelfBackend`(默认) | 用户态 `OpenProcess` + `ReadProcessMemory` | ❌ 读不到 |
10
+ | `NativeCeBackend` | 连 `ceserver` 的 TCP 协议 | ✅(如果那个 ceserver 提权/带驱动) |
11
+ | **`DbkBackend`(本文)** | **DBK 驱动的 `DeviceIoControl`** | ✅ |
12
+
13
+ 受保护进程连句柄都打不开,用户态无解 —— 唯一办法是让**内核**代劳。
14
+
15
+ ## 二、协议(逐行核对 CE 源码,不是猜的)
16
+
17
+ ### 2.1 ioctl 码
18
+
19
+ 来源:`Cheat Engine/dbk32/DBK32functions.pas` 第 22–93 行。
20
+
21
+ ```pascal
22
+ const FILE_READ_ACCESS = $0001;
23
+ const FILE_WRITE_ACCESS = $0002;
24
+ const FILE_RW_ACCESS = FILE_READ_ACCESS or FILE_WRITE_ACCESS; // = 3
25
+ const METHOD_BUFFERED = 0;
26
+ const FILE_DEVICE_UNKNOWN = $00000022;
27
+ const IOCTL_UNKNOWN_BASE = FILE_DEVICE_UNKNOWN;
28
+
29
+ IOCTL_CE_READMEMORY = (IOCTL_UNKNOWN_BASE shl 16) or ($0800 shl 2) or METHOD_BUFFERED or (FILE_RW_ACCESS shl 14);
30
+ IOCTL_CE_WRITEMEMORY = (IOCTL_UNKNOWN_BASE shl 16) or ($0801 shl 2) or ...
31
+ ```
32
+
33
+ 代入 READMEMORY:
34
+
35
+ ```
36
+ (0x22 << 16) | (0x0800 << 2) | 0 | (3 << 14)
37
+ = 0x220000 | 0x2000 | 0 | 0xC000
38
+ = 0x220000 | 0x2000 = 0x222000
39
+ = 0x222000 | 0xC000 = 0x22E000
40
+ ```
41
+
42
+ > ⚠️ **这一步很容易算错**。我在这上面错了两次:先后误以为结果是 `0x22C000`。
43
+ > 原因是 `0x22 << 16` 会占用 bit 17/21,与 `0x0800 << 2` 的 bit 13 **在同一数值区间叠加**,
44
+ > 不能凭"整齐的十六进制"直觉去拼。**必须逐项 OR 累加。**
45
+ > 真机上 ioctl 码错了只表现为"驱动返回无效请求",几乎无法从现象反推原因 ——
46
+ > 所以测试里把具体数值写死了。
47
+
48
+ 本后端用到的码(都由上面的公式算出):
49
+
50
+ | 名称 | 功能号 | ioctl 码 |
51
+ | --- | --- | --- |
52
+ | `READ_MEMORY` | `$0800` | `0x22E000` |
53
+ | `WRITE_MEMORY` | `$0801` | `0x22E004` |
54
+ | `OPEN_PROCESS` | `$0802` | `0x22E008` |
55
+ | `QUERY_VIRTUAL_MEMORY` | `$0803` | `0x22E00C` |
56
+ | `GET_VERSION` | `$0816` | `0x22E058` |
57
+ | `MAKE_WRITABLE` | `$0819` | `0x22E064` |
58
+ | `ALLOCATE_MEM` | `$081F` | `0x22E07C` |
59
+ | `SUSPEND_PROCESS` / `RESUME_PROCESS` | `$0824` / `$0825` | `0x22E090` / `0x22E094` |
60
+
61
+ ### 2.2 读内存
62
+
63
+ 来源:`ReadProcessMemory64_Internal`,第 1523–1584 行。
64
+
65
+ ```pascal
66
+ type TInputstruct = packed record // ← packed:无对齐填充
67
+ processid: uint64;
68
+ startaddress: uint64;
69
+ bytestoread: word;
70
+ end;
71
+ ...
72
+ deviceiocontrol(hdevice, cc, @input, sizeof(input), pointer(bufpointer), toread, br, nil)
73
+ ```
74
+
75
+ - 输入是 **18 字节**(8+8+2)。若按自然对齐会变成 24 字节,驱动读到的偏移就全错。
76
+ - 输出直接写到调用方缓冲区。
77
+ - CE 按 4096 字节分块,且**不跨页**:
78
+
79
+ ```pascal
80
+ if (mempointer and $fff) > 0 then
81
+ toread := 4096 - (mempointer and $fff)
82
+ else
83
+ toread := 4096;
84
+ ```
85
+
86
+ ### 2.3 写内存
87
+
88
+ 来源:第 1620–1686 行。
89
+
90
+ ```pascal
91
+ input: TInputstruct absolute ao[0]; // 512 字节固定缓冲
92
+ zeromemory(@ao[0], 512);
93
+ input.bytestowrite := towrite;
94
+ input.startaddress := mempointer;
95
+ copymemory(@ao[sizeof(tinputstruct)], bufpointer2, towrite); // 数据紧跟头部
96
+ deviceiocontrol(hdevice, cc, @ao[0], 512, @ao[0], 512, br, nil); // ← in/out 同一块
97
+ ```
98
+
99
+ - 缓冲固定 **512 字节**,单次最多 `512 - 18 = 494` 字节。
100
+ - **in 与 out 传同一块缓冲** —— 这是 `METHOD_BUFFERED` 允许的,也是 CE 的做法。
101
+
102
+ ### 2.4 设备名
103
+
104
+ 来源:第 3194 行与第 3436 行。
105
+
106
+ ```pascal
107
+ servicename := 'CEDRIVER73'; // 出厂默认(第 3194 行)
108
+ ...
109
+ hDevice := CreateFileW(pwidechar('\\.\'+servicename), GENERIC_READ or GENERIC_WRITE,
110
+ FILE_SHARE_READ or FILE_SHARE_WRITE, ...);
111
+ ```
112
+
113
+ 但 `servicename` 可以被 CE 目录下的 `driver.dat` / `driver64.dat` **覆盖**
114
+ (第 3217–3225 行按行读入,第 1 行就是服务名)—— CE 安装时会把服务名**随机化**来降低被检测的概率。
115
+
116
+ 因此本后端按以下顺序解析设备路径:
117
+
118
+ 1. 显式配置的 `serviceName`;
119
+ 2. `driver.dat` / `driver64.dat` 的第一行(需要给 `cheatEngineDir`);
120
+ 3. 默认 `CEDRIVER73`。
121
+
122
+ ## 三、用法
123
+
124
+ ### 3.1 在 node-cheat-engine 里直接用
125
+
126
+ ```js
127
+ const { DbkBackend, Process, setDefaultBackend } = require('node-cheat-engine');
128
+
129
+ const backend = new DbkBackend({ cheatEngineDir: 'C:\\Program Files\\Cheat Engine' });
130
+
131
+ // 先自检:设备能不能打开、驱动版本能不能读到
132
+ const probe = backend.probe();
133
+ console.log(probe);
134
+ // { devicePath: '\\\\.\\CEDRIVER73', opened: false, version: null, error: '...' }
135
+
136
+ setDefaultBackend(backend);
137
+ const proc = Process.open(1234);
138
+ const buf = proc.readMemory(0x7ff000001000n, 64); // ← 走内核 ioctl
139
+ proc.close();
140
+ backend.dispose();
141
+ ```
142
+
143
+ ### 3.2 在 DSH 插件里切换
144
+
145
+ ```yaml
146
+ config:
147
+ backend: dbk
148
+ cheatEngineDir: 'C:\Program Files\Cheat Engine' # 可选:让插件读 driver.dat 拿真实服务名
149
+ dbkServiceName: '' # 可选:直接指定服务名
150
+ ```
151
+
152
+ ### 3.3 自检工具
153
+
154
+ ```
155
+ ce_kernel { action: "probe_driver" }
156
+ ```
157
+
158
+ 这是**唯一能在真机上验证本后端的手段**(原因见下一节),它会报告:
159
+
160
+ ```
161
+ 设备路径:\\.\CEDRIVER73
162
+ 能否打开:能 / 不能
163
+ 驱动版本:x.y.z.w
164
+ 内核服务 DBK64:RUNNING
165
+ ```
166
+
167
+ ## 五、支持的能力
168
+
169
+ | 操作 | 实现方式 | 说明 |
170
+ | --- | --- | --- |
171
+ | 读 / 写内存 | ✅ **驱动**(`READ_MEMORY` / `WRITE_MEMORY`) | 本后端存在的唯一理由;受保护进程也能读写 |
172
+ | **分配内存** | ✅ **驱动**(`IOCTL_CE_ALLOCATEMEM` = `0x22E07C`) | 驱动 `KeAttachProcess` 到目标进程再调 `ZwAllocateVirtualMemory`,**受保护进程也能分配** |
173
+ | 进程/模块/线程枚举 | 委托 `SelfBackend` | 本来就不受进程保护影响 |
174
+ | 内存区域枚举 | 委托 `SelfBackend`(`VirtualQueryEx`) | 同上 |
175
+ | 改内存保护 | ⚠️ **用户态**(`VirtualProtectEx`) | 见下方说明 —— 驱动侧在 x64 上是空的 |
176
+ | 释放内存 | ⚠️ **用户态**(`VirtualFreeEx`) | 驱动没有释放目标进程内存的 ioctl |
177
+ | 挂起/恢复线程 | ❌ 未实现 | 驱动有 `SUSPENDTHREAD`/`RESUMETHREAD`(`$0822`/`$0823`) |
178
+ | 物理内存读写 | ❌ 未实现 | `IOCTL_CE_READPHYSICALMEMORY`(`$0806`)等 |
179
+ | 内核调试(断点/单步) | ❌ 未实现 | 涉及驱动侧调试器状态机,风险高 |
180
+
181
+ ### 5.1 为什么 `changeProtection` 不走驱动
182
+
183
+ CE 的驱动里**确实**有一个 `IOCTL_CE_MAKEWRITABLE`(功能号 `$0819`),但它在
184
+ **x64 驱动上是空的** —— `DBKKernel/IOPLDispatcher.c:474-491`:
185
+
186
+ ```c
187
+ case IOCTL_CE_MAKEWRITABLE:
188
+ {
189
+ #ifdef AMD64
190
+ //untill I know how win64 handles paging, not implemented
191
+ #else
192
+ struct InputBuf {
193
+ UINT64 StartAddress;
194
+ ULONG Size;
195
+ BYTE CopyOnWrite;
196
+ } *PInputBuf;
197
+ PInputBuf = Irp->AssociatedIrp.SystemBuffer;
198
+ ntStatus = MakeWritable(...) ? STATUS_SUCCESS : STATUS_UNSUCCESSFUL;
199
+ #endif
200
+ break;
201
+ }
202
+ ```
203
+
204
+ 所以 64 位 Windows 上发这个 ioctl 只会拿到「无效请求」(`STATUS_INVALID_DEVICE_REQUEST`)。
205
+
206
+ 而且它的输入结构里**没有 `processid`** —— `MakeWritable` 是按**调用者进程**的页表
207
+ 改 PTE(`deepkernel.c` 里那个 `x->RW=1` 直接写 PTE),本来也不适合"改另一个进程的保护"。
208
+
209
+ **因此 `changeProtection` 委托用户态 `VirtualProtectEx`**,并如实报告这一点。
210
+
211
+ > **好消息**:**写内存不需要先改保护**。驱动的写路径本身就在内核态执行,
212
+ > 只读页照样能写(`SelfBackend` 那种"先临时放开保护"的麻烦在 DBK 后端里不存在)。
213
+ > 需要 `changeProtection` 的场景主要是把页改成可执行、或让目标进程自己也能写。
214
+
215
+ ### 5.2 `allocate` 的数据流(容易写错的地方)
216
+
217
+ CE 源码(`DBK32functions.pas:2217-2223`)的输入结构:
218
+
219
+ ```pascal
220
+ x: record
221
+ processid: uint64;
222
+ baseaddress: uint64;
223
+ size: uint64;
224
+ AllocationType: uint64;
225
+ Protect: uint64;
226
+ end; // 5 × 8 = 40 字节
227
+ ```
228
+
229
+ 驱动侧(`IOPLDispatcher.c` 的 `ALLOCATEMEM` 分支)**把结果写回同一块 SystemBuffer**:
230
+
231
+ ```c
232
+ *(PUINT64)Irp->AssociatedIrp.SystemBuffer = 0;
233
+ *(PUINT_PTR)Irp->AssociatedIrp.SystemBuffer = (UINT_PTR)BaseAddress;
234
+ ```
235
+
236
+ ⚠️ 所以调用时 **in 与 out 必须是同一块缓冲**,再从**偏移 0** 读返回值:
237
+
238
+ ```js
239
+ const buffer = packAllocInput(pid, preferred, BigInt(size), MEM_COMMIT | MEM_RESERVE, protect);
240
+ ioctl(DBK_IOCTL.ALLOCATE_MEM, buffer, buffer.length, buffer); // in/out 同一块
241
+ const address = readAllocResult(buffer); // 偏移 0 的 uint64
242
+ ```
243
+
244
+ 若误把 out 传成另一块空缓冲,读到的永远是 0。
245
+
246
+ ## 六、未验证部分(**请务必读完**)
247
+
248
+ ### 4.1 为什么无法在本机验证
249
+
250
+ 只有驱动**真正加载**后 `\\.\CEDRIVER73` 才存在,而**加载内核驱动需要管理员权限**
251
+ (Windows 硬边界,见 [README 的内核驱动一节](../README.md))。
252
+ 开发这个后端的环境没有管理员权限,因此:
253
+
254
+ | 项目 | 状态 |
255
+ | --- | --- |
256
+ | ioctl 码与 CE 源码一致 | ✅ 已用**逐项 OR 累加**核对,测试写死具体数值 |
257
+ | 输入结构是 packed 18 字节 | ✅ 已用字段偏移断言验证 |
258
+ | 设备名解析(含 driver.dat) | ✅ 已用真实文件格式测试 |
259
+ | 设备打不开时的错误路径 | ✅ 已测试(本机就是这种情况) |
260
+ | **驱动侧实际期待的结构布局** | ❌ **未验证** —— 只能在装好驱动的机器上试 |
261
+ | **读写在真实受保护进程上的效果** | ❌ **未验证** |
262
+
263
+ ### 4.2 已知的具体风险
264
+
265
+ 1. **`processid` 的宽度**。CE 的 Pascal 源码里是 `uint64`,但 32 位与 64 位构建的
266
+ `packed record` 布局可能不同。本实现按 **8 字节**写(与 DBK64 的 64 位驱动对应)。
267
+ 若驱动侧实际期待 `dword`,读到的 `startaddress` 会错位 —— 表现为读到全 0 或乱码。
268
+ 2. **驱动侧可能有额外的对齐要求**。CE 的 Pascal 用 `packed record`,
269
+ 但 C 侧(`DBKKernel/`)的结构体若未加 `#pragma pack(1)`,布局会不同。
270
+ CE 的 `DBK32functions.pas` 是**用户态**那份,理论上与驱动侧一致,但这一点没能实测。
271
+ 3. **`GET_VERSION` 的返回布局**。本实现把返回的前几个非零 dword 拼成版本号,
272
+ 布局未经确认,因此 `probe()` 里对"版本读不出来"单独给了一条提示
273
+ (它意味着**协议可能不一致**,而不是驱动没工作)。
274
+
275
+ ### 4.3 出问题时怎么排查
276
+
277
+ 先跑 `ce_kernel action=probe_driver`:
278
+
279
+ - **打不开设备** → 驱动没加载 / 服务名不对 / 未提权。这一层与本后端无关,
280
+ 按输出里的三条原因逐条排。
281
+ - **能打开但版本读不到** → 高度怀疑 §4.2 的结构布局问题。
282
+ - **能打开、版本正常、但读到的字节不对** → 大概率是 §4.2 第 1 条
283
+ (`processid` 宽度)。此时可以试着把 `packReadInput` / `packWriteBuffer` 里的
284
+ `writeBigUInt64LE(pid, 0)` 改成 `writeUInt32LE(pid, 0)` 并把后续偏移整体前移 4 字节,
285
+ 对比两种布局哪个能读对。
286
+
287
+ ## 七、尚未实现的能力
288
+
289
+ 见 §五 的表格。要点复述:
290
+
291
+ - **挂起/恢复线程**:驱动有 `IOCTL_CE_SUSPENDTHREAD`(`$0822`)/
292
+ `RESUMETHREAD`(`$0823`),未实现,能力声明里 `suspendThreads: false`。
293
+ - **物理内存读写**:`IOCTL_CE_READPHYSICALMEMORY`(`$0806`)/
294
+ `WRITEPHYSICALMEMORY`(`$0807`)/ `GETPHYSICALADDRESS`(`$0808`),未实现。
295
+ - **内核调试**:断点、单步、`GETCR3`/`GETSDT`/`GETIDT` 等。涉及驱动侧调试器状态机,
296
+ 风险高,未做。
297
+ - **`ALLOCATEMEM_NONPAGED`**(`$0826`,内核态非分页内存):CE 用它给驱动自己分配
298
+ 通信缓冲,与"目标进程内存"无关,本后端不需要。
299
+
300
+ > 这些**不声称支持**是刻意的:能力声明里对应的字段就是 `false`,
301
+ > 上层可以据此降级,而不是在运行时神秘失败。
302
+ >
303
+ > 另外要注意 `changeProtection` / `free` 虽然声明为 `true`,
304
+ > 但它们是**用户态**实现的 —— 对受保护进程会失败。
305
+ > 这是 CE 驱动本身的能力缺口(见 §5.1),不是本后端的偷懒。
@@ -0,0 +1,273 @@
1
+ # 两种内存后端:自研引擎 与 原生 CE
2
+
3
+ 本移植提供**统一接口 + 两个可切换后端**。上层 API(扫描、地址表、`.CT` 读写、
4
+ 地址表达式)对两者一视同仁,因为它们都只依赖 `MemoryBackend` 接口。
5
+
6
+ | | `SelfBackend`(默认) | `NativeCeBackend` |
7
+ | --- | --- | --- |
8
+ | 名称 | `'self'` | `'native-ce'` |
9
+ | 原理 | 本进程内通过 koffi 直接调 Win32 API | 作为客户端连接 CE 官方 `ceserver` 的 TCP 协议 |
10
+ | 外部依赖 | 无 | 需要运行中的 `ceserver` |
11
+ | 同步性 | **同步**(`capabilities.sync === true`) | **异步**(返回 Promise) |
12
+ | 需要管理员 | 视目标而定 | 视目标与 ceserver 而定 |
13
+ | 受保护进程 | 读不到(无内核驱动) | 取决于服务端权限与扩展 |
14
+
15
+ ---
16
+
17
+ ## 一、自研后端(默认,推荐先用这个)
18
+
19
+ 开箱即用、无外部依赖,覆盖 Cheat Engine 在**未加载内核驱动**时能做的全部用户态操作。
20
+
21
+ ```js
22
+ const { Process, ScanSession, VariableType, ScanOption, RoundingType } = require('node-cheat-engine');
23
+
24
+ // 默认就是 SelfBackend
25
+ const proc = Process.open(1234);
26
+ const s = new ScanSession(proc);
27
+ s.firstScan({
28
+ variableType: VariableType.DWord,
29
+ scanOption: ScanOption.ExactValue,
30
+ roundingType: RoundingType.Rounded,
31
+ floatAccuracy: 0,
32
+ value1: '1000',
33
+ });
34
+ console.log('命中', s.resultCount, '个');
35
+
36
+ proc.close();
37
+ ```
38
+
39
+ 显式指定后端:
40
+
41
+ ```js
42
+ const { Process, SelfBackend } = require('node-cheat-engine');
43
+
44
+ const backend = new SelfBackend();
45
+ const proc = Process.open(1234, { backend });
46
+ // ...
47
+ proc.close();
48
+ backend.dispose();
49
+ ```
50
+
51
+ 全局切换默认后端:
52
+
53
+ ```js
54
+ const { setDefaultBackend, NativeCeBackend } = require('node-cheat-engine');
55
+ setDefaultBackend(new NativeCeBackend({ host: '127.0.0.1', port: 52736 }));
56
+ setDefaultBackend(null); // 恢复自研后端
57
+ ```
58
+
59
+ ---
60
+
61
+ ## 二、原生 CE 后端(通过 ceserver)
62
+
63
+ ### 2.1 先准备一个 ceserver
64
+
65
+ `ceserver` 是 Cheat Engine 官方仓库里的服务端程序(源码在
66
+ `Cheat Engine/ceserver/`),它把 CE 的内存能力通过 TCP 暴露出来。
67
+ 本仓库**不含它的可执行文件**,你需要自己准备:
68
+
69
+ 1. 从 Cheat Engine 官方发行版中查找 `ceserver` / `ceserver.exe`;
70
+ 2. 或者用 CE 源码里的 `Cheat Engine/ceserver/` 自行编译
71
+ (Linux/Android 版本在 CE 源码中;Windows 版本同样位于该目录);
72
+ 3. 启动它,默认监听 **52736** 端口。
73
+
74
+ 先用 telnet / 自备脚本确认端口可达,再跑下面的代码。
75
+
76
+ ### 2.2 用法(注意 `await`)
77
+
78
+ ```js
79
+ const { NativeCeBackend, openProcessAsync, ScanSession } = require('node-cheat-engine');
80
+
81
+ const backend = new NativeCeBackend({ host: '127.0.0.1', port: 52736 });
82
+
83
+ // 版本探测(可选,用于确认连通)
84
+ console.log('ceserver 版本:', await backend.getVersion());
85
+
86
+ // 打开进程:必须用异步入口
87
+ const proc = await openProcessAsync(1234, { backend });
88
+
89
+ // 读内存:用异步变体
90
+ const buf = await proc.readMemoryAsync(0x7ff000001000n, 16);
91
+
92
+ // 写内存
93
+ await proc.writeMemoryAsync(0x7ff000001000n, Buffer.from([1, 2, 3, 4]));
94
+
95
+ // 区域与模块
96
+ const regions = await proc.regionsAsync();
97
+ const modules = await proc.modulesAsync();
98
+
99
+ await proc.closeAsync();
100
+ await backend.dispose();
101
+ ```
102
+
103
+ ### 2.3 也可以用异步版的进程枚举
104
+
105
+ ```js
106
+ const { listProcessesAsync, findProcessesByNameAsync } = require('node-cheat-engine');
107
+ const all = await listProcessesAsync(backend);
108
+ const notepads = await findProcessesByNameAsync('notepad', backend);
109
+ ```
110
+
111
+ ---
112
+
113
+ ## 三、同步与异步的分工(重要)
114
+
115
+ `MemoryBackend` 的方法返回 `MaybePromise<T>`:
116
+
117
+ - **同步后端**(`SelfBackend`)直接返回值,**不**返回 Promise;
118
+ - **异步后端**(`NativeCeBackend`)返回 Promise。
119
+
120
+ `Process` 同时提供两套方法:
121
+
122
+ | 同步(仅同步后端可用) | 异步(两种后端都可用) |
123
+ | --- | --- |
124
+ | `Process.open(pid)` | `await openProcessAsync(pid)` |
125
+ | `proc.readMemory(addr, n)` | `await proc.readMemoryAsync(addr, n)` |
126
+ | `proc.writeMemory(addr, buf)` | `await proc.writeMemoryAsync(addr, buf)` |
127
+ | `proc.regions()` | `await proc.regionsAsync()` |
128
+ | `proc.modules()` | `await proc.modulesAsync()` |
129
+ | `proc.threadIds()` | `await proc.threadIdsAsync()` |
130
+ | `proc.allocate(...)` | `await proc.allocateAsync(...)` |
131
+ | `proc.protect(...)` | `await proc.protectAsync(...)` |
132
+ | `proc.isWow64()` | `await proc.isWow64Async()` |
133
+ | `proc.close()` | `await proc.closeAsync()` |
134
+
135
+ 在异步后端上误用同步方法时,会得到**明确错误**而不是崩掉:
136
+
137
+ ```
138
+ 后端 "native-ce" 是异步的,不能使用同步方法 readMemory()。
139
+ 请改用异步变体(例如 readMemoryAsync())并 await,
140
+ 或改用同步后端 SelfBackend。
141
+ ```
142
+
143
+ `await` 对两种后端都安全(`await` 一个非 Promise 值会原样返回),
144
+ 所以如果你只写一套代码,统一用 `await` + `*Async` 即可。
145
+
146
+ ---
147
+
148
+ ## 四、为什么原生 CE 后端必须是异步的
149
+
150
+ 这不是偷懒,而是**已证明技术上不可行**。曾尝试两种同步包装方式,都实测死锁:
151
+
152
+ ### 尝试 1:主线程 `Promise` + `Atomics.wait`
153
+
154
+ ```js
155
+ // 反例(不要这么写)
156
+ const p = socketRoundTrip();
157
+ while (!done) Atomics.wait(ctrl, 0, 0, 50); // ← 卡死
158
+ ```
159
+
160
+ `Atomics.wait` 会完全阻塞当前线程的事件循环,socket 的 `connect`/`data`
161
+ 回调与 Promise 微任务都永远得不到执行。
162
+ 最小复现:`docs/evidence/atomics-wait-io.js` → 输出 `FAIL: 死锁/未完成`。
163
+
164
+ ### 尝试 2:socket 放 worker 线程,主线程仍 `Atomics.wait`
165
+
166
+ ```js
167
+ // 同样反例:worker 里也阻塞
168
+ worker: Atomics.wait(ctrl, 0, seq, 20) // ← worker 自己的事件循环也停了
169
+ ```
170
+
171
+ **worker 线程被阻塞后,它自己的 socket 回调同样不会执行**,
172
+ 于是主线程每次都等到超时。最小复现:`docs/evidence/mini-repro.js`。
173
+
174
+ (补充验证:`setImmediate` 自旋轮询会饥饿 I/O 阶段;`setTimeout` 轮询也不行,
175
+ 因为阻塞发生在同一线程。)
176
+
177
+ ### 结论
178
+
179
+ `Atomics.wait` 与 socket I/O **无法在任何同一线程内共存**。
180
+ 子进程方案也验证失败(沙箱下 `spawnSync` 子进程的 socket 无响应)。
181
+
182
+ 因此选择最诚实的做法:**原生 CE 后端暴露为异步 API**,
183
+ 而不是用子进程或忙轮询把复杂度藏到别处。
184
+
185
+ ---
186
+
187
+ ## 五、能力矩阵(用于运行时判断)
188
+
189
+ ```js
190
+ const { SelfBackend, NativeCeBackend } = require('node-cheat-engine');
191
+
192
+ console.table({
193
+ SelfBackend: new SelfBackend().capabilities,
194
+ NativeCeBackend: new NativeCeBackend({ port: 1 }).capabilities,
195
+ });
196
+ ```
197
+
198
+ | 能力 | SelfBackend | NativeCeBackend |
199
+ | --- | --- | --- |
200
+ | `sync`(同步完成) | `true` | `false` |
201
+ | `enumerateProcesses` | ✅ | ✅ |
202
+ | `queryRegions` | ✅ | ✅ |
203
+ | `queryModules` | ✅ | ✅ |
204
+ | `queryThreads` | ✅ | ❌(明确抛错) |
205
+ | `suspendThreads` | ✅ | ❌ |
206
+ | `allocateMemory` | ✅ | ✅ |
207
+ | `changeProtection` | ✅ | ✅ |
208
+ | `local` | `true` | `false` |
209
+ | `requiresServer` | `false` | `true` |
210
+
211
+ 调用前建议检查 `backend.capabilities.xxx`,不要假设能力存在。
212
+ `NativeCeBackend.queryThreads()` 会抛出带解释的错误(而不是返回空数组假装成功)。
213
+
214
+ ---
215
+
216
+ ## 六、ceserver 协议实现范围
217
+
218
+ 完整命令号列表见 `CESERVER_CMD`(严格对齐 `ceserver.h`)。已实现:
219
+
220
+ | 命令 | 用途 |
221
+ | --- | --- |
222
+ | `GETVERSION(0)` | 版本探测 |
223
+ | `OPENPROCESS(3)` / `CLOSEHANDLE(7)` | 打开/关闭进程 |
224
+ | `CREATETOOLHELP32SNAPSHOT(4)` | 建进程/模块快照 |
225
+ | `PROCESS32FIRST(5)` / `PROCESS32NEXT(6)` | 进程枚举 |
226
+ | `VIRTUALQUERYEX(8)` | 内存区域枚举 |
227
+ | `READPROCESSMEMORY(9)` | 读内存(`compress=0`) |
228
+ | `WRITEPROCESSMEMORY(10)` | 写内存 |
229
+ | `MODULE32FIRST(22)` / `MODULE32NEXT(23)` | 模块枚举 |
230
+ | `ALLOC(26)` / `FREE(27)` | 目标内分配/释放 |
231
+ | `CHANGEMEMORYPROTECTION(36)` | 改内存保护 |
232
+
233
+ **未实现**(调用会抛 `BackendUnsupportedError` 或明确错误):
234
+ 线程枚举 `CREATETOOLHELP32SNAPSHOTEX(35)`(响应布局随版本变化)、
235
+ 调试器命令(`STARTDEBUG`/`SETBREAKPOINT` 等)、速度修改 `SPEEDHACK_SETSPEED(30)`、
236
+ 管道(`OPENNAMEDPIPE`/`PIPEREAD`/`PIPEWRITE`)、符号文件加载 `GETSYMBOLLISTFROMFILE(24)`。
237
+
238
+ 协议组帧细节(字段宽度、响应布局、两轮读取的原因)都写在
239
+ `src/backend/native-ce-backend.ts` 的注释里,并有用本地假服务端逐字段核对的测试
240
+ (`test/backend.test.js`,20 个用例)。
241
+
242
+ ---
243
+
244
+ ## 七、联调排查
245
+
246
+ | 现象 | 原因与处理 |
247
+ | --- | --- |
248
+ | `无法连接 ceserver(host:port):...` | 服务端没起、端口不对、或防火墙。错误信息里带了 host:port。 |
249
+ | `连接 ceserver 超时` | 地址不可达。确认 ceserver 在目标机器上监听。 |
250
+ | `ceserver 无法打开进程 N(...)` | 进程不存在,或服务端权限不足 → 以管理员身份运行 ceserver。 |
251
+ | `ceserver 命令 "X" 响应超时` | 服务端卡住或响应长度与预期不符。可调大 `timeoutMs`。 |
252
+ | `后端 "native-ce" 是异步的...` | 你用了同步方法。改用 `*Async` 变体 + `await`。 |
253
+ | `ceserver 写入不完整` | 服务端只写了部分字节(常见于保护页)。本实现会**抛错而不是静默通过**。 |
254
+
255
+ 需要真实联调时,把 `ceserver` 起来后:
256
+
257
+ ```bash
258
+ npm run test:backend
259
+ ```
260
+
261
+ **注意**:`test/backend.test.js` 目前**全部**使用本地假服务端来逐字段核对协议
262
+ (20 个用例),**不含**依赖真实 `ceserver` 的用例 —— 因为本仓库不含该二进制,
263
+ 测试必须能在没有它的环境下通过。因此这 20 个用例证明的是
264
+ **组帧/解析正确**,不证明"你的 ceserver 部署可用"。
265
+ 后者请按上面的排查表手工验证:
266
+
267
+ ```js
268
+ const { NativeCeBackend } = require('node-cheat-engine');
269
+ const b = new NativeCeBackend({ host: '127.0.0.1', port: 52736 });
270
+ console.log(await b.getVersion()); // 能打印版本即说明连通
271
+ console.log((await b.listProcesses()).length, '个进程');
272
+ await b.dispose();
273
+ ```
@@ -0,0 +1,20 @@
1
+ // 验证:主线程 Atomics.wait 期间,Promise 微任务与 socket I/O 能否推进
2
+ const net = require('node:net');
3
+ const server = net.createServer(s => { s.on('data', d => s.write('pong')); });
4
+ server.listen(0, '127.0.0.1', () => {
5
+ const port = server.address().port;
6
+ const state = new Int32Array(new SharedArrayBuffer(4));
7
+ let done = false; let result = null;
8
+ const p = new Promise((resolve, reject) => {
9
+ const c = net.connect({ host: '127.0.0.1', port });
10
+ c.on('connect', () => c.write('ping'));
11
+ c.on('data', d => { result = d.toString(); c.destroy(); resolve(result); });
12
+ c.on('error', reject);
13
+ });
14
+ p.then(v => { result = v; done = true; Atomics.store(state,0,1); Atomics.notify(state,0); },
15
+ e => { result = 'ERR:'+e.message; done = true; Atomics.store(state,0,1); Atomics.notify(state,0); });
16
+ const t0 = Date.now();
17
+ while (!done && Date.now() - t0 < 3000) { Atomics.wait(state, 0, 0, 50); }
18
+ console.log(done ? `OK: 主线程 Atomics.wait 期间 socket 往返成功 -> ${result} (${Date.now()-t0}ms)` : `FAIL: 死锁/未完成 (${Date.now()-t0}ms)`);
19
+ server.close();
20
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * 最小复现(self-contained):验证
3
+ * 主线程 `Atomics.wait` 阻塞 + worker 里非阻塞轮询 + worker 里 socket I/O
4
+ * 三者能否共存。
5
+ *
6
+ * 结论(跑通即证明):worker 侧**必须全程非阻塞**(setImmediate 轮询)。
7
+ * 若 worker 里用 `Atomics.wait` 阻塞,则 worker 自己的事件循环被卡死,
8
+ * socket 回调与 postMessage 都执行不了 —— 主线程每次都超时。
9
+ */
10
+ 'use strict';
11
+
12
+ const { Worker } = require('node:worker_threads');
13
+ const net = require('node:net');
14
+ const path = require('node:path');
15
+ const fs = require('node:fs');
16
+
17
+ const WORKER_SRC = `
18
+ const { parentPort } = require('node:worker_threads');
19
+ const { connect } = require('node:net');
20
+
21
+ let shared = null, host = '127.0.0.1', port = 0;
22
+
23
+ parentPort.on('message', (msg) => {
24
+ if (msg.type !== 'init') return;
25
+ host = msg.host; port = msg.port; shared = msg.buf;
26
+
27
+ const ctrl = new Int32Array(shared, 0, 4);
28
+ let last = Atomics.load(ctrl, 0);
29
+
30
+ const pump = () => {
31
+ const seq = Atomics.load(ctrl, 0);
32
+ if (seq === last) { setTimeout(pump, 1); return; } // 关键:用 setTimeout 让出到"定时器阶段",
33
+ // 事件循环才会进入 poll 阶段处理 socket I/O。
34
+ // 用 setImmediate 会自旋饥饿 I/O(实测超时)。
35
+ last = seq;
36
+ console.log('[worker] 收到请求 seq =', seq);
37
+ const c = connect({ host, port });
38
+ c.on('connect', () => c.write('ping'));
39
+ c.on('data', (d) => {
40
+ console.log('[worker] 收到响应:', d.toString());
41
+ c.destroy();
42
+ Atomics.store(ctrl, 3, seq);
43
+ Atomics.notify(ctrl, 3);
44
+ setTimeout(pump, 1);
45
+ });
46
+ c.on('error', (e) => {
47
+ console.log('[worker] socket 错误', e.message);
48
+ Atomics.store(ctrl, 3, seq);
49
+ Atomics.notify(ctrl, 3);
50
+ setTimeout(pump, 1);
51
+ });
52
+ };
53
+
54
+ parentPort.postMessage({ type: 'ready' });
55
+ setImmediate(pump);
56
+ });
57
+ `;
58
+
59
+ // 写入 worker 文件(每次运行重写,保证与上面的源码一致)
60
+ const workerPath = path.join(__dirname, '_mini-worker.js');
61
+ fs.writeFileSync(workerPath, WORKER_SRC, 'utf8');
62
+
63
+ const server = net.createServer((s) => {
64
+ s.on('data', () => s.write('pong'));
65
+ });
66
+
67
+ server.listen(0, '127.0.0.1', () => {
68
+ const port = server.address().port;
69
+ const buf = new SharedArrayBuffer(64);
70
+ const ctrl = new Int32Array(buf, 0, 4);
71
+
72
+ const w = new Worker(workerPath);
73
+ w.on('message', (m) => {
74
+ if (m.type !== 'ready') return;
75
+ console.log('[main] worker 就绪,发请求并阻塞等待');
76
+ Atomics.store(ctrl, 0, 1);
77
+ Atomics.notify(ctrl, 0);
78
+
79
+ const t0 = Date.now();
80
+ for (;;) {
81
+ const done = Atomics.load(ctrl, 3);
82
+ if (done === 1) {
83
+ console.log(`[main] ✅ 成功,用时 ${Date.now() - t0}ms`);
84
+ break;
85
+ }
86
+ if (Date.now() - t0 > 5000) {
87
+ console.log('[main] ❌ 超时 5s');
88
+ break;
89
+ }
90
+ Atomics.wait(ctrl, 3, done, 100);
91
+ }
92
+ void w.terminate();
93
+ server.close();
94
+ fs.unlinkSync(workerPath);
95
+ });
96
+
97
+ w.postMessage({ type: 'init', host: '127.0.0.1', port, buf });
98
+ });