Este artigo explora o desenvolvimento de patches focados na modificação do comportamento de programas executáveis, tanto em disco (patches de arquivo) quanto em tempo de execução (patches de memória). O objetivo é demonstrar como alterar o conteúdo de uma caixa de mensagem exibida por um programa simples.
Programa Alvo
Considere o seguinte programa C++ que exibe uma caixa de diálogo padrão do Windows:
#include <windows.h>
int main(int argc, char* argv[])
{
MessageBox(NULL, "Parabéns, você entrou no jogo com sucesso!", "Sucesso", MB_OK);
return 0;
}
Nosso objetivo será modificar a mensagem e o título exibidos por esta função.
Patch de Arquivo
Princípio
Um patch de arquivo envolve a modificação direta do binário executável em disco. O desafio principal é localizar as posições exatas (offsets de arquivo) dos dados a serem alterados dentro do PE (Portable Executable) do programa.
Metodologia
- Análise Estática: Usando ferramentas como o IDA Pro, é possível inspecionar o binário e identificar as strings "Parabéns, você entrou no jogo com sucesso!" e "Sucesso". O IDA mostrará os endereços de memória virtual (RVAs - Relative Virtual Addresses) onde essas strings são carregadas.
- Conversão para Offset de Arquivo: Os RVAs obtidos do IDA são endereços em tempo de execução. Para aplicá-los ao arquivo em disco, é necessário convertê-los em offsets de arquivo. Isso geralmente envolve subtrair o RVA base da seção de dados (por exemplo,
.rdata, onde strings estáticas são armazenadas) e somar o offset da seção dentro do arquivo PE. Ferramentas como o CFF Explorer ou PE-bear podem ajudar a encontrar o offset de arquivo da seção.rdata. Para este exemplo, assumiremos que o offset da string de título é0x89b0e o da mensagem é0x89b8. - Aplicação do Patch: Abrimos o arquivo executável em modo de escrita, posicionamos o ponteiro de arquivo nos offsets calculados e escrevemos os novos dados. É crucial considerar o tamanho das strings (caracteres multi-byte, como em UTF-8 ou UTF-16, podem ocupar mais espaço) e preencher o restante com bytes nulos (
\x00) se a nova string for menor que a original para evitar corrupção de dados adjacentes.
Exemplo de Código
#include <windows.h>
#include <cstdio>
int main(int argc, char* argv[])
{
HANDLE arquivoHandle = CreateFile("ProgramaAlvo.exe", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (arquivoHandle == INVALID_HANDLE_VALUE) {
printf("Erro ao abrir o arquivo. Código: %d\n", GetLastError());
return 1;
}
DWORD bytesEscritos;
char novoTitulo[] = "Falha";
DWORD deslocamentoTitulo = 0x89b0; // Exemplo de offset calculado
SetFilePointer(arquivoHandle, deslocamentoTitulo, NULL, FILE_BEGIN);
if (!WriteFile(arquivoHandle, novoTitulo, sizeof(novoTitulo) - 1, &bytesEscritos, NULL)) {
printf("Erro ao escrever o título. Código: %d\n", GetLastError());
CloseHandle(arquivoHandle);
return 1;
}
char novaMensagem[] = "Falha ao entrar no jogo!\x00\x00\x00\x00\x00\x00"; // Preencher com nulos se menor que a original
DWORD deslocamentoMensagem = 0x89b8; // Exemplo de offset calculado
SetFilePointer(arquivoHandle, deslocamentoMensagem, NULL, FILE_BEGIN);
if (!WriteFile(arquivoHandle, novaMensagem, sizeof(novaMensagem) - 1, &bytesEscritos, NULL)) {
printf("Erro ao escrever a mensagem. Código: %d\n", GetLastError());
CloseHandle(arquivoHandle);
return 1;
}
CloseHandle(arquivoHandle);
printf("Patch de arquivo aplicado com sucesso.\n");
return 0;
}
Patch de Memória
Princípio
Patches de memória são aplicados enquanto o programa alvo está em execução. Isso é particularmente útil em cenários onde o arquivo executável é ofuscado, criptografado ou compactado em disco, e os dados relevantes só são decodificados ou descompactados na memória durante a execução. O patch de memória permite modificar o estado do programa após essas operações, sem tocar no binário original.
Metodologia
A abordagem aqui envolve iniciar o processo alvo em um estado suspenso, monitorar ou aguardar que ele atinja um determinado ponto (onde os dados a serem modificados estejam disponíveis em memória) e, em seguida, aplicar as alterações diretamente na memória do processo.
- Criação do Processo Suspenso: O programa patcher inicia o programa alvo com a flag
CREATE_SUSPENDED, impedindo que sua thread principal execute imediatamente. - Monitoramento da Memória: Em um loop, a thread principal do programa alvo é suspensa e a memória em um endereço chave (por exemplo, onde a instrução
CALL MessageBoxou os dados da string devem aparecer desprotegidos) é lida. - Detecção do Estado Desejado: Compara-se o conteúdo lido com uma assinatura de bytes esperada, indicando que o programa atingiu o ponto de execução onde a decodificação ou descompactação ocorreu.
- Modificação da Memória: Uma vez que o estado desejado é detectado, a proteção de memória na região das strings é alterada usando
VirtualProtectExpara permitir escrita. Em seguida,WriteProcessMemoryé usada para inserir as novas strings. - Retomada da Execução: Finalmente, a thread principal do programa alvo é retomada com
ResumeThread. É notável que, neste cenário específico de teste, foram necessárias duas chamadas aResumeThreadpara que o processo alvo retomasse a execução normalmente, um copmortamento que pode indicar particularidades na sincronização do processo ou no ambiente de execução.
Exemplo de Código
#include <windows.h>
#include <cstdio>
#include <psapi.h> // Necessário para GetModuleBaseName ou outras funções de processo, embora não usadas diretamente aqui
// Endereços de memória virtual para o exemplo. Em um cenário real, seriam descobertos dinamicamente ou por análise.
#define ENDERECO_CALL_MBOX 0x00007FF7FA171796 // Exemplo de endereço da instrução CALL MessageBox
#define TAMANHO_ASSINATURA 6
#define NOME_EXECUTAVEL_ALVO ".\\ProgramaAlvo.exe"
// Endereços das strings em memória após carregamento do módulo.
#define ENDERECO_TITULO 0x00007FF7FA179BB0
#define ENDERECO_MENSAGEM 0x00007FF7FA179BB8
// Assinatura da instrução CALL antes do patch
BYTE codigoOriginal[] = { 0xFF,0x15, 0xB4, 0xE9, 0x00, 0x00 };
char novoTituloMemoria[] = "Falha";
char novaMensagemMemoria[] = "Falha ao entrar no jogo!\x00\x00\x00\x00\x00\x00";
int main(int argc, char* argv[])
{
STARTUPINFOA infoInicializacao;
PROCESS_INFORMATION infoProcesso;
BYTE bufferLeitura[MAX_PATH] = { 0 };
BOOL continuarExecucao = TRUE;
DWORD protecaoAntiga;
ZeroMemory(&infoInicializacao, sizeof(STARTUPINFO));
ZeroMemory(&infoProcesso, sizeof(PROCESS_INFORMATION));
if (!CreateProcess(NOME_EXECUTAVEL_ALVO,
NULL,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED, // Inicia o processo suspenso
NULL,
NULL,
&infoInicializacao,
&infoProcesso)
)
{
MessageBox(NULL, "Falha ao criar o processo alvo.", "ERRO", MB_OK);
return FALSE;
}
while (continuarExecucao) {
SuspendThread(infoProcesso.hThread); // Suspende a thread principal do processo alvo
ReadProcessMemory(infoProcesso.hProcess, (LPVOID)ENDERECO_CALL_MBOX, &bufferLeitura, TAMANHO_ASSINATURA, NULL);
// Verifica se o código de destino foi decodificado/restaurado
if (!memcmp(codigoOriginal, bufferLeitura, TAMANHO_ASSINATURA)) {
MessageBox(NULL, "Código alvo detectado na memória, aplicando patch...", "Informação", MB_OK);
// Altera a proteção de memória para permitir escrita nas regiões das strings
VirtualProtectEx(infoProcesso.hProcess, (LPVOID)ENDERECO_TITULO, sizeof(novoTituloMemoria) - 1, PAGE_EXECUTE_READWRITE, &protecaoAntiga);
VirtualProtectEx(infoProcesso.hProcess, (LPVOID)ENDERECO_MENSAGEM, sizeof(novaMensagemMemoria) - 1, PAGE_EXECUTE_READWRITE, &protecaoAntiga);
// Escreve as novas strings na memória do processo alvo
WriteProcessMemory(infoProcesso.hProcess, (LPVOID)ENDERECO_TITULO, novoTituloMemoria, sizeof(novoTituloMemoria) - 1, NULL);
WriteProcessMemory(infoProcesso.hProcess, (LPVOID)ENDERECO_MENSAGEM, novaMensagemMemoria, sizeof(novaMensagemMemoria) - 1, NULL);
ResumeThread(infoProcesso.hThread); // Retoma a execução (primeira vez)
ResumeThread(infoProcesso.hThread); // Retoma a execução (segunda vez, conforme observado no teste)
continuarExecucao = FALSE; // Sai do loop
} else {
ResumeThread(infoProcesso.hThread); // Se não detectado, continua a execução e tenta novamente
Sleep(10); // Pequena pausa para evitar consumo excessivo de CPU
}
}
CloseHandle(infoProcesso.hProcess);
CloseHandle(infoProcesso.hThread);
return 0;
}