WebAssembly Reversing: как ломать браузерные игры и крипто-майнеры
Привет, друг!
WebAssembly (WASM) — это низкоуровневый байткод для браузера, который работает почти со скоростью нативного кода. Разрабы используют его для игр, обработки видео, криптографии… и для майнинга в браузере. Проблема в том, что WASM сложнее реверсить, чем JavaScript, но не невозможно. Давай научимся вскрывать эти чёрные ящики.
Анатомия WASM: что внутри .wasm файла
WASM модуль — это бинарный файл с несколькими секциями:
- Type — сигнатуры функций (параметры и возвращаемые типы)
- Function — индексы типов для каждой функции
- Memory — линейная память (как массив байт)
- Export — что доступно из JavaScript
- Code — сам байткод функций
- Data — инициализация памяти (константы, строки)
В отличие от JavaScript, WASM компилируется из C/C++/Rust, поэтому логика более «низкоуровневая» — циклы, указатели, прямая работа с памятью.
Инструменты для реверсинга
Базовый набор:
- wasm2wat (из WABT) — конвертирует бинарный WASM в текстовый WebAssembly Text Format
- wasm-decompile — декомпилирует WASM в псевдо-C код
- Ghidra — мощный дизассемблер с поддержкой WASM
- IDA Pro — если есть деньги, лучший инструмент
- Chrome DevTools — для динамического анализа
Установка WABT:
|
1 2 3 4 5 |
git clone https://github.com/WebAssembly/wabt cd wabt mkdir build && cd build cmake .. && make # wasm2wat будет в build/ |
Кейс #1: Взлом браузерной игры
Представь, что у тебя есть игра, где нужно фармить виртуальную валюту. Логика подсчёта монет спрятана в WASM. Задача — найти функцию, которая отвечает за начисление монет, и модифицировать её.
Шаг 1: Извлечение WASM файла
Открываем DevTools → Network → фильтруем по .wasm. Качаем файл.
|
1 |
curl -o game.wasm https://example.com/game.wasm |
Шаг 2: Дизассемблирование
|
1 |
wasm2wat game.wasm -o game.wat |
Получаем текстовый формат:
|
1 2 3 4 5 6 7 8 9 10 |
(func $addCoins (param $amount i32) (result i32) (local $current i32) global.get $totalCoins local.set $current local.get $current local.get $amount i32.add global.set $totalCoins global.get $totalCoins ) |
Видим функцию addCoins, которая берёт текущее количество монет, добавляет amount и сохраняет обратно.
Шаг 3: Модификация
Меняем логику — умножаем на 100 вместо простого добавления:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
(func $addCoins (param $amount i32) (result i32) (local $current i32) global.get $totalCoins local.set $current local.get $current local.get $amount i32.const 100 ;; добавили константу 100 i32.mul ;; умножаем вместо add i32.add global.set $totalCoins global.get $totalCoins ) |
Шаг 4: Компиляция и подмена
|
1 |
wat2wasm game.wat -o game_modded.wasm |
Теперь подменяем файл через Burp Suite или локальный прокси:
|
1 2 3 4 5 |
# mitmproxy скрипт def response(flow): if "game.wasm" in flow.request.url: with open("game_modded.wasm", "rb") as f: flow.response.content = f.read() |
Запускаем игру — монеты начисляются в 100 раз больше. Профит!
Кейс #2: Анализ скрытого крипто-майнера
Некоторые сайты подгружают WASM майнеры (Coinhive, CryptoLoot), которые жрут CPU в фоне. Задача — найти майнер, понять, что он майнит, и заблокировать.
Шаг 1: Поиск подозрительного WASM
Открываем DevTools → Sources, ищем .wasm файлы. Обычно они имеют невзрачные названия типа worker.wasm или закодированные в Base64.
|
1 2 3 4 |
// Часто встречается такой код fetch('data:application/wasm;base64,AGFzbQEAAAA...') .then(r => r.arrayBuffer()) .then(WebAssembly.instantiate); |
Декодируем Base64:
|
1 |
echo "AGFzbQEAAAA..." | base64 -d > miner.wasm |
Шаг 2: Декомпиляция с помощью Ghidra
Загружаем miner.wasm в Ghidra:
- File → Import File → выбираем WASM
- Анализируем (Auto Analyze)
В декомпилированном коде ищем характерные паттерны майнеров:
- Функции хеширования (SHA256, Scrypt, CryptoNight)
- Nonce iteration (перебор значений)
- WebSocket соединения (отправка шар на пул)
|
1 2 3 4 5 6 7 8 9 10 |
// Декомпилированный код (упрощённо) void hash_function(uint8_t *data, uint32_t nonce) { uint8_t hash[32]; // Вызов SHA256/Scrypt crypto_hash(data, nonce, hash); if (hash[0] == 0 && hash[1] == 0) { // проверка сложности send_to_pool(hash, nonce); } } |
Шаг 3: Поиск pool адреса
Майнеры обычно хранят адрес пула в секции data. Смотрим через wasm2wat:
|
1 |
(data (i32.const 1024) "wss://pool.monero.org:3333") |
Бинго! Майнер подключается к Monero пулу.
Шаг 4: Блокировка
Создаём Content Script для браузера:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Chrome Extension const originalFetch = window.fetch; window.fetch = function(...args) { const url = args[0]; // Блокируем загрузку WASM майнеров if (url.includes('.wasm') || url.startsWith('data:application/wasm')) { console.log('[Blocked] WASM miner detected:', url); return Promise.reject('Blocked by anti-miner'); } return originalFetch.apply(this, args); }; |
Или используем uBlock Origin с правилом:
|
1 |
||*.wasm^$script,domain=suspicious-site.com |
Продвинутые техники реверсинга
Динамический анализ через Chrome DevTools:
Ставим breakpoint на функции WASM:
|
1 2 3 4 5 6 7 8 9 10 |
// В консоли const wasmInstance = /* получаем инстанс */; const originalFunc = wasmInstance.exports.calculateReward; wasmInstance.exports.calculateReward = function(...args) { console.log('[WASM Call] Args:', args); const result = originalFunc.apply(this, args); console.log('[WASM Return]:', result); return result; }; |
Дампинг памяти WASM:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Читаем линейную память const memory = wasmInstance.exports.memory; const buffer = new Uint8Array(memory.buffer); // Дампим первые 1KB console.log(buffer.slice(0, 1024)); // Ищем строки let str = ''; for (let i = 0; i < buffer.length; i++) { if (buffer[i] >= 32 && buffer[i] <= 126) { str += String.fromCharCode(buffer[i]); } else if (str.length > 4) { console.log('Found string:', str); str = ''; } } |
Патчинг на лету:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// Подменяем функцию в runtime const wasmInstance = await WebAssembly.instantiateStreaming( fetch('game.wasm') ); // Перехватываем экспортированную функцию const original = wasmInstance.instance.exports.checkLicense; wasmInstance.instance.exports.checkLicense = () => 1; // всегда true // Или модифицируем память const memory = new Uint8Array(wasmInstance.instance.exports.memory.buffer); memory[0x1000] = 0xFF; // меняем флаг isPremium |
Обход обфускации
Некоторые WASM модули используют обфускацию:
- Name mangling — функции названы как
$func123вместоcalculateDamage - Dead code — тысячи пустых функций для запутывания
- Control flow flattening — превращение if/else в switch с множеством case
Способы борьбы:
- Анализ export/import — начинай с публичных функций
- Cross-reference — следи за вызовами через Ghidra/IDA
- Эмуляция — запускай подозрительные функции с разными параметрами
- Статистический анализ — большие функции (> 1000 инструкций) обычно важные
|
1 2 3 4 5 6 7 8 9 |
# Скрипт для поиска "интересных" функций import wasmtime # Загружаем WASM with open('obfuscated.wasm', 'rb') as f: wasm_bytes = f.read() # Ищем функции с большим количеством инструкций # (используй wasm-objdump для анализа) |
Автоматизация через скрипты
Ghidra script для поиска crypto функций:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# findCrypto.py (Ghidra) from ghidra.program.model.block import BasicBlockModel bbm = BasicBlockModel(currentProgram) funcManager = currentProgram.getFunctionManager() for func in funcManager.getFunctions(True): body = func.getBody() size = body.getNumAddresses() # Криптофункции обычно большие и с циклами if size > 500: print("Suspicious function: {} (size: {})".format( func.getName(), size )) |
Frida для runtime hook:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Проверка целостности const expectedHash = 'sha256-abc123...'; fetch('game.wasm') .then(r => r.arrayBuffer()) .then(async buffer => { const hash = await crypto.subtle.digest('SHA-256', buffer); const hashHex = Array.from(new Uint8Array(hash)) .map(b => b.toString(16).padStart(2, '0')).join(''); if ('sha256-' + hashHex !== expectedHash) { throw new Error('WASM integrity check failed!'); } return WebAssembly.instantiate(buffer); }); |
Ресурсы для углубления
- wasmdec — декомпилятор WASM в C (github.com/wwwg/wasmdec)
- wasm-reverse — набор инструментов для анализа
- r2 (radare2) — командная альтернатива Ghidra
- awesome-wasm-reversing — список ресурсов на GitHub

На этом все. Всем хорошего дня!
