ทดลองอ่านฟรีอ่าน 10 นาที
การเรียกซ้ำ (reentrancy): มุกเก่าแก่ที่สุดที่ยังได้ผล
คอนแทรกต์ที่ส่งเงินออกไปก่อนจะอัปเดตบันทึกของตัวเอง ถูกขอเงินก้อนเดิมอีกครั้งได้ในระหว่างที่ยังส่งไม่เสร็จ นั่นคือการเรียกซ้ำ หรือ reentrancy ซึ่งเป็นช่องโหว่เบื้องหลังหายนะครั้งแรกที่ดังที่สุดในประวัติศาสตร์ของ Ethereum และมันยังหาเหยื่อได้อยู่จนทุกวันนี้
กลไก
- คอนแทรกต์เก็บยอดคงเหลือของผู้ฝาก และมีฟังก์ชันถอนเงินให้ใช้
- ฟังก์ชันถอนส่งเงินออกไปก่อน แล้วจึงตั้งยอดคงเหลือของผู้ฝากให้เป็นศูนย์
- การส่งเงินไปให้คอนแทรกต์อื่นคือการมอบการควบคุมให้โค้ดของคอนแทรกต์นั้น
- โค้ดนั้นเรียกฟังก์ชันถอนอีกครั้ง ยอดคงเหลือยังไม่ถูกตั้งเป็นศูนย์ การตรวจสอบจึงผ่านเป็นครั้งที่สอง
- วนซ้ำไปเรื่อย ๆ จนพูลว่างเปล่า
ทางแก้คือกฎเรื่องลำดับที่เรียกว่า checks, effects, interactions ตรวจสอบก่อน อัปเดตสถานะของตัวเองเป็นลำดับที่สอง แล้วคุยกับคนอื่นเป็นลำดับสุดท้าย ส่วนตัวกันการเรียกซ้ำ ซึ่งเป็นล็อกที่ปฏิเสธการเข้ารอบที่สองขณะที่การเรียกครั้งแรกยังเปิดอยู่ คือเข็มขัดที่รัดทับสายเอี๊ยมคู่นั้นไว้
มันเกิดขึ้นที่ไหนมาแล้ว
- The DAO เดือนมิถุนายน 2016 ช่องโหว่การเรียกซ้ำดูดเงินประมาณ 3.6 ล้าน ETH ออกจากกองทุนลงทุนที่สร้างบน Ethereum การรับมือทำให้เชนแตกเป็นสองสาย เสียงส่วนใหญ่ฟอร์กเพื่อย้อนการโจรกรรม และเชนที่ปฏิเสธการย้อนนั้นยังเดินอยู่ถึงวันนี้ในชื่อ Ethereum Classic
- Curve Finance เดือนกรกฎาคม 2023 ช่องโหว่ไม่ได้อยู่ในลอจิกของ Curve เอง แต่อยู่ในคอมไพเลอร์ Vyper บางเวอร์ชัน ที่ตัวกันการเรียกซ้ำไม่ทำงานอย่างที่เขียนไว้ มีหลายพูลถูกดูดเงินออกไป โดยมีรายงานความสูญเสียราว 70 ล้านดอลลาร์ โค้ดที่ผ่านการตรวจสอบแล้วก็ยังถูกเครื่องมือที่คอมไพล์มันทำลายได้
ทำไมมันจึงยังอยู่
- การเรียกออกไปข้างนอกทุกแบบมอบการควบคุมให้คนอื่นได้ ทั้งฮุกตอนโอนโทเค็น คอลแบ็ก และมาตรฐานโทเค็นรุ่นใหม่ที่แจ้งเตือนผู้รับ
- การเรียกซ้ำแบบอ่านอย่างเดียวนั้นแนบเนียนกว่า ผู้โจมตีเข้าไปเรียกฟังก์ชันอ่านค่าซ้ำขณะที่สถานะถูกอัปเดตไปครึ่งทาง โปรโตคอลอื่นจึงอ่านราคาหรือยอดคงเหลือที่ผิด แล้วตัดสินใจบนตัวเลขนั้น
- การเรียกซ้ำข้ามฟังก์ชันใช้อีกฟังก์ชันที่ใช้สถานะชุดเดียวกัน ตัวกันที่ติดไว้บนฟังก์ชันเดียวจึงไม่พอ
อะไรที่ป้องกันมันได้จริง
- ใช้กฎ checks, effects, interactions ทุกที่ ไม่ใช่แค่ในฟังก์ชันถอนที่เห็นได้ชัด
- ติดตัวกันการเรียกซ้ำไว้บนทุกจุดทางเข้าที่เปลี่ยนสถานะและมีการเรียกออกไปข้างนอก
- ปฏิบัติกับโทเค็นทุกตัวเหมือนเป็นโค้ดที่เป็นศัตรู เพราะคอนแทรกต์ของโทเค็นรันลอจิกของตัวเองในระหว่างการโอน
- เขียนเทสต์ที่เรียกย้อนกลับเข้ามาในคอนแทรกต์จากผู้รับที่เป็นอันตราย ซึ่งเป็นวิธีที่ทำให้เจอเรื่องนี้ก่อนเปิดใช้งาน
นี่คือเนื้อหาเพื่อการศึกษา ไม่ใช่คำแนะนำทางการเงิน ไม่มีส่วนใดในที่นี้เป็นคำแนะนำให้ใช้หรือหลีกเลี่ยงโปรโตคอลใด