Взламываем игры с помощью Python

В стародавние времена, когда по земле ходили мамонты, а я был в два раза моложе, среди игрового сообщества пользовалась популярностью компьютерная программа для «взлома» игр под названием ArtMoney. С помощью этой софтины можно было не только облегчить себе жизнь в прохождении хардкорного приключения, модифицировав значения ресурсов в игре, но и просто поразвлечься, изучив полюбившийся проект с разных сторон.

А на днях мне вдруг захотелось вспомнить молодость и поиграть в бумерский диаблойд под названием Titan Quest, выпущенный аж в 2006 году. Да вот только времени на беготню, прокачку, и вот это вот всё, у меня нет. И ArtMoney нет. Зато есть определенные знания программирования. Вот я и решил совместить приятное с полезным, написав аналог ArtMoney на Python, а заодно стать супербогатым, хотя бы в Titan Quest.

Для этого дела понадобились только Python и библиотека Pymem, с помощью которой можно взламывать процессы Windows и манипулировать памятью (читать и записывать).

Программа состоит из класса MemoryEditor, который отвечает за взаимодействие с процессом игры, поиск и замену значений в его памяти. И функции, которая выступает интерактивным меню для взаимодействия с юзером.

Класс MemoryEditor

class MemoryEditor:
    def __init__(self, process_name: str) -> None:
        self.pm = pymem.Pymem(process_name)
        self.process_base = pymem.process.module_from_name(self.pm.process_handle, process_name).lpBaseOfDll

Конструктор класса принимает имя процесса (process_name), которое нужно открыть (например, process.exe).

  • pymem.Pymem(process_name) — открывает процесс и позволяет взаимодействовать с его памятью.
  • process_base — это базовый адрес основного модуля процесса (обычно самого .exe файла).

Метод search_value

def search_value(self, value: int) -> list:
        search_results = []
        memory_size = 0x7FFFFFFF  # Размер памяти для сканирования (большой диапазон)
        chunk_size = 0x1000  # Размер блока чтения
        search_bytes = ctypes.c_uint32(value).value.to_bytes(4, byteorder='little')
        
        offset = 0
        while offset < memory_size:
            current_address = self.process_base + offset
            
            if self.is_memory_readable(current_address):
                try:
                    buffer = self.pm.read_bytes(current_address, chunk_size)
                except pymem.exception.MemoryReadError:
                    offset += chunk_size
                    continue
                
                chunk_offset = 0
                while True:
                    chunk_offset = buffer.find(search_bytes, chunk_offset)
                    if chunk_offset == -1:
                        break

                    # Сохранение адреса найденного значения
                    address = current_address + chunk_offset
                    search_results.append(address)
                    
                    chunk_offset += len(search_bytes)
            
            offset += chunk_size

        return search_results

Эта функция выполняет поиск заданного значения (value) в памяти процесса.

  • memory_size определяет область памяти, в которой будет производиться поиск.
  • chunk_size определяет, какой объем данных будет считываться за раз (в данном случае 4KB).
  • search_bytes преобразует значение в байтовую строку для поиска в памяти.
  • Цикл while offset < memory_size: проходит по всей указанной области памяти, проверяя каждую часть на наличие нужного значения.
  • self.is_memory_readable(current_address) проверяет, доступна ли память для чтения.
  • Если значение найдено в текущем блоке памяти, его адрес сохраняется в search_results.

Метод is_memory_readable

def is_memory_readable(self, address) -> bool:
        mbi = pymem.memory.virtual_query(self.pm.process_handle, address)
        if mbi.Protect & 0xF != 0x0 and mbi.State == 0x1000 and mbi.Protect & 0x100 == 0:
            return True
        return False

Эта функция проверяет, доступна ли память по указанному адресу для чтения.

  • Использует функцию virtual_query из библиотеки pymem, которая возвращает информацию о состоянии и защите памяти.
  • Проверяет, что память доступна, не защищена и не имеет флага PAGE_GUARD.

Метод search_next_value

def search_next_value(self, addresses: list, next_value: int) -> list:
        search_results = []
        search_bytes = ctypes.c_uint32(next_value).value.to_bytes(4, byteorder='little')
        
        for address in addresses:
            if self.is_memory_readable(address):
                try:
                    buffer = self.pm.read_bytes(address, 4)
                except pymem.exception.MemoryReadError:
                    continue
                
                if buffer == search_bytes:
                    search_results.append(address)
        
        return search_results

Эта функция ищет новое значение (next_value) только среди адресов, найденных на предыдущем этапе поиска.

  • Функция принимает список адресов (addresses) и значение для поиска (next_value).
  • Если новое значение найдено по одному из адресов, этот адрес добавляется в список search_results.

И метод replace_value

def replace_value(self, addresses: list, new_value: int) -> None:
        replace_bytes = ctypes.c_uint32(new_value).value.to_bytes(4, byteorder='little')
        for address in addresses:
            self.pm.write_bytes(address, replace_bytes, 4)
            print(f"Замена значения по адресу: {hex(address)} на {new_value}")

Функция заменяет значения по указанным адресам (addresses) на новое значение (new_value).

  • replace_bytes — это новое значение в виде байтовой строки.
  • self.pm.write_bytes(address, replace_bytes, 4) записывает новое значение в память по указанному адресу.

Вот и всё. Теперь в основной части программы необходимо просто создать экземпляр написанного выше класса с названием процесса игры, и поочередно вызывать необходимые методы, для поиска нужных ячеек, и замены значений. Я не стал мудрить с интерфейсом, и написал простейшее меню в командной строке, с запросами нужной информации у пользователя. Выглядит это так:

В игре у моего персонажа было 12345 монет, это значение я и ввел. Однако программа нашла слишком много адресов с идентичными значениями, поэтому в игре я собрал еще немного голды, изменив количество монет у персонажа, и отфильтровал адреса уже по новым данным. Во второй раз адресов было значительно меньше и я решил дальше не фильтровать, а изменить значения во всех. В итоге мой персонаж стал почти миллионером (совсем уж наглеть не стал).

Что сказать, я доволен, и могу без всяких там читов чистить данжы, закупившись хилками на все деньги.

Кто желает воспользоваться программой или дополнить её: репозиторий PyMoney.

Благодарю за внимание!