В прошлом месяце OnePlus представила рекламную акцию для своего будущего OnePlus 6T. Предпосылка была проста: коснитесь или щелкните экран, чтобы разблокировать загадочные призы. Чем больше вы нажмете, тем лучше будет приз. После клика до первого приза, всего 60 кликов, я решил попробовать автоматизировать процесс.

Если вы зайдете на сайт игры со своего рабочего стола, вы получите сообщение с просьбой открыть его на мобильном устройстве. К счастью, достаточно просто симулировать мобильное устройство с помощью инструментов разработчика Google Chrome.

xdotool

Щелкнуть несколько тысяч раз с помощью такого инструмента, как AutoHotkey, было бы легко. К сожалению, AutoHotKey недоступен в Linux, но есть альтернатива: xdotool. С помощью нескольких быстрых поисков в Google я смог настроить этот скрипт bash, чтобы он начал кликать за меня. За несколько часов я набрал до 60 000 кликов.

Следующей вехой было 300 000 кликов. Я хотел попробовать, чтобы увидеть, какие призы стоят за таким огромным количеством кликов. К сожалению, у метода xdotool было несколько недостатков:

  • Я больше не мог пользоваться своим компьютером
  • Это было относительно медленно
  • Браузер иногда зависал (вероятно, из-за того, что инструменты разработки хранили много информации)

Веб-сокеты

При проверке сетевых запросов я обнаружил, что игра не использует обычные HTTP-запросы, а отправляет всю информацию через вебсокеты. У меня был опыт имитации HTTP-запросов, но я не знал, как имитировать фреймы веб-сокетов.

Во-первых, я попытался смоделировать щелчок по экрану, используя старый добрый javascript. Взяв код из этого вопроса stackoverflow, я смог отправить событие click, но по какой-то причине счетчик не увеличивался. Озадаченный, я включил отладчик JS непосредственно перед запуском события click. Где-то в мешанине минифицированного JS скрипт проверял легитимность события клика с помощью свойства isTrusted. По сути, isTrusted — это функция безопасности в браузерах, позволяющая определить, был ли щелчок сгенерирован пользователем или javascript. Я не нашел простого способа обойти это.

Когда я копался в JavaScript, мне пришла в голову идея обойти обработчик событий и напрямую выполнить код, который отправлял сообщение в веб-сокет. Из-за минимизированного кода и моего незнания веб-сокетов мне не удалось найти точный код, который отправлял кадр.

Затем я попытался найти объект Websocket в памяти, чтобы самостоятельно отправлять кадры, используя его. Мне не повезло найти объект в памяти. Что, если я просто создам новое соединение?

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

Рамка аутентификации*:

420[“authenticate user”,{“username”:”raybb”,”token”:”ff45fb994e3124a5e0ce7bf7f27745dcbe62e71275532f982498b798",”squad”:””,”locale”:”en-gb”,”count”:62301,”csrf”:”0xsn5yq9SAb65OvTXDl3OjzRoVPgr1zVvDtzoVR4ZhjdptbHClRSVaSJDXKwXy8b”}]

Примеры кадров (1-й, 2-й и 10-й)*:

421[“tap_logs”,{“username”:”raybb”,”timestamp”:1540163213460,”touchX”:19,”touchY”:78}]
422[“tap_logs”,{“username”:”raybb”,”timestamp”:1540163297196,”touchX”:389,”touchY”:737}]
4200[“tap_logs”,{“username”:”raybb”,”timestamp”:1540163347125,”touchX”:306,”touchY”:442}]

*данные кадры имеют измененные значения по соображениям конфиденциальности.

В результате экспериментов я обнаружил:

  • token и csrf: не менялись между обновлениями
  • count: количество нажатий при загрузке страницы (изменение не повлияло)
  • Фрейм авторизации всегда начинался с 420 (который мы будем называть счетчиком фреймов).
  • Счетчик кадров в основном увеличился на 1
  • Крайние левые цифры счетчика кадров никогда не достигали 43. Например: [420, 421, 422, ..., 429, 4200, 4201, ..., 4299, 42000, ...]

Имея это в виду, я сделал свою первую попытку смоделировать щелчок с помощью веб-сокета:

const socket = new WebSocket('wss://socket-unlock.oneplus.com/socket.io/?EIO=3&transport=websocket');
socket.addEventListener('open', function(event {
   console.log("opened");
   socket.send(authFrame);
   socket.send(firstTap);
  });

Он успешно подключится, но первое нажатие не отправит 🤔

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

const socket = new WebSocket('wss://socket-unlock.oneplus.com/socket.io/?EIO=3&transport=websocket');
socket.addEventListener('open', function(event) {
 console.log("opened");
 socket.send(authFrame);
});
socket.addEventListener('message', function(event) {
 if (event.data.indexOf("raybb") > -1) {
  if (event.data.indexOf("430") > -1) {
   socket.send(firstTap);
  }
 }
});

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

Единственная проблема с этим методом заключалась в том, что отправка нажатий в конечном итоге замедлялась. После отправки нескольких тысяч нажатий скрипт станет заметно медленнее и останется на этой скорости. Я подозреваю, что это было связано с рекурсивным характером сценария, который требует создания множества слоев переменных с областью видимости. Я изменил код, чтобы отправить 100 нажатий на каждом уровне рекурсии. Это, казалось, немного помогло, но замедление все еще было заметным. Затем я заблокировал загрузку всех скриптов на веб-сайте OnePlus (по сути, это была просто пустая страница). Это тоже немного помогло.

Я так и не нашел решения для скрипта замедления. Конкурс подходил к концу слишком долго. Это не было большой проблемой, и, прежде чем я это понял, я был на пути к 1 миллиону нажатий. Возможно, я не выиграл никаких призов от этого, но я узнал о событиях кликов, веб-сокетах, xdotool и немного больше о просмотре сайтов с помощью отладчика.

Спасибо за прочтение 🤓👏