Можно ли модифицировать любимую игру без дизассемблера? Запросто. Тебе понадобятся лишь HEX-редактор и несколько хитрых приемов, которыми я поделюсь в этой статье. Мы рассмотрим, как легко ломаются неизвестные форматы файлов на примере всемирно признанного шедевра — игры «Ядерный титбит».
Во многих играх ресурсы упакованы в специальный файл — игровой архив. Здесь могут лежать модели, текстуры, скрипты, анимация, звуки и прочее. Некоторые разработчики не запариваются и используют ZIP, разве что для минимальной маскировки меняют расширение на какой‑нибудь произвольный набор букв.
Сейчас почти все игры делаются на одном из популярных движков, но в начале двухтысячных разработчики еще частенько делали всё с нуля. У многих фирм были свои уникальные наработки, которые ложились в основу одной или нескольких игр. Потрошить игры на таких движках намного интереснее, чем ковырять ассеты популярных движков, для которых полно готового инструментария.
Дальше в статье я буду активно использовать встроенный Inspector, который интерпретирует байты как числа, даты или ассемблерные опкоды. Кликаешь на интересующий тебя байт и видишь, чем он может быть. Также часто бывает нужна «линейка» — выделяешь мышкой данные и узнаёшь расстояние между структурами.
Догадаться, какой из файлов в папке игры — это архив с ресурсами, можно, просто глянув на размеры. QUEST.ezd самый большой и занимает 216 Мбайт. Это явно наш клиент. Открываем его в HEX-редакторе:
Обычно первые четыре байта содержат сигнатуру формата. Здесь мы видим строку ezd., что совпадает с расширением файла. Значит, это сигнатура. Если нет явных противоречий, продолжаем разбор с шагом в четыре байта. Структуры часто выравниваются по INT32.
Следующие четыре байта: 0x4D080000. Программа собрана для x86, поэтому числа записаны в обратном порядке байтов. Таким образом, 0x4D080000 интерпретируется как 0x0000084D, что в десятичной системе равно 2125. Что это может быть? Возможно, заголовок архива содержит число файлов. Две тысячи — вполне адекватное число. Проверим эту гипотезу позже.
Далее идет 0x3D750200, это декодируется в число 161 085, что составляет 157 Кбайт. Это может быть размер текущей структуры или смещение начала следующей. Действительно, по адресу 161 085 заканчивается список строк, похожих на имена файлов.
Следующий DWORD — 0xD69A820D, что эквивалентно 226 663 126. Это точно размер файла — 226 663 126 байт.
Опыт подсказывает, что на 16-м байте файловый заголовок заканчивается. Дальше следуют строки, перемешанные с неизвестными числами. Скорее всего, это массив структур, описывающих конкретные файлы. Попробуем определить размер и назначение всех полей.
Первая структура начинается с ASCII-строчки door_01.meat. Если размер строки заранее не указан, значит, она читается до нулевого байта. До начала следующей строки 18 байт. Расстояние между случайными строками не меняется, следовательно, это значимые поля.
Сравнивая предполагаемые структуры, замечаем после каждой строки шесть неизменных значений 0x000002000000. Считаем их константой. Остается понять назначение 12 изменяемых байтов.
Что сразу бросается в глаза? Во‑первых, число 161 085 совпадает со смещением из файлового заголовка. Вероятно, это адрес с данными первого файла. Следующие два числа могут быть размером и контрольной суммой. Смещение второго файла — 163 654. Вычитая смещение первого, получаем 2569. Значит, это поле с размером данных.
Мы уже знаем название, смещение и размер любого файла. Попробуем сохранить файл известного формата. Например, gameboom.xml лежит по смещению 226 662 685 и имеет размер 441 байт. Любопытно! Это последний файл в архиве. Сложив адрес и размер, получаем смещение конца файла. Значит, предположения верны. Но вместо ожидаемого XML видим мусор. Данные зашифрованы или сжаты. Сравним с любым другим XML-файлом. Первые два байта совпадают. Гуглим 0x78DA и понимаем, что это сигнатура zlib:
Итак, формат пал. Проверяем, что zlib работает.
Повторяем с door_01.meat. Распакованный файл занимает 7393 байта. Вот что значило то неизвестное поле.
Мы не узнаем размер ITEM_HEADER, пока не подсчитаем длину строки. Заглянув в справку, находим нужные функции API. К счастью, скрипты работают даже внутри шаблонов. Чтобы переменная не отобразилась как часть структуры, ставим перед ней local. Мы вправе обращаться из одной структуры к другой, как это сделано в files[head.FileCount].
В треугольных скобках указываются параметры отображения полей или структуры целиком:
Спускаемся к последней структуре и видим, что записей о файлах намного больше. Но они не имеют ссылок на данные и содержат исходный путь. Например:
Видимо, их оставили для отладки. Как и предполагалось, рабочих файлов ровно 2125. Формат полностью разобран.
Запускаем его, и в папке с архивом появляются ресурсы игры в первозданном виде.
Из‑за особенностей TGA-текстур мы никак не можем повлиять на размер сохраняемого графическим редактором файла. Но можем уменьшить количество используемых цветов, что повысит эффективность сжатия и поможет zlib создавать файлы меньшего размера.
Диалоги хранятся в XML. Нам опять же понадобится вписаться в границы старого размера, поэтому сократим текст в произвольном месте. Заменяем файлы и пробуем распаковать архив прошлым скриптом. Ошибок не найдено, значит, все работает!
Автор @mr.grogrig, t.me/mr_grogrig
Источник xakep.ru
Во многих играх ресурсы упакованы в специальный файл — игровой архив. Здесь могут лежать модели, текстуры, скрипты, анимация, звуки и прочее. Некоторые разработчики не запариваются и используют ZIP, разве что для минимальной маскировки меняют расширение на какой‑нибудь произвольный набор букв.
Сейчас почти все игры делаются на одном из популярных движков, но в начале двухтысячных разработчики еще частенько делали всё с нуля. У многих фирм были свои уникальные наработки, которые ложились в основу одной или нескольких игр. Потрошить игры на таких движках намного интереснее, чем ковырять ассеты популярных движков, для которых полно готового инструментария.
SweetScape 010 Editor
Кто‑то скажет, что главный инструмент хакера — его мозги. И будет прав. Но на втором месте — удобный HEX-редактор. Я горячо рекомендую SweetScape 010 Editor. Редактор кросс‑платформенный: есть версии для Windows, Linux и macOS.Дальше в статье я буду активно использовать встроенный Inspector, который интерпретирует байты как числа, даты или ассемблерные опкоды. Кликаешь на интересующий тебя байт и видишь, чем он может быть. Также часто бывает нужна «линейка» — выделяешь мышкой данные и узнаёшь расстояние между структурами.
ФОРМАТ С КИСЛОТНЫМ ПРИВКУСОМ
Сегодня продегустируем «Ядерный титбит», игру 2003 года, созданную при участии Дани Шеповалова.Догадаться, какой из файлов в папке игры — это архив с ресурсами, можно, просто глянув на размеры. QUEST.ezd самый большой и занимает 216 Мбайт. Это явно наш клиент. Открываем его в HEX-редакторе:
Код:
0000h: 65 7A 64 FF 4D 08 00 00 3D 75 02 00 D6 9A 82 0D ezd.M...=u..Öš‚.
0010h: 64 6F 6F 72 5F 30 31 2E 6D 65 61 74 00 00 02 00 door_01.meat....
0020h: 00 00 3D 75 02 00 E1 1C 00 00 09 0A 00 00 64 6F ..=u..á.......do
0030h: 6F 72 5F 30 32 2E 6D 65 61 74 00 00 02 00 00 00 or_02.meat......
0040h: 46 7F 02 00 95 21 00 00 53 0B 00 00 64 6F 6F 72 F...•!..S...door
0050h: 5F 30 33 2E 6D 65 61 74 00 00 02 00 00 00 99 8A _03.meat......™Š
0060h: 02 00 71 20 00 00 0F 0B 00 00 64 6F 6F 72 5F 30 ..q ......door_0
Следующие четыре байта: 0x4D080000. Программа собрана для x86, поэтому числа записаны в обратном порядке байтов. Таким образом, 0x4D080000 интерпретируется как 0x0000084D, что в десятичной системе равно 2125. Что это может быть? Возможно, заголовок архива содержит число файлов. Две тысячи — вполне адекватное число. Проверим эту гипотезу позже.
Далее идет 0x3D750200, это декодируется в число 161 085, что составляет 157 Кбайт. Это может быть размер текущей структуры или смещение начала следующей. Действительно, по адресу 161 085 заканчивается список строк, похожих на имена файлов.
Следующий DWORD — 0xD69A820D, что эквивалентно 226 663 126. Это точно размер файла — 226 663 126 байт.
Код:
0x00 657A64FF ezd. Сигнатура формата
0x04 4D080000 2125 Возможно, количество файлов
0x08 3D750200 161085 Неизвестное смещение
0x0C D69A820D 226663126 Размер файла
Первая структура начинается с ASCII-строчки door_01.meat. Если размер строки заранее не указан, значит, она читается до нулевого байта. До начала следующей строки 18 байт. Расстояние между случайными строками не меняется, следовательно, это значимые поля.
Сравнивая предполагаемые структуры, замечаем после каждой строки шесть неизменных значений 0x000002000000. Считаем их константой. Остается понять назначение 12 изменяемых байтов.
Код:
0x10 646F6F725F30312E6D656174 door_01.meat
0x1C 000002000000 const
0x22 3D750200 161085
0x26 E11C0000 7393
0x2A 090A0000 2569
Мы уже знаем название, смещение и размер любого файла. Попробуем сохранить файл известного формата. Например, gameboom.xml лежит по смещению 226 662 685 и имеет размер 441 байт. Любопытно! Это последний файл в архиве. Сложив адрес и размер, получаем смещение конца файла. Значит, предположения верны. Но вместо ожидаемого XML видим мусор. Данные зашифрованы или сжаты. Сравним с любым другим XML-файлом. Первые два байта совпадают. Гуглим 0x78DA и понимаем, что это сигнатура zlib:
Код:
0x7801 zlib No Compression/low
0x789C zlib Default Compression
0x78DA zlib Best Compression
Код:
import zlib
in_str = '78 DA 63 60 60 62 80 01 46 20 94 60 40 02 21 41 A1 AE 61 9E C1 9E FE 7E BA 11 6E 9E 3E AE 7A 0C 00 36 EA 05 08'
data = bytes.fromhex(in_str)
out = zlib.decompress(data)
with open('black.TGA', "wb") as output:
output.write(out)
БЫСТРАЯ ПРОВЕРКА СТРУКТУР
Перед тем как писать скрипт, стоит проверить наши догадки в HEX-редакторе. 010 Editor использует C-подобный язык для шаблонов и скриптов, что позволяет оперативно проверять предположения о структуре файла. Кривой шаблон вылетает за границу конца файла или интерпретирует поля одной из структур с ошибками. А мы получаем некоторую связь с реальностью.
Код:
struct ITEM_HEADER
{
local int name_len = Strlen(ReadString(startof(this)));
CHAR Name[name_len];
BYTE Const[6] <format=hex>; // 00 00 02 00 00 00
DWORD Offset;
DWORD UnPackedSize ;
DWORD PackedSize;
};
string read_file_name(ITEM_HEADER & h)
{
return h.Name;
}
struct FILE_HEADER
{
DWORD Signature <format=hex>; // 'ezd\xff'
DWORD FileCount;
DWORD EndOfHeaders;
DWORD FileSize;
};
FILE_HEADER head <open=true>;
ITEM_HEADER files[head.FileCount] <optimize=false, read=read_file_name>;
В треугольных скобках указываются параметры отображения полей или структуры целиком:
- <format=hex> показывает строку байтов вместо числа;
- <open=true> раскрывает структуру при каждом исполнении скрипта;
- <optimize=false> отключает оптимизацию. Иначе массив будет состоять из копий первого элемента;
- <read=read_file_name> — имя функции, дающей структуре значение, в нашем деле — имя файла.
Спускаемся к последней структуре и видим, что записей о файлах намного больше. Но они не имеют ссылок на данные и содержат исходный путь. Например:
Код:
D:\HoloSpace3D\maket\1\MEAT_MAT\club-bar02\bar02stol.meat
ПИШЕМ ФИНАЛЬНЫЙ СКРИПТ
Осталось накидать скрипт, который будет извлекать файлы.
Код:
import os
import zlib
import struct
from collections import namedtuple
FILE_HEADER = namedtuple('FILE_HEADER', ['Signature', 'FileCount', 'EndOfHeaders', 'FileSize'])
ITEM_HEADER = namedtuple('ITEM_HEADER', ['Name', 'Const', 'Offset', 'UnPackedSize', 'PackedSize'])
os.makedirs('files', exist_ok = True)
with open('QUEST.ezd', "rb") as input:
data = input.read()
file_header = FILE_HEADER(*struct.unpack('LLLL', data[0:16]))
offset = 16
for i in range(file_header.FileCount):
name_end = data.find(b'\x00', offset)
name_len = name_end - offset
struct_end = name_end + 18
struct_body = data[offset:struct_end]
item_header = ITEM_HEADER(*struct.unpack(f'<{name_len}s6sLLL', struct_body))
file_body = data[item_header.Offset:item_header.Offset + item_header.PackedSize]
unpacked = zlib.decompress(file_body)
file_name = item_header.Name.decode('utf-8')
with open(f'files/{file_name}', "wb") as output:
output.write(unpacked)
offset = name_end + 18
ВНОСИМ ИЗМЕНЕНИЯ
Хочешь не только вытащить ресурсы, но и побаловаться, меняя изображения и тексты в любимой игре? Легко! Можем даже не пересобирать архив целиком: старые файлы можно заменить новыми прямо в HEX-редакторе. Нужно только не забыть упаковать их при помощи zlib. В этом поможет вот такой несложный скриптик:
Код:
import zlib
file_name = 'podvor_render_cc.tga.tga'
with open(file_name, "rb") as input:
data = input.read()
level = 9 # Z_BEST_COMPRESSION
packed = zlib.compress(data, level = level)
out_name = f'{file_name}.{level}.zlib'
with open(out_name, "wb") as output:
output.write(packed)
Диалоги хранятся в XML. Нам опять же понадобится вписаться в границы старого размера, поэтому сократим текст в произвольном месте. Заменяем файлы и пробуем распаковать архив прошлым скриптом. Ошибок не найдено, значит, все работает!
ВЫВОДЫ
Как видишь, для взлома полезно понимать, какие данные могут храниться в исследуемом контейнере. Этим методом можно вскрывать ресурсы к большинству игр. Впрочем, применения могут быть и менее безобидными. Изменение ресурсов иногда может привести и к выполнению произвольного кода. Но это уже совсем другая история.Автор @mr.grogrig, t.me/mr_grogrig
Источник xakep.ru