Potato Sorvor в $NOTCOIN или история одного реверса

Приветствую. Речь в статье пойдёт про мой опыт реверсинга и написания ботнета для $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 полных восстановления энергии в день

После того, как лимиты узнали, пора делать стратегию.

Для начала выясним, нужен ли нам вообще робот:

5f531de0355eeee4ca45d109a7632501.png

Судя по всему — нужен. (код построения графиков этих не дам — тоже утерян)

Постарался посчитать, на какой левел лучше прокачивать запас энергии и клик:

c56d6dceeeee242b9d4d0e6997458823.png

Нехитрыми манипуляциями и симуляциями, я пришёл к выводу, что лучший максимальный уровень прокачки клика — 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

Сделал небольшую симуляцию, можете наглядно посмотреть как меняется кол-во заработанных монет за один цикл восстановления полной энергии:

a7d9498fa04c9c2aac305acd58f7e982.png

Код:

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))

Полный алгоритм нашей стратегии:

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 (*тык*)

Это моя первая статья, надеюсь на понимание и отзывы после прочтения, спасибо.

© Habrahabr.ru