July 5

Burp Suite ламає ігри або абузимо blum drop game

В blum якийсь час вже є drop game і я згадавши сьогодні про цю гру я вирішив її поламати.
(Через певні тести зроблені мною в процесі написання цієї статті мене можливо забанять з Blum, раджу вам бути обережними якщо вам не байдуже на цей проект)

Як це роблять зазвичай

Більшість людей запускають просто клікер і радіють життю, але в нашому підвалі клікери не те щоб вітаються(мені лінь їх кодити).

План

  1. Через Burp Suite перехопити запити зроблені Blum.
  2. Поміняти в запиті на зміну рахунку кількість поінтів.
  3. Автоматизувати кидання таких запитів через скрипт.
  4. Профіт

Виконання плану(або ж основна частина статті)

Я вирішив перехоплювати запити з мобільного девайсу.
Для цього треба додати сертифікат з Burp Suite на ваш девайс(так само як зазвичай, але можливо доведеться поміняти формат сертифікату для чого існує openssh).
Також в самому Burp треба в налаштуваннях проксі додати ваш локальний айпі як доступний для використання(нижче скріншоти з тим як це робити).

Тепер перехопивши запити ми можемо помітити два цікавих для нас запити на адреси game-domain.blum.codes/api/v1/game/play та game-domain.blum.codes/api/v1/game/claim.
Структура запитів максимально проста, перший запит має лише хедери авторизації і у відповідь ми отримуємо gameId.
В другому надісланні данні складаються з цього самого gameId та поінтів.
Таким чином ми можемо просто відправляти ці запити і вказувати потрібну кількість поінтів.

Ось невеличкий PoC(proof of concept) скрипт на rust який це робить.
Цей скрипт схожий на скрипт зі статті про tonpoker wheel, частина речей пояснена трохи більш детально там.
Для початку dependencies з Cargo.toml(версії бібліотек тут найновіші, не обов'язково використовувати саме ці)

[dependencies]
rand = "0.8.5"
reqwest = { version = "0.12.5", features = ["json"] }
serde = { version = "1.0.203", features = ["derive"] }
tokio = { version = "1.38.0", features = ["full"] }
use rand::Rng;
use reqwest::header::HeaderMap;
use reqwest::{self, StatusCode};
use serde::{Deserialize, Serialize};
use std::time::Duration;

Імпортуємо потрібні функції з бібліотек.

#[derive(Serialize, Deserialize, Debug)]
struct GameClaimRequest {
    gameId: String,
    points: i32,
}

#[derive(Serialize, Deserialize, Debug)]
struct GamePlayResponse {
    gameId: String,
}

Структури для нашого запиту на клейм та для відповіді на наш запит на початок гри.

#[tokio::main]
async fn main() {
    let repeat_count = 1;
    let min_points = 350;
    let max_points = 450;
    let auth_token = "";
    let wait_duration = Duration::from_secs(31);

Початок основної функції, вказуємо кількість повторів, мінімальну кількість поінтів для рандмому і максимальну, токен авторизації, який можна взяти з burp suite і час очікування(blum наче як вимагає 30 секунд, ставимо 31 секунду для безпеки).

Червоним закритий токен який і треба скопіювати
let mut rng = rand::thread_rng();
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
headers.insert(
    "accept",
    "application/json, text/plain, */*".try_into().unwrap(),
);
headers.insert(
    "authorization",
    format!("Bearer {}", auth_token).try_into().unwrap(),
);
headers.insert("sec-fetch-site", "same-site".try_into().unwrap());
headers.insert("accept-encoding", "gzip, deflate, br".try_into().unwrap());
headers.insert("accept-language", "en-GB,en;q=0.9".try_into().unwrap());
headers.insert("sec-fetch-mode", "cors".try_into().unwrap());
headers.insert("origin", "https://telegram.blum.codes".try_into().unwrap());
headers.insert(
    "user-agent",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)"
        .try_into()
        .unwrap(),
);
headers.insert("sec-fetch-dest", "empty".try_into().unwrap());

Ініціалізуємо рандом, reqwest, і ставимо хедери(скопіювані з Burp Suite).
Оскільки значення хедерів мають тип HeaderValue то ми можемо використати try_into щоб конвертувати значення в цей тип і unwrap який закінчує виконання помилкою якщо не виникає помилка конвертації.
Почитати про try_into можна тут або тут.
Про unwrap тут.

for _ in 0..repeat_count {
    let req = client
        .post("https://game-domain.blum.codes/api/v1/game/play")
        .headers(headers.clone())
        .send()
        .await
        .unwrap();

Починаємо цикл для використання наших тікетів.
Запитуємо початок гри у сервера і отримуємо gameId.

let status = req.status();
match status {
    StatusCode::OK => {
        let deserialized = req.json::<GamePlayResponse>().await;

У випадку якщо статус запиту 200(OK) то десеріалізуємо відповідь на запит в нашу структуру.

match deserialized {
    Ok(deserialized) => {
        println!("Got game id: {}", deserialized.gameId);
        println!("Sleeping for {}ms", wait_duration.as_millis());
        tokio::time::sleep(wait_duration).await;
        println!("Finished sleeping");
        let points = rng.gen_range(min_points..max_points);
        let params = GameClaimRequest {
gameId: deserialized.gameId,
            points,
        };
        println!("Trying to claim {} points", points);
        let response = client
            .post("https://game-domain.blum.codes/api/v1/game/claim")
            .headers(headers.clone())
            .json(&params)
            .send()
            .await
            .unwrap();
        println!(
                "Claim response status {}, {}",
                response.status(),
                response.text().await.unwrap()
                )
    }
    Err(_) => {
        println!("Unable to deserialize");
        println!("Sleeping for {}ms", wait_duration.as_millis());
        tokio::time::sleep(wait_duration).await;
        println!("Finished sleeping");
    }

Якщо десеріалізація успішна то чекаємо wait_duration і випадковим чином вибираємо кількість поінтів.
Далі формуємо данні запиту через нашу структуру і відсилаємо наш запит на сервер.
У випадку помилки десеріалізації просто чекаємо wait_duration.

_ => {
    println!("{}", status);
    println!(
            "Sleeping for {}ms hoping it will work",
            wait_duration.as_millis()
            );
    tokio::time::sleep(wait_duration).await;
    println!("Finished sleeping");
}

Якщо відповідь на наш початковий запит була не 200(OK) то також чекаємо час очікування.

Приклад роботи програми

Весь код:

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

#[derive(Serialize, Deserialize, Debug)]
struct GameClaimRequest {
    gameId: String,
    points: i32,
}

#[derive(Serialize, Deserialize, Debug)]
struct GamePlayResponse {
    gameId: String,
}

#[tokio::main]
async fn main() {
    let repeat_count = 1;
    let min_points = 350;
    let max_points = 450;
    let auth_token = "";
    let wait_duration = Duration::from_secs(31);

    let mut rng = rand::thread_rng();
    let client = reqwest::Client::new();
    let mut headers = HeaderMap::new();
    headers.insert(
        "accept",
        "application/json, text/plain, */*".try_into().unwrap(),
    );
    headers.insert(
        "authorization",
        format!("Bearer {}", auth_token).try_into().unwrap(),
    );
    headers.insert("sec-fetch-site", "same-site".try_into().unwrap());
    headers.insert("accept-encoding", "gzip, deflate, br".try_into().unwrap());
    headers.insert("accept-language", "en-GB,en;q=0.9".try_into().unwrap());
    headers.insert("sec-fetch-mode", "cors".try_into().unwrap());
    headers.insert("origin", "https://telegram.blum.codes".try_into().unwrap());
    headers.insert(
        "user-agent",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)"
            .try_into()
            .unwrap(),
    );
    headers.insert("sec-fetch-dest", "empty".try_into().unwrap());

    for _ in 0..repeat_count {
        let req = client
            .post("https://game-domain.blum.codes/api/v1/game/play")
            .headers(headers.clone())
            .send()
            .await
            .unwrap();
        let status = req.status();
        match status {
            StatusCode::OK => {
                let deserialized = req.json::<GamePlayResponse>().await;
                match deserialized {
                    Ok(deserialized) => {
                        println!("Got game id: {}", deserialized.gameId);
                        println!("Sleeping for {}ms", wait_duration.as_millis());
                        tokio::time::sleep(wait_duration).await;
                        println!("Finished sleeping");
                        let points = rng.gen_range(min_points..max_points);
                        let params = GameClaimRequest {
                            gameId: deserialized.gameId,
                            points,
                        };
                        println!("Trying to claim {} points", points);
                        let response = client
                            .post("https://game-domain.blum.codes/api/v1/game/claim")
                            .headers(headers.clone())
                            .json(&params)
                            .send()
                            .await
                            .unwrap();
                        println!(
                            "Claim response status {}, {}",
                            response.status(),
                            response.text().await.unwrap()
                        )
                    }
                    Err(_) => {
                        println!("Unable to deserialize");
                        println!("Sleeping for {}ms", wait_duration.as_millis());
                        tokio::time::sleep(wait_duration).await;
                        println!("Finished sleeping");
                    }
                }
            }
            _ => {
                println!("{}", status);
                println!(
                    "Sleeping for {}ms hoping it will work",
                    wait_duration.as_millis()
                );
                tokio::time::sleep(wait_duration).await;
                println!("Finished sleeping");
            }
        }
    }
}

Таким простим чином можна абузити Blum без усіх цих клікерів.
Чи можуть цього бота відслідкувати?
Мабуть так, але швидше лише тому що ми кидаємо мало запитів по балансу і тд, що можна додати про потребі.

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

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

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

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