Anchor — це найпопулярніший фреймворк для розробки в екосистемі Solana. Він значно знижує бар’єри для входу завдяки декларативній перевірці облікових записів, автоматичній серіалізації та вбудованим перевіркам безпеки. Однак, надаючи зручність, фреймворк у тилі додає до кожної програми деякі внутрішні інструкції, яких розробники можуть не усвідомлювати. Ці «приховані» інструкції можуть бути використані зловмисниками в певних умовах, що призводить до серйозних втрат коштів.
Команда безпеки Beosin розкриє ключовий шаблон вразливості: у старих версіях Anchor, коли розробник використовує AccountInfo для оголошення PDA-рахунків, що належать програмі, зловмисник може за допомогою інструкцій IDL, автоматично вбудованих Anchor, за двома кроками заволодіти контролем над рахунком та вивести всі SOL, не отримуючи жодних привілеїв.
I. Аналіз інструкцій IDL та відповідних механізмів
1.1 IDL інструкція
Anchor за замовчуванням впроваджує набір інструкцій IDL (Interface Definition Language, мова визначення інтерфейсу) у кожну програму, якщо під час збирання не увімкнено функцію no-idl. Ці інструкції включають:
IdlCreateAccount: створення аккаунту IDL у ланцюзі
IdlWrite: запис даних у обліковий запис IDL / буферний обліковий запис
IdlSetAuthority: зміна authority (контролюючого) облікового запису IDL
IdlCloseAccount: закрити обліковий запис IDL і переказати всі lamports вказаному отримувачу
IdlResizeAccount: зміна розміру облікового запису IDL
IdlCreateBuffer: створення облікового запису буфера IDL (IDL Buffer)
IdlSetBuffer: замінити дані офіційного IDL-рахунку даними з рахунку буфера
Ці інструкції були розроблені для управління IDL у ланцюжку, але вони надають спеціальні права до облікових записів, що належать програмі (читання та запис даних, зміна контролера, закриття та переказ lamports) — саме це й є суть вразливості. Зловмиснику не потрібно викликати жодну з бізнес-інструкцій, написаних розробником — достатньо безпосередньо викликати ці вбудовані інструкції.
1.2 Буферний рахунок IDL
IDL Buffer — це тимчасовий обліковий запис, введений Anchor для поетапного завантаження великих даних IDL. Оскільки повний IDL (після стиснення в JSON) може перевищувати обмеження розміру однієї транзакції, Anchor дозволяє спочатку створити буфер за допомогою IdlCreateBuffer, потім записати дані частинами за допомогою кількох IdlWrite, і нарешті одноразово надіслати їх за допомогою IdlSetBuffer.
Ключовим є структура даних IDL-рахунку / буферного рахунку: він починається з заголовка з фіксованою компоновкою, який містить поле authority: Pubkey (у виводі тесту цього документа воно називається controller). Інструкції IDL використовують це поле для визначення «хто має право на керування цим рахунком».
Проблема полягає в тому, що логіка обробки IdlCreateBuffer у старіших версіях Anchor ініціалізує будь-який обліковий запис, переданий з цієї програми, як буферний обліковий запис і безпосередньо встановлює authority як підписувача транзакції. Іншими словами, якщо власником облікового запису є ця програма (наприклад, PDA-скарбниця програми), зловмисник може встановити себе як його контролера, а потім за допомогою IdlCloseAccount легітимно вивести всі SOL з цього облікового запису.
1.3 Умови виклику вразливості
Для виклику цієї вразливості необхідно одночасне виконання таких умов:
- Використання старої версії Anchor: небезпечна інструкція IDL не має достатньої перевірки облікових записів, що дозволяє помилково обробляти бізнес-облікові записи як IDL-облікові записи.
- no-idl не увімкнено: під час збирання програми зберігається за замовчуванням вбудований вхід IDL, що відкриває атакувану поверхню.
- Оголошення PDA, яким володіє програма, за допомогою AccountInfo: розробники використовують сирі AccountInfo для завантаження рахунків засобів (наприклад, скарбниці PDA), що не містять discriminator / owner, які є в типізованих рахунках Anchor (наприклад, Account), що робить цей рахунок нерозрізнюваним з рахунками IDL у інструкціях IDL.
- Обліковий запис належить програмі та містить lamports: owner == ця програма — умова, за якої інструкції IDL можуть з ним працювати; має сенс очищати лише тоді, коли він містить SOL.
Після виконання вищезазначених умов зловмиснику достатньо двох звичайних транзакцій: спочатку IdlCreateBuffer, щоб захопити контролер, а потім IdlCloseAccount, щоб переказати всі SOL, щоб завершити атаку, не отримуючи жодних дозволів від цільової програми.
1.4 Ланцюг атак
Нижче, з урахуванням внутрішньої реалізації Anchor, поетапно розглянемо, як зловмисник може за допомогою лише двох вбудованих команд «приховати» звичайний кошик коштів як обліковий запис IDL і повністю його опорожнити.
Огляд процесу атаки
Крок 1: IdlCreateBuffer(treasury, signer = attacker)
└─ treasury.controller ==> attacker (стає контролером)
Крок 2: IdlCloseAccount(treasury, authority = attacker, dest = attacker)
└─ treasury.lamports ==> attacker (вичерпує скарбницю)
Крок 1: IdlCreateBuffer викрадає повноваження
Внутрішня реалізація Anchor приблизно така:
#[derive(Accounts)]pub struct IdlCreateBuffer {
#[account(zero)] // ← Key: акаунт з його дискримінатором, заповненим нулями pub buffer: Account,
pub authority: Signer,}
pub fn idl_create_buffer(ctx: Context) -> Result
{
let idl = &mut ctx.accounts.buffer; idl.authority = *ctx.accounts.authority.key; // встановити attack як повноважену особу Ok(())}
Значення #[account(zero)] полягає в тому, що приймається акаунт з discriminator, що складається з нулів і належить програмі, як неініціалізований акаунт IDL для ініціалізації. Vault одночасно задовольняє обидві ці умови:
Стан vault
Умови
належить програмі
після init_if_needed власником встановлено цю програму
дискримінатор повністю нульовий
Використовуйте тип AccountInfo, Anchor не записує дискримінатор, дані нульові
Тоді атакуючий передає vault у IdlCreateBuffer:
- Anchor записує discriminator IdlAccount у перші 8 байтів vault;
- Запишіть публічний ключ атакувача у поле authority.
На цьому vault був «підімнений» під IdlAccount із атакувачем як authority — а всередині нього SOL залишився незчепленим.
Другий крок: IdlCloseAccount —— Очистити кошти
#[derive(Accounts)]pub struct IdlCloseAccount { #[account(mut, has_one = authority)] // ← check authority == signer pub account: Account, pub authority: Signer, #[account(mut)] pub destination: AccountInfo, // ← attack wallet}
В цей момент vault повністю пройшов перевірку:
- дискримінатор відповідає IdlAccount
- поле authority = публічний ключ атакувача
- has_one = authority перевірка пройдена
Отже, всі lamports (усі SOL, внесені користувачем) у vault були законно переведені на рахунок атакуючого, і відсоток став нульовим.
Основна причина:
Використання AccountInfo у deposit.rs замість типізованих Anchor Account є смертельним корінням цієї вразливості:
(1) Акаунти з типами (наприклад, Account) під час ініціалізації записують 8-байтовий дискримінатор, властивий цій структурі;
(2) Після запису власного дискримінатора Anchor більше не може вважати його IdlAccount (дискримінатор не збігається), і перший крок IdlCreateBuffer завершиться невдачею, що призводить до розриву ланцюжка атаки.
Другий. Аналіз випадків
Тестування засноване на контракті мосту PoC, збудованому на Anchor 0.31.0. Тест симулює справжній мостовий скарбницю (Bridge Treasury), власником якої є сам мостовий програмний код, що містить 1,001281 SOL, внесені користувачами. Гаманець атакуючого спочатку мав 2 SOL і не мав жодних привілеїв.
Проект
Значення / Опис
Мостова програма (Bridge Program)
CJutNlr8d3oxdb11ReOLaZd5jPsqUxgwt2mv5e7equtE
Казначейство (Treasury)
DxGkzaMhP5Wy824GhoehErdD6MudfuEPGPwaV2s77FsM
Власник KuCoin
= Програма-власник PDA
Початковий баланс коштовностей
1,001281 SOL (кошти користувача)
Початковий контролер
0x0000...0000 (усі нулі, не встановлено)
Початковий баланс гаманця атакувача
2.000000 SOL
Потрібні привілеї
ЖОДНОГО (не потрібно жодного дозволу)
Кількість угод
2 угоди
Фінальний залишок коштів
0,000000 SOL (вичищено)
Зловмисник отримав прибуток
+1.001281 SOL
Знімок екрану виконання PoC (tests/poc-idl-hijack.ts):
Як видно, IdlCreateBuffer у кроці 1 встановлює атакувача як контролера скарбниці (баланс не змінюється); IdlCloseAccount у кроці 2 переводить усі 1,001281 SOL скарбниці на гаманець атакувача, після чого баланс скарбниці стає нульовим.
Рекомендації щодо виправлення/захисту:
(1) Оновлення версії Anchor: остання версія виправила цю проблему (інструкції IDL строго розрізняють облікові записи IDL та бізнес-облікові записи); уникання використання старих версій Anchor — найпростіший і найефективніший захисний захід.
(2) Увімкніть no-idl під час збирання: явно вимкніть ін’єкцію інструкцій IDL для програм у виробничому середовищі, щоб усунути цю поверхню атаки з самого початку.
(3) Використовуйте типізовані акаунти замість сирого AccountInfo: використовуйте типи Account / SystemAccount з перевіркою discriminator та owner для зберігання коштів, щоб уникнути помилкового визначення цих акаунтів інструкціями IDL.
(4) Мінімізація облікових записів, якими володіє програма: додайте чіткі обмеження owner / seeds / discriminator для PDA, де зберігаються кошти, і перевіряйте ідентифікатори облікових записів у ключових інструкціях.
Висновок
Сутністю цієї вразливості є поєднання «прихованих інструкцій фреймворку + відсутності перевірки типу облікового запису». Розробники повинні усвідомити, що інструкції IDL, які за замовчуванням вставляються Anchor, є реальними векторами атаки; оновлення версії фреймворку та застосування суворих типів до облікових записів з коштами дозволить ефективно запобігти такому ризику неавторизованого зняття коштів.
Beosin — це лідируюча компанія у сфері безпеки блокчейну та відповідності регуляторним вимогам, яка спеціалізується на аудиті безпеки смарт-контрактів до запуску проекту, моніторингу та блокування ризиків безпеки під час роботи проекту, відновленні вкрадених коштів, протидії відмиванню коштів (AML) та слідчим діям щодо віртуальних активів. Beosin надає «однохвилинні» рішення для відповідності блокчейну та послуги безпеки для 200+ постачальників віртуальних активів, 4500+ проектів Web3 та регуляторних та правоохоронних органів у понад 20 країнах та регіонах світу. Будь ласка, зв’яжіться з нами через поле для повідомлень у нашому каналі.

