memoryjs
é um pacote NPM para ler e escrever memória de processo!
Recursos • Primeiros passos • Uso • Documentação • Depuração
PENDÊNCIA:
Este é um complemento do Node (testado pela última vez para funcionar em v14.15.0
) e, portanto, requer node-gyp para ser usado.
Você também pode precisar seguir estas etapas para instalar e configurar node-gyp
.
npm install memoryjs
Ao usar memoryjs, o processo de destino deve corresponder à arquitetura da plataforma da versão do Node em execução. Por exemplo, se você deseja atingir um processo de 64 bits, tente usar uma versão de 64 bits do Node.
Você também precisa recompilar a biblioteca e direcionar a plataforma desejada. Vá para o diretório do módulo do nó memoryjs, abra um terminal e execute um dos seguintes scripts de compilação:
# 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
Se você está planejando usar este módulo com Node Webkit ou Electron, dê uma olhada nas notas de construção de Liam Mitchell aqui.
const memoryjs = require('memoryjs');
const processName = "csgo.exe";
// 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);
Consulte a seção Documentação deste README para ver a aparência de um objeto de processo.
// 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) => {});
Consulte a seção Documentação deste README para ver a aparência de um objeto de módulo.
// 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) => {});
Consulte a seção Documentação deste README para ver quais valores dataType
podem ser.
// 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);
Consulte a seção Documentação deste README para ver detalhes sobre os parâmetros e valores de retorno para essas funções.
// sync: change/set the protection on a region of memory
const oldProtection = memoryjs.virtualProtectEx(handle, address, size, protection);
Consulte a seção Documentação deste README para ver o que pode ser protection
de valores.
// 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) => {});
// 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) => {});
Clique aqui para ver a aparência de um objeto de resultado.
Clique aqui para obter detalhes sobre como formatar os argumentos e o tipo de retorno.
// 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) => {});
// 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);
Nota: esta documentação está sendo atualizada, consulte o Wiki para obter mais informações.
{ dwSize: 304,
th32ProcessID: 10316,
cntThreads: 47,
th32ParentProcessID: 7804,
pcPriClassBase: 8,
szExeFile: "csgo.exe",
modBaseAddr: 1673789440,
handle: 808 }
As propriedades handle
e modBaseAddr
só estão disponíveis ao abrir um processo e não ao listar processos.
{ 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 }
{ returnValue: 1.23,
exitCode: 2 }
Este objeto é retornado quando uma função é executada em um processo remoto:
returnValue
é o valor retornado da função que foi chamadaexitCode
é o status de encerramento do thread Ao usar as funções de gravação ou leitura, o parâmetro de tipo de dados (dataType) deve fazer referência a uma constante de dentro da biblioteca:
Constante | Bytes | Aliases | Faixa |
---|---|---|---|
memoryjs.BOOL | 1 | memoryjs.BOOLEAN | 0 a 1 |
memoryjs.INT8 | 1 | memoryjs.BYTE , memoryjs.CHAR | -128 a 127 |
memoryjs.UINT8 | 1 | memoryjs.UBYTE , memoryjs.UCHAR | 0 a 255 |
memoryjs.INT16 | 2 | memoryjs.SHORT | -32.768 a 32.767 |
memoryjs.UINT16 | 2 | memoryjs.USHORT , memoryjs.WORD | 0 a 65.535 |
memoryjs.INT32 | 4 | memoryjs.INT , memoryjs.LONG | -2.147.483.648 a 2.147.483.647 |
memoryjs.UINT32 | 4 | memoryjs.UINT , memoryjs.ULONG , memoryjs.DWORD | 0 a 4.294.967.295 |
memoryjs.INT64 | 8 | n / D | -9.223.372.036.854.775.808 a 9.223.372.036.854.775.807 |
memoryjs.UINT64 | 8 | n / D | 0 a 18.446.744.073.709.551.615 |
memoryjs.FLOAT | 4 | n / D | 3,4E +/- 38 (7 dígitos) |
memoryjs.DOUBLE | 8 | n / D | 1,7E +/- 308 (15 dígitos) |
memoryjs.PTR | 4/8 | memoryjs.POINTER | n / D |
memoryjs.UPTR | 4/8 | memoryjs.UPOINTER | n / D |
memoryjs.STR | n / D | memoryjs.STRING | n / D |
memoryjs.VEC3 | 12 | memoryjs.VECTOR3 | n / D |
memoryjs.VEC4 | 16 | memoryjs.VECTOR4 | n / D |
Notas:
_BE
ao tipo de dados. Por exemplo: memoryjs.DOUBLE_BE
.INT64
, UINT64
, INT64_BE
, UINT64_BE
) você precisará fornecer um BigInt. Ao ler um número inteiro de 64 bits, você receberá um BigInt.Esses tipos de dados são usados para denotar o tipo de dados que estão sendo lidos ou gravados.
Exemplo de número inteiro de 64 bits:
const value = memoryjs.readMemory(handle, address, memoryjs.INT64);
console.log(typeof value); // bigint
memoryjs.writeMemory(handle, address, value + 1n, memoryjs.INT64);
Vector3 é uma estrutura de dados de três carros alegóricos:
const vector3 = { x: 0.0, y: 0.0, z: 0.0 };
memoryjs.writeMemory(handle, address, vector3, memoryjs.VEC3);
Vector4 é uma estrutura de dados de quatro carros alegóricos:
const vector4 = { w: 0.0, x: 0.0, y: 0.0, z: 0.0 };
memoryjs.writeMemory(handle, address, vector4, memoryjs.VEC4);
Se você tiver uma estrutura que deseja gravar na memória, poderá usar buffers. Para obter um exemplo de como fazer isso, veja o exemplo de buffers.
Para escrever/ler uma estrutura de/para a memória, você pode usar structron para definir suas estruturas e usá-las para escrever ou analisar buffers.
Se você quiser ler um std::string
usando structron
, a biblioteca expõe um tipo personalizado que pode ser usado para ler/escrever strings:
// 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');
// 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');
Alternativamente, você pode usar as bibliotecas de concentração e dissolução para conseguir o mesmo. Um exemplo antigo disso está aqui.
O tipo de proteção é um valor DWORD de sinalizador de bit.
Este parâmetro deve fazer referência a uma constante da biblioteca:
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
Consulte Constantes de proteção de memória do MSDN para obter mais informações.
O tipo de alocação de memória é um valor DWORD de sinalizador de bit.
Este parâmetro deve fazer referência a uma constante da biblioteca:
memoryjs.MEM_COMMIT, memoryjs.MEM_RESERVE, memoryjs.MEM_RESET, memoryjs.MEM_RESET_UNDO
Consulte a documentação do VirtualAllocEx do MSDN para obter mais informações.
Você pode usar esta biblioteca para ler uma "string" ou "char*" e escrever uma string.
Em ambos os casos você deseja obter o endereço do array char:
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;
A partir daqui você pode simplesmente usar este endereço para escrever e ler a memória.
Há uma ressalva ao ler uma string na memória, porém, devido ao fato de a biblioteca não saber o tamanho da string, ela continuará lendo até encontrar o primeiro terminador nulo. Para evitar um loop infinito, ele interromperá a leitura se não encontrar um terminador nulo após 1 milhão de caracteres.
Uma maneira de contornar essa limitação no futuro seria permitir um parâmetro que permitisse aos usuários definir a contagem máxima de caracteres.
Ao digitalizar padrões, os sinalizadores precisam ser levantados para os tipos de assinatura. O parâmetro de tipo de assinatura precisa ser um dos seguintes:
0x0
ou memoryjs.NORMAL
que denota uma assinatura normal.
0x1
ou memoryjs.READ
que irá ler a memória no endereço.
0x2
ou memoryjs.SUBSTRACT
que subtrairá a base da imagem do endereço.
Para gerar vários sinalizadores, use o operador OR bit a bit: memoryjs.READ | memoryjs.SUBTRACT
.
A biblioteca expõe funções para mapear, obter um identificador e ler um arquivo mapeado na memória.
openFileMapping(nomeDoArquivo)
Consulte a documentação OpenFileMappingA do MSDN para obter mais informações.
mapViewOfFile(processHandle, nomeArquivo)
memoryjs.openFileMapping
constants.PAGE_READONLY
.mapViewOfFile(processHandle, fileName, offset, viewSize, pageProtection)
memoryjs.openFileMapping
number
ou bigint
): o deslocamento desde o início do arquivo (deve ser múltiplo de 64KB)number
ou bigint
): o número de bytes a serem mapeados (se 0
, o arquivo inteiro será lido, independentemente do deslocamento)Consulte a documentação MapViewOfFile2 do MSDN para obter mais informações.
Consulte Tipo de proteção para tipos de proteção de página.
Temos um processo que cria um mapeamento de arquivo:
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");
Podemos mapear o arquivo para um processo de destino especificado e ler o arquivo com memoryjs
:
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);
Se quiser ler um arquivo mapeado na memória sem ter um processo de destino para mapear o arquivo, você pode mapeá-lo para o processo atual do Node com a variável global process.pid
:
const processObject = memoryjs.openProcess(process.pid);
A execução remota de funções funciona construindo um array de argumentos e gerando dinamicamente shellcode que é injetado no processo alvo e executado, por esse motivo podem ocorrer travamentos.
Para chamar uma função em um processo, a função callFunction
pode ser usada. A biblioteca suporta a passagem de argumentos para a função e precisa estar no seguinte formato:
[{ type: T_INT, value: 4 }]
A biblioteca espera que os argumentos sejam uma matriz de objetos onde cada objeto possui um type
que denota o tipo de dados do argumento e um value
que é o valor real do argumento. Os vários tipos de dados suportados podem ser encontrados abaixo.
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,
Ao usar callFunction
, você também precisa fornecer o tipo de retorno da função, que novamente precisa ser um dos valores acima.
Por exemplo, dada a seguinte função C++:
int add(int a, int b) {
return a + b;
}
Você chamaria essa função assim:
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 }
Consulte a documentação do objeto de resultado para obter detalhes sobre o que callFunction
retorna.
Notas: atualmente não há suporte para passar um double
como argumento, mas retornar um é.
Muito obrigado aos vários colaboradores que tornaram esse recurso possível.
Os pontos de interrupção de hardware funcionam anexando um depurador ao processo, definindo um ponto de interrupção em um determinado endereço e declarando um tipo de gatilho (por exemplo, ponto de interrupção na gravação no endereço) e, em seguida, aguardando continuamente o surgimento de um evento de depuração (e, consequentemente, manipulando-o).
Esta biblioteca expõe as funções principais, mas também inclui uma classe wrapper para simplificar o processo. Para um exemplo de código completo, confira nosso exemplo de depuração.
Ao definir um ponto de interrupção, você deve passar um tipo de gatilho:
memoryjs.TRIGGER_ACCESS
- o ponto de interrupção ocorre quando o endereço é acessadomemoryjs.TRIGGER_WRITE
- o ponto de interrupção ocorre quando o endereço é gravado Observe que ao monitorar um endereço contendo uma string, o parâmetro size
da função setHardwareBreakpoint
deve ser o comprimento da string. Ao usar a classe wrapper Debugger
, o wrapper determinará automaticamente o tamanho da string tentando lê-la.
Para resumir:
Ao usar a classe Debugger
:
size
para setHardwareBreakpoint
setHardwareBreakpoint
retorna o registro que foi usado para o ponto de interrupçãoAo usar manualmente as funções do depurador:
size
é o tamanho da variável na memória (por exemplo, int32 = 4 bytes). Para uma string, este parâmetro é o comprimento da stringmemoryjs.DR0
até memoryhs.DR3
). Apenas 4 registros de hardware estão disponíveis (algumas CPUs podem até ter menos de 4 disponíveis). Isso significa que apenas 4 pontos de interrupção podem ser definidos a qualquer momentosetHardwareBreakpoint
retorna um booleano informando se a operação foi bem-sucedidaPara obter mais informações sobre depuração e pontos de interrupção de hardware, confira os seguintes links:
O wrapper do Debugger contém estas funções que você deve usar:
class Debugger {
attach(processId, killOnDetach = false);
detach(processId);
setHardwareBreakpoint(processId, address, trigger, dataType);
removeHardwareBreakpoint(processId, register);
}
const hardwareDebugger = memoryjs.Debugger;
hardwareDebugger.attach(processId);
const address = 0xDEADBEEF;
const trigger = memoryjs.TRIGGER_ACCESS;
const dataType = memoryjs.INT;
const register = 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(register, (event) => {
console.log(event);
});
const hardwareDebugger = memoryjs.Debugger;
hardwareDebugger.attach(processId);
// 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);
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);
Nota: um loop não é necessário, por exemplo, nenhum loop é necessário se você quiser simplesmente esperar até a primeira detecção do endereço que está sendo acessado ou gravado.
Vá para o diretório raiz do módulo e execute um dos seguintes comandos:
# 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
index.js
para exigir o módulo de depuração Vá para o diretório raiz e altere a linha em index.js
de:
const memoryjs = require('./build/Release/memoryjs');
Para o seguinte:
const memoryjs = require('./build/Debug/memoryjs');
Abra a solução binding.sln
no Visual Studio, encontrada na pasta build
no diretório raiz do projeto.
node.exe
(por exemplo, C:nodejsnode.exe
)C:projecttest.js
) Explore os arquivos do projeto no Visual Studio (expandindo ..
e depois lib
no Solution Explorer). Os arquivos de cabeçalho podem ser visualizados segurando Alt
e clicando nos nomes dos arquivos de cabeçalho na parte superior dos arquivos de código-fonte.
Os pontos de interrupção são definidos clicando à esquerda do número da linha.
Inicie a depuração pressionando F5
, clicando em "Depurar" na barra de ferramentas e depois em "Iniciar depuração" ou clicando em "Depurador local do Windows".
O script que você definiu como argumento de comando na etapa 4 será executado e o Visual Studio fará uma pausa nos pontos de interrupção definidos e permitirá que você percorra o código linha por linha e inspecione variáveis.