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