Manipulação de Memória em Processos Externos com C#

A manipulação de memória de processos externos é uma técnica comum em engenharia reversa e no desenvolvimento de ferramentas de automação ou modificação de software (como trainers de jogos). No ecossistema .NET, essa tarefa é realizada através de chamadas de plataforma (P/Invoke) para interagir com a API nativa do Windows (kernel32.dll).

1. Importação de APIs do Windows

Para ler e escrever na memória de outro processo, precisamos das funções OpenProcess, ReadProcessMemory e WriteProcessMemory. Abaixo, apresentamos uma implementação encaspulada em uma classe utilitária.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace GestorMemoria
{
    public static class OperacoesMemoria
    {
        // Permissões de acesso ao processo
        private const int PROCESS_ALL_ACCESS = 0x1F0FFF;

        [DllImport("kernel32.dll")]
        public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);

        [DllImport("kernel32.dll")]
        public static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, [Out] byte[] lpBuffer, int dwSize, out IntPtr lpNumberOfBytesRead);

        [DllImport("kernel32.dll")]
        public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, out IntPtr lpNumberOfBytesWritten);

        [DllImport("kernel32.dll")]
        public static extern bool CloseHandle(IntPtr hObject);

        public static int ObterIdPeloNome(string nomeProcesso)
        {
            Process[] processos = Process.GetProcessesByName(nomeProcesso);
            return processos.Length > 0 ? processos[0].Id : 0;
        }

        public static int LerInteiro(IntPtr handle, int endereco)
        {
            byte[] buffer = new byte[4];
            ReadProcessMemory(handle, (IntPtr)endereco, buffer, buffer.Length, out _);
            return BitConverter.ToInt32(buffer, 0);
        }

        public static void EscreverInteiro(IntPtr handle, int endereco, int valor)
        {
            byte[] dados = BitConverter.GetBytes(valor);
            WriteProcessMemory(handle, (IntPtr)endereco, dados, dados.Length, out _);
        }
    }
}

2. Implementação da Lógica de Negócio

Abaixo, demonstramos como aplicar essa lógica para modificar valores em um jogo, utilizando cálculos de endereços base e offsets (deslocamentos). Neste exemplo, tratamos da modificação de recursos como "Sol" e "Dinheiro" em um processo específico.

using System;
using System.Windows.Forms;

namespace GestorMemoria
{
    public partial class FormPrincipal : Form
    {
        private string nomeProcesso = "PlantsVsZombies";
        private int enderecoBaseGlobal = 0x006A9EC0;

        public FormPrincipal()
        {
            InitializeComponent();
        }

        private void AtualizarRecursos()
        {
            int pid = OperacoesMemoria.ObterIdPeloNome(nomeProcesso);
            if (pid == 0) return;

            IntPtr handle = OperacoesMemoria.OpenProcess(0x1F0FFF, false, pid);
            if (handle == IntPtr.Zero) return;

            try
            {
                // Lógica para Sol (Ponteiro Multinível)
                int nivel1 = OperacoesMemoria.LerInteiro(handle, enderecoBaseGlobal);
                int enderecoSol = OperacoesMemoria.LerInteiro(handle, nivel1 + 0x768) + 0x5560;
                OperacoesMemoria.EscreverInteiro(handle, enderecoSol, 9999);

                // Lógica para Dinheiro
                int nivelDinheiro = OperacoesMemoria.LerInteiro(handle, enderecoBaseGlobal);
                int enderecoDinheiro = OperacoesMemoria.LerInteiro(handle, nivelDinheiro + 0x82C) + 0x28;
                OperacoesMemoria.EscreverInteiro(handle, enderecoDinheiro, 150000);
            }
            finally
            {
                OperacoesMemoria.CloseHandle(handle);
            }
        }

        private void btnAtivar_Click(object sender, EventArgs e)
        {
            if (OperacoesMemoria.ObterIdPeloNome(nomeProcesso) != 0)
            {
                timerModificacao.Start();
                btnAtivar.Text = "Modificando...";
            }
            else
            {
                MessageBox.Show("O processo alvo não foi encontrado.");
            }
        }

        private void timerModificacao_Tick(object sender, EventArgs e)
        {
            AtualizarRecursos();
        }

        private void btnAlterarFase_Click(object sender, EventArgs e)
        {
            int pid = OperacoesMemoria.ObterIdPeloNome(nomeProcesso);
            if (pid == 0) return;

            IntPtr handle = OperacoesMemoria.OpenProcess(0x1F0FFF, false, pid);
            
            // Cálculo de endereço para nível do jogo
            int basePointer = OperacoesMemoria.LerInteiro(handle, enderecoBaseGlobal);
            int enderecoNivel = OperacoesMemoria.LerInteiro(handle, basePointer + 0x82C) + 0x24;

            if (int.TryParse(txtNovaFase.Text, out int novaFase))
            {
                OperacoesMemoria.EscreverInteiro(handle, enderecoNivel, novaFase);
            }

            OperacoesMemoria.CloseHandle(handle);
        }
    }
}

3. Considerações sobre Endereçamento

Os endereços de memória em jogos modernos raramente são estáticos devido a mecanismos como o ASLR (Address Space Layout Randomization). Por isso, o uso de Endereços Base somados a Offsets é fundamental. O processo geralmente envolve:

  • Localizar o endereço base do módulo principal.
  • Seguir a cadeia de ponteiros até o valor final desejado.
  • Garantir que a aplicação possua privilégios administrativos para abrir o handle do processo alvo com permissões de escrita.

Tags: C# .NET Win32-API P-Invoke Memory-Manipulation

Publicado em 6-9 17:06 por Thomas