Alterando o Comportamento de Programas via Patches em Arquivo e Memória

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

  1. 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.
  2. 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 é 0x89b0 e o da mensagem é 0x89b8.
  3. 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.

  1. Criação do Processo Suspenso: O programa patcher inicia o programa alvo com a flag CREATE_SUSPENDED, impedindo que sua thread principal execute imediatamente.
  2. 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 MessageBox ou os dados da string devem aparecer desprotegidos) é lida.
  3. 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.
  4. 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 VirtualProtectEx para permitir escrita. Em seguida, WriteProcessMemory é usada para inserir as novas strings.
  5. 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 a ResumeThread para 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;
}

Tags: WindowsAPI C++ EngenhariaReversa Patching ModificacaoDeMemoria

Publicado em 7-26 09:28