Как писать ботов на websocket
https://t.me/iam_limonov
Вступление
Вы никогда не задумывались, как пишутся боты под игры?
Нет, не обычные кликеры, которые кликают мышкой по интерфейсу игры.
Проблема таких ботов множество: их тяжело дебажить, нужно сильное железо для запуска нескольких копий, а также под большинство задач обычный кликер не подойдет.
Все эти проблемы решают сетевые боты, как post/get в ботах на запросах.
Однако в играх используются websockets.
В этой статье я расскажу свой опыт написания бота под браузерную игру CryptoMayor.
Статья поможет разобраться с основами вебсокетов, а также понять, в какую сторону копать, чтобы сделать бота под нужный вам проект.
Если вы не знаете что такое post/get запросы, то эта статья может показаться сложной. Cначала изучите основы.
P.s Это мой первый бот на вебсокетах. Возможно, я многого не знаю, но предполагаю суть одна:)
Великая База
- Что такое вебсокеты?
WebSocket — протокол связи поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб-сервером, используя постоянное соединение.
Самое важное в Websocket — это постоянное соединение.
Например в post/get запросах вы просто отправляете запрос и сразу же получаете ответ.
В WebSocket соединение открывается один раз, и в него уже можно посылать запросы любое количество раз (либо вообще не посылать). Также мы можем получить ответ от сервера в любой момент прочитав сокет.
Ответ придет в зависимости от того, когда сервер захочет его отправить. (например, если произошло какое-то событие, по типу вам отправили сообщение или вы подключились к вебсокету)
- Вебсокеты в Python
Для работы с вебсокетами мой выбор пал на библиотеку aiohttp.
Я нашел код на github для регистрации рефералов в CryptoMayor. (спс @jero1n)
Хотя есть еще и библиотека websockets и другие, я выбрал aiohttp не только потому что можно сделать Ctrl + C / Ctrl + v.
В aiohttp как мне показалось проще всего взаимодействовать с прокси без костылей.
Так же в связки с этим нам понадобится asyncio, т.к aiohttp - асинхронная библиотека. Вебсокеты в целом асинхронные.
Если вы не знакомы с asyncio, то вот обзоры: статья или видос.
async with aiohttp.ClientSession() as session: #url укажем позже async with session.ws_connect(url, proxy=proxy_url) as ws:
Дальше переменная ws и будет нашим подключением.
Мы можем получить ответ от вебсокета используя receive:
response = await ws.receive()
если сервер не пошлет ответ, receive просто будет ждать его, а если пошлет несколько их можно вывести в цикле по очереди.
Мы можем обработать ответ так:
if response.type == aiohttp.WSMsgType.BINARY return response.data elif response.type == aiohttp.WSMsgType.TEXT: return response.data else: raise Exception("Received non-binary/text message")
В aiohttp.WSMsgType, например при ошибки соединения может быть CLOSED и тогда мы выкинем ошибку.
await ws.send_bytes(data)
Аналогично есть методы: send_str()
, send_json().
- Снифаем трафик и пишем бота
После того как мы разобрались в том как создавать вебсокет соединение посылать или получать ответ, остается один вопрос.
К чему собственно подключаться и какие запросы слать?
Для начала я хотел чтобы бот совершал авторизацию в аккаунт.
Для этого нам и нужно отснифать трафик, то-есть понять какие запросы шлет игра чтобы повторить их.
Это точно также как и с post/get, запросы можно посмотреть прямо в браузере через devTools, а можно в специальных программах: fiddler, burp suite и пр.
Всем рекомендую burp suite, в нем очень удобный снифер трафика, кроме того можно перехватывать и подменять данные в запросах, воcпроизводить запросы и видеть действие этого запроса на клиенте.
Я покажу как это делается через devTools, и так открываю браузер, захожу и авторизуюсь в игре. Если перейти в Network -> ws, мы увидим это:
Первым делом мне надо понять к какому url создавать подключение.
Что бы посмотреть url, нам надо перейти в Headers, посмотрев запросы я нашел это:
wss://gamews.cryptomayor.net/game/coins?user_id=0x875154b52a631ac22ce0c1d32fb2ea3db10aacd8&s=0x95916c4d255f65d79f47c75cff179b6d0986745075ad5a3555bcd0b31d9f385c26a4e907e6faf70dd358cd1d6b861524a1b6b72ed30d3d3fc1bf74ab67887a461b&sd=cm-1704060629873
Там нужный протокол и домен, очевидно что с ним и создается вебсокет соединение клиентом игры и сервером.
Вы можете заметить, что в GET-параметры еще и передается параметр s, это сигнатура кошелька, я не буду подробно останавливаться над этим, т.к это статья больше о боте в целом, но если вам интересно как генерить сигнатуру кошелька для авторизации web3 можете глянуть код.
Я не много был удивлен что в url подключения могут еще и передаваться get запрос, хотя это логично.
По аналогии выше мы можем создать соединение с вебсокетом, только уже указать нужный нам url.
Теперь вернемся в Messages и видим обмен сообщений между клиентом (браузером) и сервером.
Зеленое это то что клиент посылает на сервер (send), а красное - то, что сервер шлет на клиент (recive).
Несколько раз авторизовавшись, я заметил, что сначала всегда клиент шлет серверу 3 запроса серверу (зеленые сообщения).
Я предположил, что они как раз и отвечают за полную авторизация (мы уже частично авторизовались, подписав сигнатуру и подключившись к вебсокету).
При клике на сообщение мы можем увидеть передаваемые данные.
Вам может повезти и в вашем проекте будет формат json, но это маловероятно, т.к большинство проектов сжимает/шифрует данные они передаются в байтах.
Почему тут hex, что за байты, я расскажу более детально в "Реверс" ниже.
Теперь если мы конвертируем этот hex в byte, получим данные, которые можно отправить, они валидно принимаются сервером.
(либо можно кликнуть правой кнопкой по message -> copy as base64 и декодировав его через python мы получим нужные байты).
Например байты второго сообщения выглядят так: b'\x08\xe8\x07'
.
Зная это все мы можем попробовать сделать авторизацию:
url = 'wss://gamews.cryptomayor.net/game/coins?...тут get параметры'
async with aiohttp.ClientSession() as session: async with session.ws_connect(url) as ws: #Шлем данные на сервер await self.send_bytes(b'\x08\x00') await self.send_bytes(b'\x08\xe8\x07') await self.send_bytes(b'\x08\x87\x10') #получаем ответ response = await ws.receive()
Теперь если вывести response нам приходит такие же байты как те байты в браузере когда авторизация проходит успешно. Бот успешно авторизовался!
По этой же логике можно отснифать байты например, для отправки сообщения и если послать эти байты серверу через send_bytes, бот отправит сообщения с аккаунта.
Таким образом, можно автоматизировать любые действия в игре.
Ужасный Реверс
- Зачем это нужно?
Как вы заметили мы шлем непонятные байты по типу: '\x08\xe8\x07'
.
Да, можно вслепую их отсылать, и это даже может работать. Однако ответ от сервера приходит тоже в непонятных байтах, как нам например получить информацию о своем аккаунте, где привычный, родной json?
Что если нам надо отправить какие-то уникальные данные, по типу адреса кошелька, непонятные данные и есть закодированный json, как мы их засунем в байты?
Большинство проектов кодируют данные для оптимизации, либо шифруют, поэтому они в таком странном виде байт.
Тут все зависит от проекта, но что можно сказать точно вам придется реверсить (разбирать) код игры, чтобы найти методы которые кодируют/декодируют данные.
Дальше вам нужно понять как они работают и написать аналогичную функцию, которой вы будете декодировать/кодировать данные.
Клиент как-то принимает данные, а значит в нем 99% должны быть функции их декодировки/кодировки, а к клиенту у вас всегда есть доступ.
Тут мы разбираем браузерную игру.
Но также у вас может быть приложение под пк или телефон на вебсокетах, рекомендую прочитать статью, там есть упоминание способов реверса под пк. Под android из единственного что я знаю это книга "Android Глазами хакера", там есть база с которой можно начать.
- Способ отправить данные без реверса
Все таки есть способы отправить данные и вас это может спасти от долгого реверса.
Допустим, нам нужно сделать авторегер рефералов. На примере с CryptoMayor при подключению к url игра шлет адрес кошелька реферала в качестве реф кода.
*Я захожу в игру и регистрируюсь по реф ссылке:https://play.cryptomayor.xyz/...inviteUid=0x875154b52a631ac22ce0c1d32fb2ea3db10aacd8
*
В этот раз игра уже шлет 4 запроса (вместо 3-х как при авторизации).
Если hex декодировать через python (как это было выше).
Получим такие байты: b'\x08\xeb\x0f\x12,\n*0x875154b52a631ac22ce0c1d32fb2ea3db10aacd8'
Тут мы явно видим адрес "875154b52a631ac22ce0c1d32fb2ea3db10aacd8"
, да это тот самый адрес реферала.
Мы можем изменить этот адрес на любой нужный и послать его на сервер.
Единственное любые данные также нужно кодировать в байты при помощи .encode('utf-8').
address = '875154b52a631ac22ce0c1d32fb2ea3db10aacd8' data = b'\x08\xeb\x0f\x12,\n*'+address.encode('utf-8') await ws.send_bytes(data)
Сервер примет такие данные и зарегистрирует аккаунт на нужный реф код.
Таким образом если вам надо сделать что-то простое по типу регера рефов этот способ может сократить вам уйму времени.
Способ работает не всегда, например если вам нужно получить данные о аккаунте игрока.
Так выглядят данные о пользователи, вы тут видите информацию о балансе, имя игрока и пр ? А они есть, правда зашифрованные:
Также может оказаться, что b'\x08\xeb\x0f\x12,\n*' (первая часть без адреса), тоже содержит какие-то уникальные закодированные данные, которые вам надо передавать, но вы будете передавать одно и тоже и из-за этого код не будет работать, поэтому способ не подходит всегда.
- Реверсим код игры
После того как я осознал, что это не просто байты, а закодированные данные, мне предстояло найти методы кодировки и декодировки.
Благо это браузерная игра, она написана на javascript.
javascript выполняется в браузере, а это значит мы можем посмотреть весь код игры, также как css или html файлы.
Кроме того мы можем напрямую влиять на игру, смотреть данные в переменных в процессе выполнения кода и останавливать код на нужном нам месте, в этом поможет отладчик встроенный в devTools.
Я перехожу в devTools -> Sources -> Page, тут и находятся файлы игры, теперь на следующие несколько бессонных ночей это будет нашем домом. (Эти файлы даже можно скачать и запустить копию игры у себя на пк:))
Я стал ресерчить код, пытаясь понять что он делает и найти нужные функции, но подумав логически "Кодировка данных должна происходить перед отправкой данных на сервер", это значит если я найду функцию отправки данных на сервер, то скорее всего я найду и функцию кодировки данных.
Тогда я нажал Ctrl + F и стал проходится по .js файлам, и искать все что может быть связанно с отправкой данных на сервер: "Websocket", "send", "message".
Так я наткнулся на файл "index.787ff.js", в нем было найдено 101 сходство "Websocket", определенно это какой-то главный файл.
Мне повезло и код не был обфусцирован или специально запутан, разобраться в нем было не сложно, но вам может так не повезти и придется провести еще больше бессонных ночей.
Полистав по сходствам я уже обнаружил функции encode и decode (очень удобно, но я не знал сразу что они не обфусцированы, поэтому искал по Websocket, который по другому не назовешь).
Теперь мне предстояло по их аналогии написать свой код, который будет также декодировать/кодировать данные.
Для этого надо понять как работают эти функции, с этим очень помогли точки останова (подробней в гайде о отладчике).
Для начала я нашел место куда приходят данные с сервера и где они декодируются, туда поставил точку и запустил игру.
Сразу прилетели данные в переменную:
В decode передается некая переменная e, в ней содержится Uint8Array (байты), если кликнуть по ней в Memory Inspector мы можем увидеть знакомый hex, точно такой же как тот когда мы снифали трафик. (Так же если преобразовать этот Hex в list через python, получится тот же самый Uint8Array как в js).
Дальше я нажал следующий шаг в отладчике (Step over next func..), чтобы эта функция выполнилась.
И тут функция декодировала часть данных,
Нажав еще несколько раз Step over next func.., данные декодировались полностью.
Вот он любимый родной json, в hex данных "08E807120608CDC3CCAC06"
скрывалось {low: 1704141129, high: 0, unsigned: false}.
Так же посмотрев другие запросы удалось найти все что нужно: информацию о игроке, функции кодирования и пр.
Теперь я точно знал что это то, что мне нужно, предстояло только перенести код.
Я стал разбираться как работает функция decode погружаясь в нее при помощи отладчика (Step into next func...).
Я пытался копировать все функции: int32, bytes, int64 и пр. Но это было ошибкой.
Изучив код внимательней я заметил функция decode вызывается из переменной s.
let t, n = s.gamepb.PushRes.decode(e);
А s в свою очередь была неким s = r.Writer
. r оказалась r = protobuf
Уже раньше я видел файл protobuf, тут меня осенило. Погуглив я в этом убедился: protobuf - это библиотека для кодировки и декодировки данных.
Функции: int32, bytes, int64 и пр вызывается из нее, а это значит что работа в 99% легче чем я думал, мне нужно просто подключить библиотеку, скопировать функции кодировки/декодировки и все должно работать.
Так я и сделал, скопировал файл "index.787ff.js", удалил все лишнее , оставив только "каркас" и подключив библиотеку, дальше я экспортировал этот каркас как модуль.
ChatGpt очень помог убрать лишние куски, так что бы это осталось рабочим
Дело осталось за малым, теперь я просто копировал ту самую PushRes, с которой у меня началось знакомство (s.gamepb.PushRes.decode(e)) в свой код.
Теперь я мог ее вызвать в своем коде передав Uint8array (Uint8array я получал конвертировав hex в list).
И вуаля, в n были декодированные данные, по аналогии я сделал и другие функции декодировки и кодировки:
Теперь мы можем видеть данные в человеческом формате и кодировать в машинный.
Связываем Python и JS
Но ведь бот на Python, как это связать с JS ?
- Разобраться и переписать логику js на python
- Запускать js код из Python
- Связать как-то js и python посредником, например через сервер.
У меня было мало времени, поэтому я не стал переписывать на Python, к тому же это очень геморно.
Первым вариантом для тестов я написал простой скрипт, который запустив через консоль и передавая hex он декодировал данные.
Дальше это можно было использовать в python, запустив cmd и получив из нее ответ (да это костыль).
result = subprocess.run('node main.js hexdata', capture_output=True, text=True) output = result.stdout
Например: Получаем ответ от вебсокета -> декодируем данные через cmd в python -> работаем с ними.
Но у этого костыля есть несколько проблем, главное для меня - это производительность, запустив например 500 потоков, 500 cmd процессов просто не потянет сервак за 14$ (да можно использовать один cmd переключаясь в потоках, но время поджимало так запариваться).
Поэтому лучшем для себя вариантом я выбрал некий промежуточный сервер-посредник, как тот же cmd, но выдерживающий большую нагрузку.
(мне написали за 10$) очень простой nodejs сервер, получая POST запрос он декодировал присланный hex при помощи моих функций и выдавал ответ в json, хоть сервер и был очень простым он мог выдержать 500 запросов в секунду.
Теперь это выглядело так: Получаю данные с вебсокета в hex -> шлю запрос на свой сервер, сервер декодирует и отправляет json -> работаю с json.
Это по прежнему костыль, но он решал все мои проблемы. Если вы знаете способы лучше напишите в коменты буду признателен.
Итоги
Надеюсь, эта статья дала базовые представления о том, как писать бота на вебсокетах, самое сложное в этом - реверс (все зависит от вашего проекта).
Возможно, вам повезет, и данные с сервера будет приходить в JSON формате. Тогда вам не придется реверсить, возможно вам хватит отправки байтов.
Но может быть и не повезет, код еще и будет обфусцирован.
В любом случае, вы теперь знаете с чего начать).
- Пример: Авторегер для CryptoMayor на вебсокетах
- библиотека aiohttp (документация)
- библиотека asyncio (Гайды: статья, видео)
- Анализаторы трафика: devTools, Fiddler, burp
- Как пользоваться отладчиком в браузере
- Деобфускация javascript
- Статья с базой по реверсу пк приложений
- Android Глазами хакера Книга (база по реверсу android приложений)
- ChatGpt
- Как написать nodejs сервер