@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,683 @@
1
+ "use strict";
2
+ /**
3
+ * DBK 内核驱动后端 —— 通过 CE 自己的 DBK 驱动 ioctl 读写受保护进程内存。
4
+ *
5
+ * ## 为什么需要它
6
+ *
7
+ * `SelfBackend` 走用户态 `OpenProcess`/`ReadProcessMemory`,因此**受保护进程
8
+ * (PPL、反作弊、以更高权限运行的进程)读不到**。CE 解决这个问题的办法是加载
9
+ * 自己的内核驱动(DBK64),再用一组 ioctl 让内核代劳。
10
+ * 本后端就是那组 ioctl 的 Node 实现。
11
+ *
12
+ * ## 协议来源(全部逐行核对 CE 源码,不是猜的)
13
+ *
14
+ * 1. **ioctl 码**(`Cheat Engine/dbk32/DBK32functions.pas` 第 26–93 行):
15
+ *
16
+ * ```pascal
17
+ * const METHOD_BUFFERED = 0;
18
+ * const FILE_DEVICE_UNKNOWN = $00000022;
19
+ * const FILE_RW_ACCESS = 3;
20
+ * IOCTL_CE_READMEMORY = (IOCTL_UNKNOWN_BASE shl 16) or ($0800 shl 2) or METHOD_BUFFERED or (FILE_RW_ACCESS shl 14);
21
+ * IOCTL_CE_WRITEMEMORY = (IOCTL_UNKNOWN_BASE shl 16) or ($0801 shl 2) or ...
22
+ * ```
23
+ *
24
+ * 2. **读内存的调用形态**(`ReadProcessMemory64_Internal`,第 1523–1584 行):
25
+ *
26
+ * ```pascal
27
+ * type TInputstruct = packed record
28
+ * processid: uint64;
29
+ * startaddress: uint64;
30
+ * bytestoread: word;
31
+ * end;
32
+ * ...
33
+ * input.processid := processid;
34
+ * input.bytestoread := toread;
35
+ * input.startaddress := mempointer;
36
+ * deviceiocontrol(hdevice, cc, @input, sizeof(input), pointer(bufpointer), toread, br, nil)
37
+ * ```
38
+ *
39
+ * 注意 `packed record` —— **没有对齐填充**,所以输入是 8+8+2 = **18 字节**。
40
+ * 输出直接写到调用方缓冲区。
41
+ *
42
+ * 3. **写内存的调用形态**(第 1620–1686 行):
43
+ *
44
+ * ```pascal
45
+ * input: TInputstruct absolute ao[0]; // 512 字节的固定缓冲
46
+ * zeromemory(@ao[0], 512);
47
+ * input.bytestowrite := towrite;
48
+ * input.startaddress := mempointer;
49
+ * copymemory(@ao[sizeof(tinputstruct)], bufpointer2, towrite); // 数据紧跟头部
50
+ * deviceiocontrol(hdevice, cc, @ao[0], 512, @ao[0], 512, br, nil); // 同一块缓冲 in/out
51
+ * ```
52
+ *
53
+ * 单次最多写 `512 - sizeof(TInputstruct)` 字节。
54
+ *
55
+ * 4. **设备名**:`CreateFileW('\\.\' + servicename, ...)`(第 3436 行),
56
+ * 而 `servicename` 默认是 `'CEDRIVER73'`(第 3194 行),
57
+ * 也可被 CE 目录下的 `driver.dat` / `driver64.dat` 覆盖
58
+ * (第 3217–3225 行按行读入)。因此设备名**不能写死**,本模块按
59
+ * `driver.dat` → 默认值 → 常见候选的顺序探测。
60
+ *
61
+ * ## 必须说明的风险(诚实部分)
62
+ *
63
+ * **本后端的 ioctl 结构布局无法在没有管理员权限的机器上验证。**
64
+ * 原因是:只有内核驱动真正加载后 `\\.\CEDRIVER73` 才存在,而加载驱动需要管理员。
65
+ * 因此:
66
+ * - `IOCTL` 码与输入结构是**按源码逐行实现**的,不是猜测;
67
+ * - 但"驱动侧实际期待的结构布局"只能靠**加载后实测**确认;
68
+ * - 所以本模块提供 `probe()`:它用 `IOCTL_CE_GETVERSION`(一个参数最少的 ioctl)
69
+ * 验证设备是否真的响应,并如实报告结果,而不是假装能用。
70
+ *
71
+ * @module node-cheat-engine/backend/dbk-backend
72
+ */
73
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
74
+ if (k2 === undefined) k2 = k;
75
+ var desc = Object.getOwnPropertyDescriptor(m, k);
76
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
77
+ desc = { enumerable: true, get: function() { return m[k]; } };
78
+ }
79
+ Object.defineProperty(o, k2, desc);
80
+ }) : (function(o, m, k, k2) {
81
+ if (k2 === undefined) k2 = k;
82
+ o[k2] = m[k];
83
+ }));
84
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
85
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
86
+ }) : function(o, v) {
87
+ o["default"] = v;
88
+ });
89
+ var __importStar = (this && this.__importStar) || (function () {
90
+ var ownKeys = function(o) {
91
+ ownKeys = Object.getOwnPropertyNames || function (o) {
92
+ var ar = [];
93
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
94
+ return ar;
95
+ };
96
+ return ownKeys(o);
97
+ };
98
+ return function (mod) {
99
+ if (mod && mod.__esModule) return mod;
100
+ var result = {};
101
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
102
+ __setModuleDefault(result, mod);
103
+ return result;
104
+ };
105
+ })();
106
+ Object.defineProperty(exports, "__esModule", { value: true });
107
+ exports.DbkBackend = exports.DbkDeviceError = exports.DEFAULT_DBK_SERVICE_NAME = exports.DBK_ALLOC_INPUT_SIZE = exports.DBK_MAX_READ_CHUNK = exports.DBK_MAX_WRITE_PAYLOAD = exports.DBK_WRITE_BUFFER_SIZE = exports.DBK_WRITE_INPUT_SIZE = exports.DBK_READ_INPUT_SIZE = exports.DBK_IOCTL = void 0;
108
+ exports.packReadInput = packReadInput;
109
+ exports.packWriteBuffer = packWriteBuffer;
110
+ exports.packAllocInput = packAllocInput;
111
+ exports.readAllocResult = readAllocResult;
112
+ exports.readServiceNameFromDriverDat = readServiceNameFromDriverDat;
113
+ exports.resolveDevicePath = resolveDevicePath;
114
+ const self_backend_1 = require("./self-backend");
115
+ const w = __importStar(require("../win/winapi"));
116
+ const types_1 = require("../types");
117
+ /* ------------------------------------------------------------------ *
118
+ * ioctl 码
119
+ * ------------------------------------------------------------------ */
120
+ /** `FILE_DEVICE_UNKNOWN` */
121
+ const FILE_DEVICE_UNKNOWN = 0x00000022;
122
+ /** `METHOD_BUFFERED` */
123
+ const METHOD_BUFFERED = 0;
124
+ /** `FILE_READ_ACCESS | FILE_WRITE_ACCESS` = 0x0001 | 0x0002 */
125
+ const FILE_RW_ACCESS = 3;
126
+ /**
127
+ * CE 的 ioctl 码编码宏(对应 Delphi 的 `CTL_CODE`)。
128
+ *
129
+ * ```pascal
130
+ * (DeviceType shl 16) or (Function shl 2) or Method or (Access shl 14)
131
+ * ```
132
+ *
133
+ * 参数语义与 CE 源码里那行完全一致:
134
+ * - `deviceType` = `IOCTL_UNKNOWN_BASE` = `FILE_DEVICE_UNKNOWN` = `$22`(**未左移**)
135
+ * - `func` = `$0800` 这类功能号(**未左移**,本函数负责 `<< 2`)
136
+ * - `method` = `METHOD_BUFFERED` = 0
137
+ * - `access` = `FILE_RW_ACCESS` = 3(**未左移**,本函数负责 `<< 14`)
138
+ *
139
+ * 代入 READMEMORY 手算校验:
140
+ * `($22 << 16) | ($0800 << 2) | 0 | (3 << 14)`
141
+ * = `0x220000 | 0x2000 | 0 | 0xC000` = **`0x22C000`** ✅
142
+ *
143
+ * 这个手算值在测试里被写死 —— 因为这处很容易搞错,而在真机上
144
+ * ioctl 码错了只表现为"驱动返回无效请求",几乎无法从现象反推原因。
145
+ */
146
+ function ctlCode(deviceType, func, method, access) {
147
+ return ((deviceType << 16) | (func << 2) | method | (access << 14)) >>> 0;
148
+ }
149
+ /**
150
+ * 由 CE 的"功能号序号"算 ioctl 码。
151
+ *
152
+ * CE 源码里的功能号是 `$0800`、`$0801`…… 所以 `funcCode=0x00` 对应 `$0800`。
153
+ * 注意**不要**在这里预先左移:左移由 `ctlCode` 负责。
154
+ */
155
+ function ceIoctl(funcCode) {
156
+ return ctlCode(FILE_DEVICE_UNKNOWN, 0x0800 + funcCode, METHOD_BUFFERED, FILE_RW_ACCESS);
157
+ }
158
+ /**
159
+ * 本后端用到的 ioctl(功能号来自 `DBK32functions.pas` 的常量定义)。
160
+ *
161
+ * 值可以直接算出来校验:`0x22 << 16 = 0x220000`,`access << 14 = 0xC000`,
162
+ * 所以 READMEMORY(func 0)= `0x220000 | 0 | 0 | 0xC000` = **`0x22C000`**。
163
+ */
164
+ exports.DBK_IOCTL = Object.freeze({
165
+ READ_MEMORY: ceIoctl(0x00),
166
+ WRITE_MEMORY: ceIoctl(0x01),
167
+ OPEN_PROCESS: ceIoctl(0x02),
168
+ QUERY_VIRTUAL_MEMORY: ceIoctl(0x03),
169
+ TEST: ceIoctl(0x04),
170
+ /** `IOCTL_CE_GETVERSION`:功能号 0x16,参数最少,适合当"驱动是否活着"的探针 */
171
+ GET_VERSION: ceIoctl(0x16),
172
+ /** `IOCTL_CE_MAKEWRITABLE`:功能号 0x19,把只读页改成可写 */
173
+ MAKE_WRITABLE: ceIoctl(0x19),
174
+ /** `IOCTL_CE_ALLOCATEMEM`:功能号 0x1F */
175
+ ALLOCATE_MEM: ceIoctl(0x1f),
176
+ /** `IOCTL_CE_SUSPENDPROCESS` / `RESUMEPROCESS`:功能号 0x24 / 0x25 */
177
+ SUSPEND_PROCESS: ceIoctl(0x24),
178
+ RESUME_PROCESS: ceIoctl(0x25),
179
+ });
180
+ /* ------------------------------------------------------------------ *
181
+ * 输入结构(packed,无对齐填充)
182
+ * ------------------------------------------------------------------ */
183
+ /** 读内存输入结构的大小:`processid(8) + startaddress(8) + bytestoread(2)`。 */
184
+ exports.DBK_READ_INPUT_SIZE = 18;
185
+ /** 写内存输入结构的大小:`processid(8) + startaddress(8) + bytestowrite(2)`。 */
186
+ exports.DBK_WRITE_INPUT_SIZE = 18;
187
+ /** 写内存用的固定缓冲区总大小(CE 源码里写死 512)。 */
188
+ exports.DBK_WRITE_BUFFER_SIZE = 512;
189
+ /** 单次写入能携带的最大数据量。 */
190
+ exports.DBK_MAX_WRITE_PAYLOAD = exports.DBK_WRITE_BUFFER_SIZE - exports.DBK_WRITE_INPUT_SIZE;
191
+ /** 读内存单次请求上限(CE 按 4096 分块)。 */
192
+ exports.DBK_MAX_READ_CHUNK = 4096;
193
+ /**
194
+ * 分配内存输入结构的大小。
195
+ *
196
+ * CE 源码(`DBK32functions.pas:2217-2223`):
197
+ * ```pascal
198
+ * x: record
199
+ * processid: uint64;
200
+ * baseaddress: uint64;
201
+ * size: uint64;
202
+ * AllocationType: uint64;
203
+ * Protect: uint64;
204
+ * end;
205
+ * ```
206
+ * 5 × uint64 = **40 字节**。
207
+ *
208
+ * ⚠️ 与读/写不同,这个记录**没有** `packed` 关键字 —— 但 5 个 uint64 本来
209
+ * 就是 8 字节对齐的,所以 packed 与否大小相同(40),无需纠结。
210
+ */
211
+ exports.DBK_ALLOC_INPUT_SIZE = 40;
212
+ /**
213
+ * 打包「读内存」的输入结构。
214
+ *
215
+ * `packed record` 语义:**没有对齐填充**,所以是紧凑的 18 字节。
216
+ * 这一点很关键 —— 若按自然对齐会变成 24 字节,驱动读到的 `bytestoread` 位置就错了。
217
+ *
218
+ * @param pid 目标进程 id(CE 用 uint64 存)
219
+ * @param address 起始地址
220
+ * @param bytesToRead 要读的字节数(word,最大 65535)
221
+ * @returns 18 字节的输入缓冲
222
+ */
223
+ function packReadInput(pid, address, bytesToRead) {
224
+ const buf = Buffer.alloc(exports.DBK_READ_INPUT_SIZE);
225
+ buf.writeBigUInt64LE(BigInt(pid), 0);
226
+ buf.writeBigUInt64LE(address, 8);
227
+ buf.writeUInt16LE(bytesToRead & 0xffff, 16);
228
+ return buf;
229
+ }
230
+ /**
231
+ * 打包「写内存」的输入缓冲(512 字节固定大小,数据紧跟头部)。
232
+ *
233
+ * 与读不同,写是**同一块 512 字节缓冲同时当 in/out**:
234
+ * - 前 18 字节是 `{ processid, startaddress, bytestowrite }`;
235
+ * - 从第 18 字节起是要写的数据;
236
+ * - 调用时 in/out 都传这块缓冲。
237
+ *
238
+ * @param pid 目标进程 id
239
+ * @param address 起始地址
240
+ * @param data 要写入的数据(长度不得超过 `DBK_MAX_WRITE_PAYLOAD`)
241
+ * @returns 512 字节的缓冲区
242
+ */
243
+ function packWriteBuffer(pid, address, data) {
244
+ if (data.length > exports.DBK_MAX_WRITE_PAYLOAD) {
245
+ throw new RangeError(`单次写入 ${data.length} 字节超过驱动上限 ${exports.DBK_MAX_WRITE_PAYLOAD} 字节(调用方应先分块)。`);
246
+ }
247
+ const buf = Buffer.alloc(exports.DBK_WRITE_BUFFER_SIZE);
248
+ buf.writeBigUInt64LE(BigInt(pid), 0);
249
+ buf.writeBigUInt64LE(address, 8);
250
+ buf.writeUInt16LE(data.length, 16);
251
+ data.copy(buf, exports.DBK_WRITE_INPUT_SIZE);
252
+ return buf;
253
+ }
254
+ /**
255
+ * 打包「分配内存」的输入缓冲(40 字节,5 个 uint64)。
256
+ *
257
+ * 驱动侧(`IOPLDispatcher.c` 的 `IOCTL_CE_ALLOCATEMEM` 分支)会
258
+ * `KeAttachProcess` 到目标进程再调 `ZwAllocateVirtualMemory`,
259
+ * 并**把结果地址写回同一块 SystemBuffer 的前 8 字节**:
260
+ *
261
+ * ```c
262
+ * *(PUINT64)Irp->AssociatedIrp.SystemBuffer = 0;
263
+ * *(PUINT_PTR)Irp->AssociatedIrp.SystemBuffer = (UINT_PTR)BaseAddress;
264
+ * ```
265
+ *
266
+ * 所以调用方必须**用同一块缓冲同时作为 in 与 out**,再从偏移 0 读返回值。
267
+ */
268
+ function packAllocInput(pid, baseAddress, size, allocationType, protect) {
269
+ const buf = Buffer.alloc(exports.DBK_ALLOC_INPUT_SIZE);
270
+ buf.writeBigUInt64LE(BigInt(pid), 0);
271
+ buf.writeBigUInt64LE(baseAddress, 8);
272
+ buf.writeBigUInt64LE(size, 16);
273
+ buf.writeBigUInt64LE(allocationType, 24);
274
+ buf.writeBigUInt64LE(protect, 32);
275
+ return buf;
276
+ }
277
+ /** 从驱动写回的缓冲里读出分配到的地址(偏移 0 的 uint64)。 */
278
+ function readAllocResult(buffer) {
279
+ if (buffer.length < 8) {
280
+ throw new RangeError(`分配结果缓冲只有 ${buffer.length} 字节,读不出 8 字节地址。`);
281
+ }
282
+ return buffer.readBigUInt64LE(0);
283
+ }
284
+ /* ------------------------------------------------------------------ *
285
+ * 设备名探测
286
+ * ------------------------------------------------------------------ */
287
+ /** 默认设备名(`DBK32functions.pas` 第 3194 行的出厂值)。 */
288
+ exports.DEFAULT_DBK_SERVICE_NAME = 'CEDRIVER73';
289
+ /**
290
+ * 从 CE 目录的 `driver.dat` / `driver64.dat` 读取真实服务名。
291
+ *
292
+ * 文件格式(`DBK32functions.pas` 第 3217–3225 行,按行读):
293
+ * ```
294
+ * 第 1 行:servicename
295
+ * 第 2 行:processeventname
296
+ * 第 3 行:threadeventname
297
+ * 第 4 行:sysfile
298
+ * ...
299
+ * ```
300
+ * 只有第 1 行与本后端相关。
301
+ *
302
+ * @param ceDir Cheat Engine 安装目录(可选;给了才去读)
303
+ * @returns 服务名,读不到返回 null
304
+ */
305
+ function readServiceNameFromDriverDat(ceDir) {
306
+ if (ceDir === undefined || ceDir === '')
307
+ return null;
308
+ // 延迟 require:避免在非 Windows / 无 fs 场景下的加载开销
309
+ const fs = require('node:fs');
310
+ const path = require('node:path');
311
+ for (const name of ['driver.dat', 'driver64.dat']) {
312
+ const file = path.join(ceDir, name);
313
+ try {
314
+ if (!fs.existsSync(file))
315
+ continue;
316
+ const first = fs.readFileSync(file, 'utf8').split(/\r?\n/)[0]?.trim() ?? '';
317
+ if (first !== '')
318
+ return first;
319
+ }
320
+ catch {
321
+ /* 读不到就试下一个 */
322
+ }
323
+ }
324
+ return null;
325
+ }
326
+ /**
327
+ * 解析要用的设备路径。
328
+ *
329
+ * 顺序:显式配置 → `driver.dat` → 默认 `CEDRIVER73`。
330
+ *
331
+ * @param options.serviceName 显式服务名
332
+ * @param options.cheatEngineDir CE 安装目录(用于读 driver.dat)
333
+ * @returns `\\.\<服务名>`
334
+ */
335
+ function resolveDevicePath(options = {}) {
336
+ const explicit = options.serviceName?.trim();
337
+ if (explicit !== undefined && explicit !== '') {
338
+ return explicit.startsWith('\\\\.\\') ? explicit : `\\\\.\\${explicit}`;
339
+ }
340
+ const fromDat = readServiceNameFromDriverDat(options.cheatEngineDir);
341
+ if (fromDat !== null)
342
+ return `\\\\.\\${fromDat}`;
343
+ return `\\\\.\\${exports.DEFAULT_DBK_SERVICE_NAME}`;
344
+ }
345
+ /**
346
+ * 驱动后端的能力声明。
347
+ *
348
+ * - `allocateMemory` / `changeProtection`:**支持,但方式不同** ——
349
+ * `allocate` 走驱动的 `IOCTL_CE_ALLOCATEMEM`(受保护进程也能分配);
350
+ * `changeProtection` 走用户态(因为驱动的 `MAKEWRITABLE` 在 x64 上是空的,
351
+ * 见 `changeProtection` 的文档)。
352
+ * - `suspendThreads`:本类**未实现**,声称为 false(上层据此降级)。
353
+ * - `queryRegions`:走用户态 `VirtualQueryEx`(区域枚举不受进程保护影响)。
354
+ */
355
+ function capabilitiesFor() {
356
+ return {
357
+ sync: true,
358
+ enumerateProcesses: true,
359
+ queryRegions: true,
360
+ queryModules: true,
361
+ queryThreads: true,
362
+ suspendThreads: false,
363
+ // 分配走驱动(IOCTL_CE_ALLOCATEMEM),改保护走用户态,两者都可用
364
+ allocateMemory: true,
365
+ changeProtection: true,
366
+ local: true,
367
+ requiresServer: false,
368
+ };
369
+ }
370
+ /** 设备不可用(未加载驱动 / 权限不足)时抛出的错误。 */
371
+ class DbkDeviceError extends Error {
372
+ devicePath;
373
+ win32Code;
374
+ constructor(message, devicePath, win32Code) {
375
+ super(message);
376
+ this.devicePath = devicePath;
377
+ this.win32Code = win32Code;
378
+ this.name = 'DbkDeviceError';
379
+ }
380
+ }
381
+ exports.DbkDeviceError = DbkDeviceError;
382
+ /**
383
+ * 通过 CE 的 DBK 内核驱动读写目标进程内存。
384
+ *
385
+ * ## 与其他后端的关系
386
+ *
387
+ * - 进程/模块/线程枚举、分配、改保护这些**不需要内核**的操作,直接委托给
388
+ * 内部的 `SelfBackend`(避免重复实现,也避免为这些操作引入未验证的 ioctl);
389
+ * - **只有内存读写走驱动** —— 这正是受保护进程唯一需要内核的地方,
390
+ * 也是风险最高、最需要精确复刻协议的部分。
391
+ *
392
+ * ## 为什么同步
393
+ *
394
+ * 驱动 ioctl 是同步的系统调用(`DeviceIoControl` 直接返回),
395
+ * 因此本后端声明 `capabilities.sync === true`,扫描/指针扫描/AutoAssembler
396
+ * 都能正常使用(它们内部要求同步后端)。
397
+ */
398
+ class DbkBackend {
399
+ options;
400
+ name = 'dbk';
401
+ capabilities = capabilitiesFor();
402
+ self;
403
+ device = null;
404
+ devicePath;
405
+ versionCache;
406
+ constructor(options = {}) {
407
+ this.options = options;
408
+ this.self = new self_backend_1.SelfBackend();
409
+ this.devicePath = resolveDevicePath(options);
410
+ if (options.deviceHandle !== undefined)
411
+ this.device = options.deviceHandle;
412
+ }
413
+ /** 当前使用的设备路径(诊断用)。 */
414
+ get resolvedDevicePath() {
415
+ return this.devicePath;
416
+ }
417
+ /** 驱动是否已打开。 */
418
+ get isDeviceOpen() {
419
+ return this.device !== null;
420
+ }
421
+ /* ---------------- 设备生命周期 ---------------- */
422
+ /**
423
+ * 打开驱动设备。**需要驱动已加载**(加载驱动需要管理员权限,见 kernel-driver.js)。
424
+ *
425
+ * @throws DbkDeviceError 打开失败(未加载驱动 / 权限不足 / 名字不对)
426
+ */
427
+ openDevice() {
428
+ if (this.device !== null)
429
+ return;
430
+ const handle = w.CreateFileW(this.devicePath, (w.GENERIC_READ | w.GENERIC_WRITE) >>> 0, (w.FILE_SHARE_READ | w.FILE_SHARE_WRITE) >>> 0, null, w.CREATE_EXISTING, w.FILE_ATTRIBUTE_NORMAL, null);
431
+ const code = w.GetLastError();
432
+ if (handle === null || handle === undefined || w.handleToBigInt(handle) === 0xffffffffffffffffn) {
433
+ throw new DbkDeviceError(`打不开内核驱动设备 ${this.devicePath}(Win32 ${code}: ${w.win32ErrorMessage(code)})。\n` +
434
+ '可能原因:\n' +
435
+ ' 1. DBK 驱动还没加载 —— 用 ce_kernel action=install + action=load(需要管理员);\n' +
436
+ ' 2. 服务名不是默认的 CEDRIVER73 —— CE 会在安装时随机化,可从 CE 目录的 driver.dat 读取;\n' +
437
+ ' 3. 当前进程权限不足(打开驱动设备同样要管理员)。', this.devicePath, code);
438
+ }
439
+ this.device = handle;
440
+ }
441
+ /** 关闭设备句柄。 */
442
+ closeDevice() {
443
+ if (this.device === null)
444
+ return;
445
+ try {
446
+ w.CloseHandle(this.device);
447
+ }
448
+ catch {
449
+ /* 忽略关闭失败 */
450
+ }
451
+ this.device = null;
452
+ }
453
+ /**
454
+ * 拿驱动版本,用来确认「设备真的在工作」。
455
+ *
456
+ * 这是**最有价值的自检**:它用参数最少的 ioctl,
457
+ * 能区分「设备打不开」与「设备打开了但协议不对」这两种完全不同的故障。
458
+ *
459
+ * @returns 版本字符串;调用失败返回 null
460
+ */
461
+ queryVersion() {
462
+ if (this.versionCache !== undefined)
463
+ return this.versionCache;
464
+ let out;
465
+ try {
466
+ out = this.ioctl(exports.DBK_IOCTL.GET_VERSION, Buffer.alloc(0), 64);
467
+ }
468
+ catch {
469
+ this.versionCache = null;
470
+ return null;
471
+ }
472
+ // 版本是 4 个小整数拼接(CE 的 DBK_GETVERSION 返回 4 个 dword),
473
+ // 但具体布局未经实测,因此这里只做保守解释:把可打印部分与非零 dword 都列出来。
474
+ const parts = [];
475
+ for (let i = 0; i + 4 <= out.length && i < 16; i += 4) {
476
+ const v = out.readUInt32LE(i);
477
+ if (v !== 0)
478
+ parts.push(String(v));
479
+ }
480
+ this.versionCache = parts.length > 0 ? parts.join('.') : null;
481
+ return this.versionCache;
482
+ }
483
+ /**
484
+ * 一次性自检:设备能否打开、版本能否读到。
485
+ *
486
+ * 供工具输出使用 —— 让"驱动到底通没通"变成一个明确的结论,而不是靠猜。
487
+ */
488
+ probe() {
489
+ try {
490
+ this.openDevice();
491
+ }
492
+ catch (error) {
493
+ return {
494
+ devicePath: this.devicePath,
495
+ opened: false,
496
+ version: null,
497
+ error: error.message,
498
+ };
499
+ }
500
+ const version = this.queryVersion();
501
+ return { devicePath: this.devicePath, opened: true, version, error: null };
502
+ }
503
+ /** 发一个 ioctl。in/out 可以是同一块 Buffer(METHOD_BUFFERED 允许)。 */
504
+ ioctl(code, input, outSize, outBuffer) {
505
+ if (this.device === null)
506
+ this.openDevice();
507
+ const out = outBuffer ?? Buffer.alloc(outSize);
508
+ const returned = Buffer.alloc(4);
509
+ const ok = w.DeviceIoControl(this.device, code, input.length > 0 ? w.bufferAsPointer(input) : null, input.length, out.length > 0 ? w.bufferAsPointer(out) : null, out.length, returned, null);
510
+ if (!ok) {
511
+ const err = w.GetLastError();
512
+ throw new Error(`驱动 ioctl 0x${code.toString(16)} 失败(Win32 ${err}: ${w.win32ErrorMessage(err)})`);
513
+ }
514
+ const n = returned.readUInt32LE(0);
515
+ return n > 0 && n < out.length ? out.subarray(0, n) : out;
516
+ }
517
+ /* ---------------- 委托给 SelfBackend 的部分 ---------------- */
518
+ listProcesses() {
519
+ return this.self.listProcesses();
520
+ }
521
+ openProcess(pid) {
522
+ return this.self.openProcess(pid);
523
+ }
524
+ closeProcess(handle) {
525
+ this.self.closeProcess(handle);
526
+ }
527
+ queryModules(handle) {
528
+ return this.self.queryModules(handle);
529
+ }
530
+ queryThreads(handle) {
531
+ return this.self.queryThreads(handle);
532
+ }
533
+ isWow64(handle) {
534
+ return this.self.isWow64(handle);
535
+ }
536
+ queryRegions(handle, filter) {
537
+ // 走用户态 VirtualQueryEx 即可:区域枚举本来就不受进程保护影响
538
+ // (受保护进程连句柄都打不开,那时 SelfBackend 会抛错并给出权限建议)
539
+ return this.self.queryRegions(handle, filter);
540
+ }
541
+ /* ---------------- 走驱动的核心:内存读写 ---------------- */
542
+ /**
543
+ * 读内存 —— 通过 `IOCTL_CE_READMEMORY`。
544
+ *
545
+ * 按 CE 的做法**分块**(每块最多 4096 字节,且不跨页边界,
546
+ * 因为驱动侧按页处理)。读失败时返回已读到的部分,不抛错(与接口约定一致)。
547
+ */
548
+ readProcessMemory(handle, address, size) {
549
+ this.openDevice();
550
+ if (size <= 0)
551
+ return Buffer.alloc(0);
552
+ const out = Buffer.alloc(size);
553
+ let done = 0;
554
+ while (done < size) {
555
+ const current = address + BigInt(done);
556
+ const pageOffset = Number(current & 0xfffn);
557
+ // 不跨页:CE 也是这么算的(`if (mempointer and $fff) > 0`)
558
+ let chunk = pageOffset > 0 ? 4096 - pageOffset : Math.min(4096, size - done);
559
+ chunk = Math.min(chunk, size - done, exports.DBK_MAX_READ_CHUNK);
560
+ if (chunk <= 0)
561
+ break;
562
+ const input = packReadInput(handle.pid, current, chunk);
563
+ let got;
564
+ try {
565
+ got = this.ioctl(exports.DBK_IOCTL.READ_MEMORY, input, chunk);
566
+ }
567
+ catch {
568
+ // 读失败:返回已读到的部分(接口约定不抛错)
569
+ break;
570
+ }
571
+ if (got.length === 0)
572
+ break;
573
+ got.copy(out, done);
574
+ done += got.length;
575
+ if (got.length < chunk)
576
+ break;
577
+ }
578
+ return done === size ? out : out.subarray(0, done);
579
+ }
580
+ /**
581
+ * 写内存 —— 通过 `IOCTL_CE_WRITEMEMORY`。
582
+ *
583
+ * 按 CE 的做法分块:每块最多 `512 - 18 = 494` 字节,
584
+ * 且 512 字节缓冲**同时作为 in 与 out**。
585
+ *
586
+ * 失败**抛错**(写失败静默化会写坏目标,接口约定要求抛错)。
587
+ */
588
+ writeProcessMemory(_handle, address, data, _fixProtection) {
589
+ this.openDevice();
590
+ if (data.length === 0)
591
+ return 0;
592
+ let done = 0;
593
+ while (done < data.length) {
594
+ const current = address + BigInt(done);
595
+ const chunk = Math.min(exports.DBK_MAX_WRITE_PAYLOAD, data.length - done);
596
+ const payload = data.subarray(done, done + chunk);
597
+ const buffer = packWriteBuffer(_handle.pid, current, payload);
598
+ // 关键:in 与 out 传**同一块**缓冲(CE 源码就是 @ao[0], @ao[0])
599
+ this.ioctl(exports.DBK_IOCTL.WRITE_MEMORY, buffer, buffer.length, buffer);
600
+ done += chunk;
601
+ }
602
+ return done;
603
+ }
604
+ /* ---------------- 分配内存(走驱动) ---------------- */
605
+ /**
606
+ * 在目标进程内分配内存 —— 通过 `IOCTL_CE_ALLOCATEMEM`。
607
+ *
608
+ * 驱动会 `KeAttachProcess` 到目标进程后用 `ZwAllocateVirtualMemory` 分配,
609
+ * 因此**受保护进程也能分配**(这是用户态 `VirtualAllocEx` 做不到的)。
610
+ *
611
+ * @param size 请求大小(会被驱动按页向上取整)
612
+ * @param protect 保护属性,默认 `PAGE_EXECUTE_READWRITE`
613
+ * @param preferred 期望地址(`0n` 表示由系统选择),对应 `lpAddress`
614
+ * @returns 实际分配到的基址
615
+ */
616
+ allocate(handle, size, protect = types_1.PAGE_EXECUTE_READWRITE, preferred = 0n) {
617
+ // 参数校验必须**先于**设备访问:否则在没有驱动的机器上,
618
+ // 参数错误会被误报成"设备打不开",掩盖真正的问题。
619
+ if (size <= 0)
620
+ throw new RangeError(`分配大小必须为正数,收到 ${size}。`);
621
+ this.openDevice();
622
+ // MEM_COMMIT | MEM_RESERVE:与 CE 调用 VirtualAllocEx 时一致
623
+ const allocationType = BigInt(types_1.MEM_COMMIT | types_1.MEM_RESERVE);
624
+ const buffer = packAllocInput(handle.pid, preferred, BigInt(size), allocationType, BigInt(protect >>> 0));
625
+ // 关键:in 与 out 用**同一块**缓冲(驱动把结果写回 SystemBuffer 前 8 字节)
626
+ this.ioctl(exports.DBK_IOCTL.ALLOCATE_MEM, buffer, buffer.length, buffer);
627
+ const address = readAllocResult(buffer);
628
+ if (address === 0n) {
629
+ throw new Error(`驱动分配返回了 0(目标 pid ${handle.pid},请求 ${size} 字节,protect 0x${(protect >>> 0).toString(16)})。` +
630
+ '常见原因:目标进程已退出、请求过大、或保护属性组合非法。');
631
+ }
632
+ return address;
633
+ }
634
+ /**
635
+ * 改变内存保护属性。
636
+ *
637
+ * ## 为什么这里**不走驱动**
638
+ *
639
+ * CE 的驱动里确实有一个 `IOCTL_CE_MAKEWRITABLE`(功能号 `$0819`),但它
640
+ * **在 x64 驱动上是空的**(`DBKKernel/IOPLDispatcher.c:474-491`):
641
+ *
642
+ * ```c
643
+ * case IOCTL_CE_MAKEWRITABLE:
644
+ * #ifdef AMD64
645
+ * //untill I know how win64 handles paging, not implemented
646
+ * #else
647
+ * ... MakeWritable(...)
648
+ * #endif
649
+ * ```
650
+ *
651
+ * 所以 64 位 Windows 上发这个 ioctl 只会得到「无效请求」。
652
+ * 而且它的输入结构里**没有 processid** —— 它是按调用者(驱动自己所在的
653
+ * 进程上下文)的页表改 PTE 的,本来也不适合"改另一个进程的保护"。
654
+ *
655
+ * 因此本方法**委托 `SelfBackend`(用户态 `VirtualProtectEx`)**,并明确告知这一点。
656
+ * 好消息是:**内存写入不需要先改保护** —— 驱动的写路径本身就绕过了页保护,
657
+ * 只读页照样能写。需要 `changeProtection` 的场景主要是为了「把页改成可执行」
658
+ * 或「让目标自己也能写」,那属于目标进程自身的权限,用户态调用是否成功取决于
659
+ * 当前进程对目标的权限(受保护进程仍会失败)。
660
+ *
661
+ * @returns 旧保护值(用户态 `VirtualProtectEx` 的返回值)
662
+ */
663
+ changeProtection(handle, address, size, newProtect) {
664
+ return this.self.changeProtection(handle, address, size, newProtect);
665
+ }
666
+ /**
667
+ * 释放内存。
668
+ *
669
+ * 驱动**没有**提供释放 ioctl(`KernelFree` 释放的是驱动自己内核态分配的内存,
670
+ * 不是目标进程的)。因此这里委托用户态 `VirtualFreeEx`;
671
+ * 对受保护进程会失败 —— 那种情况下这块内存会一直留在目标进程里,
672
+ * 这是需要如实告诉调用方的限制。
673
+ */
674
+ free(handle, address) {
675
+ this.self.free(handle, address);
676
+ }
677
+ dispose() {
678
+ this.closeDevice();
679
+ this.self.dispose();
680
+ }
681
+ }
682
+ exports.DbkBackend = DbkBackend;
683
+ //# sourceMappingURL=dbk-backend.js.map