Подведение итогов TON Hack Challenge
Эта страница переведена сообществом на русский язык, но нуждается в улучшениях. Если вы хотите принять участие в переводе свяжитесь с @alexgton.
TON Hack Challenge был проведен 23 октября. В TON mainnet было развернуто несколько смарт-контрактов с искусственно созданными уязвимостями. Каждый контракт имел баланс 3000 или 5000 TON, что позволяло участникам взломать его и немедленно получить вознаграждение.
Исходный код и правила контеста были размещены на GitHub [здесь] (https://github.com/ton-blockchain/hack-challenge-1).
Контракты
1. Паевой инвестиционный фонд
Всегда проверяйте функции на наличие модификатора impure
.
Первая задача была очень простой. Злоумышленник мог обнаружить, что функция authorize
не является impure
. Отсутствие этого модификатора позволяет компилятору пропускать вызовы этой функции, если она ничего не возвращает или возвращаемое значение не используется.
() authorize (sender) inline {
throw_unless(187, equal_slice_bits(sender, addr1) | equal_slice_bits(sender, addr2));
}
2. Банк
Всегда проверяйте наличие изменяющих/не изменяющих методов.
udict_delete_get?
вызывался с .
вместо ~
, поэтому реальный словарь остался нетронутым.
(_, slice old_balance_slice, int found?) = accounts.udict_delete_get?(256, sender);
3. DAO
Используйте знаковые целые числа, если вам это действительно необходимо.
Вес голоса хранился в сообщении как целое число. Злоумышленник мог отправить отрицательное значение при передаче веса голоса и получить бесконечное количество голосов.
(cell,()) transfer_voting_power (cell votes, slice from, slice to, int amount) impure {
int from_votes = get_voting_power(votes, from);
int to_votes = get_voting_power(votes, to);
from_votes -= amount;
to_votes += amount;
;; No need to check that result from_votes is positive: set_voting_power will throw for negative votes
;; throw_unless(998, from_votes > 0);
votes~set_voting_power(from, from_votes);
votes~set_voting_power(to, to_votes);
return (votes,());
}
4. Лотерея
Всегда рандомизируйте начальное значение перед выполнением rand()
Начальное значение было получено из логического времени транзакции, и хакер может выиграть, применяя перебор логического времени в текущем блоке (потому что lt последовательно в пределах одного блока).
int seed = cur_lt();
int seed_size = min(in_msg_body.slice_bits(), 128);
if(in_msg_body.slice_bits() > 0) {
seed += in_msg_body~load_uint(seed_size);
}
set_seed(seed);
var balance = get_balance().pair_first();
if(balance > 5000 * 1000000000) {
;; forbid too large jackpot
raw_reserve( balance - 5000 * 1000000000, 0);
}
if(rand(10000) == 7777) { ...send reward... }
5. Кошелек
Помните, что все хранится в блокчейне.
Кошелек был защищен паролем, его хеш был сохранен в данных контракта. Однако блокчейн помнит все - пароль был в истории транзакций.
6. Сейф
Всегда проверяйте отскочившие сообщения. Не забывайте об ошибках, вызванных стандартными функциями. Сделайте свои условия максимально строгими.
В сейфе есть следующий код в обработчике сообщений базы данных:
int mode = null();
if (op == op_not_winner) {
mode = 64; ;; Refund remaining check-TONs
;; addr_hash corresponds to check requester
} else {
mode = 128; ;; Award the prize
;; addr_hash corresponds to the withdrawal address from the winning entry
}
В сейфе нет обработчика отскоков или прокси-сообщений в базу данных, если пользователь отправляет "check". В базе данных можно установить msg_addr_none
как адрес награды, поскольку load_msg_address
позволяет это сделать. Мы запрашиваем проверку из сейфа, база данных пытается разобрать msg_addr_none
, используя parse_std_addr
, но это не удается. Сообщение отскакивает в сейф из базы данных, а операция не является op_not_winner
.
7. Улучшенный банк
Никогда не уничтожайте аккаунт ради забавы.
Используйте raw_reserve
вместо того, чтобы отправлять деньги самому себе.
Подумайте о возможных условиях гонки.
Будьте осторожны с расходом газа при работе с hashmap.
В контракте были условия гонки: вы могли внести деньги, а затем попытаться вывести их дважды с помощью параллельных сообщений. Нет гарантии, что сообщение с зарезервированными средствами будет обработано, поэтому банк может закрыться после второго вывода. После этого контракт мог быть развернут заново, и любой мог бы вывести невостребованные средства.
8. Dehasher
Избегайте выполнения стороннего кода в Вашем контракте.
slice try_execute(int image, (int -> slice) dehasher) asm "<{ TRY:<{ EXECUTE DEPTH 2 THROWIFNOT }>CATCH<{ 2DROP NULL }> }>CONT" "2 1 CALLXARGS";
slice safe_execute(int image, (int -> slice) dehasher) inline {
cell c4 = get_data();
slice preimage = try_execute(image, dehasher);
;; restore c4 if dehasher spoiled it
set_data(c4);
;; clean actions if dehasher spoiled them
set_c5(begin_cell().end_cell());
return preimage;
}
Не существует способа безопасно выполнить сторонний код в контракте, поскольку исключение out of gas
не может быть обработано CATCH
. Злоумышленник просто может использовать COMMIT
для любого состояния контракта и поднять out of gas
.
Заключение
Надеемся, эта статья прояснила некоторые неочевидные правила для разработчиков FunC.
Ссылки
Автор оригинальной статьи: Dan Volkov