23 октября прошел TON Hack Challenge.
В основной сети TON было развернуто несколько смарт-контрактов с искусственными нарушениями безопасности. Каждый контракт имел на балансе 3000 или 5000 TON, поэтому участник может взломать его и сразу получить вознаграждение.

Что касается меня, то мы взломали 6-е задание этого конкурса, но в этой статье я не хочу делиться своей историей, я хочу поделиться с вами некоторыми мыслями о бреши в заданиях.

Исходный код и правила конкурса размещены на Github. здесь.


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. ДАО

Используйте целые числа со знаком, если вам это действительно нужно.

Количество голосов сохранялось в сообщении как целое число. Таким образом, злоумышленник может отправить отрицательное значение во время передачи мощности и получить бесконечную мощность голосования.

(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
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Vault не имеет обработчика отказов и прокси-сообщения в базу данных, если пользователь отправляет «проверить». В базе данных мы можем установить msg_addr_none в качестве награды адрес причина load_msg_address позволяет это. Мы запрашиваем проверку из хранилища, база данных пытается выполнить синтаксический анализ msg_addr_none с использованием parse_std_addr и терпит неудачу. Сообщение возвращается в хранилище из базы данных, а операция не op_not_winner.


7. Лучший банк

Никогда не уничтожайте учетную запись ради развлечения.

Делать raw_reserve вместо того, чтобы отправлять деньги себе.

Подумайте о возможных условиях гонки.

Будьте осторожны с потреблением газа hashmap.

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


8. Дехэшер

Избегайте выполнения стороннего кода в вашем контракте.

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.