Patch Thursday — TON 노드 취약점 공개

ChainLight가 TON 네트워크의 모든 노드를 중단시킬 수 있는 서비스 거부(DoS) 취약점을 발견했습니다. 본 글에서는 취약점의 기술적 세부 사항과 공격 시나리오, 보안 패치 대응 방안 등을 분석합니다.
ChainLight's avatar
Jul 17, 2023
Patch Thursday — TON 노드 취약점 공개

요약

✨ ChainLight는 모든 노드(node)를 중단시킬 수 있는 TON 네트워크 노드의 서비스 거부(DoS) 취약점을 발견했습니다. 저희 팀은 작년에 본 취약점을 보고했으며, TON 재단은 2022년 10월 3일에 이를 수정했습니다. 자세한 내용은 링크를 통해 확인하실 수 있습니다. 본 글을 통해 노드 취약점에 대한 인식 증진에 기여하고자 합니다.

취약점: RoundRobinDecoder 내의 부적절한 입력 검증으로 인한 원격 DoS 발생

취약점 설명

해당 취약점으로 인해 TON 네트워크 전체에서 검증이 중단될 수 있습니다. 예를 들어, 노드 목록이 비공개가 아니기 때문에 공격자는 네트워크의 모든 노드를 파악한 후 작동을 중단시킬 수 있습니다. 또한 공격에 소모되는 비용이 거의 0에 가깝기 때문에 모든 노드에 대해 해당 공격을 반복할 수 있습니다(약 2개의 UDP 데이터그램을 전송하는 것만으로 충분하다고 판단).

TVM의 버그를 이용한 공격처럼, 공격의 효과가 자동으로 지속되지는 않지만, RLDP 핸들러(Reliable Large Datagram Protocol handler)가 가동되는 즉시 노드를 다시 중단시킬 수 있기 때문에 노드를 재시작한다고 해서 문제가 해결되는 것은 아닙니다.

ChainLight 팀은 TON 노드를 점검해보았고, 그 결과 TON 노드는 일부 충돌 일관성(Crash Consistency)이 있는 것으로 나타났습니다. 따라서 충돌이 발생해도, 노드 데이터베이스의 상태가 수동 복구가 필요한 상태로 손상되지는 않을 것입니다. 하지만 그렇다고 해서 데이터베이스의 상태가 마치 노드가 충돌한 적이 없는 것처럼 완벽한 상태라는 의미는 아닙니다.

예를 들어, TON은 수동 WAL 플러시(Write-Ahead Log flush) 설정이 되어있는 RocksDB를 사용하기 때문에 최근 기록된 데이터의 일부가 손실될 수 있습니다. 거래소나 브릿지(bridge)가 충분히 높지 않은 블록 컨펌(confirmation) 횟수를 가진다고 가정해 보겠습니다.

TON의 완결성(finality) — TON 재단은 TON 네트워크의 완결성 확보에 필요한 시간이 6초 미만이라고 소개합니다.

이 경우 공격자는 TON을 예치한 다음, 모든 노드에 DoS 공격을 가해 최근 블록을 취소함으로써 TON의 이중 지불을 가능케 할 수 있습니다.

노드 프로세스를 재시작하는 것으로 블록체인을 다시 가동할 수 있지만, 블록체인의 가동 중단의 경우 짧은 시간이라도 해당 체인의 평판을 하락시키고 사용자에게 금전적 피해를 안길 수 있습니다(예: 시장 변동이 큰 시기에 가동 중단으로 인한 디파이 프로토콜의 악성 부채 발생).

취약점 상세

(1)(2) RoundRobinDecoder를 구성하는 과정에서 symbols_count를 검사하지 않습니다. (symbols_count(data_size_ + symbol_size_ — 1) / symbol_size_여야 합니다.)

(3)(4)(5) 추후 symbol.id, symbols_count_, symbol_size_ 는 데이터 버퍼(buffer)의 오프셋을 도출하는 데 사용되는데, data_size_에 작은 값이 입력되는 경우 도출된 오프셋이 임계값을 벗어날 수 있습니다. 이로 인해 (6)에서 어설션 실패(assertion failure)가 발생하고 노드가 충돌합니다.

(7)(8) 특수하게 조작된 rldp_messagePart가 수신되면 취약한 코드 경로가 RLDP를 통해 도달할 수 있습니다.

fec/fec.cpp:L100

td::Result FecType::create(tl_object_ptr obj) {
  FecType T;
  ton_api::downcast_call(
      *obj.get(), td::overloaded(
                      [&](const ton_api::fec_raptorQ &obj) {
                        T.type_ = td::fec::RaptorQEncoder::Parameters{static_cast(obj.data_size_),
                                                                      static_cast(obj.symbol_size_),
                                                                      static_cast(obj.symbols_count_)};
                      },
                      [&](const ton_api::fec_roundRobin &obj) { // (1)
                        T.type_ = td::fec::RoundRobinEncoder::Parameters{static_cast(obj.data_size_),
                                                                         static_cast(obj.symbol_size_),
                                                                         static_cast(obj.symbols_count_)};
                      },
                      [&](const ton_api::fec_online &obj) {
                        T.type_ = td::fec::OnlineEncoder::Parameters{static_cast(obj.data_size_),
                                                                     static_cast(obj.symbol_size_),
                                                                     static_cast(obj.symbols_count_)};
                      }));
  return T;
}


tdfec/td/fec/fec.cpp:L86

RoundRobinDecoder::RoundRobinDecoder(RoundRobinEncoder::Parameters parameters) // (2)
    : data_(BufferSlice(parameters.data_size))
    , data_mask_(parameters.symbols_count, false)
    , left_symbols_(parameters.symbols_count)
    , symbol_size_(parameters.symbol_size)
    , symbols_count_(parameters.symbols_count) {
}


tdfec/td/fec/fec.cpp:L71

Status RoundRobinDecoder::add_symbol(Symbol symbol) {
  if (symbol.data.size() != symbol_size_) {
    return Status::Error("Symbol has invalid length");
  }
  auto pos = symbol.id % symbols_count_;  // (3)
  if (data_mask_[pos]) {
    return td::Status::OK();
  }
  data_mask_[pos] = true;
  auto offset = pos * symbol_size_; // (4)
  data_.as_slice().substr(offset).truncate(symbol_size_).copy_from(symbol.data.as_slice()); // (5)
  left_symbols_--;
  return td::Status::OK();
}


tdutils/td/utils/Slice.h:L110

inline MutableSlice MutableSlice::substr(size_t from) const {
  CHECK(from <= len_);  // (6)
  return MutableSlice(s_ + from, len_ - from);
}


rldp/rldp.cpp:L105

void RldpIn::process_message_part(adnl::AdnlNodeIdShort source, adnl::AdnlNodeIdShort local_id,
                                  ton_api::rldp_messagePart &part) {
  auto it = receivers_.find(part.transfer_id_);
  if (it == receivers_.end()) {
    if (part.part_ != 0) {
      VLOG(RLDP_INFO) << "dropping new part";
      return;
    }
    if (static_cast(part.total_size_) > mtu()) {
      VLOG(RLDP_NOTICE) << "dropping too big rldp packet of size=" << part.total_size_ << " mtu=" << mtu();
      return;
    }
    auto ite = max_size_.find(part.transfer_id_);
    if (ite == max_size_.end()) {
      if (static_cast(part.total_size_) > default_mtu()) {
        VLOG(RLDP_NOTICE) << "dropping too big rldp packet of size=" << part.total_size_
                          << " default_mtu=" << default_mtu();
        return;
      }
    } else {
      if (static_cast(part.total_size_) > ite->second) {
        VLOG(RLDP_NOTICE) << "dropping too big rldp packet of size=" << part.total_size_ << " allowed=" << ite->second;
        return;
      }
    }
    if (lru_set_.count(part.transfer_id_) == 1) {
      auto obj = create_tl_object(part.transfer_id_, part.part_);
      td::actor::send_closure(adnl_, &adnl::Adnl::send_message, local_id, source, serialize_tl_object(obj, true));
      return;
    }
    auto P = td::PromiseCreator::lambda(
        [SelfId = actor_id(this), source, local_id, transfer_id = part.transfer_id_](td::Result R) {
          if (R.is_error()) {
            VLOG(RLDP_INFO) << "failed to receive: " << R.move_as_error();
            return;
          }
          td::actor::send_closure(SelfId, &RldpIn::in_transfer_completed, transfer_id);
          td::actor::send_closure(SelfId, &RldpIn::receive_message, source, local_id, transfer_id, R.move_as_ok());
        });
​
    receivers_.emplace(part.transfer_id_,
                       RldpTransferReceiver::create(part.transfer_id_, local_id, source, part.total_size_,
                                                    td::Timestamp::in(60.0), actor_id(this), adnl_, std::move(P)));
    it = receivers_.find(part.transfer_id_);
  }
  auto F = fec::FecType::create(std::move(part.fec_type_)); // (7)
  if (F.is_ok()) {
    td::actor::send_closure(it->second, &RldpTransferReceiver::receive_part, F.move_as_ok(), part.part_,
                            part.total_size_, part.seqno_, std::move(part.data_));
  } else {
    VLOG(RLDP_NOTICE) << "received bad fec type: " << F.move_as_error();
  }
}


rldp/rldp-peer.cpp:L111

void RldpTransferReceiverImpl::receive_part(fec::FecType fec_type, td::uint32 part, td::uint64 total_size,
                                            td::uint32 seqno, td::BufferSlice data) {
  if (part < part_) {
    auto obj = create_tl_object(transfer_id_, part);
    td::actor::send_closure(adnl_, &adnl::Adnl::send_message, local_id_, peer_id_, serialize_tl_object(obj, true));
    return;
  }
  if (part > part_) {
    return;
  }
  cnt_++;
​
  if (seqno > max_seqno_) {
    max_seqno_ = seqno;
  }
​
  if (!decoder_) {
    auto D = fec_type.create_decoder();
    if (D.is_error()) {
      VLOG(RLDP_WARNING) << "failed to create decoder: " << D.move_as_error();
      return;
    }
    decoder_ = D.move_as_ok();
  }
  decoder_->add_symbol(td::fec::Symbol{seqno, std::move(data)}); // (8)
  if (decoder_->may_try_decode()) {
    auto D = decoder_->try_decode(false);
    if (D.is_ok()) {
      auto data = D.move_as_ok();
      if (data.data.size() + offset_ > total_size_) {
        abort(td::Status::Error(ErrorCode::protoviolation,
                                PSTRING() << "too big part: offset=" << offset_ << " total_size=" << total_size_
                                          << " data_size=" << data.data.size() << " part=" << part_));
        return;
      }
      data_.as_slice().remove_prefix(td::narrow_cast(offset_)).copy_from(data.data.as_slice());
      offset_ += data.data.size();
      auto obj = create_tl_object(transfer_id_, part_);
      td::actor::send_closure(adnl_, &adnl::Adnl::send_message, local_id_, peer_id_, serialize_tl_object(obj, true));
      part_++;
      cnt_ = 0;
      max_seqno_ = 0;
      decoder_ = nullptr;
      if (offset_ == total_size_) {
        finish();
        return;
      }
    }
  }
​
  if (cnt_ >= 10) {
    auto obj = create_tl_object(transfer_id_, part_, max_seqno_);
    td::actor::send_closure(adnl_, &adnl::Adnl::send_message, local_id_, peer_id_, serialize_tl_object(obj, true));
    cnt_ = 0;
  }
}​

취약점 영향

  1. 모든 노드/검증자의 구동이 중단될 수 있으므로 TON 블록체인은 멈출 수 있습니다.

  2. 악의적인 노드/검증자는 모든 노드를 중단시킴으로써, 사용 가능한 유일한 라이트 서버(light server)가 될 수 있습니다. 그런 다음 라이트 클라이언트(light client)에 조작된 데이터를 제공하여 추가적인 피해를 입힐 수 있습니다.

PoC를 통한 검증

PoC를 통해 검증해보고 싶으시다면 링크를 통해 필요한 파일을 다운로드 후 검증해보실 수 있습니다.

  1. TON 재단이 제공하는 공식 절차에 따라 풀노드(full node) 세팅 (https://ton.org/docs/#/nodes/run-node)

  2. get_connection_info.py을 실행하여 PoC 코드의 파라미터(parameter)를 가져옴(실제 공격자는 DHT — Distributed Hash Table를 통해 가져옴)

  3. TON 소스 코드를 다운로드(git clone — recurse-submodules https://github.com/ton-blockchain/ton.git)

  4. test-exploit.cpp 파일에 매개 변수를 추가(EDIT THIS 아래 줄 편집)

  5. test-exploit.cppCMakeLists.txt 파일에 추가(test-exploit.cpp 안의 지침을 따르시기 바랍니다).

  6. test-exploit 컴파일(compile) 및 실행

cd ton && mkdir ton-build && cd ton-build &&
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo .. &&
cmake --build . --target test-exploit

7. test-exploit을 실행하고 밸리데이터(validator)의 충돌을 확인합니다. (./test-exploit & 서비스 밸리데이터 상태)

결론

노드는 블록체인 네트워크를 구동하는 데 중요한 역할을 합니다. 노드가 트랜잭션 데이터를 제대로 저장하지 못하거나 갑자기 동작을 중단하면 블록체인 생태계에 큰 영향을 미칠 수 있습니다. 블록체인 네트워크 내의 데이터 손상이 발생하거나 업데이트가 적기에 이루어지지 않으면 서로 다른 블록체인 간의 상호 작용에도 문제를 일으킬 수 있습니다. 따라서 노드와 관련된 취약점은 매우 중요하게 다루어야 하며 발견 즉시 수정해야 합니다. 블록체인 메인넷을 개발하는 재단과 팀은 이 점을 염두에 두어야 합니다.


✨ We are ChainLight!

ChainLight explores new and effective blockchain security technologies with rich practical experience and deep technical understanding. Our innovative security audits built upon such research proactively identify and eliminate various security risks and vulnerabilities in the Web3 ecosystem. To ensure continuous security even after the audit, we provide a digital asset risk management solution using on-chain data monitoring and automated vulnerability detection services.

ChainLight serves to guide and protect all users of decentralized services, lighting the way for a safer Web3 ecosystem.

  • Want to see more from the ChainLight team? 👉 Check out our Twitter account.


🌐 Website: chainlight.io | 📩 TG: @chainlight | 📧 chainlight@theori.io


Originally published at https://blog.theori.io on July 18, 2023.

Share article

Theori © 2025 All rights reserved.