Приветствую. Речь в статье пойдёт про мой опыт реверсинга и написания ботнета для $NotCoin. Ивент с игрой уже закончился, так что, считаю, что можно поделиться.
Дело было вечером, делать было нечего, подружка села на заборе — и скинула мне ссылку на ноткоин в альфе.
Посмотрел, потыкал, недолго думая, я забыл про него на месяц.
И вот он уже набрал аудиторию (достаточно большую) и я подумал, что всё же стоит посмотреть что там да как.
Суть игры в одном слове: кликер.
И что же нужно делать?
— У тебя есть монетка, на неё нужно кликать, чем больше монет — тем лучше.
Что ж, разобрались что это такое, теперь приступаем к реверсу:
Включил WebView debug на своем Android смартфоне в Telegram
Подключил по кабелю к компу
Открыл
chrome://inspect/#devices
в хромеЗапустил вебапку в боте
Начал анализ трафика
Собрал все необходимые запросы:
Клик по монетке
Покупка улучшений
Получение наград за задания
Активация плюшек (турбо и восстановление энергии)
Примеры ответа сервера на клик:
{
"id": 1657956,
"userId": 1659140,
"teamId": 4,
"leagueId": 3,
"limitCoins": 6000,
"totalCoins": "357549",
"balanceCoins": 70249,
"spentCoins": 2070300,
"miningPerTime": 4,
"multipleClicks": 11,
"autoClicks": 12,
"withRobot": true,
"lastMiningAt": "2024-01-08T15:16:23.000Z",
"lastAvailableCoins": 5281,
"turboTimes": 0,
"avatar": "https://cobuild.ams3.cdn.digitaloceanspaces.com/api-clicker/tg/avatars/1659140.jpg",
"createdAt": "2023-11-07T22:47:49.000Z",
"hash": [
"TWF0aC5wb3coMywgMyk="
],
"availableCoins": 5468
}
Первая проблема с которой я столкнулся был хэш при клике, я испугался что придётся что-то сложное выдумывать. То есть, в ответ на клик приходил hash: list[str]
Однако, после 20 запросов на клик, все мои вопросы отпали.
Их пул хэшей состоял из 10 js кодов, которые исполнялись на клиенте, чтобы подтвердить, что юзер не читерит.
Чтобы решать их без js, пришлось сделать страшилку:
def calc_hash(hash_expression: str) -> int:
hash_expression = hash_expression.strip()
if "Math" in hash_expression:
try:
hash_expression = (
hash_expression.replace("Math", "math")
.replace("math.abs", "abs")
.replace("math.PI", "math.pi")
.replace("math.max", "max")
.replace("math.min", "min")
)
return int(eval(hash_expression, {"math": __import__("math")}))
except:
return 0
elif "?" in hash_expression and ":" in hash_expression:
return int(hash_expression.split("?")[1].split(":")[0].strip())
elif hash_expression.isdigit():
return int(hash_expression)
elif hash_expression == "document.querySelectorAll('body').length":
return 1
else:
return random.randint(0, 10000)
Лимиты опытным путём, замерами и чтением ответов от бэка, были вычислены следующие:
Максимальный уровень клик бустера: 10000
Максимальный уровень запаса энергии: 10000
Максимальный уровень восстановления в секунду : 3 (4 энергии в секунду)
Максимальный уровень робота: 1
Максимум кликов за запрос: 159
Турбо длится: 12 секунд
Робот начинает работать через: 650 секунд после последнего клика. (Однако окно сбора появляется через час, после последнего клика, но кого это волнует, мы абузим запросы)
Хэш от тг, для авторизации действует 2 или 3 часа
(забыл..), как и рефреш токенВ какой-то момент они убрали бесконечно падающие турбо, и сделали 3 в день, поэтому: 3 турбо в день, 3 полных восстановления энергии в день
После того, как лимиты узнали, пора делать стратегию.
Для начала выясним, нужен ли нам вообще робот:
Судя по всему - нужен. (код построения графиков этих не дам - тоже утерян)
Постарался посчитать, на какой левел лучше прокачивать запас энергии и клик:
Нехитрыми манипуляциями и симуляциями, я пришёл к выводу, что лучший максимальный уровень прокачки клика - 3. Всё равно по 159 кликов за запрос, будет быстро тратить энергию, а прокачка выше, даёт линейный и копеечный прирост, а нам ведь нужно копить баланс, чтобы перевести его в money. Проще говоря — не окупается.
Про лимит энергии вопрос неоднозначный, можно поставить лимит около 15 уровня.
Попробовать свои симуляции можете с моим кодом на гитхабе (*тык*)
Что мы имеем в итоге?
Если позволить роботу помайнить и дать энергии поднакопиться — мы можем получить двойную выгоду, потому что робот майнит с той же скоростью, с которой у нас восстанавливается энергия, но через 650 секунд после последнего клика.
Энергия восстанавливается со скоростью 4 энергии в секунду.
Если мы опустили энергию до 0, робот запустит майнинг после восстановления 2600 энергии (650 секунд на максимальном левеле восстановления энергии:
650 × 4
)Каждый клик потребляет 1 энергию, и даёт 4 монеты.
Мы можем за один запрос отправить 159 кликов.
Чтобы у нас не списали под конец ивента все клики мы должны поставить паузу в условные 10 секунд между запросами.
Учитываем, что пока мы спим и кликаем — энергия копится (возьмём, что на 159 кликов приходится 11 секунд)
Не сложными рассчётами имеем:
За 11 секунд, пока мы кликаем 159 штук, восстановится 44 энергии:
11 × 4 = 44
Каждый клик дает 4 монеты, так что за 159 кликов получится 646 монет:
159 × 4 = 636
Сделал небольшую симуляцию, можете наглядно посмотреть как меняется кол-во заработанных монет за один цикл восстановления полной энергии:
Код:
import pandas as pd
max_energy_levels = range(500, 8500, 500)
energy_recovery_rate = 4
coins_per_click = 4
clicks_per_request = 159
session_duration = 11
energy_per_session = clicks_per_request
coins_per_session = clicks_per_request * coins_per_click
recovery_per_session = 44
def calculate_click_sessions(energy: int) -> int:
return energy // (energy_per_session - recovery_per_session)
def calculate_bot_coins(energy: int, total_click_time: int, recovery_rate: int, bot_start_time: int) -> int:
total_time = energy / recovery_rate + total_click_time
return int(max(total_time - bot_start_time, 0) * 4)
data = {
"Макс. уровень энергии": max_energy_levels,
"Время восстановления (сек)": [energy / energy_recovery_rate for energy in max_energy_levels],
"Циклы кликов": [calculate_click_sessions(energy) for energy in max_energy_levels],
"Монеты с кликов": [(energy // energy_per_session) * coins_per_session for energy in max_energy_levels],
"Время кликов (сек)": [(energy // energy_per_session) * session_duration for energy in max_energy_levels],
}
df = pd.DataFrame(data)
df["Монеты с бота"] = [
calculate_bot_coins(energy, df["Время кликов (сек)"].iloc[i], energy_recovery_rate, 650)
for i, energy in enumerate(max_energy_levels)
]
df["Общее количество монет"] = df["Монеты с кликов"] + df["Монеты с бота"]
print(df.to_string(index=False))
Полный алгоритм нашей стратегии:
Обновляем сессию, если 1.5 часа прошло с последнего обновления сессии
last_update_time = arrow.now().shift(hours=-2)
while True:
if (arrow.now() - last_update_time).seconds > 60 * 60 * 1.5:
await self.update_webapp_session()
# other code ...
Получаем инфу о профиле
await self.api.get_profile()
Проверяем куплен ли у нас бот и можем ли мы его собрать
if self.api.last_profile_data.with_robot:
full_recharge_time = (
self.api.last_profile_data.energy_limit // self.api.last_profile_data.recharging_speed - 10
) # Время полной перезарядки
elapsed_time_since_last_click = (
(arrow.now() - self.api.last_profile_data.last_click_at).seconds
) # Сколько секунд прошло с последнего клика
remaining_time = max(
0, full_recharge_time - elapsed_time_since_last_click
) # Оставшееся время восстановления
await asyncio.sleep(remaining_time)
robot_data = await self.api.check_robot()
if robot_data > 0:
await self.api.claim_robot()
Проверяем, есть ли выполненные задания, если есть — собираем награды
Покупаем бустеры до нашего лимита: Энергию до 15, Клик до 4 (Забыл уточнить, у меня в статье разнится между 3 и 4 максимальный левел, суть в том, что у них уровень от 0 считается на всех бустерах, и условный 3 уровень будет давать 4 монеты за клик), Скорость восстановления до 4, Робот до 1
for item in shop:
match item.id:
case 1: # Energy Limit
claimed_count += await self.buy_booster_while_possible(item, self.ENERGY_LIMIT_MAX_LEVEL)
case 2: # Recharging Speed
claimed_count += await self.buy_booster_while_possible(item, self.RECHARGING_SPEED_MAX_LEVEL)
case 3: # Multiple Clicks
claimed_count += await self.buy_booster_while_possible(item, self.MULTIPLE_CLICKS_MAX_LEVEL)
case 18: # Robot
claimed_count += await self.buy_booster_while_possible(item, 1)
*Кликаем*
energy_per_click_session = 159 - 44
clicks_to_full_mine = self.api.last_profile_data.energy_limit / (
self.api.last_profile_data.multiple_clicks * energy_per_click_session
)
requests_count = math.ceil(clicks_to_full_mine) + 1
for _ in range(requests_count):
await self.api.click(
ClickRequest(
web_app_data=self.api.webapp_session,
count=min(
self.api.last_profile_data.available_energy,
159 * self.api.last_profile_data.multiple_clicks,
),
hash=calculate_hash(self.api.last_clicker_data.hash, self.pyrogram_client.me.id)
if self.api.last_clicker_data
else None,
)
)
await asyncio.sleep(self.SLEEP_BETWEEN_CLICKS)
Активируем восстановление полной энергии, если есть - кликаем снова
Активируем турбо - кликаем снова, только 2 запроса с паузой в 4 секунды
И вот нам осталось подрубить кучу акков и можно жить в шоколаде. Но не тут-то было. Повысили защиту клаудфлеера ребята из ноткоина. Будто нас это остановит:
Берём вкусные прокси и добавляем свои TLS
import ssl
from typing import ClassVar
from aiohttp import TCPConnector
from aiohttp_proxy import ProxyConnector
class BypassTLS(TCPConnector):
SUPPORTED_CIPHERS: ClassVar[list[str]] = [
"ECDHE-ECDSA-AES128-GCM-SHA256",
"ECDHE-RSA-AES128-GCM-SHA256",
"ECDHE-ECDSA-AES256-GCM-SHA384",
"ECDHE-RSA-AES256-GCM-SHA384",
"ECDHE-ECDSA-CHACHA20-POLY1305",
"ECDHE-RSA-CHACHA20-POLY1305",
"ECDHE-RSA-AES128-SHA",
"ECDHE-RSA-AES256-SHA",
"AES128-GCM-SHA256",
"AES256-GCM-SHA384",
"AES128-SHA",
"AES256-SHA",
"DES-CBC3-SHA",
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256",
]
def __init__(self, *args, **kwargs):
self.ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
self.ssl_context.set_ciphers(":".join(BypassTLSProxy.SUPPORTED_CIPHERS))
self.ssl_context.set_ecdh_curve("prime256v1")
self.ssl_context.minimum_version = ssl.TLSVersion.TLSv1_3
self.ssl_context.maximum_version = ssl.TLSVersion.TLSv1_3
super().__init__(*args, ssl=self.ssl_context, **kwargs)
class BypassTLSProxy(BypassTLS, ProxyConnector): ...
Вуаля, у нас 200. Теперь точно можем подключать акки и фармиться.
Спасибо за просмотр, почти полный код того, что я описал выше, находится тут: github repo (*тык*)
Кстати говоря, ни один коин списан не был их системой после окончания ивента :D
Это моя первая статья, надеюсь на понимание и отзывы после прочтения, спасибо.
rekashet
неплохая реализация. Хочется верить, что Вы станете миллиардером, когда Notcoin можно будет обменять на деньги : )
unffuunnyy Автор
Мечтать - это хорошо :) Но меня больше интересовал опыт написания ботов для игрушки и поиск эффективных алгоритмов