Игра «угадай аниме по кадру» – защита от читерства

в 9:14, , рубрики: javascript, php, аниме, Веб-разработка, информационная безопасность, Песочница, метки: , ,

Наверное, многие участвовали в играх, где нужно было угадать фильм или сериал по кадру из него. Очень часто такие топики-игры встречаются на форумах. А недавно мне показали сайт, целиком посвящённый угадыванию аниме по кадру. С автоматизированной игрой: с учётом очков, с вариантами ответов.

После нескольких игр мой интерес как анимешника был удовлетворён, и во мне проснулся айтишник. Стало интересно, а какая есть на сайте защита от читерства. В ходе короткого анализа, оказалось, что с защитой от читерства всё печально, взломать игру можно даже с начальными знаниями JS. Тут бы и погаснуть моему айтишному интересу, но нет, я задумался, а что нужно вообще сделать, чтобы защитить от читеров подобную игру. Так и появилась эта статья.
Игра «угадай аниме по кадру» – защита от читерства
(картинка для привлечения внимания)

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

Суть игры

На каждом шаге игроку демонстрируется кадр из аниме и 4 варианта ответа, из которых 1 правильный. Игра идёт до первого неверно угаданного кадра. На каждый кадр игроку даётся 15 секунд. Также по одному разу можно пропустить кадр или воспользоваться подсказкой «50 на 50».

Технические детали и «взлом» игры

Посмотрев на урлы страниц сайта, я легко обнаружил, что он работает на PHP. А тот факт, что игра проходила без перезагрузки страницы, говорил об использовании AJAX. Клиентский функционал находился в единственном подключённом скрипте game.js.

Первое, что меня разочаровало в безопасности игры – то, что game.js не обфусцирован. Там удалены лишние пробелы, но, не считая этого, код находится в первозданном виде.

Но всё было ещё печальнее. Запустив Firebug и посмотрев содержимое ответов AJAX, я обнаружил один очень интересный параметр.
Игра «угадай аниме по кадру» – защита от читерства
Да, этот параметр содержал не что иное, как индекс верного ответа, от 0 до 3. Я никак не ожидал, что всё будет так просто, что даже не интересно. Надежды, что надо будет подключать Google Images для отгадывания, рухнули.

Дальше было дело техники. Первым делом я пропустил код из game.js через JS Beautifier, получив читаемый код. Код оказался довольно простым и без всякой защиты. Ответы AJAX обрабатывались в функции startGame. Оставалось всего лишь переопределить эту функцию. Для этого я подключил небольшой greasemonkey-скрипт.

В скрипте я просто по полученному из XML номеру ответа подсвечивал нужную кнопку. Впоследствии сделал автоматическое нажатие её через 2 секунды. Код приводить не буду, чтобы не потакать читерам, пусть сами хоть чуть-чуть подумают при желании. Там буквально 3 строчки. Но я был удивлён, что при такой защите в топе не засветился никто с максимальным количеством очков.

Слабые места игры и защита от читерства

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

Первым, что я рассмотрю, будет защита на клиентской стороне. Игра имеет два ограничения в JS: она автоматом завершается, если истекло время на угадывание кадра, или в случае переключения на другое окно. Мне не совсем понятно, зачем запрещать переключение на другое окно, тогда как время и так ограничено. Только если чтобы игрок быстро не загуглил кадр. Но так решили создатели сайта.

Защита от переключения на другое окно.
Слабое место её в том, что JS полностью подконтролен клиенту. Полностью решить проблему нельзя, можно лишь затруднить злоумышленнику взлом. Чтобы хоть как-то защитить игру от изменений JS, его, конечно же, нужно обфусцировать. Совсем хорошо бы было генерировать и обфусцировать js-файл на лету при каждой загрузке страницы, помещая туда разные функции вычисления некого токена, который бы проверялся через AJAX. Так злоумышленнику было бы сложно подменить JS. Сложно, да не совсем.

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

var onblur_handler = function() {
    alert('game over');
};
window.onblur = onblur_handler;

var check_onblur = function() {
	if (window.onblur !== onblur_handler) {
		alert('Попался, читер! Мы вычислили тебя по IP. Братва уже выехала!');
		window.onblur = onblur_handler;
	}
}

setInterval(check_onblur, 2000);

// evil code
setTimeout(function() {
	window.onblur = function() {
		console.log('cheating');
	};
}, 2500);

Злоумышленник, конечно, может вообще не выполнять наш JS, но если он будет обфусцирован, да ещё содержать какие-нибудь токены для AJAX-запроса, это доставит хлопот.

Таким образом, получается, что наше приложение должно будет посылать на сервер номер ответа и токен, полученный из JS; а принимать с сервера ссылку на следующее изображение и варианты ответа.

Ограничение по времени.
С ограничением по времени можно поступить аналогично, оставив защиту на стороне клиента. Но можно и подстраховаться, проверяя время и на сервере. Но стоит учесть, что картинка отображается у клиента не мгновенно, и после нажатия на кнопку запрос на сервер приходит тоже не сразу. Поэтому нужно дать клиенту некоторую фору по сравнению с сервером. Идеально было бы анализировать «отставание» клиента, и принимать меры, только если оно значительно.

Повторение кадров.
Эта проблема относится не совсем к безопасности, но я её тоже рассмотрю. Суть в том, что раз кадры выбираются случайно, то в рамках одной игры два кадра могут совпасть. Это не правильно. Решить проблему можно довольно просто. Учитывая, что общее количество кадров не очень велико, можно при старте игры загружать все кадры (а точнее их идентификаторы) в массив, сортировать его случайным образом, сохранять массив в сессию, и по очереди выдавать кадры игроку. В PHP для сортировки в случайном порядке как раз есть функция.

shuffle($screenshots);

Ссылки на скриншоты.
Ещё одно слабое место игры – ссылки на скриншоты. На сайте игры все они имеют формат вида /web/ANIME_ID/SCREENSHOT_INDEX.jpg, то есть ответ засвечивается уже в адресе картинки. Поэтому не стоит вообще отдавать прямые постоянные ссылки на скриншоты. Можно отдавать скриншот средствами PHP всегда по одному и тому же адресу. Просто перед каждой новой загадкой нужно сохранять в сессию идентификатор выбранного кадра и отправлять этот кадр при обращении к определённому php-скрипту.

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

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

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

Самое простое – случайно менять качество JPEG, передаваемого клиенту.

function open_screenshot($filename)
{
	return imagecreatefromjpeg($filename);
}

function echo_screenshot($image)
{
	$quality = rand(60, 100);
	
	ob_start();
	imagejpeg($image, null, $quality);
	$buffer = ob_get_clean();
	
	header('Content-Type: image/jpeg');
	header('Content-Length: '.strlen($buffer));
	echo $buffer;
}

$image = open_screenshot(dirname(__FILE__).'/1.jpg');
echo_screenshot($image);

Если этого недостаточно, то можно поиграть с яркостью и контрастностью или другими фильтрами.

function filter_image($image)
{
	$brightness = rand(-10, 10);
	$contrast = rand(-10, 10);
	
	imagefilter($image, IMG_FILTER_BRIGHTNESS, $brightness);
	imagefilter($image, IMG_FILTER_CONTRAST, $contrast);
	
	return $image;
}

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

function cut_image($image)
{
	$k = rand(98000, 100000) * 0.00001;
	
	$w1 = imagesx($image);
	$h1 = imagesy($image);
	$w2 = round($k * $w1);
	$h2 = round($k * $h1);
	
	$x = rand(0, $w1 - $w2);
	$y = rand(0, $h1 - $h2);
	
	$result = imagecreatetruecolor($w1, $h1);
	imagecopyresampled ($result, $image, 0, 0, $x, $y, $w1, $h1, $w2, $h2);
	return $result;
}

Теперь, если кадров в игре достаточно много, задача сравнения станет не такой уж тривиальной. Хотя если в распоряжении злоумышленника есть алгоритмы сравнения как в Google Images, то и это не будет помехой. А также эффект от обрезания меньше, если сравнивать уменьшенные версии кадров.
Игра «угадай аниме по кадру» – защита от читерства
Картинка состоит из 6 вариантов кадра, полученных вышеприведённым способом. Как видите, везде разные значения яркости и контрастности. Если приглядеться, можно обнаружить, что в некоторых кадрах изображение обрезано по краям больше, чем в других.

Кстати, от банального гугленья скриншота я защиты не придумал. Разве что искажать его ещё сильнее, но это может повлиять и на узнаваемость человеком.

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

Заключение

Заниматься читерством на сайте я всё-таки пока не стал даже из айтишного интереса, потому что взлом защиты подобного уровня – не большой повод похвастаться.

На самом деле, отсутствие защиты от читерства – тоже защита. Читерство ведь в данном случае ничего не даёт, а лёгкость нечестной игры убивает всякий интерес в нём. Конечно, если сайт хотел бы поддерживать реальные рейтинги, то ему стоило бы позаботиться о защите.

Кстати, хочу заметить, что кое-что описанное в данной статье можно применить не только к такого рода играм, а к чему-то более серьёзному. Например, онлайн-тестам.

P.S.

Адрес сайта я до сих пор не писал, чтобы не было похоже на рекламу. Но играть с вами в «угадай сайт по скриншоту» я не буду. Сайт, о котором идёт речь, – guessanime.com.
Я никакого отношения к сайту не имею. Если статья как-то заденет владельцев, мне очень жаль.

Автор: Rick1906

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js