Было это 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 был сразу заменен на функцию, встроенную в движок. Ее не было бы, если бы интерпретатор начинал панику, наткнувшись на неизвестный опкод.
Автор: adic3x