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(¶ms) .send() .await .unwrap();
Починаємо нескінченний цикл запитів і кидаємо запит.
.post означає що це POST запит, використовуємо .await щоб викликати future(асинхронна функція). .unwrap() означає дістати значення і у випадку помилки завершити програму(де-факто припустити що помилки нема і спробувати виконатися так).
let status = req.status();
Записуємо статус запита в змінну, в цьому випадку я зустрівчав лише два статуси:
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(¶ms) .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 разів.
Отож чи гарантовано там не можна виграти більше? Ні.
Але як можна помітити шанс на це дуже маленький.
- The Rust Book — основний ресурс для вивчення Rust.
- Rust by Example — Rust показаний на прикладах коду.
- Rustlings — вправи де вам дають поламаний код і вам треба його поремонтувати.
Всі бібліотеки використані в статті можна знайти на crates.io, а документації до них на docs.rs.
На цьому все.
Всім удачі!
Підготовлено каналом: https://t.me/cryptopidval