iT邦幫忙

0

Day 17: Denial-of-Service(DoS)與資源耗盡的攻擊

  • 分享至 

  • xImage
  •  

在傳統網路安全裡,DoS(Denial-of-Service,拒絕服務)代表「讓系統忙到無法服務他人」。
在區塊鏈上,同樣有這類攻擊,只是形式不同——攻擊者不一定要癱瘓整條鏈,而是讓特定合約或功能無法被正常使用。
今天,我們要看看在智能合約中,DoS 攻擊是如何出現的,以及我們能如何預防。

🧠 什麼是鏈上 DoS 攻擊?

在區塊鏈環境下,DoS 通常指:

攻擊者透過特定輸入、交易順序或大量操作,導致合約執行失敗、Gas 用盡,或讓某功能永久卡住(例如提款、回圈、投票等)。

由於智能合約不可修改、執行需付費(Gas),設計不良就可能被惡意利用成「邏輯死鎖」。

⚙️ 常見類型一覽表
類型 攻擊原理 範例說明
Gas 耗盡型 /利用回圈或大量資料導致交易超出 Gas 限制/大量提款或迴圈遍歷全名單
狀態鎖定型 /攻擊者使狀態卡住,使其他人無法操作/投票永遠未結束、鎖倉永不釋放
依賴外部呼叫型/外部呼叫(call/send/transfer)失敗造成revert/攻擊者刻意讓fallback revert,導致整個函式中斷
競態 DoS(front-run/revert/攻擊者透過搶先交易或干擾邏輯,讓其他人無法成功執行交易/出價競標中,攻擊者不斷小幅提高價以阻斷他人

🧩 範例 1:提款清單導致 Gas 耗盡
mapping(address => uint) public balance;
address[] public users;

function withdrawAll() public {
for (uint i = 0; i < users.length; i++) {
address user = users[i];
uint amount = balance[user];
if (amount > 0) {
balance[user] = 0;
payable(user).transfer(amount);
}
}
}

問題:
• 每次呼叫 withdrawAll() 都要遍歷整個使用者名單。
• 隨著使用者增多,Gas 成本急劇上升。
• 到最後會超出 block gas limit,導致整個交易失敗。

結果:沒有人能再成功提領,合約陷入「永久卡死」狀態。
這就是典型的 Gas DoS。

改進方式:
• 不要一次性遍歷所有人。
• 改成每個人自己提領(pull over push):

function withdraw() public {
uint amount = balance[msg.sender];
require(amount > 0, "No balance");
balance[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}

✅ 優點:
每人自行提領 → 單次交易成本固定 → 不會卡死整體。

⚠️ 範例 2:外部呼叫 Revert DoS

function payout(address[] memory recipients) public {
for (uint i = 0; i < recipients.length; i++) {
recipients[i].call{value: 1 ether}("");
}
}

若其中一個 recipients[i] 是惡意合約,它的 fallback 函式若強制 revert:
整個迴圈都會失敗,導致所有合法收款者也拿不到錢。

解法:
改用可忽略錯誤的低層呼叫,並記錄結果:

(bool success, ) = recipients[i].call{value: 1 ether}("");
if (!success) {
failedRecipients.push(recipients[i]);
}

✅ 優點:
惡意地址不會影響其他人;
可在後續 retry 或人工審查時補發。

💣 範例 3:依賴外部輸入的競態 DoS
舉例:一個競標系統。

function bid() public payable {
require(msg.value > highestBid);
if (highestBidder != address(0)) {
highestBidder.call{value: highestBid}(""); // 退回前一名
}
highestBidder = msg.sender;
highestBid = msg.value;
}

攻擊手法:
• 攻擊者成為最高出價者後,在 fallback 函式中 revert()。
• 當下次有人出更高價時,call() 會失敗整個交易 → 新出價永遠失敗。

結果:攻擊者永久佔據第一名,其他人無法競標。

解法:
• 改用「提領式退款」模式(withdraw pattern)。
• 保留退款額度記錄,讓前一名自行取回:

mapping(address => uint) pendingReturns;

function bid() public payable {
require(msg.value > highestBid);
pendingReturns[highestBidder] += highestBid;
highestBidder = msg.sender;
highestBid = msg.value;
}

function withdraw() public {
uint amount = pendingReturns[msg.sender];
require(amount > 0);
pendingReturns[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}

🧠 預防重點清單
• ❌ 避免遍歷大量動態資料(用 mapping + 單筆操作代替)。
• ✅ 使用 pull 模式(withdraw pattern),減少外部依賴。
• ✅ 記錄失敗而非直接 revert,例如用事件或陣列追蹤。
• ✅ 限制每筆交易能修改的狀態量,降低 Gas 被拖垮風險。
• ⚠️ 警惕外部呼叫(call / delegatecall)失敗的副作用。
• 🧰 若需高安全合約,可使用 OpenZeppelin ReentrancyGuard + PullPayment 模式。

區塊鏈的 DoS 攻擊不像傳統網路那麼顯眼,卻更「安靜致命」。
只要你的合約有一個無限迴圈、或依賴不可信外部呼叫,就可能讓所有人「卡住」資金。
寫合約時應該習慣反問自己:

「這段邏輯若有 1000 個使用者會怎樣?」
「這個函式失敗時,其他人還能繼續嗎?」

只要能回答這兩個問題,基本就能避免大部分 DoS 風險。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言