บทความนี้อธิบายรายละเอียดตั้งแต่ระดับซอร์สโค้ดของคอมไพเลอร์ Solidity (0.8.13<=Solidity<0.8.17) ในกระบวนการคอมไพล์ เนื่องจากข้อบกพร่องของกลไกการปรับให้เหมาะสม Yul การดำเนินการกำหนดตัวแปรสถานะจึงถูกลบโดยไม่ตั้งใจ สอดคล้องกัน ข้อควรระวัง.
ชื่อระดับแรก
1. รายละเอียดช่องโหว่
เอกสารราชการเอกสารราชการ。
ในขั้นตอนการเพิ่มประสิทธิภาพ UnusedStoreEliminator ของกระบวนการคอมไพล์ คอมไพลเลอร์จะลบการดำเนินการเขียนสตอเรจที่ "ซ้ำซ้อน" แต่เนื่องจากข้อบกพร่องในการระบุ "ความซ้ำซ้อน" เมื่อบล็อกฟังก์ชัน Yul เรียกฟังก์ชันที่ผู้ใช้กำหนดโดยเฉพาะ (ฟังก์ชันมี สาขาภายในที่ไม่กระทบกระแสการดำเนินการของบล็อกการเรียกใช้) และมีการดำเนินการเขียนไปยังตัวแปรสถานะเดียวกันก่อนและหลังฟังก์ชันที่เรียกใช้ในบล็อกฟังก์ชัน Yul ซึ่งจะทำให้ฟังก์ชันที่ผู้ใช้กำหนดในบล็อกนั้น ถูกเรียกใช้โดยกลไกการเพิ่มประสิทธิภาพ Yul ก่อนหน้านี้การดำเนินการเขียนที่เก็บข้อมูลจะถูกลบอย่างถาวรจากระดับการคอมไพล์。
พิจารณารหัสต่อไปนี้:
contract Eocene {
uint public x;
function attack() public {
x = 1;
x = 2;
}
}
x=1 เห็นได้ชัดว่าซ้ำซ้อนสำหรับการดำเนินการทั้งหมดของ function attack() เมื่อ UnusedStoreEliminator ปรับให้เหมาะสม โดยธรรมชาติแล้ว รหัส Yul ที่ปรับให้เหมาะสมจะลบ x= 1 เพื่อลดการใช้ก๊าซของสัญญา
ถัดไป ให้พิจารณาแทรกการเรียกไปยังฟังก์ชันแบบกำหนดเองตรงกลาง:
contract Eocene {
uint public x;
function attack(uint i) public {
x = 1;
y(i);
x = 2;
}
function y(uint i) internal{
if (i > 0){
return;
}
assembly { return( 0, 0) }
}
}
เห็นได้ชัดว่า เนื่องจากการเรียกใช้ฟังก์ชัน y() เราจำเป็นต้องตัดสินว่าฟังก์ชัน y() จะส่งผลต่อการดำเนินการของฟังก์ชัน attack() หรือไม่ หากฟังก์ชัน y() สามารถทำให้โฟลว์การดำเนินการของฟังก์ชันทั้งหมดยุติลง ( โปรดทราบว่าไม่ใช่การย้อนกลับ ฟังก์ชัน Yul code return()) ดังนั้น x= 1 จึงไม่สามารถลบออกได้ ดังนั้นสำหรับสัญญาข้างต้น การมีอยู่ของชุดประกอบ {return(0, 0)} ในฟังก์ชัน y() สามารถนำไปสู่การยุติการโทรข้อความทั้งหมด x= 1 ไม่สามารถลบได้ตามธรรมชาติ
แต่ในคอมไพเลอร์ Solidity เนื่องจากปัญหาโค้ดลอจิก x= 1 ถูกลบโดยไม่ได้ตั้งใจระหว่างการคอมไพล์ ซึ่งทำให้โค้ดลอจิกเปลี่ยนไปอย่างถาวร
ผลการทดสอบการรวบรวมจริงมีดังนี้:
ช็อก! โค้ด Yul ที่มี x=1 ที่ไม่ควรปรับจะหายไป! หากคุณต้องการทราบว่าเกิดอะไรขึ้นต่อไป โปรดอ่านด้านล่าง
ใน UnusedStoreEliminator ของโค้ดคอมไพเลอร์ solidiry การติดตามตัวแปร SSA และการติดตามโฟลว์การควบคุมจะถูกใช้เพื่อระบุว่าการดำเนินการเขียนที่เก็บข้อมูลซ้ำซ้อนหรือไม่ เมื่อป้อนฟังก์ชันแบบกำหนดเอง หาก UnusedStoreEliminator พบ:
การดำเนินการเขียนหน่วยความจำหรือหน่วยเก็บข้อมูล: จัดเก็บหน่วยความจำและการดำเนินการเขียนหน่วยเก็บข้อมูลลงในตัวแปร m_store และตั้งค่าสถานะเริ่มต้นของการดำเนินการเป็น Undecided
การเรียกใช้ฟังก์ชัน: รับตำแหน่งการดำเนินการอ่านและเขียนหน่วยความจำหรือหน่วยเก็บข้อมูลของฟังก์ชัน และเปรียบเทียบกับการดำเนินการทั้งหมดในสถานะ Undecided ที่จัดเก็บไว้ในตัวแปร m_store:
1. หากเป็นการเขียนทับการดำเนินการจัดเก็บข้อมูลใน m_store ให้เปลี่ยนสถานะการดำเนินการที่เกี่ยวข้องใน m_store เป็น Unused
2. หากเป็นการอ่านการดำเนินการจัดเก็บใน m_store ให้เปลี่ยนสถานะการดำเนินการที่เกี่ยวข้องใน m_store ที่เกี่ยวข้องเป็น ใช้แล้ว
3. หากฟังก์ชันไม่มีสาขาใดๆ ที่สามารถเรียกใช้งานการเรียกข้อความต่อไปได้ ให้เปลี่ยนการดำเนินการเขียนหน่วยความจำทั้งหมดใน m_store เป็น Unused
1. ภายใต้เงื่อนไขการอุทธรณ์ ถ้าฟังก์ชันสามารถยุติโฟลว์การดำเนินการได้ ให้เปลี่ยนการดำเนินการเขียนที่เก็บข้อมูลซึ่งมีสถานะเป็น Undecided ใน m_store เป็นใช้แล้ว มิฉะนั้น ให้ทำเครื่องหมายเป็นยังไม่ได้ใช้
สิ้นสุดฟังก์ชัน: ลบการดำเนินการเขียนทั้งหมดที่ทำเครื่องหมายว่าไม่ได้ใช้
รหัสการเริ่มต้นสำหรับการเขียนไปยังหน่วยความจำหรือหน่วยเก็บข้อมูลมีดังนี้:
จะเห็นได้ว่าหน่วยความจำที่พบและการดำเนินการเขียนที่เก็บข้อมูลถูกจัดเก็บไว้ใน m_store
รหัสลอจิกการประมวลผลเมื่อพบการเรียกใช้ฟังก์ชันจะเป็นดังนี้:
ในหมู่พวกเขา operationFromFunctionCall() และ applyOperation() ใช้ตรรกะการประมวลผลของ 2.1 และ 2.2 ของการอุทธรณ์ คำสั่ง If ที่พิจารณาจากฟังก์ชัน canContinue และ canTerminate ด้านล่างใช้ตรรกะ 2.3
ควรสังเกตว่ามันเป็นข้อบกพร่องของการตัดสินของ If ด้านล่างที่นำไปสู่การมีอยู่ของช่องโหว่! ! !
operationFromFunctionCall() เพื่อรับการดำเนินการอ่านและเขียนหน่วยความจำหรือหน่วยเก็บข้อมูลทั้งหมดของฟังก์ชันนี้ ควรสังเกตว่า มีฟังก์ชันในตัวมากมายใน Yul เช่น sstore(), return() ที่นี่ คุณจะเห็นว่ามีตรรกะการประมวลผลที่แตกต่างกันสำหรับฟังก์ชันในตัวและฟังก์ชันที่ผู้ใช้กำหนด
ฟังก์ชัน applyOperation() เปรียบเทียบการดำเนินการอ่านและเขียนทั้งหมดที่ได้รับจาก operationFromFuncitonCall() เพื่อพิจารณาว่าข้อมูลที่จัดเก็บไว้ใน m_store ถูกอ่านหรือเขียนในการเรียกใช้ฟังก์ชันนี้ และแก้ไขสถานะการดำเนินการที่สอดคล้องกันใน m_store
พิจารณาการประมวลผลของตรรกะการเพิ่มประสิทธิภาพ UnusedStoreEliminator ที่กล่าวถึงข้างต้นบนฟังก์ชันการโจมตี () ของสัญญา Eocene:
เก็บ x= 1 ไว้ในตัวแปร m_store และตั้งค่าสถานะเป็น Undecided
1. พบการเรียกฟังก์ชัน y() รับการดำเนินการอ่านและเขียนทั้งหมดของการเรียกฟังก์ชัน y()
2. สำรวจตัวแปร m_store และพบว่าการดำเนินการอ่านและเขียนทั้งหมดที่เกิดจากการเรียก y() ไม่มีส่วนเกี่ยวข้องกับ x= 1 และสถานะของ x= 1 ยังไม่แน่ใจ
1. รับลอจิกโฟลว์ควบคุมของฟังก์ชัน y() เนื่องจากฟังก์ชัน y() มีแบรนช์ที่สามารถย้อนกลับได้ตามปกติ ดังนั้น canContinue จึงเป็น True และไม่เข้าสู่การตัดสิน If x= 1 สถานะยังไม่ตัดสินใจ! ! !
3. พบ x= 2 การดำเนินการจัดเก็บ:
1. สำรวจตัวแปร m_store และพบว่า x= 1 อยู่ในสถานะ Undecided การดำเนินการ x= 2 จะแทนที่ x= 1 และตั้งค่าสถานะของ x= 1 เป็น Unused
2. จัดเก็บการดำเนินการ x= 2 ไว้ใน m_store และสถานะเริ่มต้นยังไม่ได้กำหนด
4. สิ้นสุดฟังก์ชัน:
1. เปลี่ยนสถานะการดำเนินการของสถานะที่ยังไม่ได้ตัดสินใจใน m_stores ทั้งหมดเป็น ใช้แล้ว
2. ลบการดำเนินการทั้งหมดในสถานะที่ไม่ได้ใช้ใน m_store
เห็นได้ชัดว่าเมื่อมีการเรียกใช้ฟังก์ชัน หากฟังก์ชันที่เรียกใช้สามารถยุติการดำเนินการของข้อความได้ การดำเนินการเขียนทั้งหมดของสถานะ Undecided ก่อนฟังก์ชันที่เรียกใช้ควรเปลี่ยนเป็น Used แทนที่จะเหลือเป็น Undecided ส่งผลให้การดำเนินการเขียนก่อนหน้า ฟังก์ชันที่เรียกใช้ถูกลบโดยไม่ได้ตั้งใจ
มีอยู่
มีอยู่Solidityตัวอย่างของรหัสสัญญาที่จะไม่ได้รับผลกระทบภายใต้ตรรกะเดียวกัน อย่างไรก็ตาม โค้ดไม่ได้รับผลกระทบจากช่องโหว่นี้ ไม่ใช่เพราะมีความเป็นไปได้อื่น ๆ ในตรรกะการประมวลผลของ UnusedStoreEliminator แต่ในขั้นตอนการปรับให้เหมาะสม Yul ก่อน UnusedStoreEliminator มีกระบวนการเพิ่มประสิทธิภาพแบบ FullInliner ที่จะฝังฟังก์ชันที่เรียกว่ามีขนาดเล็กหรือเพียงอย่างเดียว เรียกใช้ฟังก์ชัน In the call ฟังก์ชันที่ผู้ใช้กำหนดในเงื่อนไขทริกเกอร์ช่องโหว่จะถูกหลีกเลี่ยง
contract Normal {
uint public x;
function f(bool a) public {
x = 1;
g(a);
x = 2;
}
function g(bool a) internal {
if (!a)
assembly { return( 0, 0) }
}
}
ผลการรวบรวมเป็นดังนี้:
ชื่อระดับแรก
2. โซลูชั่น
วิธีแก้ไขขั้นพื้นฐานที่สุดคือการคอมไพล์โดยไม่ใช้ solidity compiler ในช่วงที่ได้รับผลกระทบ หากคุณจำเป็นต้องใช้คอมไพเลอร์เวอร์ชันที่มีช่องโหว่ คุณสามารถพิจารณาลบขั้นตอนการเพิ่มประสิทธิภาพ UnusedStoreEliminator ระหว่างการคอมไพล์
หากคุณต้องการลดช่องโหว่จากระดับรหัสสัญญา โดยพิจารณาจากความซับซ้อนของขั้นตอนการเพิ่มประสิทธิภาพหลายขั้นตอนและความซับซ้อนของขั้นตอนการเรียกใช้ฟังก์ชันจริง โปรดหาเจ้าหน้าที่รักษาความปลอดภัยมืออาชีพเพื่อทำการตรวจสอบรหัสเพื่อช่วยค้นหาช่องโหว่ในสัญญาที่เกิดจากสิ่งนี้ คำถามเพื่อความปลอดภัยเกี่ยวกับช่องโหว่
