Reentrancy Attack อธิบาย วิธีป้องกันใน Solidity

Article author

Reentrancy attacks ยังคงเป็นหนึ่งในภัยคุกคามที่รู้จักกันดีและเรื้อรังต่อความปลอดภัยของสมาร์ตคอนแทรกต์บน Ethereum และเชนที่เข้ากันได้กับ EVM แม้ว่าจะมีความพยายามของวงการอย่างต่อเนื่องมาหลายปี โปรโตคอลใหม่ ๆ ยังคงเผชิญกับช่องโหว่ reentrancy ทำให้เกิดความสูญเสียทางการเงินอย่างมีนัยสำคัญ—บางครั้งมีมูลค่าถึงหลายล้านดอลลาร์ การโจมตีเหล่านี้มักเกิดขึ้นในสัญญา DeFi lending, staking และ bridge ที่มีการเรียกภายนอกและการโอนโทเคนบ่อยครั้ง

ในบทความนี้ เราจะวิเคราะห์กลไกของการโจมตีแบบ reentrancy ระบุรูปแบบสัญญาที่มีช่องโหว่ทั่วไป และแบ่งปันกลยุทธ์ป้องกันที่ผ่านการทดสอบ โดยอิงจากการวิเคราะห์การตรวจสอบสมาร์ตคอนแทรกต์กว่า 255 รายการที่ Soken เราจะเน้นให้เห็นว่าการเขียนโค้ด Solidity อย่างละเอียดอ่อนและกระบวนการทดสอบที่ครอบคลุมสามารถป้องกันภัย reentrancy ได้อย่างไร บทความนี้ยังเชื่อมโยงไปยังความเสี่ยงที่เกี่ยวข้อง เช่น การใช้งาน delegatecall ผิดวิธีและกลยุทธ์ flash loan เพื่อให้เข้าใจภาพรวมที่จำเป็นสำหรับนักพัฒนา Web3 หรือผู้ตรวจสอบความปลอดภัยที่จริงจัง


การโจมตีแบบ Reentrancy คืออะไร และมันใช้ประโยชน์จากสมาร์ตคอนแทรกต์อย่างไร?

การโจมตีแบบ reentrancy เกิดขึ้นเมื่อสัญญาที่เป็นอันตรายเรียกใช้ฟังก์ชันของสัญญาที่มีช่องโหว่อย่างซ้ำ ๆ ก่อนที่จะเสร็จสิ้นการดำเนินการในรอบแรก โดยการใช้ประโยชน์จากสถานะของสัญญาที่ไม่สอดคล้องกัน

ในทางปฏิบัติ ช่องโหว่ reentrancy จะเกิดขึ้นจากการเปลี่ยนแปลงสถานะและการเรียกภายนอกที่จัดลำดับไม่ดีภายในสัญญา เมื่อสัญญาทำการโอน ETH หรือโทเคนผ่านการ call หรือ transfer ไปยังที่อยู่ที่ไม่น่าเชื่อถือ ผู้รับนั้นสามารถกลับเข้าฟังก์ชันที่มีช่องโหว่ซ้ำได้ก่อนที่ตัวแปรสถานะจะถูกอัปเดต ทำให้นักโจมตีสามารถถอนเงินหรือใช้ประโยชน์จากตรรกะของสัญญาได้โดยไม่คาดคิด

ตัวอย่างเช่น การโจมตี DAO ที่มีชื่อเสียงในปี 2016 ส่งผลให้ถูกขโมยเงินไปเกือบ 60 ล้านดอลลาร์ผ่านช่องโหว่ reentrancy ซึ่งเป็นบทเรียนที่ควรระวัง ตั้งแต่นั้นมา วงการได้ระบุว่าต้นเหตุหลักคือการอัปเดตยอดเงินหรือสถานะของสัญญาจะเกิดขึ้น หลัง การเรียกภายนอก ทำให้เกิดช่องว่างสถานะที่ไม่สอดคล้องกันที่นักโจมตีใช้ประโยชน์

ข้อมูลเชิงลึกจากการตรวจสอบของ Soken: “ในการตรวจสอบความปลอดภัย DeFi ที่ผ่านมามากกว่า 40% เราพบรูปแบบ reentrancy ในรูปแบบบางอย่าง โดยมักเป็นการแปรผันละเอียดอ่อนที่เกี่ยวข้องกับ proxy contracts หรือ layered calls การระบุจุดเหล่านี้จำเป็นต้องวิเคราะห์การเรียกระหว่างสัญญาอย่างละเอียดเกินกว่าการตรวจสอบสัญญาเดี่ยวเพียงอย่างเดียว”


รูปแบบ Solidity ทั่วไปที่ก่อให้เกิดช่องโหว่ Reentrancy

ช่องโหว่ reentrancy ส่วนใหญ่เกิดจากลำดับการทำงานเฉพาะ: การเรียกภายนอก (เช่น การโอน ETH หรือการเรียกโทเคน) ที่เกิดขึ้นก่อนที่สัญญาจะอัปเดตตัวแปรสถานะภายใน

รูปแบบที่มีช่องโหว่อย่างมากคือ:

function withdraw(uint256 amount) public {
    require(balances[msg.sender] >= amount);
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
    balances[msg.sender] -= amount;
}

ตรงนี้ การเรียกภายนอกด้วย call.value() เกิดขึ้น ก่อน จะหักยอดเงินของผู้ใช้ ระหว่างการเรียกนี้ ฟังก์ชัน fallback ของนักโจมตีสามารถเรียกใช้งาน withdraw() ซ้ำได้ ทำให้ถอนเงินได้ก่อนที่ยอดเงินจะถูกอัปเดต

โครงสร้างที่เสี่ยงหลักได้แก่:

  • เรียกภายนอกก่อนการอัปเดตสถานะ: ส่ง ETH หรือโทเคนก่อนอัปเดตยอดเงิน
  • ใช้ call หรือ send โดยไม่มีมาตรการป้องกัน: การเรียกภายนอกผ่านฟังก์ชันระดับต่ำมักหลีกเลี่ยงการตรวจสอบและสามารถเรียกใช้ fallback functions ได้
  • ไม่มีการป้องกัน reentrancy: ไม่มี mutex หรือกลไกล็อกอื่น
  • สายการเรียกที่ซับซ้อนโดยใช้ delegatecall: อาจเปิดช่องโหว่ reentrancy โดยอ้อมผ่านการเรียกภายนอกในสัญญาที่ถูกเรียก

ในทางตรงกันข้าม รูปแบบที่ปลอดภัยจะกลับลำดับเป็น:

function withdraw(uint256 amount) public {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;  // อัปเดตสถานะก่อน
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
}

วิธีป้องกันการโจมตี Reentrancy ใน Solidity: แนวทางปฏิบัติที่ดีที่สุดและเครื่องมือ

แนวทางป้องกันที่ได้ผลที่สุดต่อการโจมตี reentrancy คือ อัปเดตสถานะก่อนการเรียกภายนอก พร้อมทั้งใช้รูปแบบป้องกัน reentrancy อย่างชัดเจน

เทคนิคมาตรฐานในการลดความเสี่ยง:

  1. Checks-Effects-Interactions Pattern:
    ควรอัปเดตสถานะภายในก่อนทำการเรียกภายนอกเสมอ เพื่อหลีกเลี่ยงสถานะของสัญญาที่ไม่สอดคล้องกันขณะเรียกภายนอก

  2. Reentrancy Guards / Mutexes:
    ReentrancyGuard ของ Solidity จาก OpenZeppelin ใช้ mutex ง่าย ๆ เพื่อบล็อกการเรียกซ้อนกัน ใช้งานโดย:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureContract is ReentrancyGuard {
    mapping(address => uint256) balances;

    function withdraw(uint256 amount) public nonReentrant {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
    }
}
  1. จำกัดการเรียกภายนอก:
    ลดการโอนหรือการโต้ตอบในฟังก์ชันที่สำคัญ—ใช้วิธีให้ผู้ใช้ถอนเงิน (pull) แทนการโอนแบบอัตโนมัติ (push)

  2. การวิเคราะห์สแตติกและเครื่องมืออัตโนมัติ:
    เครื่องมืออย่าง Slither และเฟรมเวิร์กการตรวจสอบของ Soken แจ้งเตือนช่องโหว่ reentrancy

  3. ใช้เวอร์ชัน Solidity ล่าสุดและฟีเจอร์ใหม่:
    Solidity 0.8+ มีการตรวจสอบการล้นในตัว แต่ความเสี่ยง reentrancy ยังอยู่ ดังนั้นควรรวมโค้ดที่ปลอดภัยกับรูปแบบการออกแบบที่เหมาะสม

แนวทางของ Soken: การตรวจสอบของเราผสมผสานการสร้างแบบจำลองภัยคุกคามด้วยมือ การดำเนินการเชิงสัญลักษณ์ และการทำ fuzz testing ซึ่งตรวจหา flow پیچیده reentrancy ที่เครื่องสแกนทั่วไปมักมองไม่เห็น


ตัวอย่างจริงของการโจมตี Reentrancy และผลกระทบ

แม้ว่าจะมีการรับรู้เพิ่มขึ้น การโจมตี reentrancy ยังคงแพร่หลาย ด้านล่างเป็นเปรียบเทียบเหตุการณ์โจมตีที่แสดงความหลากหลายและต้นทุนของการโจมตีดังกล่าว:

เหตุการณ์ วันที่ จำนวนเงินสูญเสีย รูปแบบที่ถูกโจมตี บทเรียนสำคัญ
The DAO Hack 2016-06 ~$60 ล้านดอลลาร์ Reentrancy ใน splitDAO proposal อัปเดตสถานะก่อนเรียกภายนอก
The Lendf.Me Hack 2020-04 $25 ล้าน+ Flash loan เสริม reentrancy รวม reentrancy guard + ความทนทาน flash loan
Euler Finance Hack 2023-03 $197 ล้าน Reentrancy ซ้อน + delegatecall สายการเรียกซับซ้อนเพิ่มความเสี่ยง
Recent DeFi Bridge Hack 2025-11 $15 ล้าน โจมตี transfer token แบบ reentrancy ตรวจสอบการโต้ตอบข้ามสัญญา

หมายเหตุ: การตรวจสอบของ Soken เน้นความปลอดภัยแบบหลายชั้น โดยเฉพาะบริเวณการเรียกระหว่างสัญญาและมาตรฐานโทเคนที่อาจเรียก fallback functions แบบไดนามิก


Delegatecall และ Flash Loan: ตัวเร่งความเสี่ยง Reentrancy

Delegatecall ที่รันโค้ดในบริบทของสัญญาที่เรียก อาจปกปิดความเสี่ยง reentrancy และเพิ่มพื้นผิวการโจมตี โดยเฉพาะเมื่อใช้ร่วมกับ flash loans

การโจมตี reentrancy มักใช้ประโยชน์จาก flash loan เพื่อเพิ่มทุนสำหรับการถอนซ้ำ ๆ:

  • Flash loans มอบสภาพคล่องก้อนใหญ่ทันทีในธุรกรรมเดียว
  • นักโจมตีใช้เงินกู้ flash เพื่อกระตุ้นการเรียก reentrant เพื่อถอนสภาพคล่องของโปรโตคอลก่อนคืนเงินกู้
  • การใช้ delegatecall อาจเรียกโค้ดภายนอกที่ไม่น่าเชื่อถือ แก้ไขสถานะสัญญาหรือเปิดช่องทางให้เกิดการกลับเข้าซ้ำ

มุมมองความปลอดภัย: “จากประสบการณ์ของ Soken การป้องกัน reentrancy ขยายไปไกลกว่าสัญญาเดียวถึงการออกแบบโปรโตคอลโดยรวม โดยเฉพาะเมื่อ delegatecall และ flash loans ทำงานร่วมกัน สัญญาจำเป็นต้องผสมผสาน reentrancy guards กับการตรวจสอบการเข้าถึงและการเช็คเฉพาะ flash loan (เช่น ขีดจำกัดจำนวนเงินกู้ และข้อจำกัดการเรียกซ้ำ)”


ตารางเปรียบเทียบ: เทคนิคป้องกัน Reentrancy ทั่วไป

เทคนิค คำอธิบาย ข้อดี ข้อเสีย การใช้งาน
Checks-Effects-Interactions อัปเดตสถานะก่อนเรียกภายนอก ง่ายและมีประสิทธิภาพ ต้องมีวินัย การปฏิบัติพื้นฐานด้านความปลอดภัย
Reentrancy Guard (mutex) ใช้ตัวแก้ไขของ Solidity ป้องกันการกลับเข้าซ้ำ บล็อกการเรียกซ้อนชัดเจน เพิ่มค่าแก๊สเล็กน้อย แนะนำสำหรับฟังก์ชันถอนเงินทั้งหมด
Pull Payment Pattern ให้ผู้ใช้ถอนเงินโดยสมัครใจ ลดความเสี่ยงจากการโอนอัตโนมัติ ต้องมีปฏิสัมพันธ์จากผู้ใช้ เหมาะกับสัญญา escrow และ staking
Static and Dynamic Analysis ตรวจสอบโค้ดด้วยอัตโนมัติและแมนนวล ตรวจพบช่องโหว่แต่เนิ่น ๆ อาจพลาด flow ซับซ้อนข้ามสัญญา จำเป็นสำหรับความมั่นใจก่อนปล่อย
Flash Loan & Delegatecall Checks ตรวจสอบผู้เรียกและบริบทธุรกรรม ป้องกัน reentrancy ที่ถูกเร่งด้วยเงินกู้ ซับซ้อนในการใช้งาน สำคัญในโปรโตคอล DeFi ที่มีการกู้

ตัวอย่างโค้ด Solidity: สัญญาที่มีช่องโหว่ Reentrancy และการแก้ไข

นี่คือตัวอย่างสัญญาที่มีช่องโหว่ และเวอร์ชันที่แข็งแกร่งขึ้นด้วย reentrancy guard:

// ช่องโหว่: ถอนเงินก่อนอัปเดตยอดเงิน
contract Vulnerable {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "Insufficient funds");
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
        balances[msg.sender] -= amount; // ช่องโหว่: อัปเดตสถานะหลังการเรียกภายนอก
    }
}

แก้ไขโดยใช้ ReentrancyGuard ของ OpenZeppelin และอัปเดตสถานะก่อนการโต้ตอบ:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract Secure is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external nonReentrant {
        require(balances[msg.sender] >= amount, "Insufficient funds");
        balances[msg.sender] -= amount; // อัปเดตสถานะก่อนเรียกภายนอก
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

คำแนะนำผู้เชี่ยวชาญ: ควรรวม checks-effects-interactions กับ reentrancy guards เสมอ การตรวจสอบกราฟการเรียกสัญญาและการทดสอบด้วย fuzzing สามารถเปิดเผยช่องโหว่ reentrancy ที่ซับซ้อนได้ก่อนการปรับใช้


ช่องโหว่ reentrancy ยังคงเป็นภัยคุกคามร้ายแรงต่อสมาร์ตคอนแทรกต์ โดยเฉพาะในระบบนิเวศ DeFi ที่ซับซ้อนซึ่งใช้ flash loans และ delegatecall การป้องกันที่ได้ผลที่สุดคือการปฏิบัติตามวินัยโค้ด (checks-effects-interactions) การใช้ reentrancy guards อย่างชัดเจน การตรวจสอบอย่างเข้มงวด และการยืนยันบริบทของธุรกรรม

ที่ Soken งานวิจัยด้านความปลอดภัยของเราผสมผสานเทคนิคแมนนวลและอัตโนมัติที่สั่งสมจากการตรวจสอบ 282+ สมาร์ตคอนแทรกต์ ซึ่งช่วยในการระบุและแก้ไขช่องโหว่ reentrancy และความเสี่ยงที่เกี่ยวข้อง การยึดมั่นในแนวทางปฏิบัติที่ดีที่สุดและวิธีการทดสอบที่ละเอียดถี่ถ้วนจึงเป็นสิ่งจำเป็นสำหรับทุกโปรเจกต์ที่จัดการเงินของผู้ใช้


ต้องการคำปรึกษาด้านความปลอดภัยจากผู้เชี่ยวชาญ? ทีมผู้ตรวจสอบของ Soken ได้ตรวจสอบสมาร์ตคอนแทรกต์กว่า 255 รายการและปกป้องมูลค่าโปรโตคอลมากกว่า 2 พันล้านดอลลาร์ ไม่ว่าคุณจะต้องการ การตรวจสอบอย่างครบถ้วน, ประเมินความปลอดภัย X-Ray ฟรี, หรือช่วยนำทางด้าน กฎระเบียบคริปโต เราพร้อมช่วยคุณเสมอ

พูดคุยกับผู้เชี่ยวชาญของ Soken | ดูรายงานการตรวจสอบของเรา

Article author

คำถามที่พบบ่อย

การโจมตี reentrancy ใน smart contract คืออะไร?

การโจมตี reentrancy เกิดขึ้นเมื่อ smart contract ที่ประสงค์ร้ายเรียกกลับไปยัง contract เป้าหมายซ้ำๆ ก่อนที่การเรียกครั้งแรกจะเสร็จสิ้น เพื่อแสวงประโยชน์จากการเรียกภายนอก ดึงทรัพย์สิน หรือเปลี่ยนสถานะอย่างไม่คาดคิด

รูปแบบ smart contract แบบใดที่เสี่ยงต่อการถูกโจมตี reentrancy มากที่สุด?

contract ที่ทำการเรียกภายนอก เช่น โอนโทเคน หรือติดต่อ contract อื่น ก่อนอัปเดตสถานะภายใน มีความเสี่ยงสูงเนื่องจากอาจโดนเรียกกลับซ้ำ (recursive reentry) ระหว่างการเรียกภายนอกเหล่านั้น

จะป้องกันช่องโหว่ reentrancy ใน Solidity ได้อย่างไร?

วิธีป้องกันที่นิยมใช้ได้แก่ การใช้รูปแบบ Checks-Effects-Interactions การใช้ modifier 'ReentrancyGuard' จาก OpenZeppelin และลดจำนวนการเรียกภายนอก หรือใช้วิธีจ่ายเงินแบบ pull แทน push

เครื่องมือใดช่วยตรวจจับช่องโหว่ reentrancy ก่อนการ deploy?

เครื่องมือวิเคราะห์ความปลอดภัยอัตโนมัติเช่น MythX, Slither และ Securify สามารถตรวจจับรูปแบบการโจมตี reentrancy ได้ นอกจากนี้การตรวจสอบโค้ดด้วยมือและการทดสอบ fuzz ยังช่วยเพิ่มความแม่นยำในการค้นหาช่องโหว่

delegatecall และ flash loans มีความเกี่ยวข้องกับการโจมตี reentrancy หรือไม่?

ใช่ delegatecall ที่ถูกใช้ผิดวิธีสามารถเปิดช่องทางให้เกิดการโจมตี reentrancy ได้โดยรันโค้ดในบริบท contract อื่น ส่วน flash loans ถูกใช้โดยผู้โจมตีเพื่อเพิ่มผลกระทบ แม้ไม่ใช่การโจมตี reentrancy โดยตรง

แชท