Общая тактика и стратегия взлома
Патроны, жизни, артефакты и прочее барахло — все это переменные, хранящиеся в определенных ячейках
и всегда выражаемые числом. С точки зрения компьютера, эти ячейки ничем не отличаются от огромного множества остальных, содержащих в себе координаты монстров, текстуры и прочие объекты игрового мира. Как установить за что отвечает та или иная ячейка? Неэффективность дизассемблирования мы уже отмечали, но… что если просто методично изменять одну ячейку за другой, наблюдая за реакцией игры, которой скорее всего будет срыв крыши или повисание.
Зная точное количество жизней/патронов, мы значительно сужаем круг поиска, исследуя только те ячейки, которые содержат нужное значение. Однако, следует помнить, что соответствие может быть как прямым, так и обратным. Одни программисты ведут учет жизней, другие — смертей, причем отсчет может вестись как единицы, так и от нуля, а в некоторых случаях и -1. Допустим, у нас есть три нерастраченных жизни. Означает ли это, что переменная live_count обязательно будет равна трем? Разумеется, нет! В ней вполне может два (и тогда игра закачивается когда live_count < 0), или ноль (игра заканчивается при live_count > 2). Возможны и другие значения. С патронами в этом плане все обстоит намного лучше и чаще всего они хранятся в памяти "как есть", однако, количество ложных срабатываний все равно будет очень велико! Допустим, у нас есть 50 патронов и мы ищем 32h в дампе программы. Да там этих 32h целый миллион! До конца сезона все не переберешь!
Ключ к решению лежит в изменениях! Наблюдая за характером изменения различных ячеек, мы легко отделим зерна от плевел. План наших действий выглядит так:
Сравнение первого и второго дампов (сейвов) показывает кучу отличий, соответствующих передвижениям монстров и прочим изменениям игрового мира. Но ни патронов, ни жизней в изменившихся ячейках не оказывается — ведь эти параметры заведомо не менялись! ОК, вычеркиваем изменившиеся ячейки из списка "подозреваемых" лиц, и сравниваем второй дам с третьим, игнорируя ранее изменявшиеся ячейки. На этот раз отличий будет не так уж и много. Ищем ячейки, дельта изменения которых соответствует количеству выстрелов или потерянных жизней/здоровья. Если таких ячеек больше одной, повторяем операцию 3 до тех пор, пока не останется только одна изменившаяся ячейка или, как вариант, последовательно хачим все подходящие ячейки, в надежде, что рано или поздно нам повезет.
В некоторых играх количество патронов хранится в нескольких переменных, дублирующих друг друга, но только одна из них значима, а остальные на хакерском жаргоне называются "тенями", отвечающими, например, за вывод текущего значения на экран. При модификации "теневой" переменной количество патронов/жизней чаще всего остается неизменным, и даже если оно возрастает, оружие перестает стрелять задолго до исчерпания своего "боезапаса", а нас все равно убивают. Ну не уроды?!
Сравнивать можно как дампы памяти, снятые с работающей программы, так и "сейвы" — файлы сохранений. Зная адрес нужной ячейки, мы можем повесить резидента, прописывающего сюда максимально возможное значение и при необходимости обновляющего его каждые несколько секунд (или чаще). Еще можно запустить soft-ice и, установив точку останова, перехватить тот код, который уменьшает количество патронов с каждым выстрелом — тогда мы сможем его хакнуть. Но это потребует дополнительных телодвижений, что не всегда удобно, поэтому многие хакеры ограничиваются правкой сейвов, выдавая игроку полный боезапас и максимум здоровья, однако, подлинное бессмертие в этом случае уже не достигается — патроны и жизни продолжают убывать и чтобы не умереть, их приходится постоянно пополнять.
Кроме того, некоторые монстры убивают вас наповал одной ракетой!
Маленький ликбез для самых начинающих: для хранения переменных используется шесть основных типов данных: байт, слово и двойное слово, которые могут быть как со знаком, так и без. Диапазон допустимых значений приведен в таблице 1. Без переделки программы, мы не можем присвоить больше игровых единиц, чем вмещается в назначенный программистом тип переменной, который обычно приходится определять экспериментально.
На этом ноте мы заканчиваем с теорией и переходим к практике.
тип |
минимальное значение |
максимальное значение |
unsigned byte |
0 |
255 |
signed byte |
-128 |
127 |
unsigned word |
0 |
65.535 |
signed word |
-32.768 |
32.767 |
unsigned dword |
0 |
4.294.967.295 |
signed dword |
-2.147.483.648 |
2.147.483.647 |
Рисунок 2 главная страница open-source проекта DOOM legacy, портироващего бессмертную игрушку на win32/linux