May 26

Tonpoker Wheel точно не скам або казочка про детектива

Якось ввечері я помічаю пост в телеграм каналі Catizen Announcement пост про їхній спільний івент з Ton Poker та Playdeck.
Зайшовши з основного акаунта я отримую лише один спін(мабуть інші були прокручені раніше) і отримую 1 цент.
Прокрутивши ще спін на 1 цент з другого акаунту в мене закрадається підозра, що тут завжди випадає якесь сміття.
Я відкриваю Burp Suite вмикаю intercept(щоб запити не відправлялися на сервер а зупинялися).
Я не бачу запитів в яких є хоч якийсь рандом і припускаю(як виявиться потім помилково), що рандому або нема або він на стороні клієнта.
Відкривши дебагер я починаю пробувати розібратися що відбувається.
Щоб знайти який шматок відповідає за сам спін я вмикаю event listener breakpoint на click.

Таким чином поставивши брейкпоінти в коді я знаходжу функцію яка якраз оброблює спін.

Обфускований код

Код був обфускований але я більш менш зрозумів що він робить.
Із геніальних мувів при обфускації(вона можливо була навіть зроблена руками):

  • Функції в змінних з однолітерними назвами.
  • Функції дублюються в декількох змінних, деякі з цих змінних навіть не використовуються.
  • Індекс рандомного значення завжди більший на 0.5, від нього віднімають 0.5 щоб зробити правильним.
  • І можливо навіть більше того, на що я не звернув увагу.

Передивившись історію запитів я помітив, що рандомне значення відсилається зразу після відкриття колеса, після прокрутки просто відправляється запит на підтвердження спіну за його id(sp).

Початковий запит.
Запит для підтвердження спіна.

Отож у нас два варіанти, або вони завжди видають невигідний результат, або ми можемо респінити поки не випаде дуже хороший результат і розвести їх на купу бабла(думаю очевидно який більш ймовірний).
Але звісно це так собі пруф і це не цікаво.
Отож я вирішив написати скрипт який буде відправляти такі запити і дивитися що випадає.
Написати я вирішив його на Rust.
Зараз буде код і пояснення до нього, перейти до результатів можна за цим лінком.

use rand::Rng;
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::{self, StatusCode};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use tokio;

Підключаємо ліби:

  • rand — рандом, для рандомізації часу між запитами.
  • reqwest — серце програми, бібліотека для кидання запитів.
  • serde — білбіотека для серіалізації та десеріалізації, яку ми юзаємо щоб парсити json.
  • tokio — бібліотека для роботи з асинхронністю(тут асинхронність не дуже грала роль насправді).
  • std — стандартна бібліотека rust.
#[derive(Serialize, Deserialize, Debug)]
struct PokerSector {
    key: String,
    r#type: String,
    label: String,
    color: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct PokerResponse {
    tickets: i32,
    sectors: Vec<PokerSector>,
    sp: String,
    userShareLink: String,
    randInd: f32,
}

Оголошуємо структури які відповідають відповідді на запит.
r# це raw identifier бо type вже використовується.

#[tokio::main]
async fn main() {

Оголошуємо головну функцію і даємо tokio знати що вона є головною(по дефолту вона не асинхронна).

let mut rng = rand::thread_rng();

Ініціалізуємо рандом.
let — це оголошення змінної.
mut — означає що змінна може міняти значення(по дефолту всі змінні в расті сталі).

let wait_millis = 1000;
let mut requests_count = 0;

Ставимо початковий час очікування між запитами.
Та ставимо рахівник запитів на 0.

let client = reqwest::Client::new();

Оголошуємо наш клієнт для запитів.

let mut params = HashMap::new();
params.insert("initData", "<initData>");

Оголошуємо параметри для нашого майбутнього запиту.
Замість <initData> має бути initData, який можна взяти з запитів в браузері, але я брав з Burp Suite.

let mut headers = HeaderMap::new();
headers.insert(
    "sec-ch-ua",
    HeaderValue::from_str(r#""Brave";v="123", "Not:A-Brand";v="8", "Chromium";v="123""#)
        .unwrap(),
);
headers.insert(
    "sec-ch-ua-platform",
    HeaderValue::from_str(r#""Linux""#).unwrap(),
);
headers.insert("sec-ch-ua-mobile", HeaderValue::from_str(r#"?0"#).unwrap());
headers.insert("user-agent", HeaderValue::from_str(r#"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"#).unwrap());
headers.insert(
    "accept-language",
    HeaderValue::from_str(r#"en-US,en;q=0.8"#).unwrap(),
);
headers.insert(
    "origin",
    HeaderValue::from_str(r#"https://tgwa.tonpoker.online"#).unwrap(),
);
headers.insert(
    "sec-fetch-site",
    HeaderValue::from_str(r#"same-site"#).unwrap(),
);
headers.insert("sec-fetch-mode", HeaderValue::from_str(r#"cors"#).unwrap());
headers.insert("sec-fetch-dest", HeaderValue::from_str(r#"empty"#).unwrap());
headers.insert(
    "referer",
    HeaderValue::from_str(r#"https://tgwa.tonpoker.online/"#).unwrap(),
);

Оголошуємо наші заголовки запиту і заповнюємо їх, не знаю чи всі вони треба, бо я їх просто скопіював з Burp Suite з запита який кидався через браузер.

loop {
    let req = client
        .post("https://bridge.tonpoker.online/wheel/get")
        .headers(headers.clone())
        .json(&params)
        .send()
        .await
        .unwrap();

Починаємо нескінченний цикл запитів і кидаємо запит.
.post означає що це POST запит, використовуємо .await щоб викликати future(асинхронна функція). .unwrap() означає дістати значення і у випадку помилки завершити програму(де-факто припустити що помилки нема і спробувати виконатися так).

let status = req.status();

Записуємо статус запита в змінну, в цьому випадку я зустрівчав лише два статуси:

  • 200 OK
  • 429 Too Many Requests
match status {
    StatusCode::OK => {

Конструкція match дає можливість нам розглянути різні випадки статусу(це зручніше чим купа if-else).
Для початку розглянемо статсу OK(200).

let deserialized = req.json::<PokerResponse>().await;

Парсимо json, тут ми вже не припускаємо що помилки не буде, бо іноді може бути некоректна інфа і при 200(так один раз сталося).

match deserialized {
    Ok(deserialized) => {
        let index: usize = (deserialized.randInd - 0.5) as usize;
        requests_count += 1;
        println!(
            "Count: {} Status:{} Index: {} Type: {} Label: {} SP: {}",
            requests_count,
            status,
            index,
            deserialized.sectors[index].r#type,
            deserialized.sectors[index].label,
            deserialized.sp
        );
    }
    Err(e) => {
        println!("Strange error with OK status");
    }
}

Знову конструкція match, якщо запит запирсився то отримуємо індекс(віднімаємо 0.5 і переводимо в тип для індексів usize), збільшуємо рахівник запитів на 1 і виводимо в консоль інформацію про запит.
У випадку помилки пишемо в консоль про це(це не найкращий спосіб логів, краще розділяти помилки і дефолтну інфу, але це маленький скрипт тож можна й так).

let wait_time = Duration::from_millis(wait_millis + rng.gen_range(0..200));
println!("Sleeping for {}", wait_time.as_millis());
tokio::time::sleep(wait_time).await;
println!("Finished sleeping");

Генеруємо число від 0 до 200(0..200 це range схоже на range(0,200) в python) і додаємо його до базового часу очікування.
Конкретно тут ми могли і синхронно очікувати, але в асинхронних програмах краще робити це асинхронно, бо в цей час інші частини програми могли б щось робити.

_ => {
    println!("{}", status);
    let wait_time =
        Duration::from_millis(wait_millis + 1000 + rng.gen_range(1000..3000));
    println!("Sleeping for {}", wait_time.as_millis());
    tokio::time::sleep(wait_time).await;
    println!("Finished sleeping");
}

Ми закінчили розглядати випадок OK(200), тепер для усіх інших випадків ми виводимо статус в консоль і анлогічо очікуємо, але тепер вже довший час.

Це кінець програми, весь код ось:

use rand::Rng;
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::{self, StatusCode};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use tokio;

#[derive(Serialize, Deserialize, Debug)]
struct PokerSector {
    key: String,
    r#type: String,
    label: String,
    color: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct PokerResponse {
    tickets: i32,
    sectors: Vec<PokerSector>,
    sp: String,
    userShareLink: String,
    randInd: f32,
}

#[tokio::main]
async fn main() {
    let mut rng = rand::thread_rng();
    let wait_millis = 1000;
    let mut requests_count = 0;
    let client = reqwest::Client::new();
    let mut params = HashMap::new();
    params.insert("initData", "<initData>");
    let mut headers = HeaderMap::new();
    headers.insert(
        "sec-ch-ua",
        HeaderValue::from_str(r#""Brave";v="123", "Not:A-Brand";v="8", "Chromium";v="123""#)
            .unwrap(),
    );
    headers.insert(
        "sec-ch-ua-platform",
        HeaderValue::from_str(r#""Linux""#).unwrap(),
    );
    headers.insert("sec-ch-ua-mobile", HeaderValue::from_str(r#"?0"#).unwrap());
    headers.insert("user-agent", HeaderValue::from_str(r#"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"#).unwrap());
    headers.insert(
        "accept-language",
        HeaderValue::from_str(r#"en-US,en;q=0.8"#).unwrap(),
    );
    headers.insert(
        "origin",
        HeaderValue::from_str(r#"https://tgwa.tonpoker.online"#).unwrap(),
    );
    headers.insert(
        "sec-fetch-site",
        HeaderValue::from_str(r#"same-site"#).unwrap(),
    );
    headers.insert("sec-fetch-mode", HeaderValue::from_str(r#"cors"#).unwrap());
    headers.insert("sec-fetch-dest", HeaderValue::from_str(r#"empty"#).unwrap());
    headers.insert(
        "referer",
        HeaderValue::from_str(r#"https://tgwa.tonpoker.online/"#).unwrap(),
    );
    loop {
        let req = client
            .post("https://bridge.tonpoker.online/wheel/get")
            .headers(headers.clone())
            .json(&params)
            .send()
            .await
            .unwrap();
        let status = req.status();
        match status {
            StatusCode::OK => {
                let deserialized = req.json::<PokerResponse>().await;
                match deserialized {
                    Ok(deserialized) => {
                        let index: usize = (deserialized.randInd - 0.5) as usize;
                        requests_count += 1;
                        println!(
                            "Count: {} Status:{} Index: {} Type: {} Label: {} SP: {}",
                            requests_count,
                            status,
                            index,
                            deserialized.sectors[index].r#type,
                            deserialized.sectors[index].label,
                            deserialized.sp
                        );
                    }
                    Err(e) => {
                        println!("Strange error with OK status");
                    }
                }
                let wait_time = Duration::from_millis(wait_millis + rng.gen_range(0..200));
                println!("Sleeping for {}", wait_time.as_millis());
                tokio::time::sleep(wait_time).await;
                println!("Finished sleeping");
            }
            _ => {
                println!("{}", status);
                let wait_time =
                    Duration::from_millis(wait_millis + 1000 + rng.gen_range(1000..3000));
                println!("Sleeping for {}", wait_time.as_millis());
                tokio::time::sleep(wait_time).await;
                println!("Finished sleeping");
            }
        }
    }
}

Якщо ви з якоїсь причини хочете це запустити(краще почитайте до кінця щоб зрозуміти чому ви мабуть не хочете цього робити), то не забудьте замінити <initData> на ваше значення initData.

Отож які результати?
Під час тестів я один раз зміг вибити 5 центів і не зберіг sp, із того що я зберіг в файлик, таке:

  • Всього успішних запитів 94.
  • На 10 центів 1 раз(я не пробував його активувати бо я отримую постійно Too Many Requests тепер).
  • На 1 цент 66 разів.
  • Фріспін(перекрутка) 27 разів.
Графік результатів

Отож чи гарантовано там не можна виграти більше? Ні.
Але як можна помітити шанс на це дуже маленький.

Ресурси для вивчення Rust:

  • The Rust Book — основний ресурс для вивчення Rust.
  • Rust by Example — Rust показаний на прикладах коду.
  • Rustlings — вправи де вам дають поламаний код і вам треба його поремонтувати.

Всі бібліотеки використані в статті можна знайти на crates.io, а документації до них на docs.rs.

На цьому все.
Всім удачі!
Підготовлено каналом: https://t.me/cryptopidval