From 398e76dee6d3b333cd131fc367ac5b69951d8340 Mon Sep 17 00:00:00 2001 From: JoShMiQueL Date: Thu, 17 Apr 2025 23:42:35 +0200 Subject: [PATCH 01/11] chore(package): update package metadata and repository URLs --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d2251d1..4ff13ab 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "memoryjs", + "name": "@joshmiquel/memoryjs", "version": "3.5.1", "description": "Node add-on for memory reading and writing!", "main": "index.js", @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/Rob--/memoryjs.git" + "url": "git+https://github.com/joshmiquel/memoryjs.git" }, "keywords": [ "memory", @@ -25,13 +25,13 @@ "management", "addon" ], - "author": "Rob--", + "author": "JoShMiQueL", "license": "MIT", "gypfile": true, "bugs": { - "url": "https://github.com/Rob--/memoryjs/issues" + "url": "https://github.com/joshmiquel/memoryjs/issues" }, - "homepage": "https://github.com/Rob--/memoryjs#readme", + "homepage": "https://github.com/joshmiquel/memoryjs#readme", "dependencies": { "eslint": "^8.5.0", "eslint-config-airbnb-base": "^12.1.0", From 90dc192ee29223a9b48316c8c23bc5f65b85cb00 Mon Sep 17 00:00:00 2001 From: JoShMiQueL Date: Fri, 18 Apr 2025 01:12:37 +0200 Subject: [PATCH 02/11] refactor(core): use const char* for error messages Replaces char* with const char* for error message parameters and variables in debugger, module, and process components to improve type safety and clarify intent. --- lib/debugger.cc | 2 +- lib/dll.h | 4 ++-- lib/functions.h | 14 +++++++------- lib/memoryjs.cc | 33 +++++++++++++++++---------------- lib/module.cc | 8 ++++---- lib/module.h | 6 +++--- lib/process.cc | 6 +++--- lib/process.h | 6 +++--- 8 files changed, 40 insertions(+), 39 deletions(-) diff --git a/lib/debugger.cc b/lib/debugger.cc index fef5165..e979405 100644 --- a/lib/debugger.cc +++ b/lib/debugger.cc @@ -25,7 +25,7 @@ bool debugger::detatch(DWORD processId) { } bool debugger::setHardwareBreakpoint(DWORD processId, DWORD64 address, Register reg, int trigger, int size) { - char* errorMessage = ""; + const char* errorMessage = ""; std::vector threads = module::getThreads(0, &errorMessage); if (strcmp(errorMessage, "")) { diff --git a/lib/dll.h b/lib/dll.h index 11181bc..c52b5a6 100644 --- a/lib/dll.h +++ b/lib/dll.h @@ -9,7 +9,7 @@ #include namespace dll { - bool inject(HANDLE handle, std::string dllPath, char** errorMessage, LPDWORD moduleHandle) { + bool inject(HANDLE handle, std::string dllPath, const char** errorMessage, LPDWORD moduleHandle) { // allocate space in target process memory for DLL path LPVOID targetProcessPath = VirtualAllocEx(handle, NULL, dllPath.length() + 1, MEM_COMMIT, PAGE_EXECUTE_READWRITE); @@ -53,7 +53,7 @@ namespace dll { return *moduleHandle > 0; } - bool unload(HANDLE handle, char** errorMessage, HMODULE moduleHandle) { + bool unload(HANDLE handle, const char** errorMessage, HMODULE moduleHandle) { HMODULE kernel32 = LoadLibrary("kernel32"); if (kernel32 == 0) { diff --git a/lib/functions.h b/lib/functions.h index dea685b..72679cb 100644 --- a/lib/functions.h +++ b/lib/functions.h @@ -33,7 +33,7 @@ namespace functions { char readChar(HANDLE hProcess, DWORD64 address); template - Call call(HANDLE pHandle, std::vector args, Type returnType, DWORD64 address, char** errorMessage) { + Call call(HANDLE pHandle, std::vector args, Type returnType, DWORD64 address, const char** errorMessage) { std::vector argShellcode; std::reverse(args.begin(), args.end()); @@ -61,9 +61,9 @@ namespace functions { LPVOID address = functions::reserveString(pHandle, value.c_str(), value.length()); // Little endian representation - for (int i = 0; i < 4; i++) { - int shifted = ((int)address >> (i * 8)) & 0xFF; - argShellcode.push_back(shifted); + for (int i = 0; i < sizeof(LPVOID); i++) { + unsigned char byte = ((reinterpret_cast(address) >> (i * 8)) & 0xFF); + argShellcode.push_back(byte); } continue; @@ -112,9 +112,9 @@ namespace functions { callShellcode.push_back(0xA3); } - for (int i = 0; i < 4; i++) { - int shifted = ((DWORD)returnValuePointer >> (i * 8)) & 0xFF; - callShellcode.push_back(shifted); + for (int i = 0; i < sizeof(LPVOID); i++) { + unsigned char byte = ((reinterpret_cast(returnValuePointer) >> (i * 8)) & 0xFF); + callShellcode.push_back(byte); } } diff --git a/lib/memoryjs.cc b/lib/memoryjs.cc index 754906b..dd24913 100644 --- a/lib/memoryjs.cc +++ b/lib/memoryjs.cc @@ -48,7 +48,7 @@ Napi::Value openProcess(const Napi::CallbackInfo& args) { } // Define error message that may be set by the function that opens the process - char* errorMessage = ""; + const char* errorMessage = ""; process::Pair pair; @@ -125,7 +125,7 @@ Napi::Value getProcesses(const Napi::CallbackInfo& args) { } // Define error message that may be set by the function that gets the processes - char* errorMessage = ""; + const char* errorMessage = ""; std::vector processEntries = Process.getProcesses(&errorMessage); @@ -186,7 +186,7 @@ Napi::Value getModules(const Napi::CallbackInfo& args) { } // Define error message that may be set by the function that gets the modules - char* errorMessage = ""; + const char* errorMessage = ""; std::vector moduleEntries = module::getModules(args[0].As().Int32Value(), &errorMessage); @@ -251,7 +251,7 @@ Napi::Value findModule(const Napi::CallbackInfo& args) { std::string moduleName(args[0].As().Utf8Value()); // Define error message that may be set by the function that gets the modules - char* errorMessage = ""; + const char* errorMessage = ""; MODULEENTRY32 module = module::findModule(moduleName.c_str(), args[1].As().Int32Value(), &errorMessage); @@ -312,7 +312,7 @@ Napi::Value readMemory(const Napi::CallbackInfo& args) { const char* dataType = dataTypeArg.c_str(); // Define the error message that will be set if no data type is recognised - char* errorMessage = ""; + const char* errorMessage = ""; Napi::Value retVal = env.Null(); HANDLE handle = (HANDLE)args[0].As().Int64Value(); @@ -696,7 +696,7 @@ Napi::Value writeBuffer(const Napi::CallbackInfo& args) { // // matching address // uintptr_t address = 0; -// char* errorMessage = ""; +// const char* errorMessage = ""; // // read memory region occupied by the module to pattern match inside // std::vector moduleBytes = std::vector(baseSize); @@ -743,7 +743,7 @@ Napi::Value findPattern(const Napi::CallbackInfo& args) { // matching address uintptr_t address = 0; - char* errorMessage = ""; + const char* errorMessage = ""; std::vector modules = module::getModules(GetProcessId(handle), &errorMessage); Pattern.search(handle, modules, 0, pattern.c_str(), flags, patternOffset, &address); @@ -793,7 +793,7 @@ Napi::Value findPatternByModule(const Napi::CallbackInfo& args) { // matching address uintptr_t address = 0; - char* errorMessage = ""; + const char* errorMessage = ""; MODULEENTRY32 module = module::findModule(moduleName.c_str(), GetProcessId(handle), &errorMessage); @@ -854,7 +854,7 @@ Napi::Value findPatternByAddress(const Napi::CallbackInfo& args) { // matching address uintptr_t address = 0; - char* errorMessage = ""; + const char* errorMessage = ""; std::vector modules = module::getModules(GetProcessId(handle), &errorMessage); Pattern.search(handle, modules, baseAddress, pattern.c_str(), flags, patternOffset, &address); @@ -945,7 +945,7 @@ Napi::Value callFunction(const Napi::CallbackInfo& args) { address = args[3].As().Int64Value(); } - char* errorMessage = ""; + const char* errorMessage = ""; Call data = functions::call(handle, parsedArgs, returnType, address, &errorMessage); // Free all the memory we allocated @@ -1030,7 +1030,7 @@ Napi::Value virtualProtectEx(const Napi::CallbackInfo& args) { bool success = VirtualProtectEx(handle, (LPVOID) address, size, protection, &result); - char* errorMessage = ""; + const char* errorMessage = ""; if (success == 0) { errorMessage = "an error occurred calling VirtualProtectEx"; @@ -1135,7 +1135,7 @@ Napi::Value virtualQueryEx(const Napi::CallbackInfo& args) { MEMORY_BASIC_INFORMATION information; SIZE_T result = VirtualQueryEx(handle, (LPVOID)address, &information, sizeof(information)); - char* errorMessage = ""; + const char* errorMessage = ""; if (result == 0 || result != sizeof(information)) { errorMessage = "an error occurred calling VirtualQueryEx"; @@ -1202,7 +1202,7 @@ Napi::Value virtualAllocEx(const Napi::CallbackInfo& args) { LPVOID allocatedAddress = VirtualAllocEx(handle, address, size, allocationType, protection); - char* errorMessage = ""; + const char* errorMessage = ""; // If null, it means an error occurred if (allocatedAddress == NULL) { @@ -1402,7 +1402,7 @@ Napi::Value injectDll(const Napi::CallbackInfo& args) { std::string dllPath(args[1].As().Utf8Value()); Napi::Function callback = args[2].As(); - char* errorMessage = ""; + const char* errorMessage = ""; DWORD moduleHandle = -1; bool success = dll::inject(handle, dllPath, &errorMessage, &moduleHandle); @@ -1463,7 +1463,7 @@ Napi::Value unloadDll(const Napi::CallbackInfo& args) { // find module handle from name of DLL if (args[1].IsString()) { std::string moduleName(args[1].As().Utf8Value()); - char* errorMessage = ""; + const char* errorMessage = ""; MODULEENTRY32 module = module::findModule(moduleName.c_str(), GetProcessId(handle), &errorMessage); @@ -1480,7 +1480,7 @@ Napi::Value unloadDll(const Napi::CallbackInfo& args) { moduleHandle = (HMODULE) module.modBaseAddr; } - char* errorMessage = ""; + const char* errorMessage = ""; bool success = dll::unload(handle, &errorMessage, moduleHandle); if (strcmp(errorMessage, "") && args.Length() != 3) { @@ -1575,6 +1575,7 @@ std::string GetLastErrorToString() { Napi::Object init(Napi::Env env, Napi::Object exports) { exports.Set(Napi::String::New(env, "openProcess"), Napi::Function::New(env, openProcess)); + exports.Set(Napi::String::New(env, "closeHandle"), Napi::Function::New(env, closeHandle)); exports.Set(Napi::String::New(env, "getProcesses"), Napi::Function::New(env, getProcesses)); exports.Set(Napi::String::New(env, "getModules"), Napi::Function::New(env, getModules)); exports.Set(Napi::String::New(env, "findModule"), Napi::Function::New(env, findModule)); diff --git a/lib/module.cc b/lib/module.cc index 8d5b52e..6ec1f86 100644 --- a/lib/module.cc +++ b/lib/module.cc @@ -7,12 +7,12 @@ #include "memoryjs.h" DWORD64 module::getBaseAddress(const char* processName, DWORD processId) { - char* errorMessage = ""; + const char* errorMessage = ""; MODULEENTRY32 baseModule = module::findModule(processName, processId, &errorMessage); return (DWORD64)baseModule.modBaseAddr; } -MODULEENTRY32 module::findModule(const char* moduleName, DWORD processId, char** errorMessage) { +MODULEENTRY32 module::findModule(const char* moduleName, DWORD processId, const char** errorMessage) { MODULEENTRY32 module; bool found = false; @@ -36,7 +36,7 @@ MODULEENTRY32 module::findModule(const char* moduleName, DWORD processId, char** return module; } -std::vector module::getModules(DWORD processId, char** errorMessage) { +std::vector module::getModules(DWORD processId, const char** errorMessage) { // Take a snapshot of all modules inside a given process. HANDLE hModuleSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, processId); MODULEENTRY32 mEntry; @@ -67,7 +67,7 @@ std::vector module::getModules(DWORD processId, char** errorMessa return modules; } -std::vector module::getThreads(DWORD processId, char** errorMessage) { +std::vector module::getThreads(DWORD processId, const char** errorMessage) { HANDLE hThreadSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, processId); THREADENTRY32 mEntry; diff --git a/lib/module.h b/lib/module.h index d90ed46..b617e64 100644 --- a/lib/module.h +++ b/lib/module.h @@ -9,9 +9,9 @@ namespace module { DWORD64 getBaseAddress(const char* processName, DWORD processId); - MODULEENTRY32 findModule(const char* moduleName, DWORD processId, char** errorMessage); - std::vector getModules(DWORD processId, char** errorMessage); - std::vector getThreads(DWORD processId, char** errorMessage); + MODULEENTRY32 findModule(const char* moduleName, DWORD processId, const char** errorMessage); + std::vector getModules(DWORD processId, const char** errorMessage); + std::vector getThreads(DWORD processId, const char** errorMessage); }; #endif diff --git a/lib/process.cc b/lib/process.cc index b493d99..51afba7 100644 --- a/lib/process.cc +++ b/lib/process.cc @@ -12,7 +12,7 @@ using v8::Exception; using v8::Isolate; using v8::String; -process::Pair process::openProcess(const char* processName, char** errorMessage){ +process::Pair process::openProcess(const char* processName, const char** errorMessage){ PROCESSENTRY32 process; HANDLE handle = NULL; @@ -38,7 +38,7 @@ process::Pair process::openProcess(const char* processName, char** errorMessage) }; } -process::Pair process::openProcess(DWORD processId, char** errorMessage) { +process::Pair process::openProcess(DWORD processId, const char** errorMessage) { PROCESSENTRY32 process; HANDLE handle = NULL; @@ -64,7 +64,7 @@ process::Pair process::openProcess(DWORD processId, char** errorMessage) { }; } -std::vector process::getProcesses(char** errorMessage) { +std::vector process::getProcesses(const char** errorMessage) { // Take a snapshot of all processes. HANDLE hProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); PROCESSENTRY32 pEntry; diff --git a/lib/process.h b/lib/process.h index 31a241e..a0b8196 100644 --- a/lib/process.h +++ b/lib/process.h @@ -17,10 +17,10 @@ class process { process(); ~process(); - Pair openProcess(const char* processName, char** errorMessage); - Pair openProcess(DWORD processId, char** errorMessage); + Pair openProcess(const char* processName, const char** errorMessage); + Pair openProcess(DWORD processId, const char** errorMessage); void closeProcess(HANDLE hProcess); - std::vector getProcesses(char** errorMessage); + std::vector getProcesses(const char** errorMessage); }; #endif From 0741b045b785e229afb95ffb85ed73786eca07ab Mon Sep 17 00:00:00 2001 From: JoShMiQueL Date: Fri, 18 Apr 2025 19:47:53 +0200 Subject: [PATCH 03/11] chore: remove legacy C++ bindings and JavaScript code --- .eslintrc.js | 11 - assets/logo.png | Bin 21327 -> 0 bytes examples/buffers.js | 78 -- examples/debugging.js | 92 -- examples/general.js | 104 -- examples/vectors.js | 47 - index.js | 362 ------ lib/debugger.cc | 169 --- lib/debugger.h | 79 -- lib/dll.h | 83 -- lib/functions.cc | 17 - lib/functions.h | 209 ---- lib/memory.cc | 21 - lib/memory.h | 94 -- lib/memoryjs.cc | 1607 --------------------------- lib/memoryjs.h | 15 - lib/module.cc | 94 -- lib/module.h | 18 - lib/pattern.cc | 103 -- lib/pattern.h | 31 - lib/process.cc | 95 -- lib/process.h | 27 - scripts/debug.js | 35 - scripts/install.js | 35 - src/consts.js | 138 --- src/debugger.js | 150 --- src/utils.js | 75 -- test/allocationTest.js | 16 - test/debuggerTest.js | 44 - test/functionTest.js | 20 - test/memoryTest.js | 68 -- test/project.sln | 37 - test/protectionTest.js | 12 - test/queryTest.js | 55 - test/src/MemoryTest.cpp | 42 - test/src/functionTest.cpp | 17 - test/src/protectionTest.cpp | 71 -- test/vcxproj/FunctionTest.vcxproj | 48 - test/vcxproj/MemoryTest.vcxproj | 48 - test/vcxproj/ProtectionTest.vcxproj | 48 - 40 files changed, 4315 deletions(-) delete mode 100644 .eslintrc.js delete mode 100644 assets/logo.png delete mode 100644 examples/buffers.js delete mode 100644 examples/debugging.js delete mode 100644 examples/general.js delete mode 100644 examples/vectors.js delete mode 100644 index.js delete mode 100644 lib/debugger.cc delete mode 100644 lib/debugger.h delete mode 100644 lib/dll.h delete mode 100644 lib/functions.cc delete mode 100644 lib/functions.h delete mode 100644 lib/memory.cc delete mode 100644 lib/memory.h delete mode 100644 lib/memoryjs.cc delete mode 100644 lib/memoryjs.h delete mode 100644 lib/module.cc delete mode 100644 lib/module.h delete mode 100644 lib/pattern.cc delete mode 100644 lib/pattern.h delete mode 100644 lib/process.cc delete mode 100644 lib/process.h delete mode 100644 scripts/debug.js delete mode 100644 scripts/install.js delete mode 100644 src/consts.js delete mode 100644 src/debugger.js delete mode 100644 src/utils.js delete mode 100644 test/allocationTest.js delete mode 100644 test/debuggerTest.js delete mode 100644 test/functionTest.js delete mode 100644 test/memoryTest.js delete mode 100644 test/project.sln delete mode 100644 test/protectionTest.js delete mode 100644 test/queryTest.js delete mode 100644 test/src/MemoryTest.cpp delete mode 100644 test/src/functionTest.cpp delete mode 100644 test/src/protectionTest.cpp delete mode 100644 test/vcxproj/FunctionTest.vcxproj delete mode 100644 test/vcxproj/MemoryTest.vcxproj delete mode 100644 test/vcxproj/ProtectionTest.vcxproj diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 4622ce8..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - root: true, - parserOptions: { - sourceType: 'module' - }, - extends: 'airbnb-base', - rules: { - 'linebreak-style': 0, - 'import/no-unresolved': 0, - }, -}; diff --git a/assets/logo.png b/assets/logo.png deleted file mode 100644 index a8d3330b459645d6f58fe61db085394c06b274ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21327 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 MHZ(V=)OWx4UrFbWe*gdg diff --git a/examples/buffers.js b/examples/buffers.js deleted file mode 100644 index f1b447c..0000000 --- a/examples/buffers.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * This example uses the following structs defined in C++: - struct vector { - float x, y, z; - }; - - struct player { - vector position; - double health; - std::string name; - float distance; - bool alive; - }; -*/ - -// https://github.com/LordVonAdel/structron -const Struct = require('structron'); -const memoryjs = require('../index'); - -const processObject = memoryjs.openProcess('Testing.exe'); -const structAddress = 0x000000DEADBEEF; - -// -- Step 1: define the structures - -// Custom double consumer/producer (since it's not yet implemented in `structron`) -const double = { - read(buffer, offset) { - return buffer.readDoubleLE(offset); - }, - write(value, context, offset) { - context.buffer.writeDoubleLE(value, offset); - }, - SIZE: 8, -}; - -// Use string consumer/producer provided by the library (custom implementation for `std::string`), -// pass process handle and base address of structure so the library can read/write the string, -// also requires passing the platform architecture to determine the structure of `std::string` -const string = memoryjs.STRUCTRON_TYPE_STRING(processObject.handle, structAddress, '64'); - -// Define vector structure -const Vector = new Struct() - .addMember(Struct.TYPES.FLOAT, 'x') // 4 bytes - .addMember(Struct.TYPES.FLOAT, 'y') // 4 bytes - .addMember(Struct.TYPES.FLOAT, 'z'); // 4 bytes - -// Define player structure -const Player = new Struct() - .addMember(Vector, 'position') // 12 bytes - .addMember(Struct.TYPES.SKIP(4), 'unused') // compiler padding to put member on 8 byte boundary - .addMember(double, 'health') // 8 bytes - .addMember(string, 'name') // 32 bytes (in 64bit process, 24 bytes in 32bit process) - .addMember(Struct.TYPES.FLOAT, 'distance') // 4 bytes - .addMember(Struct.TYPES.BYTE, 'alive'); // 1 byte - -// -- Step 2: create object to write to memory -const object = { - position: { - x: 1.23, y: 4.56, z: 7.89, - }, - health: 80.12, - name: 'Example Name 1234567890', - distance: 4.20, - alive: false, -}; - -// -- Step 3: create buffer from object and write to memory -let context = Player.write(object); -memoryjs.writeBuffer(processObject.handle, structAddress, context.buffer); - -// -- Step 4: read buffer from memory and parse -const buffer = memoryjs.readBuffer(processObject.handle, structAddress, context.buffer.length); - -context = Player.readContext(buffer); - -if (!context.hasErrors()) { - console.log(context.data); -} diff --git a/examples/debugging.js b/examples/debugging.js deleted file mode 100644 index 225e446..0000000 --- a/examples/debugging.js +++ /dev/null @@ -1,92 +0,0 @@ -const memoryjs = require('../index'); -const processName = 'Testing Things.exe'; - -const processObject = memoryjs.openProcess(processName); -const processId = processObject.th32ProcessID; - -// Address of variable -const address = 0xEFFBF0; - -// When should we breakpoint? On read, write or execute -const trigger = memoryjs.TRIGGER_ACCESS; -const dataType = memoryjs.INT; - -// Whether to end the process once debugging has finished -const killOnDetatch = false; - -/** - * Example 1: Using the `Debugger` wrapper class. - * The library contanis a wrapper class for hardware debugging. - * It works by simply registering a hardware breakpoint and - * then listening for all debug events that are emitted when - * a breakpoint occurs. - */ - -const hardwareDebugger = memoryjs.Debugger; - -// Attach the debugger to the process -hardwareDebugger.attach(processId, killOnDetatch); - -const registerUsed = hardwareDebugger.setHardwareBreakpoint(processId, address, trigger, dataType); - -// `debugEvent` event emission catches debug events from all registers -hardwareDebugger.on('debugEvent', ({ register, event }) => { - console.log(`Hardware Register ${register} breakpoint`); - console.log(event); -}); - -// You can listen to debug events from specific hardware registers -// by listening to whatever register was returned from `setHardwareBreakpoint` -hardwareDebugger.on(registerUsed, (event) => { - console.log(event); -}); - -// Don't forget to call `hardwareDebugger.detatch()` when you're done! - -/** - * Example 2: Manually using the exposed functions - * There are a few steps involved when not using the wrapper: - * - * 1. Attatch the debugger - * 2. Set your hardware breakpoints by manually referencing - * which register you want to set. It's important you keep - * track of which hardware registers you use as there are only 4 - * meaning only 4 breakpoints can be set. - * You also need to manually reference the size of the data type. - * 3. Constantly call `awaitDebugEvent` to wait for debug events - * 4. When a debug event occurs, call `handleDebugEvent` - * 5. Don't forget to detatch the debugger via `memoryjs.detatch(processId)` - */ - -memoryjs.attachDebugger(processId, killOnDetatch); - -// There are 4 hardware registers: -// `memoryjs.DR0` through `memoryjs.DR3` -const registerToUse = memoryjs.DR0; - -// Our `address` references an integer variable. An integer -// is 4 bytes therefore we pass `4` to the `size` parameter. -const size = 4; -memoryjs.setHardwareBreakpoint(processId, address, registerToUse, trigger, size); - -// How long to wait for the debug event before timing out -const timeout = 100; - -// The interval duration must be the same or larger than the `timeout` value. -// `awaitDebugEvent` works by waiting a certain amount of time before timing out, -// therefore we only want to call the method again when we're sure the previous -// call has already timed out. -setInterval(() => { - // `debugEvent` can be null if no event occurred - const debugEvent = memoryjs.awaitDebugEvent(registerToUse, timeout); - - // If a breakpoint occurred, handle it - if (debugEvent) { - memoryjs.handleDebugEvent(debugEvent.processId, debugEvent.threadId); - } -}, timeout); - -// Don't forget to detatch the debugger! -// memoryjs.detatchDebugger(processId); - -memoryjs.closeProcess(processObject.handle); diff --git a/examples/general.js b/examples/general.js deleted file mode 100644 index 0b1e5e2..0000000 --- a/examples/general.js +++ /dev/null @@ -1,104 +0,0 @@ -const memoryjs = require('./index'); -const processName = 'csgo.exe'; -let clientModule; -const offset = 0x00A9D44C; - -// open a process (sync) -const processObject = memoryjs.openProcess(processName); - -// open a process (async) -memoryjs.openProcess(processName, (error, processObject) => { - console.log(JSON.stringify(processObject, null, 3)); - - if (process.szExeFile) { - console.log('Successfully opened handle on', processName); - - memoryjs.closeProcess(processObject.handle); - console.log('Closed handle on', processName); - } else { - console.log('Unable to open handle on', processName); - } -}); - -// get all processes (sync) -const processes = memoryjs.getProcesses(); -console.log('\nGetting all processes (sync)\n---\n'); -processes.forEach(({ szExeFile }) => console.log(szExeFile)); - -// get all processes (async) -console.log('\nGetting all processes (async)\n---\n'); -memoryjs.getProcesses((error, processes) => { - processes.forEach(({ szExeFile }) => console.log(szExeFile)); -}); - -/* process = -{ cntThreads: 47, - szExeFile: "csgo.exe", - th32ProcessID: 10316, - th32ParentProcessID: 7804, - pcPriClassBase: 8 } */ - -// get all modules (sync) -console.log('\nGetting all modules (sync)\n---\n'); -const modules = memoryjs.getModules(processObject.th32ProcessID); -modules.forEach(({ szExeFile }) => console.log(szExeFile)); - -// get all modules (async) -console.log('\nGetting all modules (async)\n---\n'); -memoryjs.getModules(processObject.th32ProcessID, (error, modules) => { - modules.forEach(({ szExeFile }) => console.log(szExeFile)); -}); - -// find a module associated with a process (sync) -console.log('\nFinding module "client.dll" (sync)\n---\n'); -console.log(memoryjs.findModule('client.dll', processObject.th32ProcessID)); - -// find a module associated with a process (async) -console.log('\nFinding module "client.dll" (async)\n---\n'); -memoryjs.findModule('client.dll', processObject.th32ProcessID, (error, module) => { - console.log(module.szModule); - clientModule = module; -}); - -/* module = -{ modBaseAddr: 468123648, - modBaseSize: 80302080, - szExePath: 'c:\\program files (x86)\\steam\\steamapps\\common\\counter-strike global offensive\\csgo\\bin\\client.dll', - szModule: 'client.dll', - th32ProcessID: 10316 } */ - -const address = clientModule.modBaseAddr + offset; - -// read memory (sync) -console.log(`value of 0x${address.toString(16)}: ${memoryjs.readMemory(processObject.handle, address, memoryjs.INT)}`); - -// read memory (async) -memoryjs.readMemory(processObject.handle, address, memoryjs.INT, (error, result) => { - console.log(`value of 0x${address.toString(16)}: ${result}`); -}); - -// write memory -memoryjs.writeMemory(processObject.handle, address, 1, memoryjs.INT); - -// pattern reading -const signature = 'A3 ? ? ? ? C7 05 ? ? ? ? ? ? ? ? E8 ? ? ? ? 59 C3 6A'; -const signatureTypes = memoryjs.READ | memoryjs.SUBTRACT; -const patternOffset = 0x1; -const addressOffset = 0x10; -const dwLocalPlayer = memoryjs.findPattern(processObject.handle, clientModule.szModule, signature, signatureTypes, patternOffset, addressOffset); -console.log(`value of dwLocalPlayer: 0x${dwLocalPlayer.toString(16)}`); - -// inject dll -memoryjs.injectDll(processObject.handle, 'C:\\TestDLL.dll', (error, success) => { - console.log(`injected TestDLL.dll into process: ${success} ${error}`); -}); - -// unload dll (by name) -memoryjs.unloadDll(processObject.handle, 'TestDLL.dll', (error, success) => { - console.log(`unloaded TestDLL.dll from process: ${success} ${error}`); -}); - -// unload dll (by module base address) -// const testDll = memoryjs.findModule('TestDLL.dll', processObject.th32ProcessID); -// const success = memoryjs.unloadDll(processObject.handle, testDll.modBaseAddr); -// console.log(`unloaded TestDLL.dll from process: ${success}`); diff --git a/examples/vectors.js b/examples/vectors.js deleted file mode 100644 index 7e1dc11..0000000 --- a/examples/vectors.js +++ /dev/null @@ -1,47 +0,0 @@ -const memoryjs = require('./index'); -const processName = 'Test Project.exe'; - -const processObject = memoryjs.openProcess(processName); -console.log(JSON.stringify(processObject, null, 3)); - -const modules = memoryjs.getModules(processObject.th32ProcessID); -modules.forEach(module => console.log(module)); - -/* How to read a "string" - * C++ code: - * ``` - * std::string str = "this is a sample string"; - * std::cout << "Address: " << (DWORD) str.c_str() << ", value: " << str.c_str() << std::endl; - * ``` - * This will create a string and the address of the string and the value of the string. - */ - -const str = memoryjs.readMemory(0x69085bc0, memoryjs.STRING); -console.log(str); // "this is a sample string"; - -/* How to read and write vectors - * C++ code: - * ``` - * struct Vector3 { float x, y, z; }; - * struct Vector4 { float w, x, y, z; }; - * Vector3 vec3 = { 1.0f, 2.0f, 3.0f }; - * Vector4 vec4 = { 1.0f, 2.0f, 3.0f, 4.0f }; - * std::cout << "[Vec3] Address: " << &vec3 << std::endl; - * std::cout << "[Vec4] Address: " << &vec4 << std::endl; - */ - -let vec3 = { - x: 0, y: 0, z: 0, -}; -memoryjs.writeMemory(0x000001, vec3, memoryjs.VEC3); -vec3 = memoryjs.readMemory(0x000001, memoryjs.VEC3); // { x, y, z } -console.log(vec3); - -let vec4 = { - w: 0, x: 0, y: 0, z: 0, -}; -memoryjs.writeMemory(0x000002, vec4, memoryjs.VEC4); -vec4 = memoryjs.readMemory(0x000002, memoryjs.VEC4); // { w, x, y, z } -console.log(vec4); - -memoryjs.closeProcess(processObject.handle); diff --git a/index.js b/index.js deleted file mode 100644 index 4aeda2f..0000000 --- a/index.js +++ /dev/null @@ -1,362 +0,0 @@ -const fs = require('fs'); -const memoryjs = require('./build/Release/memoryjs'); -const Debugger = require('./src/debugger'); -const constants = require('./src/consts'); -const { STRUCTRON_TYPE_STRING } = require('./src/utils'); - -/* TODO: - * - remove callbacks from all functions and implement promise support using Napi - * - validate argument types in JS space instead of C++ - * - refactor read/write memory functions to use buffers instead? - * - remove `closeProcess` in favour of `closeHandle` - */ - -function openProcess(processIdentifier, callback) { - if (arguments.length === 1) { - return memoryjs.openProcess(processIdentifier); - } - - return memoryjs.openProcess(processIdentifier, callback); -} - -function closeProcess(handle) { - return memoryjs.closeHandle(handle); -} - -function getProcesses(callback) { - if (arguments.length === 0) { - return memoryjs.getProcesses(); - } - - return memoryjs.getProcesses(callback); -} - -function findModule(moduleName, processId, callback) { - if (arguments.length === 2) { - return memoryjs.findModule(moduleName, processId); - } - - return memoryjs.findModule(moduleName, processId, callback); -} - -function getModules(processId, callback) { - if (arguments.length === 1) { - return memoryjs.getModules(processId); - } - - return memoryjs.getModules(processId, callback); -} - -function readMemory(handle, address, dataType, callback) { - if (dataType.toLowerCase().endsWith('_be')) { - return readMemoryBE(handle, address, dataType, callback); - } - - if (arguments.length === 3) { - return memoryjs.readMemory(handle, address, dataType.toLowerCase()); - } - - return memoryjs.readMemory(handle, address, dataType.toLowerCase(), callback); -} - -function readMemoryBE(handle, address, dataType, callback) { - let value = null; - - switch (dataType) { - case constants.INT64_BE: - value = readBuffer(handle, address, 8).readBigInt64BE(); - break; - - case constants.UINT64_BE: - value = readBuffer(handle, address, 8).readBigUInt64BE(); - break; - - case constants.INT32_BE: - case constants.INT_BE: - case constants.LONG_BE: - value = readBuffer(handle, address, 4).readInt32BE(); - break; - - case constants.UINT32_BE: - case constants.UINT_BE: - case constants.ULONG_BE: - value = readBuffer(handle, address, 4).readUInt32BE(); - break; - - case constants.INT16_BE: - case constants.SHORT_BE: - value = readBuffer(handle, address, 2).readInt16BE(); - break; - - case constants.UINT16_BE: - case constants.USHORT_BE: - value = readBuffer(handle, address, 2).readUInt16BE(); - break; - - case constants.FLOAT_BE: - value = readBuffer(handle, address, 4).readFloatBE(); - break; - - case constants.DOUBLE_BE: - value = readBuffer(handle, address, 8).readDoubleBE(); - break; - } - - if (typeof callback !== 'function') { - if (value === null) { - throw new Error('Invalid data type argument!'); - } - - return value; - } - - callback(value === null ? 'Invalid data type argument!' : '', value); -} - -function readBuffer(handle, address, size, callback) { - if (arguments.length === 3) { - return memoryjs.readBuffer(handle, address, size); - } - - return memoryjs.readBuffer(handle, address, size, callback); -} - -function writeMemory(handle, address, value, dataType) { - let dataValue = value; - if (dataType === constants.STR || dataType === constants.STRING) { - dataValue += '\0'; // add terminator - } - - const bigintTypes = [constants.INT64, constants.INT64_BE, constants.UINT64, constants.UINT64_BE]; - if (bigintTypes.indexOf(dataType) != -1 && typeof value !== 'bigint') { - throw new Error(`${dataType.toUpperCase()} expects type BigInt`); - } - - if (dataType.endsWith('_be')) { - return writeMemoryBE(handle, address, dataValue, dataType); - } - - return memoryjs.writeMemory(handle, address, dataValue, dataType.toLowerCase()); -} - -function writeMemoryBE(handle, address, value, dataType) { - let buffer = null; - - switch (dataType) { - case constants.INT64_BE: - if (typeof value !== 'bigint') { - throw new Error('INT64_BE expects type BigInt'); - } - buffer = Buffer.alloc(8); - buffer.writeBigInt64BE(value); - break; - - case constants.UINT64_BE: - if (typeof value !== 'bigint') { - throw new Error('UINT64_BE expects type BigInt'); - } - buffer = Buffer.alloc(8); - buffer.writeBigUInt64BE(value); - break; - - case constants.INT32_BE: - case constants.INT_BE: - case constants.LONG_BE: - buffer = Buffer.alloc(4); - buffer.writeInt32BE(value); - break; - - case constants.UINT32_BE: - case constants.UINT_BE: - case constants.ULONG_BE: - buffer = Buffer.alloc(4); - buffer.writeUInt32BE(value); - break; - - case constants.INT16_BE: - case constants.SHORT_BE: - buffer = Buffer.alloc(2); - buffer.writeInt16BE(value); - break; - - case constants.UINT16_BE: - case constants.USHORT_BE: - buffer = Buffer.alloc(2); - buffer.writeUInt16BE(value); - break; - - case constants.FLOAT_BE: - buffer = Buffer.alloc(4); - buffer.writeFloatBE(value); - break; - - case constants.DOUBLE_BE: - buffer = Buffer.alloc(8); - buffer.writeDoubleBE(value); - break; - } - - if (buffer == null) { - throw new Error('Invalid data type argument!'); - } - - writeBuffer(handle, address, buffer); -} - -function writeBuffer(handle, address, buffer) { - return memoryjs.writeBuffer(handle, address, buffer); -} - -function findPattern() { - const findPattern = ['number', 'string', 'number', 'number'].toString(); - const findPatternByModule = ['number', 'string', 'string', 'number', 'number'].toString(); - const findPatternByAddress = ['number', 'number', 'string', 'number', 'number'].toString(); - - const args = Array.from(arguments).map(arg => typeof arg); - - if (args.slice(0, 4).toString() === findPattern) { - if (args.length === 4 || (args.length === 5 && args[4] === 'function')) { - return memoryjs.findPattern(...arguments); - } - } - - if (args.slice(0, 5).toString() === findPatternByModule) { - if (args.length === 5 || (args.length === 6 && args[5] === 'function')) { - return memoryjs.findPatternByModule(...arguments); - } - } - - if (args.slice(0, 5).toString() === findPatternByAddress) { - if (args.length === 5 || (args.length === 6 && args[5] === 'function')) { - return memoryjs.findPatternByAddress(...arguments); - } - } - - throw new Error('invalid arguments!'); -} - -function callFunction(handle, args, returnType, address, callback) { - if (arguments.length === 4) { - return memoryjs.callFunction(handle, args, returnType, address); - } - - return memoryjs.callFunction(handle, args, returnType, address, callback); -} - -function virtualAllocEx(handle, address, size, allocationType, protection, callback) { - if (arguments.length === 5) { - return memoryjs.virtualAllocEx(handle, address, size, allocationType, protection); - } - - return memoryjs.virtualAllocEx(handle, address, size, allocationType, protection, callback); -} - -function virtualProtectEx(handle, address, size, protection, callback) { - if (arguments.length === 4) { - return memoryjs.virtualProtectEx(handle, address, size, protection); - } - - return memoryjs.virtualProtectEx(handle, address, size, protection, callback); -} - -function getRegions(handle, getOffsets, callback) { - if (arguments.length === 1) { - return memoryjs.getRegions(handle); - } - - return memoryjs.getRegions(handle, callback); -} - -function virtualQueryEx(handle, address, callback) { - if (arguments.length === 2) { - return memoryjs.virtualQueryEx(handle, address); - } - - return memoryjs.virtualQueryEx(handle, address, callback); -} - -function injectDll(handle, dllPath, callback) { - if (!dllPath.endsWith('.dll')) { - throw new Error("Given path is invalid: file is not of type 'dll'."); - } - - if (!fs.existsSync(dllPath)) { - throw new Error('Given path is invaild: file does not exist.'); - } - - if (arguments.length === 2) { - return memoryjs.injectDll(handle, dllPath); - } - - return memoryjs.injectDll(handle, dllPath, callback); -} - -function unloadDll(handle, module, callback) { - if (arguments.length === 2) { - return memoryjs.unloadDll(handle, module); - } - - return memoryjs.unloadDll(handle, module, callback); -} - -function openFileMapping(fileName) { - if (arguments.length !== 1 || typeof fileName !== 'string') { - throw new Error('invalid arguments!'); - } - - return memoryjs.openFileMapping(fileName); -} - -function mapViewOfFile(processHandle, fileHandle, offset, viewSize, pageProtection) { - const validArgs = [ - ['number', 'number'], - ['number', 'number', 'number', 'number', 'number'], - ['number', 'number', 'bigint', 'bigint', 'number'] - ]; - const receivedArgs = Array.from(arguments).map(arg => typeof arg); - - if (!validArgs.some(args => args.join(",") == receivedArgs.join(","))) { - throw new Error('invalid arguments!'); - } - - if (arguments.length == 2) { - return memoryjs.mapViewOfFile(processHandle, fileHandle, 0, 0, constants.PAGE_READONLY); - } - - return memoryjs.mapViewOfFile(processHandle, fileHandle, offset, viewSize, pageProtection); -} - -const library = { - openProcess, - closeProcess, - getProcesses, - findModule, - getModules, - readMemory, - readBuffer, - writeMemory, - writeBuffer, - findPattern, - callFunction, - virtualAllocEx, - virtualProtectEx, - getRegions, - virtualQueryEx, - injectDll, - unloadDll, - openFileMapping, - mapViewOfFile, - attachDebugger: memoryjs.attachDebugger, - detachDebugger: memoryjs.detachDebugger, - awaitDebugEvent: memoryjs.awaitDebugEvent, - handleDebugEvent: memoryjs.handleDebugEvent, - setHardwareBreakpoint: memoryjs.setHardwareBreakpoint, - removeHardwareBreakpoint: memoryjs.removeHardwareBreakpoint, - Debugger: new Debugger(memoryjs), -}; - -module.exports = { - ...constants, - ...library, - STRUCTRON_TYPE_STRING: STRUCTRON_TYPE_STRING(library), -}; diff --git a/lib/debugger.cc b/lib/debugger.cc deleted file mode 100644 index e979405..0000000 --- a/lib/debugger.cc +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Hardware debugger for memory.js - * A lot of the hardware debugging code is based on ReClass.NET - * https://github.com/ReClassNET/ReClass.NET - */ - -#include -#include -#include -#include -#include "debugger.h" -#include "module.h" - -bool debugger::attach(DWORD processId, bool killOnDetatch) { - if (DebugActiveProcess(processId) == 0) { - return false; - } - - DebugSetProcessKillOnExit(killOnDetatch); - return true; -} - -bool debugger::detatch(DWORD processId) { - return DebugActiveProcessStop(processId) != 0; -} - -bool debugger::setHardwareBreakpoint(DWORD processId, DWORD64 address, Register reg, int trigger, int size) { - const char* errorMessage = ""; - std::vector threads = module::getThreads(0, &errorMessage); - - if (strcmp(errorMessage, "")) { - return false; - } - - for (std::vector::size_type i = 0; i != threads.size(); i++) { - if (threads[i].th32OwnerProcessID != processId) { - continue; - } - - HANDLE threadHandle = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, false, threads[i].th32ThreadID); - - if (threadHandle == 0) { - continue; - } - - SuspendThread(threadHandle); - - CONTEXT context = { 0 }; - context.ContextFlags = CONTEXT_DEBUG_REGISTERS; - GetThreadContext(threadHandle, &context); - - DebugRegister7 dr7; - dr7.Value = context.Dr7; - - if (reg == Register::DR0) { - context.Dr0 = address; - dr7.G0 = true; - dr7.RW0 = trigger; - dr7.Len0 = size; - } - - if (reg == Register::DR1) { - context.Dr1 = address; - dr7.G1 = true; - dr7.RW1 = trigger; - dr7.Len1 = size; - } - - if (reg == Register::DR2) { - context.Dr2 = address; - dr7.G2 = true; - dr7.RW2 = trigger; - dr7.Len2 = size; - } - - if (reg == Register::DR3) { - context.Dr3 = address; - dr7.G3 = true; - dr7.RW3 = trigger; - dr7.Len3 = size; - } - - context.Dr7 = dr7.Value; - - SetThreadContext(threadHandle, &context); - ResumeThread(threadHandle); - CloseHandle(threadHandle); - - return true; - } - - return false; -} - -bool debugger::awaitDebugEvent(DWORD millisTimeout, DebugEvent *info) { - DEBUG_EVENT debugEvent = {}; - - if (WaitForDebugEvent(&debugEvent, millisTimeout) == 0) { - return false; - } - - if (debugEvent.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT) { - CloseHandle(debugEvent.u.CreateProcessInfo.hFile); - } - - if (debugEvent.dwDebugEventCode == LOAD_DLL_DEBUG_EVENT) { - CloseHandle(debugEvent.u.LoadDll.hFile); - } - - if (debugEvent.dwDebugEventCode == EXCEPTION_DEBUG_EVENT) { - EXCEPTION_DEBUG_INFO exception = debugEvent.u.Exception; - - info->processId = debugEvent.dwProcessId; - info->threadId = debugEvent.dwThreadId; - info->exceptionAddress = exception.ExceptionRecord.ExceptionAddress; - info->exceptionCode = exception.ExceptionRecord.ExceptionCode; - info->exceptionFlags = exception.ExceptionRecord.ExceptionFlags; - - HANDLE handle = OpenThread(THREAD_GET_CONTEXT, false, debugEvent.dwThreadId); - - if (handle == 0) { - return false; - } - - CONTEXT context = {}; - context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_DEBUG_REGISTERS; - GetThreadContext(handle, &context); - - DebugRegister6 dr6; - dr6.Value = context.Dr6; - - if (dr6.DR0) { - info->hardwareRegister = Register::DR0; - } - - if (dr6.DR1) { - info->hardwareRegister = Register::DR1; - } - - if (dr6.DR2) { - info->hardwareRegister = Register::DR2; - } - - if (dr6.DR3) { - info->hardwareRegister = Register::DR3; - } - - if (!dr6.DR0 && !dr6.DR1 && !dr6.DR2 && !dr6.DR3) { - info->hardwareRegister = Register::Invalid; - } - - CloseHandle(handle); - } else { - ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE); - } - - return true; -} - -bool debugger::handleDebugEvent(DWORD processId, DWORD threadId) { - return ContinueDebugEvent(processId, threadId, DBG_CONTINUE); - // if (status == DebugContinueStatus::Handled) { - // return ContinueDebugEvent(processId, threadId, DBG_CONTINUE) != 0; - // } - - // if (status == DebugContinueStatus::NotHandled) { - // return ContinueDebugEvent(processId, threadId, DBG_EXCEPTION_NOT_HANDLED) != 0; - // } -} \ No newline at end of file diff --git a/lib/debugger.h b/lib/debugger.h deleted file mode 100644 index 9679c6e..0000000 --- a/lib/debugger.h +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once -#ifndef debugger_H -#define debugger_H -#define WIN32_LEAN_AND_MEAN - -#include -#include -#include - -enum class Register { - Invalid = -0x1, - DR0 = 0x0, - DR1 = 0x1, - DR2 = 0x2, - DR3 = 0x3 -}; - -struct DebugRegister6 { - union { - uintptr_t Value; - struct { - unsigned DR0 : 1; - unsigned DR1 : 1; - unsigned DR2 : 1; - unsigned DR3 : 1; - unsigned Reserved : 9; - unsigned BD : 1; - unsigned BS : 1; - unsigned BT : 1; - }; - }; -}; - -struct DebugRegister7 { - union { - uintptr_t Value; - struct { - unsigned G0 : 1; - unsigned L0 : 1; - unsigned G1 : 1; - unsigned L1 : 1; - unsigned G2 : 1; - unsigned L2 : 1; - unsigned G3 : 1; - unsigned L3 : 1; - unsigned GE : 1; - unsigned LE : 1; - unsigned Reserved : 6; - unsigned RW0 : 2; - unsigned Len0 : 2; - unsigned RW1 : 2; - unsigned Len1 : 2; - unsigned RW2 : 2; - unsigned Len2 : 2; - unsigned RW3 : 2; - unsigned Len3 : 2; - }; - }; -}; - -struct DebugEvent { - DWORD processId; - DWORD threadId; - DWORD exceptionCode; - DWORD exceptionFlags; - void* exceptionAddress; - Register hardwareRegister; -}; - -namespace debugger { - bool attach(DWORD processId, bool killOnDetatch); - bool detatch(DWORD processId); - bool setHardwareBreakpoint(DWORD processId, DWORD64 address, Register reg, int trigger, int size); - bool awaitDebugEvent(DWORD millisTimeout, DebugEvent *info); - bool handleDebugEvent(DWORD processId, DWORD threadId); -} - -#endif -#pragma once diff --git a/lib/dll.h b/lib/dll.h deleted file mode 100644 index c52b5a6..0000000 --- a/lib/dll.h +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once -#ifndef DLL_H -#define DLL_H -#define WIN32_LEAN_AND_MEAN - -#include -#include -#include -#include - -namespace dll { - bool inject(HANDLE handle, std::string dllPath, const char** errorMessage, LPDWORD moduleHandle) { - // allocate space in target process memory for DLL path - LPVOID targetProcessPath = VirtualAllocEx(handle, NULL, dllPath.length() + 1, MEM_COMMIT, PAGE_EXECUTE_READWRITE); - - if (targetProcessPath == NULL) { - *errorMessage = "unable to allocate memory in target process"; - return false; - } - - // write DLL path to reserved memory space - if (WriteProcessMemory(handle, targetProcessPath, dllPath.c_str(), dllPath.length() + 1, 0) == 0) { - *errorMessage = "unable to to write dll path to target process"; - VirtualFreeEx(handle, targetProcessPath, 0, MEM_RELEASE); - return false; - } - - HMODULE kernel32 = LoadLibrary("kernel32"); - - if (kernel32 == 0) { - VirtualFreeEx(handle, targetProcessPath, 0, MEM_RELEASE); - *errorMessage = "unable to load kernel32"; - return false; - } - - // call LoadLibrary from target process - LPTHREAD_START_ROUTINE loadLibraryAddress = (LPTHREAD_START_ROUTINE) GetProcAddress(kernel32, "LoadLibraryA"); - HANDLE thread = CreateRemoteThread(handle, NULL, NULL, loadLibraryAddress, targetProcessPath, NULL, NULL); - - if (thread == NULL) { - *errorMessage = "unable to call LoadLibrary from target process"; - VirtualFreeEx(handle, targetProcessPath, 0, MEM_RELEASE); - return false; - } - - WaitForSingleObject(thread, INFINITE); - GetExitCodeThread(thread, moduleHandle); - - // free memory reserved in target process - VirtualFreeEx(handle, targetProcessPath, 0, MEM_RELEASE); - CloseHandle(thread); - - return *moduleHandle > 0; - } - - bool unload(HANDLE handle, const char** errorMessage, HMODULE moduleHandle) { - HMODULE kernel32 = LoadLibrary("kernel32"); - - if (kernel32 == 0) { - *errorMessage = "unable to load kernel32"; - return false; - } - - // call FreeLibrary from target process - LPTHREAD_START_ROUTINE freeLibraryAddress = (LPTHREAD_START_ROUTINE) GetProcAddress(kernel32, "FreeLibrary"); - HANDLE thread = CreateRemoteThread(handle, NULL, NULL, freeLibraryAddress, (void*)moduleHandle, NULL, NULL); - - if (thread == NULL) { - *errorMessage = "unable to call FreeLibrary from target process"; - return false; - } - - WaitForSingleObject(thread, INFINITE); - DWORD exitCode = -1; - GetExitCodeThread(thread, &exitCode); - CloseHandle(thread); - - return exitCode != 0; - } -} - -#endif -#pragma once diff --git a/lib/functions.cc b/lib/functions.cc deleted file mode 100644 index 1685a9d..0000000 --- a/lib/functions.cc +++ /dev/null @@ -1,17 +0,0 @@ -#include -#include -#include -#include -#include "functions.h" - -char functions::readChar(HANDLE hProcess, DWORD64 address) { - char value; - ReadProcessMemory(hProcess, (LPVOID)address, &value, sizeof(char), NULL); - return value; -} - -LPVOID functions::reserveString(HANDLE hProcess, const char* value, SIZE_T size) { - LPVOID memoryAddress = VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); - WriteProcessMemory(hProcess, memoryAddress, value, size, NULL); - return memoryAddress; - } diff --git a/lib/functions.h b/lib/functions.h deleted file mode 100644 index 72679cb..0000000 --- a/lib/functions.h +++ /dev/null @@ -1,209 +0,0 @@ -#pragma once -#ifndef FUNCTIONS_H -#define FUNCTIONS_H -#define WIN32_LEAN_AND_MEAN - -#include -#include -#include - -struct Call { - int returnValue; - std::string returnString; - DWORD exitCode; -}; - -namespace functions { - enum class Type { - T_VOID = 0x0, - T_STRING = 0x1, - T_CHAR = 0x2, - T_BOOL = 0x3, - T_INT = 0x4, - T_DOUBLE = 0x5, - T_FLOAT = 0x6 - }; - - struct Arg { - Type type; - LPVOID value; - }; - - LPVOID reserveString(HANDLE hProcess, const char* value, SIZE_T size); - char readChar(HANDLE hProcess, DWORD64 address); - - template - Call call(HANDLE pHandle, std::vector args, Type returnType, DWORD64 address, const char** errorMessage) { - std::vector argShellcode; - - std::reverse(args.begin(), args.end()); - - for (auto &arg : args) { - // 0x68: PUSH imm16/imm32 - // 0x6A: PUSH imm8 - - if (arg.type == Type::T_INT || arg.type == Type::T_FLOAT) { - argShellcode.push_back(0x68); - int value = *static_cast(arg.value); - - // Little endian representation - for (int i = 0; i < 4; i++) { - int shifted = (value >> (i * 8)) & 0xFF; - argShellcode.push_back(shifted); - } - - continue; - } - - if (arg.type == Type::T_STRING) { - argShellcode.push_back(0x68); - std::string value = *static_cast(arg.value); - LPVOID address = functions::reserveString(pHandle, value.c_str(), value.length()); - - // Little endian representation - for (int i = 0; i < sizeof(LPVOID); i++) { - unsigned char byte = ((reinterpret_cast(address) >> (i * 8)) & 0xFF); - argShellcode.push_back(byte); - } - - continue; - } - - argShellcode.push_back(0x6A); - unsigned char value = *static_cast(arg.value); - argShellcode.push_back(value); - } - - // 83: ADD r/m16/32 imm8 - std::vector callShellcode = { - // call 0x00000000 - 0xE8, 0x00, 0x00, 0x00, 0x00, - // add esp, [arg count * 4] - 0x83, 0xC4, (unsigned char)(args.size() * 0x4), - }; - - LPVOID returnValuePointer = 0; - if (returnType != Type::T_VOID) { - // We will reserve memory for where we want to store the result, - // and move the return value to this address. - returnValuePointer = VirtualAllocEx(pHandle, NULL, sizeof(returnDataType), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); - - if (returnType == Type::T_FLOAT) { - // fstp DWORD PTR [0x12345678] - // when `call` is executed, if return type is float, it's stored - // in fpu registers (st0 through st7). we can use the `fst` - // instruction to move st0 to a place in memory - // D9 FSTP m32real ST Store Floating Point Value and Pop - // D9 = for m32 - callShellcode.push_back(0xD9); - callShellcode.push_back(0x1C); - callShellcode.push_back(0x25); - } else if (returnType == Type::T_DOUBLE) { - // fstp QWORD PTR [0x12345678] - // DD FSTP m64real ST Store Floating Point Value and Pop - // DD = for m64 - callShellcode.push_back(0xDD); - callShellcode.push_back(0x1C); - callShellcode.push_back(0x25); - } else { - // mov [0x1234], eax - // A3: MOVE moffs16/32 eAX - // Call routines places return value inside EAX - callShellcode.push_back(0xA3); - } - - for (int i = 0; i < sizeof(LPVOID); i++) { - unsigned char byte = ((reinterpret_cast(returnValuePointer) >> (i * 8)) & 0xFF); - callShellcode.push_back(byte); - } - } - - // C3: return - callShellcode.push_back(0xC3); - - // concatenate the arg shellcode with the calling shellcode - std::vector shellcode; - shellcode.reserve(argShellcode.size() + callShellcode.size()); - shellcode.insert(shellcode.end(), argShellcode.begin(), argShellcode.end()); - shellcode.insert(shellcode.end(), callShellcode.begin(), callShellcode.end()); - - // 5 = 0xE8 (call) + 4 empty bytes for where the address will go - int addessShellcodeOffset = argShellcode.size() + 5; - - // Allocate space for the shellcode - SIZE_T size = shellcode.size() * sizeof(unsigned char); - LPVOID pShellcode = VirtualAllocEx(pHandle, NULL, size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); - - // `call` opcode takes relative address, so calculate the relative address - // taking into account where the shellcode will be written in memory - DWORD64 relative = address - (uintptr_t)pShellcode - addessShellcodeOffset; - - // Write the relative address to the shellcode - for (int i = 0; i < 4; i++) { - int shifted = (relative >> (i * 8)) & 0xFF; - - // argShellcode.size() will offset to the `0xE8` opcode, add 1 to offset that instruction - shellcode.at(argShellcode.size() + 1 + i) = shifted; - } - - // Write the shellcode - WriteProcessMemory(pHandle, pShellcode, shellcode.data(), size, NULL); - - // Execute the shellcode - HANDLE thread = CreateRemoteThread(pHandle, NULL, NULL, (LPTHREAD_START_ROUTINE)pShellcode, NULL, NULL, NULL); - - Call data = { 0, "", (DWORD) -1 }; - - if (thread == NULL) { - *errorMessage = "unable to create remote thread."; - return data; - } - - WaitForSingleObject(thread, INFINITE); - GetExitCodeThread(thread, &data.exitCode); - - if (returnType != Type::T_VOID && returnType != Type::T_STRING) { - ReadProcessMemory(pHandle, (LPVOID)returnValuePointer, &data.returnValue, sizeof(int), NULL); - VirtualFreeEx(pHandle, returnValuePointer, 0, MEM_RELEASE); - } - - if (returnType == Type::T_STRING) { - // String is stored in memory somewhere - // When returning a string, the address of the string is placed in EAX. - // So we read the current returnValuePointer address to get the actual address of the string - ReadProcessMemory(pHandle, (LPVOID)returnValuePointer, &returnValuePointer, sizeof(int), NULL); - - std::vector chars; - int offset = 0x0; - while (true) { - char c = functions::readChar(pHandle, (DWORD64)returnValuePointer + offset); - chars.push_back(c); - - // break at 1 million chars - if (offset == (sizeof(char) * 1000000)) { - chars.clear(); - break; - } - - // break at terminator (end of string) - if (c == '\0') { - break; - } - - // go to next char - offset += sizeof(char); - } - - std::string str(chars.begin(), chars.end()); - // TODO: pass str as LPVOID and cast back to string - data.returnString = str; - } - - VirtualFreeEx(pHandle, pShellcode, 0, MEM_RELEASE); - - return data; - } -} - -#endif -#pragma once diff --git a/lib/memory.cc b/lib/memory.cc deleted file mode 100644 index 6943dba..0000000 --- a/lib/memory.cc +++ /dev/null @@ -1,21 +0,0 @@ -#include -#include -#include -#include -#include "memory.h" - -memory::memory() {} -memory::~memory() {} - -std::vector memory::getRegions(HANDLE hProcess) { - std::vector regions; - - MEMORY_BASIC_INFORMATION region; - DWORD64 address; - - for (address = 0; VirtualQueryEx(hProcess, (LPVOID)address, ®ion, sizeof(region)) == sizeof(region); address += region.RegionSize) { - regions.push_back(region); - } - - return regions; -} \ No newline at end of file diff --git a/lib/memory.h b/lib/memory.h deleted file mode 100644 index 2c47139..0000000 --- a/lib/memory.h +++ /dev/null @@ -1,94 +0,0 @@ -#pragma once -#ifndef MEMORY_H -#define MEMORY_H -#define WIN32_LEAN_AND_MEAN - -#include -#include - -class memory { -public: - memory(); - ~memory(); - std::vector getRegions(HANDLE hProcess); - - template - dataType readMemory(HANDLE hProcess, DWORD64 address) { - dataType cRead; - ReadProcessMemory(hProcess, (LPVOID)address, &cRead, sizeof(dataType), NULL); - return cRead; - } - - BOOL readBuffer(HANDLE hProcess, DWORD64 address, SIZE_T size, char* dstBuffer) { - return ReadProcessMemory(hProcess, (LPVOID)address, dstBuffer, size, NULL); - } - - char readChar(HANDLE hProcess, DWORD64 address) { - char value; - ReadProcessMemory(hProcess, (LPVOID)address, &value, sizeof(char), NULL); - return value; - } - - BOOL readString(HANDLE hProcess, DWORD64 address, std::string* pString) { - int length = 0; - int BATCH_SIZE = 256; - char* data = (char*) malloc(sizeof(char) * BATCH_SIZE); - while (length <= BATCH_SIZE * 4096) { - BOOL success = readBuffer(hProcess, address + length, BATCH_SIZE, data); - - if (success == 0) { - free(data); - break; - } - - for (const char* ptr = data; ptr - data < BATCH_SIZE; ++ptr) { - if (*ptr == '\0') { - length += ptr - data + 1; - - char* buffer = (char*) malloc(length); - readBuffer(hProcess, address, length, buffer); - - *pString = std::string(buffer); - - free(data); - free(buffer); - - return TRUE; - } - } - - length += BATCH_SIZE; - } - - return FALSE; - } - - template - void writeMemory(HANDLE hProcess, DWORD64 address, dataType value) { - WriteProcessMemory(hProcess, (LPVOID)address, &value, sizeof(dataType), NULL); - } - - template - void writeMemory(HANDLE hProcess, DWORD64 address, dataType value, SIZE_T size) { - LPVOID buffer = value; - - if (typeid(dataType) != typeid(char*)) { - buffer = &value; - } - - WriteProcessMemory(hProcess, (LPVOID)address, buffer, size, NULL); - } - - // Write String, Method 1: Utf8Value is converted to string, get pointer and length from string - // template <> - // void writeMemory(HANDLE hProcess, DWORD address, std::string value) { - // WriteProcessMemory(hProcess, (LPVOID)address, value.c_str(), value.length(), NULL); - // } - - // Write String, Method 2: get pointer and length from Utf8Value directly - void writeMemory(HANDLE hProcess, DWORD64 address, char* value, SIZE_T size) { - WriteProcessMemory(hProcess, (LPVOID)address, value, size, NULL); - } -}; -#endif -#pragma once \ No newline at end of file diff --git a/lib/memoryjs.cc b/lib/memoryjs.cc deleted file mode 100644 index dd24913..0000000 --- a/lib/memoryjs.cc +++ /dev/null @@ -1,1607 +0,0 @@ -#include -#include -#include -#include -#include "module.h" -#include "process.h" -#include "memoryjs.h" -#include "memory.h" -#include "pattern.h" -#include "functions.h" -#include "dll.h" -#include "debugger.h" - -#pragma comment(lib, "psapi.lib") -#pragma comment(lib, "onecore.lib") - - -process Process; -// module Module; -memory Memory; -pattern Pattern; -// functions Functions; - -struct Vector3 { - float x, y, z; -}; - -struct Vector4 { - float w, x, y, z; -}; - -Napi::Value openProcess(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 1 && args.Length() != 2) { - Napi::Error::New(env, "requires 1 argument, or 2 arguments if a callback is being used").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsString() && !args[0].IsNumber()) { - Napi::Error::New(env, "first argument must be a string or a number").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 2 && !args[1].IsFunction()) { - Napi::Error::New(env, "second argument must be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - // Define error message that may be set by the function that opens the process - const char* errorMessage = ""; - - process::Pair pair; - - if (args[0].IsString()) { - std::string processName(args[0].As().Utf8Value()); - pair = Process.openProcess(processName.c_str(), &errorMessage); - - // In case it failed to open, let's keep retrying - // while(!strcmp(process.szExeFile, "")) { - // process = Process.openProcess((char*) *(processName), &errorMessage); - // }; - } - - if (args[0].IsNumber()) { - pair = Process.openProcess(args[0].As().Uint32Value(), &errorMessage); - - // In case it failed to open, let's keep retrying - // while(!strcmp(process.szExeFile, "")) { - // process = Process.openProcess(info[0].As().Uint32Value(), &errorMessage); - // }; - } - - // If an error message was returned from the function that opens the process, throw the error. - // Only throw an error if there is no callback (if there's a callback, the error is passed there). - if (strcmp(errorMessage, "") && args.Length() != 2) { - Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); - return env.Null(); - } - - // Create a v8 Object (JSON) to store the process information - Napi::Object processInfo = Napi::Object::New(env); - - processInfo.Set(Napi::String::New(env, "dwSize"), Napi::Value::From(env, (int)pair.process.dwSize)); - processInfo.Set(Napi::String::New(env, "th32ProcessID"), Napi::Value::From(env, (int)pair.process.th32ProcessID)); - processInfo.Set(Napi::String::New(env, "cntThreads"), Napi::Value::From(env, (int)pair.process.cntThreads)); - processInfo.Set(Napi::String::New(env, "th32ParentProcessID"), Napi::Value::From(env, (int)pair.process.th32ParentProcessID)); - processInfo.Set(Napi::String::New(env, "pcPriClassBase"), Napi::Value::From(env, (int)pair.process.pcPriClassBase)); - processInfo.Set(Napi::String::New(env, "szExeFile"), Napi::String::New(env, pair.process.szExeFile)); - processInfo.Set(Napi::String::New(env, "handle"), Napi::Value::From(env, (uintptr_t)pair.handle)); - - DWORD64 base = module::getBaseAddress(pair.process.szExeFile, pair.process.th32ProcessID); - processInfo.Set(Napi::String::New(env, "modBaseAddr"), Napi::Value::From(env, (uintptr_t)base)); - - // openProcess can either take one argument or can take - // two arguments for asychronous use (second argument is the callback) - if (args.Length() == 2) { - // Callback to let the user handle with the information - Napi::Function callback = args[1].As(); - callback.Call(env.Global(), { Napi::String::New(env, errorMessage), processInfo }); - return env.Null(); - } else { - // return JSON - return processInfo; - } -} - -Napi::Value closeHandle(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - BOOL success = CloseHandle((HANDLE)args[0].As().Int64Value()); - return Napi::Boolean::New(env, success); -} - -Napi::Value getProcesses(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() > 1) { - Napi::Error::New(env, "requires either 0 arguments or 1 argument if a callback is being used").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 1 && !args[0].IsFunction()) { - Napi::Error::New(env, "first argument must be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - // Define error message that may be set by the function that gets the processes - const char* errorMessage = ""; - - std::vector processEntries = Process.getProcesses(&errorMessage); - - // If an error message was returned from the function that gets the processes, throw the error. - // Only throw an error if there is no callback (if there's a callback, the error is passed there). - if (strcmp(errorMessage, "") && args.Length() != 1) { - Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); - return env.Null(); - } - - // Creates v8 array with the size being that of the processEntries vector processes is an array of JavaScript objects - Napi::Array processes = Napi::Array::New(env, processEntries.size()); - - // Loop over all processes found - for (std::vector::size_type i = 0; i != processEntries.size(); i++) { - // Create a v8 object to store the current process' information - Napi::Object process = Napi::Object::New(env); - - process.Set(Napi::String::New(env, "cntThreads"), Napi::Value::From(env, (int)processEntries[i].cntThreads)); - process.Set(Napi::String::New(env, "szExeFile"), Napi::String::New(env, processEntries[i].szExeFile)); - process.Set(Napi::String::New(env, "th32ProcessID"), Napi::Value::From(env, (int)processEntries[i].th32ProcessID)); - process.Set(Napi::String::New(env, "th32ParentProcessID"), Napi::Value::From(env, (int)processEntries[i].th32ParentProcessID)); - process.Set(Napi::String::New(env, "pcPriClassBase"), Napi::Value::From(env, (int)processEntries[i].pcPriClassBase)); - - // Push the object to the array - processes.Set(i, process); - } - - /* getProcesses can either take no arguments or one argument - one argument is for asychronous use (the callback) */ - if (args.Length() == 1) { - // Callback to let the user handle with the information - Napi::Function callback = args[0].As(); - callback.Call(env.Global(), { Napi::String::New(env, errorMessage), processes }); - return env.Null(); - } else { - // return JSON - return processes; - } -} - -Napi::Value getModules(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 1 && args.Length() != 2) { - Napi::Error::New(env, "requires 1 argument, or 2 arguments if a callback is being used").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber()) { - Napi::Error::New(env, "first argument must be a number").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 2 && !args[1].IsFunction()) { - Napi::Error::New(env, "first argument must be a number, second argument must be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - // Define error message that may be set by the function that gets the modules - const char* errorMessage = ""; - - std::vector moduleEntries = module::getModules(args[0].As().Int32Value(), &errorMessage); - - // If an error message was returned from the function getting the modules, throw the error. - // Only throw an error if there is no callback (if there's a callback, the error is passed there). - if (strcmp(errorMessage, "") && args.Length() != 2) { - Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); - return env.Null(); - } - - // Creates v8 array with the size being that of the moduleEntries vector - // modules is an array of JavaScript objects - Napi::Array modules = Napi::Array::New(env, moduleEntries.size()); - - // Loop over all modules found - for (std::vector::size_type i = 0; i != moduleEntries.size(); i++) { - // Create a v8 object to store the current module's information - Napi::Object module = Napi::Object::New(env); - - module.Set(Napi::String::New(env, "modBaseAddr"), Napi::Value::From(env, (uintptr_t)moduleEntries[i].modBaseAddr)); - module.Set(Napi::String::New(env, "modBaseSize"), Napi::Value::From(env, (int)moduleEntries[i].modBaseSize)); - module.Set(Napi::String::New(env, "szExePath"), Napi::String::New(env, moduleEntries[i].szExePath)); - module.Set(Napi::String::New(env, "szModule"), Napi::String::New(env, moduleEntries[i].szModule)); - module.Set(Napi::String::New(env, "th32ProcessID"), Napi::Value::From(env, (int)moduleEntries[i].th32ProcessID)); - module.Set(Napi::String::New(env, "GlblcntUsage"), Napi::Value::From(env, (int)moduleEntries[i].GlblcntUsage)); - - // Push the object to the array - modules.Set(i, module); - } - - // getModules can either take one argument or two arguments - // one/two arguments is for asychronous use (the callback) - if (args.Length() == 2) { - // Callback to let the user handle with the information - Napi::Function callback = args[1].As(); - callback.Call(env.Global(), { Napi::String::New(env, errorMessage), modules }); - return env.Null(); - } else { - // return JSON - return modules; - } -} - -Napi::Value findModule(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 1 && args.Length() != 2 && args.Length() != 3) { - Napi::Error::New(env, "requires 1 argument, 2 arguments, or 3 arguments if a callback is being used").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsString() && !args[1].IsNumber()) { - Napi::Error::New(env, "first argument must be a string, second argument must be a number").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 3 && !args[2].IsFunction()) { - Napi::Error::New(env, "third argument must be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - std::string moduleName(args[0].As().Utf8Value()); - - // Define error message that may be set by the function that gets the modules - const char* errorMessage = ""; - - MODULEENTRY32 module = module::findModule(moduleName.c_str(), args[1].As().Int32Value(), &errorMessage); - - // If an error message was returned from the function getting the module, throw the error. - // Only throw an error if there is no callback (if there's a callback, the error is passed there). - if (strcmp(errorMessage, "") && args.Length() != 3) { - Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); - return env.Null(); - } - - // In case it failed to open, let's keep retrying - while (!strcmp(module.szExePath, "")) { - module = module::findModule(moduleName.c_str(), args[1].As().Int32Value(), &errorMessage); - }; - - // Create a v8 Object (JSON) to store the process information - Napi::Object moduleInfo = Napi::Object::New(env); - - moduleInfo.Set(Napi::String::New(env, "modBaseAddr"), Napi::Value::From(env, (uintptr_t)module.modBaseAddr)); - moduleInfo.Set(Napi::String::New(env, "modBaseSize"), Napi::Value::From(env, (int)module.modBaseSize)); - moduleInfo.Set(Napi::String::New(env, "szExePath"), Napi::String::New(env, module.szExePath)); - moduleInfo.Set(Napi::String::New(env, "szModule"), Napi::String::New(env, module.szModule)); - moduleInfo.Set(Napi::String::New(env, "th32ProcessID"), Napi::Value::From(env, (int)module.th32ProcessID)); - moduleInfo.Set(Napi::String::New(env, "GlblcntUsage"), Napi::Value::From(env, (int)module.GlblcntUsage)); - - // findModule can either take one or two arguments, - // three arguments for asychronous use (third argument is the callback) - if (args.Length() == 3) { - // Callback to let the user handle with the information - Napi::Function callback = args[2].As(); - callback.Call(env.Global(), { Napi::String::New(env, errorMessage), moduleInfo }); - return env.Null(); - } else { - // return JSON - return moduleInfo; - } -} - -Napi::Value readMemory(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 3 && args.Length() != 4) { - Napi::Error::New(env, "requires 3 arguments, or 4 arguments if a callback is being used").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() && !args[1].IsNumber() && !args[2].IsString()) { - Napi::Error::New(env, "first and second argument must be a number, third argument must be a string").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 4 && !args[3].IsFunction()) { - Napi::Error::New(env, "fourth argument must be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - std::string dataTypeArg(args[2].As().Utf8Value()); - const char* dataType = dataTypeArg.c_str(); - - // Define the error message that will be set if no data type is recognised - const char* errorMessage = ""; - Napi::Value retVal = env.Null(); - - HANDLE handle = (HANDLE)args[0].As().Int64Value(); - - DWORD64 address; - if (args[1].As().IsBigInt()) { - bool lossless; - address = args[1].As().Uint64Value(&lossless); - } else { - address = args[1].As().Int64Value(); - } - - if (!strcmp(dataType, "int8") || !strcmp(dataType, "byte") || !strcmp(dataType, "char")) { - - int8_t result = Memory.readMemory(handle, address); - retVal = Napi::Value::From(env, result); - - } else if (!strcmp(dataType, "uint8") || !strcmp(dataType, "ubyte") || !strcmp(dataType, "uchar")) { - - uint8_t result = Memory.readMemory(handle, address); - retVal = Napi::Value::From(env, result); - - } else if (!strcmp(dataType, "int16") || !strcmp(dataType, "short")) { - - int16_t result = Memory.readMemory(handle, address); - retVal = Napi::Value::From(env, result); - - } else if (!strcmp(dataType, "uint16") || !strcmp(dataType, "ushort") || !strcmp(dataType, "word")) { - - uint16_t result = Memory.readMemory(handle, address); - retVal = Napi::Value::From(env, result); - - } else if (!strcmp(dataType, "int32") || !strcmp(dataType, "int") || !strcmp(dataType, "long")) { - - int32_t result = Memory.readMemory(handle, address); - retVal = Napi::Value::From(env, result); - - } else if (!strcmp(dataType, "uint32") || !strcmp(dataType, "uint") || !strcmp(dataType, "ulong") || !strcmp(dataType, "dword")) { - - uint32_t result = Memory.readMemory(handle, address); - retVal = Napi::Value::From(env, result); - - } else if (!strcmp(dataType, "int64")) { - - int64_t result = Memory.readMemory(handle, address); - retVal = Napi::Value::From(env, Napi::BigInt::New(env, result)); - - } else if (!strcmp(dataType, "uint64")) { - - uint64_t result = Memory.readMemory(handle, address); - retVal = Napi::Value::From(env, Napi::BigInt::New(env, result)); - - } else if (!strcmp(dataType, "float")) { - - float result = Memory.readMemory(handle, address); - retVal = Napi::Value::From(env, result); - - } else if (!strcmp(dataType, "double")) { - - double result = Memory.readMemory(handle, address); - retVal = Napi::Value::From(env, result); - - } else if (!strcmp(dataType, "ptr") || !strcmp(dataType, "pointer")) { - - intptr_t result = Memory.readMemory(handle, address); - - if (sizeof(intptr_t) == 8) { - retVal = Napi::Value::From(env, Napi::BigInt::New(env, (int64_t) result)); - } else { - retVal = Napi::Value::From(env, result); - } - - } else if (!strcmp(dataType, "uptr") || !strcmp(dataType, "upointer")) { - - uintptr_t result = Memory.readMemory(handle, address); - - if (sizeof(uintptr_t) == 8) { - retVal = Napi::Value::From(env, Napi::BigInt::New(env, (uint64_t) result)); - } else { - retVal = Napi::Value::From(env, result); - } - - } else if (!strcmp(dataType, "bool") || !strcmp(dataType, "boolean")) { - - bool result = Memory.readMemory(handle, address); - retVal = Napi::Boolean::New(env, result); - - } else if (!strcmp(dataType, "string") || !strcmp(dataType, "str")) { - - std::string str; - if (!Memory.readString(handle, address, &str)) { - errorMessage = "unable to read string"; - } else { - retVal = Napi::String::New(env, str); - } - - } else if (!strcmp(dataType, "vector3") || !strcmp(dataType, "vec3")) { - - Vector3 result = Memory.readMemory(handle, address); - Napi::Object moduleInfo = Napi::Object::New(env); - moduleInfo.Set(Napi::String::New(env, "x"), Napi::Value::From(env, result.x)); - moduleInfo.Set(Napi::String::New(env, "y"), Napi::Value::From(env, result.y)); - moduleInfo.Set(Napi::String::New(env, "z"), Napi::Value::From(env, result.z)); - retVal = moduleInfo; - - } else if (!strcmp(dataType, "vector4") || !strcmp(dataType, "vec4")) { - - Vector4 result = Memory.readMemory(handle, address); - Napi::Object moduleInfo = Napi::Object::New(env); - moduleInfo.Set(Napi::String::New(env, "w"), Napi::Value::From(env, result.w)); - moduleInfo.Set(Napi::String::New(env, "x"), Napi::Value::From(env, result.x)); - moduleInfo.Set(Napi::String::New(env, "y"), Napi::Value::From(env, result.y)); - moduleInfo.Set(Napi::String::New(env, "z"), Napi::Value::From(env, result.z)); - retVal = moduleInfo; - - } else { - errorMessage = "unexpected data type"; - } - - if (strcmp(errorMessage, "") && args.Length() != 4) { - Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 4) { - Napi::Function callback = args[3].As(); - callback.Call(env.Global(), { Napi::String::New(env, errorMessage), retVal }); - return env.Null(); - } else { - return retVal; - } -} - -Napi::Value readBuffer(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 3 && args.Length() != 4) { - Napi::Error::New(env, "requires 3 arguments, or 4 arguments if a callback is being used").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() && !args[1].IsNumber() && !args[2].IsNumber()) { - Napi::Error::New(env, "first, second and third arguments must be a number").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 4 && !args[3].IsFunction()) { - Napi::Error::New(env, "fourth argument must be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - HANDLE handle = (HANDLE)args[0].As().Int64Value(); - - DWORD64 address; - if (args[1].As().IsBigInt()) { - bool lossless; - address = args[1].As().Uint64Value(&lossless); - } else { - address = args[1].As().Int64Value(); - } - - SIZE_T size = args[2].As().Int64Value(); - - // To fix the memory leak problem that was happening here, we need to release the - // temporary buffer we create after we're done creating a Napi::Buffer from it. - // Napi::Buffer::New doesn't free the memory, so it has be done manually - // but it can segfault when the memory is freed before being accessed. - // The solution is to use Napi::Buffer::Copy, and then we can manually free it. - // - // see: https://github.com/nodejs/node/issues/40936 - // see: https://sagivo.com/2015/09/30/Go-Native-Calling-C-From-NodeJS.html - char* data = (char*) malloc(sizeof(char) * size); - Memory.readBuffer(handle, address, size, data); - - Napi::Buffer buffer = Napi::Buffer::Copy(env, data, size); - free(data); - - if (args.Length() == 4) { - Napi::Function callback = args[3].As(); - callback.Call(env.Global(), { Napi::String::New(env, ""), buffer }); - return env.Null(); - } else { - return buffer; - } -} - -Napi::Value writeMemory(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 4) { - Napi::Error::New(env, "requires 4 arguments").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() && !args[1].IsNumber() && !args[3].IsString()) { - Napi::Error::New(env, "first and second argument must be a number, third argument must be a string").ThrowAsJavaScriptException(); - return env.Null(); - } - - std::string dataTypeArg(args[3].As().Utf8Value()); - const char* dataType = dataTypeArg.c_str(); - - HANDLE handle = (HANDLE)args[0].As().Int64Value(); - - DWORD64 address; - if (args[1].As().IsBigInt()) { - bool lossless; - address = args[1].As().Uint64Value(&lossless); - } else { - address = args[1].As().Int64Value(); - } - - if (!strcmp(dataType, "int8") || !strcmp(dataType, "byte") || !strcmp(dataType, "char")) { - - Memory.writeMemory(handle, address, args[2].As().Int32Value()); - - } else if (!strcmp(dataType, "uint8") || !strcmp(dataType, "ubyte") || !strcmp(dataType, "uchar")) { - - Memory.writeMemory(handle, address, args[2].As().Uint32Value()); - - } else if (!strcmp(dataType, "int16") || !strcmp(dataType, "short")) { - - Memory.writeMemory(handle, address, args[2].As().Int32Value()); - - } else if (!strcmp(dataType, "uint16") || !strcmp(dataType, "ushort") || !strcmp(dataType, "word")) { - - Memory.writeMemory(handle, address, args[2].As().Uint32Value()); - - } else if (!strcmp(dataType, "int32") || !strcmp(dataType, "int") || !strcmp(dataType, "long")) { - - Memory.writeMemory(handle, address, args[2].As().Int32Value()); - - } else if (!strcmp(dataType, "uint32") || !strcmp(dataType, "uint") || !strcmp(dataType, "ulong") || !strcmp(dataType, "dword")) { - - Memory.writeMemory(handle, address, args[2].As().Uint32Value()); - - } else if (!strcmp(dataType, "int64")) { - - if (args[2].As().IsBigInt()) { - bool lossless; - Memory.writeMemory(handle, address, args[2].As().Int64Value(&lossless)); - } else { - Memory.writeMemory(handle, address, args[2].As().Int64Value()); - } - - } else if (!strcmp(dataType, "uint64")) { - - if (args[2].As().IsBigInt()) { - bool lossless; - Memory.writeMemory(handle, address, args[2].As().Uint64Value(&lossless)); - } else { - Memory.writeMemory(handle, address, args[2].As().Int64Value()); - } - - } else if (!strcmp(dataType, "float")) { - - Memory.writeMemory(handle, address, args[2].As().FloatValue()); - - } else if (!strcmp(dataType, "double")) { - - Memory.writeMemory(handle, address, args[2].As().DoubleValue()); - - } else if (!strcmp(dataType, "ptr") || !strcmp(dataType, "pointer")) { - - Napi::BigInt valueBigInt = args[2].As(); - - if (sizeof(intptr_t) == 8 && !valueBigInt.IsBigInt()) { - std::string error = "Writing memoryjs.PTR or memoryjs.POINTER on 64 bit target build requires you to supply a BigInt."; - error += " Rebuild the library with `npm run build32` to target 32 bit applications."; - Napi::Error::New(env, error).ThrowAsJavaScriptException(); - return env.Null(); - } - - if (valueBigInt.IsBigInt()) { - bool lossless; - Memory.writeMemory(handle, address, valueBigInt.Int64Value(&lossless)); - } else { - Memory.writeMemory(handle, address, args[2].As().Int32Value()); - } - - } else if (!strcmp(dataType, "uptr") || !strcmp(dataType, "upointer")) { - - Napi::BigInt valueBigInt = args[2].As(); - - if (sizeof(uintptr_t) == 8 && !valueBigInt.IsBigInt()) { - std::string error = "Writing memoryjs.PTR or memoryjs.POINTER on 64 bit target build requires you to supply a BigInt."; - error += " Rebuild the library with `npm run build32` to target 32 bit applications."; - Napi::Error::New(env, error).ThrowAsJavaScriptException(); - return env.Null(); - } - - if (valueBigInt.IsBigInt()) { - bool lossless; - Memory.writeMemory(handle, address, valueBigInt.Uint64Value(&lossless)); - } else { - Memory.writeMemory(handle, address, args[2].As().Uint32Value()); - } - - } else if (!strcmp(dataType, "bool") || !strcmp(dataType, "boolean")) { - - Memory.writeMemory(handle, address, args[2].As().Value()); - - } else if (!strcmp(dataType, "string") || !strcmp(dataType, "str")) { - - std::string valueParam(args[2].As().Utf8Value()); - valueParam.append("", 1); - - // Write String, Method 1 - //Memory.writeMemory(handle, address, std::string(*valueParam)); - - // Write String, Method 2 - Memory.writeMemory(handle, address, (char*) valueParam.data(), valueParam.size()); - - } else if (!strcmp(dataType, "vector3") || !strcmp(dataType, "vec3")) { - - Napi::Object value = args[2].As(); - Vector3 vector = { - value.Get(Napi::String::New(env, "x")).As().FloatValue(), - value.Get(Napi::String::New(env, "y")).As().FloatValue(), - value.Get(Napi::String::New(env, "z")).As().FloatValue() - }; - Memory.writeMemory(handle, address, vector); - - } else if (!strcmp(dataType, "vector4") || !strcmp(dataType, "vec4")) { - - Napi::Object value = args[2].As(); - Vector4 vector = { - value.Get(Napi::String::New(env, "w")).As().FloatValue(), - value.Get(Napi::String::New(env, "x")).As().FloatValue(), - value.Get(Napi::String::New(env, "y")).As().FloatValue(), - value.Get(Napi::String::New(env, "z")).As().FloatValue() - }; - Memory.writeMemory(handle, address, vector); - - } else { - Napi::Error::New(env, "unexpected data type").ThrowAsJavaScriptException(); - } - - return env.Null(); -} - -Napi::Value writeBuffer(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 3) { - Napi::Error::New(env, "required 3 arguments").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() && !args[1].IsNumber()) { - Napi::Error::New(env, "first and second argument must be a number").ThrowAsJavaScriptException(); - return env.Null(); - } - - HANDLE handle = (HANDLE)args[0].As().Int64Value(); - - DWORD64 address; - if (args[1].As().IsBigInt()) { - bool lossless; - address = args[1].As().Uint64Value(&lossless); - } else { - address = args[1].As().Int64Value(); - } - - SIZE_T length = args[2].As>().Length(); - char* data = args[2].As>().Data(); - Memory.writeMemory(handle, address, data, length); - - return env.Null(); -} - -// Napi::Value findPattern(const Napi::CallbackInfo& args) { -// Napi::Env env = args.Env(); - -// HANDLE handle = (HANDLE)args[0].As().Int64Value(); -// DWORD64 baseAddress = args[1].As().Int64Value(); -// DWORD64 baseSize = args[2].As().Int64Value(); -// std::string signature(args[3].As().Utf8Value()); -// short flags = args[4].As().Uint32Value(); -// uint32_t patternOffset = args[5].As().Uint32Value(); - -// // matching address -// uintptr_t address = 0; -// const char* errorMessage = ""; - -// // read memory region occupied by the module to pattern match inside -// std::vector moduleBytes = std::vector(baseSize); -// ReadProcessMemory(handle, (LPVOID)baseAddress, &moduleBytes[0], baseSize, nullptr); -// unsigned char* byteBase = const_cast(&moduleBytes.at(0)); - -// Pattern.findPattern(handle, baseAddress, byteBase, baseSize, signature.c_str(), flags, patternOffset, &address); - -// if (address == 0) { -// errorMessage = "unable to match pattern inside any modules or regions"; -// } - -// if (args.Length() == 5) { -// Napi::Function callback = args[4].As(); -// callback.Call(env.Global(), { Napi::String::New(env, errorMessage), Napi::Value::From(env, address) }); -// return env.Null(); -// } else { -// return Napi::Value::From(env, address); -// } -// } - -Napi::Value findPattern(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 4 && args.Length() != 5) { - Napi::Error::New(env, "requires 4 arguments, 5 with callback").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() || !args[1].IsString() || !args[2].IsNumber() || !args[3].IsNumber()) { - Napi::Error::New(env, "expected: number, string, string, number").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 5 && !args[4].IsFunction()) { - Napi::Error::New(env, "callback argument must be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - HANDLE handle = (HANDLE)args[0].As().Int64Value(); - std::string pattern(args[1].As().Utf8Value()); - short flags = args[2].As().Uint32Value(); - uint32_t patternOffset = args[3].As().Uint32Value(); - - // matching address - uintptr_t address = 0; - const char* errorMessage = ""; - - std::vector modules = module::getModules(GetProcessId(handle), &errorMessage); - Pattern.search(handle, modules, 0, pattern.c_str(), flags, patternOffset, &address); - - // if no match found inside any modules, search memory regions - if (address == 0) { - std::vector regions = Memory.getRegions(handle); - Pattern.search(handle, regions, 0, pattern.c_str(), flags, patternOffset, &address); - } - - if (address == 0) { - errorMessage = "unable to match pattern inside any modules or regions"; - } - - if (args.Length() == 5) { - Napi::Function callback = args[4].As(); - callback.Call(env.Global(), { Napi::String::New(env, errorMessage), Napi::Value::From(env, address) }); - return env.Null(); - } else { - return Napi::Value::From(env, address); - } -} - -Napi::Value findPatternByModule(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 5 && args.Length() != 6) { - Napi::Error::New(env, "requires 5 arguments, 6 with callback").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() || !args[1].IsString() || !args[2].IsString() || !args[3].IsNumber() || !args[4].IsNumber()) { - Napi::Error::New(env, "expected: number, string, string, number, number").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 6 && !args[5].IsFunction()) { - Napi::Error::New(env, "callback argument must be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - HANDLE handle = (HANDLE)args[0].As().Int64Value(); - std::string moduleName(args[1].As().Utf8Value()); - std::string pattern(args[2].As().Utf8Value()); - short flags = args[3].As().Uint32Value(); - uint32_t patternOffset = args[4].As().Uint32Value(); - - // matching address - uintptr_t address = 0; - const char* errorMessage = ""; - - MODULEENTRY32 module = module::findModule(moduleName.c_str(), GetProcessId(handle), &errorMessage); - - uintptr_t baseAddress = (uintptr_t) module.modBaseAddr; - DWORD baseSize = module.modBaseSize; - - // read memory region occupied by the module to pattern match inside - std::vector moduleBytes = std::vector(baseSize); - ReadProcessMemory(handle, (LPVOID)baseAddress, &moduleBytes[0], baseSize, nullptr); - unsigned char* byteBase = const_cast(&moduleBytes.at(0)); - - Pattern.findPattern(handle, baseAddress, byteBase, baseSize, pattern.c_str(), flags, patternOffset, &address); - - if (address == 0) { - errorMessage = "unable to match pattern inside any modules or regions"; - } - - if (args.Length() == 6) { - Napi::Function callback = args[5].As(); - callback.Call(env.Global(), { Napi::String::New(env, errorMessage), Napi::Value::From(env, address) }); - return env.Null(); - } else { - return Napi::Value::From(env, address); - } -} - -Napi::Value findPatternByAddress(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 5 && args.Length() != 6) { - Napi::Error::New(env, "requires 5 arguments, 6 with callback").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() || !args[1].IsNumber() || !args[2].IsString() || !args[3].IsNumber() || !args[4].IsNumber()) { - Napi::Error::New(env, "expected: number, number, string, number, number").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 6 && !args[5].IsFunction()) { - Napi::Error::New(env, "callback argument must be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - HANDLE handle = (HANDLE)args[0].As().Int64Value(); - - DWORD64 baseAddress; - if (args[1].As().IsBigInt()) { - bool lossless; - baseAddress = args[1].As().Uint64Value(&lossless); - } else { - baseAddress = args[1].As().Int64Value(); - } - - std::string pattern(args[2].As().Utf8Value()); - short flags = args[3].As().Uint32Value(); - uint32_t patternOffset = args[4].As().Uint32Value(); - - // matching address - uintptr_t address = 0; - const char* errorMessage = ""; - - std::vector modules = module::getModules(GetProcessId(handle), &errorMessage); - Pattern.search(handle, modules, baseAddress, pattern.c_str(), flags, patternOffset, &address); - - if (address == 0) { - std::vector regions = Memory.getRegions(handle); - Pattern.search(handle, regions, baseAddress, pattern.c_str(), flags, patternOffset, &address); - } - - if (address == 0) { - errorMessage = "unable to match pattern inside any modules or regions"; - } - - if (args.Length() == 6) { - Napi::Function callback = args[5].As(); - callback.Call(env.Global(), { Napi::String::New(env, errorMessage), Napi::Value::From(env, address) }); - return env.Null(); - } else { - return Napi::Value::From(env, address); - } -} - -Napi::Value callFunction(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 4 && args.Length() != 5) { - Napi::Error::New(env, "requires 4 arguments, 5 with callback").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() && !args[1].IsObject() && !args[2].IsNumber() && !args[3].IsNumber()) { - Napi::Error::New(env, "invalid arguments").ThrowAsJavaScriptException(); - return env.Null(); - } - - // TODO: temp (?) solution to forcing variables onto the heap - // to ensure consistent addresses. copy everything to a vector, and use the - // vector's instances of the variables as the addresses being passed to `functions.call()`. - // Another solution: do `int x = new int(4)` and then use `&x` for the address - std::vector heap; - - std::vector parsedArgs; - Napi::Array arguments = args[1].As(); - for (unsigned int i = 0; i < arguments.Length(); i++) { - Napi::Object argument = arguments.Get(i).As(); - - functions::Type type = (functions::Type) argument.Get(Napi::String::New(env, "type")).As().Uint32Value(); - - if (type == functions::Type::T_STRING) { - std::string stringValue = argument.Get(Napi::String::New(env, "value")).As().Utf8Value(); - parsedArgs.push_back({ type, &stringValue }); - } - - if (type == functions::Type::T_INT) { - int data = argument.Get(Napi::String::New(env, "value")).As().Int32Value(); - - // As we only pass the addresses of the variable to the `call` function and not a copy - // of the variable itself, we need to ensure that the variable stays alive and in a unique - // memory location until the `call` function has been executed. So manually allocate memory, - // track it, and then free it once the function has been called. - // TODO: find a better solution? - int* memory = (int*) malloc(sizeof(int)); - *memory = data; - heap.push_back(memory); - - parsedArgs.push_back({ type, memory }); - } - - if (type == functions::Type::T_FLOAT) { - float data = argument.Get(Napi::String::New(env, "value")).As().FloatValue(); - - float* memory = (float*) malloc(sizeof(float)); - *memory = data; - heap.push_back(memory); - - parsedArgs.push_back({ type, memory }); - } - } - - HANDLE handle = (HANDLE)args[0].As().Int64Value(); - functions::Type returnType = (functions::Type) args[2].As().Uint32Value(); - - DWORD64 address; - if (args[3].As().IsBigInt()) { - bool lossless; - address = args[3].As().Uint64Value(&lossless); - } else { - address = args[3].As().Int64Value(); - } - - const char* errorMessage = ""; - Call data = functions::call(handle, parsedArgs, returnType, address, &errorMessage); - - // Free all the memory we allocated - for (auto &memory : heap) { - free(memory); - } - - heap.clear(); - - if (strcmp(errorMessage, "") && args.Length() != 5) { - Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); - return env.Null(); - } - - Napi::Object info = Napi::Object::New(env); - - Napi::String keyString = Napi::String::New(env, "returnValue"); - - if (returnType == functions::Type::T_STRING) { - info.Set(keyString, Napi::String::New(env, data.returnString.c_str())); - } - - if (returnType == functions::Type::T_CHAR) { - info.Set(keyString, Napi::Value::From(env, (char) data.returnValue)); - } - - if (returnType == functions::Type::T_BOOL) { - info.Set(keyString, Napi::Value::From(env, (bool) data.returnValue)); - } - - if (returnType == functions::Type::T_INT) { - info.Set(keyString, Napi::Value::From(env, (int) data.returnValue)); - } - - if (returnType == functions::Type::T_FLOAT) { - float value = *(float *)&data.returnValue; - info.Set(keyString, Napi::Value::From(env, value)); - } - - if (returnType == functions::Type::T_DOUBLE) { - double value = *(double *)&data.returnValue; - info.Set(keyString, Napi::Value::From(env, value)); - } - - info.Set(Napi::String::New(env, "exitCode"), Napi::Value::From(env, data.exitCode)); - - if (args.Length() == 5) { - // Callback to let the user handle with the information - Napi::Function callback = args[2].As(); - callback.Call(env.Global(), { Napi::String::New(env, errorMessage), info }); - return env.Null(); - } else { - // return JSON - return info; - } - -} - -Napi::Value virtualProtectEx(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 4 && args.Length() != 5) { - Napi::Error::New(env, "requires 4 arguments, 5 with callback").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() && !args[1].IsNumber() && !args[2].IsNumber()) { - Napi::Error::New(env, "All arguments should be numbers.").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 5 && !args[4].IsFunction()) { - Napi::Error::New(env, "callback needs to be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - DWORD result; - HANDLE handle = (HANDLE)args[0].As().Int64Value(); - DWORD64 address = args[1].As().Int64Value(); - SIZE_T size = args[2].As().Int64Value(); - DWORD protection = args[3].As().Uint32Value(); - - bool success = VirtualProtectEx(handle, (LPVOID) address, size, protection, &result); - - const char* errorMessage = ""; - - if (success == 0) { - errorMessage = "an error occurred calling VirtualProtectEx"; - // errorMessage = GetLastErrorToString().c_str(); - } - - // If there is an error and there is no callback, throw the error - if (strcmp(errorMessage, "") && args.Length() != 5) { - Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 5) { - // Callback to let the user handle with the information - Napi::Function callback = args[5].As(); - callback.Call(env.Global(), { - Napi::String::New(env, errorMessage), - Napi::Value::From(env, result) - }); - return env.Null(); - } else { - return Napi::Value::From(env, result); - } -} - -Napi::Value getRegions(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 1 && args.Length() != 2) { - Napi::Error::New(env, "requires 1 argument, 2 with callback").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber()) { - Napi::Error::New(env, "invalid arguments: first argument must be a number").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 2 && !args[1].IsFunction()) { - Napi::Error::New(env, "callback needs to be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - HANDLE handle = (HANDLE)args[0].As().Int64Value(); - std::vector regions = Memory.getRegions(handle); - - Napi::Array regionsArray = Napi::Array::New(env, regions.size()); - - for (std::vector::size_type i = 0; i != regions.size(); i++) { - Napi::Object region = Napi::Object::New(env); - - region.Set(Napi::String::New(env, "BaseAddress"), Napi::Value::From(env, (DWORD64) regions[i].BaseAddress)); - region.Set(Napi::String::New(env, "AllocationBase"), Napi::Value::From(env, (DWORD64) regions[i].AllocationBase)); - region.Set(Napi::String::New(env, "AllocationProtect"), Napi::Value::From(env, (DWORD) regions[i].AllocationProtect)); - region.Set(Napi::String::New(env, "RegionSize"), Napi::Value::From(env, (SIZE_T) regions[i].RegionSize)); - region.Set(Napi::String::New(env, "State"), Napi::Value::From(env, (DWORD) regions[i].State)); - region.Set(Napi::String::New(env, "Protect"), Napi::Value::From(env, (DWORD) regions[i].Protect)); - region.Set(Napi::String::New(env, "Type"), Napi::Value::From(env, (DWORD) regions[i].Type)); - - char moduleName[MAX_PATH]; - DWORD size = GetModuleFileNameExA(handle, (HINSTANCE)regions[i].AllocationBase, moduleName, MAX_PATH); - - if (size != 0) { - region.Set(Napi::String::New(env, "szExeFile"), Napi::String::New(env, moduleName)); - } - - regionsArray.Set(i, region); - } - - if (args.Length() == 2) { - // Callback to let the user handle with the information - Napi::Function callback = args[1].As(); - callback.Call(env.Global(), { Napi::String::New(env, ""), regionsArray }); - return env.Null(); - } else { - // return JSON - return regionsArray; - } -} - -Napi::Value virtualQueryEx(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 2 && args.Length() != 3) { - Napi::Error::New(env, "requires 2 arguments, 3 with callback").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() || !args[1].IsNumber()) { - Napi::Error::New(env, "first and second argument need to be a number").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 3 && !args[2].IsFunction()) { - Napi::Error::New(env, "callback needs to be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - HANDLE handle = (HANDLE)args[0].As().Int64Value(); - DWORD64 address = args[1].As().Int64Value(); - - MEMORY_BASIC_INFORMATION information; - SIZE_T result = VirtualQueryEx(handle, (LPVOID)address, &information, sizeof(information)); - - const char* errorMessage = ""; - - if (result == 0 || result != sizeof(information)) { - errorMessage = "an error occurred calling VirtualQueryEx"; - // errorMessage = GetLastErrorToString().c_str(); - } - - // If there is an error and there is no callback, throw the error - if (strcmp(errorMessage, "") && args.Length() != 3) { - Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); - return env.Null(); - } - - Napi::Object region = Napi::Object::New(env); - - region.Set(Napi::String::New(env, "BaseAddress"), Napi::Value::From(env, (DWORD64) information.BaseAddress)); - region.Set(Napi::String::New(env, "AllocationBase"), Napi::Value::From(env, (DWORD64) information.AllocationBase)); - region.Set(Napi::String::New(env, "AllocationProtect"), Napi::Value::From(env, (DWORD) information.AllocationProtect)); - region.Set(Napi::String::New(env, "RegionSize"), Napi::Value::From(env, (SIZE_T) information.RegionSize)); - region.Set(Napi::String::New(env, "State"), Napi::Value::From(env, (DWORD) information.State)); - region.Set(Napi::String::New(env, "Protect"), Napi::Value::From(env, (DWORD) information.Protect)); - region.Set(Napi::String::New(env, "Type"), Napi::Value::From(env, (DWORD) information.Type)); - - if (args.Length() == 3) { - // Callback to let the user handle with the information - Napi::Function callback = args[1].As(); - callback.Call(env.Global(), { Napi::String::New(env, ""), region }); - return env.Null(); - } else { - // return JSON - return region; - } -} - -Napi::Value virtualAllocEx(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 5 && args.Length() != 6) { - Napi::Error::New(env, "requires 5 arguments, 6 with callback").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() || !args[2].IsNumber() || !args[3].IsNumber() || !args[4].IsNumber()) { - Napi::Error::New(env, "invalid arguments: arguments 0, 2, 3 and 4 need to be numbers").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 6 && !args[5].IsFunction()) { - Napi::Error::New(env, "callback needs to be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - HANDLE handle = (HANDLE)args[0].As().Int64Value(); - SIZE_T size = args[2].As().Int64Value(); - DWORD allocationType = args[3].As().Uint32Value(); - DWORD protection = args[4].As().Uint32Value(); - LPVOID address; - - // Means in the JavaScript space `null` was passed through. - if (args[1] == env.Null()) { - address = NULL; - } else { - address = (LPVOID) args[1].As().Int64Value(); - } - - LPVOID allocatedAddress = VirtualAllocEx(handle, address, size, allocationType, protection); - - const char* errorMessage = ""; - - // If null, it means an error occurred - if (allocatedAddress == NULL) { - errorMessage = "an error occurred calling VirtualAllocEx"; - // errorMessage = GetLastErrorToString().c_str(); - } - - // If there is an error and there is no callback, throw the error - if (strcmp(errorMessage, "") && args.Length() != 6) { - Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 6) { - // Callback to let the user handle with the information - Napi::Function callback = args[5].As(); - callback.Call(env.Global(), { - Napi::String::New(env, errorMessage), - Napi::Value::From(env, (intptr_t)allocatedAddress) - }); - return env.Null(); - } else { - return Napi::Value::From(env, (intptr_t)allocatedAddress); - } -} - -Napi::Value attachDebugger(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 2) { - Napi::Error::New(env, "requires 2 arguments").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() || !args[1].IsBoolean()) { - Napi::Error::New(env, "first argument needs to be a number, second a boolean").ThrowAsJavaScriptException(); - return env.Null(); - } - - DWORD processId = args[0].As().Uint32Value(); - bool killOnExit = args[1].As().Value(); - - bool success = debugger::attach(processId, killOnExit); - return Napi::Boolean::New(env, success); -} - -Napi::Value detachDebugger(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - DWORD processId = args[0].As().Uint32Value(); - - if (args.Length() != 1) { - Napi::Error::New(env, "requires only 1 argument").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber()) { - Napi::Error::New(env, "only argument needs to be a number").ThrowAsJavaScriptException(); - return env.Null(); - } - - bool success = debugger::detatch(processId); - return Napi::Boolean::New(env, success); -} - -Napi::Value awaitDebugEvent(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 2) { - Napi::Error::New(env, "requires 2 arguments").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() || !args[1].IsNumber()) { - Napi::Error::New(env, "both arguments need to be a number").ThrowAsJavaScriptException(); - return env.Null(); - } - - int millisTimeout = args[1].As().Uint32Value(); - - DebugEvent debugEvent; - bool success = debugger::awaitDebugEvent(millisTimeout, &debugEvent); - - Register hardwareRegister = static_cast(args[0].As().Uint32Value()); - - if (success && debugEvent.hardwareRegister == hardwareRegister) { - Napi::Object info = Napi::Object::New(env); - - info.Set(Napi::String::New(env, "processId"), Napi::Value::From(env, (DWORD) debugEvent.processId)); - info.Set(Napi::String::New(env, "threadId"), Napi::Value::From(env, (DWORD) debugEvent.threadId)); - info.Set(Napi::String::New(env, "exceptionCode"), Napi::Value::From(env, (DWORD) debugEvent.exceptionCode)); - info.Set(Napi::String::New(env, "exceptionFlags"), Napi::Value::From(env, (DWORD) debugEvent.exceptionFlags)); - info.Set(Napi::String::New(env, "exceptionAddress"), Napi::Value::From(env, (DWORD64) debugEvent.exceptionAddress)); - info.Set(Napi::String::New(env, "hardwareRegister"), Napi::Value::From(env, static_cast(debugEvent.hardwareRegister))); - - return info; - } - - // If we aren't interested in passing this event back to the JS space, - // just silently handle it - if (success && debugEvent.hardwareRegister != hardwareRegister) { - debugger::handleDebugEvent(debugEvent.processId, debugEvent.threadId); - } - - return env.Null(); -} - -Napi::Value handleDebugEvent(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 2) { - Napi::Error::New(env, "requires 2 arguments").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() || !args[1].IsNumber()) { - Napi::Error::New(env, "both arguments need to be numbers").ThrowAsJavaScriptException(); - return env.Null(); - } - - DWORD processId = args[0].As().Uint32Value(); - DWORD threadId = args[1].As().Uint32Value(); - - bool success = debugger::handleDebugEvent(processId, threadId); - return Napi::Boolean::New(env, success); -} - -Napi::Value setHardwareBreakpoint(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 5) { - Napi::Error::New(env, "requires 5 arguments").ThrowAsJavaScriptException(); - return env.Null(); - } - - for (unsigned int i = 0; i < args.Length(); i++) { - if (!args[i].IsNumber()) { - Napi::Error::New(env, "all arguments need to be numbers").ThrowAsJavaScriptException(); - return env.Null(); - } - } - - DWORD processId = args[0].As().Uint32Value(); - DWORD64 address = args[1].As().Int64Value(); - Register hardwareRegister = static_cast(args[2].As().Uint32Value()); - - // Execute = 0x0 - // Access = 0x3 - // Writer = 0x1 - int trigger = args[3].As().Uint32Value(); - - int length = args[4].As().Uint32Value(); - - bool success = debugger::setHardwareBreakpoint(processId, address, hardwareRegister, trigger, length); - return Napi::Boolean::New(env, success); -} - -Napi::Value removeHardwareBreakpoint(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 2) { - Napi::Error::New(env, "requires 2 arguments").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() || !args[1].IsNumber()) { - Napi::Error::New(env, "both arguments need to be numbers").ThrowAsJavaScriptException(); - return env.Null(); - } - - DWORD processId = args[0].As().Uint32Value(); - Register hardwareRegister = static_cast(args[1].As().Uint32Value()); - - bool success = debugger::setHardwareBreakpoint(processId, 0, hardwareRegister, 0, 0); - return Napi::Boolean::New(env, success); -} - -Napi::Value injectDll(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 2 && args.Length() != 3) { - Napi::Error::New(env, "requires 2 arguments, or 3 with a callback").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber() || !args[1].IsString()) { - Napi::Error::New(env, "first argument needs to be a number, second argument needs to be a string").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 3 && !args[2].IsFunction()) { - Napi::Error::New(env, "callback needs to be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - HANDLE handle = (HANDLE)args[0].As().Int64Value(); - std::string dllPath(args[1].As().Utf8Value()); - Napi::Function callback = args[2].As(); - - const char* errorMessage = ""; - DWORD moduleHandle = -1; - bool success = dll::inject(handle, dllPath, &errorMessage, &moduleHandle); - - if (strcmp(errorMessage, "") && args.Length() != 3) { - Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); - return env.Null(); - } - - // `moduleHandle` above is the return value of the `LoadLibrary` procedure, - // which we retrieve through `GetExitCode`. This value can become truncated - // in large address spaces such as 64 bit since `GetExitCode` just returns BOOL, - // so it's unreliable to use as the handle. Since the handle of a module is just a pointer - // to the address of the DLL mapped in the process' virtual address space, we can fetch - // this separately, so we won't return it to prevent it being passed to `unloadDll` - // and in some cases unexpectedly failing when it is truncated. - - if (args.Length() == 3) { - callback.Call(env.Global(), { Napi::String::New(env, errorMessage), Napi::Boolean::New(env, success) }); - return env.Null(); - } else { - return Napi::Boolean::New(env, success); - } -} - -Napi::Value unloadDll(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - if (args.Length() != 2 && args.Length() != 3) { - Napi::Error::New(env, "requires 2 arguments, or 3 with a callback").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[0].IsNumber()) { - Napi::Error::New(env, "first argument needs to be a number").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (!args[1].IsNumber() && !args[1].IsString()) { - Napi::Error::New(env, "second argument needs to be a number or a string").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (args.Length() == 3 && !args[2].IsFunction()) { - Napi::Error::New(env, "callback needs to be a function").ThrowAsJavaScriptException(); - return env.Null(); - } - - HANDLE handle = (HANDLE)args[0].As().Int64Value(); - Napi::Function callback = args[2].As(); - - HMODULE moduleHandle; - - // get module handle (module base address) directly - if (args[1].IsNumber()) { - moduleHandle = (HMODULE)args[1].As().Int64Value(); - } - - // find module handle from name of DLL - if (args[1].IsString()) { - std::string moduleName(args[1].As().Utf8Value()); - const char* errorMessage = ""; - - MODULEENTRY32 module = module::findModule(moduleName.c_str(), GetProcessId(handle), &errorMessage); - - if (strcmp(errorMessage, "")) { - if (args.Length() != 3) { - Napi::Error::New(env, "unable to find specified module").ThrowAsJavaScriptException(); - return env.Null(); - } else { - callback.Call(env.Global(), { Napi::String::New(env, errorMessage) }); - return Napi::Boolean::New(env, false); - } - } - - moduleHandle = (HMODULE) module.modBaseAddr; - } - - const char* errorMessage = ""; - bool success = dll::unload(handle, &errorMessage, moduleHandle); - - if (strcmp(errorMessage, "") && args.Length() != 3) { - Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); - return Napi::Boolean::New(env, false); - } - - if (args.Length() == 3) { - callback.Call(env.Global(), { Napi::String::New(env, errorMessage), Napi::Boolean::New(env, success) }); - return env.Null(); - } else { - return Napi::Boolean::New(env, success); - } -} - -Napi::Value openFileMapping(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - std::string fileName(args[0].As().Utf8Value()); - - HANDLE fileHandle = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, fileName.c_str()); - - if (fileHandle == NULL) { - Napi::Error::New(env, Napi::String::New(env, "Error opening handle to file!")).ThrowAsJavaScriptException(); - return env.Null(); - } - - return Napi::Value::From(env, (uintptr_t) fileHandle); -} - -Napi::Value mapViewOfFile(const Napi::CallbackInfo& args) { - Napi::Env env = args.Env(); - - HANDLE processHandle = (HANDLE)args[0].As().Int64Value(); - HANDLE fileHandle = (HANDLE)args[1].As().Int64Value(); - - uint64_t offset; - if (args[2].As().IsBigInt()) { - bool lossless; - offset = args[2].As().Uint64Value(&lossless); - } else { - offset = args[2].As().Int64Value(); - } - - size_t viewSize; - if (args[3].As().IsBigInt()) { - bool lossless; - viewSize = args[3].As().Uint64Value(&lossless); - } else { - viewSize = args[3].As().Int64Value(); - } - - ULONG pageProtection = args[4].As().Int64Value(); - - LPVOID baseAddress = MapViewOfFile2(fileHandle, processHandle, offset, NULL, viewSize, 0, pageProtection); - - if (baseAddress == NULL) { - Napi::Error::New(env, Napi::String::New(env, "Error mapping file to process!")).ThrowAsJavaScriptException(); - return env.Null(); - } - - return Napi::Value::From(env, (uintptr_t) baseAddress); -} - -// https://stackoverflow.com/a/17387176 -std::string GetLastErrorToString() { - DWORD errorMessageID = ::GetLastError(); - - // No error message, return empty string - if(errorMessageID == 0) { - return std::string(); - } - - LPSTR messageBuffer = nullptr; - - size_t size = FormatMessageA( - FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, - errorMessageID, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPSTR)&messageBuffer, - 0, - NULL - ); - - std::string message(messageBuffer, size); - - // Free the buffer - LocalFree(messageBuffer); - return message; -} - -Napi::Object init(Napi::Env env, Napi::Object exports) { - exports.Set(Napi::String::New(env, "openProcess"), Napi::Function::New(env, openProcess)); - exports.Set(Napi::String::New(env, "closeHandle"), Napi::Function::New(env, closeHandle)); - exports.Set(Napi::String::New(env, "getProcesses"), Napi::Function::New(env, getProcesses)); - exports.Set(Napi::String::New(env, "getModules"), Napi::Function::New(env, getModules)); - exports.Set(Napi::String::New(env, "findModule"), Napi::Function::New(env, findModule)); - exports.Set(Napi::String::New(env, "readMemory"), Napi::Function::New(env, readMemory)); - exports.Set(Napi::String::New(env, "readBuffer"), Napi::Function::New(env, readBuffer)); - exports.Set(Napi::String::New(env, "writeMemory"), Napi::Function::New(env, writeMemory)); - exports.Set(Napi::String::New(env, "writeBuffer"), Napi::Function::New(env, writeBuffer)); - exports.Set(Napi::String::New(env, "findPattern"), Napi::Function::New(env, findPattern)); - exports.Set(Napi::String::New(env, "findPatternByModule"), Napi::Function::New(env, findPatternByModule)); - exports.Set(Napi::String::New(env, "findPatternByAddress"), Napi::Function::New(env, findPatternByAddress)); - exports.Set(Napi::String::New(env, "virtualProtectEx"), Napi::Function::New(env, virtualProtectEx)); - exports.Set(Napi::String::New(env, "callFunction"), Napi::Function::New(env, callFunction)); - exports.Set(Napi::String::New(env, "virtualAllocEx"), Napi::Function::New(env, virtualAllocEx)); - exports.Set(Napi::String::New(env, "getRegions"), Napi::Function::New(env, getRegions)); - exports.Set(Napi::String::New(env, "virtualQueryEx"), Napi::Function::New(env, virtualQueryEx)); - exports.Set(Napi::String::New(env, "attachDebugger"), Napi::Function::New(env, attachDebugger)); - exports.Set(Napi::String::New(env, "detachDebugger"), Napi::Function::New(env, detachDebugger)); - exports.Set(Napi::String::New(env, "awaitDebugEvent"), Napi::Function::New(env, awaitDebugEvent)); - exports.Set(Napi::String::New(env, "handleDebugEvent"), Napi::Function::New(env, handleDebugEvent)); - exports.Set(Napi::String::New(env, "setHardwareBreakpoint"), Napi::Function::New(env, setHardwareBreakpoint)); - exports.Set(Napi::String::New(env, "removeHardwareBreakpoint"), Napi::Function::New(env, removeHardwareBreakpoint)); - exports.Set(Napi::String::New(env, "injectDll"), Napi::Function::New(env, injectDll)); - exports.Set(Napi::String::New(env, "unloadDll"), Napi::Function::New(env, unloadDll)); - exports.Set(Napi::String::New(env, "openFileMapping"), Napi::Function::New(env, openFileMapping)); - exports.Set(Napi::String::New(env, "mapViewOfFile"), Napi::Function::New(env, mapViewOfFile)); - return exports; -} - -NODE_API_MODULE(memoryjs, init) \ No newline at end of file diff --git a/lib/memoryjs.h b/lib/memoryjs.h deleted file mode 100644 index c0428ef..0000000 --- a/lib/memoryjs.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once -#ifndef MEMORYJS_H -#define MEMORYJS_H -#define WIN32_LEAN_AND_MEAN - -#include - -class memoryjs { - -public: - memoryjs(); - ~memoryjs(); -}; -#endif -#pragma once diff --git a/lib/module.cc b/lib/module.cc deleted file mode 100644 index 6ec1f86..0000000 --- a/lib/module.cc +++ /dev/null @@ -1,94 +0,0 @@ -#include -#include -#include -#include -#include "module.h" -#include "process.h" -#include "memoryjs.h" - -DWORD64 module::getBaseAddress(const char* processName, DWORD processId) { - const char* errorMessage = ""; - MODULEENTRY32 baseModule = module::findModule(processName, processId, &errorMessage); - return (DWORD64)baseModule.modBaseAddr; -} - -MODULEENTRY32 module::findModule(const char* moduleName, DWORD processId, const char** errorMessage) { - MODULEENTRY32 module; - bool found = false; - - std::vector moduleEntries = getModules(processId, errorMessage); - - // Loop over every module - for (std::vector::size_type i = 0; i != moduleEntries.size(); i++) { - // Check to see if this is the module we want. - if (!strcmp(moduleEntries[i].szModule, moduleName)) { - // module is returned and moduleEntry is used internally for reading/writing to memory - module = moduleEntries[i]; - found = true; - break; - } - } - - if (!found) { - *errorMessage = "unable to find module"; - } - - return module; -} - -std::vector module::getModules(DWORD processId, const char** errorMessage) { - // Take a snapshot of all modules inside a given process. - HANDLE hModuleSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, processId); - MODULEENTRY32 mEntry; - - if (hModuleSnapshot == INVALID_HANDLE_VALUE) { - *errorMessage = "method failed to take snapshot of the modules"; - } - - // Before use, set the structure size. - mEntry.dwSize = sizeof(mEntry); - - // Exit if unable to find the first module. - if (!Module32First(hModuleSnapshot, &mEntry)) { - CloseHandle(hModuleSnapshot); - *errorMessage = "method failed to retrieve the first module"; - } - - std::vector modules; - - // Loop through modules. - do { - // Add the module to the vector - modules.push_back(mEntry); - } while (Module32Next(hModuleSnapshot, &mEntry)); - - CloseHandle(hModuleSnapshot); - - return modules; -} - -std::vector module::getThreads(DWORD processId, const char** errorMessage) { - HANDLE hThreadSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, processId); - THREADENTRY32 mEntry; - - if (hThreadSnapshot == INVALID_HANDLE_VALUE) { - *errorMessage = "method failed to take snapshot of the threads"; - } - - mEntry.dwSize = sizeof(mEntry); - - if(!Thread32First(hThreadSnapshot, &mEntry)) { - CloseHandle(hThreadSnapshot); - *errorMessage = "method failed to retrieve the first thread"; - } - - std::vector threads; - - do { - threads.push_back(mEntry); - } while (Thread32Next(hThreadSnapshot, &mEntry)); - - CloseHandle(hThreadSnapshot); - - return threads; -} diff --git a/lib/module.h b/lib/module.h deleted file mode 100644 index b617e64..0000000 --- a/lib/module.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once -#ifndef MODULE_H -#define MODULE_H -#define WIN32_LEAN_AND_MEAN - -#include -#include -#include - -namespace module { - DWORD64 getBaseAddress(const char* processName, DWORD processId); - MODULEENTRY32 findModule(const char* moduleName, DWORD processId, const char** errorMessage); - std::vector getModules(DWORD processId, const char** errorMessage); - std::vector getThreads(DWORD processId, const char** errorMessage); - -}; -#endif -#pragma once diff --git a/lib/pattern.cc b/lib/pattern.cc deleted file mode 100644 index 7c07632..0000000 --- a/lib/pattern.cc +++ /dev/null @@ -1,103 +0,0 @@ -#include -#include -#include -#include -#include "pattern.h" -#include "memoryjs.h" -#include "process.h" -#include "memory.h" - -#define INRANGE(x,a,b) (x >= a && x <= b) -#define getBits( x ) (INRANGE(x,'0','9') ? (x - '0') : ((x&(~0x20)) - 'A' + 0xa)) -#define getByte( x ) (getBits(x[0]) << 4 | getBits(x[1])) - -pattern::pattern() {} -pattern::~pattern() {} - -bool pattern::search(HANDLE handle, std::vector regions, DWORD64 searchAddress, const char* pattern, short flags, uint32_t patternOffset, uintptr_t* pAddress) { - for (std::vector::size_type i = 0; i != regions.size(); i++) { - uintptr_t baseAddress = (uintptr_t) regions[i].BaseAddress; - DWORD baseSize = regions[i].RegionSize; - - // if `searchAddress` has been set, only pattern match if the address lies inside of this region - if (searchAddress != 0 && (searchAddress < baseAddress || searchAddress > (baseAddress + baseSize))) { - continue; - } - - // read memory region to pattern match inside - std::vector regionBytes = std::vector(baseSize); - ReadProcessMemory(handle, (LPVOID)baseAddress, ®ionBytes[0], baseSize, nullptr); - unsigned char* byteBase = const_cast(®ionBytes.at(0)); - - if (findPattern(handle, baseAddress, byteBase, baseSize, pattern, flags, patternOffset, pAddress)) { - return true; - } - } - - return false; -} - -bool pattern::search(HANDLE handle, std::vector modules, DWORD64 searchAddress, const char* pattern, short flags, uint32_t patternOffset, uintptr_t* pAddress) { - for (std::vector::size_type i = 0; i != modules.size(); i++) { - uintptr_t baseAddress = (uintptr_t) modules[i].modBaseAddr; - DWORD baseSize = modules[i].modBaseSize; - - // if `searchAddress` has been set, only pattern match if the address lies inside of this module - if (searchAddress != 0 && (searchAddress < baseAddress || searchAddress > (baseAddress + baseSize))) { - continue; - } - - // read memory region occupied by the module to pattern match inside - std::vector moduleBytes = std::vector(baseSize); - ReadProcessMemory(handle, (LPVOID)baseAddress, &moduleBytes[0], baseSize, nullptr); - unsigned char* byteBase = const_cast(&moduleBytes.at(0)); - - if (findPattern(handle, baseAddress, byteBase, baseSize, pattern, flags, patternOffset, pAddress)) { - return true; - } - } - - return false; -} - -/* based off Y3t1y3t's implementation */ -bool pattern::findPattern(HANDLE handle, uintptr_t memoryBase, unsigned char* byteBase, DWORD memorySize, const char* pattern, short flags, uint32_t patternOffset, uintptr_t* pAddress) { - // uintptr_t moduleBase = (uintptr_t)module.hModule; - auto maxOffset = memorySize - 0x1000; - - for (uintptr_t offset = 0; offset < maxOffset; ++offset) { - if (compareBytes(byteBase + offset, pattern)) { - uintptr_t address = memoryBase + offset + patternOffset; - - if (flags & ST_READ) { - ReadProcessMemory(handle, LPCVOID(address), &address, sizeof(uintptr_t), nullptr); - } - - if (flags & ST_SUBTRACT) { - address -= memoryBase; - } - - *pAddress = address; - - return true; - } - } - - return false; -}; - -bool pattern::compareBytes(const unsigned char* bytes, const char* pattern) { - for (; *pattern; *pattern != ' ' ? ++bytes : bytes, ++pattern) { - if (*pattern == ' ' || *pattern == '?') { - continue; - } - - if (*bytes != getByte(pattern)) { - return false; - } - - ++pattern; - } - - return true; -} \ No newline at end of file diff --git a/lib/pattern.h b/lib/pattern.h deleted file mode 100644 index 5b108c4..0000000 --- a/lib/pattern.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once -#ifndef PATTERN_H -#define PATTERN_H -#define WIN32_LEAN_AND_MEAN - -#include -#include - -class pattern { -public: - pattern(); - ~pattern(); - - // Signature/pattern types - enum { - // normal: normal - // read: read memory at pattern - // subtract: subtract module base - ST_NORMAL = 0x0, - ST_READ = 0x1, - ST_SUBTRACT = 0x2 - }; - - bool search(HANDLE handle, std::vector regions, DWORD64 searchAddress, const char* pattern, short flags, uint32_t patternOffset, uintptr_t* pAddress); - bool search(HANDLE handle, std::vector modules, DWORD64 searchAddress, const char* pattern, short flags, uint32_t patternOffset, uintptr_t* pAddress); - bool findPattern(HANDLE handle, uintptr_t memoryBase, unsigned char* module, DWORD memorySize, const char* pattern, short flags, uint32_t patternOffset, uintptr_t* pAddress); - bool compareBytes(const unsigned char* bytes, const char* pattern); -}; - -#endif -#pragma once diff --git a/lib/process.cc b/lib/process.cc deleted file mode 100644 index 51afba7..0000000 --- a/lib/process.cc +++ /dev/null @@ -1,95 +0,0 @@ -#include -#include -#include -#include -#include "process.h" -#include "memoryjs.h" - -process::process() {} -process::~process() {} - -using v8::Exception; -using v8::Isolate; -using v8::String; - -process::Pair process::openProcess(const char* processName, const char** errorMessage){ - PROCESSENTRY32 process; - HANDLE handle = NULL; - - // A list of processes (PROCESSENTRY32) - std::vector processes = getProcesses(errorMessage); - - for (std::vector::size_type i = 0; i != processes.size(); i++) { - // Check to see if this is the process we want. - if (!strcmp(processes[i].szExeFile, processName)) { - handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processes[i].th32ProcessID); - process = processes[i]; - break; - } - } - - if (handle == NULL) { - *errorMessage = "unable to find process"; - } - - return { - handle, - process, - }; -} - -process::Pair process::openProcess(DWORD processId, const char** errorMessage) { - PROCESSENTRY32 process; - HANDLE handle = NULL; - - // A list of processes (PROCESSENTRY32) - std::vector processes = getProcesses(errorMessage); - - for (std::vector::size_type i = 0; i != processes.size(); i++) { - // Check to see if this is the process we want. - if (processId == processes[i].th32ProcessID) { - handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processes[i].th32ProcessID); - process = processes[i]; - break; - } - } - - if (handle == NULL) { - *errorMessage = "unable to find process"; - } - - return { - handle, - process, - }; -} - -std::vector process::getProcesses(const char** errorMessage) { - // Take a snapshot of all processes. - HANDLE hProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); - PROCESSENTRY32 pEntry; - - if (hProcessSnapshot == INVALID_HANDLE_VALUE) { - *errorMessage = "method failed to take snapshot of the process"; - } - - // Before use, set the structure size. - pEntry.dwSize = sizeof(pEntry); - - // Exit if unable to find the first process. - if (!Process32First(hProcessSnapshot, &pEntry)) { - CloseHandle(hProcessSnapshot); - *errorMessage = "method failed to retrieve the first process"; - } - - std::vector processes; - - // Loop through processes. - do { - // Add the process to the vector - processes.push_back(pEntry); - } while (Process32Next(hProcessSnapshot, &pEntry)); - - CloseHandle(hProcessSnapshot); - return processes; -} diff --git a/lib/process.h b/lib/process.h deleted file mode 100644 index a0b8196..0000000 --- a/lib/process.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once -#ifndef PROCESS_H -#define PROCESS_H -#define WIN32_LEAN_AND_MEAN - -#include -#include -#include - -class process { -public: - struct Pair { - HANDLE handle; - PROCESSENTRY32 process; - }; - - process(); - ~process(); - - Pair openProcess(const char* processName, const char** errorMessage); - Pair openProcess(DWORD processId, const char** errorMessage); - void closeProcess(HANDLE hProcess); - std::vector getProcesses(const char** errorMessage); -}; - -#endif -#pragma once diff --git a/scripts/debug.js b/scripts/debug.js deleted file mode 100644 index aa40c71..0000000 --- a/scripts/debug.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * [npm debug script] - * Purpose is to automatically detect Node process architecture and run the - * corresponding script to build the library for debugging. - * Defaults to `node-gyp rebuild` if unable to detect the architecture. - */ - -/* eslint-disable no-console */ -const { spawn } = require('child_process'); - -function run(script) { - console.log(`Node architecture is ${process.arch}: running "${script}"`); - - const program = script.split(' ')[0]; - const args = script.split(' ').slice(1); - - // inherit stdio to print colour (helpful for warnings/errors readability) - const child = spawn(program, args, { stdio: 'inherit' }); - - child.on('close', code => console.log(`Script "${script}" exited with ${code}`)); -} - -const buildScripts = { - x64: 'run debug64', - ia32: 'run debug32', -}; - -if (Object.prototype.hasOwnProperty.call(buildScripts, process.arch)) { - // on Windows, npm is actually `npm.cmd` - const npm = /^win/.test(process.platform) ? 'npm.cmd' : 'npm'; - run(`${npm} ${buildScripts[process.arch]}`); -} else { - console.log('Unfamiliar architecture detected, this library is probably not compatible with your OS.'); - run('node-gyp --debug configure rebuild'); -} diff --git a/scripts/install.js b/scripts/install.js deleted file mode 100644 index cf65fb5..0000000 --- a/scripts/install.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * [npm install script] - * Purpose is to automatically detect Node process architecture and run the - * corresponding script to build the library to target the appropriate architecture. - * Defaults to `node-gyp rebuild` if unable to detect the architecture. - */ - -/* eslint-disable no-console */ -const { spawn } = require('child_process'); - -function run(script) { - console.log(`Node architecture is ${process.arch}: running "${script}"`); - - const program = script.split(' ')[0]; - const args = script.split(' ').slice(1); - - // inherit stdio to print colour (helpful for warnings/errors readability) - const child = spawn(program, args, { stdio: 'inherit' }); - - child.on('close', code => console.log(`Script "${script}" exited with ${code}`)); -} - -const buildScripts = { - x64: 'run build64', - ia32: 'run build32', -}; - -if (Object.prototype.hasOwnProperty.call(buildScripts, process.arch)) { - // on Windows, npm is actually `npm.cmd` - const npm = /^win/.test(process.platform) ? 'npm.cmd' : 'npm'; - run(`${npm} ${buildScripts[process.arch]}`); -} else { - console.log('Unfamiliar architecture detected, this library is probably not compatible with your OS.'); - run('node-gyp rebuild'); -} diff --git a/src/consts.js b/src/consts.js deleted file mode 100644 index a7a827a..0000000 --- a/src/consts.js +++ /dev/null @@ -1,138 +0,0 @@ -const dataTypes = { - standard: { - BYTE: 'byte', - UBYTE: 'ubyte', - CHAR: 'char', - UCHAR: 'uchar', - INT8: 'int8', - UINT8: 'uint8', - INT16: 'int16', - INT16_BE: 'int16_be', - UINT16: 'uint16', - UINT16_BE: 'uint16_be', - SHORT: 'short', - SHORT_BE: 'short_be', - USHORT: 'ushort', - USHORT_BE: 'ushort_be', - LONG: 'long', - LONG_BE: 'long_be', - ULONG: 'ulong', - ULONG_BE: 'ulong_be', - INT: 'int', - INT_BE: 'int_be', - UINT: 'uint', - UINT_BE: 'uint_be', - INT32: 'int32', - INT32_BE: 'int32_be', - UINT32: 'uint32', - UINT32_BE: 'uint32_be', - INT64: 'int64', - INT64_BE: 'int64_be', - UINT64: 'uint64', - UINT64_BE: 'uint64_be', - WORD: 'word', - DWORD: 'dword', - FLOAT: 'float', - FLOAT_BE: 'float_be', - DOUBLE: 'double', - DOUBLE_BE: 'double_be', - BOOL: 'bool', - BOOLEAN: 'boolean', - PTR: 'ptr', - POINTER: 'pointer', - UPTR: 'uptr', - UPOINTER: 'upointer', - STR: 'str', - STRING: 'string', - VEC3: 'vec3', - VECTOR3: 'vector3', - VEC4: 'vec4', - VECTOR4: 'vector4', - }, - function: { - T_VOID: 0x0, - T_STRING: 0x1, - T_CHAR: 0x2, - T_BOOL: 0x3, - T_INT: 0x4, - T_DOUBLE: 0x5, - T_FLOAT: 0x6, - }, -}; - -const signatureTypes = { - NORMAL: 0x0, - READ: 0x1, - SUBTRACT: 0x2, -}; - -const memoryFlags = { - // see: https://docs.microsoft.com/en-gb/windows/desktop/Memory/memory-protection-constants - access: { - PAGE_NOACCESS: 0x01, - PAGE_READONLY: 0x02, - PAGE_READWRITE: 0x04, - PAGE_WRITECOPY: 0x08, - PAGE_EXECUTE: 0x10, - PAGE_EXECUTE_READ: 0x20, - PAGE_EXECUTE_READWRITE: 0x40, - PAGE_EXECUTE_WRITECOPY: 0x80, - PAGE_GUARD: 0x100, - PAGE_NOCACHE: 0x200, - PAGE_WRITECOMBINE: 0x400, - PAGE_ENCLAVE_UNVALIDATED: 0x20000000, - PAGE_TARGETS_NO_UPDATE: 0x40000000, - PAGE_TARGETS_INVALID: 0x40000000, - PAGE_ENCLAVE_THREAD_CONTROL: 0x80000000, - }, - - // see: https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex#parameters - allocation: { - MEM_COMMIT: 0x00001000, - MEM_RESERVE: 0x00002000, - MEM_RESET: 0x00080000, - MEM_TOP_DOWN: 0x00100000, - MEM_RESET_UNDO: 0x1000000, - MEM_LARGE_PAGES: 0x20000000, - MEM_PHYSICAL: 0x00400000, - }, - - // see: https://docs.microsoft.com/en-us/windows/desktop/api/winnt/ns-winnt-_memory_basic_information - page: { - MEM_PRIVATE: 0x20000, - MEM_MAPPED: 0x40000, - MEM_IMAGE: 0x1000000, - }, -}; - -const hardwareDebug = { - registers: { - DR0: 0x0, - DR1: 0x1, - DR2: 0x2, - DR3: 0x3, - }, - breakpointTriggerTypes: { - TRIGGER_EXECUTE: 0x0, - TRIGGER_ACCESS: 0x3, - TRIGGER_WRITE: 0x1, - }, -}; - -module.exports = { - // data type constants - ...dataTypes.standard, - ...dataTypes.function, - - // pattern scanning flags - ...signatureTypes, - - // memory flags - ...memoryFlags.access, - ...memoryFlags.allocation, - ...memoryFlags.page, - - // debugger consts - ...hardwareDebug.registers, - ...hardwareDebug.breakpointTriggerTypes, -}; diff --git a/src/debugger.js b/src/debugger.js deleted file mode 100644 index f3f494c..0000000 --- a/src/debugger.js +++ /dev/null @@ -1,150 +0,0 @@ -const EventEmitter = require('events'); - -const lengths = { - byte: 1, - int: 4, - int32: 4, - uint32: 4, - int64: 8, - uint64: 8, - dword: 4, - short: 2, - long: 8, - float: 4, - double: 8, - bool: 1, - boolean: 1, - ptr: 4, - pointer: 4, - // str: 0, - // string: 0, - // vec3: 0, - // vector3: 0, - // vec4: 0, - // vector4: 0, -}; - -// Tracks used and unused registers -class Registers { - constructor() { - this.registers = Object.freeze({ - DR0: 0x0, - DR1: 0x1, - DR2: 0x2, - DR3: 0x3, - }); - - this.used = []; - } - - getRegister() { - const unused = Object - .values(this.registers) - .filter(r => !this.used.includes(r)); - - return unused[0]; - } - - busy(register) { - this.used.push(register); - } - - unbusy(register) { - this.used.splice(this.used.indexOf(register), 1); - } -} - -class Debugger extends EventEmitter { - constructor(memoryjs) { - super(); - this.memoryjs = memoryjs; - this.registers = new Registers(); - this.attached = false; - this.intervals = []; - } - - attach(processId, killOnDetach = false) { - const success = this.memoryjs.attachDebugger(processId, killOnDetach); - - if (success) { - this.attached = true; - } - - return success; - } - - detach(processId) { - this.intervals.map(({ id }) => clearInterval(id)); - return this.memoryjs.detachDebugger(processId); - } - - removeHardwareBreakpoint(processId, register) { - const success = this.memoryjs.removeHardwareBreakpoint(processId, register); - - if (success) { - this.registers.unbusy(register); - } - - // Find the register's corresponding interval and delete it - this.intervals.forEach(({ register: r, id }) => { - if (r === register) { - clearInterval(id); - } - }); - - return success; - } - - setHardwareBreakpoint(processId, address, trigger, dataType) { - let size = lengths[dataType]; - - // If we are breakpointing a string, we need to determine the length of it - if (dataType === 'str' || dataType === 'string') { - const { handle } = this.memoryjs.openProcess(processId); - const value = this.memoryjs.readMemory(handle, address, this.memoryjs.STRING); - - size = value.length; - - this.memoryjs.closeProcess(handle); - } - - // Obtain an available register - const register = this.registers.getRegister(); - const success = this.memoryjs - .setHardwareBreakpoint(processId, address, register, trigger, size); - - // If the breakpoint was set, mark this register as busy - if (success) { - this.registers.busy(register); - this.monitor(register); - } - - return register; - } - - monitor(register, timeout = 100) { - const id = setInterval(() => { - const debugEvent = this.memoryjs.awaitDebugEvent(register, timeout); - - if (debugEvent) { - this.memoryjs.handleDebugEvent(debugEvent.processId, debugEvent.threadId); - - // Global event for all registers - this.emit('debugEvent', { - register, - event: debugEvent, - }); - - // Event per register - this.emit(register, debugEvent); - } - }, 100); - - this.intervals.push({ - register, - id, - }); - } -} - -module.exports = Debugger; diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index d69217e..0000000 --- a/src/utils.js +++ /dev/null @@ -1,75 +0,0 @@ -const SIZEOF_STDSTRING_32BIT = 24; -const SIZEOF_STDSTRING_64BIT = 32; -const STDSTRING_LENGTH_OFFSET = 0x10; - -/** - * Custom string consumer/producer for Structron (due to complexity of `std::string`) - * `std::string` is a container for a string which makes reading/writing to it tricky, - * it will either store the string itself, or a pointer to the string, based on the - * length of the string. When we want to read from or write to a buffer, we need - * to determine if the string is in the buffer itself, or if the buffer - * just contains a pointer to the string. Based on one of these options, - * we can read from or write to the string. - * - * @param handle the handle to the process - * @param structAddress the base address of the structure in memory - * @param platform the architecture of the process, either "32" or "64" - * @param encoding the encoding type of the string - */ -const STRUCTRON_TYPE_STRING = memoryjs => (handle, structAddress, platform, encoding = 'utf8') => ({ - read(buffer, offset) { - // get string length from `std::string` container - const length = buffer.readUInt32LE(offset + STDSTRING_LENGTH_OFFSET); - - // if length > 15, `std::string` has a pointer to the string - if (length > 15) { - const pointer = platform === '64' ? buffer.readBigInt64LE(offset) : buffer.readUInt32LE(offset); - return memoryjs.readMemory(handle, Number(pointer), memoryjs.STRING); - } - - // if length <= 15, `std::string` directly contains the string - return buffer.toString(encoding, offset, offset + length); - }, - write(value, context, offset) { - // address containing the length of the string - const lengthAddress = structAddress + offset + STDSTRING_LENGTH_OFFSET; - - // get existing `std::string` buffer - const bufferSize = platform === '64' ? SIZEOF_STDSTRING_64BIT : SIZEOF_STDSTRING_32BIT; - const existingBuffer = memoryjs.readBuffer(handle, structAddress + offset, bufferSize); - - // fetch length of string in memory (to determine if it's pointer based) - const length = memoryjs.readMemory(handle, lengthAddress, memoryjs.INT); - - if ((length > 15 && value.length <= 15) || (length <= 15 && value.length > 15)) { - // there are two ways strings are stored: directly or with a pointer, - // we can't go from one to the other (without introducing more complexity), - // so just skip the bytes to prevent crashing. if a pointer is used, we could - // technically write any length, but the next time we try writing, we will read - // the length and assume it's not stored via pointer and will lead to crashes - - // write existing buffer without changes - existingBuffer.copy(context.buffer, offset); - return; - } - - // write new length - memoryjs.writeMemory(handle, lengthAddress, value.length, memoryjs.UINT32); - existingBuffer.writeUInt32LE(value.length, STDSTRING_LENGTH_OFFSET); - - if (length > 15 && value.length > 15) { - // write new string in memory - const pointer = memoryjs.readMemory(handle, structAddress + offset, memoryjs.POINTER); - memoryjs.writeMemory(handle, pointer, value, memoryjs.STRING); - } else if (length <= 15 && value.length <= 15) { - // write new string directly into buffer - existingBuffer.write(value, encoding); - } - - // write our new `std::string` buffer into the buffer we are creating - existingBuffer.copy(context.buffer, offset); - }, - SIZE: platform === '64' ? SIZEOF_STDSTRING_64BIT : SIZEOF_STDSTRING_32BIT, -}); - -module.exports = { STRUCTRON_TYPE_STRING }; diff --git a/test/allocationTest.js b/test/allocationTest.js deleted file mode 100644 index cef6a9d..0000000 --- a/test/allocationTest.js +++ /dev/null @@ -1,16 +0,0 @@ -const memoryjs = require('../index'); -const processName = 'notepad.exe'; - -const processObject = memoryjs.openProcess(processName); - -const address = memoryjs.virtualAllocEx( - processObject.handle, - null, - 0x60, - memoryjs.MEM_RESERVE | memoryjs.MEM_COMMIT, - memoryjs.PAGE_EXECUTE_READWRITE, -); - -console.log(`Allocated address: 0x${address.toString(16).toUpperCase()}`); - -memoryjs.closeProcess(processObject.handle); diff --git a/test/debuggerTest.js b/test/debuggerTest.js deleted file mode 100644 index 6f579ed..0000000 --- a/test/debuggerTest.js +++ /dev/null @@ -1,44 +0,0 @@ -const memoryjs = require('../index'); -const processName = 'Testing Things.exe'; - -const processObject = memoryjs.openProcess(processName); -const processId = processObject.th32ProcessID; - -// Address of variable -const address = 0xEFFBF0; - -// When should we breakpoint? On read, write or execute -const trigger = memoryjs.TRIGGER_ACCESS; - -memoryjs.attachDebugger(processId); - -// There are 4 hardware registers: -// `memoryjs.DR0` through `memoryjs.DR3` -const registerToUse = memoryjs.DR0; - -// Our `address` references an integer variable. An integer -// is 4 bytes therefore we pass `4` to the `size` parameter. -const size = 4; -memoryjs.setHardwareBreakpoint(processId, address, registerToUse, trigger, size); - -// How long to wait for the debug event before timing out -const timeout = 100; - -// The interval duration must be the same or larger than the `timeout` value. -// `awaitDebugEvent` works by waiting a certain amount of time before timing out, -// therefore we only want to call the method again when we're sure the previous -// call has already timed out. -setInterval(() => { - // `debugEvent` can be null if no event occurred - const debugEvent = memoryjs.awaitDebugEvent(registerToUse, timeout); - - // If a breakpoint occurred, handle it - if (debugEvent) { - memoryjs.handleDebugEvent(debugEvent.processId, debugEvent.threadId); - } -}, timeout); - -// Don't forget to detatch the debugger! -// memoryjs.detatchDebugger(processId); - -memoryjs.closeProcess(processObject.handle); diff --git a/test/functionTest.js b/test/functionTest.js deleted file mode 100644 index da54f18..0000000 --- a/test/functionTest.js +++ /dev/null @@ -1,20 +0,0 @@ -const memoryjs = require('../index'); -const processName = 'FunctionTest.exe'; - -// TODO: Start the target process and obtain the absolute address of -// the function that you want to call and update the variable below. - -const processObject = memoryjs.openProcess(processName); - -const args = [{ type: memoryjs.T_FLOAT, value: 12.34 }]; -const returnType = memoryjs.T_FLOAT; - -const { - returnValue, - exitCode, -} = memoryjs.callFunction(processObject.handle, args, returnType, address); - -console.log(`Return value: ${returnValue}`); -console.log(`Exit code: ${exitCode}`); - -memoryjs.closeProcess(processObject.handle); diff --git a/test/memoryTest.js b/test/memoryTest.js deleted file mode 100644 index 3995ab6..0000000 --- a/test/memoryTest.js +++ /dev/null @@ -1,68 +0,0 @@ -const memoryjs = require('../index'); -const processName = 'MemoryTest.exe'; - -// TODO: Start the MemoryTest process, and check it's output against the outputs of this - -/* Example Output: - -$ node test/memoryTest -type address value -int 0x3AFCB4 2003818640 -dword 0x3AFCA8 2648673792 -short 0x3AFC9C 0 -long 0x3AFC90 0 -float 0x3AFC84 0 -double 0x3AFC74 4.031792002834e-312 -pointer 0x3AFC68 816043786240 -bool 0x3AFC5F false -string 0xB1FAA4 robert -*/ - -const processObject = memoryjs.openProcess(processName); - -const data = [{ - type: memoryjs.INT, - name: 'int', - address: 0x003AFCB4, -}, { - type: memoryjs.DWORD, - name: 'dword', - address: 0x003AFCA8, -}, { - type: memoryjs.SHORT, - name: 'short', - address: 0x003AFC9C, -}, { - type: memoryjs.LONG, - name: 'long', - address: 0x003AFC90, -}, { - type: memoryjs.FLOAT, - name: 'float', - address: 0x003AFC84, -}, { - type: memoryjs.DOUBLE, - name: 'double', - address: 0x003AFC74, -}, { - type: memoryjs.POINTER, - name: 'pointer', - address: 0x003AFC68, -}, { - type: memoryjs.BOOL, - name: 'bool', - address: 0x003AFC5F, -}, { - type: memoryjs.STRING, - name: 'string', - address: 0xb1faa4, -}]; - -console.log('type\taddress\t\tvalue'); - -data.forEach(({ type, name, address }) => { - const result = memoryjs.readMemory(processObject.handle, address, type); - console.log(`${name}\t0x${address.toString(16).toUpperCase()}\t${result}`); -}); - -memoryjs.closeProcess(processObject.handle); diff --git a/test/project.sln b/test/project.sln deleted file mode 100644 index fa1e230..0000000 --- a/test/project.sln +++ /dev/null @@ -1,37 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30503.244 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MemoryTest", "vcxproj\MemoryTest.vcxproj", "{67039DFF-7CB0-4034-8A19-470C8F8CADE9}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FunctionTest", "vcxproj\FunctionTest.vcxproj", "{D2E35EBA-E7AE-424D-B022-52440B65B235}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ProtectionTest", "vcxproj\ProtectionTest.vcxproj", "{8E68E3F6-3D5C-4D4B-AFCC-F13A9DA4D539}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x86 = Debug|x86 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {67039DFF-7CB0-4034-8A19-470C8F8CADE9}.Debug|x86.ActiveCfg = Debug|Win32 - {67039DFF-7CB0-4034-8A19-470C8F8CADE9}.Debug|x86.Build.0 = Debug|Win32 - {67039DFF-7CB0-4034-8A19-470C8F8CADE9}.Release|x86.ActiveCfg = Release|Win32 - {67039DFF-7CB0-4034-8A19-470C8F8CADE9}.Release|x86.Build.0 = Release|Win32 - {D2E35EBA-E7AE-424D-B022-52440B65B235}.Debug|x86.ActiveCfg = Debug|Win32 - {D2E35EBA-E7AE-424D-B022-52440B65B235}.Debug|x86.Build.0 = Debug|Win32 - {D2E35EBA-E7AE-424D-B022-52440B65B235}.Release|x86.ActiveCfg = Release|Win32 - {D2E35EBA-E7AE-424D-B022-52440B65B235}.Release|x86.Build.0 = Release|Win32 - {8E68E3F6-3D5C-4D4B-AFCC-F13A9DA4D539}.Debug|x86.ActiveCfg = Debug|Win32 - {8E68E3F6-3D5C-4D4B-AFCC-F13A9DA4D539}.Debug|x86.Build.0 = Debug|Win32 - {8E68E3F6-3D5C-4D4B-AFCC-F13A9DA4D539}.Release|x86.ActiveCfg = Release|Win32 - {8E68E3F6-3D5C-4D4B-AFCC-F13A9DA4D539}.Release|x86.Build.0 = Release|Win32 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {403312F9-8A55-4219-A498-9B79B46EC898} - EndGlobalSection -EndGlobal diff --git a/test/protectionTest.js b/test/protectionTest.js deleted file mode 100644 index ad8244d..0000000 --- a/test/protectionTest.js +++ /dev/null @@ -1,12 +0,0 @@ -const memoryjs = require('../index'); -const processName = 'ProtectionTest.exe'; - -// TODO: Start the TestTarget process, and monitor it's return value. - -const processObject = memoryjs.openProcess(processName); -console.log(processObject); - -memoryjs.setProtection(processObject.handle, 0x00FE102D, 4, memoryjs.PAGE_EXECUTE_READWRITE); -memoryjs.writeMemory(processObject.handle, 0x00FE102D, 1337, memoryjs.INT); - -memoryjs.closeProcess(processObject.handle); diff --git a/test/queryTest.js b/test/queryTest.js deleted file mode 100644 index 467072b..0000000 --- a/test/queryTest.js +++ /dev/null @@ -1,55 +0,0 @@ -const memoryjs = require('../index'); -const processName = 'chrome.exe'; - -const processObject = memoryjs.openProcess(processName); - -const regions = memoryjs.getRegions(processObject.handle).reverse().slice(0, 40); - -// Minimum lengths for each column -const lengths = { - BaseAddress: 'BaseAddress'.length, - AllocationBase: 'AllocationBase'.length, - AllocationProtect: 'AllocationProtect'.length, - RegionSize: 'RegionSize'.length, - State: 'State'.length, - Protect: 'Protect'.length, - Type: 'Type'.length, - szExeFile: 'szExeFile'.length, -}; - -// Calculate maximum lengths -regions.forEach((region) => { - Object.entries(region).forEach(([key, value]) => { - const formatted = `0x${value.toString(16)}`; - if (formatted.length > lengths[key]) { - lengths[key] = formatted.length; - } - }); -}); - -let text = ''; -Object.entries(lengths).forEach(([key, value]) => { - if (key === 'szExeFile') { - text += ` ${key}`.padEnd(value + 2, ' '); - } else { - text += key.padStart(value + 2, ' '); - text += ' |'; - } -}); -console.log(text); - -regions.forEach((region) => { - let text = ''; - Object.entries(region).forEach(([key, value]) => { - if (key === 'szExeFile') { - text += ` ${value}`.padEnd(lengths[key] + 2, ' '); - } else { - text += `0x${value.toString(16)}`.padStart(lengths[key] + 2, ' '); - text += ' |'; - } - }); - - console.log(text); -}); - -memoryjs.closeProcess(processObject.handle); diff --git a/test/src/MemoryTest.cpp b/test/src/MemoryTest.cpp deleted file mode 100644 index 48618da..0000000 --- a/test/src/MemoryTest.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include -#include -#include - -using namespace std; - -int main() -{ - cout << "type\taddress\t\tvalue" << endl; - - int _int = -2147483647; - cout << "int\t0x" << hex << &_int << dec << "\t" << _int << endl; - - DWORD _dword = 2147483647; - cout << "dword\t0x" << hex << &_dword << dec << "\t" << _dword << endl; - - short _short = -32768; - cout << "short\t0x" << hex << &_short << dec << "\t" << _short << endl; - - long _long = -2147483647; - cout << "long\t0x" << hex << &_long << dec << "\t" << _long << endl; - - float _float = 3.402823466e+38F / 2; - cout << "float\t0x" << hex << &_float << dec << "\t" << _float << endl; - - double _double = 2.2250738585072014e-308; - cout << "double\t0x" << hex << &_double << dec << "\t" << _double << endl; - - intptr_t _intptr_t = 2147483647; - cout << "pointer\t0x" << hex << &_intptr_t << dec << "\t" << _intptr_t << endl; - - bool _bool = true; - cout << "bool\t0x" << hex << &_bool << dec << "\t" << _bool << endl; - - string _string = "robert"; - cout << "string\t0x" << hex << (DWORD64)_string.c_str() << dec << "\t" << _string << endl; - - getchar(); - - return 0; -} - diff --git a/test/src/functionTest.cpp b/test/src/functionTest.cpp deleted file mode 100644 index a196524..0000000 --- a/test/src/functionTest.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include -#include -#include - -float testAdd(float a) { - std::cout << a << std::endl; - return a; -} - -int main() { - DWORD offset = (DWORD)testAdd - (DWORD)GetModuleHandle(NULL); - std::cout << "Function offset from base: 0x" << std::hex << offset << std::dec << std::endl; - std::cout << "Absolute: 0x" << std::hex << (DWORD)testAdd << std::dec << std::endl; - - getchar(); - return 0; -} \ No newline at end of file diff --git a/test/src/protectionTest.cpp b/test/src/protectionTest.cpp deleted file mode 100644 index a73356e..0000000 --- a/test/src/protectionTest.cpp +++ /dev/null @@ -1,71 +0,0 @@ -// TestTarget.cpp : Defines the entry point for the console application. -// -#include -#include "stdio.h" -#include - -int value = 0; - -__declspec(noinline) void routine() { - value = 100; -} - - -int main() -{ - printf("This program will return an exit code of 0 if you successfuly modify the memory in the given time frame.\n\ -\nWe want to modify the .code section of this exe.\n\ -Address of operation is likely around: 0x%08X.\n\ -Look for the MOV opcode + from its address to get the value address and modify it's memory you will need to change protection on the section of memory.\n", &routine); - - - // Assert that the program is compiled and running with PAGE_EXECUTE_READ on the routine method. - MEMORY_BASIC_INFORMATION mbi; - if (VirtualQuery(&routine, &mbi, sizeof(mbi)) == 0) { - printf("Unable to query memory protection for some reason.\n"); - return 1; - } - if (! (mbi.Protect & PAGE_EXECUTE_READ) ) { - printf("Warning: Expecting memory to be EXECUTE_READ\n"); - return 1; - } - - printf("The address of the value is at 0x%08X but please modify the code in the routine that is setting it to 100 to set it to something else.\n", &value); - printf("On MSVC 2017 Release mode x86 I'm getting this address 0x%08X.\n", ((char*)&routine) + 6); - - - int counter = 0; - while (1) { - // Intentionally set the value to this before and after the call. - value = 0xDEADBEEF; - - routine(); - - if (value != 100) { - routine(); - if (value != 100) { - return 0; - } - else { - printf("Please modify the code not the value in memory.\n"); - } - } - - value = 0xDEADBEEF; - - Sleep(1000); - counter++; - - if (value != 0xDEADBEEF) { - printf("You must modify the code not the value.\n"); - } - - if (counter > 60) { - break; - } - } - - printf("Attempt time expired closing application as failed.\n"); - return 1; -} - diff --git a/test/vcxproj/FunctionTest.vcxproj b/test/vcxproj/FunctionTest.vcxproj deleted file mode 100644 index caf19bf..0000000 --- a/test/vcxproj/FunctionTest.vcxproj +++ /dev/null @@ -1,48 +0,0 @@ - - - - Debug - Win32 - - - Release - Win32 - - - - {D2E35EBA-E7AE-424D-B022-52440B65B235} - - - - Application - v142 - v141 - - - false - $(SolutionDir) - $(Configuration)\$(ProjectName)\ - $(ProjectName) - - - false - $(SolutionDir) - $(Configuration)\$(ProjectName)\ - $(ProjectName) - - - - false - - - - - false - - - - - - - - \ No newline at end of file diff --git a/test/vcxproj/MemoryTest.vcxproj b/test/vcxproj/MemoryTest.vcxproj deleted file mode 100644 index 3c59799..0000000 --- a/test/vcxproj/MemoryTest.vcxproj +++ /dev/null @@ -1,48 +0,0 @@ - - - - Debug - Win32 - - - Release - Win32 - - - - {67039DFF-7CB0-4034-8A19-470C8F8CADE9} - - - - Application - v142 - v141 - - - false - $(SolutionDir) - $(Configuration)\$(ProjectName)\ - $(ProjectName) - - - false - $(SolutionDir) - $(Configuration)\$(ProjectName)\ - $(ProjectName) - - - - false - - - - - false - - - - - - - - \ No newline at end of file diff --git a/test/vcxproj/ProtectionTest.vcxproj b/test/vcxproj/ProtectionTest.vcxproj deleted file mode 100644 index dfacd7a..0000000 --- a/test/vcxproj/ProtectionTest.vcxproj +++ /dev/null @@ -1,48 +0,0 @@ - - - - Debug - Win32 - - - Release - Win32 - - - - {8E68E3F6-3D5C-4D4B-AFCC-F13A9DA4D539} - - - - Application - v142 - v141 - - - false - $(SolutionDir) - $(Configuration)\$(ProjectName)\ - $(ProjectName) - - - false - $(SolutionDir) - $(Configuration)\$(ProjectName)\ - $(ProjectName) - - - - false - - - - - false - - - - - - - - \ No newline at end of file From c20cca8fcd9789f64221708285e07dbca9ba7557 Mon Sep 17 00:00:00 2001 From: JoShMiQueL Date: Fri, 18 Apr 2025 19:47:56 +0200 Subject: [PATCH 04/11] feat: migrate implementation to TypeScript --- index.ts | 2 + native/debugger.cc | 169 +++++ native/debugger.h | 79 ++ native/dll.h | 83 ++ native/functions.cc | 17 + native/functions.h | 209 +++++ native/memory.cc | 21 + native/memory.h | 94 +++ native/memoryprocess.cc | 1605 +++++++++++++++++++++++++++++++++++++++ native/memoryprocess.h | 15 + native/module.cc | 94 +++ native/module.h | 18 + native/pattern.cc | 103 +++ native/pattern.h | 31 + native/process.cc | 95 +++ native/process.h | 27 + scripts/build.ts | 206 +++++ src/debugger.ts | 152 ++++ src/memoryprocess.ts | 547 +++++++++++++ src/types.ts | 175 +++++ src/utils.ts | 75 ++ 21 files changed, 3817 insertions(+) create mode 100644 index.ts create mode 100644 native/debugger.cc create mode 100644 native/debugger.h create mode 100644 native/dll.h create mode 100644 native/functions.cc create mode 100644 native/functions.h create mode 100644 native/memory.cc create mode 100644 native/memory.h create mode 100644 native/memoryprocess.cc create mode 100644 native/memoryprocess.h create mode 100644 native/module.cc create mode 100644 native/module.h create mode 100644 native/pattern.cc create mode 100644 native/pattern.h create mode 100644 native/process.cc create mode 100644 native/process.h create mode 100644 scripts/build.ts create mode 100644 src/debugger.ts create mode 100644 src/memoryprocess.ts create mode 100644 src/types.ts create mode 100644 src/utils.ts diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..b7a0dfb --- /dev/null +++ b/index.ts @@ -0,0 +1,2 @@ +export { default } from './src/memoryprocess'; +export * from './src/types'; \ No newline at end of file diff --git a/native/debugger.cc b/native/debugger.cc new file mode 100644 index 0000000..e979405 --- /dev/null +++ b/native/debugger.cc @@ -0,0 +1,169 @@ +/** + * Hardware debugger for memory.js + * A lot of the hardware debugging code is based on ReClass.NET + * https://github.com/ReClassNET/ReClass.NET + */ + +#include +#include +#include +#include +#include "debugger.h" +#include "module.h" + +bool debugger::attach(DWORD processId, bool killOnDetatch) { + if (DebugActiveProcess(processId) == 0) { + return false; + } + + DebugSetProcessKillOnExit(killOnDetatch); + return true; +} + +bool debugger::detatch(DWORD processId) { + return DebugActiveProcessStop(processId) != 0; +} + +bool debugger::setHardwareBreakpoint(DWORD processId, DWORD64 address, Register reg, int trigger, int size) { + const char* errorMessage = ""; + std::vector threads = module::getThreads(0, &errorMessage); + + if (strcmp(errorMessage, "")) { + return false; + } + + for (std::vector::size_type i = 0; i != threads.size(); i++) { + if (threads[i].th32OwnerProcessID != processId) { + continue; + } + + HANDLE threadHandle = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, false, threads[i].th32ThreadID); + + if (threadHandle == 0) { + continue; + } + + SuspendThread(threadHandle); + + CONTEXT context = { 0 }; + context.ContextFlags = CONTEXT_DEBUG_REGISTERS; + GetThreadContext(threadHandle, &context); + + DebugRegister7 dr7; + dr7.Value = context.Dr7; + + if (reg == Register::DR0) { + context.Dr0 = address; + dr7.G0 = true; + dr7.RW0 = trigger; + dr7.Len0 = size; + } + + if (reg == Register::DR1) { + context.Dr1 = address; + dr7.G1 = true; + dr7.RW1 = trigger; + dr7.Len1 = size; + } + + if (reg == Register::DR2) { + context.Dr2 = address; + dr7.G2 = true; + dr7.RW2 = trigger; + dr7.Len2 = size; + } + + if (reg == Register::DR3) { + context.Dr3 = address; + dr7.G3 = true; + dr7.RW3 = trigger; + dr7.Len3 = size; + } + + context.Dr7 = dr7.Value; + + SetThreadContext(threadHandle, &context); + ResumeThread(threadHandle); + CloseHandle(threadHandle); + + return true; + } + + return false; +} + +bool debugger::awaitDebugEvent(DWORD millisTimeout, DebugEvent *info) { + DEBUG_EVENT debugEvent = {}; + + if (WaitForDebugEvent(&debugEvent, millisTimeout) == 0) { + return false; + } + + if (debugEvent.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT) { + CloseHandle(debugEvent.u.CreateProcessInfo.hFile); + } + + if (debugEvent.dwDebugEventCode == LOAD_DLL_DEBUG_EVENT) { + CloseHandle(debugEvent.u.LoadDll.hFile); + } + + if (debugEvent.dwDebugEventCode == EXCEPTION_DEBUG_EVENT) { + EXCEPTION_DEBUG_INFO exception = debugEvent.u.Exception; + + info->processId = debugEvent.dwProcessId; + info->threadId = debugEvent.dwThreadId; + info->exceptionAddress = exception.ExceptionRecord.ExceptionAddress; + info->exceptionCode = exception.ExceptionRecord.ExceptionCode; + info->exceptionFlags = exception.ExceptionRecord.ExceptionFlags; + + HANDLE handle = OpenThread(THREAD_GET_CONTEXT, false, debugEvent.dwThreadId); + + if (handle == 0) { + return false; + } + + CONTEXT context = {}; + context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_DEBUG_REGISTERS; + GetThreadContext(handle, &context); + + DebugRegister6 dr6; + dr6.Value = context.Dr6; + + if (dr6.DR0) { + info->hardwareRegister = Register::DR0; + } + + if (dr6.DR1) { + info->hardwareRegister = Register::DR1; + } + + if (dr6.DR2) { + info->hardwareRegister = Register::DR2; + } + + if (dr6.DR3) { + info->hardwareRegister = Register::DR3; + } + + if (!dr6.DR0 && !dr6.DR1 && !dr6.DR2 && !dr6.DR3) { + info->hardwareRegister = Register::Invalid; + } + + CloseHandle(handle); + } else { + ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE); + } + + return true; +} + +bool debugger::handleDebugEvent(DWORD processId, DWORD threadId) { + return ContinueDebugEvent(processId, threadId, DBG_CONTINUE); + // if (status == DebugContinueStatus::Handled) { + // return ContinueDebugEvent(processId, threadId, DBG_CONTINUE) != 0; + // } + + // if (status == DebugContinueStatus::NotHandled) { + // return ContinueDebugEvent(processId, threadId, DBG_EXCEPTION_NOT_HANDLED) != 0; + // } +} \ No newline at end of file diff --git a/native/debugger.h b/native/debugger.h new file mode 100644 index 0000000..9679c6e --- /dev/null +++ b/native/debugger.h @@ -0,0 +1,79 @@ +#pragma once +#ifndef debugger_H +#define debugger_H +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include + +enum class Register { + Invalid = -0x1, + DR0 = 0x0, + DR1 = 0x1, + DR2 = 0x2, + DR3 = 0x3 +}; + +struct DebugRegister6 { + union { + uintptr_t Value; + struct { + unsigned DR0 : 1; + unsigned DR1 : 1; + unsigned DR2 : 1; + unsigned DR3 : 1; + unsigned Reserved : 9; + unsigned BD : 1; + unsigned BS : 1; + unsigned BT : 1; + }; + }; +}; + +struct DebugRegister7 { + union { + uintptr_t Value; + struct { + unsigned G0 : 1; + unsigned L0 : 1; + unsigned G1 : 1; + unsigned L1 : 1; + unsigned G2 : 1; + unsigned L2 : 1; + unsigned G3 : 1; + unsigned L3 : 1; + unsigned GE : 1; + unsigned LE : 1; + unsigned Reserved : 6; + unsigned RW0 : 2; + unsigned Len0 : 2; + unsigned RW1 : 2; + unsigned Len1 : 2; + unsigned RW2 : 2; + unsigned Len2 : 2; + unsigned RW3 : 2; + unsigned Len3 : 2; + }; + }; +}; + +struct DebugEvent { + DWORD processId; + DWORD threadId; + DWORD exceptionCode; + DWORD exceptionFlags; + void* exceptionAddress; + Register hardwareRegister; +}; + +namespace debugger { + bool attach(DWORD processId, bool killOnDetatch); + bool detatch(DWORD processId); + bool setHardwareBreakpoint(DWORD processId, DWORD64 address, Register reg, int trigger, int size); + bool awaitDebugEvent(DWORD millisTimeout, DebugEvent *info); + bool handleDebugEvent(DWORD processId, DWORD threadId); +} + +#endif +#pragma once diff --git a/native/dll.h b/native/dll.h new file mode 100644 index 0000000..c52b5a6 --- /dev/null +++ b/native/dll.h @@ -0,0 +1,83 @@ +#pragma once +#ifndef DLL_H +#define DLL_H +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include +#include + +namespace dll { + bool inject(HANDLE handle, std::string dllPath, const char** errorMessage, LPDWORD moduleHandle) { + // allocate space in target process memory for DLL path + LPVOID targetProcessPath = VirtualAllocEx(handle, NULL, dllPath.length() + 1, MEM_COMMIT, PAGE_EXECUTE_READWRITE); + + if (targetProcessPath == NULL) { + *errorMessage = "unable to allocate memory in target process"; + return false; + } + + // write DLL path to reserved memory space + if (WriteProcessMemory(handle, targetProcessPath, dllPath.c_str(), dllPath.length() + 1, 0) == 0) { + *errorMessage = "unable to to write dll path to target process"; + VirtualFreeEx(handle, targetProcessPath, 0, MEM_RELEASE); + return false; + } + + HMODULE kernel32 = LoadLibrary("kernel32"); + + if (kernel32 == 0) { + VirtualFreeEx(handle, targetProcessPath, 0, MEM_RELEASE); + *errorMessage = "unable to load kernel32"; + return false; + } + + // call LoadLibrary from target process + LPTHREAD_START_ROUTINE loadLibraryAddress = (LPTHREAD_START_ROUTINE) GetProcAddress(kernel32, "LoadLibraryA"); + HANDLE thread = CreateRemoteThread(handle, NULL, NULL, loadLibraryAddress, targetProcessPath, NULL, NULL); + + if (thread == NULL) { + *errorMessage = "unable to call LoadLibrary from target process"; + VirtualFreeEx(handle, targetProcessPath, 0, MEM_RELEASE); + return false; + } + + WaitForSingleObject(thread, INFINITE); + GetExitCodeThread(thread, moduleHandle); + + // free memory reserved in target process + VirtualFreeEx(handle, targetProcessPath, 0, MEM_RELEASE); + CloseHandle(thread); + + return *moduleHandle > 0; + } + + bool unload(HANDLE handle, const char** errorMessage, HMODULE moduleHandle) { + HMODULE kernel32 = LoadLibrary("kernel32"); + + if (kernel32 == 0) { + *errorMessage = "unable to load kernel32"; + return false; + } + + // call FreeLibrary from target process + LPTHREAD_START_ROUTINE freeLibraryAddress = (LPTHREAD_START_ROUTINE) GetProcAddress(kernel32, "FreeLibrary"); + HANDLE thread = CreateRemoteThread(handle, NULL, NULL, freeLibraryAddress, (void*)moduleHandle, NULL, NULL); + + if (thread == NULL) { + *errorMessage = "unable to call FreeLibrary from target process"; + return false; + } + + WaitForSingleObject(thread, INFINITE); + DWORD exitCode = -1; + GetExitCodeThread(thread, &exitCode); + CloseHandle(thread); + + return exitCode != 0; + } +} + +#endif +#pragma once diff --git a/native/functions.cc b/native/functions.cc new file mode 100644 index 0000000..1685a9d --- /dev/null +++ b/native/functions.cc @@ -0,0 +1,17 @@ +#include +#include +#include +#include +#include "functions.h" + +char functions::readChar(HANDLE hProcess, DWORD64 address) { + char value; + ReadProcessMemory(hProcess, (LPVOID)address, &value, sizeof(char), NULL); + return value; +} + +LPVOID functions::reserveString(HANDLE hProcess, const char* value, SIZE_T size) { + LPVOID memoryAddress = VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + WriteProcessMemory(hProcess, memoryAddress, value, size, NULL); + return memoryAddress; + } diff --git a/native/functions.h b/native/functions.h new file mode 100644 index 0000000..72679cb --- /dev/null +++ b/native/functions.h @@ -0,0 +1,209 @@ +#pragma once +#ifndef FUNCTIONS_H +#define FUNCTIONS_H +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include + +struct Call { + int returnValue; + std::string returnString; + DWORD exitCode; +}; + +namespace functions { + enum class Type { + T_VOID = 0x0, + T_STRING = 0x1, + T_CHAR = 0x2, + T_BOOL = 0x3, + T_INT = 0x4, + T_DOUBLE = 0x5, + T_FLOAT = 0x6 + }; + + struct Arg { + Type type; + LPVOID value; + }; + + LPVOID reserveString(HANDLE hProcess, const char* value, SIZE_T size); + char readChar(HANDLE hProcess, DWORD64 address); + + template + Call call(HANDLE pHandle, std::vector args, Type returnType, DWORD64 address, const char** errorMessage) { + std::vector argShellcode; + + std::reverse(args.begin(), args.end()); + + for (auto &arg : args) { + // 0x68: PUSH imm16/imm32 + // 0x6A: PUSH imm8 + + if (arg.type == Type::T_INT || arg.type == Type::T_FLOAT) { + argShellcode.push_back(0x68); + int value = *static_cast(arg.value); + + // Little endian representation + for (int i = 0; i < 4; i++) { + int shifted = (value >> (i * 8)) & 0xFF; + argShellcode.push_back(shifted); + } + + continue; + } + + if (arg.type == Type::T_STRING) { + argShellcode.push_back(0x68); + std::string value = *static_cast(arg.value); + LPVOID address = functions::reserveString(pHandle, value.c_str(), value.length()); + + // Little endian representation + for (int i = 0; i < sizeof(LPVOID); i++) { + unsigned char byte = ((reinterpret_cast(address) >> (i * 8)) & 0xFF); + argShellcode.push_back(byte); + } + + continue; + } + + argShellcode.push_back(0x6A); + unsigned char value = *static_cast(arg.value); + argShellcode.push_back(value); + } + + // 83: ADD r/m16/32 imm8 + std::vector callShellcode = { + // call 0x00000000 + 0xE8, 0x00, 0x00, 0x00, 0x00, + // add esp, [arg count * 4] + 0x83, 0xC4, (unsigned char)(args.size() * 0x4), + }; + + LPVOID returnValuePointer = 0; + if (returnType != Type::T_VOID) { + // We will reserve memory for where we want to store the result, + // and move the return value to this address. + returnValuePointer = VirtualAllocEx(pHandle, NULL, sizeof(returnDataType), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); + + if (returnType == Type::T_FLOAT) { + // fstp DWORD PTR [0x12345678] + // when `call` is executed, if return type is float, it's stored + // in fpu registers (st0 through st7). we can use the `fst` + // instruction to move st0 to a place in memory + // D9 FSTP m32real ST Store Floating Point Value and Pop + // D9 = for m32 + callShellcode.push_back(0xD9); + callShellcode.push_back(0x1C); + callShellcode.push_back(0x25); + } else if (returnType == Type::T_DOUBLE) { + // fstp QWORD PTR [0x12345678] + // DD FSTP m64real ST Store Floating Point Value and Pop + // DD = for m64 + callShellcode.push_back(0xDD); + callShellcode.push_back(0x1C); + callShellcode.push_back(0x25); + } else { + // mov [0x1234], eax + // A3: MOVE moffs16/32 eAX + // Call routines places return value inside EAX + callShellcode.push_back(0xA3); + } + + for (int i = 0; i < sizeof(LPVOID); i++) { + unsigned char byte = ((reinterpret_cast(returnValuePointer) >> (i * 8)) & 0xFF); + callShellcode.push_back(byte); + } + } + + // C3: return + callShellcode.push_back(0xC3); + + // concatenate the arg shellcode with the calling shellcode + std::vector shellcode; + shellcode.reserve(argShellcode.size() + callShellcode.size()); + shellcode.insert(shellcode.end(), argShellcode.begin(), argShellcode.end()); + shellcode.insert(shellcode.end(), callShellcode.begin(), callShellcode.end()); + + // 5 = 0xE8 (call) + 4 empty bytes for where the address will go + int addessShellcodeOffset = argShellcode.size() + 5; + + // Allocate space for the shellcode + SIZE_T size = shellcode.size() * sizeof(unsigned char); + LPVOID pShellcode = VirtualAllocEx(pHandle, NULL, size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); + + // `call` opcode takes relative address, so calculate the relative address + // taking into account where the shellcode will be written in memory + DWORD64 relative = address - (uintptr_t)pShellcode - addessShellcodeOffset; + + // Write the relative address to the shellcode + for (int i = 0; i < 4; i++) { + int shifted = (relative >> (i * 8)) & 0xFF; + + // argShellcode.size() will offset to the `0xE8` opcode, add 1 to offset that instruction + shellcode.at(argShellcode.size() + 1 + i) = shifted; + } + + // Write the shellcode + WriteProcessMemory(pHandle, pShellcode, shellcode.data(), size, NULL); + + // Execute the shellcode + HANDLE thread = CreateRemoteThread(pHandle, NULL, NULL, (LPTHREAD_START_ROUTINE)pShellcode, NULL, NULL, NULL); + + Call data = { 0, "", (DWORD) -1 }; + + if (thread == NULL) { + *errorMessage = "unable to create remote thread."; + return data; + } + + WaitForSingleObject(thread, INFINITE); + GetExitCodeThread(thread, &data.exitCode); + + if (returnType != Type::T_VOID && returnType != Type::T_STRING) { + ReadProcessMemory(pHandle, (LPVOID)returnValuePointer, &data.returnValue, sizeof(int), NULL); + VirtualFreeEx(pHandle, returnValuePointer, 0, MEM_RELEASE); + } + + if (returnType == Type::T_STRING) { + // String is stored in memory somewhere + // When returning a string, the address of the string is placed in EAX. + // So we read the current returnValuePointer address to get the actual address of the string + ReadProcessMemory(pHandle, (LPVOID)returnValuePointer, &returnValuePointer, sizeof(int), NULL); + + std::vector chars; + int offset = 0x0; + while (true) { + char c = functions::readChar(pHandle, (DWORD64)returnValuePointer + offset); + chars.push_back(c); + + // break at 1 million chars + if (offset == (sizeof(char) * 1000000)) { + chars.clear(); + break; + } + + // break at terminator (end of string) + if (c == '\0') { + break; + } + + // go to next char + offset += sizeof(char); + } + + std::string str(chars.begin(), chars.end()); + // TODO: pass str as LPVOID and cast back to string + data.returnString = str; + } + + VirtualFreeEx(pHandle, pShellcode, 0, MEM_RELEASE); + + return data; + } +} + +#endif +#pragma once diff --git a/native/memory.cc b/native/memory.cc new file mode 100644 index 0000000..6943dba --- /dev/null +++ b/native/memory.cc @@ -0,0 +1,21 @@ +#include +#include +#include +#include +#include "memory.h" + +memory::memory() {} +memory::~memory() {} + +std::vector memory::getRegions(HANDLE hProcess) { + std::vector regions; + + MEMORY_BASIC_INFORMATION region; + DWORD64 address; + + for (address = 0; VirtualQueryEx(hProcess, (LPVOID)address, ®ion, sizeof(region)) == sizeof(region); address += region.RegionSize) { + regions.push_back(region); + } + + return regions; +} \ No newline at end of file diff --git a/native/memory.h b/native/memory.h new file mode 100644 index 0000000..2c47139 --- /dev/null +++ b/native/memory.h @@ -0,0 +1,94 @@ +#pragma once +#ifndef MEMORY_H +#define MEMORY_H +#define WIN32_LEAN_AND_MEAN + +#include +#include + +class memory { +public: + memory(); + ~memory(); + std::vector getRegions(HANDLE hProcess); + + template + dataType readMemory(HANDLE hProcess, DWORD64 address) { + dataType cRead; + ReadProcessMemory(hProcess, (LPVOID)address, &cRead, sizeof(dataType), NULL); + return cRead; + } + + BOOL readBuffer(HANDLE hProcess, DWORD64 address, SIZE_T size, char* dstBuffer) { + return ReadProcessMemory(hProcess, (LPVOID)address, dstBuffer, size, NULL); + } + + char readChar(HANDLE hProcess, DWORD64 address) { + char value; + ReadProcessMemory(hProcess, (LPVOID)address, &value, sizeof(char), NULL); + return value; + } + + BOOL readString(HANDLE hProcess, DWORD64 address, std::string* pString) { + int length = 0; + int BATCH_SIZE = 256; + char* data = (char*) malloc(sizeof(char) * BATCH_SIZE); + while (length <= BATCH_SIZE * 4096) { + BOOL success = readBuffer(hProcess, address + length, BATCH_SIZE, data); + + if (success == 0) { + free(data); + break; + } + + for (const char* ptr = data; ptr - data < BATCH_SIZE; ++ptr) { + if (*ptr == '\0') { + length += ptr - data + 1; + + char* buffer = (char*) malloc(length); + readBuffer(hProcess, address, length, buffer); + + *pString = std::string(buffer); + + free(data); + free(buffer); + + return TRUE; + } + } + + length += BATCH_SIZE; + } + + return FALSE; + } + + template + void writeMemory(HANDLE hProcess, DWORD64 address, dataType value) { + WriteProcessMemory(hProcess, (LPVOID)address, &value, sizeof(dataType), NULL); + } + + template + void writeMemory(HANDLE hProcess, DWORD64 address, dataType value, SIZE_T size) { + LPVOID buffer = value; + + if (typeid(dataType) != typeid(char*)) { + buffer = &value; + } + + WriteProcessMemory(hProcess, (LPVOID)address, buffer, size, NULL); + } + + // Write String, Method 1: Utf8Value is converted to string, get pointer and length from string + // template <> + // void writeMemory(HANDLE hProcess, DWORD address, std::string value) { + // WriteProcessMemory(hProcess, (LPVOID)address, value.c_str(), value.length(), NULL); + // } + + // Write String, Method 2: get pointer and length from Utf8Value directly + void writeMemory(HANDLE hProcess, DWORD64 address, char* value, SIZE_T size) { + WriteProcessMemory(hProcess, (LPVOID)address, value, size, NULL); + } +}; +#endif +#pragma once \ No newline at end of file diff --git a/native/memoryprocess.cc b/native/memoryprocess.cc new file mode 100644 index 0000000..11c2423 --- /dev/null +++ b/native/memoryprocess.cc @@ -0,0 +1,1605 @@ +#include +#include +#include +#include +#include "module.h" +#include "process.h" +#include "memoryprocess.h" +#include "memory.h" +#include "pattern.h" +#include "functions.h" +#include "dll.h" +#include "debugger.h" + +#pragma comment(lib, "psapi.lib") +#pragma comment(lib, "onecore.lib") + + +process Process; +// module Module; +memory Memory; +pattern Pattern; +// functions Functions; + +struct Vector3 { + float x, y, z; +}; + +struct Vector4 { + float w, x, y, z; +}; + +Napi::Value openProcess(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 1 && args.Length() != 2) { + Napi::Error::New(env, "requires 1 argument, or 2 arguments if a callback is being used").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsString() && !args[0].IsNumber()) { + Napi::Error::New(env, "first argument must be a string or a number").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 2 && !args[1].IsFunction()) { + Napi::Error::New(env, "second argument must be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + // Define error message that may be set by the function that opens the process + const char* errorMessage = ""; + + process::Pair pair; + + if (args[0].IsString()) { + std::string processName(args[0].As().Utf8Value()); + pair = Process.openProcess(processName.c_str(), &errorMessage); + + // In case it failed to open, let's keep retrying + // while(!strcmp(process.szExeFile, "")) { + // process = Process.openProcess((char*) *(processName), &errorMessage); + // }; + } + + if (args[0].IsNumber()) { + pair = Process.openProcess(args[0].As().Uint32Value(), &errorMessage); + + // In case it failed to open, let's keep retrying + // while(!strcmp(process.szExeFile, "")) { + // process = Process.openProcess(info[0].As().Uint32Value(), &errorMessage); + // }; + } + + // If an error message was returned from the function that opens the process, throw the error. + // Only throw an error if there is no callback (if there's a callback, the error is passed there). + if (strcmp(errorMessage, "") && args.Length() != 2) { + Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); + return env.Null(); + } + + // Create a v8 Object (JSON) to store the process information + Napi::Object processInfo = Napi::Object::New(env); + + processInfo.Set(Napi::String::New(env, "dwSize"), Napi::Value::From(env, (int)pair.process.dwSize)); + processInfo.Set(Napi::String::New(env, "th32ProcessID"), Napi::Value::From(env, (int)pair.process.th32ProcessID)); + processInfo.Set(Napi::String::New(env, "cntThreads"), Napi::Value::From(env, (int)pair.process.cntThreads)); + processInfo.Set(Napi::String::New(env, "th32ParentProcessID"), Napi::Value::From(env, (int)pair.process.th32ParentProcessID)); + processInfo.Set(Napi::String::New(env, "pcPriClassBase"), Napi::Value::From(env, (int)pair.process.pcPriClassBase)); + processInfo.Set(Napi::String::New(env, "szExeFile"), Napi::String::New(env, pair.process.szExeFile)); + processInfo.Set(Napi::String::New(env, "handle"), Napi::Value::From(env, (uintptr_t)pair.handle)); + + DWORD64 base = module::getBaseAddress(pair.process.szExeFile, pair.process.th32ProcessID); + processInfo.Set(Napi::String::New(env, "modBaseAddr"), Napi::Value::From(env, (uintptr_t)base)); + + // openProcess can either take one argument or can take + // two arguments for asychronous use (second argument is the callback) + if (args.Length() == 2) { + // Callback to let the user handle with the information + Napi::Function callback = args[1].As(); + callback.Call(env.Global(), { Napi::String::New(env, errorMessage), processInfo }); + return env.Null(); + } else { + // return JSON + return processInfo; + } +} + +Napi::Value closeHandle(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + BOOL success = CloseHandle((HANDLE)args[0].As().Int64Value()); + return Napi::Boolean::New(env, success); +} + +Napi::Value getProcesses(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() > 1) { + Napi::Error::New(env, "requires either 0 arguments or 1 argument if a callback is being used").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 1 && !args[0].IsFunction()) { + Napi::Error::New(env, "first argument must be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + // Define error message that may be set by the function that gets the processes + const char* errorMessage = ""; + + std::vector processEntries = Process.getProcesses(&errorMessage); + + // If an error message was returned from the function that gets the processes, throw the error. + // Only throw an error if there is no callback (if there's a callback, the error is passed there). + if (strcmp(errorMessage, "") && args.Length() != 1) { + Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); + return env.Null(); + } + + // Creates v8 array with the size being that of the processEntries vector processes is an array of JavaScript objects + Napi::Array processes = Napi::Array::New(env, processEntries.size()); + + // Loop over all processes found + for (std::vector::size_type i = 0; i != processEntries.size(); i++) { + // Create a v8 object to store the current process' information + Napi::Object process = Napi::Object::New(env); + + process.Set(Napi::String::New(env, "cntThreads"), Napi::Value::From(env, (int)processEntries[i].cntThreads)); + process.Set(Napi::String::New(env, "szExeFile"), Napi::String::New(env, processEntries[i].szExeFile)); + process.Set(Napi::String::New(env, "th32ProcessID"), Napi::Value::From(env, (int)processEntries[i].th32ProcessID)); + process.Set(Napi::String::New(env, "th32ParentProcessID"), Napi::Value::From(env, (int)processEntries[i].th32ParentProcessID)); + process.Set(Napi::String::New(env, "pcPriClassBase"), Napi::Value::From(env, (int)processEntries[i].pcPriClassBase)); + + // Push the object to the array + processes.Set(i, process); + } + + /* getProcesses can either take no arguments or one argument + one argument is for asychronous use (the callback) */ + if (args.Length() == 1) { + // Callback to let the user handle with the information + Napi::Function callback = args[0].As(); + callback.Call(env.Global(), { Napi::String::New(env, errorMessage), processes }); + return env.Null(); + } else { + // return JSON + return processes; + } +} + +Napi::Value getModules(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 1 && args.Length() != 2) { + Napi::Error::New(env, "requires 1 argument, or 2 arguments if a callback is being used").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber()) { + Napi::Error::New(env, "first argument must be a number").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 2 && !args[1].IsFunction()) { + Napi::Error::New(env, "first argument must be a number, second argument must be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + // Define error message that may be set by the function that gets the modules + const char* errorMessage = ""; + + std::vector moduleEntries = module::getModules(args[0].As().Int32Value(), &errorMessage); + + // If an error message was returned from the function getting the modules, throw the error. + // Only throw an error if there is no callback (if there's a callback, the error is passed there). + if (strcmp(errorMessage, "") && args.Length() != 2) { + Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); + return env.Null(); + } + + // Creates v8 array with the size being that of the moduleEntries vector + // modules is an array of JavaScript objects + Napi::Array modules = Napi::Array::New(env, moduleEntries.size()); + + // Loop over all modules found + for (std::vector::size_type i = 0; i != moduleEntries.size(); i++) { + // Create a v8 object to store the current module's information + Napi::Object module = Napi::Object::New(env); + + module.Set(Napi::String::New(env, "modBaseAddr"), Napi::Value::From(env, (uintptr_t)moduleEntries[i].modBaseAddr)); + module.Set(Napi::String::New(env, "modBaseSize"), Napi::Value::From(env, (int)moduleEntries[i].modBaseSize)); + module.Set(Napi::String::New(env, "szExePath"), Napi::String::New(env, moduleEntries[i].szExePath)); + module.Set(Napi::String::New(env, "szModule"), Napi::String::New(env, moduleEntries[i].szModule)); + module.Set(Napi::String::New(env, "th32ProcessID"), Napi::Value::From(env, (int)moduleEntries[i].th32ProcessID)); + module.Set(Napi::String::New(env, "GlblcntUsage"), Napi::Value::From(env, (int)moduleEntries[i].GlblcntUsage)); + + // Push the object to the array + modules.Set(i, module); + } + + // getModules can either take one argument or two arguments + // one/two arguments is for asychronous use (the callback) + if (args.Length() == 2) { + // Callback to let the user handle with the information + Napi::Function callback = args[1].As(); + callback.Call(env.Global(), { Napi::String::New(env, errorMessage), modules }); + return env.Null(); + } else { + // return JSON + return modules; + } +} + +Napi::Value findModule(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 1 && args.Length() != 2 && args.Length() != 3) { + Napi::Error::New(env, "requires 1 argument, 2 arguments, or 3 arguments if a callback is being used").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsString() && !args[1].IsNumber()) { + Napi::Error::New(env, "first argument must be a string, second argument must be a number").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 3 && !args[2].IsFunction()) { + Napi::Error::New(env, "third argument must be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + std::string moduleName(args[0].As().Utf8Value()); + + // Define error message that may be set by the function that gets the modules + const char* errorMessage = ""; + + MODULEENTRY32 module = module::findModule(moduleName.c_str(), args[1].As().Int32Value(), &errorMessage); + + // If an error message was returned from the function getting the module, throw the error. + // Only throw an error if there is no callback (if there's a callback, the error is passed there). + if (strcmp(errorMessage, "") && args.Length() != 3) { + Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); + return env.Null(); + } + + // In case it failed to open, let's keep retrying + while (!strcmp(module.szExePath, "")) { + module = module::findModule(moduleName.c_str(), args[1].As().Int32Value(), &errorMessage); + }; + + // Create a v8 Object (JSON) to store the process information + Napi::Object moduleInfo = Napi::Object::New(env); + + moduleInfo.Set(Napi::String::New(env, "modBaseAddr"), Napi::Value::From(env, (uintptr_t)module.modBaseAddr)); + moduleInfo.Set(Napi::String::New(env, "modBaseSize"), Napi::Value::From(env, (int)module.modBaseSize)); + moduleInfo.Set(Napi::String::New(env, "szExePath"), Napi::String::New(env, module.szExePath)); + moduleInfo.Set(Napi::String::New(env, "szModule"), Napi::String::New(env, module.szModule)); + moduleInfo.Set(Napi::String::New(env, "th32ProcessID"), Napi::Value::From(env, (int)module.th32ProcessID)); + moduleInfo.Set(Napi::String::New(env, "GlblcntUsage"), Napi::Value::From(env, (int)module.GlblcntUsage)); + + // findModule can either take one or two arguments, + // three arguments for asychronous use (third argument is the callback) + if (args.Length() == 3) { + // Callback to let the user handle with the information + Napi::Function callback = args[2].As(); + callback.Call(env.Global(), { Napi::String::New(env, errorMessage), moduleInfo }); + return env.Null(); + } else { + // return JSON + return moduleInfo; + } +} + +Napi::Value readMemory(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 3 && args.Length() != 4) { + Napi::Error::New(env, "requires 3 arguments, or 4 arguments if a callback is being used").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() && !args[1].IsNumber() && !args[2].IsString()) { + Napi::Error::New(env, "first and second argument must be a number, third argument must be a string").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 4 && !args[3].IsFunction()) { + Napi::Error::New(env, "fourth argument must be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + std::string dataTypeArg(args[2].As().Utf8Value()); + const char* dataType = dataTypeArg.c_str(); + + // Define the error message that will be set if no data type is recognised + const char* errorMessage = ""; + Napi::Value retVal = env.Null(); + + HANDLE handle = (HANDLE)args[0].As().Int64Value(); + + DWORD64 address; + if (args[1].As().IsBigInt()) { + bool lossless; + address = args[1].As().Uint64Value(&lossless); + } else { + address = args[1].As().Int64Value(); + } + + if (!strcmp(dataType, "int8") || !strcmp(dataType, "byte") || !strcmp(dataType, "char")) { + + int8_t result = Memory.readMemory(handle, address); + retVal = Napi::Value::From(env, result); + + } else if (!strcmp(dataType, "uint8") || !strcmp(dataType, "ubyte") || !strcmp(dataType, "uchar")) { + + uint8_t result = Memory.readMemory(handle, address); + retVal = Napi::Value::From(env, result); + + } else if (!strcmp(dataType, "int16") || !strcmp(dataType, "short")) { + + int16_t result = Memory.readMemory(handle, address); + retVal = Napi::Value::From(env, result); + + } else if (!strcmp(dataType, "uint16") || !strcmp(dataType, "ushort") || !strcmp(dataType, "word")) { + + uint16_t result = Memory.readMemory(handle, address); + retVal = Napi::Value::From(env, result); + + } else if (!strcmp(dataType, "int32") || !strcmp(dataType, "int") || !strcmp(dataType, "long")) { + + int32_t result = Memory.readMemory(handle, address); + retVal = Napi::Value::From(env, result); + + } else if (!strcmp(dataType, "uint32") || !strcmp(dataType, "uint") || !strcmp(dataType, "ulong") || !strcmp(dataType, "dword")) { + + uint32_t result = Memory.readMemory(handle, address); + retVal = Napi::Value::From(env, result); + + } else if (!strcmp(dataType, "int64")) { + + int64_t result = Memory.readMemory(handle, address); + retVal = Napi::Value::From(env, Napi::BigInt::New(env, result)); + + } else if (!strcmp(dataType, "uint64")) { + + uint64_t result = Memory.readMemory(handle, address); + retVal = Napi::Value::From(env, Napi::BigInt::New(env, result)); + + } else if (!strcmp(dataType, "float")) { + + float result = Memory.readMemory(handle, address); + retVal = Napi::Value::From(env, result); + + } else if (!strcmp(dataType, "double")) { + + double result = Memory.readMemory(handle, address); + retVal = Napi::Value::From(env, result); + + } else if (!strcmp(dataType, "ptr") || !strcmp(dataType, "pointer")) { + + intptr_t result = Memory.readMemory(handle, address); + + if (sizeof(intptr_t) == 8) { + retVal = Napi::Value::From(env, Napi::BigInt::New(env, (int64_t) result)); + } else { + retVal = Napi::Value::From(env, result); + } + + } else if (!strcmp(dataType, "uptr") || !strcmp(dataType, "upointer")) { + + uintptr_t result = Memory.readMemory(handle, address); + + if (sizeof(uintptr_t) == 8) { + retVal = Napi::Value::From(env, Napi::BigInt::New(env, (uint64_t) result)); + } else { + retVal = Napi::Value::From(env, result); + } + + } else if (!strcmp(dataType, "bool") || !strcmp(dataType, "boolean")) { + + bool result = Memory.readMemory(handle, address); + retVal = Napi::Boolean::New(env, result); + + } else if (!strcmp(dataType, "string") || !strcmp(dataType, "str")) { + + std::string str; + if (!Memory.readString(handle, address, &str)) { + errorMessage = "unable to read string"; + } else { + retVal = Napi::String::New(env, str); + } + + } else if (!strcmp(dataType, "vector3") || !strcmp(dataType, "vec3")) { + + Vector3 result = Memory.readMemory(handle, address); + Napi::Object moduleInfo = Napi::Object::New(env); + moduleInfo.Set(Napi::String::New(env, "x"), Napi::Value::From(env, result.x)); + moduleInfo.Set(Napi::String::New(env, "y"), Napi::Value::From(env, result.y)); + moduleInfo.Set(Napi::String::New(env, "z"), Napi::Value::From(env, result.z)); + retVal = moduleInfo; + + } else if (!strcmp(dataType, "vector4") || !strcmp(dataType, "vec4")) { + + Vector4 result = Memory.readMemory(handle, address); + Napi::Object moduleInfo = Napi::Object::New(env); + moduleInfo.Set(Napi::String::New(env, "w"), Napi::Value::From(env, result.w)); + moduleInfo.Set(Napi::String::New(env, "x"), Napi::Value::From(env, result.x)); + moduleInfo.Set(Napi::String::New(env, "y"), Napi::Value::From(env, result.y)); + moduleInfo.Set(Napi::String::New(env, "z"), Napi::Value::From(env, result.z)); + retVal = moduleInfo; + + } else { + errorMessage = "unexpected data type"; + } + + if (strcmp(errorMessage, "") && args.Length() != 4) { + Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 4) { + Napi::Function callback = args[3].As(); + callback.Call(env.Global(), { Napi::String::New(env, errorMessage), retVal }); + return env.Null(); + } else { + return retVal; + } +} + +Napi::Value readBuffer(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 3 && args.Length() != 4) { + Napi::Error::New(env, "requires 3 arguments, or 4 arguments if a callback is being used").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() && !args[1].IsNumber() && !args[2].IsNumber()) { + Napi::Error::New(env, "first, second and third arguments must be a number").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 4 && !args[3].IsFunction()) { + Napi::Error::New(env, "fourth argument must be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + HANDLE handle = (HANDLE)args[0].As().Int64Value(); + + DWORD64 address; + if (args[1].As().IsBigInt()) { + bool lossless; + address = args[1].As().Uint64Value(&lossless); + } else { + address = args[1].As().Int64Value(); + } + + SIZE_T size = args[2].As().Int64Value(); + + // To fix the memory leak problem that was happening here, we need to release the + // temporary buffer we create after we're done creating a Napi::Buffer from it. + // Napi::Buffer::New doesn't free the memory, so it has be done manually + // but it can segfault when the memory is freed before being accessed. + // The solution is to use Napi::Buffer::Copy, and then we can manually free it. + // + // see: https://github.com/nodejs/node/issues/40936 + // see: https://sagivo.com/2015/09/30/Go-Native-Calling-C-From-NodeJS.html + char* data = (char*) malloc(sizeof(char) * size); + Memory.readBuffer(handle, address, size, data); + + Napi::Buffer buffer = Napi::Buffer::Copy(env, data, size); + free(data); + + if (args.Length() == 4) { + Napi::Function callback = args[3].As(); + callback.Call(env.Global(), { Napi::String::New(env, ""), buffer }); + return env.Null(); + } else { + return buffer; + } +} + +Napi::Value writeMemory(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 4) { + Napi::Error::New(env, "requires 4 arguments").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() && !args[1].IsNumber() && !args[3].IsString()) { + Napi::Error::New(env, "first and second argument must be a number, third argument must be a string").ThrowAsJavaScriptException(); + return env.Null(); + } + + std::string dataTypeArg(args[3].As().Utf8Value()); + const char* dataType = dataTypeArg.c_str(); + + HANDLE handle = (HANDLE)args[0].As().Int64Value(); + + DWORD64 address; + if (args[1].As().IsBigInt()) { + bool lossless; + address = args[1].As().Uint64Value(&lossless); + } else { + address = args[1].As().Int64Value(); + } + + if (!strcmp(dataType, "int8") || !strcmp(dataType, "byte") || !strcmp(dataType, "char")) { + + Memory.writeMemory(handle, address, args[2].As().Int32Value()); + + } else if (!strcmp(dataType, "uint8") || !strcmp(dataType, "ubyte") || !strcmp(dataType, "uchar")) { + + Memory.writeMemory(handle, address, args[2].As().Uint32Value()); + + } else if (!strcmp(dataType, "int16") || !strcmp(dataType, "short")) { + + Memory.writeMemory(handle, address, args[2].As().Int32Value()); + + } else if (!strcmp(dataType, "uint16") || !strcmp(dataType, "ushort") || !strcmp(dataType, "word")) { + + Memory.writeMemory(handle, address, args[2].As().Uint32Value()); + + } else if (!strcmp(dataType, "int32") || !strcmp(dataType, "int") || !strcmp(dataType, "long")) { + + Memory.writeMemory(handle, address, args[2].As().Int32Value()); + + } else if (!strcmp(dataType, "uint32") || !strcmp(dataType, "uint") || !strcmp(dataType, "ulong") || !strcmp(dataType, "dword")) { + + Memory.writeMemory(handle, address, args[2].As().Uint32Value()); + + } else if (!strcmp(dataType, "int64")) { + + if (args[2].As().IsBigInt()) { + bool lossless; + Memory.writeMemory(handle, address, args[2].As().Int64Value(&lossless)); + } else { + Memory.writeMemory(handle, address, args[2].As().Int64Value()); + } + + } else if (!strcmp(dataType, "uint64")) { + + if (args[2].As().IsBigInt()) { + bool lossless; + Memory.writeMemory(handle, address, args[2].As().Uint64Value(&lossless)); + } else { + Memory.writeMemory(handle, address, args[2].As().Int64Value()); + } + + } else if (!strcmp(dataType, "float")) { + + Memory.writeMemory(handle, address, args[2].As().FloatValue()); + + } else if (!strcmp(dataType, "double")) { + + Memory.writeMemory(handle, address, args[2].As().DoubleValue()); + + } else if (!strcmp(dataType, "ptr") || !strcmp(dataType, "pointer")) { + + Napi::BigInt valueBigInt = args[2].As(); + + if (sizeof(intptr_t) == 8 && !valueBigInt.IsBigInt()) { + std::string error = "Writing 'ptr' or 'pointer' on 64 bit target build requires you to supply a BigInt."; + Napi::Error::New(env, error).ThrowAsJavaScriptException(); + return env.Null(); + } + + if (valueBigInt.IsBigInt()) { + bool lossless; + Memory.writeMemory(handle, address, valueBigInt.Int64Value(&lossless)); + } else { + Memory.writeMemory(handle, address, args[2].As().Int32Value()); + } + + } else if (!strcmp(dataType, "uptr") || !strcmp(dataType, "upointer")) { + + Napi::BigInt valueBigInt = args[2].As(); + + if (sizeof(uintptr_t) == 8 && !valueBigInt.IsBigInt()) { + std::string error = "Writing 'ptr' or 'pointer' on 64 bit target build requires you to supply a BigInt."; + Napi::Error::New(env, error).ThrowAsJavaScriptException(); + return env.Null(); + } + + if (valueBigInt.IsBigInt()) { + bool lossless; + Memory.writeMemory(handle, address, valueBigInt.Uint64Value(&lossless)); + } else { + Memory.writeMemory(handle, address, args[2].As().Uint32Value()); + } + + } else if (!strcmp(dataType, "bool") || !strcmp(dataType, "boolean")) { + + Memory.writeMemory(handle, address, args[2].As().Value()); + + } else if (!strcmp(dataType, "string") || !strcmp(dataType, "str")) { + + std::string valueParam(args[2].As().Utf8Value()); + valueParam.append("", 1); + + // Write String, Method 1 + //Memory.writeMemory(handle, address, std::string(*valueParam)); + + // Write String, Method 2 + Memory.writeMemory(handle, address, (char*) valueParam.data(), valueParam.size()); + + } else if (!strcmp(dataType, "vector3") || !strcmp(dataType, "vec3")) { + + Napi::Object value = args[2].As(); + Vector3 vector = { + value.Get(Napi::String::New(env, "x")).As().FloatValue(), + value.Get(Napi::String::New(env, "y")).As().FloatValue(), + value.Get(Napi::String::New(env, "z")).As().FloatValue() + }; + Memory.writeMemory(handle, address, vector); + + } else if (!strcmp(dataType, "vector4") || !strcmp(dataType, "vec4")) { + + Napi::Object value = args[2].As(); + Vector4 vector = { + value.Get(Napi::String::New(env, "w")).As().FloatValue(), + value.Get(Napi::String::New(env, "x")).As().FloatValue(), + value.Get(Napi::String::New(env, "y")).As().FloatValue(), + value.Get(Napi::String::New(env, "z")).As().FloatValue() + }; + Memory.writeMemory(handle, address, vector); + + } else { + Napi::Error::New(env, "unexpected data type").ThrowAsJavaScriptException(); + } + + return env.Null(); +} + +Napi::Value writeBuffer(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 3) { + Napi::Error::New(env, "required 3 arguments").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() && !args[1].IsNumber()) { + Napi::Error::New(env, "first and second argument must be a number").ThrowAsJavaScriptException(); + return env.Null(); + } + + HANDLE handle = (HANDLE)args[0].As().Int64Value(); + + DWORD64 address; + if (args[1].As().IsBigInt()) { + bool lossless; + address = args[1].As().Uint64Value(&lossless); + } else { + address = args[1].As().Int64Value(); + } + + SIZE_T length = args[2].As>().Length(); + char* data = args[2].As>().Data(); + Memory.writeMemory(handle, address, data, length); + + return env.Null(); +} + +// Napi::Value findPattern(const Napi::CallbackInfo& args) { +// Napi::Env env = args.Env(); + +// HANDLE handle = (HANDLE)args[0].As().Int64Value(); +// DWORD64 baseAddress = args[1].As().Int64Value(); +// DWORD64 baseSize = args[2].As().Int64Value(); +// std::string signature(args[3].As().Utf8Value()); +// short flags = args[4].As().Uint32Value(); +// uint32_t patternOffset = args[5].As().Uint32Value(); + +// // matching address +// uintptr_t address = 0; +// const char* errorMessage = ""; + +// // read memory region occupied by the module to pattern match inside +// std::vector moduleBytes = std::vector(baseSize); +// ReadProcessMemory(handle, (LPVOID)baseAddress, &moduleBytes[0], baseSize, nullptr); +// unsigned char* byteBase = const_cast(&moduleBytes.at(0)); + +// Pattern.findPattern(handle, baseAddress, byteBase, baseSize, signature.c_str(), flags, patternOffset, &address); + +// if (address == 0) { +// errorMessage = "unable to match pattern inside any modules or regions"; +// } + +// if (args.Length() == 5) { +// Napi::Function callback = args[4].As(); +// callback.Call(env.Global(), { Napi::String::New(env, errorMessage), Napi::Value::From(env, address) }); +// return env.Null(); +// } else { +// return Napi::Value::From(env, address); +// } +// } + +Napi::Value findPattern(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 4 && args.Length() != 5) { + Napi::Error::New(env, "requires 4 arguments, 5 with callback").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() || !args[1].IsString() || !args[2].IsNumber() || !args[3].IsNumber()) { + Napi::Error::New(env, "expected: number, string, string, number").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 5 && !args[4].IsFunction()) { + Napi::Error::New(env, "callback argument must be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + HANDLE handle = (HANDLE)args[0].As().Int64Value(); + std::string pattern(args[1].As().Utf8Value()); + short flags = args[2].As().Uint32Value(); + uint32_t patternOffset = args[3].As().Uint32Value(); + + // matching address + uintptr_t address = 0; + const char* errorMessage = ""; + + std::vector modules = module::getModules(GetProcessId(handle), &errorMessage); + Pattern.search(handle, modules, 0, pattern.c_str(), flags, patternOffset, &address); + + // if no match found inside any modules, search memory regions + if (address == 0) { + std::vector regions = Memory.getRegions(handle); + Pattern.search(handle, regions, 0, pattern.c_str(), flags, patternOffset, &address); + } + + if (address == 0) { + errorMessage = "unable to match pattern inside any modules or regions"; + } + + if (args.Length() == 5) { + Napi::Function callback = args[4].As(); + callback.Call(env.Global(), { Napi::String::New(env, errorMessage), Napi::Value::From(env, address) }); + return env.Null(); + } else { + return Napi::Value::From(env, address); + } +} + +Napi::Value findPatternByModule(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 5 && args.Length() != 6) { + Napi::Error::New(env, "requires 5 arguments, 6 with callback").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() || !args[1].IsString() || !args[2].IsString() || !args[3].IsNumber() || !args[4].IsNumber()) { + Napi::Error::New(env, "expected: number, string, string, number, number").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 6 && !args[5].IsFunction()) { + Napi::Error::New(env, "callback argument must be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + HANDLE handle = (HANDLE)args[0].As().Int64Value(); + std::string moduleName(args[1].As().Utf8Value()); + std::string pattern(args[2].As().Utf8Value()); + short flags = args[3].As().Uint32Value(); + uint32_t patternOffset = args[4].As().Uint32Value(); + + // matching address + uintptr_t address = 0; + const char* errorMessage = ""; + + MODULEENTRY32 module = module::findModule(moduleName.c_str(), GetProcessId(handle), &errorMessage); + + uintptr_t baseAddress = (uintptr_t) module.modBaseAddr; + DWORD baseSize = module.modBaseSize; + + // read memory region occupied by the module to pattern match inside + std::vector moduleBytes = std::vector(baseSize); + ReadProcessMemory(handle, (LPVOID)baseAddress, &moduleBytes[0], baseSize, nullptr); + unsigned char* byteBase = const_cast(&moduleBytes.at(0)); + + Pattern.findPattern(handle, baseAddress, byteBase, baseSize, pattern.c_str(), flags, patternOffset, &address); + + if (address == 0) { + errorMessage = "unable to match pattern inside any modules or regions"; + } + + if (args.Length() == 6) { + Napi::Function callback = args[5].As(); + callback.Call(env.Global(), { Napi::String::New(env, errorMessage), Napi::Value::From(env, address) }); + return env.Null(); + } else { + return Napi::Value::From(env, address); + } +} + +Napi::Value findPatternByAddress(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 5 && args.Length() != 6) { + Napi::Error::New(env, "requires 5 arguments, 6 with callback").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() || !args[1].IsNumber() || !args[2].IsString() || !args[3].IsNumber() || !args[4].IsNumber()) { + Napi::Error::New(env, "expected: number, number, string, number, number").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 6 && !args[5].IsFunction()) { + Napi::Error::New(env, "callback argument must be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + HANDLE handle = (HANDLE)args[0].As().Int64Value(); + + DWORD64 baseAddress; + if (args[1].As().IsBigInt()) { + bool lossless; + baseAddress = args[1].As().Uint64Value(&lossless); + } else { + baseAddress = args[1].As().Int64Value(); + } + + std::string pattern(args[2].As().Utf8Value()); + short flags = args[3].As().Uint32Value(); + uint32_t patternOffset = args[4].As().Uint32Value(); + + // matching address + uintptr_t address = 0; + const char* errorMessage = ""; + + std::vector modules = module::getModules(GetProcessId(handle), &errorMessage); + Pattern.search(handle, modules, baseAddress, pattern.c_str(), flags, patternOffset, &address); + + if (address == 0) { + std::vector regions = Memory.getRegions(handle); + Pattern.search(handle, regions, baseAddress, pattern.c_str(), flags, patternOffset, &address); + } + + if (address == 0) { + errorMessage = "unable to match pattern inside any modules or regions"; + } + + if (args.Length() == 6) { + Napi::Function callback = args[5].As(); + callback.Call(env.Global(), { Napi::String::New(env, errorMessage), Napi::Value::From(env, address) }); + return env.Null(); + } else { + return Napi::Value::From(env, address); + } +} + +Napi::Value callFunction(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 4 && args.Length() != 5) { + Napi::Error::New(env, "requires 4 arguments, 5 with callback").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() && !args[1].IsObject() && !args[2].IsNumber() && !args[3].IsNumber()) { + Napi::Error::New(env, "invalid arguments").ThrowAsJavaScriptException(); + return env.Null(); + } + + // TODO: temp (?) solution to forcing variables onto the heap + // to ensure consistent addresses. copy everything to a vector, and use the + // vector's instances of the variables as the addresses being passed to `functions.call()`. + // Another solution: do `int x = new int(4)` and then use `&x` for the address + std::vector heap; + + std::vector parsedArgs; + Napi::Array arguments = args[1].As(); + for (unsigned int i = 0; i < arguments.Length(); i++) { + Napi::Object argument = arguments.Get(i).As(); + + functions::Type type = (functions::Type) argument.Get(Napi::String::New(env, "type")).As().Uint32Value(); + + if (type == functions::Type::T_STRING) { + std::string stringValue = argument.Get(Napi::String::New(env, "value")).As().Utf8Value(); + parsedArgs.push_back({ type, &stringValue }); + } + + if (type == functions::Type::T_INT) { + int data = argument.Get(Napi::String::New(env, "value")).As().Int32Value(); + + // As we only pass the addresses of the variable to the `call` function and not a copy + // of the variable itself, we need to ensure that the variable stays alive and in a unique + // memory location until the `call` function has been executed. So manually allocate memory, + // track it, and then free it once the function has been called. + // TODO: find a better solution? + int* memory = (int*) malloc(sizeof(int)); + *memory = data; + heap.push_back(memory); + + parsedArgs.push_back({ type, memory }); + } + + if (type == functions::Type::T_FLOAT) { + float data = argument.Get(Napi::String::New(env, "value")).As().FloatValue(); + + float* memory = (float*) malloc(sizeof(float)); + *memory = data; + heap.push_back(memory); + + parsedArgs.push_back({ type, memory }); + } + } + + HANDLE handle = (HANDLE)args[0].As().Int64Value(); + functions::Type returnType = (functions::Type) args[2].As().Uint32Value(); + + DWORD64 address; + if (args[3].As().IsBigInt()) { + bool lossless; + address = args[3].As().Uint64Value(&lossless); + } else { + address = args[3].As().Int64Value(); + } + + const char* errorMessage = ""; + Call data = functions::call(handle, parsedArgs, returnType, address, &errorMessage); + + // Free all the memory we allocated + for (auto &memory : heap) { + free(memory); + } + + heap.clear(); + + if (strcmp(errorMessage, "") && args.Length() != 5) { + Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); + return env.Null(); + } + + Napi::Object info = Napi::Object::New(env); + + Napi::String keyString = Napi::String::New(env, "returnValue"); + + if (returnType == functions::Type::T_STRING) { + info.Set(keyString, Napi::String::New(env, data.returnString.c_str())); + } + + if (returnType == functions::Type::T_CHAR) { + info.Set(keyString, Napi::Value::From(env, (char) data.returnValue)); + } + + if (returnType == functions::Type::T_BOOL) { + info.Set(keyString, Napi::Value::From(env, (bool) data.returnValue)); + } + + if (returnType == functions::Type::T_INT) { + info.Set(keyString, Napi::Value::From(env, (int) data.returnValue)); + } + + if (returnType == functions::Type::T_FLOAT) { + float value = *(float *)&data.returnValue; + info.Set(keyString, Napi::Value::From(env, value)); + } + + if (returnType == functions::Type::T_DOUBLE) { + double value = *(double *)&data.returnValue; + info.Set(keyString, Napi::Value::From(env, value)); + } + + info.Set(Napi::String::New(env, "exitCode"), Napi::Value::From(env, data.exitCode)); + + if (args.Length() == 5) { + // Callback to let the user handle with the information + Napi::Function callback = args[2].As(); + callback.Call(env.Global(), { Napi::String::New(env, errorMessage), info }); + return env.Null(); + } else { + // return JSON + return info; + } + +} + +Napi::Value virtualProtectEx(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 4 && args.Length() != 5) { + Napi::Error::New(env, "requires 4 arguments, 5 with callback").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() && !args[1].IsNumber() && !args[2].IsNumber()) { + Napi::Error::New(env, "All arguments should be numbers.").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 5 && !args[4].IsFunction()) { + Napi::Error::New(env, "callback needs to be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + DWORD result; + HANDLE handle = (HANDLE)args[0].As().Int64Value(); + DWORD64 address = args[1].As().Int64Value(); + SIZE_T size = args[2].As().Int64Value(); + DWORD protection = args[3].As().Uint32Value(); + + bool success = VirtualProtectEx(handle, (LPVOID) address, size, protection, &result); + + const char* errorMessage = ""; + + if (success == 0) { + errorMessage = "an error occurred calling VirtualProtectEx"; + // errorMessage = GetLastErrorToString().c_str(); + } + + // If there is an error and there is no callback, throw the error + if (strcmp(errorMessage, "") && args.Length() != 5) { + Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 5) { + // Callback to let the user handle with the information + Napi::Function callback = args[5].As(); + callback.Call(env.Global(), { + Napi::String::New(env, errorMessage), + Napi::Value::From(env, result) + }); + return env.Null(); + } else { + return Napi::Value::From(env, result); + } +} + +Napi::Value getRegions(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 1 && args.Length() != 2) { + Napi::Error::New(env, "requires 1 argument, 2 with callback").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber()) { + Napi::Error::New(env, "invalid arguments: first argument must be a number").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 2 && !args[1].IsFunction()) { + Napi::Error::New(env, "callback needs to be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + HANDLE handle = (HANDLE)args[0].As().Int64Value(); + std::vector regions = Memory.getRegions(handle); + + Napi::Array regionsArray = Napi::Array::New(env, regions.size()); + + for (std::vector::size_type i = 0; i != regions.size(); i++) { + Napi::Object region = Napi::Object::New(env); + + region.Set(Napi::String::New(env, "BaseAddress"), Napi::Value::From(env, (DWORD64) regions[i].BaseAddress)); + region.Set(Napi::String::New(env, "AllocationBase"), Napi::Value::From(env, (DWORD64) regions[i].AllocationBase)); + region.Set(Napi::String::New(env, "AllocationProtect"), Napi::Value::From(env, (DWORD) regions[i].AllocationProtect)); + region.Set(Napi::String::New(env, "RegionSize"), Napi::Value::From(env, (SIZE_T) regions[i].RegionSize)); + region.Set(Napi::String::New(env, "State"), Napi::Value::From(env, (DWORD) regions[i].State)); + region.Set(Napi::String::New(env, "Protect"), Napi::Value::From(env, (DWORD) regions[i].Protect)); + region.Set(Napi::String::New(env, "Type"), Napi::Value::From(env, (DWORD) regions[i].Type)); + + char moduleName[MAX_PATH]; + DWORD size = GetModuleFileNameExA(handle, (HINSTANCE)regions[i].AllocationBase, moduleName, MAX_PATH); + + if (size != 0) { + region.Set(Napi::String::New(env, "szExeFile"), Napi::String::New(env, moduleName)); + } + + regionsArray.Set(i, region); + } + + if (args.Length() == 2) { + // Callback to let the user handle with the information + Napi::Function callback = args[1].As(); + callback.Call(env.Global(), { Napi::String::New(env, ""), regionsArray }); + return env.Null(); + } else { + // return JSON + return regionsArray; + } +} + +Napi::Value virtualQueryEx(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 2 && args.Length() != 3) { + Napi::Error::New(env, "requires 2 arguments, 3 with callback").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() || !args[1].IsNumber()) { + Napi::Error::New(env, "first and second argument need to be a number").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 3 && !args[2].IsFunction()) { + Napi::Error::New(env, "callback needs to be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + HANDLE handle = (HANDLE)args[0].As().Int64Value(); + DWORD64 address = args[1].As().Int64Value(); + + MEMORY_BASIC_INFORMATION information; + SIZE_T result = VirtualQueryEx(handle, (LPVOID)address, &information, sizeof(information)); + + const char* errorMessage = ""; + + if (result == 0 || result != sizeof(information)) { + errorMessage = "an error occurred calling VirtualQueryEx"; + // errorMessage = GetLastErrorToString().c_str(); + } + + // If there is an error and there is no callback, throw the error + if (strcmp(errorMessage, "") && args.Length() != 3) { + Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); + return env.Null(); + } + + Napi::Object region = Napi::Object::New(env); + + region.Set(Napi::String::New(env, "BaseAddress"), Napi::Value::From(env, (DWORD64) information.BaseAddress)); + region.Set(Napi::String::New(env, "AllocationBase"), Napi::Value::From(env, (DWORD64) information.AllocationBase)); + region.Set(Napi::String::New(env, "AllocationProtect"), Napi::Value::From(env, (DWORD) information.AllocationProtect)); + region.Set(Napi::String::New(env, "RegionSize"), Napi::Value::From(env, (SIZE_T) information.RegionSize)); + region.Set(Napi::String::New(env, "State"), Napi::Value::From(env, (DWORD) information.State)); + region.Set(Napi::String::New(env, "Protect"), Napi::Value::From(env, (DWORD) information.Protect)); + region.Set(Napi::String::New(env, "Type"), Napi::Value::From(env, (DWORD) information.Type)); + + if (args.Length() == 3) { + // Callback to let the user handle with the information + Napi::Function callback = args[1].As(); + callback.Call(env.Global(), { Napi::String::New(env, ""), region }); + return env.Null(); + } else { + // return JSON + return region; + } +} + +Napi::Value virtualAllocEx(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 5 && args.Length() != 6) { + Napi::Error::New(env, "requires 5 arguments, 6 with callback").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() || !args[2].IsNumber() || !args[3].IsNumber() || !args[4].IsNumber()) { + Napi::Error::New(env, "invalid arguments: arguments 0, 2, 3 and 4 need to be numbers").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 6 && !args[5].IsFunction()) { + Napi::Error::New(env, "callback needs to be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + HANDLE handle = (HANDLE)args[0].As().Int64Value(); + SIZE_T size = args[2].As().Int64Value(); + DWORD allocationType = args[3].As().Uint32Value(); + DWORD protection = args[4].As().Uint32Value(); + LPVOID address; + + // Means in the JavaScript space `null` was passed through. + if (args[1] == env.Null()) { + address = NULL; + } else { + address = (LPVOID) args[1].As().Int64Value(); + } + + LPVOID allocatedAddress = VirtualAllocEx(handle, address, size, allocationType, protection); + + const char* errorMessage = ""; + + // If null, it means an error occurred + if (allocatedAddress == NULL) { + errorMessage = "an error occurred calling VirtualAllocEx"; + // errorMessage = GetLastErrorToString().c_str(); + } + + // If there is an error and there is no callback, throw the error + if (strcmp(errorMessage, "") && args.Length() != 6) { + Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 6) { + // Callback to let the user handle with the information + Napi::Function callback = args[5].As(); + callback.Call(env.Global(), { + Napi::String::New(env, errorMessage), + Napi::Value::From(env, (intptr_t)allocatedAddress) + }); + return env.Null(); + } else { + return Napi::Value::From(env, (intptr_t)allocatedAddress); + } +} + +Napi::Value attachDebugger(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 2) { + Napi::Error::New(env, "requires 2 arguments").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() || !args[1].IsBoolean()) { + Napi::Error::New(env, "first argument needs to be a number, second a boolean").ThrowAsJavaScriptException(); + return env.Null(); + } + + DWORD processId = args[0].As().Uint32Value(); + bool killOnExit = args[1].As().Value(); + + bool success = debugger::attach(processId, killOnExit); + return Napi::Boolean::New(env, success); +} + +Napi::Value detachDebugger(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + DWORD processId = args[0].As().Uint32Value(); + + if (args.Length() != 1) { + Napi::Error::New(env, "requires only 1 argument").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber()) { + Napi::Error::New(env, "only argument needs to be a number").ThrowAsJavaScriptException(); + return env.Null(); + } + + bool success = debugger::detatch(processId); + return Napi::Boolean::New(env, success); +} + +Napi::Value awaitDebugEvent(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 2) { + Napi::Error::New(env, "requires 2 arguments").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() || !args[1].IsNumber()) { + Napi::Error::New(env, "both arguments need to be a number").ThrowAsJavaScriptException(); + return env.Null(); + } + + int millisTimeout = args[1].As().Uint32Value(); + + DebugEvent debugEvent; + bool success = debugger::awaitDebugEvent(millisTimeout, &debugEvent); + + Register hardwareRegister = static_cast(args[0].As().Uint32Value()); + + if (success && debugEvent.hardwareRegister == hardwareRegister) { + Napi::Object info = Napi::Object::New(env); + + info.Set(Napi::String::New(env, "processId"), Napi::Value::From(env, (DWORD) debugEvent.processId)); + info.Set(Napi::String::New(env, "threadId"), Napi::Value::From(env, (DWORD) debugEvent.threadId)); + info.Set(Napi::String::New(env, "exceptionCode"), Napi::Value::From(env, (DWORD) debugEvent.exceptionCode)); + info.Set(Napi::String::New(env, "exceptionFlags"), Napi::Value::From(env, (DWORD) debugEvent.exceptionFlags)); + info.Set(Napi::String::New(env, "exceptionAddress"), Napi::Value::From(env, (DWORD64) debugEvent.exceptionAddress)); + info.Set(Napi::String::New(env, "hardwareRegister"), Napi::Value::From(env, static_cast(debugEvent.hardwareRegister))); + + return info; + } + + // If we aren't interested in passing this event back to the JS space, + // just silently handle it + if (success && debugEvent.hardwareRegister != hardwareRegister) { + debugger::handleDebugEvent(debugEvent.processId, debugEvent.threadId); + } + + return env.Null(); +} + +Napi::Value handleDebugEvent(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 2) { + Napi::Error::New(env, "requires 2 arguments").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() || !args[1].IsNumber()) { + Napi::Error::New(env, "both arguments need to be numbers").ThrowAsJavaScriptException(); + return env.Null(); + } + + DWORD processId = args[0].As().Uint32Value(); + DWORD threadId = args[1].As().Uint32Value(); + + bool success = debugger::handleDebugEvent(processId, threadId); + return Napi::Boolean::New(env, success); +} + +Napi::Value setHardwareBreakpoint(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 5) { + Napi::Error::New(env, "requires 5 arguments").ThrowAsJavaScriptException(); + return env.Null(); + } + + for (unsigned int i = 0; i < args.Length(); i++) { + if (!args[i].IsNumber()) { + Napi::Error::New(env, "all arguments need to be numbers").ThrowAsJavaScriptException(); + return env.Null(); + } + } + + DWORD processId = args[0].As().Uint32Value(); + DWORD64 address = args[1].As().Int64Value(); + Register hardwareRegister = static_cast(args[2].As().Uint32Value()); + + // Execute = 0x0 + // Access = 0x3 + // Writer = 0x1 + int trigger = args[3].As().Uint32Value(); + + int length = args[4].As().Uint32Value(); + + bool success = debugger::setHardwareBreakpoint(processId, address, hardwareRegister, trigger, length); + return Napi::Boolean::New(env, success); +} + +Napi::Value removeHardwareBreakpoint(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 2) { + Napi::Error::New(env, "requires 2 arguments").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() || !args[1].IsNumber()) { + Napi::Error::New(env, "both arguments need to be numbers").ThrowAsJavaScriptException(); + return env.Null(); + } + + DWORD processId = args[0].As().Uint32Value(); + Register hardwareRegister = static_cast(args[1].As().Uint32Value()); + + bool success = debugger::setHardwareBreakpoint(processId, 0, hardwareRegister, 0, 0); + return Napi::Boolean::New(env, success); +} + +Napi::Value injectDll(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 2 && args.Length() != 3) { + Napi::Error::New(env, "requires 2 arguments, or 3 with a callback").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber() || !args[1].IsString()) { + Napi::Error::New(env, "first argument needs to be a number, second argument needs to be a string").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 3 && !args[2].IsFunction()) { + Napi::Error::New(env, "callback needs to be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + HANDLE handle = (HANDLE)args[0].As().Int64Value(); + std::string dllPath(args[1].As().Utf8Value()); + Napi::Function callback = args[2].As(); + + const char* errorMessage = ""; + DWORD moduleHandle = -1; + bool success = dll::inject(handle, dllPath, &errorMessage, &moduleHandle); + + if (strcmp(errorMessage, "") && args.Length() != 3) { + Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); + return env.Null(); + } + + // `moduleHandle` above is the return value of the `LoadLibrary` procedure, + // which we retrieve through `GetExitCode`. This value can become truncated + // in large address spaces such as 64 bit since `GetExitCode` just returns BOOL, + // so it's unreliable to use as the handle. Since the handle of a module is just a pointer + // to the address of the DLL mapped in the process' virtual address space, we can fetch + // this separately, so we won't return it to prevent it being passed to `unloadDll` + // and in some cases unexpectedly failing when it is truncated. + + if (args.Length() == 3) { + callback.Call(env.Global(), { Napi::String::New(env, errorMessage), Napi::Boolean::New(env, success) }); + return env.Null(); + } else { + return Napi::Boolean::New(env, success); + } +} + +Napi::Value unloadDll(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (args.Length() != 2 && args.Length() != 3) { + Napi::Error::New(env, "requires 2 arguments, or 3 with a callback").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[0].IsNumber()) { + Napi::Error::New(env, "first argument needs to be a number").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!args[1].IsNumber() && !args[1].IsString()) { + Napi::Error::New(env, "second argument needs to be a number or a string").ThrowAsJavaScriptException(); + return env.Null(); + } + + if (args.Length() == 3 && !args[2].IsFunction()) { + Napi::Error::New(env, "callback needs to be a function").ThrowAsJavaScriptException(); + return env.Null(); + } + + HANDLE handle = (HANDLE)args[0].As().Int64Value(); + Napi::Function callback = args[2].As(); + + HMODULE moduleHandle; + + // get module handle (module base address) directly + if (args[1].IsNumber()) { + moduleHandle = (HMODULE)args[1].As().Int64Value(); + } + + // find module handle from name of DLL + if (args[1].IsString()) { + std::string moduleName(args[1].As().Utf8Value()); + const char* errorMessage = ""; + + MODULEENTRY32 module = module::findModule(moduleName.c_str(), GetProcessId(handle), &errorMessage); + + if (strcmp(errorMessage, "")) { + if (args.Length() != 3) { + Napi::Error::New(env, "unable to find specified module").ThrowAsJavaScriptException(); + return env.Null(); + } else { + callback.Call(env.Global(), { Napi::String::New(env, errorMessage) }); + return Napi::Boolean::New(env, false); + } + } + + moduleHandle = (HMODULE) module.modBaseAddr; + } + + const char* errorMessage = ""; + bool success = dll::unload(handle, &errorMessage, moduleHandle); + + if (strcmp(errorMessage, "") && args.Length() != 3) { + Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); + return Napi::Boolean::New(env, false); + } + + if (args.Length() == 3) { + callback.Call(env.Global(), { Napi::String::New(env, errorMessage), Napi::Boolean::New(env, success) }); + return env.Null(); + } else { + return Napi::Boolean::New(env, success); + } +} + +Napi::Value openFileMapping(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + std::string fileName(args[0].As().Utf8Value()); + + HANDLE fileHandle = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, fileName.c_str()); + + if (fileHandle == NULL) { + Napi::Error::New(env, Napi::String::New(env, "Error opening handle to file!")).ThrowAsJavaScriptException(); + return env.Null(); + } + + return Napi::Value::From(env, (uintptr_t) fileHandle); +} + +Napi::Value mapViewOfFile(const Napi::CallbackInfo& args) { + Napi::Env env = args.Env(); + + HANDLE processHandle = (HANDLE)args[0].As().Int64Value(); + HANDLE fileHandle = (HANDLE)args[1].As().Int64Value(); + + uint64_t offset; + if (args[2].As().IsBigInt()) { + bool lossless; + offset = args[2].As().Uint64Value(&lossless); + } else { + offset = args[2].As().Int64Value(); + } + + size_t viewSize; + if (args[3].As().IsBigInt()) { + bool lossless; + viewSize = args[3].As().Uint64Value(&lossless); + } else { + viewSize = args[3].As().Int64Value(); + } + + ULONG pageProtection = args[4].As().Int64Value(); + + LPVOID baseAddress = MapViewOfFile2(fileHandle, processHandle, offset, NULL, viewSize, 0, pageProtection); + + if (baseAddress == NULL) { + Napi::Error::New(env, Napi::String::New(env, "Error mapping file to process!")).ThrowAsJavaScriptException(); + return env.Null(); + } + + return Napi::Value::From(env, (uintptr_t) baseAddress); +} + +// https://stackoverflow.com/a/17387176 +std::string GetLastErrorToString() { + DWORD errorMessageID = ::GetLastError(); + + // No error message, return empty string + if(errorMessageID == 0) { + return std::string(); + } + + LPSTR messageBuffer = nullptr; + + size_t size = FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + errorMessageID, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&messageBuffer, + 0, + NULL + ); + + std::string message(messageBuffer, size); + + // Free the buffer + LocalFree(messageBuffer); + return message; +} + +Napi::Object init(Napi::Env env, Napi::Object exports) { + exports.Set(Napi::String::New(env, "openProcess"), Napi::Function::New(env, openProcess)); + exports.Set(Napi::String::New(env, "closeHandle"), Napi::Function::New(env, closeHandle)); + exports.Set(Napi::String::New(env, "getProcesses"), Napi::Function::New(env, getProcesses)); + exports.Set(Napi::String::New(env, "getModules"), Napi::Function::New(env, getModules)); + exports.Set(Napi::String::New(env, "findModule"), Napi::Function::New(env, findModule)); + exports.Set(Napi::String::New(env, "readMemory"), Napi::Function::New(env, readMemory)); + exports.Set(Napi::String::New(env, "readBuffer"), Napi::Function::New(env, readBuffer)); + exports.Set(Napi::String::New(env, "writeMemory"), Napi::Function::New(env, writeMemory)); + exports.Set(Napi::String::New(env, "writeBuffer"), Napi::Function::New(env, writeBuffer)); + exports.Set(Napi::String::New(env, "findPattern"), Napi::Function::New(env, findPattern)); + exports.Set(Napi::String::New(env, "findPatternByModule"), Napi::Function::New(env, findPatternByModule)); + exports.Set(Napi::String::New(env, "findPatternByAddress"), Napi::Function::New(env, findPatternByAddress)); + exports.Set(Napi::String::New(env, "virtualProtectEx"), Napi::Function::New(env, virtualProtectEx)); + exports.Set(Napi::String::New(env, "callFunction"), Napi::Function::New(env, callFunction)); + exports.Set(Napi::String::New(env, "virtualAllocEx"), Napi::Function::New(env, virtualAllocEx)); + exports.Set(Napi::String::New(env, "getRegions"), Napi::Function::New(env, getRegions)); + exports.Set(Napi::String::New(env, "virtualQueryEx"), Napi::Function::New(env, virtualQueryEx)); + exports.Set(Napi::String::New(env, "attachDebugger"), Napi::Function::New(env, attachDebugger)); + exports.Set(Napi::String::New(env, "detachDebugger"), Napi::Function::New(env, detachDebugger)); + exports.Set(Napi::String::New(env, "awaitDebugEvent"), Napi::Function::New(env, awaitDebugEvent)); + exports.Set(Napi::String::New(env, "handleDebugEvent"), Napi::Function::New(env, handleDebugEvent)); + exports.Set(Napi::String::New(env, "setHardwareBreakpoint"), Napi::Function::New(env, setHardwareBreakpoint)); + exports.Set(Napi::String::New(env, "removeHardwareBreakpoint"), Napi::Function::New(env, removeHardwareBreakpoint)); + exports.Set(Napi::String::New(env, "injectDll"), Napi::Function::New(env, injectDll)); + exports.Set(Napi::String::New(env, "unloadDll"), Napi::Function::New(env, unloadDll)); + exports.Set(Napi::String::New(env, "openFileMapping"), Napi::Function::New(env, openFileMapping)); + exports.Set(Napi::String::New(env, "mapViewOfFile"), Napi::Function::New(env, mapViewOfFile)); + return exports; +} + +NODE_API_MODULE(memoryprocess, init) \ No newline at end of file diff --git a/native/memoryprocess.h b/native/memoryprocess.h new file mode 100644 index 0000000..d30e879 --- /dev/null +++ b/native/memoryprocess.h @@ -0,0 +1,15 @@ +#pragma once +#ifndef MEMORYPROCESS_H +#define MEMORYPROCESS_H +#define WIN32_LEAN_AND_MEAN + +#include + +class memoryprocess { + +public: + memoryprocess(); + ~memoryprocess(); +}; +#endif +#pragma once diff --git a/native/module.cc b/native/module.cc new file mode 100644 index 0000000..4bc8b5d --- /dev/null +++ b/native/module.cc @@ -0,0 +1,94 @@ +#include +#include +#include +#include +#include "module.h" +#include "process.h" +#include "memoryprocess.h" + +DWORD64 module::getBaseAddress(const char* processName, DWORD processId) { + const char* errorMessage = ""; + MODULEENTRY32 baseModule = module::findModule(processName, processId, &errorMessage); + return (DWORD64)baseModule.modBaseAddr; +} + +MODULEENTRY32 module::findModule(const char* moduleName, DWORD processId, const char** errorMessage) { + MODULEENTRY32 module; + bool found = false; + + std::vector moduleEntries = getModules(processId, errorMessage); + + // Loop over every module + for (std::vector::size_type i = 0; i != moduleEntries.size(); i++) { + // Check to see if this is the module we want. + if (!strcmp(moduleEntries[i].szModule, moduleName)) { + // module is returned and moduleEntry is used internally for reading/writing to memory + module = moduleEntries[i]; + found = true; + break; + } + } + + if (!found) { + *errorMessage = "unable to find module"; + } + + return module; +} + +std::vector module::getModules(DWORD processId, const char** errorMessage) { + // Take a snapshot of all modules inside a given process. + HANDLE hModuleSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, processId); + MODULEENTRY32 mEntry; + + if (hModuleSnapshot == INVALID_HANDLE_VALUE) { + *errorMessage = "method failed to take snapshot of the modules"; + } + + // Before use, set the structure size. + mEntry.dwSize = sizeof(mEntry); + + // Exit if unable to find the first module. + if (!Module32First(hModuleSnapshot, &mEntry)) { + CloseHandle(hModuleSnapshot); + *errorMessage = "method failed to retrieve the first module"; + } + + std::vector modules; + + // Loop through modules. + do { + // Add the module to the vector + modules.push_back(mEntry); + } while (Module32Next(hModuleSnapshot, &mEntry)); + + CloseHandle(hModuleSnapshot); + + return modules; +} + +std::vector module::getThreads(DWORD processId, const char** errorMessage) { + HANDLE hThreadSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, processId); + THREADENTRY32 mEntry; + + if (hThreadSnapshot == INVALID_HANDLE_VALUE) { + *errorMessage = "method failed to take snapshot of the threads"; + } + + mEntry.dwSize = sizeof(mEntry); + + if(!Thread32First(hThreadSnapshot, &mEntry)) { + CloseHandle(hThreadSnapshot); + *errorMessage = "method failed to retrieve the first thread"; + } + + std::vector threads; + + do { + threads.push_back(mEntry); + } while (Thread32Next(hThreadSnapshot, &mEntry)); + + CloseHandle(hThreadSnapshot); + + return threads; +} diff --git a/native/module.h b/native/module.h new file mode 100644 index 0000000..b617e64 --- /dev/null +++ b/native/module.h @@ -0,0 +1,18 @@ +#pragma once +#ifndef MODULE_H +#define MODULE_H +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include + +namespace module { + DWORD64 getBaseAddress(const char* processName, DWORD processId); + MODULEENTRY32 findModule(const char* moduleName, DWORD processId, const char** errorMessage); + std::vector getModules(DWORD processId, const char** errorMessage); + std::vector getThreads(DWORD processId, const char** errorMessage); + +}; +#endif +#pragma once diff --git a/native/pattern.cc b/native/pattern.cc new file mode 100644 index 0000000..1526ef5 --- /dev/null +++ b/native/pattern.cc @@ -0,0 +1,103 @@ +#include +#include +#include +#include +#include "pattern.h" +#include "memoryprocess.h" +#include "process.h" +#include "memory.h" + +#define INRANGE(x,a,b) (x >= a && x <= b) +#define getBits( x ) (INRANGE(x,'0','9') ? (x - '0') : ((x&(~0x20)) - 'A' + 0xa)) +#define getByte( x ) (getBits(x[0]) << 4 | getBits(x[1])) + +pattern::pattern() {} +pattern::~pattern() {} + +bool pattern::search(HANDLE handle, std::vector regions, DWORD64 searchAddress, const char* pattern, short flags, uint32_t patternOffset, uintptr_t* pAddress) { + for (std::vector::size_type i = 0; i != regions.size(); i++) { + uintptr_t baseAddress = (uintptr_t) regions[i].BaseAddress; + DWORD baseSize = regions[i].RegionSize; + + // if `searchAddress` has been set, only pattern match if the address lies inside of this region + if (searchAddress != 0 && (searchAddress < baseAddress || searchAddress > (baseAddress + baseSize))) { + continue; + } + + // read memory region to pattern match inside + std::vector regionBytes = std::vector(baseSize); + ReadProcessMemory(handle, (LPVOID)baseAddress, ®ionBytes[0], baseSize, nullptr); + unsigned char* byteBase = const_cast(®ionBytes.at(0)); + + if (findPattern(handle, baseAddress, byteBase, baseSize, pattern, flags, patternOffset, pAddress)) { + return true; + } + } + + return false; +} + +bool pattern::search(HANDLE handle, std::vector modules, DWORD64 searchAddress, const char* pattern, short flags, uint32_t patternOffset, uintptr_t* pAddress) { + for (std::vector::size_type i = 0; i != modules.size(); i++) { + uintptr_t baseAddress = (uintptr_t) modules[i].modBaseAddr; + DWORD baseSize = modules[i].modBaseSize; + + // if `searchAddress` has been set, only pattern match if the address lies inside of this module + if (searchAddress != 0 && (searchAddress < baseAddress || searchAddress > (baseAddress + baseSize))) { + continue; + } + + // read memory region occupied by the module to pattern match inside + std::vector moduleBytes = std::vector(baseSize); + ReadProcessMemory(handle, (LPVOID)baseAddress, &moduleBytes[0], baseSize, nullptr); + unsigned char* byteBase = const_cast(&moduleBytes.at(0)); + + if (findPattern(handle, baseAddress, byteBase, baseSize, pattern, flags, patternOffset, pAddress)) { + return true; + } + } + + return false; +} + +/* based off Y3t1y3t's implementation */ +bool pattern::findPattern(HANDLE handle, uintptr_t memoryBase, unsigned char* byteBase, DWORD memorySize, const char* pattern, short flags, uint32_t patternOffset, uintptr_t* pAddress) { + // uintptr_t moduleBase = (uintptr_t)module.hModule; + auto maxOffset = memorySize - 0x1000; + + for (uintptr_t offset = 0; offset < maxOffset; ++offset) { + if (compareBytes(byteBase + offset, pattern)) { + uintptr_t address = memoryBase + offset + patternOffset; + + if (flags & ST_READ) { + ReadProcessMemory(handle, LPCVOID(address), &address, sizeof(uintptr_t), nullptr); + } + + if (flags & ST_SUBTRACT) { + address -= memoryBase; + } + + *pAddress = address; + + return true; + } + } + + return false; +}; + +bool pattern::compareBytes(const unsigned char* bytes, const char* pattern) { + for (; *pattern; *pattern != ' ' ? ++bytes : bytes, ++pattern) { + if (*pattern == ' ' || *pattern == '?') { + continue; + } + + if (*bytes != getByte(pattern)) { + return false; + } + + ++pattern; + } + + return true; +} \ No newline at end of file diff --git a/native/pattern.h b/native/pattern.h new file mode 100644 index 0000000..5b108c4 --- /dev/null +++ b/native/pattern.h @@ -0,0 +1,31 @@ +#pragma once +#ifndef PATTERN_H +#define PATTERN_H +#define WIN32_LEAN_AND_MEAN + +#include +#include + +class pattern { +public: + pattern(); + ~pattern(); + + // Signature/pattern types + enum { + // normal: normal + // read: read memory at pattern + // subtract: subtract module base + ST_NORMAL = 0x0, + ST_READ = 0x1, + ST_SUBTRACT = 0x2 + }; + + bool search(HANDLE handle, std::vector regions, DWORD64 searchAddress, const char* pattern, short flags, uint32_t patternOffset, uintptr_t* pAddress); + bool search(HANDLE handle, std::vector modules, DWORD64 searchAddress, const char* pattern, short flags, uint32_t patternOffset, uintptr_t* pAddress); + bool findPattern(HANDLE handle, uintptr_t memoryBase, unsigned char* module, DWORD memorySize, const char* pattern, short flags, uint32_t patternOffset, uintptr_t* pAddress); + bool compareBytes(const unsigned char* bytes, const char* pattern); +}; + +#endif +#pragma once diff --git a/native/process.cc b/native/process.cc new file mode 100644 index 0000000..3b571c9 --- /dev/null +++ b/native/process.cc @@ -0,0 +1,95 @@ +#include +#include +#include +#include +#include "process.h" +#include "memoryprocess.h" + +process::process() {} +process::~process() {} + +using v8::Exception; +using v8::Isolate; +using v8::String; + +process::Pair process::openProcess(const char* processName, const char** errorMessage){ + PROCESSENTRY32 process; + HANDLE handle = NULL; + + // A list of processes (PROCESSENTRY32) + std::vector processes = getProcesses(errorMessage); + + for (std::vector::size_type i = 0; i != processes.size(); i++) { + // Check to see if this is the process we want. + if (!strcmp(processes[i].szExeFile, processName)) { + handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processes[i].th32ProcessID); + process = processes[i]; + break; + } + } + + if (handle == NULL) { + *errorMessage = "unable to find process"; + } + + return { + handle, + process, + }; +} + +process::Pair process::openProcess(DWORD processId, const char** errorMessage) { + PROCESSENTRY32 process; + HANDLE handle = NULL; + + // A list of processes (PROCESSENTRY32) + std::vector processes = getProcesses(errorMessage); + + for (std::vector::size_type i = 0; i != processes.size(); i++) { + // Check to see if this is the process we want. + if (processId == processes[i].th32ProcessID) { + handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processes[i].th32ProcessID); + process = processes[i]; + break; + } + } + + if (handle == NULL) { + *errorMessage = "unable to find process"; + } + + return { + handle, + process, + }; +} + +std::vector process::getProcesses(const char** errorMessage) { + // Take a snapshot of all processes. + HANDLE hProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); + PROCESSENTRY32 pEntry; + + if (hProcessSnapshot == INVALID_HANDLE_VALUE) { + *errorMessage = "method failed to take snapshot of the process"; + } + + // Before use, set the structure size. + pEntry.dwSize = sizeof(pEntry); + + // Exit if unable to find the first process. + if (!Process32First(hProcessSnapshot, &pEntry)) { + CloseHandle(hProcessSnapshot); + *errorMessage = "method failed to retrieve the first process"; + } + + std::vector processes; + + // Loop through processes. + do { + // Add the process to the vector + processes.push_back(pEntry); + } while (Process32Next(hProcessSnapshot, &pEntry)); + + CloseHandle(hProcessSnapshot); + return processes; +} diff --git a/native/process.h b/native/process.h new file mode 100644 index 0000000..a0b8196 --- /dev/null +++ b/native/process.h @@ -0,0 +1,27 @@ +#pragma once +#ifndef PROCESS_H +#define PROCESS_H +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include + +class process { +public: + struct Pair { + HANDLE handle; + PROCESSENTRY32 process; + }; + + process(); + ~process(); + + Pair openProcess(const char* processName, const char** errorMessage); + Pair openProcess(DWORD processId, const char** errorMessage); + void closeProcess(HANDLE hProcess); + std::vector getProcesses(const char** errorMessage); +}; + +#endif +#pragma once diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 0000000..203b709 --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,206 @@ +// 1. Build native code using node-gyp +// 2. Copy .node file from build/Release to src/ +// 3. Build with bun.build +// 4. Build types with tsc +// 5. Bundle types with api-extractor +// 6. Remove all .d.ts files from lib/ folder except index.d.ts +// 7. Remove .node file from src/ +// 8. Remove build/ folder +// 9. Remove all remaining empty folders from lib/ folder + +import path from "path"; +import { Extractor, ExtractorConfig, ExtractorResult } from '@microsoft/api-extractor'; + +function log(message: any, type: 'info' | 'success' | 'error' | 'warning' = 'info') { + const colors = { + info: '\x1b[36m', // cyan + success: '\x1b[32m', // green + error: '\x1b[31m', // red + warning: '\x1b[33m' // yellow + }; + + let prefix = ''; + if (process.stdout.isTTY) { + if (type === 'success') { + prefix = '✓ '; + } else if (type === 'info') { + prefix = 'ℹ '; + } else if (type === 'error') { + prefix = '✗ '; + } else if (type === 'warning') { + prefix = 'âš  '; + } + } + + const coloredMessage = `${colors[type]}${prefix}${message}\x1b[0m`; + + if (type === 'error') { + console.error(coloredMessage); + } else if (type === 'warning') { + console.warn(coloredMessage); + } else if (type === 'info') { + console.info(coloredMessage); + } else { + console.log(coloredMessage); + } +} + +function logWithTime(message: string, startTime: number, type: 'info' | 'success' | 'error' | 'warning' = 'success') { + const elapsedMs = performance.now() - startTime; + let timeString: string; + + if (elapsedMs < 1000) { + timeString = `${Math.round(elapsedMs)}ms`; + } else { + const seconds = elapsedMs / 1000; + timeString = seconds < 60 + ? `${seconds.toFixed(2)}s` + : `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`; + } + + log(`${message} \x1b[90m[${timeString}]\x1b[0m`, type); +} + +const srcGlob = new Bun.Glob("src/**/*.ts"); + +try { + // 1. Build native code using node-gyp + const startTime = performance.now(); + log("Starting native build...", "info"); + + const result = Bun.spawnSync(["node-gyp", "clean", "configure", "build", "--arch", "x64"]); + if (result.exitCode !== 0) { + log(`Native build failed with exit code ${result.exitCode}`, "error"); + throw new Error(`node-gyp build failed: ${result.stderr.toString()}`); + } + + logWithTime("Native build completed", startTime, "success"); + + // 2. Copy .node file from build/Release to src/ + const copyStartTime = performance.now(); + log("Copying native module to src directory...", "info"); + + const copyResult = Bun.spawnSync(["cp", "build/Release/native.node", "src/"]); + if (copyResult.exitCode !== 0) { + log(`Failed to copy native module: ${copyResult.stderr.toString()}`, "error"); + throw new Error("Failed to copy native module"); + } + logWithTime("Native module copied successfully", copyStartTime, "success"); + + // 3. Build with bun.build + const bundleStartTime = performance.now(); + log("Starting TypeScript build...", "info"); + const entryFiles = [ + "index.ts", + ...await Array.fromAsync(srcGlob.scan()), + "src/native.node" + ]; + log(`Found ${entryFiles.length} entry files for bundling`, "info"); + + const buildResult = await Bun.build({ + entrypoints: entryFiles, + outdir: "lib", + minify: true, + target: "node", + }); + + if (!buildResult.success) { + log(`Build failed with ${buildResult.logs.length} errors`, "error"); + for (const logItem of buildResult.logs) { + log(logItem.message, "error"); + } + throw new Error("TypeScript build failed"); + } + + logWithTime(`Build completed successfully (${buildResult.outputs.length} files)`, bundleStartTime, "success"); + + // 4. Build types with tsc + const typesStartTime = performance.now(); + log("Starting types build...", "info"); + + const tscResult = Bun.spawnSync(["tsc", "--project", "tsconfig.json"]); + if (tscResult.exitCode !== 0) { + log(`Types build failed: ${tscResult.stderr.toString()}`, "error"); + throw new Error("Types build failed"); + } + logWithTime("Types build completed", typesStartTime, "success"); + + // 5. Bundle types with api-extractor + const bundleTypesStartTime = performance.now(); + log("Starting types bundle...", "info"); + + const apiExtractorJsonPath: string = path.join(__dirname, '..', 'config', 'api-extractor.json'); + + const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(apiExtractorJsonPath); + + const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, { + localBuild: true, + }); + + if (extractorResult.succeeded) { + if (extractorResult.warningCount && extractorResult.warningCount > 0) { + logWithTime(`Types bundle completed with ${extractorResult.warningCount} warnings`, bundleTypesStartTime, "warning"); + } else { + logWithTime("Types bundle completed", bundleTypesStartTime, "success"); + } + } else { + logWithTime(`Types bundle failed with ${extractorResult.errorCount} errors`, bundleTypesStartTime, "error"); + throw new Error("Types bundle failed"); + } + + // 6. Remove all .d.ts files from lib/ folder except index.d.ts + const removeTypesStartTime = performance.now(); + log("Starting types cleanup...", "info"); + + const removeTypesResult = Bun.spawnSync(["find", "lib", "-name", "*.d.ts", "-not", "-name", "index.d.ts", "-delete"]); + if (removeTypesResult.exitCode !== 0) { + log(`Failed to remove types: ${removeTypesResult.stderr.toString()}`, "warning"); + } else { + logWithTime("Types cleanup completed", removeTypesStartTime, "success"); + } + + // 7. Remove .node file from src/ + const cleanupStartTime = performance.now(); + log("Cleaning up temporary native module...", "info"); + + const cleanupResult = Bun.spawnSync(["rm", "src/native.node"]); + if (cleanupResult.exitCode !== 0) { + log(`Failed to remove temporary native module: ${cleanupResult.stderr.toString()}`, "warning"); + } else { + logWithTime("Native module cleanup completed", cleanupStartTime, "success"); + } + + // 8. Remove build/ folder + const finalCleanupStartTime = performance.now(); + log("Removing build/ directory...", "info"); + + const finalCleanupResult = Bun.spawnSync(["rm", "-rf", "build"]); + if (finalCleanupResult.exitCode !== 0) { + log(`Failed to remove build/ directory: ${finalCleanupResult.stderr.toString()}`, "warning"); + } else { + logWithTime("Successfully removed build/ directory", finalCleanupStartTime, "success"); + } + + // 9. Remove all remaining empty folders from lib/ folder + const removeEmptyFoldersStartTime = performance.now(); + log("Starting empty folders cleanup...", "info"); + + const removeEmptyFoldersResult = Bun.spawnSync(["find", "lib", "-type", "d", "-empty", "-delete"]); + if (removeEmptyFoldersResult.exitCode !== 0) { + log(`Failed to remove empty folders: ${removeEmptyFoldersResult.stderr.toString()}`, "warning"); + } else { + logWithTime("Empty folders cleanup completed", removeEmptyFoldersStartTime, "success"); + } + + logWithTime("Total build process completed", startTime, "success"); +} catch (e) { + const error = e as AggregateError; + log("Build Failed", "error"); + + // Example: Using the built-in formatter + log(error, "error"); + + // Example: Serializing the failure as a JSON string. + log(JSON.stringify(error, null, 2), "error"); + process.exit(1); +} diff --git a/src/debugger.ts b/src/debugger.ts new file mode 100644 index 0000000..16883fa --- /dev/null +++ b/src/debugger.ts @@ -0,0 +1,152 @@ +// @ts-nocheck +// TODO: In the future, we plan to add proper typing +import EventEmitter from 'events'; + +const lengths = { + byte: 1, + int: 4, + int32: 4, + uint32: 4, + int64: 8, + uint64: 8, + dword: 4, + short: 2, + long: 8, + float: 4, + double: 8, + bool: 1, + boolean: 1, + ptr: 4, + pointer: 4, + // str: 0, + // string: 0, + // vec3: 0, + // vector3: 0, + // vec4: 0, + // vector4: 0, +}; + +// Tracks used and unused registers +class Registers { + constructor() { + this.registers = Object.freeze({ + DR0: 0x0, + DR1: 0x1, + DR2: 0x2, + DR3: 0x3, + }); + + this.used = []; + } + + getRegister() { + const unused = Object + .values(this.registers) + .filter(r => !this.used.includes(r)); + + return unused[0]; + } + + busy(register) { + this.used.push(register); + } + + unbusy(register) { + this.used.splice(this.used.indexOf(register), 1); + } +} + +class Debugger extends EventEmitter { + constructor(memoryprocess) { + super(); + this.memoryprocess = memoryprocess; + this.registers = new Registers(); + this.attached = false; + this.intervals = []; + } + + attach(processId, killOnDetach = false) { + const success = this.memoryprocess.attachDebugger(processId, killOnDetach); + + if (success) { + this.attached = true; + } + + return success; + } + + detach(processId) { + this.intervals.map(({ id }) => clearInterval(id)); + return this.memoryprocess.detachDebugger(processId); + } + + removeHardwareBreakpoint(processId, register) { + const success = this.memoryprocess.removeHardwareBreakpoint(processId, register); + + if (success) { + this.registers.unbusy(register); + } + + // Find the register's corresponding interval and delete it + this.intervals.forEach(({ register: r, id }) => { + if (r === register) { + clearInterval(id); + } + }); + + return success; + } + + setHardwareBreakpoint(processId, address, trigger, dataType) { + let size = lengths[dataType]; + + // If we are breakpointing a string, we need to determine the length of it + if (dataType === 'str' || dataType === 'string') { + const { handle } = this.memoryprocess.openProcess(processId); + const value = this.memoryprocess.readMemory(handle, address, this.memoryprocess.STRING); + + size = value.length; + + this.memoryprocess.closeProcess(handle); + } + + // Obtain an available register + const register = this.registers.getRegister(); + const success = this.memoryprocess + .setHardwareBreakpoint(processId, address, register, trigger, size); + + // If the breakpoint was set, mark this register as busy + if (success) { + this.registers.busy(register); + this.monitor(register); + } + + return register; + } + + monitor(register, timeout = 100) { + const id = setInterval(() => { + const debugEvent = this.memoryprocess.awaitDebugEvent(register, timeout); + + if (debugEvent) { + this.memoryprocess.handleDebugEvent(debugEvent.processId, debugEvent.threadId); + + // Global event for all registers + this.emit('debugEvent', { + register, + event: debugEvent, + }); + + // Event per register + this.emit(register, debugEvent); + } + }, 100); + + this.intervals.push({ + register, + id, + }); + } +} + +export default Debugger; diff --git a/src/memoryprocess.ts b/src/memoryprocess.ts new file mode 100644 index 0000000..db4b8b9 --- /dev/null +++ b/src/memoryprocess.ts @@ -0,0 +1,547 @@ +// @ts-ignore +import memoryprocess from './native.node'; +import { existsSync, type PathLike } from 'fs'; +import { MemoryAllocationFlags, type Protection, MemoryAccessFlags, MemoryPageFlags, type Process, type Module, type DataType, type MemoryData } from "./types" +import Debugger from './debugger'; +import { STRUCTRON_TYPE_STRING } from './utils'; + +/* TODO: + * - remove callbacks from all functions and implement promise support using Napi + * - validate argument types in JS space instead of C++ + * - refactor read/write memory functions to use buffers instead? + */ + +/** + * Opens a process by name or id. + * + * @param processIdentifier - The name or id of the process to open. + * @param callback - Optional callback function to handle the result asynchronously. + * @returns The handle of the opened process or undefined if the process was not found. + */ +function openProcess(processIdentifier: string | number, callback?: ((handle: number, errorMessage: string) => void) | undefined): Process { + if (!callback) { + return memoryprocess.openProcess(processIdentifier); + } + + return memoryprocess.openProcess(processIdentifier, callback); +} + +/** + * Closes a handle to a process. + * + * @param handle - The handle of the process to close. + * @returns Whether the operation was successful. + */ +function closeHandle(handle: number) { + return memoryprocess.closeHandle(handle); +} + +/** + * Retrieves a list of all processes currently running on the system. + * + * @param callback - Optional callback function to handle the result asynchronously. + * @returns An array of process objects or undefined if the operation failed. + */ +function getProcesses(callback?: ((processes: Process[], errorMessage: string) => void) | undefined) { + if (!callback) { + return memoryprocess.getProcesses(); + } + + return memoryprocess.getProcesses(callback); +} + +/** + * Finds a module by name within a process. + * + * @param moduleName - The name of the module to find. + * @param processId - The ID of the process to search within. + * @param callback - Optional callback function to handle the result asynchronously. + * @returns The module object or undefined if the module was not found. + */ +function findModule(moduleName: string, processId: number, callback?: ((module: Module, errorMessage: string) => void) | undefined) { + if (!callback) { + return memoryprocess.findModule(moduleName, processId); + } + + return memoryprocess.findModule(moduleName, processId, callback); +} + +/** + * Retrieves a list of all modules loaded by a process. + * + * @param processId - The ID of the process to search within. + * @param callback - Optional callback function to handle the result asynchronously. + * @returns An array of module objects or undefined if the operation failed. + */ +function getModules(processId: number, callback?: ((modules: Module[], errorMessage: string) => void) | undefined) { + if (!callback) { + return memoryprocess.getModules(processId); + } + + return memoryprocess.getModules(processId, callback); +} + +/** + * Reads a value from a process's memory. + * + * @param handle - The handle of the process to read from. + * @param address - The address to read from. + * @param dataType - The data type to read. + * @param callback - Optional callback function to handle the result asynchronously. + * @returns The read value or undefined if the operation failed. + */ +function readMemory(handle: number, address: number, dataType: T, callback?: (value: MemoryData, errorMessage: string) => void): MemoryData | undefined { + if (dataType.endsWith('_be')) { + return readMemoryBE(handle, address, dataType, callback); + } + + if (!callback) { + return memoryprocess.readMemory(handle, address, dataType); + } + + return memoryprocess.readMemory(handle, address, dataType, callback); +} + +/** + * Reads a value from a process's memory in big-endian format. + * + * @param handle - The handle of the process to read from. + * @param address - The address to read from. + * @param dataType - The data type to read. + * @param callback - Optional callback function to handle the result asynchronously. + * @returns The read value or undefined if the operation failed. + */ +function readMemoryBE(handle: number, address: number, dataType: T, callback?: (value: MemoryData, errorMessage: string) => void): MemoryData | undefined { + let value: number | bigint | null = null; + + switch (dataType) { + case 'int64_be': + value = readBuffer(handle, address, 8).readBigInt64BE(); + break; + + case 'uint64_be': + value = readBuffer(handle, address, 8).readBigUInt64BE(); + break; + + case 'int32_be': + case 'int_be': + case 'long_be': + value = readBuffer(handle, address, 4).readInt32BE(); + break; + + case 'uint32_be': + case 'uint_be': + case 'ulong_be': + value = readBuffer(handle, address, 4).readUInt32BE(); + break; + + case 'int16_be': + case 'short_be': + value = readBuffer(handle, address, 2).readInt16BE(); + break; + + case 'uint16_be': + case 'ushort_be': + value = readBuffer(handle, address, 2).readUInt16BE(); + break; + + case 'float_be': + value = readBuffer(handle, address, 4).readFloatBE(); + break; + + case 'double_be': + value = readBuffer(handle, address, 8).readDoubleBE(); + break; + } + + if (typeof callback !== 'function') { + if (value === null) { + throw new Error('Invalid data type argument!'); + } + + return value as MemoryData; + } + + callback(value as MemoryData, value === null ? 'Invalid data type argument!' : ''); +} + +/** + * Reads a buffer of specified size from a process's memory. + * + * @param handle - The handle of the process to read from. + * @param address - The memory address to read from. + * @param size - The number of bytes to read. + * @param callback - Optional callback function to handle the result asynchronously. + * @returns A buffer containing the read data or undefined if the operation failed. + */ +function readBuffer(handle: number, address: number, size: number, callback?: (buffer: Buffer, errorMessage: string) => void): Buffer { + if (arguments.length === 3) { + return memoryprocess.readBuffer(handle, address, size); + } + + return memoryprocess.readBuffer(handle, address, size, callback); +} + +/** + * Writes a value to a process's memory. + * + * @param handle - The handle of the process to write to. + * @param address - The address to write to. + * @param value - The value to write. + * @param dataType - The data type to write. + * @param callback - Optional callback function to handle the result asynchronously. + * @returns Whether the operation was successful. + */ +function writeMemory(handle: number, address: number, value: string, dataType: 'str'|'string'): boolean; +function writeMemory>(handle: number, address: number, value: MemoryData, dataType: T): boolean; +function writeMemory(handle: number, address: number, value: any, dataType: DataType): boolean { + let dataValue: MemoryData = value; + if (dataType === 'str' || dataType === 'string') { + // ensure TS knows this branch yields a string for T = 'str'|'string' + dataValue = ((value as string) + '\0') as MemoryData; + } + + const bigintTypes: DataType[] = ['int64', 'uint64', 'int64_be', 'uint64_be']; + if (bigintTypes.includes(dataType) && typeof value !== 'bigint') { + throw new Error(`${dataType.toUpperCase()} expects type BigInt`); + } + + if (dataType.endsWith('_be')) { + // dataValue narrowed to numeric types in BE branch + writeMemoryBE(handle, address, dataValue as number | bigint, dataType); + return true; + } + + return memoryprocess.writeMemory(handle, address, dataValue, dataType); +} + +/** + * Writes a value to a process's memory in big-endian format. + * + * @param handle - The handle of the process to write to. + * @param address - The address to write to. + * @param value - The value to write. + * @param dataType - The data type to write. + * @returns Whether the operation was successful. + */ +function writeMemoryBE(handle: number, address: number, value: number | bigint, dataType: DataType): boolean { + let buffer: Buffer | null = null; + + switch (dataType) { + case 'int64_be': + if (typeof value !== 'bigint') { + throw new Error('INT64_BE expects type BigInt'); + } + buffer = Buffer.alloc(8); + buffer.writeBigInt64BE(value); + break; + + case 'uint64_be': + if (typeof value !== 'bigint') { + throw new Error('UINT64_BE expects type BigInt'); + } + buffer = Buffer.alloc(8); + buffer.writeBigUInt64BE(value); + break; + + case 'int32_be': + case 'int_be': + case 'long_be': + buffer = Buffer.alloc(4); + buffer.writeInt32BE(value as number); + break; + + case 'uint32_be': + case 'uint_be': + case 'ulong_be': + buffer = Buffer.alloc(4); + buffer.writeUInt32BE(value as number); + break; + + case 'int16_be': + case 'short_be': + buffer = Buffer.alloc(2); + buffer.writeInt16BE(value as number); + break; + + case 'uint16_be': + case 'ushort_be': + buffer = Buffer.alloc(2); + buffer.writeUInt16BE(value as number); + break; + + case 'float_be': + buffer = Buffer.alloc(4); + buffer.writeFloatBE(value as number); + break; + + case 'double_be': + buffer = Buffer.alloc(8); + buffer.writeDoubleBE(value as number); + break; + } + + if (buffer == null) { + throw new Error('Invalid data type argument!'); + } + + writeBuffer(handle, address, buffer as Buffer); + return true; +} + +/** + * Writes a buffer of specified size to a process's memory. + * + * @param handle - The handle of the process to write to. + * @param address - The memory address to write to. + * @param buffer - The buffer containing the data to write. + * @returns Whether the operation was successful. + */ +function writeBuffer(handle: number, address: number, buffer: Buffer) { + return memoryprocess.writeBuffer(handle, address, buffer); +} + +// TODO: Implement pattern scanning functionality with various overloads to match the C++ implementation +function findPattern(...args: any[]): any { + const pattern = ['number', 'string', 'number', 'number'].toString(); + const patternByModule = ['number', 'string', 'string', 'number', 'number'].toString(); + const patternByAddress = ['number', 'number', 'string', 'number', 'number'].toString(); + + const types = args.map(arg => typeof arg); + + if (types.slice(0, 4).toString() === pattern) { + if (types.length === 4 || (types.length === 5 && types[4] === 'function')) { + // @ts-ignore + return memoryprocess.findPattern(...args); + } + } + + if (types.slice(0, 5).toString() === patternByModule) { + if (types.length === 5 || (types.length === 6 && types[5] === 'function')) { + // @ts-ignore + return memoryprocess.findPatternByModule(...args); + } + } + + if (types.slice(0, 5).toString() === patternByAddress) { + if (types.length === 5 || (types.length === 6 && types[5] === 'function')) { + // @ts-ignore + return memoryprocess.findPatternByAddress(...args); + } + } + + throw new Error('invalid arguments!'); +} + +/** + * Calls a function in a process's memory. + * + * @param handle - The handle of the process to call the function in. + * @param args - The arguments to pass to the function. + * @param returnType - The return type of the function. + * @param address - The address of the function to call. + * @param callback - Optional callback function to handle the result asynchronously. + * @returns The return value of the function or undefined if the operation failed. + */ +function callFunction(handle: number, args: any[], returnType: number, address: number, callback: ((errorMessage: string, info: { returnValue: any; exitCode: number; }) => void) | undefined) { + if (arguments.length === 4) { + return memoryprocess.callFunction(handle, args, returnType, address); + } + + return memoryprocess.callFunction(handle, args, returnType, address, callback); +} + +/** + * Allocates memory in a process's virtual address space. + * + * @param handle - The handle of the process to allocate memory in. + * @param address - The address to allocate memory at. If null, the memory will be allocated at the next available address. + * @param size - The size of the memory block to allocate. + * @param allocationType - The type of memory allocation to use. + * @param protection - The memory protection to apply. + * @param callback - Optional callback function to handle the result asynchronously. + * @returns The base address of the allocated memory or undefined if the operation failed. + */ +function virtualAllocEx( + handle: number, + address: number | null, + size: number, + allocationType: keyof typeof MemoryAllocationFlags, + protection: keyof typeof MemoryAccessFlags, + callback?: (errorMessage: string, baseAddress: number) => void +) { + const allocCode = MemoryAllocationFlags[allocationType]; + const protCode = MemoryAccessFlags[protection]; + if (arguments.length === 5) { + return memoryprocess.virtualAllocEx(handle, address, size, allocCode, protCode); + } + return memoryprocess.virtualAllocEx(handle, address, size, allocCode, protCode, callback!); +} + +/** + * Changes the protection of a region of memory in a process's virtual address space. + * + * @param handle - The handle of the process to change the memory protection in. + * @param address - The starting address of the memory region. + * @param size - The size of the memory region. + * @param protection - The new memory protection to apply. + * @param callback - Optional callback function to handle the result asynchronously. + * @returns The old memory protection or undefined if the operation failed. + */ +function virtualProtectEx( + handle: number, + address: number, + size: number, + protection: keyof typeof MemoryAccessFlags, + callback?: (errorMessage: string, oldProtection: Protection) => void +) { + const protCode = MemoryAccessFlags[protection]; + + if (arguments.length === 4) { + return memoryprocess.virtualProtectEx(handle, address, size, protCode); + } + return memoryprocess.virtualProtectEx(handle, address, size, protCode, callback!); +} + +/** + * Maps a view of a file into the virtual address space of a process. + * + * @param handle - The handle of the process to map the file into. + * @param fileHandle - The handle of the file to map. + * @param offset - The offset within the file to map. + * @param viewSize - The size of the view to map. + * @param pageProtection - The memory protection to apply. + * @returns The base address of the mapped view or undefined if the operation failed. + */ +function mapViewOfFile( + handle: number, + fileHandle: number, + offset: number, + viewSize: number, + pageProtection: keyof typeof MemoryPageFlags +) { + const pageCode = MemoryPageFlags[pageProtection]; + if (arguments.length === 2) { + return memoryprocess.mapViewOfFile(handle, fileHandle, 0, 0, pageCode); + } + return memoryprocess.mapViewOfFile(handle, fileHandle, offset, viewSize, pageCode); +} + +/** + * Retrieves a list of memory regions in a process's virtual address space. + * + * @param handle - The handle of the process to retrieve memory regions from. + * @param callback - Optional callback function to handle the result asynchronously. + * @returns An array of memory regions or undefined if the operation failed. + */ +function getRegions(handle: number, callback: ((regions: any[], errorMessage: string) => void) | undefined) { + if (arguments.length === 1) { + return memoryprocess.getRegions(handle); + } + + return memoryprocess.getRegions(handle, callback); +} + +/** + * Retrieves information about a specific memory region in a process's virtual address space. + + * @param handle - The handle of the process to retrieve memory region information from. + * @param address - The starting address of the memory region. + * @param callback - Optional callback function to handle the result asynchronously. + * @returns The memory region information or undefined if the operation failed. + */ +function virtualQueryEx(handle: number, address: number, callback: ((region: any, errorMessage: string) => void) | undefined) { + if (arguments.length === 2) { + return memoryprocess.virtualQueryEx(handle, address); + } + + return memoryprocess.virtualQueryEx(handle, address, callback); +} + +/** + * Injects a DLL into a process. + + * @param handle - The handle of the process to inject the DLL into. + * @param dllPath - The path to the DLL to inject. + * @param callback - Optional callback function to handle the result asynchronously. + * @returns Whether the operation was successful. + */ +function injectDll(handle: number, dllPath: PathLike, callback: ((errorMessage: string, success: boolean) => void) | undefined) { + if (!dllPath.toString().endsWith('.dll')) { + throw new Error("Given path is invalid: file is not of type 'dll'."); + } + + if (existsSync(dllPath)) { + throw new Error('Given path is invaild: file does not exist.'); + } + + if (arguments.length === 2) { + return memoryprocess.injectDll(handle, dllPath.toString()); + } + + return memoryprocess.injectDll(handle, dllPath.toString(), callback); +} + +/** + * Unloads a DLL from a process. + * + * @param handle - The handle of the process to unload the DLL from. + * @param module - The module to unload. + * @param callback - Optional callback function to handle the result asynchronously. + * @returns Whether the operation was successful. + */ +function unloadDll(handle: number, module: string | number, callback: ((errorMessage: string, success: boolean) => void) | undefined) { + if (arguments.length === 2) { + return memoryprocess.unloadDll(handle, module); + } + + return memoryprocess.unloadDll(handle, module, callback); +} + +/** + * Opens a file mapping object. + * + * @param fileName - The name of the file to open. + * @returns The handle of the opened file mapping object or undefined if the operation failed. + */ +function openFileMapping(fileName: string) { + if (arguments.length !== 1 || typeof fileName !== 'string') { + throw new Error('invalid arguments!'); + } + + return memoryprocess.openFileMapping(fileName); +} + +const library = { + openProcess, + closeHandle, + getProcesses, + findModule, + getModules, + readMemory, + readBuffer, + writeMemory, + writeBuffer, + findPattern, + callFunction, + virtualAllocEx, + virtualProtectEx, + getRegions, + virtualQueryEx, + injectDll, + unloadDll, + openFileMapping, + mapViewOfFile, + attachDebugger: memoryprocess.attachDebugger, + detachDebugger: memoryprocess.detachDebugger, + awaitDebugEvent: memoryprocess.awaitDebugEvent, + handleDebugEvent: memoryprocess.handleDebugEvent, + setHardwareBreakpoint: memoryprocess.setHardwareBreakpoint, + removeHardwareBreakpoint: memoryprocess.removeHardwareBreakpoint, + Debugger: new Debugger(memoryprocess), +}; + +export default { + ...library, + STRUCTRON_TYPE_STRING: STRUCTRON_TYPE_STRING(library), +}; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..ae83c9c --- /dev/null +++ b/src/types.ts @@ -0,0 +1,175 @@ +/** Function types */ +export const FunctionTypes = { + T_VOID: 0x0, + T_STRING: 0x1, + T_CHAR: 0x2, + T_BOOL: 0x3, + T_INT: 0x4, + T_DOUBLE: 0x5, + T_FLOAT: 0x6, +} as const; + +/** Signature scanning flags */ +export const SignatureTypes = { + NORMAL: 0x0, + READ: 0x1, + SUBTRACT: 0x2, +} as const; + +// Memory access flags +export const MemoryAccessFlags = { + PAGE_NOACCESS: 0x01, + PAGE_READONLY: 0x02, + PAGE_READWRITE: 0x04, + PAGE_WRITECOPY: 0x08, + PAGE_EXECUTE: 0x10, + PAGE_EXECUTE_READ: 0x20, + PAGE_EXECUTE_READWRITE: 0x40, + PAGE_EXECUTE_WRITECOPY: 0x80, + PAGE_GUARD: 0x100, + PAGE_NOCACHE: 0x200, + PAGE_WRITECOMBINE: 0x400, + PAGE_ENCLAVE_UNVALIDATED: 0x20000000, + PAGE_TARGETS_NO_UPDATE: 0x40000000, + PAGE_TARGETS_INVALID: 0x40000000, + PAGE_ENCLAVE_THREAD_CONTROL: 0x80000000, +} as const; + +// Memory allocation flags +export const MemoryAllocationFlags = { + MEM_COMMIT: 0x00001000, + MEM_RESERVE: 0x00002000, + MEM_RESET: 0x00080000, + MEM_TOP_DOWN: 0x00100000, + MEM_RESET_UNDO: 0x1000000, + MEM_LARGE_PAGES: 0x20000000, + MEM_PHYSICAL: 0x00400000, +} as const; + +// Memory page flags +export const MemoryPageFlags = { + MEM_PRIVATE: 0x20000, + MEM_MAPPED: 0x40000, + MEM_IMAGE: 0x1000000, +} as const; + +// Hardware debug registers +export const HardwareDebugRegisters = { + DR0: 0x0, + DR1: 0x1, + DR2: 0x2, + DR3: 0x3, +} as const; + +// Breakpoint trigger types +export const BreakpointTriggerTypes = { + TRIGGER_EXECUTE: 0x0, + TRIGGER_ACCESS: 0x3, + TRIGGER_WRITE: 0x1, +} as const; + + +export type Protection = typeof MemoryAccessFlags[keyof typeof MemoryAccessFlags]; +export type PageProtection = typeof MemoryPageFlags[keyof typeof MemoryPageFlags]; +export type AllocationType = typeof MemoryAllocationFlags[keyof typeof MemoryAllocationFlags]; +export type BreakpointTriggerType = typeof BreakpointTriggerTypes[keyof typeof BreakpointTriggerTypes]; + +/** + * Represents a process with its associated information and handle + */ +export interface Process { + /** + * Size of the structure, in bytes + */ + dwSize: number; + /** + * Process identifier (PID) + */ + th32ProcessID: number; + /** + * Number of execution threads started by the process + */ + cntThreads: number; + /** + * PID of the parent process + */ + th32ParentProcessID: number; + /** + * Base priority of threads created by this process + */ + pcPriClassBase: number; + /** + * Path to the executable file + */ + szExeFile: string; + /** + * Process handle (for read/write operations) + */ + handle: number; + /** + * Base address of the process's primary module + */ + modBaseAddr: number; +} + +/** + * Represents a module with its associated information + */ +export interface Module { + /** + * Base address of the module in the process's virtual address space + */ + modBaseAddr: number; + /** + * Size of the module, in bytes + */ + modBaseSize: number; + /** + * Full path to the module file + */ + szExePath: string; + /** + * Module name (filename) + */ + szModule: string; + /** + * Process identifier (PID) of the process owning this module + */ + th32ProcessID: number; + /** + * Global usage count of the module + */ + GlblcntUsage: number; +} + +/** + * Supported data types from constants.standard + */ +export type DataType = + 'byte' | 'ubyte' | 'char' | 'uchar' | + 'int8' | 'uint8' | + 'int16' | 'int16_be' | 'uint16' | 'uint16_be' | + 'short' | 'short_be' | 'ushort' | 'ushort_be' | + 'long' | 'long_be' | 'ulong' | 'ulong_be' | + 'int' | 'int_be' | 'uint' | 'uint_be' | + 'int32' | 'int32_be' | 'uint32' | 'uint32_be' | + 'int64' | 'int64_be' | 'uint64' | 'uint64_be' | + 'word' | 'dword' | + 'float' | 'float_be' | + 'double' | 'double_be'| + 'bool' | 'boolean' | + 'ptr' | 'pointer' | 'uptr' | 'upointer' | + 'str' | 'string' | + 'vec3' | 'vector3' | + 'vec4' | 'vector4'; + +/** + * Maps a DataType to its corresponding JS return type + */ +export type MemoryData = +/** 64-bit integers (LE/BE) as BigInt */ +T extends 'int64' | 'uint64' | 'int64_be' | 'uint64_be' ? bigint : +T extends 'string' | 'str' ? string : +T extends 'vector3' | 'vec3' ? { x: number; y: number; z: number } : +T extends 'vector4' | 'vec4' ? { x: number; y: number; z: number; w: number } : +number; \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..983076b --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,75 @@ +// @ts-nocheck +// TODO: In the future, we plan to add proper typing +const SIZEOF_STDSTRING_32BIT = 24; +const SIZEOF_STDSTRING_64BIT = 32; +const STDSTRING_LENGTH_OFFSET = 0x10; + +/** + * Custom string consumer/producer for Structron (due to complexity of `std::string`) + * `std::string` is a container for a string which makes reading/writing to it tricky, + * it will either store the string itself, or a pointer to the string, based on the + * length of the string. When we want to read from or write to a buffer, we need + * to determine if the string is in the buffer itself, or if the buffer + * just contains a pointer to the string. Based on one of these options, + * we can read from or write to the string. + * + * @param handle the handle to the process + * @param structAddress the base address of the structure in memory + * @param platform the architecture of the process, either "32" or "64" + * @param encoding the encoding type of the string + */ +export const STRUCTRON_TYPE_STRING = memoryprocess => (handle, structAddress, platform, encoding = 'utf8') => ({ + read(buffer, offset) { + // get string length from `std::string` container + const length = buffer.readUInt32LE(offset + STDSTRING_LENGTH_OFFSET); + + // if length > 15, `std::string` has a pointer to the string + if (length > 15) { + const pointer = platform === '64' ? buffer.readBigInt64LE(offset) : buffer.readUInt32LE(offset); + return memoryprocess.readMemory(handle, Number(pointer), 'string'); + } + + // if length <= 15, `std::string` directly contains the string + return buffer.toString(encoding, offset, offset + length); + }, + write(value, context, offset) { + // address containing the length of the string + const lengthAddress = structAddress + offset + STDSTRING_LENGTH_OFFSET; + + // get existing `std::string` buffer + const bufferSize = platform === '64' ? SIZEOF_STDSTRING_64BIT : SIZEOF_STDSTRING_32BIT; + const existingBuffer = memoryprocess.readBuffer(handle, structAddress + offset, bufferSize); + + // fetch length of string in memory (to determine if it's pointer based) + const length = memoryprocess.readMemory(handle, lengthAddress, 'int'); + + if ((length > 15 && value.length <= 15) || (length <= 15 && value.length > 15)) { + // there are two ways strings are stored: directly or with a pointer, + // we can't go from one to the other (without introducing more complexity), + // so just skip the bytes to prevent crashing. if a pointer is used, we could + // technically write any length, but the next time we try writing, we will read + // the length and assume it's not stored via pointer and will lead to crashes + + // write existing buffer without changes + existingBuffer.copy(context.buffer, offset); + return; + } + + // write new length + memoryprocess.writeMemory(handle, lengthAddress, value.length, 'uint32'); + existingBuffer.writeUInt32LE(value.length, STDSTRING_LENGTH_OFFSET); + + if (length > 15 && value.length > 15) { + // write new string in memory + const pointer = memoryprocess.readMemory(handle, structAddress + offset, 'pointer'); + memoryprocess.writeMemory(handle, pointer, value, 'string'); + } else if (length <= 15 && value.length <= 15) { + // write new string directly into buffer + existingBuffer.write(value, encoding); + } + + // write our new `std::string` buffer into the buffer we are creating + existingBuffer.copy(context.buffer, offset); + }, + SIZE: platform === '64' ? SIZEOF_STDSTRING_64BIT : SIZEOF_STDSTRING_32BIT, +}); From 66e4dfdde753e3992a0f6d59e1d8fa9268bff5ac Mon Sep 17 00:00:00 2001 From: JoShMiQueL Date: Fri, 18 Apr 2025 19:47:58 +0200 Subject: [PATCH 05/11] build: update build tooling and configuration --- .commitlintrc | 3 + .gitignore | 14 +- .npmignore | 30 +- .releaserc | 20 + binding.gyp | 16 +- biome.json | 27 + bun.lock | 331 ++++++ config/api-extractor.json | 454 +++++++ lefthook.yml | 17 + package-lock.json | 2374 ++++++++++++++++++++++--------------- package.json | 45 +- tsconfig.json | 30 + 12 files changed, 2345 insertions(+), 1016 deletions(-) create mode 100644 .commitlintrc create mode 100644 .releaserc create mode 100644 biome.json create mode 100644 bun.lock create mode 100644 config/api-extractor.json create mode 100644 lefthook.yml create mode 100644 tsconfig.json diff --git a/.commitlintrc b/.commitlintrc new file mode 100644 index 0000000..c30e5a9 --- /dev/null +++ b/.commitlintrc @@ -0,0 +1,3 @@ +{ + "extends": ["@commitlint/config-conventional"] +} diff --git a/.gitignore b/.gitignore index 067fd47..95cb72f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ +node_modules +lib build -test.js -npm-debug.log .vscode .vs -node_modules -test/vcxproj/Debug -test/vcxproj/Release -test/*.exe -test/.vs \ No newline at end of file +.windsurfrules + +# Logs +*.log +npm-debug.log* \ No newline at end of file diff --git a/.npmignore b/.npmignore index 067fd47..37dbd8d 100644 --- a/.npmignore +++ b/.npmignore @@ -1,10 +1,20 @@ -build -test.js -npm-debug.log -.vscode -.vs -node_modules -test/vcxproj/Debug -test/vcxproj/Release -test/*.exe -test/.vs \ No newline at end of file +# Ignore everything by default +* + +# Keep the lib directory and its contents +!lib/ +!lib/** + +# Keep the assets directory and its contents +!assets/ +!assets/** + +# Keep essential files +!package.json +!README.md +!LICENSE +!CHANGELOG.md + +# Keep the lock files +!package-lock.json +!bun.lockb \ No newline at end of file diff --git a/.releaserc b/.releaserc new file mode 100644 index 0000000..14e5826 --- /dev/null +++ b/.releaserc @@ -0,0 +1,20 @@ +{ + "branches": [ + "main", + { "name": "beta", "prerelease": true }, + { "name": "alpha", "prerelease": true } + ], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/npm", + "@semantic-release/github", + [ + "@semantic-release/git", + { + "assets": ["package.json", "CHANGELOG.md"], + "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" + } + ] + ] +} diff --git a/binding.gyp b/binding.gyp index 5f0e8ad..15de595 100644 --- a/binding.gyp +++ b/binding.gyp @@ -1,18 +1,18 @@ { "targets": [ { - "target_name": "memoryjs", + "target_name": "native", "include_dirs" : [ "=2.2.7 <3" }, "bin": "bin.js" }, ""], + + "ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, ""], + + "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" } }, ""], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, ""], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, ""], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""], + + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, ""], + + "array-ify": ["array-ify@1.0.0", "", {}, ""], + + "balanced-match": ["balanced-match@1.0.2", "", {}, ""], + + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, ""], + + "bun-types": ["bun-types@1.2.10", "", { "dependencies": { "@types/node": "*" } }, ""], + + "callsites": ["callsites@3.1.0", "", {}, ""], + + "chalk": ["chalk@5.4.1", "", {}, ""], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, ""], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, ""], + + "color-name": ["color-name@1.1.4", "", {}, ""], + + "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, ""], + + "concat-map": ["concat-map@0.0.1", "", {}, ""], + + "conventional-changelog-angular": ["conventional-changelog-angular@7.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, ""], + + "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@7.0.2", "", { "dependencies": { "compare-func": "^2.0.0" } }, ""], + + "conventional-commits-parser": ["conventional-commits-parser@5.0.0", "", { "dependencies": { "JSONStream": "^1.3.5", "is-text-path": "^2.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": "cli.mjs" }, ""], + + "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" } }, ""], + + "cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.1.0", "", { "dependencies": { "jiti": "^2.4.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, ""], + + "dargs": ["dargs@8.1.0", "", {}, ""], + + "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, ""], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, ""], + + "env-paths": ["env-paths@2.2.1", "", {}, ""], + + "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, ""], + + "escalade": ["escalade@3.2.0", "", {}, ""], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, ""], + + "find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, ""], + + "fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, ""], + + "function-bind": ["function-bind@1.1.2", "", {}, ""], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, ""], + + "git-raw-commits": ["git-raw-commits@4.0.0", "", { "dependencies": { "dargs": "^8.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": "cli.mjs" }, ""], + + "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, ""], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, ""], + + "has-flag": ["has-flag@4.0.0", "", {}, ""], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, ""], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, ""], + + "import-lazy": ["import-lazy@4.0.0", "", {}, ""], + + "import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, ""], + + "ini": ["ini@4.1.1", "", {}, ""], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, ""], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, ""], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, ""], + + "is-obj": ["is-obj@2.0.0", "", {}, ""], + + "is-text-path": ["is-text-path@2.0.0", "", { "dependencies": { "text-extensions": "^2.0.0" } }, ""], + + "jiti": ["jiti@2.4.2", "", { "bin": "lib/jiti-cli.mjs" }, ""], + + "jju": ["jju@1.4.0", "", {}, ""], + + "js-tokens": ["js-tokens@4.0.0", "", {}, ""], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, ""], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, ""], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, ""], + + "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, ""], + + "jsonparse": ["jsonparse@1.3.1", "", {}, ""], + + "lefthook": ["lefthook@1.11.10", "", { "optionalDependencies": { "lefthook-windows-x64": "1.11.10" }, "bin": "bin/index.js" }, ""], + + "lefthook-windows-x64": ["lefthook-windows-x64@1.11.10", "", { "os": "win32", "cpu": "x64" }, ""], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, ""], + + "locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, ""], + + "lodash": ["lodash@4.17.21", "", {}, ""], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, ""], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, ""], + + "lodash.kebabcase": ["lodash.kebabcase@4.1.1", "", {}, ""], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, ""], + + "lodash.mergewith": ["lodash.mergewith@4.6.2", "", {}, ""], + + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, ""], + + "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, ""], + + "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, ""], + + "lodash.upperfirst": ["lodash.upperfirst@4.3.1", "", {}, ""], + + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, ""], + + "meow": ["meow@12.1.1", "", {}, ""], + + "minimatch": ["minimatch@3.0.8", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, ""], + + "minimist": ["minimist@1.2.8", "", {}, ""], + + "node-addon-api": ["node-addon-api@3.2.1", "", {}, ""], + + "p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, ""], + + "p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, ""], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, ""], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, ""], + + "path-exists": ["path-exists@5.0.0", "", {}, ""], + + "path-parse": ["path-parse@1.0.7", "", {}, ""], + + "picocolors": ["picocolors@1.1.1", "", {}, ""], + + "punycode": ["punycode@2.3.1", "", {}, ""], + + "require-directory": ["require-directory@2.1.1", "", {}, ""], + + "require-from-string": ["require-from-string@2.0.2", "", {}, ""], + + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, ""], + + "resolve-from": ["resolve-from@5.0.0", "", {}, ""], + + "semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": "bin/semver.js" }, ""], + + "source-map": ["source-map@0.6.1", "", {}, ""], + + "split2": ["split2@4.2.0", "", {}, ""], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, ""], + + "string-argv": ["string-argv@0.3.2", "", {}, ""], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, ""], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, ""], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, ""], + + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, ""], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, ""], + + "text-extensions": ["text-extensions@2.4.0", "", {}, ""], + + "through": ["through@2.3.8", "", {}, ""], + + "tinyexec": ["tinyexec@0.3.2", "", {}, ""], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, ""], + + "undici-types": ["undici-types@6.21.0", "", {}, ""], + + "unicorn-magic": ["unicorn-magic@0.1.0", "", {}, ""], + + "universalify": ["universalify@2.0.1", "", {}, ""], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, ""], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, ""], + + "y18n": ["y18n@5.0.8", "", {}, ""], + + "yallist": ["yallist@4.0.0", "", {}, ""], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, ""], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, ""], + + "yocto-queue": ["yocto-queue@1.2.1", "", {}, ""], + + "@commitlint/config-validator/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, ""], + + "@commitlint/is-ignored/semver": ["semver@7.7.1", "", { "bin": "bin/semver.js" }, ""], + + "@microsoft/api-extractor/typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, ""], + + "@rushstack/node-core-library/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, ""], + + "ajv-formats/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, ""], + + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, ""], + + "js-yaml/argparse": ["argparse@2.0.1", "", {}, ""], + } +} diff --git a/config/api-extractor.json b/config/api-extractor.json new file mode 100644 index 0000000..5063ac4 --- /dev/null +++ b/config/api-extractor.json @@ -0,0 +1,454 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "extends": "./shared/api-extractor-base.json" + // "extends": "my-package/include/api-extractor-base.json" + + /** + * Determines the "" token that can be used with other config file settings. The project folder + * typically contains the tsconfig.json and package.json config files, but the path is user-defined. + * + * The path is resolved relative to the folder of the config file that contains the setting. + * + * The default value for "projectFolder" is the token "", which means the folder is determined by traversing + * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder + * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error + * will be reported. + * + * SUPPORTED TOKENS: + * DEFAULT VALUE: "" + */ + // "projectFolder": "..", + + /** + * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor + * analyzes the symbols exported by this module. + * + * The file extension must be ".d.ts" and not ".ts". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + */ + "mainEntryPointFilePath": "/lib/index.d.ts", + + /** + * A list of NPM package names whose exports should be treated as part of this package. + * + * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", + * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part + * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly + * imports library2. To avoid this, we might specify: + * + * "bundledPackages": [ "library2" ], + * + * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been + * local files for library1. + * + * The "bundledPackages" elements may specify glob patterns using minimatch syntax. To ensure deterministic + * output, globs are expanded by matching explicitly declared top-level dependencies only. For example, + * the pattern below will NOT match "@my-company/example" unless it appears in a field such as "dependencies" + * or "devDependencies" of the project's package.json file: + * + * "bundledPackages": [ "@my-company/*" ], + */ + "bundledPackages": [], + + /** + * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files + * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. + * To use the OS's default newline kind, specify "os". + * + * DEFAULT VALUE: "crlf" + */ + // "newlineKind": "crlf", + + /** + * Specifies how API Extractor sorts members of an enum when generating the .api.json file. By default, the output + * files will be sorted alphabetically, which is "by-name". To keep the ordering in the source code, specify + * "preserve". + * + * DEFAULT VALUE: "by-name" + */ + // "enumMemberOrder": "by-name", + + /** + * Set to true when invoking API Extractor's test harness. When `testMode` is true, the `toolVersion` field in the + * .api.json file is assigned an empty string to prevent spurious diffs in output files tracked for tests. + * + * DEFAULT VALUE: "false" + */ + // "testMode": false, + + /** + * Determines how the TypeScript compiler engine will be invoked by API Extractor. + */ + "compiler": { + /** + * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * Note: This setting will be ignored if "overrideTsconfig" is used. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/tsconfig.json" + */ + // "tsconfigFilePath": "/tsconfig.json", + /** + * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. + * The object must conform to the TypeScript tsconfig schema: + * + * http://json.schemastore.org/tsconfig + * + * If omitted, then the tsconfig.json file will be read from the "projectFolder". + * + * DEFAULT VALUE: no overrideTsconfig section + */ + // "overrideTsconfig": { + // . . . + // } + /** + * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended + * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when + * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses + * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. + * + * DEFAULT VALUE: false + */ + // "skipLibCheck": true, + }, + + /** + * Configures how the API report file (*.api.md) will be generated. + */ + "apiReport": { + /** + * (REQUIRED) Whether to generate an API report. + */ + "enabled": false + + /** + * The base filename for the API report files, to be combined with "reportFolder" or "reportTempFolder" + * to produce the full file path. The "reportFileName" should not include any path separators such as + * "\" or "/". The "reportFileName" should not include a file extension, since API Extractor will automatically + * append an appropriate file extension such as ".api.md". If the "reportVariants" setting is used, then the + * file extension includes the variant name, for example "my-report.public.api.md" or "my-report.beta.api.md". + * The "complete" variant always uses the simple extension "my-report.api.md". + * + * Previous versions of API Extractor required "reportFileName" to include the ".api.md" extension explicitly; + * for backwards compatibility, that is still accepted but will be discarded before applying the above rules. + * + * SUPPORTED TOKENS: , + * DEFAULT VALUE: "" + */ + // "reportFileName": "", + + /** + * To support different approval requirements for different API levels, multiple "variants" of the API report can + * be generated. The "reportVariants" setting specifies a list of variants to be generated. If omitted, + * by default only the "complete" variant will be generated, which includes all @internal, @alpha, @beta, + * and @public items. Other possible variants are "alpha" (@alpha + @beta + @public), "beta" (@beta + @public), + * and "public" (@public only). + * + * DEFAULT VALUE: [ "complete" ] + */ + // "reportVariants": ["public", "beta"], + + /** + * Specifies the folder where the API report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, + * e.g. for an API review. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/etc/" + */ + // "reportFolder": "/etc/", + + /** + * Specifies the folder where the temporary report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * After the temporary file is written to disk, it is compared with the file in the "reportFolder". + * If they are different, a production build will fail. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + // "reportTempFolder": "/temp/", + + /** + * Whether "forgotten exports" should be included in the API report file. Forgotten exports are declarations + * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to + * learn more. + * + * DEFAULT VALUE: "false" + */ + // "includeForgottenExports": false + }, + + /** + * Configures how the doc model file (*.api.json) will be generated. + */ + "docModel": { + /** + * (REQUIRED) Whether to generate a doc model file. + */ + "enabled": false + + /** + * The output path for the doc model file. The file extension should be ".api.json". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/.api.json" + */ + // "apiJsonFilePath": "/temp/.api.json", + + /** + * Whether "forgotten exports" should be included in the doc model file. Forgotten exports are declarations + * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to + * learn more. + * + * DEFAULT VALUE: "false" + */ + // "includeForgottenExports": false, + + /** + * The base URL where the project's source code can be viewed on a website such as GitHub or + * Azure DevOps. This URL path corresponds to the `` path on disk. + * + * This URL is concatenated with the file paths serialized to the doc model to produce URL file paths to individual API items. + * For example, if the `projectFolderUrl` is "https://github.com/microsoft/rushstack/tree/main/apps/api-extractor" and an API + * item's file path is "api/ExtractorConfig.ts", the full URL file path would be + * "https://github.com/microsoft/rushstack/tree/main/apps/api-extractor/api/ExtractorConfig.js". + * + * This setting can be omitted if you don't need source code links in your API documentation reference. + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "projectFolderUrl": "http://github.com/path/to/your/projectFolder" + }, + + /** + * Configures how the .d.ts rollup file will be generated. + */ + "dtsRollup": { + /** + * (REQUIRED) Whether to generate the .d.ts rollup file. + */ + "enabled": true, + + /** + * Specifies the output path for a .d.ts rollup file to be generated without any trimming. + * This file will include all declarations that are exported by the main entry point. + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/dist/.d.ts" + */ + "untrimmedFilePath": "/lib/index.d.ts" + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for an "alpha" release. + * This file will include only declarations that are marked as "@public", "@beta", or "@alpha". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "alphaTrimmedFilePath": "/dist/-alpha.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. + * This file will include only declarations that are marked as "@public" or "@beta". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "betaTrimmedFilePath": "/dist/-beta.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. + * This file will include only declarations that are marked as "@public". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "publicTrimmedFilePath": "/dist/-public.d.ts", + + /** + * When a declaration is trimmed, by default it will be replaced by a code comment such as + * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the + * declaration completely. + * + * DEFAULT VALUE: false + */ + // "omitTrimmingComments": true + }, + + /** + * Configures how the tsdoc-metadata.json file will be generated. + */ + "tsdocMetadata": { + /** + * Whether to generate the tsdoc-metadata.json file. + * + * DEFAULT VALUE: true + */ + "enabled": false + /** + * Specifies where the TSDoc metadata file should be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", + * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup + * falls back to "tsdoc-metadata.json" in the package folder. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" + }, + + /** + * Configures how API Extractor reports error and warning messages produced during analysis. + * + * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. + */ + "messages": { + /** + * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing + * the input .d.ts files. + * + * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "compilerMessageReporting": { + /** + * Configures the default routing for messages that don't match an explicit rule in this table. + */ + "default": { + /** + * Specifies whether the message should be written to the the tool's output log. Note that + * the "addToApiReportFile" property may supersede this option. + * + * Possible values: "error", "warning", "none" + * + * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail + * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes + * the "--local" option), the warning is displayed but the build will not fail. + * + * DEFAULT VALUE: "warning" + */ + "logLevel": "warning" + + /** + * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), + * then the message will be written inside that file; otherwise, the message is instead logged according to + * the "logLevel" option. + * + * DEFAULT VALUE: false + */ + // "addToApiReportFile": false + } + + // "TS2551": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by API Extractor during its analysis. + * + * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" + * + * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings + */ + "extractorMessageReporting": { + "default": { + "logLevel": "none" + // "addToApiReportFile": false + } + + // "ae-extra-release-tag": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by the TSDoc parser when analyzing code comments. + * + * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + } + + // "tsdoc-link-tag-unescaped-text": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + } + } +} diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..0262a61 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,17 @@ +pre-commit: + commands: + check: + glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}" + run: npx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} + stage_fixed: true + +commit-msg: + commands: + lint-commit-msg: + run: npx commitlint --edit {1} + +pre-push: + commands: + check: + glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}" + run: npx @biomejs/biome check --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {push_files} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d06d6cc..efc59ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1168 +1,1598 @@ { - "name": "memoryjs", - "version": "3.3.1", - "lockfileVersion": 1, + "name": "memoryprocess", + "version": "3.5.1", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@eslint/eslintrc": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz", - "integrity": "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==", - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.2.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" + "packages": { + "": { + "name": "memoryprocess", + "version": "3.5.1", + "license": "MIT", + "dependencies": { + "node-addon-api": "3.2.1" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@commitlint/cli": "19.8.0", + "@commitlint/config-conventional": "19.8.0", + "@microsoft/api-extractor": "7.52.4", + "@total-typescript/tsconfig": "1.0.4", + "@types/bun": "1.2.10", + "@types/node": "22.14.1", + "lefthook": "1.11.10", + "typescript": "5" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "@humanwhocodes/config-array": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz", - "integrity": "sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==", - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.4" + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" - }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" - }, - "acorn": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", - "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==" - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "node_modules/@biomejs/biome": { + "version": "1.9.4", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.9.4", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@commitlint/cli": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/format": "^19.8.0", + "@commitlint/lint": "^19.8.0", + "@commitlint/load": "^19.8.0", + "@commitlint/read": "^19.8.0", + "@commitlint/types": "^19.8.0", + "tinyexec": "^0.3.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v18" } }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" + "node_modules/@commitlint/config-conventional": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.0", + "conventional-changelog-conventionalcommits": "^7.0.2" + }, + "engines": { + "node": ">=v18" + } }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + "node_modules/@commitlint/config-validator": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.0", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v18" + } }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" + "node_modules/@commitlint/config-validator/node_modules/ajv": { + "version": "8.13.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "array-includes": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", - "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.7" - } - }, - "array.prototype.flat": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", - "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "node_modules/@commitlint/ensure": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.0", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v18" + } }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node_modules/@commitlint/execute-rule": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" } }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "node_modules/@commitlint/format": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" } }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + "node_modules/@commitlint/is-ignored": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.0", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=v18" + } }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "node_modules/@commitlint/is-ignored/node_modules/semver": { + "version": "7.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" + "node_modules/@commitlint/lint": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/is-ignored": "^19.8.0", + "@commitlint/parse": "^19.8.0", + "@commitlint/rules": "^19.8.0", + "@commitlint/types": "^19.8.0" + }, + "engines": { + "node": ">=v18" } }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "node_modules/@commitlint/load": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^19.8.0", + "@commitlint/execute-rule": "^19.8.0", + "@commitlint/resolve-extends": "^19.8.0", + "@commitlint/types": "^19.8.0", + "chalk": "^5.3.0", + "cosmiconfig": "^9.0.0", + "cosmiconfig-typescript-loader": "^6.1.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" + }, + "engines": { + "node": ">=v18" + } }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } + "node_modules/@commitlint/message": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" } }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "node_modules/@commitlint/parse": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.0", + "conventional-changelog-angular": "^7.0.0", + "conventional-commits-parser": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "^1.0.12" + "node_modules/@commitlint/read": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/top-level": "^19.8.0", + "@commitlint/types": "^19.8.0", + "git-raw-commits": "^4.0.0", + "minimist": "^1.2.8", + "tinyexec": "^0.3.0" + }, + "engines": { + "node": ">=v18" } }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "requires": { - "esutils": "^2.0.2" - } - }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "requires": { - "ansi-colors": "^4.1.1" - } - }, - "es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "node_modules/@commitlint/resolve-extends": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^19.8.0", + "@commitlint/types": "^19.8.0", + "global-directory": "^4.0.1", + "import-meta-resolve": "^4.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=v18" } }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" - }, - "eslint": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.5.0.tgz", - "integrity": "sha512-tVGSkgNbOfiHyVte8bCM8OmX+xG9PzVG/B4UCF60zx7j61WIVY/AqJECDgpLD4DbbESD0e174gOg3ZlrX15GDg==", - "requires": { - "@eslint/eslintrc": "^1.0.5", - "@humanwhocodes/config-array": "^0.9.2", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.0", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.1.0", - "espree": "^9.2.0", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.2.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - } - }, - "eslint-config-airbnb-base": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz", - "integrity": "sha512-/vjm0Px5ZCpmJqnjIzcFb9TKZrKWz0gnuG/7Gfkt0Db1ELJR51xkZth+t14rYdqWgX836XbuxtArbIHlVhbLBA==", - "requires": { - "eslint-restricted-globals": "^0.1.1" - } - }, - "eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", - "requires": { - "debug": "^3.2.7", - "resolve": "^1.20.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } + "node_modules/@commitlint/rules": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/ensure": "^19.8.0", + "@commitlint/message": "^19.8.0", + "@commitlint/to-lines": "^19.8.0", + "@commitlint/types": "^19.8.0" + }, + "engines": { + "node": ">=v18" } }, - "eslint-module-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz", - "integrity": "sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ==", - "requires": { - "debug": "^3.2.7", - "find-up": "^2.1.0", - "pkg-dir": "^2.0.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } + "node_modules/@commitlint/to-lines": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" } }, - "eslint-plugin-import": { - "version": "2.25.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz", - "integrity": "sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg==", - "requires": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.1", - "has": "^1.0.3", - "is-core-module": "^2.8.0", - "is-glob": "^4.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.5", - "resolve": "^1.20.0", - "tsconfig-paths": "^3.11.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "requires": { - "esutils": "^2.0.2" - } - } + "node_modules/@commitlint/top-level": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^7.0.0" + }, + "engines": { + "node": ">=v18" } }, - "eslint-restricted-globals": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz", - "integrity": "sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc=" + "node_modules/@commitlint/types": { + "version": "19.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/conventional-commits-parser": "^5.0.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } }, - "eslint-scope": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", - "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "node_modules/@microsoft/api-extractor": { + "version": "7.52.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/api-extractor-model": "7.30.5", + "@microsoft/tsdoc": "~0.15.1", + "@microsoft/tsdoc-config": "~0.17.1", + "@rushstack/node-core-library": "5.13.0", + "@rushstack/rig-package": "0.5.3", + "@rushstack/terminal": "0.15.2", + "@rushstack/ts-command-line": "4.23.7", + "lodash": "~4.17.15", + "minimatch": "~3.0.3", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "source-map": "~0.6.1", + "typescript": "5.8.2" + }, + "bin": { + "api-extractor": "bin/api-extractor" } }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "requires": { - "eslint-visitor-keys": "^2.0.0" + "node_modules/@microsoft/api-extractor-model": { + "version": "7.30.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "~0.15.1", + "@microsoft/tsdoc-config": "~0.17.1", + "@rushstack/node-core-library": "5.13.0" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/typescript": { + "version": "5.8.2", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.15.1", + "ajv": "~8.12.0", + "jju": "~1.4.0", + "resolve": "~1.22.2" + } + }, + "node_modules/@rushstack/node-core-library": { + "version": "5.13.0", + "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==" + "ajv": "~8.13.0", + "ajv-draft-04": "~1.0.0", + "ajv-formats": "~3.0.1", + "fs-extra": "~11.3.0", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true } } }, - "eslint-visitor-keys": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", - "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==" + "node_modules/@rushstack/node-core-library/node_modules/ajv": { + "version": "8.13.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } }, - "espree": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.2.0.tgz", - "integrity": "sha512-oP3utRkynpZWF/F2x/HZJ+AGtnIclaR7z1pYPxy7NYM2fSO6LgK/Rkny8anRSPK/VwEA1eqm2squui0T7ZMOBg==", - "requires": { - "acorn": "^8.6.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^3.1.0" + "node_modules/@rushstack/rig-package": { + "version": "0.5.3", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" } }, - "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "requires": { - "estraverse": "^5.1.0" + "node_modules/@rushstack/terminal": { + "version": "0.15.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/node-core-library": "5.13.0", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "requires": { - "estraverse": "^5.2.0" + "node_modules/@rushstack/ts-command-line": { + "version": "4.23.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/terminal": "0.15.2", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" } }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + "node_modules/@total-typescript/tsconfig": { + "version": "1.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bun": { + "version": "1.2.10", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.2.10" + } }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "node_modules/@types/node": { + "version": "22.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "node_modules/ajv": { + "version": "8.12.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "requires": { - "flat-cache": "^3.0.4" + "node_modules/ajv-formats": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "^2.0.0" + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.13.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "flatted": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", - "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==" + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } }, - "fs.realpath": { + "node_modules/array-ify": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "dev": true, + "license": "MIT" }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" + "node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "node_modules/bun-types": { + "version": "1.2.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" } }, - "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "5.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", - "requires": { - "type-fest": "^0.20.2" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" + "node_modules/cliui": { + "version": "8.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, - "has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + "node_modules/compare-func": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "requires": { - "has-symbols": "^1.0.2" + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" } }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" } }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + "node_modules/conventional-commits-parser": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, + "engines": { + "node": ">=16" + } }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" + "node_modules/cosmiconfig": { + "version": "9.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "node_modules/cosmiconfig-typescript-loader": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jiti": "^2.4.1" + }, + "engines": { + "node": ">=v18" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=9", + "typescript": ">=5" + } + }, + "node_modules/dargs": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" + "node_modules/dot-prop": { + "version": "5.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "requires": { - "has-bigints": "^1.0.1" + "node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, - "is-boolean-object": { + "node_modules/error-ex": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/find-up": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fs-extra": { + "version": "11.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } }, - "is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "requires": { - "has": "^1.0.3" + "node_modules/git-raw-commits": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "dargs": "^8.0.0", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.mjs" + }, + "engines": { + "node": ">=16" } }, - "is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "requires": { - "has-tostringtag": "^1.0.0" + "node_modules/global-directory": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "license": "ISC" }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "requires": { - "is-extglob": "^2.1.1" + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "is-negative-zero": { + "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } }, - "is-number-object": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", - "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", - "requires": { - "has-tostringtag": "^1.0.0" + "node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" } }, - "is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==" + "node_modules/import-lazy": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "requires": { - "has-tostringtag": "^1.0.0" + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "requires": { - "has-symbols": "^1.0.2" + "node_modules/ini": { + "version": "4.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "requires": { - "call-bind": "^1.0.2" + "node_modules/is-arrayish": { + "version": "0.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "isexe": { + "node_modules/is-text-path": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "dev": true, + "license": "MIT", + "dependencies": { + "text-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jju": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" }, - "js-yaml": { + "node_modules/js-yaml": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "requires": { + "dev": true, + "license": "MIT", + "dependencies": { "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "node_modules/js-yaml/node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "license": "MIT" }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "requires": { - "minimist": "^1.2.0" + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "dev": true, + "license": "(MIT OR Apache-2.0)", "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - } + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" } }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } + "node_modules/lefthook": { + "version": "1.11.10", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "lefthook": "bin/index.js" + }, + "optionalDependencies": { + "lefthook-darwin-arm64": "1.11.10", + "lefthook-darwin-x64": "1.11.10", + "lefthook-freebsd-arm64": "1.11.10", + "lefthook-freebsd-x64": "1.11.10", + "lefthook-linux-arm64": "1.11.10", + "lefthook-linux-x64": "1.11.10", + "lefthook-openbsd-arm64": "1.11.10", + "lefthook-openbsd-x64": "1.11.10", + "lefthook-windows-arm64": "1.11.10", + "lefthook-windows-x64": "1.11.10" + } + }, + "node_modules/lefthook-windows-x64": { + "version": "1.11.10", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "node_modules/locate-path": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "lodash.merge": { + "node_modules/lodash": { + "version": "4.17.21", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "dev": true, + "license": "MIT" }, - "lru-cache": { + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { + "dev": true, + "license": "ISC", + "dependencies": { "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" + "node_modules/meow": { + "version": "12.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "node_modules/minimatch": { + "version": "3.0.8", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" + "node_modules/minimist": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node-addon-api": { + "node_modules/node-addon-api": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" + "license": "MIT" }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - }, - "object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "^1.1.0" + "node_modules/p-limit": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + "node_modules/p-locate": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "parent-module": { + "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "requires": { + "dev": true, + "license": "MIT", + "dependencies": { "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "node_modules/parse-json": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + "node_modules/path-exists": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } }, - "path-parse": { + "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "dev": true, + "license": "MIT" }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "requires": { - "find-up": "^2.1.0" - } - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } }, - "punycode": { + "node_modules/require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==" + "node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" + "node_modules/resolve": { + "version": "1.22.10", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" + "node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" + "node_modules/semver": { + "version": "7.5.4", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + "node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "node_modules/split2": { + "version": "4.2.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" } }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "node_modules/sprintf-js": { + "version": "1.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" } }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "strip-ansi": { + "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { + "dev": true, + "license": "MIT", + "dependencies": { "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" - }, - "strip-json-comments": { + "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { + "node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" - }, - "tsconfig-paths": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", - "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==", - "requires": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.0", - "strip-bom": "^3.0.0" + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "requires": { - "prelude-ls": "^1.2.1" + "node_modules/text-extensions": { + "version": "2.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.8.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" } }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" + "node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" }, - "unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "requires": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" + "node_modules/unicorn-magic": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "uri-js": { + "node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { "punycode": "^2.1.0" } }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==" - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" + "node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } }, - "yallist": { + "node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 4ff13ab..9c81da2 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,17 @@ { - "name": "@joshmiquel/memoryjs", + "name": "memoryprocess", "version": "3.5.1", "description": "Node add-on for memory reading and writing!", - "main": "index.js", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "install": "npm run build", - "build": "node ./scripts/install.js", - "build32": "node-gyp clean configure build --arch=ia32", - "build64": "node-gyp clean configure build --arch=x64", - "buildtest": "cd test && MSBuild.exe project.sln //p:Configuration=Release", - "debug": "node ./scripts/debug.js", - "debug32": "node-gyp configure rebuild --debug --arch=xia32", - "debug64": "node-gyp configure rebuild --debug --arch=x64" + "build": "bun run scripts/build.ts", + "check": "biome check --write" }, "repository": { "type": "git", - "url": "git+https://github.com/joshmiquel/memoryjs.git" + "url": "git+https://github.com/joshmiquel/memoryprocess.git" }, "keywords": [ "memory", @@ -27,14 +22,26 @@ ], "author": "JoShMiQueL", "license": "MIT", - "gypfile": true, "bugs": { - "url": "https://github.com/joshmiquel/memoryjs/issues" + "url": "https://github.com/joshmiquel/memoryprocess/issues" }, - "homepage": "https://github.com/joshmiquel/memoryjs#readme", + "homepage": "https://github.com/joshmiquel/memoryprocess#readme", "dependencies": { - "eslint": "^8.5.0", - "eslint-config-airbnb-base": "^12.1.0", - "node-addon-api": "^3.2.1" - } + "node-addon-api": "3.2.1" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@commitlint/cli": "19.8.0", + "@commitlint/config-conventional": "19.8.0", + "@microsoft/api-extractor": "7.52.4", + "@total-typescript/tsconfig": "1.0.4", + "@types/bun": "1.2.10", + "@types/node": "22.14.1", + "lefthook": "1.11.10", + "typescript": "5" + }, + "trustedDependencies": [ + "@biomejs/biome", + "lefthook" + ] } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3a874db --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + /* Base Options: */ + "esModuleInterop": true, + "skipLibCheck": true, + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "verbatimModuleSyntax": true, + + /* Strictness */ + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + "module": "Preserve", + "target": "ES2022", + "lib": ["ES2022"], + + "moduleResolution": "bundler", + "declaration": true, + "emitDeclarationOnly": true, + "noEmit": false, + "outDir": "lib", + "rootDir": "." + }, + "include": ["index.ts", "src/**/*.ts", "scripts/**/*.ts"], + "exclude": ["node_modules"] +} \ No newline at end of file From 1059db854c5adc7eb853b95357d75ec7a3384452 Mon Sep 17 00:00:00 2001 From: JoShMiQueL Date: Fri, 18 Apr 2025 19:48:01 +0200 Subject: [PATCH 06/11] docs: update README and LICENSE --- LICENSE.md | 7 +- README.md | 764 ++++------------------------------------------------- 2 files changed, 56 insertions(+), 715 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 2f2e53a..884d213 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ -The MIT License (MIT) +MIT License -Copyright (c) 2019 Robert Valentyne +Copyright (c) 2025 JoShMiQueL Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,5 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 5c61f2f..c9ec0e5 100644 --- a/README.md +++ b/README.md @@ -1,741 +1,83 @@

- + Banner
- memoryjs is a an NPM package to read and write process memory! + MemoryProcess is an NPM package to read and write process memory +
+ This is a fork of the original memoryjs package, maintained by JoShMiQueL

- GitHub License - NPM Version - NPM Downloads + GitHub License + + GitHub Actions Workflow Status + + + NPM Downloads + + + NPM Version +

--- -

- Features • - Getting Started • - Usage • - Documentation • - Debug -

- - -# Features - -- List all open processes -- List all modules associated with a process -- Close process/file handles -- Find a specific module within a process -- Read and write process memory (w/big-endian support) -- Read and write buffers (arbitrary structs) -- Change memory protection -- Reserve/allocate, commit or change regions of memory -- Fetch a list of memory regions within a process -- Pattern scanning -- Execute a function within a process -- Hardware breakpoints (find out what accesses/writes to this address, etc) -- Inject & unload DLLs -- Read memory mapped files - -TODO: -- WriteFile support (for driver interactions) -- Async/await support - -# Getting Started - -## Install - -This is a Node add-on (last tested to be working on `v14.15.0`) and therefore requires [node-gyp](https://github.com/nodejs/node-gyp) to use. - -You may also need to [follow these steps](https://github.com/nodejs/node-gyp#user-content-installation) to install and setup `node-gyp`. - -```bash -npm install memoryjs -``` - -When using memoryjs, the target process should match the platform architecture of the Node version running. -For example if you want to target a 64 bit process, you should try and use a 64 bit version of Node. - -You also need to recompile the library and target the platform you want. Head to the memoryjs node module directory, open up a terminal and run one of the following compile scripts: - -```bash -# will automatically compile based on the detected Node architecture -npm run build - -# compile to target 32 bit processes -npm run build32 - -# compile to target 64 bit processes -npm run build64 -``` - -## Node Webkit / Electron - -If you are planning to use this module with Node Webkit or Electron, take a look at [Liam Mitchell](https://github.com/LiamKarlMitchell)'s build notes [here](https://github.com/Rob--/memoryjs/issues/23). - -# Usage - -## Initialise -``` javascript -const memoryjs = require('memoryjs'); -const processName = "csgo.exe"; -``` - -## Processes -- Open a process -- Get all processes -- Close a process (release handle) - -```javascript -// sync: open a process -const processObject = memoryjs.openProcess(processName); - -// async: open a process -memoryjs.openProcess(processName, (error, processObject) => {}); - - -// sync: get all processes -const processes = memoryjs.getProcesses(); - -// async: get all processes -memoryjs.getProcesses((error, processes) => {}); - - -// close a process (release handle) -memoryjs.closeHandle(handle); -``` - -See the [Documentation](#user-content-process-object) section of this README to see what a process object looks like. - -## Modules -- Find a module -- Get all modules - -``` javascript -// sync: find a module -const moduleObject = memoryjs.findModule(moduleName, processId); - -// async: find a module -memoryjs.findModule(moduleName, processId, (error, moduleObject) => {}); - - -// sync: get all modules -const modules = memoryjs.getModules(processId); - -// async: get all modules -memoryjs.getModules(processId, (error, modules) => {}); -``` - -See the [Documentation](#user-content-module-object) section of this README to see what a module object looks like. - -## Memory -- Read data type from memory -- Read buffer from memory -- Write data type to memory -- Write buffer to memory -- Fetch memory regions - -``` javascript -// sync: read data type from memory -const value = memoryjs.readMemory(handle, address, dataType); - -// async: read data type from memory -memoryjs.readMemory(handle, address, dataType, (error, value) => {}); - - -// sync: read buffer from memory -const buffer = memoryjs.readBuffer(handle, address, size); - -// async: read buffer from memory -memoryjs.readBuffer(handle, address, size, (error, buffer) => {}); - - -// sync: write data type to memory -memoryjs.writeMemory(handle, address, value, dataType); - - -// sync: write buffer to memory -memoryjs.writeBuffer(handle, address, buffer); - - -// sync: fetch memory regions -const regions = memoryjs.getRegions(handle); - -// async: fetch memory regions -memoryjs.getRegions(handle, (regions) => {}); -``` - -See the [Documentation](#user-content-documentation) section of this README to see what values `dataType` can be. - -## Memory Mapped Files -- Open a named file mapping object -- Map a view of a file into a specified process -- Close handle to the file mapping object - -```javascript -// sync: open a named file mapping object -const fileHandle = memoryjs.openFileMapping(fileName); - - -// sync: map entire file into a specified process -const baseAddress = memoryjs.mapViewOfFile(processHandle, fileName); - - -// sync: map portion of a file into a specified process -const baseAddress = memoryjs.mapViewOfFile(processHandle, fileName, offset, viewSize, pageProtection); - - -// sync: close handle to a file mapping object -const success = memoryjs.closeHandle(fileHandle); -``` - -See the [Documentation](#user-content-documentation) section of this README to see details on the parameters and return values for these functions. - -## Protection -- Change/set the protection on a region of memory - -```javascript -// sync: change/set the protection on a region of memory -const oldProtection = memoryjs.virtualProtectEx(handle, address, size, protection); -``` - -See the [Documentation](#user-content-protection-type) section of this README to see what values `protection` can be. - -## Pattern Scanning -- Pattern scan all modules and memory regions -- Pattern scan a given module -- Pattern scan a memory region or module at the given base address - -```javascript -// sync: pattern scan all modules and memory regions -const address = memoryjs.findPattern(handle, pattern, flags, patternOffset); - -// async: pattern scan all modules and memory regions -memoryjs.findPattern(handle, pattern, flags, patternOffset, (error, address) => {}); - - -// sync: pattern scan a given module -const address = memoryjs.findPattern(handle, moduleName, pattern, flags, patternOffset); - -// async: pattern scan a given module -memoryjs.findPattern(handle, moduleName, pattern, flags, patternOffset, (error, address) => {}); - - -// sync: pattern scan a memory region or module at the given base address -const address = memoryjs.findPattern(handle, baseAddress, pattern, flags, patternOffset); - -// async: pattern scan a memory region or module at the given base address -memoryjs.findPattern(handle, baseAddress, pattern, flags, patternOffset, (error, address) => {}); -``` - -## Function Execution -- Execute a function in a remote process - -``` javascript -// sync: execute a function in a remote process -const result = memoryjs.callFunction(handle, args, returnType, address); - -// async: execute a function in a remote process -memoryjs.callFunction(handle, args, returnType, address, (error, result) => {}); -``` - -Click [here](#user-content-result-object) to see what a result object looks like. - -Click [here](#user-content-function-execution-1) for details about how to format the arguments and the return type. - -## DLL Injection -- Inject a DLL -- Unload a DLL by module base address -- Unload a DLL by module name - -```javascript -// sync: inject a DLL -const success = memoryjs.injectDll(handle, dllPath); - -// async: inject a DLL -memoryjs.injectDll(handle, dllPath, (error, success) => {}); - - -// sync: unload a DLL by module base address -const success = memoryjs.unloadDll(handle, moduleBaseAddress); - -// async: unload a DLL by module base address -memoryjs.unloadDll(handle, moduleBaseAddress, (error, success) => {}); - - -// sync: unload a DLL by module name -const success = memoryjs.unloadDll(handle, moduleName); - -// async: unload a DLL by module name -memoryjs.unloadDll(handle, moduleName, (error, success) => {}); -``` - -## Hardware Breakpoints -- Attach debugger -- Detach debugger -- Wait for debug event -- Handle debug event -- Set hardware breakpoint -- Remove hardware breakpoint - -``` javascript -// sync: attach debugger -const success = memoryjs.attachDebugger(processId, exitOnDetach); - -// sync: detach debugger -const success = memoryjs.detachDebugger(processId); - -// sync: wait for debug event -const success = memoryjs.awaitDebugEvent(hardwareRegister, millisTimeout); - -// sync: handle debug event -const success = memoryjs.handleDebugEvent(processId, threadId); - -// sync: set hardware breakpoint -const success = memoryjs.setHardwareBreakpoint(processId, address, hardwareRegister, trigger, length); - -// sync: remove hardware breakpoint -const success = memoryjs.removeHardwareBreakpoint(processId, hardwareRegister); -``` - -# Documentation - -Note: this documentation is currently being updated, refer to the [Wiki](https://github.com/Rob--/memoryjs/wiki) for more information. - -## Process Object -``` javascript -{ dwSize: 304, - th32ProcessID: 10316, - cntThreads: 47, - th32ParentProcessID: 7804, - pcPriClassBase: 8, - szExeFile: "csgo.exe", - modBaseAddr: 1673789440, - handle: 808 } -``` +## 📖 API References (`src`) -The `handle` and `modBaseAddr` properties are only available when opening a process and not when listing processes. +### Main Functions -## Module Object -``` javascript -{ modBaseAddr: 468123648, - modBaseSize: 80302080, - szExePath: 'c:\\program files (x86)\\steam\\steamapps\\common\\counter-strike global offensive\\csgo\\bin\\client.dll', - szModule: 'client.dll', - th32ProcessID: 10316, - GlblcntUsage: 2 } - ``` +- **openProcess(processIdentifier: string | number, callback?): Process** + Opens a process by name or ID. Returns the process handle or undefined if not found. -## Result Object -``` javascript -{ returnValue: 1.23, - exitCode: 2 } -``` +- **closeHandle(handle: number): boolean** + Closes a process handle. -This object is returned when a function is executed in a remote process: -- `returnValue` is the value returned from the function that was called -- `exitCode` is the termination status of the thread +- **getProcesses(callback?): Process[]** + Returns the list of running processes. -## Data Types +- **findModule(moduleName: string, processId: number, callback?): Module | undefined** + Finds a module by name in a process. -When using the write or read functions, the data type (dataType) parameter should reference a constant from within the library: +- **getModules(processId: number, callback?): Module[]** + Returns the modules loaded by a process. -| Constant | Bytes | Aliases | Range | -|-------------------|-------|------------------------------------|-------| -| `memoryjs.BOOL` | 1 | `memoryjs.BOOLEAN` | 0 to 1 | -| `memoryjs.INT8` | 1 | `memoryjs.BYTE`, `memoryjs.CHAR` | -128 to 127 | -| `memoryjs.UINT8` | 1 | `memoryjs.UBYTE`, `memoryjs.UCHAR` | 0 to 255 | -| `memoryjs.INT16` | 2 | `memoryjs.SHORT` | -32,768 to 32,767 | -| `memoryjs.UINT16` | 2 | `memoryjs.USHORT`, `memoryjs.WORD` | 0 to 65,535 | -| `memoryjs.INT32` | 4 | `memoryjs.INT`, `memoryjs.LONG` | -2,147,483,648 to 2,147,483,647 | -| `memoryjs.UINT32` | 4 | `memoryjs.UINT`, `memoryjs.ULONG`, `memoryjs.DWORD` | 0 to 4,294,967,295 | -| `memoryjs.INT64` | 8 | n/a | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | -| `memoryjs.UINT64` | 8 | n/a | 0 to 18,446,744,073,709,551,615 | -| `memoryjs.FLOAT` | 4 | n/a | 3.4E +/- 38 (7 digits) | -| `memoryjs.DOUBLE` | 8 | n/a | 1.7E +/- 308 (15 digits) | -| `memoryjs.PTR` | 4/8 | `memoryjs.POINTER` | n/a | -| `memoryjs.UPTR` | 4/8 | `memoryjs.UPOINTER` | n/a | -| `memoryjs.STR` | n/a | `memoryjs.STRING` | n/a | -| `memoryjs.VEC3` | 12 | `memoryjs.VECTOR3` | n/a | -| `memoryjs.VEC4` | 16 | `memoryjs.VECTOR4` | n/a | +- **readMemory(handle: number, address: number, dataType: T, callback?): MemoryData | undefined** + Reads a value from a process's memory. +- **writeMemory(handle: number, address: number, value: any, dataType: DataType): boolean** + Writes a value to a process's memory. -Notes: -- all functions that accept an address also accept the address as a BigInt -- pointer will be 4 bytes in a 32 bit build, and 8 bytes in a 64 bit build. -- to read in big-endian mode, append `_BE` to the data type. For example: `memoryjs.DOUBLE_BE`. -- when writing 64 bit integers (`INT64`, `UINT64`, `INT64_BE`, `UINT64_BE`) you will need to supply a [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt). When reading a 64 bit integer, you will receive a BigInt. +- **findPattern(...)** + Scans memory patterns (multiple overloads). -These data types are to used to denote the type of data being read or written. +- **callFunction(handle: number, args: any[], returnType: number, address: number, callback?): any** + Calls a function in the process's memory. -64 bit integer example: -```javascript -const value = memoryjs.readMemory(handle, address, memoryjs.INT64); -console.log(typeof value); // bigint -memoryjs.writeMemory(handle, address, value + 1n, memoryjs.INT64); -``` +- **Debugger** + Utility class for process debugging. Main methods: `attach`, `detach`, `setHardwareBreakpoint`, `removeHardwareBreakpoint`, `monitor`. -Vector3 is a data structure of three floats: -```javascript -const vector3 = { x: 0.0, y: 0.0, z: 0.0 }; -memoryjs.writeMemory(handle, address, vector3, memoryjs.VEC3); -``` +- **virtualAllocEx, virtualProtectEx, getRegions, virtualQueryEx, injectDll, unloadDll, openFileMapping, mapViewOfFile** + Advanced memory and DLL manipulation functions. -Vector4 is a data structure of four floats: -```javascript -const vector4 = { w: 0.0, x: 0.0, y: 0.0, z: 0.0 }; -memoryjs.writeMemory(handle, address, vector4, memoryjs.VEC4); -``` +### Main Types and Constants (`types.ts`) -## Generic Structures +- **type DataType** + Supported data types for memory read/write: `'byte'`, `'int32'`, `'float'`, `'string'`, `'vector3'`, etc. -If you have a structure you want to write to memory, you can use buffers. For an example on how to do this, view the [buffers example](https://github.com/Rob--/memoryjs/blob/master/examples/buffers.js). +- **type MemoryData** + Maps a `DataType` to its corresponding JS type (`number`, `bigint`, `string`, `{x,y,z}`...) -To write/read a structure to/from memory, you can use [structron](https://github.com/LordVonAdel/structron) to define your structures and use them to write or parse buffers. +- **interface Process** + Information about an opened process (PID, handle, etc). -If you want to read a `std::string` using `structron`, the library exposes a custom type that can be used to read/write strings: -```javascript -// To create the type, we need to pass the process handle, base address of the -// structure, and the target process architecture (either "32" or "64"). -const stringType = memoryjs.STRUCTRON_TYPE_STRING(processObject.handle, structAddress, '64'); +- **interface Module** + Information about a module loaded in a process. -// Create a custom structure using the custom type, full example in /examples/buffers.js -const Struct = require('structron'); -const Player = new Struct() - .addMember(string, 'name'); -``` +- **type Protection, PageProtection, AllocationType, BreakpointTriggerType** + Auxiliary types for memory flags and protection. -Alternatively, you can use the [concentrate](https://github.com/deoxxa/concentrate) and [dissolve](https://github.com/deoxxa/dissolve) libraries to achieve the same thing. An old example of this is [here](https://github.com/Rob--/memoryjs/blob/aa6ed7d302fb1ac315aaa90558db43d128746912/examples/buffers.js). +- **const FunctionTypes, SignatureTypes, MemoryAccessFlags, MemoryPageFlags, HardwareDebugRegisters, BreakpointTriggerTypes** + Constants for flags and function types. -## Protection Type - -Protection type is a bit flag DWORD value. - -This parameter should reference a constant from the library: - -`memoryjs.PAGE_NOACCESS, memoryjs.PAGE_READONLY, memoryjs.PAGE_READWRITE, memoryjs.PAGE_WRITECOPY, memoryjs.PAGE_EXECUTE, memoryjs.PAGE_EXECUTE_READ, memoryjs.PAGE_EXECUTE_READWRITE, memoryjs.PAGE_EXECUTE_WRITECOPY, memoryjs.PAGE_GUARD, memoryjs.PAGE_NOCACHE, memoryjs.PAGE_WRITECOMBINE, memoryjs.PAGE_ENCLAVE_THREAD_CONTROL, memoryjs.PAGE_TARGETS_NO_UPDATE, memoryjs.PAGE_TARGETS_INVALID, memoryjs.PAGE_ENCLAVE_UNVALIDATED` - -Refer to MSDN's [Memory Protection Constants](https://docs.microsoft.com/en-gb/windows/desktop/Memory/memory-protection-constants) for more information. - -## Memory Allocation Type - -Memory allocation type is a bit flag DWORD value. - -This parameter should reference a constat from the library: - -`memoryjs.MEM_COMMIT, memoryjs.MEM_RESERVE, memoryjs.MEM_RESET, memoryjs.MEM_RESET_UNDO` - -Refer to MSDN's [VirtualAllocEx](https://docs.microsoft.com/en-us/windows/desktop/api/memoryapi/nf-memoryapi-virtualallocex) documentation for more information. - -## Strings - -You can use this library to read either a "string", or "char*" and to write a string. - -In both cases you want to get the address of the char array: - -```c++ -std::string str1 = "hello"; -std::cout << "Address: 0x" << hex << (DWORD) str1.c_str() << dec << std::endl; - -char* str2 = "hello"; -std::cout << "Address: 0x" << hex << (DWORD) str2 << dec << std::endl; -``` - -From here you can simply use this address to write and read memory. - -There is one caveat when reading a string in memory however, due to the fact that the library does not know -how long the string is, it will continue reading until it finds the first null-terminator. To prevent an -infinite loop, it will stop reading if it has not found a null-terminator after 1 million characters. - -One way to bypass this limitation in the future would be to allow a parameter to let users set the maximum -character count. - -### Signature Type - -When pattern scanning, flags need to be raised for the signature types. The signature type parameter needs to be one of the following: - -`0x0` or `memoryjs.NORMAL` which denotes a normal signature. - -`0x1` or `memoryjs.READ` which will read the memory at the address. - -`0x2` or `memoryjs.SUBSTRACT` which will subtract the image base from the address. - -To raise multiple flags, use the bitwise OR operator: `memoryjs.READ | memoryjs.SUBTRACT`. - -## Memory Mapped Files - -The library exposes functions to map obtain a handle to and read a memory mapped file. - -**openFileMapping(fileName)** -- *fileName*: name of the file mapping object to be opened -- returns: handle to the file mapping object - -Refer to [MSDN's OpenFileMappingA](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-openfilemappinga) documentation for more information. - -**mapViewOfFile(processHandle, fileName)** -- *processHandle*: the target process to map the file to -- *fileHandle*: handle of the file mapping object, obtained by `memoryjs.openFileMapping` -- Description: maps the entire file to target process' memory. Page protection defaults to `constants.PAGE_READONLY`. -- Returns: the base address of the mapped file - -**mapViewOfFile(processHandle, fileName, offset, viewSize, pageProtection)** -- *processHandle*: the target process to map the file to -- *fileHandle*: handle of the file mapping object, obtained by `memoryjs.openFileMapping` -- *offset* (`number` or `bigint`): the offset from the beginning of the file (has to be multiple of 64KB) -- *viewSize* (`number` or `bigint`): the number of bytes to map (if `0`, the entire file will be read, regardless of offset) -- *pageProtection*: desired page protection -- Description: maps a view of the file to the target process' memory -- Returns: the base address of the mapped file - -Refer to [MSDN's MapViewOfFile2](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-mapviewoffile2) documentation for more information. - -See [Protection Type](#user-content-protection-type) for page protection types. - -### Example -We have a process that creates a file mapping: -```c++ -HANDLE fileHandle = CreateFileA("C:\\foo.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); -HANDLE fileMappingHandle = CreateFileMappingA(fileHandle, NULL, PAGE_READONLY, 0, 0, "MappedFooFile"); -``` - -We can map the file to a specified target process and read the file with `memoryjs`: -```javascript -const processObject = memoryjs.openProcess("example.exe"); -const fileHandle = memoryjs.openFileMapping("MappedFooFile"); - -// read entire file -const baseAddress = memoryjs.mapViewOfFile(processObject.handle, fileHandle.handle); -const data = memoryjs.readMemory(processObject.handle, baseAddress, memoryjs.STR); - -// read 10 bytes after 64KB -const baseAddress = memoryjs.mapViewOfFile(processObject.handle, fileHandle.handle, 65536, 10, constants.PAGE_READONLY); -const buffer = memoryjs.readBuffer(processObject.handle, baseAddress, 10); -const data = buffer.toString(); - -const success = memoryjs.closeHandle(fileHandle); -``` - -If you want to read a memory mapped file without having a target process to map the file to, you can map it to the current Node process with global variable `process.pid`: -```javascript -const processObject = memoryjs.openProcess(process.pid); -``` - -## Function Execution - -Remote function execution works by building an array of arguments and dynamically generating shellcode that is injected into the target process and executed, for this reason crashes may occur. - -To call a function in a process, the `callFunction` function can be used. The library supports passing arguments to the function and need to be in the following format: - -```javascript -[{ type: T_INT, value: 4 }] -``` - -The library expects the arguments to be an array of objects where each object has a `type` which denotes the data type of the argument, and a `value` which is the actual value of the argument. The various supported data types can be found below. - - -``` javascript -memoryjs.T_VOID = 0x0, -memoryjs.T_STRING = 0x1, -memoryjs.T_CHAR = 0x2, -memoryjs.T_BOOL = 0x3, -memoryjs.T_INT = 0x4, -memoryjs.T_DOUBLE = 0x5, -memoryjs.T_FLOAT = 0x6, -``` - -When using `callFunction`, you also need to supply the return type of the function, which again needs to be one of the above values. - -For example, given the following C++ function: - -``` c++ -int add(int a, int b) { - return a + b; -} -``` - -You would call this function as so: - -```javascript -const args = [{ - type: memoryjs.T_INT, - value: 2, -}, { - type: memoryjs.T_INT, - value: 5, -}]; -const returnType = T_INT; - -> memoryjs.callFunction(handle, args, returnType, address); -{ returnValue: 7, exitCode: 7 } -``` - -See the [result object documentation](user-content-result-object) for details on what `callFunction` returns. - -Notes: currently passing a `double` as an argument is not supported, but returning one is. - -Much thanks to the [various contributors](https://github.com/Rob--/memoryjs/issues/6) that made this feature possible. - -## Hardware Breakpoints - -Hardware breakpoints work by attaching a debugger to the process, setting a breakpoint on a certain address and declaring a trigger type (e.g. breakpoint on writing to the address) and then continuously waiting for a debug event to arise (and then consequently handling it). - -This library exposes the main functions, but also includes a wrapper class to simplify the process. For a complete code example, checkout our [debugging example](https://github.com/Rob--/memoryjs/blob/master/examples/debugging.js). - -When setting a breakpoint, you are required to pass a trigger type: -- `memoryjs.TRIGGER_ACCESS` - breakpoint occurs when the address is accessed -- `memoryjs.TRIGGER_WRITE` - breakpoint occurs when the address is written to - -Do note that when monitoring an address containing a string, the `size` parameter of the `setHardwareBreakpoint` function should be the length of the string. When using the `Debugger` wrapper class, the wrapper will automatically determine the size of the string by attempting to read it. - -To summarise: -- When using the `Debugger` class: - - No need to pass the `size` parameter to `setHardwareBreakpoint` - - No need to manually pick a hardware register - - Debug events are picked up via an event listener - - `setHardwareBreakpoint` returns the register that was used for the breakpoint - -- When manually using the debugger functions: - - The `size` parameter is the size of the variable in memory (e.g. int32 = 4 bytes). For a string, this parameter is the length of the string - - Manually need to pick a hardware register (via `memoryjs.DR0` through `memoryhs.DR3`). Only 4 hardware registers are available (some CPUs may even has less than 4 available). This means only 4 breakpoints can be set at any given time - - Need to manually wait for debug and handle debug events - - `setHardwareBreakpoint` returns a boolean stating whether the operation as successful - -For more reading about debugging and hardware breakpoints, checkout the following links: -- [DebugActiveProcess](https://msdn.microsoft.com/en-us/library/windows/desktop/ms679295(v=vs.85).aspx) - attaching the debugger -- [DebugSetProcessKillOnExit](https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-debugsetprocesskillonexit) - kill the process when detaching -- [DebugActiveProcessStop](https://msdn.microsoft.com/en-us/library/windows/desktop/ms679296(v=vs.85).aspx) - detaching the debugger -- [WaitForDebugEvent](https://msdn.microsoft.com/en-us/library/windows/desktop/ms681423(v=vs.85).aspx) - waiting for the breakpoint to be triggered -- [ContinueDebugEvent](https://msdn.microsoft.com/en-us/library/windows/desktop/ms679285(v=vs.85).aspx) - handling the event - -### Using the Debugger Wrapper: - -The Debugger wrapper contains these functions you should use: - -``` javascript -class Debugger { - attach(processId, killOnDetach = false); - detach(processId); - setHardwareBreakpoint(processId, address, trigger, dataType); - removeHardwareBreakpoint(processId, register); -} -``` - -1. Attach the debugger -``` javascript -const hardwareDebugger = memoryjs.Debugger; -hardwareDebugger.attach(processId); -``` - -2. Set a hardware breakpoint -``` javascript -const address = 0xDEADBEEF; -const trigger = memoryjs.TRIGGER_ACCESS; -const dataType = memoryjs.INT; -const register = hardwareDebugger.setHardwareBreakpoint(processId, address, trigger, dataType); -``` - -3. Create an event listener for debug events (breakpoints) -``` javascript -// `debugEvent` event emission catches debug events from all registers -hardwareDebugger.on('debugEvent', ({ register, event }) => { - console.log(`Hardware Register ${register} breakpoint`); - console.log(event); -}); - -// You can listen to debug events from specific hardware registers -// by listening to whatever register was returned from `setHardwareBreakpoint` -hardwareDebugger.on(register, (event) => { - console.log(event); -}); -``` - -### When Manually Debugging: - -1. Attach the debugger -``` javascript -const hardwareDebugger = memoryjs.Debugger; -hardwareDebugger.attach(processId); -``` - -2. Set a hardware breakpoint (determine which register to use and the size of the data type) -``` javascript -// available registers: DR0 through DR3 -const register = memoryjs.DR0; -// int = 4 bytes -const size = 4; - -const address = 0xDEADBEEF; -const trigger = memoryjs.TRIGGER_ACCESS; -const dataType = memoryjs.INT; - -const success = memoryjs.setHardwareBreakpoint(processId, address, register, trigger, size); -``` - -3. Create the await/handle debug event loop -``` javascript -const timeout = 100; - -setInterval(() => { - // `debugEvent` can be null if no event occurred - const debugEvent = memoryjs.awaitDebugEvent(register, timeout); - - // If a breakpoint occurred, handle it - if (debugEvent) { - memoryjs.handleDebugEvent(debugEvent.processId, debugEvent.threadId); - } -}, timeout); -``` - -Note: a loop is not required, e.g. no loop required if you want to simply wait until the first detection of the address being accessed or written to. - -# Debug - -### 1. Re-compile the project to be debugged - -Go to the root directory of the module and run one of the following commands: -```bash -# will automatically compile based on the detected Node architecture -npm run debug - -# compile to target 32 bit processes -npm run debug32 - -# compile to target 64 bit processes -npm run debug64 -``` - -### 2. Change the `index.js` file to require the debug module - -Go to the root directory and change the line in `index.js` from: -```javascript -const memoryjs = require('./build/Release/memoryjs'); -``` - -To the following: -```javascript -const memoryjs = require('./build/Debug/memoryjs'); -``` - -### 3. Open the project in Visual Studio - -Open the `binding.sln` solution in Visual Studio, found in the `build` folder in the project's root directory. - -### 4. Setup Visual Studio debug configuration - - 1. In the toolbar, click "Project" then "Properties" - 2. Under "Configuration Properties", click "Debugging" - 3. Set the "Command" property to the location of your `node.exe` file (e.g. `C:\nodejs\node.exe`) - 4. Set the "Command Arguments" property to the location of your script file (e.g. `C:\project\test.js`) - -### 5. Set breakpoints - -Explore the project files in Visual Studio (by expanding `..` and then `lib` in the Solution Explorer). Header files can be viewed by holding `Alt` and clicking on the header file names at the top of the source code files. - -Breakpoints are set by clicking to the left of the line number. - -### 6. Run the debugger - -Start debugging by either pressing `F5`, by clicking "Debug" in the toolbar and then "Start Debugging", or by clicking "Local Windows Debugger". +--- -The script you've set as the command argument in step 4 will be run, and Visual Studio will pause on the breakpoints set and allow you to step through the code line by line and inspect variables. +> See the [`src`](./src) folder for full details and comments on each function/type. \ No newline at end of file From 12dd5b2023f9347998dc60a04c42fea4d01e9d3c Mon Sep 17 00:00:00 2001 From: JoShMiQueL Date: Fri, 18 Apr 2025 19:49:11 +0200 Subject: [PATCH 07/11] ci: add GitHub workflows --- .github/workflows/pull_request.yml | 16 ++++++ .github/workflows/release.yml | 90 ++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 .github/workflows/pull_request.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..ead28e2 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,16 @@ +name: reviewdog +on: [pull_request] +jobs: + biome: + name: Biome Linter + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: mongolyy/reviewdog-action-biome@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + reporter: github-pr-review + fail_level: any \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7a13a19 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +name: Release + +on: + workflow_dispatch: + inputs: + branch: + description: 'The branch to release from (e.g., main, beta, next). Semantic-release must be configured for this branch.' + required: true + default: 'main' + push: + branches: + - main + - beta + +jobs: + build: + strategy: + fail-fast: false + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile --ignore-scripts + + - name: Install node-gyp + run: bun add -g node-gyp + + - name: Build + run: bun run build + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: memoryprocess + path: lib/ + + release: + needs: [build] + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: write + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch || github.ref }} + persist-credentials: false + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: memoryprocess + path: lib/ + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Configure npm + run: | + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc + npm whoami + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Install semantic-release plugins + run: npm install @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/npm @semantic-release/github @semantic-release/git + + - name: Run semantic-release + run: npx semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file From 547799363252b676dd666b6b453ac43d2465c30f Mon Sep 17 00:00:00 2001 From: JoShMiQueL Date: Fri, 18 Apr 2025 19:49:13 +0200 Subject: [PATCH 08/11] chore(assets): add banner image --- assets/banner.png | Bin 0 -> 156897 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/banner.png diff --git a/assets/banner.png b/assets/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..0eb59bc84979bd556237026a17697d5dca1a6494 GIT binary patch literal 156897 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 +iR|lGZ7HAlI0)0j) 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 Date: Fri, 18 Apr 2025 19:49:14 +0200 Subject: [PATCH 09/11] feat: add TypeScript examples --- examples/read_game_memory.ts | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 examples/read_game_memory.ts diff --git a/examples/read_game_memory.ts b/examples/read_game_memory.ts new file mode 100644 index 0000000..1ba5eed --- /dev/null +++ b/examples/read_game_memory.ts @@ -0,0 +1,62 @@ +// @ts-ignore +import { openProcess, closeProcess, readMemory } from 'memoryprocess'; + +const PROCESS_NAME = "DarkSoulsRemastered.exe"; + +try { + // 1. Open the game process + console.log(`Attempting to open process: ${PROCESS_NAME}`); + const processObject = openProcess(PROCESS_NAME); + + if (!processObject || !processObject.handle) { + console.error(`Failed to open process "${PROCESS_NAME}". Is the game running?`); + process.exit(1); // Use Bun's exit + } + + console.log(`Process "${PROCESS_NAME}" opened successfully.`); + console.log(`Process ID: ${processObject.th32ProcessID}`); + console.log(`Base Address: 0x${processObject.modBaseAddr.toString(16)}`); // Log base address in hex + + // 2. Define offsets (these are specific to Dark Souls Remastered and might need updates) + const playerDataBase = 0x1D278F0; + const playerDataStruct = 0x10; + const soulsOffset = 0x94; + + // 3. Read the pointer chain to find the souls address + console.log(`Reading player data base pointer at BaseAddress + 0x${playerDataBase.toString(16)}`); + const pointer1Address = processObject.modBaseAddr + playerDataBase; + const pointer1Value = readMemory(processObject.handle, pointer1Address, "int"); // Assuming 32-bit pointers/game + if (pointer1Value === 0) { + console.error(`Failed to read base pointer at 0x${pointer1Address.toString(16)}. Value was 0.`); + closeProcess(processObject.handle); + process.exit(1); + } + console.log(`Read Pointer 1 Value (Player Data Base): 0x${pointer1Value.toString(16)}`); + + + console.log(`Reading player data struct pointer at Pointer1Value + 0x${playerDataStruct.toString(16)}`); + const pointer2Address = pointer1Value + playerDataStruct; + const pointer2Value = readMemory(processObject.handle, pointer2Address, "int"); + if (pointer2Value === 0) { + console.error(`Failed to read pointer 2 at 0x${pointer2Address.toString(16)}. Value was 0.`); + closeProcess(processObject.handle); + process.exit(1); + } + console.log(`Read Pointer 2 Value (Player Data Struct): 0x${pointer2Value.toString(16)}`); + + const soulsAddress = pointer2Value + soulsOffset; + console.log(`Calculated Souls Address: 0x${soulsAddress.toString(16)}`); + + // 4. Read the souls value from the final address + const currentSouls = readMemory(processObject.handle, soulsAddress, "int"); + + console.log(`\nCurrent Player Souls: ${currentSouls}`); + + // 5. Close the process handle + console.log("Closing process handle."); + closeProcess(processObject.handle); + console.log("Process handle closed."); + +} catch (error) { + console.error("An error occurred:", error); +} From 826f4118aa85edf88e72c0a0db81b813efd895cc Mon Sep 17 00:00:00 2001 From: JoShMiQueL Date: Fri, 18 Apr 2025 20:30:56 +0200 Subject: [PATCH 10/11] chore: remove source and config files Deleted the following files as part of repository cleanup: - .gitattributes - build.ts - bun.lock - src/index.ts - src/memory.ts - src/memoryprocess.d.ts - src/process.ts --- .gitattributes | 1 - build.ts | 121 --------------- bun.lock | 331 ----------------------------------------- src/index.ts | 19 --- src/memory.ts | 116 --------------- src/memoryprocess.d.ts | 4 - src/process.ts | 54 ------- 7 files changed, 646 deletions(-) delete mode 100644 .gitattributes delete mode 100644 build.ts delete mode 100644 bun.lock delete mode 100644 src/index.ts delete mode 100644 src/memory.ts delete mode 100644 src/memoryprocess.d.ts delete mode 100644 src/process.ts diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index df3ddcb..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.json linguist-language=JSON-with-Comments \ No newline at end of file diff --git a/build.ts b/build.ts deleted file mode 100644 index b458925..0000000 --- a/build.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { $ } from "bun"; -import { Extractor, ExtractorConfig, ExtractorResult } from '@microsoft/api-extractor'; -import * as fs from 'fs/promises'; -import * as path from 'path'; - -export const TEMP_DLL_PATH = "_temp_dllmemproc"; -export const BUILD_PATH = "lib"; - -const apiExtractorJsonPath: string = path.join(import.meta.dir, 'config', 'api-extractor.json'); - -interface BuildOptions { - outdir?: string; -} - -function formatTime(ms: number): string { - return `${(ms / 1000).toFixed(2)}s`; -} - -function log(message: string, type: 'info' | 'success' | 'error' = 'info') { - const colors = { - info: '\x1b[36m', // cyan - success: '\x1b[32m', // green - error: '\x1b[31m', // red - reset: '\x1b[0m' - }; - console.log(`${colors[type]}${message}${colors.reset}`); -} - -async function build(options: BuildOptions = {}) { - const startTime = performance.now(); - let buildSuccess = false; - - const { outdir = BUILD_PATH } = options; - const tempDllFilePath = path.join(TEMP_DLL_PATH, 'memoryprocess.dll'); - const finalDllPath = path.join(outdir, 'memoryprocess.dll'); - - log('🚀 Building MemoryProcess...', 'info'); - - try { - await fs.mkdir(outdir, { recursive: true }); - await fs.mkdir(TEMP_DLL_PATH, { recursive: true }); - log('✓ Directories ensured', 'success'); - - - await $`zig build-lib native/*.cc -dynamic -target x86_64-windows-gnu -lc -lc++ -femit-bin=${tempDllFilePath}`; - log('✓ Native module built', 'success'); - - await fs.copyFile(tempDllFilePath, finalDllPath); - log(`✓ Copied DLL to ${finalDllPath}`, 'success'); - - await Bun.build({ - entrypoints: [ - "src/index.ts" - ], - outdir, - target: 'bun', - splitting: false, - minify: true, - external: ["*.dll"] - }); - log('✓ TypeScript files built', 'success'); - - // Create the node-error.js file for Node.js environments - const nodeErrorContent = `throw new Error('The "memoryprocess" package requires the Bun runtime (https://bun.sh) and is not compatible with Node.js.');`; - const nodeErrorPath = path.join(outdir, 'node-error.js'); - await fs.writeFile(nodeErrorPath, nodeErrorContent); - log('✓ Node.js error file created', 'success'); - - await $`bunx tsc`; - log("✓ TypeScript declaration files generated", "success") - - const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(apiExtractorJsonPath); - - const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, { - localBuild: true, - }); - - if (extractorResult.succeeded) { - log("✓ API Extractor completed", "success"); - buildSuccess = true; - } else { - log( - `API Extractor completed with ${extractorResult.errorCount} errors` + - ` and ${extractorResult.warningCount} warnings`, "error" - ); - buildSuccess = false; - } - - const totalTime = performance.now() - startTime; - if (buildSuccess) { - log(`✨ Build completed successfully in ${formatTime(totalTime)}`, 'success'); - } else { - log(` Build failed after ${formatTime(totalTime)}`, 'error'); - } - - - } catch (error) { - buildSuccess = false; - if (error instanceof Error) { - log(`Build failed: ${error.message}`, 'error'); - } else { - log(`Build failed: ${String(error)}`, 'error'); - } - console.error(error); - - } finally { - try { - await fs.rm(TEMP_DLL_PATH, { recursive: true, force: true }); - log('✓ Temporary directory cleaned up', 'info'); - } catch (cleanupError) { - log(`Failed to clean up temporary directory: ${cleanupError}`, 'error'); - } - process.exit(buildSuccess ? 0 : 1); - } -} - -if (import.meta.path === Bun.main) { - await build(); -} - -export { build, BuildOptions }; diff --git a/bun.lock b/bun.lock deleted file mode 100644 index 9df497d..0000000 --- a/bun.lock +++ /dev/null @@ -1,331 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "memoryprocess", - "dependencies": { - "node-addon-api": "3.2.1", - }, - "devDependencies": { - "@biomejs/biome": "1.9.4", - "@commitlint/cli": "19.8.0", - "@commitlint/config-conventional": "19.8.0", - "@microsoft/api-extractor": "7.52.4", - "@total-typescript/tsconfig": "1.0.4", - "@types/bun": "1.2.10", - "@types/node": "22.14.1", - "lefthook": "1.11.10", - "typescript": "5", - }, - }, - }, - "trustedDependencies": [ - "@biomejs/biome", - "lefthook", - ], - "packages": { - "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, ""], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, ""], - - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, ""], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, ""], - - "@commitlint/cli": ["@commitlint/cli@19.8.0", "", { "dependencies": { "@commitlint/format": "^19.8.0", "@commitlint/lint": "^19.8.0", "@commitlint/load": "^19.8.0", "@commitlint/read": "^19.8.0", "@commitlint/types": "^19.8.0", "tinyexec": "^0.3.0", "yargs": "^17.0.0" }, "bin": { "commitlint": "cli.js" } }, ""], - - "@commitlint/config-conventional": ["@commitlint/config-conventional@19.8.0", "", { "dependencies": { "@commitlint/types": "^19.8.0", "conventional-changelog-conventionalcommits": "^7.0.2" } }, ""], - - "@commitlint/config-validator": ["@commitlint/config-validator@19.8.0", "", { "dependencies": { "@commitlint/types": "^19.8.0", "ajv": "^8.11.0" } }, ""], - - "@commitlint/ensure": ["@commitlint/ensure@19.8.0", "", { "dependencies": { "@commitlint/types": "^19.8.0", "lodash.camelcase": "^4.3.0", "lodash.kebabcase": "^4.1.1", "lodash.snakecase": "^4.1.1", "lodash.startcase": "^4.4.0", "lodash.upperfirst": "^4.3.1" } }, ""], - - "@commitlint/execute-rule": ["@commitlint/execute-rule@19.8.0", "", {}, ""], - - "@commitlint/format": ["@commitlint/format@19.8.0", "", { "dependencies": { "@commitlint/types": "^19.8.0", "chalk": "^5.3.0" } }, ""], - - "@commitlint/is-ignored": ["@commitlint/is-ignored@19.8.0", "", { "dependencies": { "@commitlint/types": "^19.8.0", "semver": "^7.6.0" } }, ""], - - "@commitlint/lint": ["@commitlint/lint@19.8.0", "", { "dependencies": { "@commitlint/is-ignored": "^19.8.0", "@commitlint/parse": "^19.8.0", "@commitlint/rules": "^19.8.0", "@commitlint/types": "^19.8.0" } }, ""], - - "@commitlint/load": ["@commitlint/load@19.8.0", "", { "dependencies": { "@commitlint/config-validator": "^19.8.0", "@commitlint/execute-rule": "^19.8.0", "@commitlint/resolve-extends": "^19.8.0", "@commitlint/types": "^19.8.0", "chalk": "^5.3.0", "cosmiconfig": "^9.0.0", "cosmiconfig-typescript-loader": "^6.1.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "lodash.uniq": "^4.5.0" } }, ""], - - "@commitlint/message": ["@commitlint/message@19.8.0", "", {}, ""], - - "@commitlint/parse": ["@commitlint/parse@19.8.0", "", { "dependencies": { "@commitlint/types": "^19.8.0", "conventional-changelog-angular": "^7.0.0", "conventional-commits-parser": "^5.0.0" } }, ""], - - "@commitlint/read": ["@commitlint/read@19.8.0", "", { "dependencies": { "@commitlint/top-level": "^19.8.0", "@commitlint/types": "^19.8.0", "git-raw-commits": "^4.0.0", "minimist": "^1.2.8", "tinyexec": "^0.3.0" } }, ""], - - "@commitlint/resolve-extends": ["@commitlint/resolve-extends@19.8.0", "", { "dependencies": { "@commitlint/config-validator": "^19.8.0", "@commitlint/types": "^19.8.0", "global-directory": "^4.0.1", "import-meta-resolve": "^4.0.0", "lodash.mergewith": "^4.6.2", "resolve-from": "^5.0.0" } }, ""], - - "@commitlint/rules": ["@commitlint/rules@19.8.0", "", { "dependencies": { "@commitlint/ensure": "^19.8.0", "@commitlint/message": "^19.8.0", "@commitlint/to-lines": "^19.8.0", "@commitlint/types": "^19.8.0" } }, ""], - - "@commitlint/to-lines": ["@commitlint/to-lines@19.8.0", "", {}, ""], - - "@commitlint/top-level": ["@commitlint/top-level@19.8.0", "", { "dependencies": { "find-up": "^7.0.0" } }, ""], - - "@commitlint/types": ["@commitlint/types@19.8.0", "", { "dependencies": { "@types/conventional-commits-parser": "^5.0.0", "chalk": "^5.3.0" } }, ""], - - "@microsoft/api-extractor": ["@microsoft/api-extractor@7.52.4", "", { "dependencies": { "@microsoft/api-extractor-model": "7.30.5", "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", "@rushstack/node-core-library": "5.13.0", "@rushstack/rig-package": "0.5.3", "@rushstack/terminal": "0.15.2", "@rushstack/ts-command-line": "4.23.7", "lodash": "~4.17.15", "minimatch": "~3.0.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", "typescript": "5.8.2" }, "bin": { "api-extractor": "bin/api-extractor" } }, ""], - - "@microsoft/api-extractor-model": ["@microsoft/api-extractor-model@7.30.5", "", { "dependencies": { "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", "@rushstack/node-core-library": "5.13.0" } }, ""], - - "@microsoft/tsdoc": ["@microsoft/tsdoc@0.15.1", "", {}, ""], - - "@microsoft/tsdoc-config": ["@microsoft/tsdoc-config@0.17.1", "", { "dependencies": { "@microsoft/tsdoc": "0.15.1", "ajv": "~8.12.0", "jju": "~1.4.0", "resolve": "~1.22.2" } }, ""], - - "@rushstack/node-core-library": ["@rushstack/node-core-library@5.13.0", "", { "dependencies": { "ajv": "~8.13.0", "ajv-draft-04": "~1.0.0", "ajv-formats": "~3.0.1", "fs-extra": "~11.3.0", "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", "semver": "~7.5.4" }, "peerDependencies": { "@types/node": "*" } }, ""], - - "@rushstack/rig-package": ["@rushstack/rig-package@0.5.3", "", { "dependencies": { "resolve": "~1.22.1", "strip-json-comments": "~3.1.1" } }, ""], - - "@rushstack/terminal": ["@rushstack/terminal@0.15.2", "", { "dependencies": { "@rushstack/node-core-library": "5.13.0", "supports-color": "~8.1.1" }, "peerDependencies": { "@types/node": "*" } }, ""], - - "@rushstack/ts-command-line": ["@rushstack/ts-command-line@4.23.7", "", { "dependencies": { "@rushstack/terminal": "0.15.2", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" } }, ""], - - "@total-typescript/tsconfig": ["@total-typescript/tsconfig@1.0.4", "", {}, ""], - - "@types/argparse": ["@types/argparse@1.0.38", "", {}, ""], - - "@types/bun": ["@types/bun@1.2.10", "", { "dependencies": { "bun-types": "1.2.10" } }, ""], - - "@types/conventional-commits-parser": ["@types/conventional-commits-parser@5.0.1", "", { "dependencies": { "@types/node": "*" } }, ""], - - "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, ""], - - "JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": "bin.js" }, ""], - - "ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, ""], - - "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" } }, ""], - - "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, ""], - - "ansi-regex": ["ansi-regex@5.0.1", "", {}, ""], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""], - - "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, ""], - - "array-ify": ["array-ify@1.0.0", "", {}, ""], - - "balanced-match": ["balanced-match@1.0.2", "", {}, ""], - - "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, ""], - - "bun-types": ["bun-types@1.2.10", "", { "dependencies": { "@types/node": "*" } }, ""], - - "callsites": ["callsites@3.1.0", "", {}, ""], - - "chalk": ["chalk@5.4.1", "", {}, ""], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, ""], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, ""], - - "color-name": ["color-name@1.1.4", "", {}, ""], - - "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, ""], - - "concat-map": ["concat-map@0.0.1", "", {}, ""], - - "conventional-changelog-angular": ["conventional-changelog-angular@7.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, ""], - - "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@7.0.2", "", { "dependencies": { "compare-func": "^2.0.0" } }, ""], - - "conventional-commits-parser": ["conventional-commits-parser@5.0.0", "", { "dependencies": { "JSONStream": "^1.3.5", "is-text-path": "^2.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": "cli.mjs" }, ""], - - "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" } }, ""], - - "cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.1.0", "", { "dependencies": { "jiti": "^2.4.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, ""], - - "dargs": ["dargs@8.1.0", "", {}, ""], - - "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, ""], - - "emoji-regex": ["emoji-regex@8.0.0", "", {}, ""], - - "env-paths": ["env-paths@2.2.1", "", {}, ""], - - "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, ""], - - "escalade": ["escalade@3.2.0", "", {}, ""], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, ""], - - "find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, ""], - - "fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, ""], - - "function-bind": ["function-bind@1.1.2", "", {}, ""], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, ""], - - "git-raw-commits": ["git-raw-commits@4.0.0", "", { "dependencies": { "dargs": "^8.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": "cli.mjs" }, ""], - - "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, ""], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, ""], - - "has-flag": ["has-flag@4.0.0", "", {}, ""], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, ""], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, ""], - - "import-lazy": ["import-lazy@4.0.0", "", {}, ""], - - "import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, ""], - - "ini": ["ini@4.1.1", "", {}, ""], - - "is-arrayish": ["is-arrayish@0.2.1", "", {}, ""], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, ""], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, ""], - - "is-obj": ["is-obj@2.0.0", "", {}, ""], - - "is-text-path": ["is-text-path@2.0.0", "", { "dependencies": { "text-extensions": "^2.0.0" } }, ""], - - "jiti": ["jiti@2.4.2", "", { "bin": "lib/jiti-cli.mjs" }, ""], - - "jju": ["jju@1.4.0", "", {}, ""], - - "js-tokens": ["js-tokens@4.0.0", "", {}, ""], - - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, ""], - - "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, ""], - - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, ""], - - "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, ""], - - "jsonparse": ["jsonparse@1.3.1", "", {}, ""], - - "lefthook": ["lefthook@1.11.10", "", { "optionalDependencies": { "lefthook-windows-x64": "1.11.10" }, "bin": "bin/index.js" }, ""], - - "lefthook-windows-x64": ["lefthook-windows-x64@1.11.10", "", { "os": "win32", "cpu": "x64" }, ""], - - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, ""], - - "locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, ""], - - "lodash": ["lodash@4.17.21", "", {}, ""], - - "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, ""], - - "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, ""], - - "lodash.kebabcase": ["lodash.kebabcase@4.1.1", "", {}, ""], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, ""], - - "lodash.mergewith": ["lodash.mergewith@4.6.2", "", {}, ""], - - "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, ""], - - "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, ""], - - "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, ""], - - "lodash.upperfirst": ["lodash.upperfirst@4.3.1", "", {}, ""], - - "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, ""], - - "meow": ["meow@12.1.1", "", {}, ""], - - "minimatch": ["minimatch@3.0.8", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, ""], - - "minimist": ["minimist@1.2.8", "", {}, ""], - - "node-addon-api": ["node-addon-api@3.2.1", "", {}, ""], - - "p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, ""], - - "p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, ""], - - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, ""], - - "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, ""], - - "path-exists": ["path-exists@5.0.0", "", {}, ""], - - "path-parse": ["path-parse@1.0.7", "", {}, ""], - - "picocolors": ["picocolors@1.1.1", "", {}, ""], - - "punycode": ["punycode@2.3.1", "", {}, ""], - - "require-directory": ["require-directory@2.1.1", "", {}, ""], - - "require-from-string": ["require-from-string@2.0.2", "", {}, ""], - - "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, ""], - - "resolve-from": ["resolve-from@5.0.0", "", {}, ""], - - "semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": "bin/semver.js" }, ""], - - "source-map": ["source-map@0.6.1", "", {}, ""], - - "split2": ["split2@4.2.0", "", {}, ""], - - "sprintf-js": ["sprintf-js@1.0.3", "", {}, ""], - - "string-argv": ["string-argv@0.3.2", "", {}, ""], - - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, ""], - - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, ""], - - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, ""], - - "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, ""], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, ""], - - "text-extensions": ["text-extensions@2.4.0", "", {}, ""], - - "through": ["through@2.3.8", "", {}, ""], - - "tinyexec": ["tinyexec@0.3.2", "", {}, ""], - - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, ""], - - "undici-types": ["undici-types@6.21.0", "", {}, ""], - - "unicorn-magic": ["unicorn-magic@0.1.0", "", {}, ""], - - "universalify": ["universalify@2.0.1", "", {}, ""], - - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, ""], - - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, ""], - - "y18n": ["y18n@5.0.8", "", {}, ""], - - "yallist": ["yallist@4.0.0", "", {}, ""], - - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, ""], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, ""], - - "yocto-queue": ["yocto-queue@1.2.1", "", {}, ""], - - "@commitlint/config-validator/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, ""], - - "@commitlint/is-ignored/semver": ["semver@7.7.1", "", { "bin": "bin/semver.js" }, ""], - - "@microsoft/api-extractor/typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, ""], - - "@rushstack/node-core-library/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, ""], - - "ajv-formats/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, ""], - - "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, ""], - - "js-yaml/argparse": ["argparse@2.0.1", "", {}, ""], - } -} diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 136a2d2..0000000 --- a/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { readMemory, writeMemory } from './memory'; -import { openProcess, closeProcess, type ProcessObject } from './process'; - -export { - readMemory, - writeMemory, - openProcess, - closeProcess, - type ProcessObject -}; - -const memoryprocess = { - readMemory, - writeMemory, - openProcess, - closeProcess -}; - -export default memoryprocess; diff --git a/src/memory.ts b/src/memory.ts deleted file mode 100644 index 4e79958..0000000 --- a/src/memory.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { dlopen } from "bun:ffi"; - -// Dynamically resolve the path to the DLL relative to the built file -const libPath = import.meta.resolve("./memoryprocess.dll"); - -const { symbols } = dlopen(libPath, { - readMemory: { - args: ["uint64_t", "uint64_t", "cstring"], - returns: "int", - }, - writeMemory: { - args: ["uint64_t", "uint64_t", "cstring", "ptr"], - returns: "void", - }, -}) - -/** Represents the valid data types for memory operations. */ -export type MemoryDataType = - | "byte" | "int8" | "char" // 8-bit signed - | "ubyte" | "uint8" | "uchar" // 8-bit unsigned - | "short" | "int16" // 16-bit signed - | "ushort" | "uint16" | "word" // 16-bit unsigned - | "int" | "int32" | "long" // 32-bit signed - | "uint" | "uint32" | "ulong" | "dword" // 32-bit unsigned - | "float" // 32-bit float - | "double" // 64-bit float - | "longlong" | "int64" // 64-bit signed (Note: JS Number limitations apply) - | "ulonglong" | "uint64" // 64-bit unsigned (Note: JS Number limitations apply) - | "bool" - | "string" | "str"; // Null-terminated string - -/** Represents the possible value types corresponding to MemoryDataType. */ -export type MemoryValueType = number | string | boolean; - -/** - * Reads memory from a process. - * @param handle - The handle to the process. - * @param address - The address to read from. - * @param dataType - The data type to read. - * @returns The value read from memory. - */ -export function readMemory(handle: number, address: number, dataType: MemoryDataType): number { - const dataTypeBuffer = Buffer.from(dataType + "\0") - const result = symbols.readMemory(handle, address, dataTypeBuffer) - return result -} - -/** - * Writes memory to a process. - * @param handle - The handle to the process. - * @param address - The address to write to. - * @param dataType - The data type to write. - * @param value - The value to write. Should be `number` for numeric types, `boolean` for bool, and `string` for string. - */ -export function writeMemory( - handle: number, - address: number, - dataType: MemoryDataType, - value: MemoryValueType, -): void { - const dataTypeBuffer = Buffer.from(dataType + "\0") - - let valueBuffer: Buffer; - - // Create buffer with correct binary representation based on dataType - // This needs to handle all types supported by the C++ function - if (dataType === "int" || dataType === "int32" || dataType === "long") { - valueBuffer = Buffer.alloc(4); - valueBuffer.writeInt32LE(value as number, 0); - } else if (dataType === "uint" || dataType === "uint32" || dataType === "ulong" || dataType === "dword") { - valueBuffer = Buffer.alloc(4); - valueBuffer.writeUInt32LE(value as number, 0); - } else if (dataType === "short" || dataType === "int16") { - valueBuffer = Buffer.alloc(2); - valueBuffer.writeInt16LE(value as number, 0); - } else if (dataType === "ushort" || dataType === "uint16" || dataType === "word") { - valueBuffer = Buffer.alloc(2); - valueBuffer.writeUInt16LE(value as number, 0); - } else if (dataType === "byte" || dataType === "int8" || dataType === "char") { - valueBuffer = Buffer.alloc(1); - valueBuffer.writeInt8(value as number, 0); - } else if (dataType === "ubyte" || dataType === "uint8" || dataType === "uchar") { - valueBuffer = Buffer.alloc(1); - valueBuffer.writeUInt8(value as number, 0); - } else if (dataType === "float") { - valueBuffer = Buffer.alloc(4); - valueBuffer.writeFloatLE(value as number, 0); - } else if (dataType === "double") { - valueBuffer = Buffer.alloc(8); - valueBuffer.writeDoubleLE(value as number, 0); - } else if (dataType === "longlong" || dataType === "int64") { - valueBuffer = Buffer.alloc(8); - valueBuffer.writeBigInt64LE(BigInt(value), 0); - } else if (dataType === "ulonglong" || dataType === "uint64") { - valueBuffer = Buffer.alloc(8); - valueBuffer.writeBigUInt64LE(BigInt(value), 0); - } else if (dataType === "bool") { - valueBuffer = Buffer.alloc(1); - valueBuffer.writeUInt8(value ? 1 : 0, 0); - } else if (dataType === "string" || dataType === "str") { - // Ensure null termination for C++ side - valueBuffer = Buffer.from((value as string) + "\0", "utf8"); - } else if (dataType === "ptr" || dataType === "pointer") { - valueBuffer = Buffer.alloc(8); // Assuming 64-bit pointers - valueBuffer.writeBigUInt64LE(BigInt(value), 0); - } else if (dataType === "uptr" || dataType === "upointer") { - valueBuffer = Buffer.alloc(8); // Assuming 64-bit pointers - valueBuffer.writeBigUInt64LE(BigInt(value), 0); - } - // TODO: Add Vector3/Vector4 handling if needed - else { - throw new Error(`Unsupported data type for writeMemory: ${dataType}`); - } - - symbols.writeMemory(handle, address, dataTypeBuffer, valueBuffer) -} \ No newline at end of file diff --git a/src/memoryprocess.d.ts b/src/memoryprocess.d.ts deleted file mode 100644 index 8a8e71b..0000000 --- a/src/memoryprocess.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "*.dll" { - const value: string; - export default value; -} \ No newline at end of file diff --git a/src/process.ts b/src/process.ts deleted file mode 100644 index 331f88d..0000000 --- a/src/process.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { dlopen } from "bun:ffi"; - -// Dynamically resolve the path to the DLL relative to the built file -const libPath = import.meta.resolve("./memoryprocess.dll"); - -export interface ProcessObject { - dwSize: number; - th32ProcessID: number; - cntThreads: number; - th32ParentProcessID: number; - pcPriClassBase: number; - szExeFile: string; - handle: number; - modBaseAddr: number; -} - -const { symbols } = dlopen(libPath, { - openProcess: { - args: ["cstring"], - returns: "cstring", - }, - closeProcess: { - args: ["int"], - returns: "void", - }, -}) - -/** - * Opens a process by name. - * @param processName - The name of the process to open. - * @returns The opened process object. - */ -export function openProcess(processName: string): ProcessObject { - const processNameBuffer = Buffer.from(processName + "\0") - const processCString = symbols.openProcess(processNameBuffer) - const process = JSON.parse(processCString.toString()) as ProcessObject - return process -} - -/** - * Closes a process handle. - * @param handle - The handle to the process to close. - */ -export function closeProcess(handle: number): void { - try { - if (symbols.closeProcess) { - symbols.closeProcess(handle); - } else { - console.error("closeProcess FFI symbol not found."); - } - } catch (e) { - console.error(`Error closing handle ${handle}:`, e); - } -} \ No newline at end of file From f9414642ceeffd20df725eb5d53fe60e1244a161 Mon Sep 17 00:00:00 2001 From: JoShMiQueL Date: Fri, 18 Apr 2025 20:33:03 +0200 Subject: [PATCH 11/11] fix(ci): add --no-save to bun install in release workflow Update the GitHub Actions release workflow to use 'bun install --frozen-lockfile --ignore-scripts --no-save' for dependency installation. This ensures no changes are written to lockfiles during CI. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a13a19..999c5b6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: bun-version: latest - name: Install dependencies - run: bun install --frozen-lockfile --ignore-scripts + run: bun install --frozen-lockfile --ignore-scripts --no-save - name: Install node-gyp run: bun add -g node-gyp