January 2

Как писать ботов на websocket 

https://t.me/iam_limonov

Вступление

Вы никогда не задумывались, как пишутся боты под игры?

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

Все эти проблемы решают сетевые боты, как post/get в ботах на запросах.
Однако в играх используются websockets.

В этой статье я расскажу свой опыт написания бота под браузерную игру CryptoMayor.

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

Если вы не знаете что такое post/get запросы, то эта статья может показаться сложной. Cначала изучите основы.

P.s Это мой первый бот на вебсокетах. Возможно, я многого не знаю, но предполагаю суть одна:)

Когда запилил бота на вебсокетах и вынес $200k (спс за фомо)

Великая База

- Что такое вебсокеты?

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

У вебсокета протокол ws://

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

Вы можете заметить, что в GET-параметры еще и передается параметр s, это сигнатура кошелька, я не буду подробно останавливаться над этим, т.к это статья больше о боте в целом, но если вам интересно как генерить сигнатуру кошелька для авторизации web3 можете глянуть код.

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

По аналогии выше мы можем создать соединение с вебсокетом, только уже указать нужный нам url.

Теперь вернемся в Messages и видим обмен сообщений между клиентом (браузером) и сервером.

Зеленое это то что клиент посылает на сервер (send), а красное - то, что сервер шлет на клиент (recive).

Несколько раз авторизовавшись, я заметил, что сначала всегда клиент шлет серверу 3 запроса серверу (зеленые сообщения).

Я предположил, что они как раз и отвечают за полную авторизация (мы уже частично авторизовались, подписав сигнатуру и подключившись к вебсокету).

При клике на сообщение мы можем увидеть передаваемые данные.

Вам может повезти и в вашем проекте будет формат json, но это маловероятно, т.к большинство проектов сжимает/шифрует данные они передаются в байтах.

Почему тут hex, что за байты, я расскажу более детально в "Реверс" ниже.

Теперь если мы конвертируем этот hex в byte, получим данные, которые можно отправить, они валидно принимаются сервером.

(либо можно кликнуть правой кнопкой по message -> copy as base64 и декодировав его через python мы получим нужные байты).

python import base64; base64.b64decode('ваш base64')

Например байты второго сообщения выглядят так: 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", определенно это какой-то главный файл.

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

Деобфускация js статья.

Полистав по сходствам я уже обнаружил функции 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)) в свой код.

Реализация функции в index.787ff.js
Реализация в моем коде убрав все "лишнее"

Теперь я мог ее вызвать в своем коде передав Uint8array (Uint8array я получал конвертировав hex в list).

И вуаля, в n были декодированные данные, по аналогии я сделал и другие функции декодировки и кодировки:

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


Связываем Python и JS

Но ведь бот на Python, как это связать с JS ?

Для себя я нашел 3 варианта:

  1. Разобраться и переписать логику js на python
  2. Запускать js код из Python
  3. Связать как-то js и python посредником, например через сервер.

У меня было мало времени, поэтому я не стал переписывать на Python, к тому же это очень геморно.

Первым вариантом для тестов я написал простой скрипт, который запустив через консоль и передавая hex он декодировал данные.

node main.js hexdata -> json

Дальше это можно было использовать в 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 формате. Тогда вам не придется реверсить, возможно вам хватит отправки байтов.

Но может быть и не повезет, код еще и будет обфусцирован.

В любом случае, вы теперь знаете с чего начать).

Всем фонк.

Ссылки из статьи: