Исследование игр без исходного кода на примере Zuma

:

: 3

Эту игру не надо, наверное, представлять. Жаба, шарики и десятки часов потреченных впустую. В первую часть (когда она вышла) играл часами, до ряби, ночи напролет. Играла жена, друзья, все играли, в общем. Любой, кто посмотрел в глаза жабе, услышал волшебное «дзынь» шариков, метким ударом сделал комбинацию из нескольких цветов, оставался с этой игрой надолго. Кот, вон, и тот лапой по экрану бил, когда видел летящий шарик. В общем если бы кто-то спросил про мои любимые казуальные игры, я бы назвал Zuma и Bejeweled.

Но это присказка. Сказка в том, что я недавно перечитывал «Футурологический конгресс» Лема и наткнулся на орден безлюдистов, который проповедуют Евангелие компьютерам. Это привело меня к мысли, что в Евангелие компьютеру, в общем-то, ни к чему, а вот в Zuma он поиграть не отказался бы. Сказано-сделано. Пишу программу которая играет в Zuma.

Сразу предупреждаю, что хотел написать мало и быстро, а получилось настолько много, что придется разбить на несколько частей. Если кому скучно читать — можно внизу статьи посмотреть видео.

В комментариях прошу ответить: интересна ли тема, и стоит ли продолжать писать подобное? Интересно ли изложение? (я пишу одновременно с тем как делаю, поэтому тому кто читает может показаться странным).

Приблизительный план действий такой:
1) Препарировать приложение
а) Найти как хранятся шарики, их координаты и цвета
б) Найти как хранится жабка
в) Определить способ запуска шарика
г) Определить где gameover, или что-то вроде этого
2) Сделать инъекцию DLL, чтобы по нажатию кнопки, компьютер играл сам
3) Наслаждаться содеянным

Для приготовления препарата мне понадобится IDA Pro, в качестве скальпеля буду использовать OllyDbg.

Итак, определяю месторасположение шариков. Думаю что нужно посмотреть, как загружаются рисунки. Смотрю на файловую структуру игры, ресурсы хранятся очень просто, в обычных gif файлах.

Есть смысл поиска в исходнике по названию файла. ALT+T, «RedBall», Enter. Нету. А если просто «Ball»? Есть! Перехожу к строке, смотрю перекрестные ссылки. Используется в одном месте:

Ого какая функция огромная. Неужели они ее вручную набивали? Все действия сводятся примерно к одному. Насколько я понимаю в функцию загрузки картинки передается ее идентификатор и еше какие-то вещи, которые мне сейчас не нужны. Функция возвращает указатель. Указатель записывается в какую-то глобальную переменную. Здесь пока понятно. Теперь перехожу к отладчику и смотрю что с этим указателем происходит дальше.

Ставлю точку останова на указатель и запускаю игру. Останавливается на этой строчке:

Тут хорошо видно, что есть таблица указателей (скорее всего на картинки). В зависимости от чего-то выбирается индекс в картинке, соответствующий шарику определенного цвета. Чтобы быть точно в этом уверенным, делаю небольшой эксперимент: делаю индекс константой. Меняю строку MOV EAX,DWORD PTR DS:[EAX*4+9E9FC8] на MOV EAX,DWORD PTR DS:[9EA760] (9E9FC8 + 1E5*4). Вот что получилось:

но это, конечно же, визуально. Игра воспринимает шарики по-прежнему. После анализа кода, отбросив все ненужное, получаю код для определения цвета в этой функции:

  1. mov eax, [ebx+14h] ; в регистр еах загружают индекс шарика
  2. add eax, 1DFh ; прибавляют смещение таблицы шариков
  3. mov eax, off_9E9FC8[eax*4] ; берут указатель на картинку из массива

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

В этой функии, в самом начале регистр ebx инициализируется значением регистра ecx. Можно сделать вывод, что это объект. И предположить что он представляет собой шарик. Попробую подняться по стеку и увидеть откуда берется этот объект.
И, буквально, парой функций выше попадается как раз то что мне нужно:

Опять же, отбрасываю все ненужное и записываю код чтобы было понятно:

  1. mov [ebp+var_C], offset dword_E43B9C ; указатель на структуру помещают в локальную переменную
  2. mov esi, [ebp+var_C] ; перемещают в регистр
  3. add esi, 0FFFF1000h ; от него отнимают 0xF000 (чтобы получить смещение начала шариков)
  4. mov ecx, [esi] ; берут шарик
  5. call edx ; вызывают его функцию-член (я думаю это рисование, но бог весть, что оно там. ну и не надо оно мне вообще-то) 
  6. add esi, 4 ; переходят к следующему шарику
  7. sub ebx, 1 ; уменьшают итератор

Для пробы внедрю сюда код, который будет перекрашивать шарики в 4 цвета, по очереди. Использую Microsoft Detours. Подробно расписывать не буду, и так накатал столько, что вряд-ли кто сюда дочитал. Вкратце код будет такой:
  1. int ball_type = 0; //глобальная переменная
  2.  
  3. for (int i = 0; i < balls_count; ++i)
  4. {
  5.     ball_record* ball = balls[i];
  6.     ball->type = ball_type % 4;
  7. }

Это чтобы сделать режим «Nightmare!»
  1. for (int i = 0; i < balls_count; ++i)
  2. {
  3.     ball_record* ball = balls[i];
  4.     ball->type = 1;
  5. }

а это «I’m too young to die»

Итак, первый пункт плана я выполнил, продолжение следует…