выебано
December 31, 2023

Трахаем бебру или debank reverse enginering (wasm)

Бебра, ищи себя в прошмандовках склада хуйни

Вступление

Берем линку и грузим, посмотрим чё вертает сайт

https://debank.com/profile/0x53d877159bfe5c693aacc51a9c0c05b7ef70e52b/

И так, мы имеем парочку хэдеров: x-api-nonce, x-api-sign, x-api-ts, x-api-ver. Чекаем след запрос.

При запросе инфы по юзеру, еще парочка "подписей": random_at, random_id

Генерация random_at и random_id на деле рил рандом. Это просто Date.now() и uuid.

Пример кода для uuid

function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-bxxx-xxxxxxxxxxxx'.replace(/[xb]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }

var browser_uid = { random_at: Math.floor(Date.now() / 1e3), random_id: uuidv4().replace(/-/g, "") }

Чекаем дальше. Посмотрим, какие функции участвовали в кастовании запроса

Акей, ставим брек на функцию феч и видим нужные данные, чекаем кто её вызвал

Fetch это case 35, смотрим чуть выше)

Так, хедеры есть, остается понять от куда они появляются туть. Но как не трудно заметить, h.a судя по всему, нужная нам функция. Кейсы тут чтобы фиксить ебучие промисы.

Штош, вся ебля туть! А точнее, это ключевой файл со всеми функциями для создания подписей.

Какие выводы на текущий момент из этого можно извлечь?

x-api-ts - просто Date.now(). function T() {return Math.floor(Date.now() / 1e3)}

x-api-ver - v2, тупо статик

x-api-nonce - E возвращает nonce (используя wasm функцию r_s), который, кстати говоря, довольно интересный. У каждого n запроса одинаковый nonce в каждой сессии. Кажется, что это просто числа по порядку, но в шифрованном виде.

x-api-sign - а эт подпись, которая получает какой-то хэш от всех данных через функцию z (используя wasm функцию mk_s_f)

Пока выглядит не страшно, вроде всё как обычно

А теперь немного про WASM

Когда впервые я увидел wasm, я просто ахуел. Я пытался понять че мне делать с этой информацией, типо тут же овердохуя строк. Как это вообще блять реверсить?

Мое крайне удивленное выражение лица

Но на самом деле, все не совсем так страшно, но всё равно страшна)

У гугла есть пару официальных статеек с примерами по wasm и юзу через dev tools, хотя я их не читал, на самом деле, наверное стоило бы.

https://developer.chrome.com/blog/wasm-debugging-2020/
https://developer.chrome.com/docs/devtools/wasm/
https://developer.chrome.com/docs/devtools/memory-inspector/
https://developer.chrome.com/blog/memory-inspector-extended-cpp/

Например, используя memory inspector кому-то удобнее было бы чекать все что происходит в памяти, нежели как я, тыкать консольку

Похуярили

Начнём с nonce.

И так, нам нужен nonce. Однако, сразу при старте у нас пока его нет, поэтому мы передёрнем его через функцию E. Она берет цифру 40 и передает в функцию u, после чего через wasm функцию r_s кастует какой-то мэджик и через функцию o вытягивает результат.

Судя по всему, функция l создаёт новую область в памяти wasm (прост чуть увеличивает массив по позиции), а через функцию u мы просто возвращаем новый массив размера, который передали, на который выделили память.

Функция же o принимает от куда и до куда в массиве отрезаем.

Массив memory.buffer от 17824 до +40 и декодит через TextDecoder("utf-8")

Я решил, что могу тупо попробовать взять и чекнуть фул память этой "вирт машины", мож че интересное есть.

Получается, наш nonce действительно есть в памяти. Интересно, а был ли он там до этого и было ли чет еще?

До вызова r_s было пусто, а после вызова появилась в памяти строка. Значит r_s всё-таки чёт шифрует.

WASM decompile

Я решил сильно не ебать себе мозгу и попробовал заюзать декомпилятор васм в C, чтобы код стал читабильнее.

Скачал с гита архивчег, распаковал, запустил через консольку сразу конкретный скрипт

Почему-то скачанный из хрома wasm файл не подошел (возможно у меня руки кривые), поэтому я тупо перевел wat (декод wasm) из дев тулз в wasm и вот уже его прога схавала.

wat2wasm
https://webassembly.github.io/wabt/demo/wat2wasm/
wasm-decompile
https://webassembly.github.io/wabt/doc/wasm-decompile.1.html
команда:

test.wasm -o test.dcmp

При установке на дедик не хватало какой-то dll, я ее прост с инета докачал и закинул в ту же папку.

А вот и наш файлик. Всё стало на много более читаемым)

Вот наша функция. Первое (страшная бяка) - это wasm, второе это декод wasm, третье стилизация под js от chatgpt.

А начать придется с func59. Эта хуета в конкретной ячейке хранит число, постоянно его теребит и уже с ним работает вся математика.

Если сравнить 2 куска кода, становится очевидно, что mul (*), add (+), shr_u(>>)

Из ячейки 4488 оно тянет значение, пересчитывает и перезаписывает на новое, при том также возвращает как результат функции его, но со сдвигом.

По сути wasm просто закидывает чиселки в стек через const, выполняет с ними какие-то операции, пишет в переменные через tee, дергает из переменной через get и прыгает по меткам. Вся остальная работа с данными происходит чисто по индексам.

Документация

Получаем что-то вроде этого, если приводить ближе к js.

function func59() { let var0 = BigInt(0); const multiplier = BigInt('6364136223846793005'); const increment = BigInt(1);

var0 = (var0 * multiplier + increment);

return Number(var0 >> BigInt(33)); }

Вот вроде выглядит норм, но оно не работает)

Въебал 2 часа, чтобы найти эту хуету (wrap i64). Тыкал доку васм, чекал гит, тыкал доку жыес, тыкал chatgpt (тупая сука), в итоге, случайно наткнулся в доке по bigint. Тяжело быть тупым и тыкать наугад не зная базы(

https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Numeric/Wrap
https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Numeric/Multiplication
https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/BigInt/asIntN

Норм код по итогу

local = 0n; function pcg32_random() { const loadValue = BigInt(local); const temp = loadValue * 6364136223846793005n + 1n; local = BigInt.asIntN(64,temp); const result = Number(BigInt.asUintN(64,local) >> 33n); return result; }

Смотрим дальше. Оно взяло чисто метку с входа, далее выполнило свой говнокод получив 4.527851014643838, а потом превратило в целое 4 и прибавило 1024, получив 1028.

Напоминаю про декомпиленый код

Через load8_u грузим число в стек из памяти по индексу

Не трудно догадаться, что эт просто алфавит. То есть из него мы просто дергаем буквы и цифры по индексам. По сути, вот и вся генерация nonce.

В памяти у нас лежат
$var2: i32 {value: 1} $var3: i32 {value: 40} в цикле оно ебошит if (var3 !== var2) { goto_B_c(); }
Как только наебошили 40 символов, кончаем

Вот эта хуета делает число
var var6 = f64_convert_i32_s(func59()) / 2147483647.0 * 61.0;
Далее она переводим его в целое, а потом прибавляет 1024 и записываем в массив. Выходной массив и будет наша хуйня.

Итоговый код для nonce:

local = 0n; function pcg32_random() { const loadValue = BigInt(local); const temp = loadValue * 6364136223846793005n + 1n; local = BigInt.asIntN(64,temp); const result = Number(BigInt.asUintN(64,local) >> 33n); return result; } function nonce_gen(){ var abc = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz' var result = []; for(var i=0; i< 40; i++){ var index = parseInt(pcg32_random() / 2147483647.0 * 61.0); result.push(abc[index]) } return result.join('') }

Однако.. Нас еще ждет sign и там тоже всё очень интересно)

Ёбём дальше - sign

В неё мы закидываем инфу о запросе

Парой строк можем почекать чё у нас в памяти.

mem = new Uint8Array(s.memory.buffer,e,t), td = new TextDecoder("utf-8"); td.decode(mem)

И так, мы передали индексы, где в памяти положили данные. Дальше идёт вызов make_keystring и make_reqstring, а потом make_sign.

Хех, защит очка от копипейста васм, в функции make_keystring

env_hNHD вызывает какие-то хитрые штуки, которые тупо чекают, что домен это бебра

make_keystring(var3, var4,var6 = malloc(func52(var3) + func52(var4) + 12)); mem = new Uint8Array(memories.$memory.buffer,18272,64), td = new TextDecoder("utf-8"); td.decode(mem)

Результат: 'debank-api\nn_0LknmB7aJePWhQezXR3SFQeLmf0Q3wDDnSpgDJxS\n1690640838'
тоесть оно взяло и добавило туда бебру свою в начало. То есть оно взяло 2 переменные, передало, чекнуло сколько надо памяти и вхуячило.

А дальше make_reqstring(var0,var1, var2,var3 = malloc(func52(var0) + func52(var1) + func52(var2) + 2));
Она передала 3 переменные и посчитала их длину. Хуяк и в новом дампе тож все кошерно уложено
mem = new Uint8Array(memories.$memory.buffer,18512,62), td = new TextDecoder("utf-8"); td.decode(mem)

Результат: 'GET\n/user/config\nid=0x53d877159bfe5c693aacc51a9c0c05b7ef70e52b'

Простыми словами, что make_keystring (var6), что make_reqstring (var2), обе на деле просто склеивают 2 строки в 1. Только make_keystring еще и с ловушкой)

make_sign(var6, var3, var2 = malloc(64));

В make_sign мы считаем хэши от наших строк, а потом перекидываем в func13

А дальше еще есть func13 с кучей какой-то хуйни..

Мысли на 20+ часу реверса этой поеботы.

Заполняем 3 куска данными через func48: 0, 54, 92

Небольшая заметка по функциям туть:
func50 просто перекидывает данные в новое место
func52 просто чекает количество элементов
func48 циклично заполняет элементом

Получается, мы взяли:
func48(var6 + 192, 0, 64);

Заполнили числом 64.
Потом перекинули.
func50(var6 + 192, var0, var1);

mem = new Uint8Array(memories.$memory.buffer,17504+64,192), td = new TextDecoder("utf-8"); td.decode(mem)
По итогу в памяти была записана вот такая хуйня
'\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\6666666666666666666666666666666666666666666666666666666666666666\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

Я решил переписать сразу цикл с xor на понятный язык

Прям в процессе, берет чиселки и ксорит, а результат перезаписывает на текущую позицию. То есть берет палку и 6, ксорит и на их место ставит результат ксора

А дальше я часа 2 не мог понять, че за за колдунства ебучие происходят. Функция func15 меня сильно напрягала, а оказалось все довольно просто.

После этого в func13 у нас идут

func15(var6 + 128, var2, var3, var6 + 32);

func15(var6 + 64, var6 + 32, 32, var6);

func13 вызывает func14, которая вызывает func16, func17, func19 + оставшаяся часть в make_sign, выглядит как и sha256_hash. Я подумал, а почему бы и да..

И это действительно оказалось так. Я еще подумал "нихуя я умный". Потому как дальше, остается просто всё собрать.

В первый вызов func15 были переданы данные:
'\x01\x02PT\x01\x02\x02R\x01\x0E\x00\x02\x02US\x02\x00\x04\x05\x07UU\x02\x06T\x00\x05\x06WW\x0FSR\x00\x07\x02T\x00ST\x03UP\x06T\x04W\x07\x06\x04\x02\x0E\x03\x04S\x07\x02UURU\x0F\x0EW7b5450037e97b98cc8f29ae99e07edff12276fe9841bbdef8ef4f8471dab9695'
Они состоят из:
1) XOR с (54) первого хэша \x01\x02PT\x01\x02\x02R\x01\x0E\x00\x02\x02US\x02\x00\x04\x05\x07UU\x02\x06T\x00\x05\x06WW\x0FSR\x00\x07\x02T\x00ST\x03UP\x06T\x04W\x07\x06\x04\x02\x0E\x03\x04S\x07\x02UURU\x0F\x0EW
2) Второй хэш 7b5450037e97b98cc8f29ae99e07edff12276fe9841bbdef8ef4f8471dab9695

Результат: q\x10��x�J\t0\x17�J"�j\x02.�@�x�??u\x00P���q


Во второй вызов func15 были переданы данные:
'kh:>khh8kdjhh?9hjnom??hl>jol==e98jmh>j9>i?:l>n=mlnhdin9mh??8?ed=q\x10��x�J\t0\x17�J"�j\x02.�@�x�??u\x00P���q'
Они состоят из:

1) XOR с (92) первого хэша kh:>khh8kdjhh?9hjnom??hl>jol==e98jmh>j9>i?:l>n=mlnhdin9mh??8?ed=
2) Результат первого вызова func15 q\x10��x�J\t0\x17�J"�j\x02.�@�x�??u\x00P���q

Результат: "�#'\x7F�w\x1F�~\x03Փ-��7���-�L�e!h_�(7"

Да, сам алго у них выглядит на самом деле довольно странным, но какой есть

Учитывая, что func15 это sha256, дальше всё становится очень просто

const hashHex = (hashArray) => hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")

А потом я закинул в бас ноду и почекал, что все заебись)

Итоговый код для sign:

const crypto = require('crypto');

function sha256_hash(data) { const hash = crypto.createHash('sha256'); hash.update(data); return hash.digest(); }

function xorHash(hash) { var rez1 = [], rez2 = []; for (var i = 0; i < 64; i++) { var char1 = hash[i].charCodeAt(0) ^ 54 var char2 = hash[i].charCodeAt(0) ^ 92 rez1.push(String.fromCharCode(char1)) rez2.push(String.fromCharCode(char2)) } return [rez1.join(''),rez2.join('')] }

function signature(data1, data2) { const hash1 = sha256_hash(data1).toString('hex'); const hash2 = sha256_hash(data2).toString('hex'); const xorData = xorHash(hash2); var xor1 = xorData[0]; var xor2 = xorData[1]; var h1 = sha256_hash(Buffer.from(xor1 + hash1, 'utf-8')); var h2 = sha256_hash(Buffer.concat([(Buffer.from(xor2, 'utf-8')), h1])) return h2.toString('hex'); } const data1 = 'GET\n/user\nid=0x53d877159bfe5c693aacc51a9c0c05b7ef70e52b'; const data2 = 'debank-api\nn_dnNAOSWvzwmQiOQu52o7i0ohyaT7vnL89JvKD1tI\n1690786847'; [[SIGN]] = signature(data1, data2)

За неделю до

Мне отписал O

Я бебру ранее чуть уже тыкал, но васм меня напугал и больше 5 часов я тыкать в первый раз не стал, а потому поведал товарищу о том, что смог найти.

По итогу у меня ушло в первый дечь часов 14 и во второй часов 16, + до этого пусть часов 5, примерно дня 3 условно и вышло)

Товарищ был неуклонен, он был уверен в своих силах (надо же где-то взять деняк на дошик) и потому я накидал пару мыслей.

Тем не менее, это была просто гипотеза, я и не думал всёрьез, что оно реально может так легко сработать

Но он сидел и всё это тестил

Утром

Я был мягко говоря удивлён

Оставалось докинуть удобную обертку, чтоб юзать в многоптоке, как было с сокетами

Однако, на бОльшем числе запросов все же фродило

Буквально уже на след утро

А вот собственно, что и фродило)

Он просто заменил кусок антифрода на флаг true и эт реально прокатило

Поэтому, я решил тож потыкать бебру, хотя изначально сомневался

Итог

Немного обидно, что действительно в данном конкретном случае копипейст, хоть и не без проблем, но был быстрее. Нахуя я тогда все это тыкал если в перспективе со всеми этими фродами, пов, придется юзать хэндлес?

С другой стороны, потыкали васм разными подходами, вроде даже чет полезное узнал.