История и описание уже исправленной уязвимости в игре WarCraft 3

Было это 2009 году, в марте месяце, я тогда увлекался созданием карт к игре WarCraft 3. Однажды мне показали карту, которая при запуске игры каким-то хитрым образом создавала консоль и писала в ней какой-то «Привет Мир!». Я мягко говоря был ошарашен — это значило, что есть возможность выполнять произвольный код в системе.

Под катом описание уязвимости и история ее закрытия.

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

Return Bug

Скрипты в WarCraft 3 пишутся на разработанном Blizzard недоязыке JASS (Just Another Script System). В целом он достаточно гибок (вся стандартная кампания сделана на визуальной надстройке этого языка), но беда: нет структур, и стандартный Sleep отсчитывает время даже когда игра стоит на паузе, да и еще с пониженной точностью, одним словом сделать плавное движение юнита например, перемещая его каждые .03 секунды невозможно. Зато есть таймеры, которые можно запускать, и которые достаточно точны, и по окончанию отсчета которых может быть вызвана указанная в аргументе callback функция. Но они тоже проблемы не решают: например, способность, которая бы делает отсроченно некоторые действия не будет работать корректно, если ее использовать дважды: в функцию, которая вызывается по окончанию отсчета таймера невозможно передать какие либо аргументы, и поэтому невозможно выяснить, какой именно экземпляр способности эта функция должна обработать.

И тут был обнаружен Return Bug (это было еще давно, думаю в 2005 году, а может и раньше. Этот момент я не застал). JASS — язык со строгой типизацией, но этот баг позволял ее обойти. Выглядело это так:

function RB takes unit u return integer
  return u  // тут бы выдать ошибку, что функция должна вернуть число,
            // а она возвращает юнита
  return 0  // игра проверяла только последнюю инструкцию возврата
endfunction


Но ошибка не выдавалась — судя по всему в отдельное поле записывался тип возвращаемого значения, но при следующей инструкции return это поле перезаписывалось опять. В результате выполнения данного кода функция вернет некоторое число, которое будет являться внутриигровым дескриптором переданного функции юнита. И для каждого юнита это число будет уникально.

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

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

Принцип работы интерпретатора JASS

На основе скрипта карты при загрузке игра создает псевдокод, в чем-то напоминающий ассемблер. Игра использует 8 байтовые опкоды, где первые 4 байта указывают код операции и опционально виртуальный регистр (их 256), а вторые 4 байта — аргумент, например при загрузке значения в регистр там будет непосредственно само значение. ИД регистров для каждой следующей по коду операции увеличиваются, например foo = 0; bar = 2; — для присвоения значения foo будет использован регистр #00, а для bar #01, после использования регистра #FF будет снова использован #00. Точно также, индексы глобальных переменных тоже увеличиваются, и идут по порядку — первая объявленная переменная будет иметь индекс n, следующая n+1.

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

Собственно уязвимость

Есть в JASS такой тип, как code, это — относительный указатель на функцию, он используется, что-бы передать callback функции, например по использованию предмета вызвать такую-то функцию. С ним по идее нельзя делать никакие математические операции, и задается он только литералом, например code c = function foo. И он не указывает прямо на память, где расположен псевдокод, а лишь задает отступ от начала всего обработонного псевдокода. И тут на помощь приходит return bug:

function StubFunc takes nothing returns nothing
endfunction

function I2C takes integer ic returns code
  return ic
  return (function StubFunc) // Просто пустая функция, нужна для проверки синтаксиса.
endfunction

function C2I takes code c returns integer
  return c
  return 0
endfunction


Такой код вернет относительный адрес первого опкода передаваемой функции в виде числа.

function HackArrayW takes nothing returns nothing
  local code ctcode = I2C(C2I(function zOmgFunc2) + 1)
  // ...


А вот так можно вызвать функцию, точнее не ее саму, а ее со смещением. Далее, путем подбора за счет объявления переменных по порядку и написания бессмысленных операций присвоения можно заставить игру генерировать свой, особенный псевдокод. Аналогичный метод кстати используется для антиотладки, когда в x86 ассемблере пишется инструкция вызова, а в качестве адреса указывается другая инструкция, и переход совершается на нее. В JASS заметно облегчает эту задачу то, что интерпретатор при встрече с незнакомой инструкцией… просто перескакивает на следующую! Так вот, благодаря такому коду мы можем присвоить значение обычной переменной переменной массива и наоборот.

Известно, что игра использует одну библиотеку, которая не обновляется с патчами — Storm.dll. В ее адресном пространстве можно найти данные, адрес которых присвоить переменной массива, и выйдет, что мы получим массив, данные которого будут находиться не в специально выделенном месте, а прямо в исполняемом коде процесса. Нужно, что бы поле размера было больше, и не было произведено realloc при попытке записать что-либо по высоким адресам, а второе поле указывало на адрес, по которому мы собираемся писать. В результате появляется возможность писать и читать память процесса. Простая запись в такой массив:

function Inj_PrepareInjector takes nothing returns nothing
  set zg0oI[0x200]=0xE8575653
  set zg0oI[0x201]=0x000000F3
  set zg0oI[0x202]=0x9F2DC88B
  set zg0oI[0x203]=0xFF000006
  set zg0oI[0x204]=0x72656BE0
  // ...


Будет писать в память процесса по нужному адресу. Дальше же дело техники… Автор уязвимости находил стек, и писал что-то в него, что позволяло перехватить управление. Если я правильно помню в архив карты можно было засунуть и свою *.dll, но код можно было зашифровать и в скрипте.

История

Некоторое время мы обсуждали, как же можно использовать находку. Помещать вирусы в карты показалось неблагородным. Разрабатывать библиотеку для расширения функционала игры не хотелось по двум причинам: возможность того, что уязвимость прикроют и возможность злоупотребления (опять же кто-то разберется как оно работает и начнет писать вирусы). Не найдя применения карта была показана еще некоторым людям. И дошла до Blizzard. И тут начался цирк — они так и не смогли понять, как уязвимость работает. Сначала они техническими средствами запретили хостить в Battle.net карты, в которых были функции, возвращающие тип code. Это решало проблему, но урезало функциональность языка.

Я взялся писать свой патч, в результате появилась маленькая программа, которая копировала одну из библиотек игры, и перезаписывала в ней… 5 байтов. Да, это было полное спасение. А сделал я (адрес мне подсказал разработчик уязвимости) перехват обработки типа code. Игра постоянно использует преобразование относительного игрового адреса опкода (тот, который можно получить с помощью Return Bug) в реальный адрес, а я просто напросто добавил операцию and 0xfffffff8, попросту не давая выполнить некорректный псевдокод. Я выложил его на форумах, посвященных созданию карт для WarCraft 3. И уехал на дачу, где интернета у меня не было. Понимаю, поступил я безответственно ;)

Пытались ли кто-либо связаться с разработчиками игры, не чуть позже они выпустили патч 1.24, в котором полностью запретили Return Bug и добавили легальный его аналог, который правда не позволял обратные преобразования — из числа получить объект теперь стало невозможно. Как результат часть карт, которые были написаны на JASS толково — быстро заменили функции и стали работать корректно, другая же часть, которая использовала обратные преобразования из числа в объект поломалась. Карты же, созданные на визуальной надстройке вообще никак не среагировали на это событие. В целом Blizzard выбрали не самый оптимальный вариант, да и к тому же поломали совместимость.

Заключение

Что я хочу сказать напоследок?

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

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

И самое главное: всегда, всегда, всегда проверять входные данные. Уязвимости бы не было, если бы return bug был сразу заменен на функцию, встроенную в движок. Ее не было бы, если бы интерпретатор начинал панику, наткнувшись на неизвестный опкод.
Метки:
Поделиться публикацией
Комментарии 20
  • +2
    автор уязвимости ранее имел негативный опыт общения с техподдержкой Blizzard, которая ему для решения всех проблем предлагала переустановить игру

    Странно, у Близзардов самый клёвый саппорт, который я только встречал.
    На первое письмо о помощи отвечали ссылками на ФАКи, конечно, но потом уже сотрудники пытались действительно разобраться в проблеме и всегда помогали.
    • +2
      Мне как-то раз три раза подряд робот отвечал. Я забил.

      Раз на раз не приходится.
      • 0
        Ну раз уж разговор об этом зашел… мне бот ответил 5 раз подряд. Каждый раз я писал «Прошу ответить без ботов» и дополнял вопрос. Итог: каждый раз ответ не в контексте проблемы, ибо боты анализировать всю переписку не умеют.
      • +3
        Могу сказать обратное, сплошные шаблонные отписки, как будто сообщение не читают, а просмартивают на кейворды.
        Хуже сапорт только разве у гугла, которые просто игнорят.
        • 0
          А разве саппорт игнорящий не равно отсутствию саппорта как такового? :)
          • 0
            Даже хуже, чем отсутствие, т.к. некоторые действия основаны на его наличии.
          • +1
            У Google есть саппорт?!
            • +2
              Ходят слухи, что при подключении Adwords шанс на саппорт увеличивается примерно в 100 раз. Аж до 0.01%. Слухи непроверенные.
          • 0
            обычный суппорт, в стиле «у нас все работает, проблемы на вашей стороне»

            Мне что-то подобное ответили на мои проблемы с авторизацией в вовке, когда поменялся мой IP адрес на другого провайдера. Система тупо банила меня и предлагала сменить пароль под предлогом, что обнаружена попытка несанкционированного доступа. После пяти смен пароля и чтения всех ФАКов на сайте, написал в суппорт. Через сутки все заработало само, а мне пришло письмо — что у них все хорошо =(
            • 0
              У меня последнее с ними общение было по поводу беты СК2. Клиент не хотел обновляться.
              Сначала ответили ссылками, но я написал что далеко не идиот, и все инструкции прочитал, как и гугление похожих проблем ничего не дало.
              Тогда уже ответил живой человек, и начали вместе искать причины проблемы.
              Оказалось, что клиент берет настройки сети из IE, а у меня там был когда-то давно вбит дохлый прокси.
              Кстати, в инструкции потом этот нюанс они так и не добавили, вроде. )
            • +4
              Чтобы восстановить аккаунт нужно писать в сапорт с рабочего аккаунта… И это многое объясняет… =)
              • 0
                А другие способы есть? Сам недавно столкнулся с этой проблемой, но не нашел решения и забил.
                • 0
                  Как уже тут писалось: по телефону.
              • 0
                Как показала практика, проще звонить +)
              • 0
                автор уязвимости

                Тот, кто ее допустил? :)
                • 0
                  А можете пояснить код функции Inj_PrepareInjector? Почему массив называется zg0oI, почему начинают записывать со смещения 0x200. Есть ли смысл у записываемых данных?
                  • +2
                    Куски кода — копипаста из оригинальной карты, и названия придумывал не я. Скорее всего это делалось для обфускации — редактор WarCraft использует достаточно убогие шрифты. А может просто для хакерского лоска.

                    Почему индекс 0x0200? Ведь это отступ в памяти. В другом месте код пишется по смещению 0x00. Насколько я помню, в данный момент массивы ссылаются на стек, поэтому возможно отступ в 0x0200 выбран просто как безопасный, не затрагивающий важные данные. Сейчас подыму историю переписки:

                    struct JassVArray
                    {
                      int *VFuncs;
                      int ListSize;
                      int NItems;
                      int *Items; // Это поле является указателем на данные массива
                      int BlockSize;
                    }
                    


                    Так вот, используя операцию присвоения значению переменной массива, которая является указателем на подобную структуру, произвольного значения, содержащегося в обычной переменной (на самом деле совсем не произвольного) мы получаем массив, Items которого ссылается на нужный нам адрес в адресном пространстве процесса, в данном случае это стек одной из веточек. Откуда можно легко перехватить управление. Теперь при записи в массив игра будет на самом деле писать в код процесса. Соответственно, при записи в массив foo[n], данные будут записаны по адресу Items+n.

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

                    Данные, показанне в примере — очень хорошие, это код. Только надо свапнуть их задом наперед (я делаю это инстинктивно):

                      set zg0oI[0x200]=0xE8575653
                      set zg0oI[0x201]=0x000000F3
                    
                    //...
                    
                    CPU Disasm
                    Hex dump          Command                                  Comments
                    53              PUSH    EBX      ; сохраняем регистры
                    56              PUSH    ESI
                    57              PUSH    EDI
                    E8 F8000000     CALL    0056C100 ; Вызов. Адрес равен адресу начала кода + 0x0100
                      
                    


                    • 0
                      Спасибо большое. Этого не хватало в статье.
                  • 0
                    да да, помню этот патч
                    • 0
                      Не забывай, адик, что в 1.24 обратные преобразования тоже реальность.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.