Привет, Хабр! С вами снова спидраннинг коммьюнити NFS. И мы снова чиним старенькую игрушку — NFS Most Wanted. Я уже рассказывал о починке багов в своих предыдущих статьях, а сегодня хотел был пойти с вами немного глубже в дебри дизассемблирования. Заинтересовавшихся прошу под кат.



Предыстория


Когда-то давно, когда EA издавала хорошие NFS, вышла одна из известнейших гоночных игр — Most Wanted. Увы, написана она была не так хорошо, как продавалась, и периодически падала. Конечно, обычный человек на это обращает мало внимания — ну вылетела разок за прохождение, ничего страшного. А вот нам это создает огромные проблемы: сколько потенциальных рекордов было убито случайными падениями без внятных симптомов. Все закончилось тем, что KuruHS лично попросил меня разобраться в ситуации. Отказаться я не смог.

Что имеем




IDA — для дизассемблирования
Cheat Engine — для редактирования памяти и инструкций
Visual Studio — для отладки (Trace Points, оказались весьма удобной вещью)

У нас есть куча дампов. Приличная куча, гигабайт на 10. С них мы и начнем — проанализируем, на каких инструкциях падает игра. А падает она довольно рандомно, хотя некоторые закономерности прослеживаются. За время решения проблем мы нашли несколько потенциально опасных мест, которые иногда крашат игру. Например:



в функции вычисления хэша строки. Видимо, разработчики не ожидали получить null-pointer в этом месте, поэтому не добавили проверку на него. Из-за этого в редких случаях игра падала. Фикс довольно банальный — прыгнуть в первый пустой кусок экзешника, да сделать test edi, edi. Потом jz retun и jmp откуда прыгали изначально.



Другой похожий случай нашёлся в процедуре по адресу
0х0057D105 mov edx, [ecx] ; я так и не смог понять, что конкретно она делает

Разработчики снова не ожидали получить там null pointer, поэтому игра падала. Фикс абсолютно идентичен предыдущему.



Наиболее распространённая причина падения оказалась в функции AllocateMemory. Попытки ее дизассемблирования повергли в ужас всех, кто работал над проблемой падений игры. Внимания удостоен уже тот факт, что в игре как минимум 5 разных подсистем управления памятью. Во что я ввязался…



Ладно, нет времени ныть, надо реверсить. Несколько вечеров за разборами этого мусора принесли свои плоды: код, хоть все еще и не читаемый, стал более понятен. Судя по всему, эта подсистема работает по стандартной схеме: грабастаем некоторое количество памяти сразу, разбивая на блоки, храним их в двусвязном списке; по требованию выдаём свободные участки, а если таковых нету — пытаемся взять у системы еще. Ах, 2005-ый, когда операции с памятью были достаточно дорогими, чтобы ей разбрасываться как попало…



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

Конечно, проблемы целиком это не решило, но как минимум игра стала действительно стабильнее — за неделю тестирования в этом конкретном месте она упала всего несколько раз (при учете, что KuruHS проводит в игре по 10 часов в день), что я считаю довольно неплохим результатом.

Pure virtual function call.


Та самая ошибка, что проиллюстрирована в шапке. Люди, знакомые с С++ сразу поймут, в чем проблема. Однако без исходного кода все становится значительно сложнее. Ситуацию усложняет CRT, который, аки партизан, упорно не хочет генерировать дампы, если вылавливает этот тип ошибки.

Purecall означает, что код попытался вызвать «чисто виртуальную функцию» (виртуальную функцию класса, не имеющую реализации). Без сомнений, этого сделать у него не получается, поэтому единственное, на что он решается — сообщить об этом пользователю и завершиться с кодом 0. В итоге вроде бы все и хорошо с кодом, а на деле все плохо.

Спасибо Microsoft за замечательную функцию — _set_purecall_handler, которая позволяет заменить обработчик purecall'ов. Ищем в экзешнике упоминания/ссылки, находим саму функцию. Теперь осталось написать свой обработчик и не забыть установить его как хэндлер. Для этого нам нужно найти достаточно большой кусок неиспользуемого кода в самом экзешнике, который мы сможем перезаписать на наш код. Недолгий поиск показал, что это будет функция _CxxThrowException (ссылок на нее не было найдено). Беспощадно записываем все ее тело nop'ами и начинаем творить поверх нее:



Вот так будет выглядеть псевдокод новых процедур:

new_handler:
	xor	eax, eax		; return *(0);
	mov	eax, [eax]		; моментально валит игру
	ret
set_handler:
	push 	new_handler
	call	_set_purecall_handler	; _set_purecall_handler(new_handler);
	add	esp, 4			; cdecl, восстанавливаем стек
	ret	

Компилируем (в моем случае руками вбиваем в Cheat Engine) и вставляем в код:



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



Единственная проблема заключается в том, что ошибка эта довольно редкая, а часами бесцельно играть не хочется. Я все же решил немного потестировать сам, и был приятно удивлен — игра упала буквально через 10 минут геймплея, причем упала на только что написанном мною участке. Перемещаемся по стеку вызовов немного выше:

0043E005  call        dword ptr [edx+80h] 

Ничего не могу сказать, кроме как: «да, это вызов виртуальной функции». Первая же мысль — а что, если без него? Выпиливаем его nop'ами, тестируем — вроде живем. Игра работает как надо. Побочных эффектов нет. Собираем патч, отсылаем на тестирование. Через день прилетает дамп, где та же процедура падает несколькими байтами ниже. Выпиливаю и ее — игра начинает падать. Все ведет к тому, что нужно думать над более серьезным решением. Но в голову ничего не лезет, поэтому откладывается на неопределенный срок.

За ночь я успел все обдумать, и пришёл к выводу. Вы скажете, что С++ не умеет в рантайме определять тип объекта? А я скажу, что может. И очень просто — по адресу виртуальной таблицы в памяти. Изучив дампы, я пришел к выводу, что периодически в процедуру прилетает неправильный класс (vtbl @ 0x00890970), а значит мы можем отловить эту ситуацию:


	cmp	edx, 00890970h
	jnz	good_class
	xor	eax, eax
	jmp	return
good_class:
	call	dword ptr[edx+80h]
	jmp	continue

Но есть одна загвоздка: это занимает достаточно много места, и это необходимо встроить в процедуру. Найти достаточно места не получится, все что есть — пара пустых кусков в несколько байт перед функцией. Спасибо уже и на том, что их много и они близко. Поэтому пишем спагетти и прыгаем из одного места в другое чуть ли не после каждой инструкции:



Лирика
Возможно я слегка погорячился и стоило все-таки впихнуть это в некогда функцию _CxxThrowException, раз уже я ее почистил. Но увы, сделал как сделал. Попробую на днях переделать этот фикс.

Патчим и запускаем. И получаем всё ту же проблему: этот крах настолько редкий, что за почти 4 часа тестирования этот кусок кода был запущен всего пару раз, и все разы на вход был получен правильный класс.

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

Быстрый осмотр показал, что игра может упасть, если один из аргументов не равен нулю. Сама процедура вызывается всего в двух местах, причем в одном из случаев вызывается с тем самым аргументом, выставленным в 0. Значит смотрим другую функцию.



убираем по максимуму «лишние» проверки и пытаемся насильно вызвать эту функцию. Запускаем тестирвоание и наконец-то получаем неправильный класс на вход. Ждём, пока отладчик студии допечатает весь текст, игра отвисает и… продолжает работать. Ура!


Скриншот мыльный, ибо запись со стрима

Заключение


Решение найдено — игра больше не падает, даже если на вход подали что-то не то. Это заметно на скриншоте выше — часть заграждения отсутствует, потому что игра попыталась поставить туда что-то не то. Что именно — загадка, покрытая мраком, но я уверен, что рано или поздно мы узнаем и это.

В целом, ситуация действительно заметно улучшилась — KuruHS смог полноценно провести в игре порядка 20 часов без единого падения, что раньше было бы просто невозможно.

Весь фикс я решил оформить в виде asi скрипта, по принципу Widescreen патчей от ThirteenAG. Почитать исходники и скачать скрипты можно на гитхабе.

Спасибо за внимание!

Комментарии (38)


  1. amarao
    16.02.2018 18:08

    Круто. Патчить любой ценой.

    А на месте EA я бы открыл сырцы. Ресурсы — как были, за деньги. А сырцы доступны.


    1. GrimMaple Автор
      16.02.2018 18:14

      EA не из тех контор, которые просто откроют исходники. Они очень переживают за свои IP, спасибо хотя бы на том, что не срубают на корню все моды, как некоторые другие конторы на букву N.


      1. amarao
        16.02.2018 18:18

        Кстати, если они откроют исходники под ограниченной лицензией, это всё равно будет лучше, чем просто закрытые.


        1. GrimMaple Автор
          16.02.2018 18:22

          Мне разрешили делать с их играми что угодно, покуда я не использую их IP. Я попробую написать им об открытии сырцов (или хотя бы узнать условия, на которых они готовы были бы их открыть)


          1. amarao
            16.02.2018 18:28
            +1

            Но вы используете их IP.

            Возможно, речь идёт про «не используете их IP с коммерческими целями». Это совсем другое.


            1. GrimMaple Автор
              16.02.2018 18:33

              We do not object to the extent your project does not incorporate any of our intellectual property (including artwork, trademarks, or game code).

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


              1. amarao
                16.02.2018 18:35
                +1

                Прикрылись. Если буквально — то «возражают». Если по духу — «можно, пока не начинаете тырить в свою проект наш код».

                Вероятнее всего, они имели в виду, что если вы будете кому-то давать код проекта (проект), то он не должен включать в себя саму игрушку.


          1. VEG
            17.02.2018 15:34

            А куда вы вообще пишете? Я вот Need For Speed III: Hot Pursuit долгое время занимался. Я пытался связаться с разработчиками на предмет может у кого завалялись ранние беты с лучшим качеством ресурсов или отладочная информация. Без результата. Те кто ответили сказали что ничего от тех времён (1998 год) не осталось =) Заполучить исходники было бы пределом мечтаний.


            1. GrimMaple Автор
              17.02.2018 17:55

              Я написал в Mass Media отдел, оттуда меня отфутболили в специальный отдел разрешений (Permission Requests Team).
              С Most Wanted попроще, так как есть отладочная информация для PS2 демо-версии. Не идеал, но хоть что-то. Думаю, при желании NFSIII можно зареверсить и ручками, ибо там явно меньше кода. А можно и OpenNFSIII сделать ^_^


      1. F376
        16.02.2018 22:01

        1. GrimMaple Автор
          16.02.2018 22:01

          Интересная находка, но не похоже на тот код, что есть в игре. Прошло уже слишком много времени, исходников конкретно этой игры может уже просто не быть.


      1. boblenin
        16.02.2018 23:24

        И на букву R*


  1. kinderandry
    16.02.2018 18:14

    Замечал такое в некоторых пиратках — типа Black edition — в блок-посте не хватало машин. Получается, пираты решили проблемы разрабов?


    1. GrimMaple Автор
      16.02.2018 18:16

      Не факт. Мы с одним товарищем посчитали хэши файлов пиратки и идентичной лицухи. За исключением очевидно отличающегося exe файла там рядышком отличалась целая куча других файлов, в том числе машин, трасс и прочего. Пиратская версия явно отличается от того, во что играли люди с лицензией.


      1. khim
        16.02.2018 18:36

        Скорее всего утекла поздняя бета, а когда вышел полноценный релиз, то повторно ломать никто не захотел…


  1. vient
    16.02.2018 21:58

    Пробовали использовать инструменты типа frida для быстрой проверки, что логика патча верна?


    1. GrimMaple Автор
      16.02.2018 22:00

      Я не думаю, что такой инструмент в принципе существует. Слишком уж неоднозначно поведение всей программы после исправления пары инструкций (из нескольких миллионов). Перестанет падать в одном месте — может спокойно начать падать в другом, казалось бы, совершенно не связанном с патчем месте. Если есть — буду только рад узнать о нём :)


      1. vient
        17.02.2018 23:35

        Я неправильно выразился, имел в виду, что впатчивать ассемблерный код достаточно долго. Чтобы не потратить время на впиливание патча, который по какой-то причине окажется неправильным, можно сначала делать патчи, например, на Javascript, как во фриде, а потом уже удачные варианты оформлять в виде бинарного патча.


  1. creker
    16.02.2018 22:23
    +1

    Ах, 2005-ый, когда операции с памятью были достаточно дорогими, чтобы ей разбрасываться как попало…

    Справедливости ради, актуальности это нисколько не потеряло и сегодня. Игры сплошь состоят из самописных аллокаторов. Иначе никак.


    1. AllexIn
      17.02.2018 11:07
      +1

      ЭЭээ?
      Последний раз в геймдеве видел кастомный аллокатор в 2006 году.
      Сейчас, только для логирования выделения памяти и детекта утечек, в продакшн идёт стандартное всё.
      Зачем в современном мире кастомный аллокатор?(Если мы не говорим о встраивании GC, как в UE)


      1. creker
        17.02.2018 13:05

        Я по аналогии говорил про крупные проекты. CryEngine открыт, аллокаторов своих там полно. Вполне стандартные вариации для геймдева. Фростбайт свое имеет, разработчики killzone тоже немного делились. Да и вообще, по всем презентация по оптимизации видно, что ничего не изменилось. Зачем? Оптимизация конечно, работа с памятью для игр это один из главных источников тормозов сегодня. Потоков много, компонентов дофига, кэш промахи дешевле не стали. Недавно вон на С++ конференции было лекция про аллокаторы с кучей статистики об их производительности. Многие варианты в геймдеве тоже используют — фиксированный пул, бакеты, свои вариации кучи и т.д. Все зависит от специфики подсистемы.


      1. Marui
        17.02.2018 17:51
        +1

        1) Консоли
        2) placement_new/memory pool
        3) алокации для мелких и больших объектов могут делаться поразному
        Тоесть я согласен с тем, что его не пишут руками по 100 раз на проект. Окей, по 1 разу на платформу пишут. Раз в 7-10 лет.


  1. crocodile2u
    17.02.2018 10:12
    +2

    Заголовок прочитал — думал про NFS, а тут про NFS :-)


    Но хорошая статья, спасибо!


  1. perfect_genius
    17.02.2018 17:10

    Планируете постепенно полностью отреверсить всю игру?


    1. GrimMaple Автор
      17.02.2018 17:52

      В идеале, я хотел бы сделать OpenNFS. Но для начала нужно хотя бы работу найти, а то кушать нечего :)


      1. perfect_genius
        17.02.2018 18:02

        А если для OpenNFS откроете страницу на Патреоне? Думаю, фанаты поддержали бы.


        1. Marui
          17.02.2018 18:05

          Фанаты будут давать 2+к долларов в месяц? Или они будут код писать?


          1. perfect_genius
            17.02.2018 18:11

            Материально поддерживать, ему ж кушать нечего.


          1. khim
            17.02.2018 19:06

            Получить 2+K долларов в месяц можно на Патреоне, но, в общем, не так-то просто. Не уверен, что NFS привлечёт столько желающих, хотя чем чёрт не шутит…


        1. GrimMaple Автор
          17.02.2018 18:12

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


          1. perfect_genius
            17.02.2018 18:51

            Вот сколько потенциальных фанатов:

            Заголовок спойлера
            image


            1. GrimMaple Автор
              17.02.2018 18:56

              Нужно будет всё взвесить и продумать. В конце концов, попытаться всегда можно, а получится/не получится — это уже не так уж и важно.

              Спасибо за идею :)


              1. khim
                17.02.2018 19:09

                Идея хорошая, вот только… привлекать народ на Патреон — та ещё работёнка. Если просто страничку создать и никак её не раскручивать — привлечёте $10 в месяц. А если раскручивать — останется ли время на то, чтобы код писать?


                1. perfect_genius
                  19.02.2018 10:19

                  Т.е. создать темы в группах в ВК, FB, Reddit и ModDB будет маловато?


                  1. khim
                    19.02.2018 14:43

                    Нужно регулярно поставлять какой-нибудь контент, который будет людей привлекать. Если вы книжку пишите или что-то подобное, то это легко… а вот если игру…

                    Хотя можно какие-то наработки выкладывать, полуфабрикаты…


      1. AllexIn
        17.02.2018 18:54

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


      1. VEG
        17.02.2018 19:27

        Уже существует OpenNFS — попытка сделать открытый движок для The Need For Speed. К сожалению, до финиша проект так и не доведён (например, вообще нет обработки коллизий).

        Также можно вспомнить про Cry for Speed (и ещё вот) — попытка сделать ремейк Need For Speed III. К сожалению, проект совсем заглох, хотя и трудилось над ним несколько человек.


        1. GrimMaple Автор
          17.02.2018 20:10

          У OpenAything помимо проблем с временем и мотивацией есть куча других проблем, например проблем с легальностью. И в случае с EA нет совершенно никакой возможности предсказать, как они отреагируют на появление такого проекта. Могут забить, могут просто пригрозить, а могут и в суд побежать. Поэтому я точно не стал бы делать нечто подобное, не получив от EA чёткий ответ: «Нам все равно, парень, делай на здоровье, только не распространяй наши ресурсы»