Testando IPC entre GTA e Elixir
PUBLISHED
FILE_ID: TESTANDO IPC ENTRE GTA E ELIXIR

Testando IPC entre GTA e Elixir

Willian Frantz
May 31, 2025
PerformanceOptimizationWeb VitalsMonitoring

Olá mundo!

Essa semana comecei a aprimorar a maneira como estava testando as funcionalidades e descobertas do meu processo de engenharia reversa com o GTA SA.

Infelizmente o processo ainda estava bem manual, onde eu precisava estar constantemente repetindo os seguintes passos:

  1. Engenharia Reversa (IDA Pro) até descobrir coisas que poderiam ser úteis.
  2. Reescrita de código seja de função ou pra conseguir acessar estar possíveis informações in-game pela minha DLL
  3. Injeção da DLL e então conseguir rodar os testes...

A partir dessa dificuldade em conseguir iterar mais rápido, me surgiu a seguinte ideia: criar um terceiro elemento que não vai precisar ser injetado (DLL), e pode executar atividades mais complexas e dinâmicas.

Assim, meio que pivotando a ideia da minha biblioteca, servir apenas como uma casca de leituras/escritas de memória privilegiada dentro do processo, tendo em vista que a memória virtual do mesmo vai estar 100% disponível.

A primeira versão desse meu teste foi estabelecer uma comunicação entre processos utilizando o conceito de NamedPipes (IPC).

Como isso funciona na minha biblioteca:

1void HandleTeleport(std::istringstream& args) { 2 Vector3 pos; 3 if (args >> pos.x >> pos.y >> pos.z) { 4 DWORD* playerPtr = GetPlayer(); 5 CPed player((uintptr_t)playerPtr); 6 7 printf("you id: %p\n", playerPtr); 8 9 player.Teleport(pos, 0); 10 printf("[TP] Teleporting to: %f, %f, %f\n", pos.x, pos.y, pos.z); 11 } 12 else { 13 printf("[TP] Invalid vector input.\n"); 14 } 15} 16 17void HandleCreateVeh(std::istringstream& args) { 18 int modelId; 19 20 if (args >> modelId) { 21 int response = SpawnVehicle(modelId); 22 printf("[Create Vehicle] model: %d\n", modelId); 23 } 24 else { 25 printf("[Create Vehicle] Invalid input\n"); 26 } 27} 28 29void DispatchCommand(const std::string& input) { 30 std::istringstream iss(input); 31 std::string cmd; 32 iss >> cmd; 33 34 typedef std::function<void(std::istringstream&)> CommandHandler; 35 36 static const std::unordered_map<std::string, CommandHandler> commandMap = { 37 { "tp", HandleTeleport }, 38 { "car", HandleCreateVeh } 39 }; 40 41 for (std::unordered_map<std::string, CommandHandler>::const_iterator it = commandMap.begin(); it != commandMap.end(); ++it) { 42 if (EqualsIgnoreCase(cmd, it->first)) { 43 it->second(iss); 44 return; 45 } 46 } 47 48 printf("[Error] Unknown command: %s\n", cmd); 49 50} 51 52void NamedPipeThread() { 53 const char* pipeName = R"(\\.\pipe\game_pipe)"; 54 55 // Define security attributes 56 SECURITY_ATTRIBUTES sa; 57 sa.nLength = sizeof(sa); 58 sa.bInheritHandle = FALSE; 59 60 // Initialize the security descriptor 61 SECURITY_DESCRIPTOR sd; 62 if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) { 63 std::cerr << "Failed to initialize security descriptor. Error: " << GetLastError() << std::endl; 64 return; 65 } 66 67 // Set a NULL DACL, which grants full access to everyone 68 if (!SetSecurityDescriptorDacl(&sd, TRUE, NULL, FALSE)) { 69 std::cerr << "Failed to set NULL DACL. Error: " << GetLastError() << std::endl; 70 return; 71 } 72 73 // Assign the security descriptor to the SECURITY_ATTRIBUTES 74 sa.lpSecurityDescriptor = &sd; 75 76 while (true) { 77 HANDLE hPipe = CreateNamedPipeA( 78 pipeName, 79 PIPE_ACCESS_DUPLEX, 80 PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 81 1, 82 40, 83 40, 84 0, 85 &sa 86 ); 87 88 if (hPipe == INVALID_HANDLE_VALUE) { 89 printf("Unable to create named pipe, check for %d!\n", GetLastError()); 90 return; 91 } 92 93 printf("Waiting for client connection...\n"); 94 if (!ConnectNamedPipe(hPipe, NULL)) { 95 printf("Unable to connect into pipe, check for: %d!\n", GetLastError()); 96 CloseHandle(hPipe); 97 return; 98 } 99 100 char buffer[64]; 101 DWORD bytesRead; 102 103 while (ReadFile(hPipe, buffer, sizeof(buffer) - 1, &bytesRead, NULL)) { 104 buffer[bytesRead] = '\0'; 105 printf("Message received: %s\n", buffer); 106 DispatchCommand(buffer); 107 } 108 109 DisconnectNamedPipe(hPipe); 110 CloseHandle(hPipe); 111 } 112}

Obviamente, no lado do jogo, a minha DLL despacha esta função como uma thread, para garantir que o mesmo vai ficar sendo escutado durante todo o processo de execução do jogo.

No lado do Elixir:

1defmodule GameConnector do 2 @pipe_name "\\\\.\\pipe\\game_pipe" 3 4 def start_link() do 5 # Start a process to manage the persistent file handle 6 Task.start_link(fn -> communication_loop() end) 7 end 8 9 defp communication_loop() do 10 with {:ok, file} <- File.open(@pipe_name, [:binary, :read, :write]) do 11 try do 12 # Continuous communication handling 13 loop(file) 14 after 15 # Ensure the file is closed on exit 16 File.close(file) 17 end 18 else 19 {:error, err} -> 20 IO.inspect("Unable to start your communication with GTA SA through msa.dll") 21 IO.inspect(err) 22 end 23 end 24 25 defp loop(file) do 26 receive do 27 {:send, message} -> 28 IO.binwrite(file, message) 29 loop(file) 30 end 31 end 32 33 # API for sending a message 34 def send_message(pid, message) do 35 send(pid, {:send, message}) 36 end 37end

E internamente também adicionei um AllocConsole pra ajudar no debug e revisão desse processo.

END_OF_FILE • ARTICLE_COMPLETE