DeFi. Безопасность. Дрейнеры. Разбираемся с подменой транзакций и эмуляций
Дисклеймер
Всё описанное - здесь исключительно потому, что мне нравится исследовать p2p-системы, в том числе, но не ограничиваясь, блокчейны, DAGs, etc., смарт-контракты и прочее. Но всё описанное никогда нельзя применять на Тёмной стороне силы: материал потому представлен исключительно в образовательных целях и я как автор и переводчик не несу ответственность за любые возможные последствия, в том числе и в первую очередь - негативные. Будьте бдительны и исследуйте, а не применяйте.
TL;DR
Суть атаки:
- Фишинговый сайт предлагает совершить транзакцию - «Claim», т.е. затребовать некие токены;
- Симуляция кошелька отображает получение незначительной суммы ETH (например, 0.000...0001 ETH);
- Тем временем бэкэнд фишингового сайта изменяет состояние контракта;
- Жертва, не зная об изменении состояния, подписывает транзакцию;
- Фактическая транзакция выполняется, опустошая кошелёк жертвы полностью.
Полный разбор: drops.scamsniffer.io/transaction-simulation-spoofing-a-new-threat-in-web3.
Исследуем процесс подмены
Сама транзакция: https://etherscan.io/tx/0x014321fbace3c22ade53fd34a81981c92b499451e70bc840f567bc22c95de700:
- Фишинговый сайт инициирует перевод через «Claim».
- Кошелёк имитирует получение крошечного ETH (0.000...0001 ETH).
- Бэкэнд изменяет состояние контракта.
- Фактическая транзакция опустошает кошелёк.
См. оригинал здесь
Давайте чуть подробней: etherscan.io/address/0x000008e4e9597890e93f60b4d8dd3610e1700000#events:
Давайте отследим создание смарта:
- 0x000008e4e9597890e93f60b4d8dd3610e1700000 => Fake_Phishing877806
- 0x505506fbb2d540e209fe5ed93ce1be50a877bba9 => создатель Fake_Phishing877806
- 0x000037bb05b2cef17c6469f4bcdb198826ce0000 => Fake_Phishing188250: создатель 0x505…bba9
- 0x854dda621785dca278df9b298825f2ec32578b76 => создатель Fake_Phishing188250
- 0x0000553f880ffa3728b290e04e819053a3590000 => создатель 0x854…b76 с громким именем: inferno-drainer-4.eth и пометкой: Fake_Phishing182232
- 0x00003d1cef5f30e34510198cb045e4ae0aa20000 => создатель Fake_Phishing182232
- 0x17d5d3d6c93ac70b8ae1c6ca4e8318968894a271 => создатель 0x000…20000 (кстати, похоже, что делали на профанити или аналогичном ПО)
- 0x00001f78189be22c3498cff1b8e02272c3220000 - самое интересное, что тут мы закольцовываемся, т.к. это уже inferno-drainer-3.eth
Вывод из этого простой: если проследить путь хотя бы до 2-5 связок, то можно понять, что с контрактом что-то не так. Плюс - он не верифицирован: к сожалению, крупные команды тоже этим грешат, чем лишь усложняют жизнь простым пользователям.
- Смарт не верифицирован;
- Молодой (мало времени с момента его разворачивания);
- Связан с другими “мутными” смартами;
- И набор его функций не ясен, то
Не жадничайте - обождите. Тем более, если у вас есть 143 ETH или что-то в этом роде.
Первичные же эфиры для всей этой фабрики дрейнеров поступили из sideshift, так что пополнить их могут с любого моста, обменника, кошелька, агрегатора, биржи…
Что ещё можно сказать? Сам контракт не верифицирован, поэтому можно лишь догадываться о том, что он делает по байт-коду и опкод-инструкциям:
Попробую подключить сюда ChatGPT и оставлю то, что мне кажется логичным:
- Функции контракта: В байт-коде видны два идентификатора функций:
- 0x0a3b0a4f: Это хеш сигнатуры функции (например, functionName(uint256)), которая определяет вызываемый метод.
- 0xbabcc539: Это другой хеш функции.
- Для более точного анализа нужно определить сигнатуры функций, но видно, что контракт имеет как минимум две функции.
- Прим. Menaskop: так и есть: ADD - первая, и вторая - CLAIM;
- Работа с msg.sender или адресами: В коде активно используются проверки и вызовы адресов. Например:
- Адреса передаются в качестве параметров.
- Проверяются 0xffffffffffffffffffffffffffffffffffffffff, что говорит о проверках на корректность адресов (например, функции могут работать только с определёнными контрактами или аккаунтами).
- Прим. Menaskop: что тоже логично, т.к. на сам контракт могут напасть, к тому же - ему нужно опустошать тех, у кого есть, что опустошать;
- Использование событий:
- Присутствует вызов логов событий (например, LOG), что говорит о том, что контракт отправляет данные для анализа вне блокчейна:
- Прим. Menaskop: Собственно, подмена на бэке логики - это подтверждает;
- Ошибки и возвраты:
- Контракт содержит проверку revert() при некорректных вызовах. Это стандартное поведение для защиты логики.
- Версия Solidity: В конце байт-кода указывается версия компилятора: solc v0.8.26. Это полезно для уточнения, какие особенности языка используются.
- Возможная обработка токенов:
Конечно, можно декомпилировать:
- app.dedaub.com/
- dashboard.tenderly.co/
- etherscan.io/bytecode-decompiler?a=0x000008e4e9597890e93f60b4d8dd3610e1700000
/ Decompiled by library.dedaub.com // 2025.01.20 11:20 UTC // Compiled using the solidity compiler version 0.8.26 // Data structures and variables inferred from the use of storage instructions mapping (address => bool) _add; // STORAGE[0x1] mapping (address => uint256) owner_2; // STORAGE[0x2] address _receive; // STORAGE[0x0] bytes 0 to 19 function _SafeAdd(uint256 varg0, uint256 varg1) private { require(varg0 <= varg1 + varg0, Panic(17)); // arithmetic overflow or underflow return varg1 + varg0; } function add(address _address) public nonPayable { find similar require(msg.data.length - 4 >= 32); _add[_address] = 1; v0 = _SafeAdd(21600, block.timestamp); owner_2[_address] = v0; } function receive() public payable { find similar v0 = 0xe7(msg.sender); if (!v0) { v1 = _SafeAdd(1, msg.value); v2, /* uint256 */ v3 = msg.sender.call().value(v1).gas(msg.gas); if (RETURNDATASIZE() == 0) { exit; } else { v4 = new bytes[](RETURNDATASIZE()); RETURNDATACOPY(v4.data, 0, RETURNDATASIZE()); exit; } } else { v5 = new bytes[](4); MEM[v5.data] = 0x9b2d7f6500000000000000000000000000000000000000000000000000000000 | uint224(MEM[v5.data]); MCOPY(MEM[64], v5.data, v5.length); MEM[MEM[64] + v5.length] = 0; v6, /* uint256 */ v7, /* uint256 */ v8 = _receive.call(MEM[MEM[64]:MEM[64] + 4], MEM[MEM[64]:MEM[64]]).gas(msg.gas); if (RETURNDATASIZE() == 0) { v9 = v10 = 96; } else { v9 = v11 = new bytes[](RETURNDATASIZE()); RETURNDATACOPY(v11.data, 0, RETURNDATASIZE()); } require(v8 + MEM[v9] - v8 >= 32); require(MEM[v8] == address(MEM[v8])); v12, /* uint256 */ v13 = address(MEM[v8]).call().value(msg.value).gas(msg.gas); if (RETURNDATASIZE() != 0) { v14 = new bytes[](RETURNDATASIZE()); RETURNDATACOPY(v14.data, 0, RETURNDATASIZE()); } if (v12) { emit 0xfceb437c298f40d64702ac26411b2316e79f3c28ffa60edfc891ad4fc8ab82ca(msg.sender, msg.value); } exit; } } function isAllowed(address varg0) public nonPayable { find similar require(msg.data.length - 4 >= 32); v0 = 0xe7(varg0); return bool(v0); } function 0xe7(address varg0) private { if (!_add[varg0]) { return _add[varg0]; } else { return block.timestamp <= owner_2[varg0]; } } // Note: The function selector is not present in the original solidity code. // However, we display it for the sake of completeness. function __function_selector__( function_selector) public payable { MEM[64] = 128; if (msg.data.length < 4) { if (!msg.data.length) { receive(); } } else if (0xa3b0a4f == function_selector >> 224) { add(address); } else if (0xbabcc539 == function_selector >> 224) { isAllowed(address); } v0 = 0xe7(msg.sender); if (!v0) { v1 = _SafeAdd(1, msg.value); v2, /* uint256 */ v3 = msg.sender.call().value(v1).gas(msg.gas); if (RETURNDATASIZE() == 0) { exit; } else { v4 = new bytes[](RETURNDATASIZE()); RETURNDATACOPY(v4.data, 0, RETURNDATASIZE()); exit; } } else { v5 = new bytes[](4); MEM[v5.data] = 0x9b2d7f6500000000000000000000000000000000000000000000000000000000 | uint224(MEM[v5.data]); MCOPY(MEM[64], v5.data, v5.length); MEM[MEM[64] + v5.length] = 0; v6, /* uint256 */ v7, /* uint256 */ v8 = _receive.nativeCryptoReceiver().gas(msg.gas); if (RETURNDATASIZE() == 0) { v9 = v10 = 96; } else { v9 = v11 = new bytes[](RETURNDATASIZE()); RETURNDATACOPY(v11.data, 0, RETURNDATASIZE()); } require(v8 + MEM[v9] - v8 >= 32); require(MEM[v8] == address(MEM[v8])); v12, /* uint256 */ v13 = address(MEM[v8]).call().value(msg.value).gas(msg.gas); if (RETURNDATASIZE() != 0) { v14 = new bytes[](RETURNDATASIZE()); RETURNDATACOPY(v14.data, 0, RETURNDATASIZE()); } if (v12) { emit 0xfceb437c298f40d64702ac26411b2316e79f3c28ffa60edfc891ad4fc8ab82ca(msg.sender, msg.value); } exit; } }
Собственно, опять же, подключу ChatGPT, но выберу лишь те тезисы, которые считаю: 1) важными, 2) логичными. Итак, анализ кода:
- Обработка платежей:
- Функция receive() предполагает приём средств, но содержит потенциально небезопасные вызовы, такие как msg.sender.call{value: ...}.gas(...), что является антипаттерном безопасности в Solidity.
- Прим. Menaskop: собственно, легко найти, что эта функция действительно часто используется для хакинга, например: https://ethereum.stackexchange.com/questions/42521/what-does-msg-sender-call-do-in-solidity
- Ещё точнее - так: а) Это функция, вызываемая при получении ETH. б) Сложная логика, вероятно, связана с перераспределением или передачей средств на адрес _receive. в) Возможно, реализует какой-то кастомный протокол.
- Кастомная логика проверки (0xe7):
- Логика проверяет, есть ли адрес в списке _add, а затем смотрит, не истек ли его срок действия (в поле owner_2).
- Прим. Menaskop: Это выглядит как некий механизм белого списка с временными ограничениями.
- Возможные уязвимости:
- Использование call() с возвратом значения и газа (например, в receive) уязвимо для атак повторного входа.
- Прим. Menaskop: собственно, речь идёт про подвид Replay attack, под которой, как известно, понимается: “атака на систему аутентификации путём записи и последующего воспроизведения ранее посланных корректных сообщений или их частей”, - это и наблюдаем в данном случае;
- Нет явной проверки безопасности при взаимодействии с внешними контрактами (например, _receive.call).
- Прим. Menaskop: это уже нужно для быстрой работы бэка - в том числе и для той самой подмены логики.
А теперь я добавлю ответ самого ChatGPT без моей корректуры и комментов:
- Атака повторного входа: Использование .call.value в функции receive() делает контракт уязвимым для атак повторного входа, особенно если _receive — это внешний контракт.
- Без явной авторизации: В функции add нет проверки, кто вызывает эту функцию. Это означает, что кто угодно может добавить адрес в список _add.
- Сложные инструкции для внешних вызовов: Логика передачи средств и вызовов функций через _receive.call выглядит неочевидной и, скорее всего, содержит скрытые уязвимости.
- Отсутствие событий: Контракт не записывает события при изменении _add или owner_2, что затрудняет аудит действий.
- Хаос в логике receive(): Код содержит лишнюю сложность и логически запутан. Такие конструкции нередко встречаются в вредоносных контрактах.
Как видим, он не сильно ошибается (если ошибается вообще). Отсюда - несколько советов.
Как противостоять такому типу атак?
Перечень, конечно же открытый:
- Проверку начинать не с контракта, а с сайта, бота и/или другого источника, где вы взаимодействуете: в 9 999 из 10 000 случаев вы получите данные о том, что ресурс - новый;
- Сам контракт проверять, как минимум:
- На верификацию;
- На то, кто его деплоил и ещё на 2-3 шага назад;
- На анализ со стороны AI;
- На транзакции: если мало холдеров у токена - насторожитесь, если мало транзакций и контракт недавно деплоили - насторожитесь и т.д.
- И обязательно используйте для проверки хотя бы 2 источника данных: это правило нерушимо везде и всюду.
Кстати, аудиторы предложили ещё и улучшения для кошельков на базе анализа атаки. Простых смертных они тоже касаются, т.к. важно понимать процесс:
- Динамическое обновление на основе времени блока: собственно, ведь вся атака свелась к этому - что блок верификации для эмуляции и настоящая транзакция прошли с разницей в целых 30 секунд (а это 2 блока - минимум);
- Принудительное обновление симуляции перед подписанием транзакции;
- Отображение меток времени симуляции и высот блоков;
- Интеграция чёрного списка фишинговых контрактов;
- Уведомления о устаревших результатах симуляции.
Сервисы, используемые для анализа ситуации
Прежде всего - отчёт ScamSniffer:
- x.com/realScamSniffer/status/1877559749690184087
- x.com/realScamSniffer/status/1877559809643524154
- drops.scamsniffer.io/transaction-simulation-spoofing-a-new-threat-in-web3/
- dashboard.tenderly.co/tx/mainnet/0x014321fbace3c22ade53fd34a81981c92b499451e70bc840f567bc22c95de700/logs
- app.dedaub.com/ethereum/address/0x000008e4e9597890e93f60b4d8dd3610e1700000/decompiled
- etherscan.io/bytecode-decompiler?a=0x000008e4e9597890e93f60b4d8dd3610e1700000
И сам эксплорер: /etherscan.io/address/0x000008e4e9597890e93f60b4d8dd3610e1700000#code
Плюс AI: chatgpt.com